diff --git a/.gitignore b/.gitignore index eeb136eea4a..596af538b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ packages/amazonq/package.nls.json packages/amazonq/resources # Icons -packages/*/resources/icons/cloud9/generated/** packages/*/resources/fonts/aws-toolkit-icons.woff packages/*/resources/css/icons.css diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4dba3042e2..087d6e838ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -633,7 +633,7 @@ If you are contribuing visual assets from other open source repos, the source re ## Using new vscode APIs The minimum required vscode version specified in [package.json](https://github.com/aws/aws-toolkit-vscode/blob/07119655109bb06105a3f53bbcd86b812b32cdbe/package.json#L16) -is decided by the version of vscode running in Cloud9 and other vscode-compatible targets. +is decided by the version of vscode running in other supported vscode-compatible targets (e.g. web). But you can still use the latest vscode APIs, by checking the current running vscode version. For example, to use a vscode 1.64 API: diff --git a/cloud9-toolkit-install.sh b/cloud9-toolkit-install.sh deleted file mode 100644 index 8b85a56a63a..00000000000 --- a/cloud9-toolkit-install.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/env bash - -# By default, this script gets the latest VSIX from: -# https://github.com/aws/aws-toolkit-vscode/releases/ -# else the first argument must be a URL or file pointing to a toolkit VSIX or -# ZIP (containing a VSIX). -# -# USAGE: -# cloud9-toolkit-install.sh [URL|FILE] -# curl -LO https://raw.githubusercontent.com/aws/aws-toolkit-vscode/master/cloud9-toolkit-install.sh && bash cloud9-toolkit-install.sh -# EXAMPLES: -# cloud9-toolkit-install.sh https://github.com/aws/aws-toolkit-vscode/releases/download/v1.24.0/aws-toolkit-vscode-1.24.0.vsix -# cloud9-toolkit-install.sh toolkit.zip - -set -eu - -# Script currently depends on $HOME so running as root is not supported. -if [ "$(whoami)" = root ]; then - echo "cannot run as root" - exit 1 -fi - -_log() { - echo >&2 "$@" -} - -# Runs whatever is passed. -# -# On failure: -# - prints the command output -# - exits the script -_run() { - local out - if ! out="$("$@" 2>&1)"; then - _log "Command failed (output below): '${*}'" - echo "$out" | sed 's/^/ /' - _log "Command failed (output above): '${*}'" - exit 1 - fi -} - -# Gets the first existing directory, or exits if none are found. -_any_dir() { - for d in "$@"; do - if test -d "$d"; then - echo "$d" - return - fi - done - - _log "error: None of the expected dirs exist:" - for d in "$@"; do - _log " $d" - done - exit 1 -} - -_setglobals() { - # Example: - # https://github.com/aws/aws-toolkit-vscode/releases/tag/v1.24.0 - TOOLKIT_LATEST_RELEASE_URL="$(curl -Ls -o /dev/null -w '%{url_effective}' 'https://github.com/aws/aws-toolkit-vscode/releases/latest')" - # Example: - # 1.24.0 - TOOLKIT_LATEST_VERSION="$(echo "$TOOLKIT_LATEST_RELEASE_URL" | grep -oh '[0-9]\+\.[0-9]\+\.[0-9]\+$')" - # Example: - # https://github.com/aws/aws-toolkit-vscode/releases/download/v1.24.0/aws-toolkit-vscode-1.24.0.vsix - TOOLKIT_LATEST_ARTIFACT_URL="https://github.com/aws/aws-toolkit-vscode/releases/download/v${TOOLKIT_LATEST_VERSION}/aws-toolkit-vscode-${TOOLKIT_LATEST_VERSION}.vsix" - # URL or local filepath pointing to toolkit VSIX or ZIP (containing a VSIX). - TOOLKIT_FILE=${1:-} - TOOLKIT_INSTALL_PARENT="$(_any_dir "/projects/.c9/aws-toolkit-vscode" "$HOME/.c9/dependencies/aws-toolkit-vscode" "$HOME/environment/.c9/extensions" "$HOME/.c9/aws-toolkit-vscode")" - # Hash name is 128 chars long. - TOOLKIT_INSTALL_DIR="$(_any_dir "$(realpath ${TOOLKIT_INSTALL_PARENT}/????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????)" "$(realpath ${TOOLKIT_INSTALL_PARENT}/extension)")" - SCRIPT_WORKDIR="$HOME/toolkit" -} - -_main() { - ( - if test -f "$TOOLKIT_FILE"; then - # Ensure full path (before `cd` below). - TOOLKIT_FILE="$(readlink -f "$TOOLKIT_FILE")" - fi - - echo "Script will DELETE these directories:" - echo " ${TOOLKIT_INSTALL_DIR}" - echo " ${SCRIPT_WORKDIR}" - read -n1 -r -p "ENTER to continue, CTRL-c to cancel" - rm -rf "${TOOLKIT_INSTALL_DIR}.old" - mv "$TOOLKIT_INSTALL_DIR" "${TOOLKIT_INSTALL_DIR}.old" - rm -rf "$SCRIPT_WORKDIR" - - cd "$HOME/" - mkdir -p "$SCRIPT_WORKDIR" - mkdir -p "$TOOLKIT_INSTALL_PARENT" - cd "${SCRIPT_WORKDIR}" - - # Set default URL if no argument was provided. - if test -z "$TOOLKIT_FILE"; then - _log "Latest release:" - _log " URL : $TOOLKIT_LATEST_RELEASE_URL" - _log " version: $TOOLKIT_LATEST_VERSION" - _log " VSIX : $TOOLKIT_LATEST_ARTIFACT_URL" - TOOLKIT_FILE="$TOOLKIT_LATEST_ARTIFACT_URL" - fi - - TOOLKIT_FILE_EXTENSION="${TOOLKIT_FILE: -4}" - if test -f "$TOOLKIT_FILE"; then - _log "Local file (not URL): ${TOOLKIT_FILE}" - if [ "$TOOLKIT_FILE_EXTENSION" = ".zip" ] || [ "$TOOLKIT_FILE_EXTENSION" = ".ZIP" ]; then - _log 'File is a .zip file' - _run unzip -- "$TOOLKIT_FILE" - _run unzip -- *.vsix - else - _log 'File is not .zip file, assuming .vsix' - _run unzip -- "$TOOLKIT_FILE" - fi - else - _log "URL: ${TOOLKIT_FILE}" - _log 'Deleting toolkit.zip' - rm -rf toolkit.zip - _log 'Downloading...' - curl -o toolkit.zip -L "$TOOLKIT_FILE" - if [ "$TOOLKIT_FILE_EXTENSION" = ".zip" ] || [ "$TOOLKIT_FILE_EXTENSION" = ".ZIP" ]; then - _log 'File is a .zip file' - _run unzip -- toolkit.zip - _run unzip -- *.vsix - else - _log 'File is not .zip file, assuming .vsix' - _run unzip -- toolkit.zip - fi - fi - - mv extension "$TOOLKIT_INSTALL_DIR" - _log "Toolkit installed to: $TOOLKIT_INSTALL_DIR" - _log "Refresh Cloud9 to load it" - ) -} - -_setglobals "$@" -_main diff --git a/docs/CODE_GUIDELINES.md b/docs/CODE_GUIDELINES.md index 50471b3d286..90153905b12 100644 --- a/docs/CODE_GUIDELINES.md +++ b/docs/CODE_GUIDELINES.md @@ -81,13 +81,10 @@ that is a net cost. - Telemetry: "active" vs "passive" - Active (`passive:false`) metrics are those intended to appear in DAU count. - Icons: - - Where possible, use IDE-specific standard icons (e.g. standard VSCode - standard icons in VSCode, and Cloud9 standard icons in Cloud9). The typical - (maintainable) way to do this is to use _named_ icons (what VSCode calls + - Where possible, use standard VSCode icons. The typical (maintainable) + way to do this is to use _named_ icons (what VSCode calls [codicons](https://microsoft.github.io/vscode-codicons/)) as opposed to icons shipped with the Toolkit build and referenced by _path_. - - For cases where icons must be statically defined (package.json), if Cloud9 - does not support the VSCode standard icon, use the Cloud9 icon. - Changelog guidelines - Prefer active voice: "You can do X" instead of "X can be done" - Avoid unnecessary use of `lodash` (which we may remove in the future). @@ -240,11 +237,11 @@ _See also [arch_develop.md](./arch_develop.md#exceptions)._ - PREFER: ```ts - things.filter(o => o.isFoo) + things.filter((o) => o.isFoo) ``` - INSTEAD OF: ```ts - things.filter(thing => thing.isFoo) + things.filter((thing) => thing.isFoo) ``` ## User settings diff --git a/docs/arch_runtime.md b/docs/arch_runtime.md index d2c5902c333..e1cbfd01661 100644 --- a/docs/arch_runtime.md +++ b/docs/arch_runtime.md @@ -31,8 +31,6 @@ If you must define a new key (is it _really_ necessary?), follow these guideline These keys are currently set by the core/ package, but many of them may eventually be migrated to toolkit/ or amazonq/ if appropriate. -- `isCloud9`: This is hardcoded by Cloud9 itself, not the Toolkit. - - Cloud9 _does not support setContext_. So this is the only usable key there. - `aws.codecatalyst.connected`: CodeCatalyst connection is active. - `aws.codewhisperer.connected`: CodeWhisperer connection is active. - `aws.codewhisperer.connectionExpired`: CodeWhisperer connection is active, but the connection is expired. diff --git a/docs/icons.md b/docs/icons.md index d5a2144085f..46e252d5dac 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -4,7 +4,6 @@ All icons that are used in the extensions can be found in `resources/icons`. A [build script](../scripts/generateIcons.ts) generates extension artifacts in [core/](../packages/core/): -- `resources/icons/cloud9/generated` - `resources/fonts/aws-toolkit-icons.woff` - `resources/css/icons.css` - `contributes.icons` in [amazonq package.json](../packages/amazonq/package.json) and [toolkit package.json](../packages/toolkit/package.json) @@ -31,7 +30,7 @@ If your desired icon does not work well as a font, see [Theme Overrides](#theme- ## Identifiers -Icons (except those in `cloud9`) can be referenced within the Toolkit by concatenating the icon path with hyphens, omitting the 'theme' if applicable. +Icons can be referenced within the Toolkit by concatenating the icon path with hyphens, omitting the 'theme' if applicable. Examples: @@ -50,11 +49,6 @@ For example, if I wanted to use a special App Runner service icon, then I need t - `resources/icons/aws/dark/apprunner-service.svg` - `resources/icons/aws/light/apprunner-service.svg` -A similar format is used for overriding icons only on Cloud9: - -- `resources/icons/cloud9/dark/aws-apprunner-service.svg` -- `resources/icons/cloud9/light/aws-apprunner-service.svg` - These icons will **not** be usuable as Codicons or as font icons. ## Third Party diff --git a/package-lock.json b/package-lock.json index 20134337793..1427e2f0716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "vscode-nls-dev": "^4.0.4" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.290", + "@aws-toolkits/telemetry": "^1.0.293", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", @@ -6047,11 +6047,10 @@ } }, "node_modules/@aws-toolkits/telemetry": { - "version": "1.0.290", - "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.290.tgz", - "integrity": "sha512-v6Eq0Hy4nyHkiEKsBuPBU0w9CNf8nPaeP/BCRrY2IGMudD/U1AdM2KLoehJGgRXkhDTzOuBdpVg27S/iCjkNQQ==", + "version": "1.0.294", + "resolved": "https://registry.npmjs.org/@aws-toolkits/telemetry/-/telemetry-1.0.294.tgz", + "integrity": "sha512-Hy/yj93pFuHhKCqAA9FgNjdJHRi4huUnyl13dZLzzICDlFVl/AHlm9iWmm9LR22KOuXUyu3uX40VtXLdExIHqw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "ajv": "^6.12.6", "cross-spawn": "^7.0.6", @@ -6084,10 +6083,11 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.21.4.tgz", - "integrity": "sha512-sYeQHJ8yEQQQsre1soXQFebbqZFcXerIxJ/d9kg/YzZUauCirW7v/0f/kHs9y7xYkYGa8y3exV6b6e4+juO1DQ==", + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.21.5.tgz", + "integrity": "sha512-Ge7/XADBx/Phm9k2pVgjtYRoB5UOsNcTwZ0VOsWOc2JBGblEIasiT4pNNfHGKgMkLf79AKYUKRSH5IAuQRKpaQ==", "hasInstallScript": true, + "license": "Apache License 2.0", "dependencies": { "escape-html": "^1.0.3", "highlight.js": "^11.11.0", @@ -10481,8 +10481,10 @@ "link": true }, "node_modules/aws-sdk": { - "version": "2.1384.0", - "license": "Apache-2.0", + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -10493,7 +10495,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" }, "engines": { "node": ">= 10.0.0" @@ -10506,17 +10508,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/aws-sdk/node_modules/xml2js": { - "version": "0.5.0", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/aws-ssm-document-language-service": { "version": "1.0.0", "license": "Apache-2.0", @@ -20821,8 +20812,9 @@ } }, "node_modules/xml2js": { - "version": "0.6.1", - "license": "MIT", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -21127,7 +21119,7 @@ }, "packages/amazonq": { "name": "amazon-q-vscode", - "version": "1.43.0-SNAPSHOT", + "version": "1.45.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -21157,7 +21149,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "^4.21.4", + "@aws/mynah-ui": "^4.21.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^2.3.1", @@ -21170,7 +21162,7 @@ "adm-zip": "^0.5.10", "amazon-states-language-service": "^1.13.0", "async-lock": "^1.4.0", - "aws-sdk": "^2.1384.0", + "aws-sdk": "^2.1692.0", "aws-ssm-document-language-service": "^1.0.0", "bytes": "^3.1.2", "cross-fetch": "^4.0.0", @@ -21287,7 +21279,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.42.0-SNAPSHOT", + "version": "3.44.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/package.json b/package.json index 2b9e770b63a..e14641a45c4 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "mergeReports": "ts-node ./scripts/mergeReports.ts" }, "devDependencies": { - "@aws-toolkits/telemetry": "^1.0.290", + "@aws-toolkits/telemetry": "^1.0.293", "@playwright/browser-chromium": "^1.43.1", "@stylistic/eslint-plugin": "^2.11.0", "@types/he": "^1.2.3", diff --git a/packages/amazonq/.changes/1.43.0.json b/packages/amazonq/.changes/1.43.0.json new file mode 100644 index 00000000000..a4f2376f2e6 --- /dev/null +++ b/packages/amazonq/.changes/1.43.0.json @@ -0,0 +1,42 @@ +{ + "date": "2025-01-15", + "version": "1.43.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Auth: Valid StartURL not accepted at login" + }, + { + "type": "Bug Fix", + "description": "Fix inline completion supplementalContext length exceeding maximum in certain cases" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /test: Unit test generation completed message shows after accept/reject action" + }, + { + "type": "Bug Fix", + "description": "/test: for unsupported languages was sometimes unreliable" + }, + { + "type": "Bug Fix", + "description": "User-selected customizations are sometimes not being persisted." + }, + { + "type": "Bug Fix", + "description": "Amazon q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining" + }, + { + "type": "Feature", + "description": "Adds capability to send new context commands to AB groups" + }, + { + "type": "Feature", + "description": "feat(amazonq): Add error message for updated README too large" + }, + { + "type": "Feature", + "description": "Enhance Q inline completion context fetching for better suggestion quality" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/1.44.0.json b/packages/amazonq/.changes/1.44.0.json new file mode 100644 index 00000000000..7593e9a7af3 --- /dev/null +++ b/packages/amazonq/.changes/1.44.0.json @@ -0,0 +1,50 @@ +{ + "date": "2025-01-23", + "version": "1.44.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q: word duplication when pressing tab on context selector fixed" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /doc: Prevent users from requesting changes if no iterations remain" + }, + { + "type": "Bug Fix", + "description": "`/test`: view diffs by clicking files in the file tree, aligning the behavior with the 'View Diff' button." + }, + { + "type": "Bug Fix", + "description": "/review: Improved error handling for code fix operations" + }, + { + "type": "Bug Fix", + "description": "Amazon Q: cursor no longer jumps after navigating prompt history" + }, + { + "type": "Bug Fix", + "description": "Improve the text description of workspace index settings" + }, + { + "type": "Bug Fix", + "description": "Notifications: 'Dismiss' command visible in command palette." + }, + { + "type": "Bug Fix", + "description": "/transform: replace icons in Transformation Hub with text" + }, + { + "type": "Bug Fix", + "description": "Amazon Q /doc: Ask for user prompt if error occurs while updating documentation" + }, + { + "type": "Feature", + "description": "Amazon Q: increase chat current active file context char limit to 40k" + }, + { + "type": "Feature", + "description": "/review: Code issues can be grouped by file location or severity" + } + ] +} \ No newline at end of file diff --git a/packages/amazonq/.changes/next-release/Bug Fix-71c6bbc1-67ae-4318-a7f0-c594e097ebc4.json b/packages/amazonq/.changes/next-release/Bug Fix-71c6bbc1-67ae-4318-a7f0-c594e097ebc4.json deleted file mode 100644 index e0c15b7f2dc..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-71c6bbc1-67ae-4318-a7f0-c594e097ebc4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Auth: Valid StartURL not accepted at login" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-72b26a2d-3647-4d07-b0ef-92238e3f0050.json b/packages/amazonq/.changes/next-release/Bug Fix-72b26a2d-3647-4d07-b0ef-92238e3f0050.json deleted file mode 100644 index 900d0953d11..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-72b26a2d-3647-4d07-b0ef-92238e3f0050.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Fix inline completion supplementalContext length exceeding maximum in certain cases" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-b6d52b75-69e6-47bb-939b-5ddede03f977.json b/packages/amazonq/.changes/next-release/Bug Fix-b6d52b75-69e6-47bb-939b-5ddede03f977.json deleted file mode 100644 index 8772b9bf10d..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-b6d52b75-69e6-47bb-939b-5ddede03f977.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q /test: Unit test generation completed message shows after accept/reject action" -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json b/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json deleted file mode 100644 index 3f2f6330b2f..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-d8c02c38-bdc4-492a-bf8c-6c35e35087be.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "User-selected customizations are sometimes not being persisted." -} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-ef93f909-3aa5-4e62-a4fc-850376161d24.json b/packages/amazonq/.changes/next-release/Bug Fix-ef93f909-3aa5-4e62-a4fc-850376161d24.json deleted file mode 100644 index 82b2eb42199..00000000000 --- a/packages/amazonq/.changes/next-release/Bug Fix-ef93f909-3aa5-4e62-a4fc-850376161d24.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining" -} diff --git a/packages/amazonq/.changes/next-release/Feature-37eb706c-57a1-4751-888d-220b2f68ee4d.json b/packages/amazonq/.changes/next-release/Feature-37eb706c-57a1-4751-888d-220b2f68ee4d.json deleted file mode 100644 index b055e2175c3..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-37eb706c-57a1-4751-888d-220b2f68ee4d.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Adds capability to send new context commands to AB groups" -} diff --git a/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json b/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json deleted file mode 100644 index 7e1d6e86caa..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-8a43c3c3-9ecb-44b6-bb0c-34d2065aee26.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "feat(amazonq): Add error message for updated README too large" -} diff --git a/packages/amazonq/.changes/next-release/Feature-d0329e3d-65bd-4987-b87c-ee11b86de399.json b/packages/amazonq/.changes/next-release/Feature-d0329e3d-65bd-4987-b87c-ee11b86de399.json deleted file mode 100644 index c8fd74b134e..00000000000 --- a/packages/amazonq/.changes/next-release/Feature-d0329e3d-65bd-4987-b87c-ee11b86de399.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Enhance Q inline completion context fetching for better suggestion quality" -} diff --git a/packages/amazonq/CHANGELOG.md b/packages/amazonq/CHANGELOG.md index 9cfaa7fe04f..668eb5726ac 100644 --- a/packages/amazonq/CHANGELOG.md +++ b/packages/amazonq/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.44.0 2025-01-23 + +- **Bug Fix** Amazon Q: word duplication when pressing tab on context selector fixed +- **Bug Fix** Amazon Q /doc: Prevent users from requesting changes if no iterations remain +- **Bug Fix** `/test`: view diffs by clicking files in the file tree, aligning the behavior with the 'View Diff' button. +- **Bug Fix** /review: Improved error handling for code fix operations +- **Bug Fix** Amazon Q: cursor no longer jumps after navigating prompt history +- **Bug Fix** Improve the text description of workspace index settings +- **Bug Fix** Notifications: 'Dismiss' command visible in command palette. +- **Bug Fix** /transform: replace icons in Transformation Hub with text +- **Bug Fix** Amazon Q /doc: Ask for user prompt if error occurs while updating documentation +- **Feature** Amazon Q: increase chat current active file context char limit to 40k +- **Feature** /review: Code issues can be grouped by file location or severity + +## 1.43.0 2025-01-15 + +- **Bug Fix** Auth: Valid StartURL not accepted at login +- **Bug Fix** Fix inline completion supplementalContext length exceeding maximum in certain cases +- **Bug Fix** Amazon Q /test: Unit test generation completed message shows after accept/reject action +- **Bug Fix** /test: for unsupported languages was sometimes unreliable +- **Bug Fix** User-selected customizations are sometimes not being persisted. +- **Bug Fix** Amazon q /dev: Remove hard-coded limits and instead rely server-side data to communicate number of code generations remaining +- **Feature** Adds capability to send new context commands to AB groups +- **Feature** feat(amazonq): Add error message for updated README too large +- **Feature** Enhance Q inline completion context fetching for better suggestion quality + ## 1.42.0 2025-01-09 - **Bug Fix** Amazon Q /doc: Improve button text phrasing diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 8981ba83502..f1b30dc28fd 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -2,7 +2,7 @@ "name": "amazon-q-vscode", "displayName": "Amazon Q", "description": "The most capable generative AI-powered assistant for building, operating, and transforming software, with advanced capabilities for managing data and AI", - "version": "1.43.0-SNAPSHOT", + "version": "1.45.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -198,7 +198,7 @@ { "id": "aws.amazonq.notifications", "name": "%AWS.notifications.title%", - "when": "!isCloud9 && !aws.isSageMaker && aws.amazonq.notifications.show" + "when": "!(isCloud9 || aws.isSageMaker) && aws.amazonq.notifications.show" }, { "type": "webview", @@ -314,16 +314,14 @@ "group": "navigation@3" }, { - "command": "aws.amazonq.transformationHub.summary.reveal", - "when": "view == aws.amazonq.transformationHub" - }, - { - "command": "aws.amazonq.transformationHub.reviewChanges.reveal", - "when": "view == aws.amazonq.transformationHub" + "command": "aws.amazonq.showTransformationPlanInHub", + "when": "view == aws.amazonq.transformationHub", + "group": "navigation@4" }, { - "command": "aws.amazonq.showTransformationPlanInHub", - "when": "view == aws.amazonq.transformationHub" + "command": "aws.amazonq.transformationHub.summary.reveal", + "when": "view == aws.amazonq.transformationHub", + "group": "navigation@5" }, { "command": "aws.amazonq.transformationHub.reviewChanges.acceptChanges", @@ -365,10 +363,15 @@ "when": "view == aws.AmazonQChatView || view == aws.amazonq.AmazonCommonAuth", "group": "y_toolkitMeta@2" }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "when": "view == aws.amazonq.SecurityIssuesTree", + "group": "navigation@1" + }, { "command": "aws.amazonq.security.showFilters", "when": "view == aws.amazonq.SecurityIssuesTree", - "group": "navigation" + "group": "navigation@2" } ], "view/item/context": [ @@ -493,7 +496,7 @@ "command": "_aws.amazonq.notifications.dismiss", "title": "%AWS.generic.dismiss%", "category": "%AWS.amazonq.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", + "enablement": "view == aws.amazonq.notifications", "icon": "$(remove-close)" }, { @@ -563,11 +566,6 @@ "title": "%AWS.command.q.transform.showChangeSummary%", "enablement": "gumby.isSummaryAvailable" }, - { - "command": "aws.amazonq.transformationHub.reviewChanges.reveal", - "title": "%AWS.command.q.transform.showChanges%", - "enablement": "gumby.reviewState == InReview" - }, { "command": "aws.amazonq.showTransformationPlanInHub", "title": "%AWS.command.q.transform.showTransformationPlan%", @@ -638,19 +636,16 @@ }, { "command": "aws.amazonq.stopTransformationInHub", - "title": "Stop Transformation", - "icon": "$(stop)", + "title": "%AWS.command.q.transform.stopJobInHub%", "enablement": "gumby.isStopButtonAvailable" }, { "command": "aws.amazonq.showPlanProgressInHub", - "title": "Show Transformation Status", - "icon": "$(checklist)" + "title": "%AWS.command.q.transform.viewJobProgress%" }, { "command": "aws.amazonq.showHistoryInHub", - "title": "Show Job Status", - "icon": "$(history)" + "title": "%AWS.command.q.transform.viewJobStatus%" }, { "command": "aws.amazonq.selectCustomization", @@ -724,6 +719,12 @@ { "command": "aws.amazonq.security.showFilters", "title": "%AWS.command.amazonq.filterIssues%", + "icon": "$(filter)", + "enablement": "view == aws.amazonq.SecurityIssuesTree" + }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "title": "%AWS.command.amazonq.groupIssues%", "icon": "$(list-filter)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 4ffa6e1e1de..7ace8d0095e 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -13,6 +13,7 @@ import { computeDecorations } from '../decorations/computeDecorations' import { CodelensProvider } from '../codeLenses/codeLenseProvider' import { PromptMessage, ReferenceLogController } from 'aws-core-vscode/codewhispererChat' import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' +import { UserWrittenCodeTracker } from 'aws-core-vscode/codewhisperer' import { codicon, getIcon, @@ -84,6 +85,7 @@ export class InlineChatController { await this.updateTaskAndLenses(task) this.referenceLogController.addReferenceLog(task.codeReferences, task.replacement ? task.replacement : '') await this.reset() + UserWrittenCodeTracker.instance.onQFinishesEdits() } public async rejectAllChanges(task = this.task, userInvoked: boolean): Promise { @@ -199,7 +201,7 @@ export class InlineChatController { getLogger().info('inlineQuickPick query is empty') return } - + UserWrittenCodeTracker.instance.onQStartsMakingEdits() this.userQuery = query await textDocumentUtil.addEofNewline(editor) this.task = await this.createTask(query, editor.document, editor.selection) diff --git a/packages/amazonq/test/e2e/amazonq/doc.test.ts b/packages/amazonq/test/e2e/amazonq/doc.test.ts index ad6b3df914c..343d228c261 100644 --- a/packages/amazonq/test/e2e/amazonq/doc.test.ts +++ b/packages/amazonq/test/e2e/amazonq/doc.test.ts @@ -146,5 +146,33 @@ describe('Amazon Q Doc', async function () { FollowUpTypes.RejectChanges, ]) }) + + it('Handle unrelated prompt error', async () => { + await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) + + tab.clickButton(FollowUpTypes.UpdateDocumentation) + + await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) + + tab.clickButton(FollowUpTypes.EditDocumentation) + + await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) + + tab.clickButton(FollowUpTypes.ProceedFolderSelection) + + tab.addChatMessage({ prompt: 'tell me about the weather' }) + + await tab.waitForEvent(() => + tab.getChatItems().some(({ body }) => body?.startsWith(i18n('AWS.amazonq.doc.error.promptUnrelated'))) + ) + + await tab.waitForEvent(() => { + const store = tab.getStore() + return ( + !store.promptInputDisabledState && + store.promptInputPlaceholder === i18n('AWS.amazonq.doc.placeholder.editReadme') + ) + }) + }) }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts index ae7114a22c8..7b0888521f4 100644 --- a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts @@ -4,7 +4,12 @@ */ import assert from 'assert' import sinon from 'sinon' -import { SecurityIssueFilters, SecurityTreeViewFilterState } from 'aws-core-vscode/codewhisperer' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + SecurityIssueFilters, + SecurityTreeViewFilterState, +} from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' describe('model', function () { @@ -70,4 +75,100 @@ describe('model', function () { assert.deepStrictEqual(hiddenSeverities, ['High', 'Low']) }) }) + + describe('CodeIssueGroupingStrategyState', function () { + let sandbox: sinon.SinonSandbox + let state: CodeIssueGroupingStrategyState + + beforeEach(function () { + sandbox = sinon.createSandbox() + state = CodeIssueGroupingStrategyState.instance + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('instance', function () { + it('should return the same instance when called multiple times', function () { + const instance1 = CodeIssueGroupingStrategyState.instance + const instance2 = CodeIssueGroupingStrategyState.instance + assert.strictEqual(instance1, instance2) + }) + }) + + describe('getState', function () { + it('should return fallback when no state is stored', function () { + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + + it('should return stored state when valid', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + await state.setState(validStrategy) + + const result = state.getState() + + assert.equal(result, validStrategy) + }) + + it('should return fallback when stored state is invalid', async function () { + const invalidStrategy = 'invalid' + await state.setState(invalidStrategy) + + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('setState', function () { + it('should update state and fire change event for valid strategy', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(validStrategy) + + sinon.assert.calledWith(eventSpy, validStrategy) + }) + + it('should use fallback and fire change event for invalid strategy', async function () { + const invalidStrategy = 'invalid' + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(invalidStrategy) + + sinon.assert.calledWith(eventSpy, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('reset', function () { + it('should set state to fallback value', async function () { + const setStateStub = sandbox.stub(state, 'setState').resolves() + + await state.reset() + + sinon.assert.calledWith(setStateStub, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('onDidChangeState', function () { + it('should allow subscribing to state changes', async function () { + const listener = sandbox.spy() + const disposable = state.onDidChangeState(listener) + + await state.setState(CodeIssueGroupingStrategy.Severity) + + sinon.assert.calledWith(listener, CodeIssueGroupingStrategy.Severity) + disposable.dispose() + }) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts index bd7c3aab8de..4d973735c9f 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts @@ -10,17 +10,24 @@ import { SecurityTreeViewFilterState, SecurityIssueProvider, SeverityItem, + CodeIssueGroupingStrategyState, + CodeIssueGroupingStrategy, } from 'aws-core-vscode/codewhisperer' import { createCodeScanIssue } from 'aws-core-vscode/test' import assert from 'assert' import sinon from 'sinon' +import path from 'path' describe('SecurityIssueTreeViewProvider', function () { - let securityIssueProvider: SecurityIssueProvider let securityIssueTreeViewProvider: SecurityIssueTreeViewProvider beforeEach(function () { - securityIssueProvider = SecurityIssueProvider.instance + SecurityIssueProvider.instance.issues = [ + { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + ] securityIssueTreeViewProvider = new SecurityIssueTreeViewProvider() }) @@ -44,13 +51,6 @@ describe('SecurityIssueTreeViewProvider', function () { describe('getChildren', function () { it('should return sorted list of severities if element is undefined', function () { - securityIssueProvider.issues = [ - { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - ] - const element = undefined const result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[] assert.strictEqual(result.length, 5) @@ -102,5 +102,55 @@ describe('SecurityIssueTreeViewProvider', function () { const result = securityIssueTreeViewProvider.getChildren(element) as IssueItem[] assert.strictEqual(result.length, 0) }) + + it('should return severity-grouped items when grouping strategy is Severity', function () { + sinon.stub(CodeIssueGroupingStrategyState.instance, 'getState').returns(CodeIssueGroupingStrategy.Severity) + + const severityItems = securityIssueTreeViewProvider.getChildren() as SeverityItem[] + for (const [index, [severity, expectedIssueCount]] of [ + ['Critical', 0], + ['High', 8], + ['Medium', 0], + ['Low', 0], + ['Info', 0], + ].entries()) { + const currentSeverityItem = severityItems[index] + assert.strictEqual(currentSeverityItem.label, severity) + assert.strictEqual(currentSeverityItem.issues.length, expectedIssueCount) + + const issueItems = securityIssueTreeViewProvider.getChildren(currentSeverityItem) as IssueItem[] + assert.ok(issueItems.every((item) => item.iconPath === undefined)) + assert.ok( + issueItems.every((item) => item.description?.toString().startsWith(path.basename(item.filePath))) + ) + } + }) + + it('should return file-grouped items when grouping strategy is FileLocation', function () { + sinon + .stub(CodeIssueGroupingStrategyState.instance, 'getState') + .returns(CodeIssueGroupingStrategy.FileLocation) + + const result = securityIssueTreeViewProvider.getChildren() as FileItem[] + for (const [index, [fileName, expectedIssueCount]] of [ + ['a', 2], + ['b', 2], + ['c', 2], + ['d', 2], + ].entries()) { + const currentFileItem = result[index] + assert.strictEqual(currentFileItem.label, fileName) + assert.strictEqual(currentFileItem.issues.length, expectedIssueCount) + assert.strictEqual(currentFileItem.description, 'file/path') + + const issueItems = securityIssueTreeViewProvider.getChildren(currentFileItem) as IssueItem[] + assert.ok( + issueItems.every((item) => + item.iconPath?.toString().includes(`${item.issue.severity.toLowerCase()}.svg`) + ) + ) + assert.ok(issueItems.every((item) => item.description?.toString().startsWith('[Ln '))) + } + }) }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts index b0086b2a205..e297dfb82b3 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityScanHandler.test.ts @@ -13,7 +13,10 @@ import { mapToAggregatedList, DefaultCodeWhispererClient, ListCodeScanFindingsResponse, + pollScanJobStatus, + SecurityScanTimedOutError, } from 'aws-core-vscode/codewhisperer' +import { timeoutUtils } from 'aws-core-vscode/shared' import assert from 'assert' import sinon from 'sinon' import * as vscode from 'vscode' @@ -50,7 +53,7 @@ const buildMockListCodeScanFindingsResponse = ( ): Awaited>> => ({ $response: { hasNextPage: () => false, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null data: undefined, error: undefined, requestId: '', @@ -239,4 +242,57 @@ describe('securityScanHandler', function () { assert.strictEqual(codeScanIssueMap.get('file1.ts')?.length, 1) }) }) + + describe('pollScanJobStatus', function () { + let mockClient: Stub + let clock: sinon.SinonFakeTimers + const mockJobId = 'test-job-id' + const mockStartTime = Date.now() + + beforeEach(function () { + mockClient = stub(DefaultCodeWhispererClient) + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }) + sinon.stub(timeoutUtils, 'sleep').resolves() + }) + + afterEach(function () { + sinon.restore() + clock.restore() + }) + + it('should return status when scan completes successfully', async function () { + mockClient.getCodeScan + .onFirstCall() + .resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + .onSecondCall() + .resolves({ status: 'Completed', $response: { requestId: 'req2' } }) + + const result = await pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.FILE_AUTO, mockStartTime) + assert.strictEqual(result, 'Completed') + }) + + it('should throw SecurityScanTimedOutError when polling exceeds timeout for express scans', async function () { + mockClient.getCodeScan.resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + + const pollPromise = pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.FILE_AUTO, mockStartTime) + + const expectedTimeoutMs = 60_000 + clock.tick(expectedTimeoutMs + 1000) + + await assert.rejects(() => pollPromise, SecurityScanTimedOutError) + }) + + it('should throw SecurityScanTimedOutError when polling exceeds timeout for standard scans', async function () { + mockClient.getCodeScan.resolves({ status: 'Pending', $response: { requestId: 'req1' } }) + + const pollPromise = pollScanJobStatus(mockClient, mockJobId, CodeAnalysisScope.PROJECT, mockStartTime) + + const expectedTimeoutMs = 600_000 + clock.tick(expectedTimeoutMs + 1000) + + await assert.rejects(() => pollPromise, SecurityScanTimedOutError) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts b/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts new file mode 100644 index 00000000000..1d9b878133f --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/tracker/userWrittenCodeTracker.test.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { UserWrittenCodeTracker, TelemetryHelper, AuthUtil } from 'aws-core-vscode/codewhisperer' +import { createMockDocument, resetCodeWhispererGlobalVariables } from 'aws-core-vscode/test' + +describe('userWrittenCodeTracker', function () { + describe('isActive()', function () { + afterEach(async function () { + await resetCodeWhispererGlobalVariables() + UserWrittenCodeTracker.instance.reset() + sinon.restore() + }) + + it('inactive case: telemetryEnable = true, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false) + }) + + it('inactive case: telemetryEnabled = false, isConnected = false', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(false) + sinon.stub(AuthUtil.instance, 'isConnected').returns(false) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), false) + }) + + it('active case: telemetryEnabled = true, isConnected = true', function () { + sinon.stub(TelemetryHelper.instance, 'isTelemetryEnabled').returns(true) + sinon.stub(AuthUtil.instance, 'isConnected').returns(true) + assert.strictEqual(UserWrittenCodeTracker.instance.isActive(), true) + }) + }) + + describe('onDocumentChange', function () { + let tracker: UserWrittenCodeTracker | undefined + + beforeEach(async function () { + await resetCodeWhispererGlobalVariables() + tracker = UserWrittenCodeTracker.instance + if (tracker) { + sinon.stub(tracker, 'isActive').returns(true) + } + }) + + afterEach(function () { + sinon.restore() + UserWrittenCodeTracker.instance.reset() + }) + + it('Should skip when content change size is more than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 600), + rangeOffset: 0, + rangeLength: 600, + text: 'def twoSum(nums, target):\nfor '.repeat(20), + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 0) + assert.strictEqual(tracker.getUserWrittenLines('python'), 0) + }) + + it('Should not skip when content change size is less than 50', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 49), + rangeOffset: 0, + rangeLength: 49, + text: 'a = 123'.repeat(7), + }, + ], + }) + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument('', 'test.java', 'java'), + contentChanges: [ + { + range: new vscode.Range(0, 0, 1, 3), + rangeOffset: 0, + rangeLength: 11, + text: 'a = 123\nbcd', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 49) + assert.strictEqual(tracker.getUserWrittenLines('python'), 0) + assert.strictEqual(tracker.getUserWrittenCharacters('java'), 11) + assert.strictEqual(tracker.getUserWrittenLines('java'), 1) + assert.strictEqual(tracker.getUserWrittenLines('cpp'), 0) + }) + + it('Should skip when Q is editing', function () { + if (!tracker) { + assert.fail() + } + tracker.onQFeatureInvoked() + tracker.onQStartsMakingEdits() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 30), + rangeOffset: 0, + rangeLength: 30, + text: 'def twoSum(nums, target):\nfor', + }, + ], + }) + tracker.onQFinishesEdits() + tracker.onTextDocumentChange({ + reason: undefined, + document: createMockDocument(), + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 2), + rangeOffset: 0, + rangeLength: 2, + text: '\na', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + assert.strictEqual(tracker.getUserWrittenLines('python'), 1) + }) + + it('Should not reduce tokens when delete', function () { + if (!tracker) { + assert.fail() + } + const doc = createMockDocument('import math', 'test.py', 'python') + + tracker.onQFeatureInvoked() + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'a', + }, + ], + }) + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 0, + rangeLength: 0, + text: 'b', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + tracker.onTextDocumentChange({ + reason: undefined, + document: doc, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 1), + rangeOffset: 1, + rangeLength: 1, + text: '', + }, + ], + }) + assert.strictEqual(tracker.getUserWrittenCharacters('python'), 2) + }) + }) +}) diff --git a/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts new file mode 100644 index 00000000000..9c5e00cd6f7 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts @@ -0,0 +1,45 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { createQuickPickPrompterTester, QuickPickPrompterTester } from 'aws-core-vscode/test' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + createCodeIssueGroupingStrategyPrompter, +} from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import assert from 'assert' +import vscode from 'vscode' + +const severity = { data: CodeIssueGroupingStrategy.Severity, label: 'Severity' } +const fileLocation = { data: CodeIssueGroupingStrategy.FileLocation, label: 'File Location' } + +describe('createCodeIssueGroupingStrategyPrompter', function () { + let tester: QuickPickPrompterTester + + beforeEach(function () { + tester = createQuickPickPrompterTester(createCodeIssueGroupingStrategyPrompter()) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should list grouping strategies', async function () { + tester.assertItems([severity, fileLocation]) + tester.hide() + await tester.result() + }) + + it('should update state on selection', async function () { + const originalState = CodeIssueGroupingStrategyState.instance.getState() + assert.equal(originalState, CodeIssueGroupingStrategy.Severity) + + tester.selectItems(fileLocation) + tester.addCallback(() => vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem')) + + await tester.result() + assert.equal(CodeIssueGroupingStrategyState.instance.getState(), fileLocation.data) + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index 5f5e59bfa13..7eca4863a77 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -508,7 +508,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "^4.21.4", + "@aws/mynah-ui": "^4.21.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/middleware-retry": "^2.3.1", @@ -521,7 +521,7 @@ "adm-zip": "^0.5.10", "amazon-states-language-service": "^1.13.0", "async-lock": "^1.4.0", - "aws-sdk": "^2.1384.0", + "aws-sdk": "^2.1692.0", "aws-ssm-document-language-service": "^1.0.0", "bytes": "^3.1.2", "cross-fetch": "^4.0.0", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index e3176f4235c..87bbbbe46b1 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -77,7 +77,7 @@ "AWS.configuration.description.amazonq": "Amazon Q creates a code reference when you insert a code suggestion from Amazon Q that is similar to training data. When unchecked, Amazon Q will not show code suggestions that have code references. If you authenticate through IAM Identity Center, this setting is controlled by your Amazon Q administrator. [Learn More](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-reference.html)", "AWS.configuration.description.amazonq.shareContentWithAWS": "When checked, your content processed by Amazon Q may be used for service improvement (except for content processed for users with the Amazon Q Developer Pro Tier). Unchecking this box will cause AWS to delete any of your content used for that purpose. The information used to provide the Amazon Q service to you will not be affected. See the [Service Terms](https://aws.amazon.com/service-terms) for more details.", "AWS.configuration.description.amazonq.importRecommendation": "Amazon Q will add import statements with inline code suggestions when necessary.", - "AWS.configuration.description.amazonq.workspaceIndex": "When you add @workspace to your question in Amazon Q chat, Amazon Q will index your open workspace files locally to use as context for its response. Extra CPU usage is expected while indexing a workspace. This will not impact Amazon Q features or your IDE, but you may manage CPU usage by setting the number of local threads in 'Local Workspace Index Threads'.", + "AWS.configuration.description.amazonq.workspaceIndex": "When you add @workspace to your question in Amazon Q chat, Amazon Q will index your workspace files locally to use as context for its response. Extra CPU usage is expected while indexing a workspace. This will not impact Amazon Q features or your IDE, but you may manage CPU usage by setting the number of local threads in 'Local Workspace Index Threads'.", "AWS.configuration.description.amazonq.workspaceIndexWorkerThreads": "Number of worker threads of Amazon Q local index process. '0' will use the system default worker threads for balance performance. You may increase this number to more quickly index your workspace, but only up to your hardware's number of CPU cores. Please restart VS Code or reload the VS Code window after changing worker threads.", "AWS.configuration.description.amazonq.workspaceIndexUseGPU": "Enable GPU to help index your local workspace files. Only applies to Linux and Windows.", "AWS.configuration.description.amazonq.workspaceIndexMaxSize": "The maximum size of local workspace files to be indexed in MB", @@ -136,6 +136,7 @@ "AWS.command.amazonq.acceptFix": "Accept Fix", "AWS.command.amazonq.regenerateFix": "Regenerate Fix", "AWS.command.amazonq.filterIssues": "Filter Issues", + "AWS.command.amazonq.groupIssues": "Group Issues", "AWS.command.deploySamApplication": "Deploy SAM Application", "AWS.command.aboutToolkit": "About", "AWS.command.downloadLambda": "Download...", @@ -243,9 +244,11 @@ "AWS.command.codewhisperer.openReferencePanel": "Open Code Reference Log", "AWS.command.q.transform.acceptChanges": "Accept", "AWS.command.q.transform.rejectChanges": "Reject", - "AWS.command.q.transform.showChanges": "Show Proposed Changes", - "AWS.command.q.transform.showChangeSummary": "Show Transformation Summary", - "AWS.command.q.transform.showTransformationPlan": "Show Transformation Plan", + "AWS.command.q.transform.stopJobInHub": "Stop job", + "AWS.command.q.transform.viewJobProgress": "View job progress", + "AWS.command.q.transform.viewJobStatus": "View job status", + "AWS.command.q.transform.showTransformationPlan": "View plan", + "AWS.command.q.transform.showChangeSummary": "View summary", "AWS.command.threatComposer.createNew": "Create New Threat Composer File", "AWS.command.threatComposer.newFile": "Threat Composer File", "AWS.threatComposer.page.title": "{0} (Threat Composer)", @@ -313,6 +316,11 @@ "AWS.amazonq.scans.projectScanInProgress": "Workspace review is in progress...", "AWS.amazonq.scans.fileScanInProgress": "File review is in progress...", "AWS.amazonq.scans.noGitRepo": "Your workspace is not in a git repository. I'll review your project files for security issues, and your in-flight changes for code quality issues.", + "AWS.amazonq.codefix.error.monthlyLimitReached": "Maximum code fix count reached for this month.", + "AWS.amazonq.scans.severity": "Severity", + "AWS.amazonq.scans.fileLocation": "File Location", + "AWS.amazonq.scans.groupIssues": "Group Issues", + "AWS.amazonq.scans.groupIssues.placeholder": "Select how to group code issues", "AWS.amazonq.featureDev.error.conversationIdNotFoundError": "Conversation id must exist before starting code generation", "AWS.amazonq.featureDev.error.contentLengthError": "The folder you selected is too large for me to use as context. Please choose a smaller folder to work on. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.featureDev.error.illegalStateTransition": "Illegal transition between states, restart the conversation", @@ -374,6 +382,7 @@ "AWS.amazonq.doc.answer.readmeCreated": "I've created a README for your code.", "AWS.amazonq.doc.answer.readmeUpdated": "I've updated your README.", "AWS.amazonq.doc.answer.codeResult": "You can accept the changes to your files, or describe any additional changes you'd like me to make.", + "AWS.amazonq.doc.answer.acceptOrReject": "You can accept or reject the changes to your files.", "AWS.amazonq.doc.answer.scanning": "Scanning source files", "AWS.amazonq.doc.answer.summarizing": "Summarizing source files", "AWS.amazonq.doc.answer.generating": "Generating documentation", @@ -383,18 +392,21 @@ "AWS.amazonq.doc.error.noFolderSelected": "It looks like you didn't choose a folder. Choose a folder to continue.", "AWS.amazonq.doc.error.contentLengthError": "Your workspace is too large for me to review. Your workspace must be within the quota, even if you choose a smaller folder. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.readmeTooLarge": "The README in your folder is too large for me to review. Try reducing the size of your README, or choose a folder with a smaller README. For more information on quotas, see the Amazon Q Developer documentation.", - "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README is too large. Try reducing the size of your README, or asking for a smaller update. For more information on quotas, see the Amazon Q Developer documentation.", + "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README exceeds document size limits. Try reducing the size of your current README or working on a smaller task that won't produce as much content. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.workspaceEmpty": "The folder you chose did not contain any source files in a supported language. Choose another folder and try again. For more information on supported languages, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptTooVague": "I need more information to make changes to your README. Try providing some of the following details:\n- Which sections you want to modify\n- The content you want to add or remove\n- Specific issues that need correcting\n\nFor more information on prompt best practices, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptUnrelated": "These changes don't seem related to documentation. Try describing your changes again, using the following best practices:\n- Changes should relate to how project functionality is reflected in the README\n- Content you refer to should be available in your codebase\n\n For more information on prompt best practices, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.docGen.default": "I'm sorry, I ran into an issue while trying to generate your documentation. Please try again.", "AWS.amazonq.doc.error.noChangeRequiredException": "I couldn't find any code changes to update in the README. Try another documentation task.", - "AWS.amazonq.doc.error.promptRefusal": "I'm sorry, I can't generate documentation for this folder. Please make sure your message and code files comply with the Please make sure your message and code files comply with the AWS Responsible AI Policy.", + "AWS.amazonq.doc.error.promptRefusal": "I'm sorry, I can't generate documentation for this folder. Please make sure your message and code files comply with the AWS Responsible AI Policy.", "AWS.amazonq.doc.placeholder.editReadme": "Describe documentation changes", "AWS.amazonq.doc.pillText.closeSession": "End session", "AWS.amazonq.doc.pillText.newTask": "Start a new documentation task", "AWS.amazonq.doc.pillText.update": "Update README to reflect code", "AWS.amazonq.doc.pillText.makeChange": "Make a specific change", + "AWS.amazonq.doc.pillText.accept": "Accept", + "AWS.amazonq.doc.pillText.reject": "Reject", + "AWS.amazonq.doc.pillText.makeChanges": "Make changes", "AWS.amazonq.inline.invokeChat": "Inline chat", "AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough", "AWS.toolkit.lambda.walkthrough.title": "Get started building your application", diff --git a/packages/core/resources/css/base-cloud9.css b/packages/core/resources/css/base-cloud9.css deleted file mode 100644 index ec1e30468c6..00000000000 --- a/packages/core/resources/css/base-cloud9.css +++ /dev/null @@ -1,68 +0,0 @@ -/* TODO: remove this when Cloud9 injects the correct styling information into webviews */ -@import url('./base.css'); - -body { - /* Temporary variables for C9 to shade/tint elements. Best-effort styling based off current theme. */ - /* Since these are applied as rgba, it's very easy to make things look 'washed-out' or too dark */ - --tint: 255, 255, 255; - --shade: 0, 0, 0; -} - -input[type='text'][data-invalid='true'], -input[type='number'][data-invalid='true'] { - border: 1px solid var(--vscode-inputValidation-errorBorder); - border-bottom: 0; - background: none; -} - -/* "Cloud9 gray" in input boxes (not buttons/radios). */ -body.vscode-dark input:not([type='submit']):not([type='radio']) { - background-color: rgba(var(--shade), 0.1); -} - -input:disabled { - filter: none; -} - -body.vscode-dark select { - background: rgba(var(--shade), 0.1); -} - -body.vscode-dark .header { - background-color: rgba(var(--tint), 0.02); -} -body.vscode-light .header { - background-color: rgba(var(--shade), 0.02); -} - -body.vscode-dark .notification { - background-color: #2a2a2a; -} -body.vscode-light .notification { - background-color: #f7f7f7; - box-shadow: 2px 2px 8px #aaa; -} - -button:disabled { - filter: none; -} - -/* Text area */ -textarea { - background: none; -} -body.vscode-dark textarea { - background: rgba(var(--shade), 0.1); -} - -/* Overrides */ -body.vscode-dark .settings-panel { - background: rgba(var(--tint), 0.02) !important; -} -body.vscode-light .settings-panel { - background: rgba(var(--shade), 0.02) !important; -} - -.button-container h1 { - margin: 0px; -} diff --git a/packages/core/resources/css/securityIssue.css b/packages/core/resources/css/securityIssue.css index d766ccd8fe2..5cd64211ae8 100644 --- a/packages/core/resources/css/securityIssue.css +++ b/packages/core/resources/css/securityIssue.css @@ -524,6 +524,13 @@ pre.center { pre.error { color: var(--vscode-diffEditorOverview-removedForeground); + background-color: var(--vscode-diffEditor-removedTextBackground); + white-space: initial; +} + +a.cursor { + cursor: pointer; + text-decoration: none; } .dot-typing { diff --git a/packages/core/resources/icons/cloud9/dark/vscode-help.svg b/packages/core/resources/icons/cloud9/dark/vscode-help.svg deleted file mode 100644 index 94b17dfd8b4..00000000000 --- a/packages/core/resources/icons/cloud9/dark/vscode-help.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/core/resources/icons/cloud9/light/vscode-help.svg b/packages/core/resources/icons/cloud9/light/vscode-help.svg deleted file mode 100644 index 81c89a3b963..00000000000 --- a/packages/core/resources/icons/cloud9/light/vscode-help.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts index ab053333432..4c29f005557 100644 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ b/packages/core/src/amazonq/commons/connector/baseMessenger.ts @@ -85,6 +85,7 @@ export class Messenger { type: 'answer', tabID: tabID, message: i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), + disableChatInput: true, }) this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled')) } diff --git a/packages/core/src/amazonq/commons/controllers/contentController.ts b/packages/core/src/amazonq/commons/controllers/contentController.ts index cdf19fe86b4..1380253f8eb 100644 --- a/packages/core/src/amazonq/commons/controllers/contentController.ts +++ b/packages/core/src/amazonq/commons/controllers/contentController.ts @@ -16,6 +16,7 @@ import { getSelectionFromRange, } from '../../../shared/utilities/textDocumentUtilities' import { extractFileAndCodeSelectionFromMessage, fs, getErrorMsg, ToolkitError } from '../../../shared' +import { UserWrittenCodeTracker } from '../../../codewhisperer/tracker/userWrittenCodeTracker' export class ContentProvider implements vscode.TextDocumentContentProvider { constructor(private uri: vscode.Uri) {} @@ -41,6 +42,7 @@ export class EditorContentController { ) { const editor = window.activeTextEditor if (editor) { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const cursorStart = editor.selection.active const indentRange = new vscode.Range(new vscode.Position(cursorStart.line, 0), cursorStart) // use the user editor intent if the position to the left of cursor is just space or tab @@ -66,9 +68,11 @@ export class EditorContentController { if (appliedEdits) { trackCodeEdit(editor, cursorStart) } + UserWrittenCodeTracker.instance.onQFinishesEdits() }, (e) => { getLogger().error('TextEditor.edit failed: %s', (e as Error).message) + UserWrittenCodeTracker.instance.onQFinishesEdits() } ) } @@ -97,6 +101,7 @@ export class EditorContentController { if (filePath && message?.code?.trim().length > 0 && selection) { try { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const doc = await vscode.workspace.openTextDocument(filePath) const code = getIndentedCode(message, doc, selection) @@ -130,6 +135,8 @@ export class EditorContentController { const wrappedError = ChatDiffError.chain(error, `Failed to Accept Diff`, { code: chatDiffCode }) getLogger().error('%s: Failed to open diff view %s', chatDiffCode, getErrorMsg(wrappedError, true)) throw wrappedError + } finally { + UserWrittenCodeTracker.instance.onQFinishesEdits() } } } diff --git a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts index cefc2b8818f..ae179fd6c41 100644 --- a/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/testChatConnector.ts @@ -13,6 +13,7 @@ import { TabsStorage, TabType } from '../storages/tabsStorage' import { TestMessageType } from '../../../../amazonqTest/chat/views/connector/connector' import { ChatPayload } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' +import { FollowUpTypes } from '../../../commons/types' export interface ConnectorProps extends BaseConnectorProps { sendMessageToExtension: (message: ExtensionMessage) => void @@ -107,15 +108,43 @@ export class Connector extends BaseConnector { } onFileDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { - // TODO: add this back once we can advance flow from here - // this.sendMessageToExtension({ - // command: 'open-diff', - // tabID, - // filePath, - // deleted, - // messageId, - // tabType: 'testgen', - // }) + if (this.onChatAnswerReceived === undefined) { + return + } + // Open diff view + this.sendMessageToExtension({ + command: 'open-diff', + tabID, + filePath, + deleted, + messageId, + tabType: 'testgen', + }) + this.onChatAnswerReceived( + tabID, + { + type: ChatItemType.ANSWER, + messageId: messageId, + followUp: { + text: ' ', + options: [ + { + type: FollowUpTypes.AcceptCode, + pillText: 'Accept', + status: 'success', + icon: MynahIcons.OK, + }, + { + type: FollowUpTypes.RejectCode, + pillText: 'Reject', + status: 'error', + icon: MynahIcons.REVERT, + }, + ], + }, + }, + {} + ) } private processChatMessage = async (messageData: any): Promise => { diff --git a/packages/core/src/amazonqDoc/constants.ts b/packages/core/src/amazonqDoc/constants.ts index 5d1d938c940..90284a90648 100644 --- a/packages/core/src/amazonqDoc/constants.ts +++ b/packages/core/src/amazonqDoc/constants.ts @@ -92,6 +92,43 @@ export const FolderSelectorFollowUps = [ }, ] +export const CodeChangeFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.accept'), + prompt: i18n('AWS.amazonq.doc.pillText.accept'), + type: FollowUpTypes.AcceptChanges, + icon: 'ok' as MynahIcons, + status: 'success' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.makeChanges'), + prompt: i18n('AWS.amazonq.doc.pillText.makeChanges'), + type: FollowUpTypes.MakeChanges, + icon: 'refresh' as MynahIcons, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.reject'), + prompt: i18n('AWS.amazonq.doc.pillText.reject'), + type: FollowUpTypes.RejectChanges, + icon: 'cancel' as MynahIcons, + status: 'error' as Status, + }, +] + +export const NewSessionFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.newTask'), + type: FollowUpTypes.NewTask, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), + type: FollowUpTypes.CloseSession, + status: 'info' as Status, + }, +] + export const SynchronizeDocumentation = { pillText: i18n('AWS.amazonq.doc.pillText.update'), prompt: i18n('AWS.amazonq.doc.pillText.update'), diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts index 8246a011fbe..c91a387484a 100644 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ b/packages/core/src/amazonqDoc/controllers/chat/controller.ts @@ -10,7 +10,9 @@ import { EditDocumentation, FolderSelectorFollowUps, Mode, + NewSessionFollowUps, SynchronizeDocumentation, + CodeChangeFollowUps, docScheme, featureName, findReadmePath, @@ -22,7 +24,6 @@ import { Session } from '../../session/session' import { i18n } from '../../../shared/i18n-helper' import path from 'path' import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' -import { MynahIcons } from '@aws/mynah-ui' import { MonthlyConversationLimitError, @@ -298,18 +299,7 @@ export class DocController { tabID: data?.tabID, disableChatInput: true, message: 'Your changes have been discarded.', - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) break case FollowUpTypes.ProceedFolderSelection: @@ -412,13 +402,19 @@ export class DocController { const errorMessage = createUserFacingErrorMessage(`${err.cause?.message ?? err.message}`) // eslint-disable-next-line unicorn/no-null this.messenger.sendUpdatePromptProgress(message.tabID, null) + if (err.constructor.name === MonthlyConversationLimitError.name) { + this.messenger.sendMonthlyLimitError(message.tabID) + } else { + const enableUserInput = this.mode === Mode.EDIT && err.remainingIterations > 0 - switch (err.constructor.name) { - case MonthlyConversationLimitError.name: - this.messenger.sendMonthlyLimitError(message.tabID) - break - default: - this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, false) + this.messenger.sendErrorMessage( + errorMessage, + message.tabID, + 0, + session?.conversationIdUnsafe, + false, + enableUserInput + ) } } @@ -427,8 +423,6 @@ export class DocController { await this.onDocsGeneration(session, message.message, message.tabID) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -461,12 +455,8 @@ export class DocController { } await this.generateDocumentation({ message, session }) - this.messenger.sendChatInputEnabled(message?.tabID, false) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -590,40 +580,21 @@ export class DocController { this.messenger.sendAnswer({ type: 'answer', tabID: tabID, - message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${i18n('AWS.amazonq.doc.answer.codeResult')}`, + message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${remainingIterations > 0 ? i18n('AWS.amazonq.doc.answer.codeResult') : i18n('AWS.amazonq.doc.answer.acceptOrReject')}`, disableChatInput: true, }) - } - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - disableChatInput: true, - followUps: [ - { - pillText: 'Accept', - prompt: 'Accept', - type: FollowUpTypes.AcceptChanges, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: 'Make changes', - prompt: 'Make changes', - type: FollowUpTypes.MakeChanges, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - { - pillText: 'Reject', - prompt: 'Reject', - type: FollowUpTypes.RejectChanges, - icon: 'cancel' as MynahIcons, - status: 'error', - }, - ], - tabID: tabID, - }) + this.messenger.sendAnswer({ + message: undefined, + type: 'system-prompt', + disableChatInput: true, + followUps: + remainingIterations > 0 + ? CodeChangeFollowUps + : CodeChangeFollowUps.filter((followUp) => followUp.type !== FollowUpTypes.MakeChanges), + tabID: tabID, + }) + } } finally { if (session?.state?.tokenSource?.token.isCancellationRequested) { await this.newTask({ tabID }) @@ -642,10 +613,8 @@ export class DocController { type: 'answer', tabID: message.tabID, message: 'Follow instructions to re-authenticate ...', + disableChatInput: true, }) - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) } private tabClosed(message: any) { @@ -670,18 +639,7 @@ export class DocController { type: 'answer', disableChatInput: true, tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) diff --git a/packages/core/src/amazonqDoc/errors.ts b/packages/core/src/amazonqDoc/errors.ts index d9794f16327..11a6514a616 100644 --- a/packages/core/src/amazonqDoc/errors.ts +++ b/packages/core/src/amazonqDoc/errors.ts @@ -7,69 +7,57 @@ import { ToolkitError } from '../shared/errors' import { i18n } from '../shared/i18n-helper' export class DocServiceError extends ToolkitError { - constructor(message: string, code: string) { + remainingIterations?: number + constructor(message: string, code: string, remainingIterations?: number) { super(message, { code }) + this.remainingIterations = remainingIterations } } -export class ReadmeTooLargeError extends ToolkitError { +export class ReadmeTooLargeError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), { - code: ReadmeTooLargeError.name, - }) + super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), ReadmeTooLargeError.name) } } -export class ReadmeUpdateTooLargeError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), { - code: ReadmeUpdateTooLargeError.name, - }) +export class ReadmeUpdateTooLargeError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), ReadmeUpdateTooLargeError.name, remainingIterations) } } -export class WorkspaceEmptyError extends ToolkitError { +export class WorkspaceEmptyError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), { - code: WorkspaceEmptyError.name, - }) + super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), WorkspaceEmptyError.name) } } -export class NoChangeRequiredException extends ToolkitError { +export class NoChangeRequiredException extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), { - code: NoChangeRequiredException.name, - }) + super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), NoChangeRequiredException.name) } } -export class PromptRefusalException extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptRefusal'), { - code: PromptRefusalException.name, - }) +export class PromptRefusalException extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptRefusal'), PromptRefusalException.name, remainingIterations) } } -export class ContentLengthError extends ToolkitError { +export class ContentLengthError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.contentLengthError'), { code: ContentLengthError.name }) + super(i18n('AWS.amazonq.doc.error.contentLengthError'), ContentLengthError.name) } } -export class PromptTooVagueError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptTooVague'), { - code: PromptTooVagueError.name, - }) +export class PromptTooVagueError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptTooVague'), PromptTooVagueError.name, remainingIterations) } } -export class PromptUnrelatedError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptUnrelated'), { - code: PromptUnrelatedError.name, - }) +export class PromptUnrelatedError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptUnrelated'), PromptUnrelatedError.name, remainingIterations) } } diff --git a/packages/core/src/amazonqDoc/messenger.ts b/packages/core/src/amazonqDoc/messenger.ts index 09be3dd11fb..f28e5e9060b 100644 --- a/packages/core/src/amazonqDoc/messenger.ts +++ b/packages/core/src/amazonqDoc/messenger.ts @@ -4,10 +4,9 @@ */ import { Messenger } from '../amazonq/commons/connector/baseMessenger' import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { FollowUpTypes } from '../amazonq/commons/types' import { messageWithConversationId } from '../amazonqFeatureDev' import { i18n } from '../shared/i18n-helper' -import { docGenerationProgressMessage, DocGenerationStep, Mode } from './constants' +import { docGenerationProgressMessage, DocGenerationStep, Mode, NewSessionFollowUps } from './constants' import { inProgress } from './types' export class DocMessenger extends Messenger { @@ -48,25 +47,19 @@ export class DocMessenger extends Messenger { tabID: string, _retries: number, conversationId?: string, - _showDefaultMessage?: boolean + _showDefaultMessage?: boolean, + enableUserInput?: boolean ) { + if (enableUserInput) { + this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) + this.sendChatInputEnabled(tabID, true) + } this.sendAnswer({ type: 'answer', tabID: tabID, message: errorMessage + messageWithConversationId(conversationId), - }) - - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID, + followUps: enableUserInput ? [] : NewSessionFollowUps, + disableChatInput: !enableUserInput, }) } } diff --git a/packages/core/src/amazonqDoc/session/sessionState.ts b/packages/core/src/amazonqDoc/session/sessionState.ts index b3404c7998a..7bf9c02e51b 100644 --- a/packages/core/src/amazonqDoc/session/sessionState.ts +++ b/packages/core/src/amazonqDoc/session/sessionState.ts @@ -97,7 +97,7 @@ abstract class CodeGenBase { ++pollingIteration ) { const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount + const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount || 0 const codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount getLogger().debug(`Codegen response: %O`, codegenResult) @@ -151,7 +151,7 @@ abstract class CodeGenBase { throw new ReadmeTooLargeError() } case codegenResult.codeGenerationStatusDetail?.includes('README_UPDATE_TOO_LARGE'): { - throw new ReadmeUpdateTooLargeError() + throw new ReadmeUpdateTooLargeError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_TOO_LARGE'): { throw new ContentLengthError() @@ -160,18 +160,19 @@ abstract class CodeGenBase { throw new WorkspaceEmptyError() } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_UNRELATED'): { - throw new PromptUnrelatedError() + throw new PromptUnrelatedError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_TOO_VAGUE'): { - throw new PromptTooVagueError() + throw new PromptTooVagueError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_REFUSAL'): { - throw new PromptRefusalException() + throw new PromptRefusalException(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { throw new DocServiceError( i18n('AWS.amazonq.doc.error.docGen.default'), - 'GuardrailsException' + 'GuardrailsException', + codeGenerationRemainingIterationCount ) } case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { @@ -186,7 +187,8 @@ abstract class CodeGenBase { case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { throw new DocServiceError( i18n('AWS.amazonq.featureDev.error.throttling'), - 'ThrottlingException' + 'ThrottlingException', + codeGenerationRemainingIterationCount ) } default: { diff --git a/packages/core/src/amazonqFeatureDev/client/featureDev.ts b/packages/core/src/amazonqFeatureDev/client/featureDev.ts index ceef0b616e7..01be84828b3 100644 --- a/packages/core/src/amazonqFeatureDev/client/featureDev.ts +++ b/packages/core/src/amazonqFeatureDev/client/featureDev.ts @@ -25,6 +25,7 @@ import { createCodeWhispererChatStreamingClient } from '../../shared/clients/cod import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util' import { extensionVersion } from '../../shared/vscode/env' import apiConfig = require('./codewhispererruntime-2022-11-11.json') +import { UserWrittenCodeTracker } from '../../codewhisperer' import { FeatureDevCodeAcceptanceEvent, FeatureDevCodeGenerationEvent, @@ -260,6 +261,7 @@ export class FeatureDevClient { references?: CodeReference[] } } + UserWrittenCodeTracker.instance.onQFeatureInvoked() const newFileContents: { zipFilePath: string; fileContent: string }[] = [] for (const [filePath, fileContent] of Object.entries(newFiles)) { diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 9ad24069f70..aad70595366 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -566,7 +566,7 @@ export class FeatureDevController { if (isStoppedGeneration) { this.messenger.sendAnswer({ message: ((remainingIterations) => { - if (remainingIterations && totalIterations) { + if (totalIterations !== undefined) { if (remainingIterations <= 0) { return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." } else if (remainingIterations <= 2) { diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index bd13a2d9a95..647c5682184 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -53,11 +53,9 @@ import { import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState' import DependencyVersions from '../../models/dependencies' import { getStringHash } from '../../../shared/utilities/textUtilities' -import { getVersionData } from '../../../codewhisperer/service/transformByQ/transformMavenHandler' import AdmZip from 'adm-zip' import { AuthError } from '../../../auth/sso/server' import { - setMaven, openBuildLogFile, parseBuildFile, validateSQLMetadataFile, @@ -321,12 +319,6 @@ export class GumbyController { telemetryJavaVersion = JDKToTelemetryValue(javaVersion) as CodeTransformJavaSourceVersionsAllowed } telemetry.record({ codeTransformLocalJavaVersion: telemetryJavaVersion }) - - await setMaven() - const versionInfo = await getVersionData() - const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})` - telemetry.record({ buildSystemVersion: mavenVersionInfoMessage }) - return validProjects }) return validProjects diff --git a/packages/core/src/amazonqTest/chat/controller/controller.ts b/packages/core/src/amazonqTest/chat/controller/controller.ts index 57d4d231d07..ab1157dca38 100644 --- a/packages/core/src/amazonqTest/chat/controller/controller.ts +++ b/packages/core/src/amazonqTest/chat/controller/controller.ts @@ -20,7 +20,9 @@ import { TelemetryHelper, TestGenerationBuildStep, testGenState, + tooManyRequestErrorMessage, unitTestGenerationCancelMessage, + UserWrittenCodeTracker, } from '../../../codewhisperer' import { fs, @@ -241,71 +243,76 @@ export class TestController { // eslint-disable-next-line unicorn/no-null this.messenger.sendUpdatePromptProgress(data.tabID, null) const session = this.sessionStorage.getSession() - const isCancel = data.error.message === unitTestGenerationCancelMessage - + const isCancel = data.error.uiMessage === unitTestGenerationCancelMessage + let telemetryErrorMessage = getTelemetryReasonDesc(data.error) + if (session.stopIteration) { + telemetryErrorMessage = getTelemetryReasonDesc(data.error.uiMessage.replaceAll('```', '')) + } TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, isCancel ? 'Cancelled' : 'Failed', session.startTestGenerationRequestId, performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc(data.error), + telemetryErrorMessage, session.isCodeBlockSelected, session.artifactsUploadDuration, session.srcPayloadSize, session.srcZipFileSize ) - if (session.stopIteration) { // Error from Science - this.messenger.sendMessage(data.error.message.replaceAll('```', ''), data.tabID, 'answer') + this.messenger.sendMessage(data.error.uiMessage.replaceAll('```', ''), data.tabID, 'answer') } else { isCancel - ? this.messenger.sendMessage(data.error.message, data.tabID, 'answer') + ? this.messenger.sendMessage(data.error.uiMessage, data.tabID, 'answer') : this.sendErrorMessage(data) } await this.sessionCleanUp() return } // Client side error messages - private sendErrorMessage(data: { tabID: string; error: { code: string; message: string } }) { + private sendErrorMessage(data: { + tabID: string + error: { uiMessage: string; message: string; code: string; statusCode: string } + }) { const { error, tabID } = data + // If user reached monthly limit for builderId + if (error.code === 'CreateTestJobError') { + if (error.message.includes(CodeWhispererConstants.utgLimitReached)) { + getLogger().error('Monthly quota reached for QSDA actions.') + return this.messenger.sendMessage( + i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), + tabID, + 'answer' + ) + } + if (error.message.includes('Too many requests')) { + getLogger().error(error.message) + return this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) + } + } if (isAwsError(error)) { if (error.code === 'ThrottlingException') { - // TODO: use the explicitly modeled exception reason for quota vs throttle - if (error.message.includes(CodeWhispererConstants.utgLimitReached)) { - getLogger().error('Monthly quota reached for QSDA actions.') - return this.messenger.sendMessage( - i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - tabID, - 'answer' - ) - } else { - getLogger().error('Too many requests.') - // TODO: move to constants file - this.messenger.sendErrorMessage('Too many requests. Please wait before retrying.', tabID) - } - } else { - // other service errors: - // AccessDeniedException - should not happen because access is validated before this point in the client - // ValidationException - shouldn't happen because client should not send malformed requests - // ConflictException - should not happen because the client will maintain proper state - // InternalServerException - shouldn't happen but needs to be caught - getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage( - 'Encountered an unexpected error when generating tests. Please try again', - tabID - ) + // TODO: use the explicitly modeled exception reason for quota vs throttle{ + getLogger().error(error.message) + this.messenger.sendErrorMessage(tooManyRequestErrorMessage, tabID) + return } - } else { - // other unexpected errors (TODO enumerate all other failure cases) + // other service errors: + // AccessDeniedException - should not happen because access is validated before this point in the client + // ValidationException - shouldn't happen because client should not send malformed requests + // ConflictException - should not happen because the client will maintain proper state + // InternalServerException - shouldn't happen but needs to be caught getLogger().error('Other error message: %s', error.message) - this.messenger.sendErrorMessage( - 'Encountered an unexpected error when generating tests. Please try again', - tabID - ) + this.messenger.sendErrorMessage('', tabID) + return } + // other unexpected errors (TODO enumerate all other failure cases) + getLogger().error('Other error message: %s', error.uiMessage) + this.messenger.sendErrorMessage('', tabID) } // This function handles actions if user clicked on any Button one of these cases will be executed @@ -456,7 +463,14 @@ export class TestController { unsupportedMessage = `I'm sorry, but /test only supports Python and Java
I will still generate a suggestion below.` } this.messenger.sendMessage(unsupportedMessage, tabID, 'answer') - await this.onCodeGeneration(session, message.prompt, tabID, fileName, filePath) + await this.onCodeGeneration( + session, + message.prompt, + tabID, + fileName, + filePath, + workspaceFolder !== undefined + ) } else { this.messenger.sendCapabilityCard({ tabID }) this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part') @@ -656,12 +670,14 @@ export class TestController { acceptedLines = acceptedLines < 0 ? 0 : acceptedLines acceptedChars -= originalContent.length acceptedChars = acceptedChars < 0 ? 0 : acceptedChars + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const document = await vscode.workspace.openTextDocument(absolutePath) await applyChanges( document, new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end), updatedContent ) + UserWrittenCodeTracker.instance.onQFinishesEdits() } else { await fs.writeFile(absolutePath, updatedContent) } @@ -719,9 +735,13 @@ export class TestController { // this.messenger.sendMessage('Accepted', message.tabID, 'prompt') telemetry.ui_click.emit({ elementId: 'unitTestGeneration_acceptDiff' }) + getLogger().info( + `Generated unit tests are accepted for ${session.fileLanguage ?? 'plaintext'} language with jobId: ${session.listOfTestGenerationJobId[0]}, jobGroupName: ${session.testGenerationJobGroupName}, result: Succeeded` + ) TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, 'Succeeded', session.startTestGenerationRequestId, session.latencyOfTestGeneration, @@ -739,7 +759,6 @@ export class TestController { ) await this.endSession(message, FollowUpTypes.SkipBuildAndFinish) - await this.sessionCleanUp() return if (session.listOfTestGenerationJobId.length === 1) { @@ -799,20 +818,21 @@ export class TestController { message: string, tabID: string, fileName: string, - filePath: string + filePath: string, + fileInWorkspace: boolean ) { try { // TODO: Write this entire gen response to basiccommands and call here. const editorText = await fs.readFileText(filePath) const triggerPayload = { - query: `Generate unit tests for the following part of my code: ${message}`, + query: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, codeSelection: undefined, trigger: ChatTriggerType.ChatMessage, fileText: editorText, fileLanguage: session.fileLanguage, filePath: filePath, - message: `Generate unit tests for the following part of my code: ${message}`, + message: `Generate unit tests for the following part of my code: ${message?.trim() || fileName}`, matchPolicy: undefined, codeQuery: undefined, userIntent: UserIntent.GENERATE_UNIT_TESTS, @@ -821,13 +841,15 @@ export class TestController { const chatRequest = triggerPayloadToChatRequest(triggerPayload) const client = await createCodeWhispererChatStreamingClient() const response = await client.generateAssistantResponse(chatRequest) + UserWrittenCodeTracker.instance.onQFeatureInvoked() await this.messenger.sendAIResponse( response, session, tabID, randomUUID.toString(), triggerPayload, - fileName + fileName, + fileInWorkspace ) } finally { this.messenger.sendChatInputEnabled(tabID, true) @@ -845,6 +867,7 @@ export class TestController { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, true, + true, 'Succeeded', session.startTestGenerationRequestId, session.latencyOfTestGeneration, @@ -860,16 +883,12 @@ export class TestController { session.numberOfTestsGenerated, session.linesOfCodeGenerated ) - telemetry.ui_click.emit({ elementId: 'unitTestGeneration_rejectDiff' }) } await this.sessionCleanUp() - // TODO: revert 'Accepted' to 'Skip build and finish' once supported - const message = step === FollowUpTypes.RejectCode ? 'Rejected' : 'Accepted' - this.messenger.sendMessage(message, data.tabID, 'prompt') - this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') + // this.messenger.sendMessage(`Unit test generation workflow is completed.`, data.tabID, 'answer') this.messenger.sendChatInputEnabled(data.tabID, true) return } @@ -1304,8 +1323,18 @@ export class TestController { 'Deleting output.log and temp result directory. testGenerationLogsDir: %s', testGenerationLogsDir ) - await fs.delete(path.join(testGenerationLogsDir, 'output.log')) - await fs.delete(this.tempResultDirPath, { recursive: true }) + const outputLogPath = path.join(testGenerationLogsDir, 'output.log') + if (await fs.existsFile(outputLogPath)) { + await fs.delete(outputLogPath) + } + if ( + await fs + .stat(this.tempResultDirPath) + .then(() => true) + .catch(() => false) + ) { + await fs.delete(this.tempResultDirPath, { recursive: true }) + } } // TODO: return build command when product approves diff --git a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts index 10b496b69d3..f842a6c1808 100644 --- a/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqTest/chat/controller/messenger/messenger.ts @@ -183,7 +183,8 @@ export class Messenger { tabID: string, triggerID: string, triggerPayload: TriggerPayload, - fileName: string + fileName: string, + fileInWorkspace: boolean ) { let message = '' let messageId = response.$metadata.requestId ?? '' @@ -277,12 +278,25 @@ export class Messenger { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, false, + fileInWorkspace, 'Cancelled', messageId, performance.now() - session.testGenerationStartTime, - getTelemetryReasonDesc(CodeWhispererConstants.unitTestGenerationCancelMessage) + getTelemetryReasonDesc( + `TestGenCancelled: ${CodeWhispererConstants.unitTestGenerationCancelMessage}` + ), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + 'TestGenCancelled' ) - this.dispatcher.sendUpdatePromptProgress( new UpdatePromptProgressMessage(tabID, cancellingProgressField) ) @@ -291,11 +305,12 @@ export class Messenger { TelemetryHelper.instance.sendTestGenerationToolkitEvent( session, false, + fileInWorkspace, 'Succeeded', messageId, - performance.now() - session.testGenerationStartTime + performance.now() - session.testGenerationStartTime, + undefined ) - this.dispatcher.sendUpdatePromptProgress( new UpdatePromptProgressMessage(tabID, testGenCompletedField) ) diff --git a/packages/core/src/amazonqTest/error.ts b/packages/core/src/amazonqTest/error.ts new file mode 100644 index 00000000000..a6694b35863 --- /dev/null +++ b/packages/core/src/amazonqTest/error.ts @@ -0,0 +1,67 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { ToolkitError } from '../shared/errors' + +export const technicalErrorCustomerFacingMessage = + 'I am experiencing technical difficulties at the moment. Please try again in a few minutes.' +const defaultTestGenErrorMessage = 'Amazon Q encountered an error while generating tests. Try again later.' +export class TestGenError extends ToolkitError { + constructor( + error: string, + code: string, + public uiMessage: string + ) { + super(error, { code }) + } +} +export class ProjectZipError extends TestGenError { + constructor(error: string) { + super(error, 'ProjectZipError', defaultTestGenErrorMessage) + } +} +export class InvalidSourceZipError extends TestGenError { + constructor() { + super('Failed to create valid source zip', 'InvalidSourceZipError', defaultTestGenErrorMessage) + } +} +export class CreateUploadUrlError extends TestGenError { + constructor(errorMessage: string) { + super(errorMessage, 'CreateUploadUrlError', technicalErrorCustomerFacingMessage) + } +} +export class UploadTestArtifactToS3Error extends TestGenError { + constructor(error: string) { + super(error, 'UploadTestArtifactToS3Error', technicalErrorCustomerFacingMessage) + } +} +export class CreateTestJobError extends TestGenError { + constructor(error: string) { + super(error, 'CreateTestJobError', technicalErrorCustomerFacingMessage) + } +} +export class TestGenTimedOutError extends TestGenError { + constructor() { + super( + 'Test generation failed. Amazon Q timed out.', + 'TestGenTimedOutError', + technicalErrorCustomerFacingMessage + ) + } +} +export class TestGenStoppedError extends TestGenError { + constructor() { + super('Unit test generation cancelled.', 'TestGenCancelled', 'Unit test generation cancelled.') + } +} +export class TestGenFailedError extends TestGenError { + constructor(error?: string) { + super(error ?? 'Test generation failed', 'TestGenFailedError', error ?? technicalErrorCustomerFacingMessage) + } +} +export class ExportResultsArchiveError extends TestGenError { + constructor(error?: string) { + super(error ?? 'Test generation failed', 'ExportResultsArchiveError', technicalErrorCustomerFacingMessage) + } +} diff --git a/packages/core/src/auth/credentials/utils.ts b/packages/core/src/auth/credentials/utils.ts index 891e17a6486..28fa5df1c4c 100644 --- a/packages/core/src/auth/credentials/utils.ts +++ b/packages/core/src/auth/credentials/utils.ts @@ -10,7 +10,6 @@ import * as vscode from 'vscode' import { Credentials } from '@aws-sdk/types' import { authHelpUrl } from '../../shared/constants' import globals from '../../shared/extensionGlobals' -import { isCloud9 } from '../../shared/extensionUtilities' import { messages, showMessageWithCancel, showViewLogsMessage } from '../../shared/utilities/messages' import { Timeout, waitTimeout } from '../../shared/utilities/timeoutUtils' import { fromExtensionManifest } from '../../shared/settings' @@ -37,8 +36,8 @@ export function asEnvironmentVariables(credentials: Credentials): NodeJS.Process export function showLoginFailedMessage(credentialsId: string, errMsg: string): void { const getHelp = localize('AWS.generic.message.getHelp', 'Get Help...') const editCreds = messages.editCredentials(false) - // TODO: getHelp page for Cloud9. - const buttons = isCloud9() ? [editCreds] : [editCreds, getHelp] + // TODO: Any work towards web/another cloud9 -esqe IDE may need different getHelp docs: + const buttons = [editCreds, getHelp] void showViewLogsMessage( localize('AWS.message.credentials.invalid', 'Credentials "{0}" failed to connect: {1}', credentialsId, errMsg), diff --git a/packages/core/src/auth/sso/model.ts b/packages/core/src/auth/sso/model.ts index 2a05692148e..6cc462d8a57 100644 --- a/packages/core/src/auth/sso/model.ts +++ b/packages/core/src/auth/sso/model.ts @@ -15,7 +15,6 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils' import { ssoAuthHelpUrl } from '../../shared/constants' import { openUrl } from '../../shared/utilities/vsCodeUtils' import { ToolkitError } from '../../shared/errors' -import { isCloud9 } from '../../shared/extensionUtilities' import { builderIdStartUrl } from './constants' export interface SsoToken { @@ -116,10 +115,7 @@ export async function openSsoPortalLink(startUrl: string, authorization: Authori async function showLoginNotification() { const name = startUrl === builderIdStartUrl ? localizedText.builderId() : localizedText.iamIdentityCenterFull() - // C9 doesn't support `detail` field with modals so we need to put it all in the `title` - const title = isCloud9() - ? `Confirm Code "${authorization.userCode}" for ${name} in the browser.` - : localize('AWS.auth.loginWithBrowser.messageTitle', 'Confirm Code for {0}', name) + const title = localize('AWS.auth.loginWithBrowser.messageTitle', 'Confirm Code for {0}', name) const detail = localize( 'AWS.auth.loginWithBrowser.messageDetail', 'Confirm this code in the browser: {0}', diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index d160ce4b490..dd008a55fb4 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -20,7 +20,7 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { createInputBox } from '../shared/ui/inputPrompter' import { CredentialSourceId, telemetry } from '../shared/telemetry/telemetry' import { createCommonButtons, createExitButton, createHelpButton, createRefreshButton } from '../shared/ui/buttons' -import { getIdeProperties, isAmazonQ, isCloud9 } from '../shared/extensionUtilities' +import { getIdeProperties, isAmazonQ } from '../shared/extensionUtilities' import { addScopes, getDependentAuths } from './secondaryAuth' import { DevSettings } from '../shared/settings' import { createRegionPrompter } from '../shared/ui/common/region' @@ -562,9 +562,9 @@ export class AuthNode implements TreeNode { if (conn !== undefined && conn.state !== 'valid') { item.iconPath = getIcon('vscode-error') if (conn.state === 'authenticating') { - this.setDescription(item, 'authenticating...') + item.description = 'authenticating...' } else { - this.setDescription(item, 'expired or invalid, click to authenticate') + item.description = 'expired or invalid, click to authenticate' item.command = { title: 'Reauthenticate', command: '_aws.toolkit.auth.reauthenticate', @@ -578,14 +578,6 @@ export class AuthNode implements TreeNode { return item } - - private setDescription(item: vscode.TreeItem, text: string) { - if (isCloud9()) { - item.tooltip = item.tooltip ?? text - } else { - item.description = text - } - } } export async function hasIamCredentials( diff --git a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts index f17bec9213a..511217481b3 100644 --- a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts +++ b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts @@ -57,7 +57,7 @@ export async function getFiles( return await vscode.workspace.findFiles(globPattern, excludePattern) } catch (error) { - getLogger().error(`Failed to get files with pattern ${pattern}:`, error) + getLogger().error(`Failed to find files with pattern ${pattern}:`, error) return [] } } diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts index 5f8c6b4a81e..d7d5e51bb51 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts @@ -73,7 +73,7 @@ export class AppNode implements TreeNode { createPlaceholderItem( localize( 'AWS.appBuilder.explorerNode.app.noResourceTree', - '[Unable to load Resource tree for this App. Update SAM template]' + '[Unable to load resource tree for this app. Ensure SAM template is correct.]' ) ), ] diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts index 913cdd067e0..cb0d1a669c8 100644 --- a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts +++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts @@ -96,7 +96,7 @@ export async function generateDeployedNode( .Configuration as Lambda.FunctionConfiguration newDeployedResource = new LambdaFunctionNode(lambdaNode, regionCode, configuration) } catch (error: any) { - getLogger().error('Error getting Lambda configuration %O', error) + getLogger().error('Error getting Lambda configuration: %O', error) throw ToolkitError.chain(error, 'Error getting Lambda configuration', { code: 'lambdaClientError', }) @@ -107,7 +107,7 @@ export async function generateDeployedNode( createPlaceholderItem( localize( 'AWS.appBuilder.explorerNode.unavailableDeployedResource', - '[Failed to retrive deployed resource.]' + '[Failed to retrive deployed resource. Ensure your AWS account is connected.]' ) ), ] @@ -119,8 +119,8 @@ export async function generateDeployedNode( try { v3configuration = (await v3Client.send(v3command)).Configuration as FunctionConfiguration logGroupName = v3configuration.LoggingConfig?.LogGroup - } catch { - getLogger().error('Error getting Lambda V3 configuration') + } catch (error: any) { + getLogger().error('Error getting Lambda V3 configuration: %O', error) } newDeployedResource.configuration = { ...newDeployedResource.configuration, @@ -156,7 +156,10 @@ export async function generateDeployedNode( getLogger().info('Details are missing or are incomplete for: %O', deployedResource) return [ createPlaceholderItem( - localize('AWS.appBuilder.explorerNode.noApps', '[This resource is not yet supported.]') + localize( + 'AWS.appBuilder.explorerNode.noApps', + '[This resource is not yet supported in AppBuilder.]' + ) ), ] } @@ -166,7 +169,7 @@ export async function generateDeployedNode( createPlaceholderItem( localize( 'AWS.appBuilder.explorerNode.unavailableDeployedResource', - '[Failed to retrive deployed resource.]' + '[Failed to retrieve deployed resource. Ensure correct stack name and region are in the samconfig.toml, and that your account is connected.]' ) ), ] diff --git a/packages/core/src/awsService/appBuilder/explorer/samProject.ts b/packages/core/src/awsService/appBuilder/explorer/samProject.ts index fd571cd6be8..ce8d0c4878a 100644 --- a/packages/core/src/awsService/appBuilder/explorer/samProject.ts +++ b/packages/core/src/awsService/appBuilder/explorer/samProject.ts @@ -42,14 +42,17 @@ export async function getStackName(projectRoot: vscode.Uri): Promise { } catch (error: any) { switch (error.code) { case SamConfigErrorCode.samNoConfigFound: - getLogger().info('No stack name or region information available in samconfig.toml: %O', error) + getLogger().info('Stack name and/or region information not found in samconfig.toml: %O', error) break case SamConfigErrorCode.samConfigParseError: - getLogger().error(`Error getting stack name or region information: ${error.message}`, error) + getLogger().error( + `Error parsing stack name and/or region information from samconfig.toml: ${error.message}. Ensure the information is correct.`, + error + ) void showViewLogsMessage('Encountered an issue reading samconfig.toml') break default: - getLogger().warn(`Error getting stack name or region information: ${error.message}`, error) + getLogger().warn(`Error parsing stack name and/or region information: ${error.message}`, error) } return {} } diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts index de3dee8770d..63b116b20eb 100644 --- a/packages/core/src/awsService/appBuilder/utils.ts +++ b/packages/core/src/awsService/appBuilder/utils.ts @@ -24,14 +24,14 @@ const localize = nls.loadMessageBundle() export async function runOpenTemplate(arg?: TreeNode) { const templateUri = arg ? (arg.resource as SamAppLocation).samTemplateUri : await promptUserForTemplate() if (!templateUri || !(await fs.exists(templateUri))) { - throw new ToolkitError('No template provided', { code: 'NoTemplateProvided' }) + throw new ToolkitError('SAM Template not found, cannot open template', { code: 'NoTemplateProvided' }) } const document = await vscode.workspace.openTextDocument(templateUri) await vscode.window.showTextDocument(document) } /** - * Find and open the lambda handler with given ResoruceNode + * Find and open the lambda handler with given ResourceNode * If not found, a NoHandlerFound error will be raised * @param arg ResourceNode */ @@ -56,9 +56,12 @@ export async function runOpenHandler(arg: ResourceNode): Promise { arg.resource.resource.Runtime ) if (!handlerFile) { - throw new ToolkitError(`No handler file found with name "${arg.resource.resource.Handler}"`, { - code: 'NoHandlerFound', - }) + throw new ToolkitError( + `No handler file found with name "${arg.resource.resource.Handler}". Ensure the file exists in the expected location."`, + { + code: 'NoHandlerFound', + } + ) } await vscode.workspace.openTextDocument(handlerFile).then(async (doc) => await vscode.window.showTextDocument(doc)) } @@ -90,7 +93,7 @@ export async function getLambdaHandlerFile( ): Promise { const family = getFamily(runtime) if (!supportedRuntimeForHandler.has(family)) { - throw new ToolkitError(`Runtime ${runtime} is not supported for open handler button`, { + throw new ToolkitError(`Runtime ${runtime} is not supported for the 'Open handler' button`, { code: 'RuntimeNotSupported', }) } diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts index 04f43d61878..26760d896aa 100644 --- a/packages/core/src/awsService/appBuilder/walkthrough.ts +++ b/packages/core/src/awsService/appBuilder/walkthrough.ts @@ -148,13 +148,13 @@ export async function getTutorial( const appSelected = appMap.get(project + runtime) telemetry.record({ action: project + runtime, source: source ?? 'AppBuilderWalkthrough' }) if (!appSelected) { - throw new ToolkitError(`Tried to get template '${project}+${runtime}', but it hasn't been registered.`) + throw new ToolkitError(`Template '${project}+${runtime}' does not exist, choose another template.`) } try { await getPattern(serverlessLandOwner, serverlessLandRepo, appSelected.asset, outputDir, true) } catch (error) { - throw new ToolkitError(`Error occurred while fetching the pattern from serverlessland: ${error}`) + throw new ToolkitError(`An error occurred while fetching this pattern from Serverless Land: ${error}`) } } @@ -190,7 +190,7 @@ export async function genWalkthroughProject( 'No' ) if (choice !== 'Yes') { - throw new ToolkitError(`${defaultTemplateName} already exist`) + throw new ToolkitError(`A file named ${defaultTemplateName} already exists in this path.`) } } @@ -256,9 +256,9 @@ export async function initWalkthroughProjectCommand() { let runtimeSelected: TutorialRuntimeOptions | undefined = undefined try { if (!walkthroughSelected || !(typeof walkthroughSelected === 'string')) { - getLogger().info('exit on no walkthrough selected') + getLogger().info('No walkthrough selected - exiting') void vscode.window.showErrorMessage( - localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Please select a template first') + localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Select a template in the walkthrough.') ) return } @@ -322,7 +322,7 @@ export async function getOrUpdateOrInstallSAMCli(source: string) { } } } catch (err) { - throw ToolkitError.chain(err, 'Failed to install or detect SAM') + throw ToolkitError.chain(err, 'Failed to install or detect SAM.') } finally { telemetry.record({ source: source, toolId: 'sam-cli' }) } diff --git a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts index a2eabbe6d2d..4eb3c4bcf5b 100644 --- a/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts +++ b/packages/core/src/awsService/apprunner/explorer/apprunnerNode.ts @@ -93,7 +93,7 @@ export class AppRunnerNode extends AWSTreeNodeBase { } public startPollingNode(id: string): void { - this.pollingSet.start(id) + this.pollingSet.add(id) } public stopPollingNode(id: string): void { diff --git a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts index 854e6eacd1c..aa4115259c9 100644 --- a/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts +++ b/packages/core/src/awsService/ec2/explorer/ec2ParentNode.ts @@ -45,7 +45,7 @@ export class Ec2ParentNode extends AWSTreeNodeBase { if (!this.ec2InstanceNodes.has(instanceId)) { throw new Error(`Attempt to track ec2 node ${instanceId} that isn't a child`) } - this.pollingSet.start(instanceId) + this.pollingSet.add(instanceId) } public async updateChildren(): Promise { diff --git a/packages/core/src/awsService/s3/explorer/s3FileNode.ts b/packages/core/src/awsService/s3/explorer/s3FileNode.ts index 31b6429d265..1d4b3034c08 100644 --- a/packages/core/src/awsService/s3/explorer/s3FileNode.ts +++ b/packages/core/src/awsService/s3/explorer/s3FileNode.ts @@ -12,7 +12,6 @@ import { inspect } from 'util' import { S3BucketNode } from './s3BucketNode' import { S3FolderNode } from './s3FolderNode' import globals from '../../../shared/extensionGlobals' -import { isCloud9 } from '../../../shared/extensionUtilities' import { getIcon } from '../../../shared/icons' import { formatLocalized, getRelativeDate } from '../../../shared/datetime' @@ -42,13 +41,11 @@ export class S3FileNode extends AWSTreeNodeBase implements AWSResourceNode { } this.iconPath = getIcon('vscode-file') this.contextValue = 'awsS3FileNode' - this.command = !isCloud9() - ? { - command: 'aws.s3.openFile', - title: localize('AWS.command.s3.openFile', 'Open File'), - arguments: [this], - } - : undefined + this.command = { + command: 'aws.s3.openFile', + title: localize('AWS.command.s3.openFile', 'Open File'), + arguments: [this], + } } /** diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts index 5ea7295bf98..224bbadb2fb 100644 --- a/packages/core/src/awsexplorer/activation.ts +++ b/packages/core/src/awsexplorer/activation.ts @@ -110,22 +110,20 @@ export async function activate(args: { ) const amazonQViewNode: ToolView[] = [] - if (!isCloud9()) { - if ( - isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq) || - globals.globalState.get('aws.toolkit.amazonq.dismissed') - ) { - await setContext('aws.toolkit.amazonq.dismissed', true) - } - - // We should create the tree even if it's dismissed, in case the user installs Amazon Q later. - amazonQViewNode.push({ - nodes: [AmazonQNode.instance], - view: 'aws.amazonq.codewhisperer', - refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode], - }) + if ( + isExtensionInstalled(VSCODE_EXTENSION_ID.amazonq) || + globals.globalState.get('aws.toolkit.amazonq.dismissed') + ) { + await setContext('aws.toolkit.amazonq.dismissed', true) } + // We should create the tree even if it's dismissed, in case the user installs Amazon Q later. + amazonQViewNode.push({ + nodes: [AmazonQNode.instance], + view: 'aws.amazonq.codewhisperer', + refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode], + }) + const viewNodes: ToolView[] = [ ...amazonQViewNode, ...codecatalystViewNode, diff --git a/packages/core/src/awsexplorer/regionNode.ts b/packages/core/src/awsexplorer/regionNode.ts index 5e6e0b06d52..a5c5bafe104 100644 --- a/packages/core/src/awsexplorer/regionNode.ts +++ b/packages/core/src/awsexplorer/regionNode.ts @@ -30,7 +30,6 @@ import { getEcsRootNode } from '../awsService/ecs/model' import { compareTreeItems, TreeShim } from '../shared/treeview/utils' import { Ec2ParentNode } from '../awsService/ec2/explorer/ec2ParentNode' import { Ec2Client } from '../shared/clients/ec2Client' -import { isCloud9 } from '../shared/extensionUtilities' import { Experiments } from '../shared/settings' interface ServiceNode { @@ -74,7 +73,6 @@ const serviceCandidates: ServiceNode[] = [ createFn: (regionCode: string) => new EcrNode(new DefaultEcrClient(regionCode)), }, { - when: () => !isCloud9(), serviceId: 'redshift', createFn: (regionCode: string) => new RedshiftNode(new DefaultRedshiftClient(regionCode)), }, diff --git a/packages/core/src/awsexplorer/toolView.ts b/packages/core/src/awsexplorer/toolView.ts index e3417f25521..99d20fbdb3a 100644 --- a/packages/core/src/awsexplorer/toolView.ts +++ b/packages/core/src/awsexplorer/toolView.ts @@ -5,8 +5,6 @@ import * as vscode from 'vscode' import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceTreeDataProvider' -import { isCloud9 } from '../shared/extensionUtilities' -import { debounce } from '../shared/utilities/functionUtils' export interface ToolView { nodes: TreeNode[] @@ -26,20 +24,8 @@ export function createToolView(viewNode: ToolView): vscode.TreeView { for (const refreshCommand of viewNode.refreshCommands ?? []) { refreshCommand(treeDataProvider) } - const view = vscode.window.createTreeView(viewNode.view, { treeDataProvider }) - // Cloud9 will only refresh when refreshing the entire tree - if (isCloud9()) { - for (const node of viewNode.nodes) { - // Refreshes are delayed to guard against excessive calls to `getTreeItem` and `getChildren` - // The 10ms delay is arbitrary. A single event loop may be good enough in many scenarios. - const refresh = debounce(() => treeDataProvider.refresh(node), 10) - node.onDidChangeTreeItem?.(() => refresh()) - node.onDidChangeChildren?.(() => refresh()) - } - } - - return view + return vscode.window.createTreeView(viewNode.view, { treeDataProvider }) } async function getChildren(roots: TreeNode[]) { diff --git a/packages/core/src/codecatalyst/activation.ts b/packages/core/src/codecatalyst/activation.ts index 3e47dd53879..db5ff9e12f0 100644 --- a/packages/core/src/codecatalyst/activation.ts +++ b/packages/core/src/codecatalyst/activation.ts @@ -16,7 +16,7 @@ import { DevEnvClient } from '../shared/clients/devenvClient' import { watchRestartingDevEnvs } from './reconnect' import { ToolkitPromptSettings } from '../shared/settings' import { dontShow } from '../shared/localizedText' -import { getIdeProperties, isCloud9 } from '../shared/extensionUtilities' +import { getIdeProperties } from '../shared/extensionUtilities' import { Commands } from '../shared/vscode/commands2' import { getCodeCatalystConfig } from '../shared/clients/codecatalystClient' import { isDevenvVscode } from './utils' @@ -78,23 +78,21 @@ export async function activate(ctx: ExtContext): Promise { }) ) - if (!isCloud9()) { - await GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then((disposable) => { - ctx.extensionContext.subscriptions.push(disposable) - }) + await GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then((disposable) => { + ctx.extensionContext.subscriptions.push(disposable) + }) - await GitExtension.instance - .registerCredentialsProvider({ - getCredentials(uri: vscode.Uri) { - if (uri.authority.endsWith(getCodeCatalystConfig().gitHostname)) { - return commands.withClient((client) => authProvider.getCredentialsForGit(client)) - } - }, - }) - .then((disposable) => ctx.extensionContext.subscriptions.push(disposable)) + await GitExtension.instance + .registerCredentialsProvider({ + getCredentials(uri: vscode.Uri) { + if (uri.authority.endsWith(getCodeCatalystConfig().gitHostname)) { + return commands.withClient((client) => authProvider.getCredentialsForGit(client)) + } + }, + }) + .then((disposable) => ctx.extensionContext.subscriptions.push(disposable)) - watchRestartingDevEnvs(ctx, authProvider) - } + watchRestartingDevEnvs(ctx, authProvider) const thisDevenv = (await getThisDevEnv(authProvider))?.unwrapOrElse((err) => { getLogger().error('codecatalyst: failed to get current Dev Enviroment: %s', err) @@ -116,9 +114,10 @@ export async function activate(ctx: ExtContext): Promise { const timeoutMin = thisDevenv.summary.inactivityTimeoutMinutes const timeout = timeoutMin === 0 ? 'never' : `${timeoutMin} min` getLogger().info('codecatalyst: Dev Environment timeout=%s, ides=%O', timeout, thisDevenv.summary.ides) - if (!isCloud9() && thisDevenv && !isDevenvVscode(thisDevenv.summary.ides)) { + if (thisDevenv && !isDevenvVscode(thisDevenv.summary.ides)) { // Prevent Toolkit from reconnecting to a "non-vscode" devenv by actively closing it. // Can happen if devenv is switched to ides="cloud9", etc. + // TODO: Is this needed without cloud9 check? void vscode.commands.executeCommand('workbench.action.remote.close') return } @@ -148,10 +147,6 @@ export async function activate(ctx: ExtContext): Promise { } async function showReadmeFileOnFirstLoad(workspaceState: vscode.ExtensionContext['workspaceState']): Promise { - if (isCloud9()) { - return - } - getLogger().debug('codecatalyst: showReadmeFileOnFirstLoad()') // Check dev env state to see if this is the first time the user has connected to a dev env const isFirstLoad = workspaceState.get('aws.codecatalyst.devEnv.isFirstLoad', true) diff --git a/packages/core/src/codecatalyst/explorer.ts b/packages/core/src/codecatalyst/explorer.ts index a2239d41d89..d20fcc6d37d 100644 --- a/packages/core/src/codecatalyst/explorer.ts +++ b/packages/core/src/codecatalyst/explorer.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode' import { DevEnvironment } from '../shared/clients/codecatalystClient' -import { isCloud9 } from '../shared/extensionUtilities' import { addColor, getIcon } from '../shared/icons' import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { Commands } from '../shared/vscode/commands2' @@ -21,7 +20,6 @@ export const learnMoreCommand = Commands.declare('aws.learnMore', () => async (d return openUrl(docsUrl) }) -// Only used in rare cases on C9 export const reauth = Commands.declare( '_aws.codecatalyst.reauthenticate', () => async (conn: SsoConnection, authProvider: CodeCatalystAuthenticationProvider) => { @@ -37,7 +35,7 @@ export const onboardCommand = Commands.declare( ) async function getLocalCommands(auth: CodeCatalystAuthenticationProvider) { - const docsUrl = isCloud9() ? codecatalyst.docs.cloud9.overview : codecatalyst.docs.vscode.overview + const docsUrl = codecatalyst.docs.overview const learnMoreNode = learnMoreCommand.build(docsUrl).asTreeNode({ label: 'Learn more about CodeCatalyst', iconPath: getIcon('vscode-question'), @@ -75,15 +73,6 @@ async function getLocalCommands(auth: CodeCatalystAuthenticationProvider) { ] } - if (isCloud9()) { - const item = reauth.build(auth.activeConnection, auth).asTreeNode({ - label: 'Failed to get the current Dev Environment. Click to try again.', - iconPath: addColor(getIcon(`vscode-error`), 'notificationsErrorIcon.foreground'), - }) - - return [item] - } - return [ CodeCatalystCommands.declared.cloneRepo.build().asTreeNode({ label: 'Clone Repository', diff --git a/packages/core/src/codecatalyst/model.ts b/packages/core/src/codecatalyst/model.ts index 768a97890ee..d49d8780cde 100644 --- a/packages/core/src/codecatalyst/model.ts +++ b/packages/core/src/codecatalyst/model.ts @@ -35,49 +35,17 @@ export type DevEnvironmentId = Pick export const connectScriptPrefix = 'codecatalyst_connect' export const docs = { - vscode: { - main: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-service'), - overview: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-overview.html' - ), - devenv: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-devenvironment.html' - ), - setup: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-setup.html' - ), - troubleshoot: vscode.Uri.parse( - 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-troubleshoot.html' - ), - }, - cloud9: { - // Working with Amazon CodeCatalyst - main: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-cloud9'), - // Getting Started - overview: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-cloud9-getstarted' - ), - // Opening Dev Environment settings in AWS Cloud9 - settings: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-settings-cloud9'), - // Resuming a Dev Environment in AWS Cloud9 - devenv: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-resume-cloud9'), - // Creating a Dev Environment in AWS Cloud9 - devenvCreate: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-create-cloud9' - ), - // Stopping a Dev Environment in AWS Cloud9 - devenvStop: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-stop-cloud9'), - // Deleting a Dev Environment in AWS Cloud9 - devenvDelete: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-delete-cloud9' - ), - // Editing the repo devfile for a Dev Environment in AWS Cloud9 - devfileEdit: vscode.Uri.parse( - 'https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-edit-devfile-cloud9' - ), - // Cloning a repository in AWS Cloud9 - cloneRepo: vscode.Uri.parse('https://docs.aws.amazon.com/cloud9/latest/user-guide/ide-toolkits-clone-cloud9'), - }, + main: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-service'), + overview: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-overview.html' + ), + devenv: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-devenvironment.html' + ), + setup: vscode.Uri.parse('https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-setup.html'), + troubleshoot: vscode.Uri.parse( + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/codecatalyst-troubleshoot.html' + ), } as const export function getCodeCatalystSsmEnv(region: string, ssmPath: string, devenv: DevEnvironmentId): NodeJS.ProcessEnv { diff --git a/packages/core/src/codecatalyst/reconnect.ts b/packages/core/src/codecatalyst/reconnect.ts index 63a932c7d1a..ecedbd3eb04 100644 --- a/packages/core/src/codecatalyst/reconnect.ts +++ b/packages/core/src/codecatalyst/reconnect.ts @@ -180,10 +180,9 @@ async function pollDevEnvs( // Don't watch this devenv, it is already being re-opened in SSH. delete devenvs[id] } else if (!isDevenvVscode(metadata.ides)) { - // Technically vscode _can_ connect to a ideRuntime=jetbrains/cloud9 devenv, but - // we refuse to anyway so that the experience is consistent with other IDEs - // (jetbrains/cloud9) which are not capable of connecting to a devenv that lacks - // their runtime/bootstrap files. + // Technically vscode _can_ connect to a ideRuntime=jetbrains devenv, but + // we refuse to anyway so that the experience is consistent since that devenv + // is not capable of connecting to a devenv that lacks their runtime/bootstrap files. const ide = metadata.ides?.[0] const toIde = ide ? ` to "${ide.name}"` : '' progress.report({ message: `Dev Environment ${devenvName} was switched${toIde}` }) diff --git a/packages/core/src/codecatalyst/vue/configure/backend.ts b/packages/core/src/codecatalyst/vue/configure/backend.ts index e2ff928f2c6..e5dd360cac7 100644 --- a/packages/core/src/codecatalyst/vue/configure/backend.ts +++ b/packages/core/src/codecatalyst/vue/configure/backend.ts @@ -25,7 +25,6 @@ import { updateDevfileCommand } from '../../devfile' import { showViewLogsMessage } from '../../../shared/utilities/messages' import { isLongReconnect, removeReconnectionInformation, saveReconnectionInformation } from '../../reconnect' import { CodeCatalystClient, DevEnvironment } from '../../../shared/clients/codecatalystClient' -import { isCloud9 } from '../../../shared/extensionUtilities' const localize = nls.loadMessageBundle() @@ -165,7 +164,7 @@ export async function showConfigureDevEnv( activePanel ??= new Panel(ctx, client, devenv, commands) const webview = await activePanel.show({ title: localize('AWS.view.configureDevEnv.title', 'Dev Environment Settings'), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codecatalyst/vue/create/backend.ts b/packages/core/src/codecatalyst/vue/create/backend.ts index d2531b12923..bdf49419243 100644 --- a/packages/core/src/codecatalyst/vue/create/backend.ts +++ b/packages/core/src/codecatalyst/vue/create/backend.ts @@ -29,7 +29,6 @@ import { isThirdPartyRepo, } from '../../../shared/clients/codecatalystClient' import { CancellationError } from '../../../shared/utilities/timeoutUtils' -import { isCloud9 } from '../../../shared/extensionUtilities' import { telemetry } from '../../../shared/telemetry/telemetry' import { isNonNullable } from '../../../shared/utilities/tsUtils' import { createOrgPrompter, createProjectPrompter } from '../../wizards/selectResource' @@ -267,7 +266,7 @@ export async function showCreateDevEnv( const webview = await activePanel!.show({ title: localize('AWS.view.createDevEnv.title', 'Create a CodeCatalyst Dev Environment'), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codecatalyst/wizards/selectResource.ts b/packages/core/src/codecatalyst/wizards/selectResource.ts index d1c3de3328b..54e3bb20951 100644 --- a/packages/core/src/codecatalyst/wizards/selectResource.ts +++ b/packages/core/src/codecatalyst/wizards/selectResource.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import * as codecatalyst from '../../shared/clients/codecatalystClient' import { createCommonButtons, createRefreshButton } from '../../shared/ui/buttons' import { @@ -104,7 +103,7 @@ function createResourcePrompter( export function createOrgPrompter( client: codecatalyst.CodeCatalystClient ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.main : docs.vscode.main + const helpUri = docs.main return createResourcePrompter(client.listSpaces(), helpUri, { title: 'Select a CodeCatalyst Organization', placeholder: 'Search for an Organization', @@ -115,7 +114,7 @@ export function createProjectPrompter( client: codecatalyst.CodeCatalystClient, spaceName?: codecatalyst.CodeCatalystOrg['name'] ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.main : docs.vscode.main + const helpUri = docs.main const projects = spaceName ? client.listProjects({ spaceName }) : client.listResources('project') return createResourcePrompter(projects, helpUri, { @@ -129,7 +128,7 @@ export function createRepoPrompter( proj?: codecatalyst.CodeCatalystProject, thirdParty?: boolean ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.cloneRepo : docs.vscode.main + const helpUri = docs.main const repos = proj ? client.listSourceRepositories({ spaceName: proj.org.name, projectName: proj.name }, thirdParty) : client.listResources('repo', thirdParty) @@ -144,7 +143,7 @@ export function createDevEnvPrompter( client: codecatalyst.CodeCatalystClient, proj?: codecatalyst.CodeCatalystProject ): QuickPickPrompter { - const helpUri = isCloud9() ? docs.cloud9.devenv : docs.vscode.devenv + const helpUri = docs.devenv const envs = proj ? client.listDevEnvironments(proj) : client.listResources('devEnvironment') const filtered = envs.map((arr) => arr.filter((env) => isDevenvVscode(env.ides))) const isData = (obj: T | DataQuickPickItem['data']): obj is T => { diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 4ea655fc4da..17934f2fe38 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -9,7 +9,6 @@ import { getTabSizeSetting } from '../shared/utilities/editorUtilities' import { KeyStrokeHandler } from './service/keyStrokeHandler' import * as EditorContext from './util/editorContext' import * as CodeWhispererConstants from './models/constants' -import { getCompletionItems } from './service/completionProvider' import { vsCodeState, ConfigurationEntry, @@ -18,18 +17,16 @@ import { SecurityTreeViewFilterState, AggregatedCodeScanIssue, CodeScanIssue, + CodeIssueGroupingStrategyState, } from './models/model' import { invokeRecommendation } from './commands/invokeRecommendation' import { acceptSuggestion } from './commands/onInlineAcceptance' -import { resetIntelliSenseState } from './util/globalStateUtil' import { CodeWhispererSettings } from './util/codewhispererSettings' import { ExtContext } from '../shared/extensions' -import { TextEditorSelectionChangeKind } from 'vscode' import { CodeWhispererTracker } from './tracker/codewhispererTracker' import * as codewhispererClient from './client/codewhisperer' import { runtimeLanguageContext } from './util/runtimeLanguageContext' import { getLogger } from '../shared/logger' -import { isCloud9 } from '../shared/extensionUtilities' import { enableCodeSuggestions, toggleCodeSuggestions, @@ -60,6 +57,7 @@ import { ignoreAllIssues, focusIssue, showExploreAgentsView, + showCodeIssueGroupingQuickPick, } from './commands/basicCommands' import { sleep } from '../shared/utilities/timeoutUtils' import { ReferenceLogViewProvider } from './service/referenceLogViewProvider' @@ -99,6 +97,7 @@ import { SecurityIssueTreeViewProvider } from './service/securityIssueTreeViewPr import { setContext } from '../shared/vscode/setContext' import { syncSecurityIssueWebview } from './views/securityIssue/securityIssueWebview' import { detectCommentAboveLine } from '../shared/utilities/commentUtils' +import { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' let localize: nls.LocalizeFunc @@ -110,14 +109,6 @@ export async function activate(context: ExtContext): Promise { const auth = AuthUtil.instance auth.initCodeWhispererHooks() - /** - * Enable essential intellisense default settings for AWS C9 IDE - */ - - if (isCloud9()) { - await enableDefaultConfigCloud9() - } - // TODO: is this indirection useful? registerDeclaredCommands( context.extensionContext.subscriptions, @@ -129,7 +120,9 @@ export async function activate(context: ExtContext): Promise { * CodeWhisperer security panel */ const securityPanelViewProvider = new SecurityPanelViewProvider(context.extensionContext) - activateSecurityScan() + context.extensionContext.subscriptions.push( + vscode.window.registerWebviewViewProvider(SecurityPanelViewProvider.viewType, securityPanelViewProvider) + ) // TODO: this is already done in packages/core/src/extensionCommon.ts, why doesn't amazonq use that? registerCommandErrorHandler((info, error) => { @@ -288,6 +281,8 @@ export async function activate(context: ExtContext): Promise { listCodeWhispererCommands.register(), // quick pick with security issues tree filters showSecurityIssueFilters.register(), + // quick pick code issue grouping strategy + showCodeIssueGroupingQuickPick.register(), // reset security issue filters clearFilters.register(), // handle security issues tree item clicked @@ -296,6 +291,10 @@ export async function activate(context: ExtContext): Promise { SecurityTreeViewFilterState.instance.onDidChangeState((e) => { SecurityIssueTreeViewProvider.instance.refresh() }), + // refresh treeview when grouping strategy changes + CodeIssueGroupingStrategyState.instance.onDidChangeState((e) => { + SecurityIssueTreeViewProvider.instance.refresh() + }), // show a no match state SecurityIssueTreeViewProvider.instance.onDidChangeTreeData((e) => { const noMatches = @@ -481,22 +480,6 @@ export async function activate(context: ExtContext): Promise { }) } - function activateSecurityScan() { - context.extensionContext.subscriptions.push( - vscode.window.registerWebviewViewProvider(SecurityPanelViewProvider.viewType, securityPanelViewProvider) - ) - - context.extensionContext.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor((editor) => { - if (isCloud9()) { - if (editor) { - securityPanelViewProvider.setDecoration(editor, editor.document.uri) - } - } - }) - ) - } - function getAutoTriggerStatus(): boolean { return CodeSuggestionsState.instance.isSuggestionsEnabled() } @@ -517,9 +500,7 @@ export async function activate(context: ExtContext): Promise { } } - if (isCloud9()) { - setSubscriptionsforCloud9() - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { await setSubscriptionsforInlineCompletion() await AuthUtil.instance.setVscodeContextProps() } @@ -552,7 +533,7 @@ export async function activate(context: ExtContext): Promise { } CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) - + UserWrittenCodeTracker.instance.onTextDocumentChange(e) /** * Handle this keystroke event only when * 1. It is not a backspace @@ -583,80 +564,6 @@ export async function activate(context: ExtContext): Promise { ) } - function setSubscriptionsforCloud9() { - /** - * Manual trigger - */ - context.extensionContext.subscriptions.push( - vscode.languages.registerCompletionItemProvider([...CodeWhispererConstants.platformLanguageIds], { - async provideCompletionItems( - document: vscode.TextDocument, - position: vscode.Position, - token: vscode.CancellationToken, - context: vscode.CompletionContext - ) { - const completionList = new vscode.CompletionList(getCompletionItems(document, position), false) - return completionList - }, - }), - /** - * Automated trigger - */ - vscode.workspace.onDidChangeTextDocument(async (e) => { - const editor = vscode.window.activeTextEditor - if (!editor) { - return - } - if (e.document !== editor.document) { - return - } - if (!runtimeLanguageContext.isLanguageSupported(e.document)) { - return - } - /** - * CodeWhisperer security panel dynamic handling - */ - securityPanelViewProvider.disposeSecurityPanelItem(e, editor) - CodeWhispererCodeCoverageTracker.getTracker(e.document.languageId)?.countTotalTokens(e) - - if (e.contentChanges.length === 0 || vsCodeState.isCodeWhispererEditing) { - return - } - /** - * Important: Doing this sleep(10) is to make sure - * 1. this event is processed by vs code first - * 2. editor.selection.active has been successfully updated by VS Code - * Then this event can be processed by our code. - */ - await sleep(CodeWhispererConstants.vsCodeCursorUpdateDelay) - await KeyStrokeHandler.instance.processKeyStroke(e, editor, client, await getConfigEntry()) - }), - - /** - * On intelliSense recommendation rejection, reset set intelli sense is active state - * Maintaining this variable because VS Code does not expose official intelliSense isActive API - */ - vscode.window.onDidChangeVisibleTextEditors(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }), - vscode.window.onDidChangeActiveTextEditor(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }), - vscode.window.onDidChangeTextEditorSelection(async (e) => { - if (e.kind === TextEditorSelectionChangeKind.Mouse) { - resetIntelliSenseState( - true, - getAutoTriggerStatus(), - RecommendationHandler.instance.isValidResponse() - ) - } - }), - vscode.workspace.onDidSaveTextDocument(async (e) => { - resetIntelliSenseState(true, getAutoTriggerStatus(), RecommendationHandler.instance.isValidResponse()) - }) - ) - } - void FeatureConfigProvider.instance.fetchFeatureConfigs().catch((error) => { getLogger().error('Failed to fetch feature configs - %s', error) }) @@ -699,19 +606,6 @@ export async function shutdown() { await CodeWhispererTracker.getTracker().shutdown() } -export async function enableDefaultConfigCloud9() { - const editorSettings = vscode.workspace.getConfiguration('editor') - try { - await editorSettings.update('suggest.showMethods', true, vscode.ConfigurationTarget.Global) - // suggest.preview is available in vsc 1.57+ - await editorSettings.update('suggest.preview', true, vscode.ConfigurationTarget.Global) - await editorSettings.update('acceptSuggestionOnEnter', 'on', vscode.ConfigurationTarget.Global) - await editorSettings.update('snippetSuggestions', 'top', vscode.ConfigurationTarget.Global) - } catch (error) { - getLogger().error('amazonq: Failed to update user settings %O', error) - } -} - function toggleIssuesVisibility(visibleCondition: (issue: CodeScanIssue, filePath: string) => boolean) { const updatedIssues: AggregatedCodeScanIssue[] = SecurityIssueProvider.instance.issues.map((group) => ({ ...group, diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 123160fb0b3..8a847e603d7 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -626,7 +626,9 @@ "timestamp": { "shape": "Timestamp" }, "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" }, "totalNewCodeCharacterCount": { "shape": "PrimitiveInteger" }, - "totalNewCodeLineCount": { "shape": "PrimitiveInteger" } + "totalNewCodeLineCount": { "shape": "PrimitiveInteger" }, + "userWrittenCodeCharacterCount": { "shape": "PrimitiveInteger" }, + "userWrittenCodeLineCount": { "shape": "PrimitiveInteger" } } }, "CodeFixAcceptanceEvent": { diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index 999c2c53ac5..3a614b003f0 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -27,7 +27,6 @@ import { connectToEnterpriseSso, getStartUrl } from '../util/getStartUrl' import { showCodeWhispererConnectionPrompt } from '../util/showSsoPrompt' import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' import { AuthUtil } from '../util/authUtil' -import { isCloud9 } from '../../shared/extensionUtilities' import { getLogger } from '../../shared/logger' import { isExtensionActive, isExtensionInstalled, localize, openUrl } from '../../shared/utilities/vsCodeUtils' import { @@ -50,7 +49,7 @@ import { once } from '../../shared/utilities/functionUtils' import { focusAmazonQPanel } from '../../codewhispererChat/commands/registerCommands' import { removeDiagnostic } from '../service/diagnosticsProvider' import { SsoAccessTokenProvider } from '../../auth/sso/ssoAccessTokenProvider' -import { ToolkitError, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' +import { ToolkitError, getErrorMsg, getTelemetryReason, getTelemetryReasonDesc } from '../../shared/errors' import { isRemoteWorkspace } from '../../shared/vscode/env' import { isBuilderIdConnection } from '../../auth/connection' import globals from '../../shared/extensionGlobals' @@ -66,7 +65,9 @@ import { cancel, confirm } from '../../shared' import { startCodeFixGeneration } from './startCodeFixGeneration' import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' +import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters' const MessageTimeOut = 5_000 @@ -105,9 +106,7 @@ export const enableCodeSuggestions = Commands.declare( await setContext('aws.codewhisperer.connected', true) await setContext('aws.codewhisperer.connectionExpired', false) vsCodeState.isFreeTierLimitReached = false - if (!isCloud9()) { - await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') - } + await vscode.commands.executeCommand('aws.amazonq.refreshStatusBar') } ) @@ -451,6 +450,7 @@ export const applySecurityFix = Commands.declare( } let languageId = undefined try { + UserWrittenCodeTracker.instance.onQStartsMakingEdits() const document = await vscode.workspace.openTextDocument(targetFilePath) languageId = document.languageId const updatedContent = await getPatchedCode(targetFilePath, suggestedFix.code) @@ -565,6 +565,7 @@ export const applySecurityFix = Commands.declare( applyFixTelemetryEntry.result, !!targetIssue.suggestedFixes.length ) + UserWrittenCodeTracker.instance.onQFinishesEdits() } } ) @@ -677,7 +678,8 @@ export const generateFix = Commands.declare( }) await updateSecurityIssueWebview({ isGenerateFixLoading: true, - isGenerateFixError: false, + // eslint-disable-next-line unicorn/no-null + generateFixError: null, context: context.extensionContext, filePath: targetFilePath, shouldRefreshView: false, @@ -734,25 +736,27 @@ export const generateFix = Commands.declare( SecurityIssueProvider.instance.updateIssue(updatedIssue, targetFilePath) SecurityIssueTreeViewProvider.instance.refresh() } catch (err) { + const error = err instanceof Error ? err : new TypeError('Unexpected error') await updateSecurityIssueWebview({ issue: targetIssue, isGenerateFixLoading: false, - isGenerateFixError: true, + generateFixError: getErrorMsg(error, true), filePath: targetFilePath, context: context.extensionContext, - shouldRefreshView: true, + shouldRefreshView: false, }) SecurityIssueProvider.instance.updateIssue(targetIssue, targetFilePath) SecurityIssueTreeViewProvider.instance.refresh() throw err + } finally { + telemetry.record({ + component: targetSource, + detectorId: targetIssue.detectorId, + findingId: targetIssue.findingId, + ruleId: targetIssue.ruleId, + variant: refresh ? 'refresh' : undefined, + }) } - telemetry.record({ - component: targetSource, - detectorId: targetIssue.detectorId, - findingId: targetIssue.findingId, - ruleId: targetIssue.ruleId, - variant: refresh ? 'refresh' : undefined, - }) }) } ) @@ -884,6 +888,14 @@ export const showSecurityIssueFilters = Commands.declare({ id: 'aws.amazonq.secu } }) +export const showCodeIssueGroupingQuickPick = Commands.declare( + { id: 'aws.amazonq.codescan.showGroupingStrategy' }, + () => async () => { + const prompter = createCodeIssueGroupingStrategyPrompter() + await prompter.prompt() + } +) + export const focusIssue = Commands.declare( { id: 'aws.amazonq.security.focusIssue' }, () => async (issue: CodeScanIssue, filePath: string) => { diff --git a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts index 508639ac45b..37fcb965774 100644 --- a/packages/core/src/codewhisperer/commands/invokeRecommendation.ts +++ b/packages/core/src/codewhisperer/commands/invokeRecommendation.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode' import { vsCodeState, ConfigurationEntry } from '../models/model' import { resetIntelliSenseState } from '../util/globalStateUtil' import { DefaultCodeWhispererClient } from '../client/codewhisperer' -import { isCloud9 } from '../../shared/extensionUtilities' import { RecommendationHandler } from '../service/recommendationHandler' import { session } from '../util/codeWhispererSession' import { RecommendationService } from '../service/recommendationService' @@ -21,17 +20,7 @@ export async function invokeRecommendation( client: DefaultCodeWhispererClient, config: ConfigurationEntry ) { - if (!config.isManualTriggerEnabled) { - return - } - /** - * IntelliSense in Cloud9 needs editor.suggest.showMethods - */ - if (!config.isShowMethodsEnabled && isCloud9()) { - void vscode.window.showWarningMessage('Turn on "editor.suggest.showMethods" to use Amazon Q inline suggestions') - return - } - if (!editor) { + if (!editor || !config.isManualTriggerEnabled) { return } diff --git a/packages/core/src/codewhisperer/commands/onAcceptance.ts b/packages/core/src/codewhisperer/commands/onAcceptance.ts index 7ed36cde581..e13c197cefd 100644 --- a/packages/core/src/codewhisperer/commands/onAcceptance.ts +++ b/packages/core/src/codewhisperer/commands/onAcceptance.ts @@ -9,7 +9,6 @@ import { runtimeLanguageContext } from '../util/runtimeLanguageContext' import { CodeWhispererTracker } from '../tracker/codewhispererTracker' import { CodeWhispererCodeCoverageTracker } from '../tracker/codewhispererCodeCoverageTracker' import { getLogger } from '../../shared/logger/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { handleExtraBrackets } from '../util/closingBracketUtil' import { RecommendationHandler } from '../service/recommendationHandler' import { ReferenceLogViewProvider } from '../service/referenceLogViewProvider' @@ -30,7 +29,7 @@ export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEn path.extname(acceptanceEntry.editor.document.fileName) ) const start = acceptanceEntry.range.start - const end = isCloud9() ? acceptanceEntry.editor.selection.active : acceptanceEntry.range.end + const end = acceptanceEntry.range.end // codewhisperer will be doing editing while formatting. // formatting should not trigger consoals auto trigger @@ -45,13 +44,8 @@ export async function onAcceptance(acceptanceEntry: OnRecommendationAcceptanceEn } // move cursor to end of suggestion before doing code format // after formatting, the end position will still be editor.selection.active - if (!isCloud9()) { - acceptanceEntry.editor.selection = new vscode.Selection(end, end) - } + acceptanceEntry.editor.selection = new vscode.Selection(end, end) - if (isCloud9()) { - vsCodeState.isIntelliSenseActive = false - } vsCodeState.isCodeWhispererEditing = false CodeWhispererTracker.getTracker().enqueue({ time: new Date(), diff --git a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts index 3fd91d0f996..da581d1aacc 100644 --- a/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts +++ b/packages/core/src/codewhisperer/commands/onInlineAcceptance.ts @@ -32,6 +32,7 @@ import { RecommendationService } from '../service/recommendationService' import { Container } from '../service/serviceContainer' import { telemetry } from '../../shared/telemetry' import { TelemetryHelper } from '../util/telemetryHelper' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' export const acceptSuggestion = Commands.declare( 'aws.amazonq.accept', @@ -126,6 +127,7 @@ export async function onInlineAcceptance(acceptanceEntry: OnRecommendationAccept acceptanceEntry.editor.document.getText(insertedCoderange), acceptanceEntry.editor.document.fileName ) + UserWrittenCodeTracker.instance.onQFinishesEdits() if (acceptanceEntry.references !== undefined) { const referenceLog = ReferenceLogViewProvider.getReferenceLog( acceptanceEntry.recommendation, diff --git a/packages/core/src/codewhisperer/commands/startSecurityScan.ts b/packages/core/src/codewhisperer/commands/startSecurityScan.ts index 698b9792187..ce91b0d6edd 100644 --- a/packages/core/src/codewhisperer/commands/startSecurityScan.ts +++ b/packages/core/src/codewhisperer/commands/startSecurityScan.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { ArtifactMap, DefaultCodeWhispererClient } from '../client/codewhisperer' -import { isCloud9 } from '../../shared/extensionUtilities' import { initSecurityScanRender } from '../service/diagnosticsProvider' import { SecurityPanelViewProvider } from '../views/securityPanelViewProvider' import { getLogger } from '../../shared/logger' @@ -291,16 +290,7 @@ export async function startSecurityScan( scanUuid ) } else { - showSecurityScanResults( - securityPanelViewProvider, - securityRecommendationCollection, - editor, - context, - scope, - zipMetadata, - total, - scanUuid - ) + showSecurityScanResults(securityRecommendationCollection, editor, context, scope, zipMetadata, total) } TelemetryHelper.instance.sendCodeScanSucceededEvent( codeScanTelemetryEntry.codewhispererLanguage, @@ -387,28 +377,22 @@ export async function startSecurityScan( } export function showSecurityScanResults( - securityPanelViewProvider: SecurityPanelViewProvider, securityRecommendationCollection: AggregatedCodeScanIssue[], editor: vscode.TextEditor | undefined, context: vscode.ExtensionContext, scope: CodeWhispererConstants.CodeAnalysisScope, zipMetadata: ZipMetadata, - totalIssues: number, - scanUuid: string | undefined + totalIssues: number ) { - if (isCloud9()) { - securityPanelViewProvider.addLines(securityRecommendationCollection, editor) - void vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-security-panel') - } else { - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) - if ( - totalIssues > 0 && - (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT || - scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND) - ) { - SecurityIssuesTree.instance.focus() - } + initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + if ( + totalIssues > 0 && + (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT || + scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND) + ) { + SecurityIssuesTree.instance.focus() } + if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { populateCodeScanLogStream(zipMetadata.scannedFiles) } @@ -424,35 +408,32 @@ export function showScanResultsInChat( totalIssues: number, scanUuid: string | undefined ) { - if (isCloud9()) { - securityPanelViewProvider.addLines(securityRecommendationCollection, editor) - void vscode.commands.executeCommand('workbench.view.extension.aws-codewhisperer-security-panel') - } else { - const tabID = ChatSessionManager.Instance.getSession().tabID - const eventData = { - message: 'Show Findings in the Chat panel', - totalIssues, - securityRecommendationCollection, - fileName: scope === CodeAnalysisScope.FILE_ON_DEMAND ? [...zipMetadata.scannedFiles][0] : undefined, - tabID, - scope, - scanUuid, - } - switch (scope) { - case CodeAnalysisScope.PROJECT: - codeScanState.getChatControllers()?.showSecurityScan.fire(eventData) - break - case CodeAnalysisScope.FILE_ON_DEMAND: - onDemandFileScanState.getChatControllers()?.showSecurityScan.fire(eventData) - break - default: - break - } - initSecurityScanRender(securityRecommendationCollection, context, editor, scope) - if (totalIssues > 0) { - SecurityIssuesTree.instance.focus() - } + const tabID = ChatSessionManager.Instance.getSession().tabID + const eventData = { + message: 'Show Findings in the Chat panel', + totalIssues, + securityRecommendationCollection, + fileName: scope === CodeAnalysisScope.FILE_ON_DEMAND ? [...zipMetadata.scannedFiles][0] : undefined, + tabID, + scope, + scanUuid, + } + switch (scope) { + case CodeAnalysisScope.PROJECT: + codeScanState.getChatControllers()?.showSecurityScan.fire(eventData) + break + case CodeAnalysisScope.FILE_ON_DEMAND: + onDemandFileScanState.getChatControllers()?.showSecurityScan.fire(eventData) + break + default: + break } + + initSecurityScanRender(securityRecommendationCollection, context, editor, scope) + if (totalIssues > 0) { + SecurityIssuesTree.instance.focus() + } + populateCodeScanLogStream(zipMetadata.scannedFiles) if (scope === CodeAnalysisScope.PROJECT) { showScanCompletedNotification(totalIssues, zipMetadata.scannedFiles) diff --git a/packages/core/src/codewhisperer/commands/startTestGeneration.ts b/packages/core/src/codewhisperer/commands/startTestGeneration.ts index 8480ee3184e..429e3585d36 100644 --- a/packages/core/src/codewhisperer/commands/startTestGeneration.ts +++ b/packages/core/src/codewhisperer/commands/startTestGeneration.ts @@ -21,7 +21,7 @@ import { ChildProcess, spawn } from 'child_process' // eslint-disable-line no-re import { BuildStatus } from '../../amazonqTest/chat/session/session' import { fs } from '../../shared/fs/fs' import { TestGenerationJobStatus } from '../models/constants' -import { TestGenFailedError } from '../models/errors' +import { TestGenFailedError } from '../../amazonqTest/error' import { Range } from '../client/codewhispereruserclient' // eslint-disable-next-line unicorn/no-null @@ -75,8 +75,9 @@ export async function startTestGenerationProcess( try { artifactMap = await getPresignedUrlAndUploadTestGen(zipMetadata) } finally { - if (await fs.existsFile(path.join(testGenerationLogsDir, 'output.log'))) { - await fs.delete(path.join(testGenerationLogsDir, 'output.log')) + const outputLogPath = path.join(testGenerationLogsDir, 'output.log') + if (await fs.existsFile(outputLogPath)) { + await fs.delete(outputLogPath) } await zipUtil.removeTmpFiles(zipMetadata) session.artifactsUploadDuration = performance.now() - uploadStartTime diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index 235da682ec9..3aea72fb4ca 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -73,7 +73,7 @@ export { DocumentChangedSource, KeyStrokeHandler, DefaultDocumentChangedType } f export { ReferenceLogViewProvider } from './service/referenceLogViewProvider' export { LicenseUtil } from './util/licenseUtil' export { SecurityIssueProvider } from './service/securityIssueProvider' -export { listScanResults, mapToAggregatedList } from './service/securityScanHandler' +export { listScanResults, mapToAggregatedList, pollScanJobStatus } from './service/securityScanHandler' export { CodeWhispererCodeCoverageTracker } from './tracker/codewhispererCodeCoverageTracker' export { TelemetryHelper } from './util/telemetryHelper' export { LineSelection, LineTracker } from './tracker/lineTracker' @@ -97,8 +97,10 @@ export * as supplementalContextUtil from './util/supplementalContext/supplementa export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' -export { SecurityScanError } from '../codewhisperer/models/errors' +export { SecurityScanError, SecurityScanTimedOutError } from '../codewhisperer/models/errors' export * as CodeWhispererConstants from '../codewhisperer/models/constants' export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil' export { Container } from './service/serviceContainer' export * from './util/gitUtil' +export * from './ui/prompters' +export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index d0b75b204db..f0ff34dcb02 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -98,6 +98,7 @@ export const completionDetail = 'Amazon Q' export const codewhisperer = 'Amazon Q' // use vscode languageId here / Supported languages +// TODO: Dropped Cloud9 support - do we need Cloud9-commented entries here? export const platformLanguageIds = [ 'java', 'python', @@ -258,9 +259,9 @@ export const codeScanZipExt = '.zip' export const contextTruncationTimeoutSeconds = 10 -export const codeScanJobTimeoutSeconds = 60 * 10 // 10 minutes +export const standardScanTimeoutMs = 600_000 // 10 minutes -export const codeFileScanJobTimeoutSeconds = 60 * 10 // 10 minutes +export const expressScanTimeoutMs = 60_000 export const codeFixJobTimeoutMs = 60_000 @@ -305,7 +306,9 @@ export const securityScanLanguageIds = [ 'csharp', 'go', 'ruby', - 'golang', // Cloud9 reports Go files with this language-id + // Cloud9 reports Go files with this language-id + // TODO: Dropped Cloud9 support - is this still needed? + 'golang', 'json', 'yaml', 'tf', @@ -726,6 +729,8 @@ export const noOpenProjectsFoundChatTestGenMessage = `Sorry, I couldn\'t find a export const unitTestGenerationCancelMessage = 'Unit test generation cancelled.' +export const tooManyRequestErrorMessage = 'Too many requests. Please wait before retrying.' + export const noJavaProjectsFoundChatMessage = `I couldn\'t find a project that I can upgrade. Currently, I support Java 8, Java 11, and Java 17 projects built on Maven. Make sure your project is open in the IDE. For more information, see the [Amazon Q documentation](${codeTransformPrereqDoc}).` export const linkToDocsHome = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html' @@ -861,7 +866,7 @@ export enum TestGenerationJobStatus { COMPLETED = 'COMPLETED', } -export enum ZipUseCase { +export enum FeatureUseCase { TEST_GENERATION = 'TEST_GENERATION', CODE_SCAN = 'CODE_SCAN', } diff --git a/packages/core/src/codewhisperer/models/errors.ts b/packages/core/src/codewhisperer/models/errors.ts index 3fe22f22af0..9466fede54d 100644 --- a/packages/core/src/codewhisperer/models/errors.ts +++ b/packages/core/src/codewhisperer/models/errors.ts @@ -172,3 +172,13 @@ export class CodeFixJobStoppedError extends CodeFixError { super('Code fix generation stopped by user.', 'CodeFixCancelled', defaultCodeFixErrorMessage) } } + +export class MonthlyCodeFixLimitError extends CodeFixError { + constructor() { + super( + i18n('AWS.amazonq.codefix.error.monthlyLimitReached'), + MonthlyCodeFixLimitError.name, + defaultCodeFixErrorMessage + ) + } +} diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index c9b3ca7c51a..d8eea5a018c 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -20,6 +20,7 @@ import { TransformationSteps } from '../client/codewhispereruserclient' import { Messenger } from '../../amazonqGumby/chat/controller/messenger/messenger' import { TestChatControllerEventEmitters } from '../../amazonqTest/chat/controller/controller' import { ScanChatControllerEventEmitters } from '../../amazonqScan/controller' +import { localize } from '../../shared/utilities/vsCodeUtils' // unavoidable global variables interface VsCodeState { @@ -564,6 +565,52 @@ export class SecurityTreeViewFilterState { } } +export enum CodeIssueGroupingStrategy { + Severity = 'Severity', + FileLocation = 'FileLocation', +} +const defaultCodeIssueGroupingStrategy = CodeIssueGroupingStrategy.Severity + +export const codeIssueGroupingStrategies = Object.values(CodeIssueGroupingStrategy) +export const codeIssueGroupingStrategyLabel: Record = { + [CodeIssueGroupingStrategy.Severity]: localize('AWS.amazonq.scans.severity', 'Severity'), + [CodeIssueGroupingStrategy.FileLocation]: localize('AWS.amazonq.scans.fileLocation', 'File Location'), +} + +export class CodeIssueGroupingStrategyState { + #fallback: CodeIssueGroupingStrategy + #onDidChangeState = new vscode.EventEmitter() + onDidChangeState = this.#onDidChangeState.event + + static #instance: CodeIssueGroupingStrategyState + static get instance() { + return (this.#instance ??= new this()) + } + + protected constructor(fallback: CodeIssueGroupingStrategy = defaultCodeIssueGroupingStrategy) { + this.#fallback = fallback + } + + public getState(): CodeIssueGroupingStrategy { + const state = globals.globalState.tryGet('aws.amazonq.codescan.groupingStrategy', String) + return this.isValidGroupingStrategy(state) ? state : this.#fallback + } + + public async setState(_state: unknown) { + const state = this.isValidGroupingStrategy(_state) ? _state : this.#fallback + await globals.globalState.update('aws.amazonq.codescan.groupingStrategy', state) + this.#onDidChangeState.fire(state) + } + + private isValidGroupingStrategy(strategy: unknown): strategy is CodeIssueGroupingStrategy { + return Object.values(CodeIssueGroupingStrategy).includes(strategy as CodeIssueGroupingStrategy) + } + + public reset() { + return this.setState(this.#fallback) + } +} + /** * Q - Transform */ @@ -1105,12 +1152,6 @@ export class TransformByQStoppedError extends ToolkitError { } } -export enum Cloud9AccessState { - NoAccess, - RequestedAccess, - HasAccess, -} - export interface TransformationCandidateProject { name: string path: string diff --git a/packages/core/src/codewhisperer/service/codeFixHandler.ts b/packages/core/src/codewhisperer/service/codeFixHandler.ts index 0358d8d3ed9..e260f3808ea 100644 --- a/packages/core/src/codewhisperer/service/codeFixHandler.ts +++ b/packages/core/src/codewhisperer/service/codeFixHandler.ts @@ -6,13 +6,14 @@ import { CodeWhispererUserClient } from '../indexNode' import * as CodeWhispererConstants from '../models/constants' import { codeFixState } from '../models/model' -import { getLogger, sleep } from '../../shared' +import { getLogger, isAwsError, sleep } from '../../shared' import { ArtifactMap, CreateUploadUrlRequest, DefaultCodeWhispererClient } from '../client/codewhisperer' import { CodeFixJobStoppedError, CodeFixJobTimedOutError, CreateCodeFixError, CreateUploadUrlError, + MonthlyCodeFixLimitError, } from '../models/errors' import { uploadArtifactToS3 } from './securityScanHandler' @@ -28,13 +29,13 @@ export async function getPresignedUrlAndUpload( } getLogger().verbose(`Prepare for uploading src context...`) const srcResp = await client.createUploadUrl(srcReq).catch((err) => { - getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err) + getLogger().error('Failed getting presigned url for uploading src context. %O', err) + throw new CreateUploadUrlError(err.message) }) getLogger().verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) getLogger().verbose(`Complete Getting presigned Url for uploading src context.`) getLogger().verbose(`Uploading src context...`) - await uploadArtifactToS3(zipFilePath, srcResp) + await uploadArtifactToS3(zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.CODE_SCAN) getLogger().verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -60,7 +61,10 @@ export async function createCodeFixJob( } const resp = await client.startCodeFixJob(req).catch((err) => { - getLogger().error(`Failed creating code fix job. Request id: ${err.requestId}`) + getLogger().error('Failed creating code fix job. %O', err) + if (isAwsError(err) && err.code === 'ThrottlingException' && err.message.includes('reached for this month')) { + throw new MonthlyCodeFixLimitError() + } throw new CreateCodeFixError() }) getLogger().info(`AmazonQ generate fix Request id: ${resp.$response.requestId}`) diff --git a/packages/core/src/codewhisperer/service/importAdderProvider.ts b/packages/core/src/codewhisperer/service/importAdderProvider.ts index 717373148c4..98b7c36adfd 100644 --- a/packages/core/src/codewhisperer/service/importAdderProvider.ts +++ b/packages/core/src/codewhisperer/service/importAdderProvider.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import { Recommendation } from '../client/codewhisperer' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { findLineToInsertImportStatement } from '../util/importAdderUtil' @@ -74,8 +73,7 @@ export class ImportAdderProvider implements vscode.CodeLensProvider { private isNotEnabled(languageId: string): boolean { return ( !this.supportedLanguages.includes(languageId) || - !CodeWhispererSettings.instance.isImportRecommendationEnabled() || - isCloud9() + !CodeWhispererSettings.instance.isImportRecommendationEnabled() ) } diff --git a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts index e5ac2212e06..a6c424c321d 100644 --- a/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts +++ b/packages/core/src/codewhisperer/service/inlineCompletionItemProvider.ts @@ -12,6 +12,7 @@ import { ReferenceInlineProvider } from './referenceInlineProvider' import { ImportAdderProvider } from './importAdderProvider' import { application } from '../util/codeWhispererApplication' import path from 'path' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' export class CWInlineCompletionItemProvider implements vscode.InlineCompletionItemProvider { private activeItemIndex: number | undefined @@ -170,6 +171,7 @@ export class CWInlineCompletionItemProvider implements vscode.InlineCompletionIt this.nextMove = 0 TelemetryHelper.instance.setFirstSuggestionShowTime() session.setPerceivedLatency() + UserWrittenCodeTracker.instance.onQStartsMakingEdits() this._onDidShow.fire() if (matchedCount >= 2 || this.nextToken !== '') { const result = [item] diff --git a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts index 623f9aaa808..d32e875e8a4 100644 --- a/packages/core/src/codewhisperer/service/keyStrokeHandler.ts +++ b/packages/core/src/codewhisperer/service/keyStrokeHandler.ts @@ -8,7 +8,6 @@ import { DefaultCodeWhispererClient } from '../client/codewhisperer' import * as CodeWhispererConstants from '../models/constants' import { ConfigurationEntry } from '../models/model' import { getLogger } from '../../shared/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { RecommendationHandler } from './recommendationHandler' import { CodewhispererAutomatedTriggerType } from '../../shared/telemetry/telemetry' import { getTabSizeSetting } from '../../shared/utilities/editorUtilities' @@ -76,9 +75,6 @@ export class KeyStrokeHandler { } public shouldTriggerIdleTime(): boolean { - if (isCloud9() && RecommendationService.instance.isRunning) { - return false - } if (isInlineCompletionEnabled() && RecommendationService.instance.isRunning) { return false } @@ -101,14 +97,6 @@ export class KeyStrokeHandler { return } - // In Cloud9, do not auto trigger when - // 1. The input is from IntelliSense acceptance event - // 2. The input is from copy and paste some code - // event.contentChanges[0].text.length > 1 is a close estimate of 1 and 2 - if (isCloud9() && event.contentChanges.length > 0 && event.contentChanges[0].text.length > 1) { - return - } - const { rightFileContent } = extractContextForCodeWhisperer(editor) const rightContextLines = rightFileContent.split(/\r?\n/) const rightContextAtCurrentLine = rightContextLines[0] @@ -259,7 +247,7 @@ export class DefaultDocumentChangedType extends DocumentChangedType { // single line && single place reformat should consist of space chars only return DocumentChangedSource.Reformatting } else { - return isCloud9() ? DocumentChangedSource.RegularKey : DocumentChangedSource.Unknown + return DocumentChangedSource.Unknown } } diff --git a/packages/core/src/codewhisperer/service/recommendationHandler.ts b/packages/core/src/codewhisperer/service/recommendationHandler.ts index 72e130a5bed..1f5096ad1cc 100644 --- a/packages/core/src/codewhisperer/service/recommendationHandler.ts +++ b/packages/core/src/codewhisperer/service/recommendationHandler.ts @@ -14,7 +14,6 @@ import { AWSError } from 'aws-sdk' import { isAwsError } from '../../shared/errors' import { TelemetryHelper } from '../util/telemetryHelper' import { getLogger } from '../../shared/logger' -import { isCloud9 } from '../../shared/extensionUtilities' import { hasVendedIamCredentials } from '../../auth/auth' import { asyncCallWithTimeout, @@ -44,6 +43,7 @@ import { openUrl } from '../../shared/utilities/vsCodeUtils' import { indent } from '../../shared/utilities/textUtilities' import path from 'path' import { isIamConnection } from '../../auth/connection' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' /** * This class is for getRecommendation/listRecommendation API calls and its states @@ -63,9 +63,7 @@ const rejectCommand = Commands.declare('aws.amazonq.rejectCodeSuggestion', () => traceId: TelemetryHelper.instance.traceId, }) - if (!isCloud9('any')) { - await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') - } + await vscode.commands.executeCommand('editor.action.inlineSuggest.hide') RecommendationHandler.instance.reportUserDecisions(-1) await Commands.tryExecute('aws.amazonq.refreshAnnotation') }) @@ -318,6 +316,7 @@ export class RecommendationHandler { getLogger().debug(msg) if (invocationResult === 'Succeeded') { CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } else { if ( (errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') || @@ -497,9 +496,7 @@ export class RecommendationHandler { session.suggestionStates, session.requestContext.supplementalMetadata ) - if (isCloud9('any')) { - this.clearRecommendations() - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { this.clearInlineCompletionStates().catch((e) => { getLogger().error('clearInlineCompletionStates failed: %s', (e as Error).message) }) diff --git a/packages/core/src/codewhisperer/service/recommendationService.ts b/packages/core/src/codewhisperer/service/recommendationService.ts index 0a9a4e3c034..1da76995781 100644 --- a/packages/core/src/codewhisperer/service/recommendationService.ts +++ b/packages/core/src/codewhisperer/service/recommendationService.ts @@ -3,17 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { ConfigurationEntry, GetRecommendationsResponse, vsCodeState } from '../models/model' -import { isCloud9 } from '../../shared/extensionUtilities' +import { ConfigurationEntry, GetRecommendationsResponse } from '../models/model' import { isInlineCompletionEnabled } from '../util/commonUtil' import { CodewhispererAutomatedTriggerType, CodewhispererTriggerType, telemetry, } from '../../shared/telemetry/telemetry' -import { AuthUtil } from '../util/authUtil' -import { isIamConnection } from '../../auth/connection' -import { RecommendationHandler } from '../service/recommendationHandler' import { InlineCompletionService } from '../service/inlineCompletionService' import { ClassifierTrigger } from './classifierTrigger' import { DefaultCodeWhispererClient } from '../client/codewhisperer' @@ -81,67 +77,7 @@ export class RecommendationService { const traceId = telemetry.attributes?.traceId ?? randomUUID() TelemetryHelper.instance.setTraceId(traceId) await telemetry.withTraceId(async () => { - if (isCloud9('any')) { - // C9 manual trigger key alt/option + C is ALWAYS enabled because the VSC version C9 is on doesn't support setContextKey which is used for CODEWHISPERER_ENABLED - // therefore we need a connection check if there is ANY connection(regardless of the connection's state) connected to CodeWhisperer on C9 - if (triggerType === 'OnDemand' && !AuthUtil.instance.isConnected()) { - return - } - - RecommendationHandler.instance.checkAndResetCancellationTokens() - vsCodeState.isIntelliSenseActive = false - this._isRunning = true - let response: GetRecommendationsResponse = { - result: 'Failed', - errorMessage: undefined, - recommendationCount: 0, - } - - try { - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: true, - triggerType: triggerType, - response: undefined, - }) - - if (isCloud9('classic') || isIamConnection(AuthUtil.instance.conn)) { - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - false - ) - } else { - if (AuthUtil.instance.isConnectionExpired()) { - await AuthUtil.instance.showReauthenticatePrompt() - } - response = await RecommendationHandler.instance.getRecommendations( - client, - editor, - triggerType, - config, - autoTriggerType, - true - ) - } - if (RecommendationHandler.instance.canShowRecommendationInIntelliSense(editor, true, response)) { - await vscode.commands.executeCommand('editor.action.triggerSuggest').then(() => { - vsCodeState.isIntelliSenseActive = true - }) - } - } finally { - this._isRunning = false - this._onSuggestionActionEvent.fire({ - editor: editor, - isRunning: false, - triggerType: triggerType, - response: response, - }) - } - } else if (isInlineCompletionEnabled()) { + if (isInlineCompletionEnabled()) { if (triggerType === 'OnDemand') { ClassifierTrigger.instance.recordClassifierResultForManualTrigger(editor) } diff --git a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts index 2a2fe867321..9ec20b8cb44 100644 --- a/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts +++ b/packages/core/src/codewhisperer/service/referenceLogViewProvider.ts @@ -9,7 +9,6 @@ import { LicenseUtil } from '../util/licenseUtil' import * as CodeWhispererConstants from '../models/constants' import { CodeWhispererSettings } from '../util/codewhispererSettings' import globals from '../../shared/extensionGlobals' -import { isCloud9 } from '../../shared/extensionUtilities' import { AuthUtil } from '../util/authUtil' import { session } from '../util/codeWhispererSession' @@ -127,22 +126,9 @@ export class ReferenceLogViewProvider implements vscode.WebviewViewProvider { } } - let csp = '' - if (isCloud9()) { - csp = `` - } return ` - ${csp} diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index e76a201be87..47490f2427f 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -4,7 +4,14 @@ */ import * as vscode from 'vscode' import path from 'path' -import { CodeScanIssue, SecurityTreeViewFilterState, severities, Severity } from '../models/model' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + CodeScanIssue, + SecurityTreeViewFilterState, + severities, + Severity, +} from '../models/model' import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger' import { SecurityIssueProvider } from './securityIssueProvider' @@ -34,6 +41,17 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + switch (groupingStrategy) { + case CodeIssueGroupingStrategy.FileLocation: + return this.getChildrenGroupedByFileLocation(element) + case CodeIssueGroupingStrategy.Severity: + default: + return this.getChildrenGroupedBySeverity(element) + } + } + + private getChildrenGroupedBySeverity(element: SecurityViewTreeItem | undefined) { const filterHiddenSeverities = (severity: Severity) => !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(severity) @@ -64,6 +82,27 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider + !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(issue.severity) + + if (element instanceof FileItem) { + return element.issues + .filter(filterHiddenSeverities) + .filter((issue) => issue.visible) + .sort((a, b) => a.startLine - b.startLine) + .map((issue) => new IssueItem(element.filePath, issue)) + } + + const result = this.issueProvider.issues + .filter((group) => group.issues.some(filterHiddenSeverities)) + .filter((group) => group.issues.some((issue) => issue.visible)) + .sort((a, b) => a.filePath.localeCompare(b.filePath)) + .map((group) => new FileItem(group.filePath, group.issues.filter(filterHiddenSeverities))) + this._onDidChangeTreeData.fire(result) + return result + } + public refresh(): void { this._onDidChangeTreeData.fire() } @@ -118,7 +157,8 @@ export class IssueItem extends vscode.TreeItem { public readonly issue: CodeScanIssue ) { super(issue.title, vscode.TreeItemCollapsibleState.None) - this.description = `${path.basename(this.filePath)} [Ln ${this.issue.startLine + 1}, Col 1]` + this.description = this.getDescription() + this.iconPath = this.getSeverityIcon() this.tooltip = this.getTooltipMarkdown() this.command = { title: 'Focus Issue', @@ -132,6 +172,22 @@ export class IssueItem extends vscode.TreeItem { return globals.context.asAbsolutePath(`resources/images/severity-${this.issue.severity.toLowerCase()}.svg`) } + private getSeverityIcon() { + const iconPath = globals.context.asAbsolutePath( + `resources/icons/aws/amazonq/severity-${this.issue.severity.toLowerCase()}.svg` + ) + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.Severity ? iconPath : undefined + } + + private getDescription() { + const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation + ? `${path.basename(this.filePath)} ${positionStr}` + : positionStr + } + private getContextValue() { return this.issue.suggestedFixes.length === 0 || !this.issue.suggestedFixes[0].code ? ContextValue.ISSUE_WITHOUT_FIX diff --git a/packages/core/src/codewhisperer/service/securityScanHandler.ts b/packages/core/src/codewhisperer/service/securityScanHandler.ts index ba70e558733..22f5fa30e60 100644 --- a/packages/core/src/codewhisperer/service/securityScanHandler.ts +++ b/packages/core/src/codewhisperer/service/securityScanHandler.ts @@ -43,6 +43,8 @@ import { getTelemetryReasonDesc } from '../../shared/errors' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { detectCommentAboveLine } from '../../shared/utilities/commentUtils' import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { FeatureUseCase } from '../models/constants' +import { UploadTestArtifactToS3Error } from '../../amazonqTest/error' export async function listScanResults( client: DefaultCodeWhispererClient, @@ -287,7 +289,7 @@ export async function getPresignedUrlAndUpload( logger.verbose(`CreateUploadUrlRequest request id: ${srcResp.$response.requestId}`) logger.verbose(`Complete Getting presigned Url for uploading src context.`) logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, scope) + await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, FeatureUseCase.CODE_SCAN, scope) logger.verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -343,6 +345,7 @@ export function throwIfCancelled(scope: CodeWhispererConstants.CodeAnalysisScope export async function uploadArtifactToS3( fileName: string, resp: CreateUploadUrlResponse, + featureUseCase: FeatureUseCase, scope?: CodeWhispererConstants.CodeAnalysisScope ) { const logger = getLoggerForScope(scope) @@ -365,14 +368,23 @@ export async function uploadArtifactToS3( }).response logger.debug(`StatusCode: ${response.status}, Text: ${response.statusText}`) } catch (error) { + let errorMessage = '' + const isCodeScan = featureUseCase === FeatureUseCase.CODE_SCAN + const featureType = isCodeScan ? 'security scans' : 'unit test generation' + const defaultMessage = isCodeScan ? 'Security scan failed.' : 'Test generation failed.' getLogger().error( - `Amazon Q is unable to upload workspace artifacts to Amazon S3 for security scans. For more information, see the Amazon Q documentation or contact your network or organization administrator.` + `Amazon Q is unable to upload workspace artifacts to Amazon S3 for ${featureType}. ` + + 'For more information, see the Amazon Q documentation or contact your network or organization administrator.' ) - const errorMessage = getTelemetryReasonDesc(error)?.includes(`"PUT" request failed with code "403"`) - ? `"PUT" request failed with code "403"` - : (getTelemetryReasonDesc(error) ?? 'Security scan failed.') - - throw new UploadArtifactToS3Error(errorMessage) + const errorDesc = getTelemetryReasonDesc(error) + if (errorDesc?.includes('"PUT" request failed with code "403"')) { + errorMessage = '"PUT" request failed with code "403"' + } else if (errorDesc?.includes('"PUT" request failed with code "503"')) { + errorMessage = '"PUT" request failed with code "503"' + } else { + errorMessage = errorDesc ?? defaultMessage + } + throw isCodeScan ? new UploadArtifactToS3Error(errorMessage) : new UploadTestArtifactToS3Error(errorMessage) } } @@ -391,10 +403,7 @@ function getPollingDelayMsForScope(scope: CodeWhispererConstants.CodeAnalysisSco } function getPollingTimeoutMsForScope(scope: CodeWhispererConstants.CodeAnalysisScope) { - return ( - (scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO || - scope === CodeWhispererConstants.CodeAnalysisScope.FILE_ON_DEMAND - ? CodeWhispererConstants.codeFileScanJobTimeoutSeconds - : CodeWhispererConstants.codeScanJobTimeoutSeconds) * 1000 - ) + return scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO + ? CodeWhispererConstants.expressScanTimeoutMs + : CodeWhispererConstants.standardScanTimeoutMs } diff --git a/packages/core/src/codewhisperer/service/testGenHandler.ts b/packages/core/src/codewhisperer/service/testGenHandler.ts index 01be77a834b..48a66fb1f83 100644 --- a/packages/core/src/codewhisperer/service/testGenHandler.ts +++ b/packages/core/src/codewhisperer/service/testGenHandler.ts @@ -13,7 +13,15 @@ import CodeWhispererUserClient, { CreateUploadUrlRequest, TargetCode, } from '../client/codewhispereruserclient' -import { CreateUploadUrlError, InvalidSourceZipError, TestGenFailedError, TestGenTimedOutError } from '../models/errors' +import { + CreateTestJobError, + CreateUploadUrlError, + ExportResultsArchiveError, + InvalidSourceZipError, + TestGenFailedError, + TestGenStoppedError, + TestGenTimedOutError, +} from '../../amazonqTest/error' import { getMd5, uploadArtifactToS3 } from './securityScanHandler' import { fs, randomUUID, sleep, tempDirPath } from '../../shared' import { ShortAnswer, testGenState } from '../models/model' @@ -24,12 +32,13 @@ import AdmZip from 'adm-zip' import path from 'path' import { ExportIntent } from '@amzn/codewhisperer-streaming' import { glob } from 'glob' +import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' // TODO: Get TestFileName and Framework and to error message export function throwIfCancelled() { // TODO: fileName will be '' if user gives propt without opening if (testGenState.isCancelling()) { - throw Error(CodeWhispererConstants.unitTestGenerationCancelMessage) + throw new TestGenStoppedError() } } @@ -47,12 +56,12 @@ export async function getPresignedUrlAndUploadTestGen(zipMetadata: ZipMetadata) logger.verbose(`Prepare for uploading src context...`) const srcResp = await codeWhisperer.codeWhispererClient.createUploadUrl(srcReq).catch((err) => { getLogger().error(`Failed getting presigned url for uploading src context. Request id: ${err.requestId}`) - throw new CreateUploadUrlError(err) + throw new CreateUploadUrlError(err.message) }) logger.verbose(`CreateUploadUrlRequest requestId: ${srcResp.$response.requestId}`) logger.verbose(`Complete Getting presigned Url for uploading src context.`) logger.verbose(`Uploading src context...`) - await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp) + await uploadArtifactToS3(zipMetadata.zipFilePath, srcResp, CodeWhispererConstants.FeatureUseCase.TEST_GENERATION) logger.verbose(`Complete uploading src context.`) const artifactMap: ArtifactMap = { SourceCode: srcResp.uploadId, @@ -96,7 +105,7 @@ export async function createTestJob( const resp = await codewhispererClient.codeWhispererClient.startTestGeneration(req).catch((err) => { ChatSessionManager.Instance.getSession().startTestGenerationRequestId = err.requestId logger.error(`Failed creating test job. Request id: ${err.requestId}`) - throw err + throw new CreateTestJobError(err.message) }) logger.info('Unit test generation request id: %s', resp.$response.requestId) logger.debug('Unit test generation data: %O', resp.$response.data) @@ -252,7 +261,7 @@ export async function exportResultsArchive( session.numberOfTestsGenerated = 0 downloadErrorMessage = (e as Error).message getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw new Error('Error downloading test generation result artifacts: ' + downloadErrorMessage) + throw new ExportResultsArchiveError(downloadErrorMessage) } } @@ -291,8 +300,9 @@ export async function downloadResultArchive( } catch (e: any) { downloadErrorMessage = (e as Error).message getLogger().error(`Unit Test Generation: ExportResultArchive error = ${downloadErrorMessage}`) - throw e + throw new ExportResultsArchiveError(downloadErrorMessage) } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index cbf6bf92710..b5f4d2d1447 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -49,6 +49,7 @@ import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSess import { encodeHTML } from '../../../shared/utilities/textUtilities' import { convertToTimeString } from '../../../shared/datetime' import { getAuthType } from '../../../auth/utils' +import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker' export function getSha256(buffer: Buffer) { const hasher = crypto.createHash('sha256') @@ -447,6 +448,7 @@ export async function startJob(uploadId: string) { target: { language: targetLanguageVersion }, // always JDK17 }, }) + getLogger().info('CodeTransformation: called startJob API successfully') if (response.$response.requestId) { transformByQState.setJobFailureMetadata(` (request ID: ${response.$response.requestId})`) } @@ -670,6 +672,7 @@ export async function pollTransformationJob(jobId: string, validStates: string[] }) } transformByQState.setPolledJobStatus(status) + getLogger().info(`CodeTransformation: polled job status = ${status}`) const errorMessage = response.transformationJob.reason if (errorMessage !== undefined) { @@ -767,6 +770,7 @@ export async function downloadResultArchive( throw e } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts index 8ba8504e436..e2bbbc6556b 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts @@ -11,7 +11,7 @@ import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-i import { CodeTransformBuildCommand, telemetry } from '../../../shared/telemetry/telemetry' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { ToolkitError } from '../../../shared/errors' -import { writeLogs } from './transformFileHandler' +import { setMaven, writeLogs } from './transformFileHandler' import { throwIfCancelled } from './transformApiHandler' // run 'install' with either 'mvnw.cmd', './mvnw', or 'mvn' (if wrapper exists, we use that, otherwise we use regular 'mvn') @@ -108,6 +108,8 @@ function copyProjectDependencies(dependenciesFolder: FolderInfo, modulePath: str } export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, rootPomPath: string) { + await setMaven() + getLogger().info('CodeTransformation: running Maven copy-dependencies') try { copyProjectDependencies(dependenciesFolder, rootPomPath) } catch (err) { @@ -117,6 +119,7 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, ) } + getLogger().info('CodeTransformation: running Maven install') try { installProjectDependencies(dependenciesFolder, rootPomPath) } catch (err) { @@ -134,9 +137,9 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo, export async function getVersionData() { const baseCommand = transformByQState.getMavenName() // will be one of: 'mvnw.cmd', './mvnw', 'mvn' - const modulePath = transformByQState.getProjectPath() + const projectPath = transformByQState.getProjectPath() const args = ['-v'] - const spawnResult = spawnSync(baseCommand, args, { cwd: modulePath, shell: true, encoding: 'utf-8' }) + const spawnResult = spawnSync(baseCommand, args, { cwd: projectPath, shell: true, encoding: 'utf-8' }) let localMavenVersion: string | undefined = '' let localJavaVersion: string | undefined = '' diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 98bdae3fcf4..ef2ce722598 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -98,7 +98,7 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider -

Job Status

+

Transformation Status

${ Object.keys(sessionJobHistory).length === 0 ? `

${CodeWhispererConstants.nothingToShowMessage}

` diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index 43c6a1cc08b..9339be10fc9 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -26,6 +26,7 @@ import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/ import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { setContext } from '../../../shared/vscode/setContext' import * as codeWhisperer from '../../client/codewhisperer' +import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker' export abstract class ProposedChangeNode { abstract readonly resourcePath: string @@ -177,6 +178,7 @@ export class DiffModel { } const changedFiles = parsePatch(diffContents) + getLogger().info('CodeTransformation: parsed patch file successfully') // path to the directory containing copy of the changed files in the transformed project const pathToTmpSrcDir = this.copyProject(pathToWorkspace, changedFiles) transformByQState.setProjectCopyFilePath(pathToTmpSrcDir) @@ -401,6 +403,7 @@ export class ProposedTransformationExplorer { pathToArchive ) + getLogger().info('CodeTransformation: downloaded results successfully') // Update downloaded artifact size exportResultsArchiveSize = (await fs.promises.stat(pathToArchive)).size @@ -424,6 +427,7 @@ export class ProposedTransformationExplorer { throw new Error('Error downloading diff') } finally { cwStreamingClient.destroy() + UserWrittenCodeTracker.instance.onQFeatureInvoked() } let deserializeErrorMessage = undefined @@ -532,6 +536,7 @@ export class ProposedTransformationExplorer { vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.acceptChanges', async () => { telemetry.codeTransform_submitSelection.run(() => { + getLogger().info('CodeTransformation: accepted changes') diffModel.saveChanges() telemetry.record({ codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(), @@ -585,6 +590,7 @@ export class ProposedTransformationExplorer { vscode.commands.registerCommand('aws.amazonq.transformationHub.reviewChanges.rejectChanges', async () => { await telemetry.codeTransform_submitSelection.run(async () => { + getLogger().info('CodeTransformation: rejected changes') diffModel.rejectChanges() await reset() telemetry.record({ diff --git a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts index d0ad76c26da..39416eafe70 100644 --- a/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts +++ b/packages/core/src/codewhisperer/tracker/codewhispererCodeCoverageTracker.ts @@ -27,6 +27,8 @@ const autoClosingKeystrokeInputs = ['[]', '{}', '()', '""', "''"] /** * This singleton class is mainly used for calculating the code written by codeWhisperer + * TODO: Remove this tracker, uses user written code tracker instead. + * This is kept in codebase for server side backward compatibility until service fully switch to user written code */ export class CodeWhispererCodeCoverageTracker { private _acceptedTokens: { [key: string]: CodeWhispererToken[] } diff --git a/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts new file mode 100644 index 00000000000..2497006b0a4 --- /dev/null +++ b/packages/core/src/codewhisperer/tracker/userWrittenCodeTracker.ts @@ -0,0 +1,194 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import { runtimeLanguageContext } from '../util/runtimeLanguageContext' +import { AuthUtil } from '../util/authUtil' +import { getSelectedCustomization } from '../util/customizationUtil' +import { codeWhispererClient as client } from '../client/codewhisperer' +import { isAwsError } from '../../shared/errors' +import { CodewhispererLanguage, globals, undefinedIfEmpty } from '../../shared' + +/** + * This singleton class is mainly used for calculating the user written code + * for active Amazon Q users. + * It reports the user written code per 5 minutes when the user is coding and using Amazon Q features + */ +export class UserWrittenCodeTracker { + private _userWrittenNewCodeCharacterCount: Map + private _userWrittenNewCodeLineCount: Map + private _qIsMakingEdits: boolean + private _timer?: NodeJS.Timer + private _qUsageCount: number + private _lastQInvocationTime: number + + static #instance: UserWrittenCodeTracker + private static copySnippetThreshold = 50 + private static resetQIsEditingTimeoutMs = 2 * 60 * 1000 + private static defaultCheckPeriodMillis = 5 * 60 * 1000 + + private constructor() { + this._userWrittenNewCodeLineCount = new Map() + this._userWrittenNewCodeCharacterCount = new Map() + this._qUsageCount = 0 + this._qIsMakingEdits = false + this._timer = undefined + this._lastQInvocationTime = 0 + } + + public static get instance() { + return (this.#instance ??= new this()) + } + + public isActive(): boolean { + return globals.telemetry.telemetryEnabled && AuthUtil.instance.isConnected() + } + + // this should be invoked whenever there is a successful Q feature invocation + // for all Q features + public onQFeatureInvoked() { + this._qUsageCount += 1 + this._lastQInvocationTime = performance.now() + } + + public onQStartsMakingEdits() { + this._qIsMakingEdits = true + } + + public onQFinishesEdits() { + this._qIsMakingEdits = false + } + + public getUserWrittenCharacters(language: CodewhispererLanguage) { + return this._userWrittenNewCodeCharacterCount.get(language) || 0 + } + + public getUserWrittenLines(language: CodewhispererLanguage) { + return this._userWrittenNewCodeLineCount.get(language) || 0 + } + + public reset() { + this._userWrittenNewCodeLineCount = new Map() + this._userWrittenNewCodeCharacterCount = new Map() + this._qUsageCount = 0 + this._qIsMakingEdits = false + this._lastQInvocationTime = 0 + if (this._timer !== undefined) { + clearTimeout(this._timer) + this._timer = undefined + } + } + + public emitCodeContributions() { + const selectedCustomization = getSelectedCustomization() + + for (const [language, charCount] of this._userWrittenNewCodeCharacterCount) { + const lineCount = this.getUserWrittenLines(language) + if (charCount > 0) { + client + .sendTelemetryEvent({ + telemetryEvent: { + codeCoverageEvent: { + customizationArn: undefinedIfEmpty(selectedCustomization.arn), + programmingLanguage: { + languageName: runtimeLanguageContext.toRuntimeLanguage(language), + }, + acceptedCharacterCount: 0, + totalCharacterCount: 0, + timestamp: new Date(Date.now()), + userWrittenCodeCharacterCount: charCount, + userWrittenCodeLineCount: lineCount, + }, + }, + }) + .then() + .catch((error) => { + let requestId: string | undefined + if (isAwsError(error)) { + requestId = error.requestId + } + getLogger().debug( + `Failed to sendTelemetryEvent, requestId: ${requestId ?? ''}, message: ${error.message}` + ) + }) + } + } + } + + private tryStartTimer() { + if (this._timer !== undefined) { + return + } + if (!this.isActive()) { + getLogger().debug(`Skip emiting code contribution metric. Telemetry disabled or not logged in. `) + this.reset() + return + } + const startTime = performance.now() + this._timer = setTimeout(() => { + try { + const currentTime = performance.now() + const delay: number = UserWrittenCodeTracker.defaultCheckPeriodMillis + const diffTime: number = startTime + delay + if (diffTime <= currentTime) { + if (this._qUsageCount <= 0) { + getLogger().debug(`Skip emiting code contribution metric. There is no active Amazon Q usage. `) + return + } + if (this._userWrittenNewCodeCharacterCount.size === 0) { + getLogger().debug(`Skip emiting code contribution metric. There is no new code added. `) + return + } + this.emitCodeContributions() + } + } catch (e) { + getLogger().verbose(`Exception Thrown from QCodeGenTracker: ${e}`) + } finally { + this.reset() + } + }, UserWrittenCodeTracker.defaultCheckPeriodMillis) + } + + private countNewLines(str: string) { + return str.split('\n').length - 1 + } + + public onTextDocumentChange(e: vscode.TextDocumentChangeEvent) { + // do not count code written by Q as user written code + if ( + !runtimeLanguageContext.isLanguageSupported(e.document.languageId) || + e.contentChanges.length === 0 || + this._qIsMakingEdits + ) { + // if the boolean of qIsMakingEdits was incorrectly set to true + // due to unhandled edge cases or early terminated code paths + // reset it back to false after a reasonable period of time + if (this._qIsMakingEdits) { + if (performance.now() - this._lastQInvocationTime > UserWrittenCodeTracker.resetQIsEditingTimeoutMs) { + getLogger().warn(`Reset Q is editing state to false.`) + this._qIsMakingEdits = false + } + } + return + } + const contentChange = e.contentChanges[0] + // if user copies code into the editor for more than 50 characters + // do not count this as total new code, this will skew the data, + // reporting highly inflated user written code + if (contentChange.text.length > UserWrittenCodeTracker.copySnippetThreshold) { + return + } + const language = runtimeLanguageContext.normalizeLanguage(e.document.languageId) + if (language) { + const charCount = this.getUserWrittenCharacters(language) + this._userWrittenNewCodeCharacterCount.set(language, charCount + contentChange.text.length) + const lineCount = this.getUserWrittenLines(language) + this._userWrittenNewCodeLineCount.set(language, lineCount + this.countNewLines(contentChange.text)) + // start 5 min data reporting once valid user input is detected + this.tryStartTimer() + } + } +} diff --git a/packages/core/src/codewhisperer/ui/prompters.ts b/packages/core/src/codewhisperer/ui/prompters.ts new file mode 100644 index 00000000000..95541d84a82 --- /dev/null +++ b/packages/core/src/codewhisperer/ui/prompters.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + codeIssueGroupingStrategies, + CodeIssueGroupingStrategy, + codeIssueGroupingStrategyLabel, + CodeIssueGroupingStrategyState, +} from '../models/model' +import { createQuickPick, QuickPickPrompter } from '../../shared/ui/pickerPrompter' +import { localize } from '../../shared/utilities/vsCodeUtils' + +export function createCodeIssueGroupingStrategyPrompter(): QuickPickPrompter { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + const prompter = createQuickPick( + codeIssueGroupingStrategies.map((strategy) => ({ + data: strategy, + label: codeIssueGroupingStrategyLabel[strategy], + })), + { + title: localize('AWS.amazonq.scans.groupIssues', 'Group Issues'), + placeholder: localize('AWS.amazonq.scans.groupIssues.placeholder', 'Select how to group code issues'), + } + ) + prompter.quickPick.activeItems = prompter.quickPick.items.filter((item) => item.data === groupingStrategy) + prompter.quickPick.onDidChangeSelection(async (items) => { + const [item] = items + await CodeIssueGroupingStrategyState.instance.setState(item.data) + prompter.quickPick.hide() + }) + return prompter +} diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 461f262d34d..a9045220ea2 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -8,7 +8,7 @@ import * as localizedText from '../../shared/localizedText' import { Auth } from '../../auth/auth' import { ToolkitError, isNetworkError, tryRun } from '../../shared/errors' import { getSecondaryAuth, setScopes } from '../../auth/secondaryAuth' -import { isCloud9, isSageMaker } from '../../shared/extensionUtilities' +import { isSageMaker } from '../../shared/extensionUtilities' import { AmazonQPromptSettings } from '../../shared/settings' import { scopesCodeWhispererCore, @@ -55,14 +55,8 @@ export const amazonQScopes = [...codeWhispererChatScopes, ...scopesGumby, ...sco * for Amazon Q. */ export const isValidCodeWhispererCoreConnection = (conn?: Connection): conn is Connection => { - if (isCloud9('classic')) { - return isIamConnection(conn) - } - return ( - (isSageMaker() && isIamConnection(conn)) || - (isCloud9('codecatalyst') && isIamConnection(conn)) || - (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) + (isSageMaker() && isIamConnection(conn)) || (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) ) } /** Superset that includes all of CodeWhisperer + Amazon Q */ @@ -144,10 +138,6 @@ export class AuthUtil { }) public async setVscodeContextProps() { - if (isCloud9()) { - return - } - await setContext('aws.codewhisperer.connected', this.isConnected()) const doShowAmazonQLoginView = !this.isConnected() || this.isConnectionExpired() await setContext('aws.amazonq.showLoginView', doShowAmazonQLoginView) diff --git a/packages/core/src/codewhisperer/util/closingBracketUtil.ts b/packages/core/src/codewhisperer/util/closingBracketUtil.ts index b9511349e9c..466ca31a0b9 100644 --- a/packages/core/src/codewhisperer/util/closingBracketUtil.ts +++ b/packages/core/src/codewhisperer/util/closingBracketUtil.ts @@ -4,8 +4,6 @@ */ import * as vscode from 'vscode' -import { workspace, WorkspaceEdit } from 'vscode' -import { isCloud9 } from '../../shared/extensionUtilities' import * as CodeWhispererConstants from '../models/constants' interface bracketMapType { @@ -97,31 +95,18 @@ const removeBracketsFromRightContext = async ( ) => { const offset = editor.document.offsetAt(endPosition) - if (isCloud9()) { - const edits = idxToRemove.map((idx) => ({ - range: new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ), - newText: '', - })) - const wEdit = new WorkspaceEdit() - wEdit.set(editor.document.uri, [...edits]) - await workspace.applyEdit(wEdit) - } else { - await editor.edit( - (editBuilder) => { - for (const idx of idxToRemove) { - const range = new vscode.Range( - editor.document.positionAt(offset + idx), - editor.document.positionAt(offset + idx + 1) - ) - editBuilder.delete(range) - } - }, - { undoStopAfter: false, undoStopBefore: false } - ) - } + await editor.edit( + (editBuilder) => { + for (const idx of idxToRemove) { + const range = new vscode.Range( + editor.document.positionAt(offset + idx), + editor.document.positionAt(offset + idx + 1) + ) + editBuilder.delete(range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) } function getBracketsToRemove( diff --git a/packages/core/src/codewhisperer/util/commonUtil.ts b/packages/core/src/codewhisperer/util/commonUtil.ts index 1d624e77b5e..d2df78f1369 100644 --- a/packages/core/src/codewhisperer/util/commonUtil.ts +++ b/packages/core/src/codewhisperer/util/commonUtil.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode' import * as semver from 'semver' import { distance } from 'fastest-levenshtein' -import { isCloud9 } from '../../shared/extensionUtilities' import { getInlineSuggestEnabled } from '../../shared/utilities/editorUtilities' import { AWSTemplateCaseInsensitiveKeyWords, @@ -31,12 +30,12 @@ export function asyncCallWithTimeout(asyncPromise: Promise, message: strin } export function isInlineCompletionEnabled() { - return getInlineSuggestEnabled() && !isCloud9() + return getInlineSuggestEnabled() } // This is the VS Code version that started to have regressions in inline completion API export function isVscHavingRegressionInlineCompletionApi() { - return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() && !isCloud9() + return semver.gte(vscode.version, '1.78.0') && getInlineSuggestEnabled() } export function getFileExt(languageId: string) { diff --git a/packages/core/src/codewhisperer/util/showSsoPrompt.ts b/packages/core/src/codewhisperer/util/showSsoPrompt.ts index fbe1e6cb41c..229583a8cc2 100644 --- a/packages/core/src/codewhisperer/util/showSsoPrompt.ts +++ b/packages/core/src/codewhisperer/util/showSsoPrompt.ts @@ -14,15 +14,12 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils' import { ToolkitError } from '../../shared/errors' import { createCommonButtons } from '../../shared/ui/buttons' import { telemetry } from '../../shared/telemetry/telemetry' -import { isCloud9 } from '../../shared/extensionUtilities' import { createBuilderIdItem, createSsoItem, createIamItem } from '../../auth/utils' import { Commands } from '../../shared/vscode/commands2' import { vsCodeState } from '../models/model' export const showCodeWhispererConnectionPrompt = async () => { - const items = isCloud9('classic') - ? [createSsoItem(), createCodeWhispererIamItem()] - : [createBuilderIdItem(), createSsoItem(), createCodeWhispererIamItem()] + const items = [createBuilderIdItem(), createSsoItem(), createCodeWhispererIamItem()] const resp = await showQuickPick(items, { title: 'Amazon Q: Add Connection to AWS', diff --git a/packages/core/src/codewhisperer/util/telemetryHelper.ts b/packages/core/src/codewhisperer/util/telemetryHelper.ts index a34deb0cdca..5276d869bb9 100644 --- a/packages/core/src/codewhisperer/util/telemetryHelper.ts +++ b/packages/core/src/codewhisperer/util/telemetryHelper.ts @@ -70,6 +70,7 @@ export class TelemetryHelper { public sendTestGenerationToolkitEvent( session: Session, isSupportedLanguage: boolean, + isFileInWorkspace: boolean, result: 'Succeeded' | 'Failed' | 'Cancelled', requestId?: string, perfClientLatency?: number, @@ -90,6 +91,7 @@ export class TelemetryHelper { cwsprChatProgrammingLanguage: session.fileLanguage ?? 'plaintext', hasUserPromptSupplied: session.hasUserPromptSupplied, isSupportedLanguage: isSupportedLanguage, + isFileInWorkspace: isFileInWorkspace, result: result, artifactsUploadDuration: artifactsUploadDuration, buildPayloadBytes: buildPayloadBytes, diff --git a/packages/core/src/codewhisperer/util/zipUtil.ts b/packages/core/src/codewhisperer/util/zipUtil.ts index ab938aeb643..64a9ccc3b8d 100644 --- a/packages/core/src/codewhisperer/util/zipUtil.ts +++ b/packages/core/src/codewhisperer/util/zipUtil.ts @@ -20,9 +20,10 @@ import { NoSourceFilesError, ProjectSizeExceededError, } from '../models/errors' -import { ZipUseCase } from '../models/constants' +import { FeatureUseCase } from '../models/constants' import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' import { removeAnsi } from '../../shared' +import { ProjectZipError } from '../../amazonqTest/error' export interface ZipMetadata { rootDir: string @@ -135,7 +136,7 @@ export class ZipUtil { if (this.reachSizeLimit(this._totalSize, scope)) { throw new FileSizeExceededError() } - const zipFilePath = this.getZipDirPath(ZipUseCase.CODE_SCAN) + CodeWhispererConstants.codeScanZipExt + const zipFilePath = this.getZipDirPath(FeatureUseCase.CODE_SCAN) + CodeWhispererConstants.codeScanZipExt zip.writeZip(zipFilePath) return zipFilePath } @@ -203,15 +204,15 @@ export class ZipUtil { await processDirectory(metadataDir) } - protected async zipProject(useCase: ZipUseCase, projectPath?: string, metadataDir?: string) { + protected async zipProject(useCase: FeatureUseCase, projectPath?: string, metadataDir?: string) { const zip = new admZip() let projectPaths = [] - if (useCase === ZipUseCase.TEST_GENERATION && projectPath) { + if (useCase === FeatureUseCase.TEST_GENERATION && projectPath) { projectPaths.push(projectPath) } else { projectPaths = this.getProjectPaths() } - if (useCase === ZipUseCase.CODE_SCAN) { + if (useCase === FeatureUseCase.CODE_SCAN) { await this.processCombinedGitDiff(zip, projectPaths, '', CodeWhispererConstants.CodeAnalysisScope.PROJECT) } const languageCount = new Map() @@ -220,7 +221,7 @@ export class ZipUtil { if (metadataDir) { await this.processMetadataDir(zip, metadataDir) } - if (useCase !== ZipUseCase.TEST_GENERATION) { + if (useCase !== FeatureUseCase.TEST_GENERATION) { this.processOtherFiles(zip, languageCount) } @@ -403,7 +404,7 @@ export class ZipUtil { zip: admZip, languageCount: Map, projectPaths: string[] | undefined, - useCase: ZipUseCase + useCase: FeatureUseCase ) { if (!projectPaths || projectPaths.length === 0) { return @@ -420,7 +421,7 @@ export class ZipUtil { const zipEntryPath = this.getZipEntryPath(projectName, file.relativeFilePath) if (ZipConstants.knownBinaryFileExts.includes(path.extname(file.fileUri.fsPath))) { - if (useCase === ZipUseCase.TEST_GENERATION) { + if (useCase === FeatureUseCase.TEST_GENERATION) { continue } await this.processBinaryFile(zip, file.fileUri, zipEntryPath) @@ -511,10 +512,10 @@ export class ZipUtil { return vscode.workspace.textDocuments.some((document) => document.uri.fsPath === uri.fsPath && document.isDirty) } - public getZipDirPath(useCase: ZipUseCase): string { + public getZipDirPath(useCase: FeatureUseCase): string { if (this._zipDir === '') { const prefix = - useCase === ZipUseCase.TEST_GENERATION + useCase === FeatureUseCase.TEST_GENERATION ? CodeWhispererConstants.TestGenerationTruncDirPrefix : CodeWhispererConstants.codeScanTruncDirPrefix @@ -528,7 +529,7 @@ export class ZipUtil { scope: CodeWhispererConstants.CodeAnalysisScope ): Promise { try { - const zipDirPath = this.getZipDirPath(ZipUseCase.CODE_SCAN) + const zipDirPath = this.getZipDirPath(FeatureUseCase.CODE_SCAN) let zipFilePath: string if ( scope === CodeWhispererConstants.CodeAnalysisScope.FILE_AUTO || @@ -536,7 +537,7 @@ export class ZipUtil { ) { zipFilePath = await this.zipFile(uri, scope) } else if (scope === CodeWhispererConstants.CodeAnalysisScope.PROJECT) { - zipFilePath = await this.zipProject(ZipUseCase.CODE_SCAN) + zipFilePath = await this.zipProject(FeatureUseCase.CODE_SCAN) } else { throw new ToolkitError(`Unknown code analysis scope: ${scope}`) } @@ -562,7 +563,7 @@ export class ZipUtil { public async generateZipTestGen(projectPath: string, initialExecution: boolean): Promise { try { // const repoMapFile = await LspClient.instance.getRepoMapJSON() - const zipDirPath = this.getZipDirPath(ZipUseCase.TEST_GENERATION) + const zipDirPath = this.getZipDirPath(FeatureUseCase.TEST_GENERATION) const metadataDir = path.join(zipDirPath, 'utgRequiredArtifactsDir') @@ -590,7 +591,7 @@ export class ZipUtil { } } - const zipFilePath: string = await this.zipProject(ZipUseCase.TEST_GENERATION, projectPath, metadataDir) + const zipFilePath: string = await this.zipProject(FeatureUseCase.TEST_GENERATION, projectPath, metadataDir) const zipFileSize = (await fs.stat(zipFilePath)).size return { rootDir: zipDirPath, @@ -604,7 +605,9 @@ export class ZipUtil { } } catch (error) { getLogger().error('Zip error caused by: %s', error) - throw error + throw new ProjectZipError( + error instanceof Error ? error.message : 'Unknown error occurred during zip operation' + ) } } // TODO: Refactor this diff --git a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts index 7c1c655a937..d511bd9a5f6 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts +++ b/packages/core/src/codewhisperer/views/securityIssue/securityIssueWebview.ts @@ -27,12 +27,12 @@ export class SecurityIssueWebview extends VueWebview { public readonly onChangeIssue = new vscode.EventEmitter() public readonly onChangeFilePath = new vscode.EventEmitter() public readonly onChangeGenerateFixLoading = new vscode.EventEmitter() - public readonly onChangeGenerateFixError = new vscode.EventEmitter() + public readonly onChangeGenerateFixError = new vscode.EventEmitter() private issue: CodeScanIssue | undefined private filePath: string | undefined private isGenerateFixLoading: boolean = false - private isGenerateFixError: boolean = false + private generateFixError: string | null | undefined = undefined public constructor() { super(SecurityIssueWebview.sourcePath) @@ -99,13 +99,13 @@ export class SecurityIssueWebview extends VueWebview { this.onChangeGenerateFixLoading.fire(isGenerateFixLoading) } - public getIsGenerateFixError() { - return this.isGenerateFixError + public getGenerateFixError() { + return this.generateFixError } - public setIsGenerateFixError(isGenerateFixError: boolean) { - this.isGenerateFixError = isGenerateFixError - this.onChangeGenerateFixError.fire(isGenerateFixError) + public setGenerateFixError(generateFixError: string | null | undefined) { + this.generateFixError = generateFixError + this.onChangeGenerateFixError.fire(generateFixError) } public generateFix() { @@ -201,7 +201,7 @@ export async function showSecurityIssueWebview(ctx: vscode.ExtensionContext, iss activePanel.server.setIssue(issue) activePanel.server.setFilePath(filePath) activePanel.server.setIsGenerateFixLoading(false) - activePanel.server.setIsGenerateFixError(false) + activePanel.server.setGenerateFixError(undefined) const webviewPanel = await activePanel.show({ title: amazonqCodeIssueDetailsTabTitle, @@ -247,7 +247,7 @@ type WebviewParams = { issue?: CodeScanIssue filePath?: string isGenerateFixLoading?: boolean - isGenerateFixError?: boolean + generateFixError?: string | null shouldRefreshView: boolean context: vscode.ExtensionContext } @@ -255,7 +255,7 @@ export async function updateSecurityIssueWebview({ issue, filePath, isGenerateFixLoading, - isGenerateFixError, + generateFixError, shouldRefreshView, context, }: WebviewParams): Promise { @@ -271,8 +271,8 @@ export async function updateSecurityIssueWebview({ if (isGenerateFixLoading !== undefined) { activePanel.server.setIsGenerateFixLoading(isGenerateFixLoading) } - if (isGenerateFixError !== undefined) { - activePanel.server.setIsGenerateFixError(isGenerateFixError) + if (generateFixError !== undefined) { + activePanel.server.setGenerateFixError(generateFixError) } if (shouldRefreshView && filePath && issue) { await showSecurityIssueWebview(context, issue, filePath) diff --git a/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue b/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue index a086aea3089..c28d12a021d 100644 --- a/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue +++ b/packages/core/src/codewhisperer/views/securityIssue/vue/root.vue @@ -48,16 +48,14 @@

Suggested code fix preview

-
-                Something went wrong. Retry
-            
+
{{ generateFixError }}
@@ -195,7 +193,7 @@ export default defineComponent({ endLine: 0, relativePath: '', isGenerateFixLoading: false, - isGenerateFixError: false, + generateFixError: undefined as string | null | undefined, languageId: 'plaintext', fixedCode: '', referenceText: '', @@ -218,8 +216,8 @@ export default defineComponent({ const relativePath = await client.getRelativePath() this.updateRelativePath(relativePath) const isGenerateFixLoading = await client.getIsGenerateFixLoading() - const isGenerateFixError = await client.getIsGenerateFixError() - this.updateGenerateFixState(isGenerateFixLoading, isGenerateFixError) + const generateFixError = await client.getGenerateFixError() + this.updateGenerateFixState(isGenerateFixLoading, generateFixError) const languageId = await client.getLanguageId() if (languageId) { this.updateLanguageId(languageId) @@ -249,16 +247,16 @@ export default defineComponent({ this.isGenerateFixLoading = isGenerateFixLoading this.scrollTo('codeFixSection') }) - client.onChangeGenerateFixError((isGenerateFixError) => { - this.isGenerateFixError = isGenerateFixError + client.onChangeGenerateFixError((generateFixError) => { + this.generateFixError = generateFixError }) }, updateRelativePath(relativePath: string) { this.relativePath = relativePath }, - updateGenerateFixState(isGenerateFixLoading: boolean, isGenerateFixError: boolean) { + updateGenerateFixState(isGenerateFixLoading: boolean, generateFixError: string | null | undefined) { this.isGenerateFixLoading = isGenerateFixLoading - this.isGenerateFixError = isGenerateFixError + this.generateFixError = generateFixError }, updateLanguageId(languageId: string) { this.languageId = languageId diff --git a/packages/core/src/codewhisperer/vue/backend.ts b/packages/core/src/codewhisperer/vue/backend.ts index e4baecadc18..f92552e247a 100644 --- a/packages/core/src/codewhisperer/vue/backend.ts +++ b/packages/core/src/codewhisperer/vue/backend.ts @@ -8,7 +8,6 @@ import * as os from 'os' import * as vscode from 'vscode' import * as path from 'path' import { VueWebview } from '../../webviews/main' -import { isCloud9 } from '../../shared/extensionUtilities' import globals from '../../shared/extensionGlobals' import { telemetry, CodewhispererLanguage, CodewhispererGettingStartedTask } from '../../shared/telemetry/telemetry' import { fs } from '../../shared' @@ -148,7 +147,7 @@ export async function showCodeWhispererWebview( } const webview = await activePanel!.show({ title: localize('AWS.view.gettingStartedPage.title', `Learn Amazon Q`), - viewColumn: isCloud9() ? vscode.ViewColumn.One : vscode.ViewColumn.Active, + viewColumn: vscode.ViewColumn.Active, }) if (!subscriptions) { diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index fc164ebb95c..b849b328bac 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -9,6 +9,7 @@ import * as vscode from 'vscode' import { ToolkitError } from '../../../../shared/errors' import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient' import { createQDeveloperStreamingClient } from '../../../../shared/clients/qDeveloperChatClient' +import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWrittenCodeTracker' export class ChatSession { private sessionId?: string @@ -48,6 +49,7 @@ export class ChatSession { } } + UserWrittenCodeTracker.instance.onQFeatureInvoked() return response } @@ -67,6 +69,8 @@ export class ChatSession { this.sessionId = response.conversationId + UserWrittenCodeTracker.instance.onQFeatureInvoked() + return response } } diff --git a/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts b/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts index d2b80f2619f..d782f7147ff 100644 --- a/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts +++ b/packages/core/src/codewhispererChat/editor/context/focusArea/focusAreaExtractor.ts @@ -7,7 +7,7 @@ import { TextEditor, Selection, TextDocument, Range } from 'vscode' import { FocusAreaContext, FullyQualifiedName } from './model' -const focusAreaCharLimit = 9_000 +const focusAreaCharLimit = 40_000 export class FocusAreaContextExtractor { public isCodeBlockSelected(editor: TextEditor): boolean { diff --git a/packages/core/src/commands.ts b/packages/core/src/commands.ts index 6177c953242..f1e77f24cf9 100644 --- a/packages/core/src/commands.ts +++ b/packages/core/src/commands.ts @@ -36,7 +36,7 @@ import { CommonAuthWebview } from './login/webview' import { AuthSource, AuthSources } from './login/webview/util' import { ServiceItemId, isServiceItemId } from './login/webview/vue/types' import { authHelpUrl } from './shared/constants' -import { isCloud9, getIdeProperties } from './shared/extensionUtilities' +import { getIdeProperties } from './shared/extensionUtilities' import { telemetry } from './shared/telemetry/telemetry' import { createCommonButtons } from './shared/ui/buttons' import { showQuickPick } from './shared/ui/pickerPrompter' @@ -66,12 +66,7 @@ export function registerCommands(context: vscode.ExtensionContext) { const addConnection = Commands.register( { id: 'aws.toolkit.auth.addConnection', telemetryThrottleMs: false }, async () => { - const c9IamItem = createIamItem() - c9IamItem.detail = - 'Activates working with resources in the Explorer. Requires an access key ID and secret access key.' - const items = isCloud9() - ? [createSsoItem(), c9IamItem] - : [createBuilderIdItem(), createSsoItem(), createIamItem()] + const items = [createBuilderIdItem(), createSsoItem(), createIamItem()] const resp = await showQuickPick(items, { title: localize('aws.auth.addConnection.title', 'Add a Connection to {0}', getIdeProperties().company), @@ -113,9 +108,7 @@ export function registerCommands(context: vscode.ExtensionContext) { source = AuthSources.vscodeComponent } - // The auth webview page does not make sense to use in C9, - // so show the auth quick pick instead. - if (isCloud9('any') || isWeb()) { + if (isWeb()) { // TODO: CW no longer exists in toolkit. This should be moved to Amazon Q if (source.toLowerCase().includes('codewhisperer')) { // Show CW specific quick pick for CW connections diff --git a/packages/core/src/dev/activation.ts b/packages/core/src/dev/activation.ts index 41af4d2d5fb..0033eaa8548 100644 --- a/packages/core/src/dev/activation.ts +++ b/packages/core/src/dev/activation.ts @@ -24,6 +24,7 @@ import { getSessionId } from '../shared/telemetry/util' import { NotificationsController } from '../notifications/controller' import { DevNotificationsState } from '../notifications/types' import { QuickPickItem } from 'vscode' +import { ChildProcess } from '../shared/utilities/processUtils' interface MenuOption { readonly label: string @@ -44,6 +45,7 @@ export type DevFunction = | 'editAuthConnections' | 'notificationsSend' | 'forceIdeCrash' + | 'startChildProcess' export type DevOptions = { context: vscode.ExtensionContext @@ -126,6 +128,11 @@ const menuOptions: () => Record = () => { detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`, executor: forceQuitIde, }, + startChildProcess: { + label: 'ChildProcess: Start child process', + detail: 'Start ChildProcess from our utility wrapper for testing', + executor: startChildProcess, + }, } } @@ -578,3 +585,15 @@ async function editNotifications() { await targetNotificationsController.pollForEmergencies() }) } + +async function startChildProcess() { + const result = await createInputBox({ + title: 'Enter a command', + }).prompt() + if (result) { + const [command, ...args] = result?.toString().split(' ') ?? [] + getLogger().info(`Starting child process: '${command}'`) + const processResult = await ChildProcess.run(command, args, { collect: true }) + getLogger().info(`Child process exited with code ${processResult.exitCode}`) + } +} diff --git a/packages/core/src/dev/beta.ts b/packages/core/src/dev/beta.ts index b09516dcc3c..89bd085e84e 100644 --- a/packages/core/src/dev/beta.ts +++ b/packages/core/src/dev/beta.ts @@ -18,7 +18,7 @@ import { isUserCancelledError, ToolkitError } from '../shared/errors' import { telemetry } from '../shared/telemetry/telemetry' import { cast } from '../shared/utilities/typeConstructors' import { CancellationError } from '../shared/utilities/timeoutUtils' -import { isAmazonQ, isCloud9, productName } from '../shared/extensionUtilities' +import { isAmazonQ, productName } from '../shared/extensionUtilities' import * as devConfig from './config' import { isReleaseVersion } from '../shared/vscode/env' import { getRelativeDate } from '../shared/datetime' @@ -49,7 +49,7 @@ async function updateBetaToolkitData(vsixUrl: string, data: BetaToolkit) { */ export async function activate(ctx: vscode.ExtensionContext) { const betaUrl = isAmazonQ() ? devConfig.betaUrl.amazonq : devConfig.betaUrl.toolkit - if (!isCloud9() && !isReleaseVersion() && betaUrl) { + if (!isReleaseVersion() && betaUrl) { ctx.subscriptions.push(watchBetaVSIX(betaUrl)) } } diff --git a/packages/core/src/dynamicResources/awsResourceManager.ts b/packages/core/src/dynamicResources/awsResourceManager.ts index a6045559675..de990e6c6b9 100644 --- a/packages/core/src/dynamicResources/awsResourceManager.ts +++ b/packages/core/src/dynamicResources/awsResourceManager.ts @@ -17,7 +17,6 @@ import { getLogger } from '../shared/logger/logger' import { getTabSizeSetting } from '../shared/utilities/editorUtilities' import { ResourceNode } from './explorer/nodes/resourceNode' import { ResourceTypeNode } from './explorer/nodes/resourceTypeNode' -import { isCloud9 } from '../shared/extensionUtilities' import globals from '../shared/extensionGlobals' import { fs } from '../shared' @@ -72,7 +71,7 @@ export class AwsResourceManager { } const doc = await vscode.workspace.openTextDocument(uri) - if (existing && !isCloud9()) { + if (existing) { await this.close(existing) } diff --git a/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts b/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts index 72374d17a5b..313ce4e7d2a 100644 --- a/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts +++ b/packages/core/src/dynamicResources/explorer/nodes/resourcesNode.ts @@ -14,7 +14,6 @@ import { ResourceTypeNode } from './resourceTypeNode' import { CloudFormation } from 'aws-sdk' import { CloudControlClient, DefaultCloudControlClient } from '../../../shared/clients/cloudControlClient' import { memoizedGetResourceTypes, ResourceTypeMetadata } from '../../model/resources' -import { isCloud9 } from '../../../shared/extensionUtilities' import { ResourcesSettings } from '../../commands/configure' const localize = nls.loadMessageBundle() @@ -57,8 +56,7 @@ export class ResourcesNode extends AWSTreeNodeBase { public async updateChildren(): Promise { const resourceTypes = memoizedGetResourceTypes() - const defaultResources = isCloud9() ? Array.from(resourceTypes.keys()) : [] - const enabledResources = this.settings.get('enabledResources', defaultResources) + const enabledResources = this.settings.get('enabledResources', []) // Use the most recently update type definition per-type const types = await toArrayAsync(this.cloudFormation.listTypes()) diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 00fd730b490..4a57f96e8fe 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -17,7 +17,7 @@ import globals, { initialize, isWeb } from './shared/extensionGlobals' import { join } from 'path' import { Commands } from './shared/vscode/commands2' import { endpointsFileUrl, githubCreateIssueUrl, githubUrl } from './shared/constants' -import { getIdeProperties, aboutExtension, isCloud9, getDocUrl } from './shared/extensionUtilities' +import { getIdeProperties, aboutExtension, getDocUrl } from './shared/extensionUtilities' import { logAndShowError, logAndShowWebviewError } from './shared/utilities/logAndShowUtils' import { telemetry } from './shared/telemetry/telemetry' import { openUrl } from './shared/utilities/vsCodeUtils' @@ -38,7 +38,6 @@ import { RegionProvider, getEndpointsFromFetcher } from './shared/regions/region import { getMachineId, isAutomation } from './shared/vscode/env' import { registerCommandErrorHandler } from './shared/vscode/commands2' import { registerWebviewErrorHandler } from './webviews/server' -import { showQuickStartWebview } from './shared/extensionStartup' import { ExtContext, VSCODE_EXTENSION_ID } from './shared/extensions' import { getSamCliContext } from './shared/sam/cli/samCliContext' import { UriHandler } from './shared/vscode/uriHandler' @@ -99,19 +98,6 @@ export async function activateCommon( void maybeShowMinVscodeWarning('1.83.0') - if (isCloud9()) { - vscode.window.withProgress = wrapWithProgressForCloud9(globals.outputChannel) - context.subscriptions.push( - Commands.register('aws.quickStart', async () => { - try { - await showQuickStartWebview(context) - } finally { - telemetry.aws_helpQuickstart.emit({ result: 'Succeeded' }) - } - }) - ) - } - // setup globals globals.machineId = await getMachineId() globals.awsContext = new DefaultAwsContext() @@ -204,12 +190,12 @@ export function registerGenericCommands(extensionContext: vscode.ExtensionContex * https://docs.aws.amazon.com/general/latest/gr/rande.html */ export function makeEndpointsProvider() { - let localManifestFetcher: ResourceFetcher - let remoteManifestFetcher: ResourceFetcher + let localManifestFetcher: ResourceFetcher + let remoteManifestFetcher: ResourceFetcher if (isWeb()) { localManifestFetcher = { get: async () => JSON.stringify(endpoints) } // Cannot use HttpResourceFetcher due to web mode breaking on import - remoteManifestFetcher = { get: async () => (await fetch(endpointsFileUrl)).text() } + remoteManifestFetcher = { get: async () => await fetch(endpointsFileUrl) } } else { localManifestFetcher = new FileResourceFetcher(globals.manifestPaths.endpoints) // HACK: HttpResourceFetcher breaks web mode when imported, so we use webpack.IgnorePlugin() @@ -224,32 +210,3 @@ export function makeEndpointsProvider() { remote: () => getEndpointsFromFetcher(remoteManifestFetcher), } } - -/** - * Wraps the `vscode.window.withProgress` functionality with functionality that also writes to the output channel. - * - * Cloud9 does not show a progress notification. - */ -function wrapWithProgressForCloud9(channel: vscode.OutputChannel): (typeof vscode.window)['withProgress'] { - const withProgress = vscode.window.withProgress.bind(vscode.window) - - return (options, task) => { - if (options.title) { - channel.appendLine(options.title) - } - - return withProgress(options, (progress, token) => { - const newProgress: typeof progress = { - ...progress, - report: (value) => { - if (value.message) { - channel.appendLine(value.message) - } - progress.report(value) - }, - } - - return task(newProgress, token) - }) - } -} diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts index c275bc5ff9d..8b25136faab 100644 --- a/packages/core/src/extensionNode.ts +++ b/packages/core/src/extensionNode.ts @@ -17,7 +17,6 @@ import { AwsContextCommands } from './shared/awsContextCommands' import { getIdeProperties, getExtEnvironmentDetails, - isCloud9, isSageMaker, showWelcomeMessage, } from './shared/extensionUtilities' @@ -183,19 +182,17 @@ export async function activate(context: vscode.ExtensionContext) { await activateSchemas(extContext) - if (!isCloud9()) { - if (!isSageMaker()) { - // Amazon Q/CodeWhisperer Tree setup. - learnMoreAmazonQCommand.register() - qExtensionPageCommand.register() - dismissQTree.register() - installAmazonQExtension.register() + if (!isSageMaker()) { + // Amazon Q Tree setup. + learnMoreAmazonQCommand.register() + qExtensionPageCommand.register() + dismissQTree.register() + installAmazonQExtension.register() - await handleAmazonQInstall() - } - await activateApplicationComposer(context) - await activateThreatComposerEditor(context) + await handleAmazonQInstall() } + await activateApplicationComposer(context) + await activateThreatComposerEditor(context) await activateStepFunctions(context, globals.awsContext, globals.outputChannel) diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts index 4a21b2e9611..3ea90d61e7c 100644 --- a/packages/core/src/lambda/activation.ts +++ b/packages/core/src/lambda/activation.ts @@ -72,10 +72,10 @@ export async function activate(context: ExtContext): Promise { await copyLambdaUrl(sourceNode, new DefaultLambdaClient(sourceNode.regionCode)) }), - registerSamInvokeVueCommand(context), + registerSamInvokeVueCommand(context.extensionContext), Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) => - registerSamDebugInvokeVueCommand(context, { resource: node }) + registerSamDebugInvokeVueCommand(context.extensionContext, { resource: node }) ) ) } diff --git a/packages/core/src/lambda/commands/createNewSamApp.ts b/packages/core/src/lambda/commands/createNewSamApp.ts index e60e60c31ed..8c6f5fbab19 100644 --- a/packages/core/src/lambda/commands/createNewSamApp.ts +++ b/packages/core/src/lambda/commands/createNewSamApp.ts @@ -39,12 +39,7 @@ import { isTemplateTargetProperties } from '../../shared/sam/debugger/awsSamDebu import { TemplateTargetProperties } from '../../shared/sam/debugger/awsSamDebugConfiguration' import { openLaunchJsonFile } from '../../shared/sam/debugger/commands/addSamDebugConfiguration' import { waitUntil } from '../../shared/utilities/timeoutUtils' -import { - getIdeProperties, - getDebugNewSamAppDocUrl, - isCloud9, - getLaunchConfigDocUrl, -} from '../../shared/extensionUtilities' +import { getIdeProperties, getDebugNewSamAppDocUrl, getLaunchConfigDocUrl } from '../../shared/extensionUtilities' import { checklogs } from '../../shared/localizedText' import globals from '../../shared/extensionGlobals' import { telemetry } from '../../shared/telemetry/telemetry' @@ -473,9 +468,7 @@ export async function writeToolkitReadme( .replace(/\$\{LISTOFCONFIGURATIONS\}/g, configString) .replace( /\$\{DOCURL\}/g, - isCloud9() - ? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html' - : 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html' + 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html' ) await fs.writeFile(readmeLocation, readme) diff --git a/packages/core/src/lambda/commands/downloadLambda.ts b/packages/core/src/lambda/commands/downloadLambda.ts index 16128ce5701..815ff2576e9 100644 --- a/packages/core/src/lambda/commands/downloadLambda.ts +++ b/packages/core/src/lambda/commands/downloadLambda.ts @@ -14,7 +14,7 @@ import { LaunchConfiguration, getReferencedHandlerPaths } from '../../shared/deb import { makeTemporaryToolkitFolder, fileExists, tryRemoveFolder } from '../../shared/filesystemUtilities' import * as localizedText from '../../shared/localizedText' import { getLogger } from '../../shared/logger' -import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher' +import { HttpResourceFetcher } from '../../shared/resourcefetcher/node/httpResourceFetcher' import { createCodeAwsSamDebugConfig } from '../../shared/sam/debugger/awsSamDebugConfiguration' import * as pathutils from '../../shared/utilities/pathUtils' import { localize } from '../../shared/utilities/vsCodeUtils' diff --git a/packages/core/src/lambda/commands/uploadLambda.ts b/packages/core/src/lambda/commands/uploadLambda.ts index 7627d53cfab..e5a4ce34755 100644 --- a/packages/core/src/lambda/commands/uploadLambda.ts +++ b/packages/core/src/lambda/commands/uploadLambda.ts @@ -20,7 +20,7 @@ import { getSamCliContext } from '../../shared/sam/cli/samCliContext' import { SamTemplateGenerator } from '../../shared/templates/sam/samTemplateGenerator' import { addCodiconToString } from '../../shared/utilities/textUtilities' import { getLambdaDetails, listLambdaFunctions } from '../utils' -import { getIdeProperties, isCloud9 } from '../../shared/extensionUtilities' +import { getIdeProperties } from '../../shared/extensionUtilities' import { createQuickPick, DataQuickPickItem } from '../../shared/ui/pickerPrompter' import { createCommonButtons } from '../../shared/ui/buttons' import { StepEstimator, Wizard, WIZARD_BACK } from '../../shared/wizards/wizard' @@ -481,10 +481,7 @@ async function uploadZipBuffer( ) } -export async function findApplicationJsonFile( - startPath: vscode.Uri, - cloud9 = isCloud9() -): Promise { +export async function findApplicationJsonFile(startPath: vscode.Uri): Promise { if (!(await fs.exists(startPath.fsPath))) { getLogger().error( 'findApplicationJsonFile() invalid path (not accessible or does not exist): "%s"', diff --git a/packages/core/src/lambda/local/debugConfiguration.ts b/packages/core/src/lambda/local/debugConfiguration.ts index 999b36ddce2..b21599b0563 100644 --- a/packages/core/src/lambda/local/debugConfiguration.ts +++ b/packages/core/src/lambda/local/debugConfiguration.ts @@ -54,20 +54,6 @@ export interface PythonDebugConfiguration extends SamLaunchRequestArgs { readonly pathMappings: PythonPathMapping[] } -/** Alternative (Cloud9) Python debugger: ikp3db */ -export interface PythonCloud9DebugConfiguration extends SamLaunchRequestArgs { - readonly runtimeFamily: RuntimeFamily.Python - /** Passed to "sam build --manifest …" */ - readonly manifestPath: string | undefined - - // Fields expected by the Cloud9 debug adapter. - // (Cloud9 sourcefile: debugger-vscode-mainthread-adapter.ts) - readonly port: number - readonly address: string - readonly localRoot: string - readonly remoteRoot: string -} - export interface DotNetDebugConfiguration extends SamLaunchRequestArgs { readonly runtimeFamily: RuntimeFamily.DotNet processName: string diff --git a/packages/core/src/lambda/models/samTemplates.ts b/packages/core/src/lambda/models/samTemplates.ts index 963cbdaafa5..5ec112a7dc4 100644 --- a/packages/core/src/lambda/models/samTemplates.ts +++ b/packages/core/src/lambda/models/samTemplates.ts @@ -12,10 +12,26 @@ import { supportsEventBridgeTemplates } from '../../../src/eventSchemas/models/s import { RuntimePackageType } from './samLambdaRuntime' import { getIdeProperties } from '../../shared/extensionUtilities' -export let helloWorldTemplate = 'helloWorldUninitialized' -export let eventBridgeHelloWorldTemplate = 'eventBridgeHelloWorldUninitialized' -export let eventBridgeStarterAppTemplate = 'eventBridgeStarterAppUnintialized' -export let stepFunctionsSampleApp = 'stepFunctionsSampleAppUnintialized' +export const helloWorldTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM Hello World', + getIdeProperties().company +) +export const eventBridgeHelloWorldTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM EventBridge Hello World', + getIdeProperties().company +) +export const eventBridgeStarterAppTemplate = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} SAM EventBridge App from Scratch', + getIdeProperties().company +) +export const stepFunctionsSampleApp = localize( + 'AWS.samcli.initWizard.template.helloWorld.name', + '{0} Step Functions Sample App', + getIdeProperties().company +) export const typeScriptBackendTemplate = 'App Backend using TypeScript' export const repromptUserForTemplate = 'REQUIRES_AWS_CREDENTIALS_REPROMPT_USER_FOR_TEMPLATE' @@ -23,33 +39,6 @@ export const cliVersionStepFunctionsTemplate = '0.52.0' export type SamTemplate = string -/** - * Lazy load strings for SAM template quick picks - * Need to be lazyloaded as `getIdeProperties` requires IDE activation for Cloud9 - */ -export function lazyLoadSamTemplateStrings(): void { - helloWorldTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM Hello World', - getIdeProperties().company - ) - eventBridgeHelloWorldTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM EventBridge Hello World', - getIdeProperties().company - ) - eventBridgeStarterAppTemplate = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} SAM EventBridge App from Scratch', - getIdeProperties().company - ) - stepFunctionsSampleApp = localize( - 'AWS.samcli.initWizard.template.helloWorld.name', - '{0} Step Functions Sample App', - getIdeProperties().company - ) -} - export function getSamTemplateWizardOption( runtime: Runtime, packageType: RuntimePackageType, diff --git a/packages/core/src/lambda/utils.ts b/packages/core/src/lambda/utils.ts index 63b4325da55..7fa56bc33e9 100644 --- a/packages/core/src/lambda/utils.ts +++ b/packages/core/src/lambda/utils.ts @@ -13,8 +13,6 @@ import { CloudFormationClient } from '../shared/clients/cloudFormationClient' import { LambdaClient } from '../shared/clients/lambdaClient' import { getFamily, getNodeMajorVersion, RuntimeFamily } from './models/samLambdaRuntime' import { getLogger } from '../shared/logger' -import { ResourceFetcher } from '../shared/resourcefetcher/resourcefetcher' -import { CompositeResourceFetcher } from '../shared/resourcefetcher/compositeResourceFetcher' import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher' import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher' import { sampleRequestManifestPath } from './constants' @@ -99,7 +97,7 @@ interface SampleRequestManifest { export async function getSampleLambdaPayloads(): Promise { const logger = getLogger() - const sampleInput = await makeSampleRequestManifestResourceFetcher().get() + const sampleInput = await getSampleRequestManifest() if (!sampleInput) { throw new Error('Unable to retrieve Sample Request manifest') @@ -120,9 +118,11 @@ export async function getSampleLambdaPayloads(): Promise { return inputs } -function makeSampleRequestManifestResourceFetcher(): ResourceFetcher { - return new CompositeResourceFetcher( - new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }), - new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests) - ) +async function getSampleRequestManifest(): Promise { + const httpResp = await new HttpResourceFetcher(sampleRequestManifestPath, { showUrl: true }).get() + if (!httpResp) { + const fileResp = new FileResourceFetcher(globals.manifestPaths.lambdaSampleRequests) + return fileResp.get() + } + return httpResp.text() } diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts index 643ea4631e2..ba624536b0f 100644 --- a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts +++ b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts @@ -7,8 +7,6 @@ import * as path from 'path' import * as vscode from 'vscode' import * as nls from 'vscode-nls' import { LaunchConfiguration } from '../../../shared/debug/launchConfiguration' - -import { ExtContext } from '../../../shared/extensions' import { getLogger } from '../../../shared/logger' import { HttpResourceFetcher } from '../../../shared/resourcefetcher/httpResourceFetcher' import { @@ -28,8 +26,6 @@ import { tryGetAbsolutePath } from '../../../shared/utilities/workspaceUtils' import * as CloudFormation from '../../../shared/cloudformation/cloudformation' import { openLaunchJsonFile } from '../../../shared/sam/debugger/commands/addSamDebugConfiguration' import { getSampleLambdaPayloads } from '../../utils' -import { isCloud9 } from '../../../shared/extensionUtilities' -import { SamDebugConfigProvider } from '../../../shared/sam/debugger/awsSamDebugger' import { samLambdaCreatableRuntimes } from '../../models/samLambdaRuntime' import globals from '../../../shared/extensionGlobals' import { VueWebview } from '../../../webviews/main' @@ -78,7 +74,6 @@ export class SamInvokeWebview extends VueWebview { public readonly id = 'createLambda' public constructor( - private readonly extContext: ExtContext, // TODO(sijaden): get rid of `ExtContext` private readonly config?: AwsSamDebuggerConfiguration, private readonly data?: ResourceData ) { @@ -170,7 +165,8 @@ export class SamInvokeWebview extends VueWebview { return } const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}` - const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' + const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get() + const sample = (await resp?.text()) ?? '' return sample } catch (err) { @@ -357,17 +353,8 @@ export class SamInvokeWebview extends VueWebview { const targetUri = await this.getUriFromLaunchConfig(finalConfig) const folder = targetUri ? vscode.workspace.getWorkspaceFolder(targetUri) : undefined - // Cloud9 currently can't resolve the `aws-sam` debug config provider. - // Directly invoke the config instead. - // NOTE: This bypasses the `${workspaceFolder}` resolution, but shouldn't naturally occur in Cloud9 - // (Cloud9 also doesn't currently have variable resolution support anyways) - if (isCloud9()) { - const provider = new SamDebugConfigProvider(this.extContext) - await provider.resolveDebugConfiguration(folder, finalConfig, undefined, source) - } else { - // startDebugging on VS Code goes through the whole resolution chain - await vscode.debug.startDebugging(folder, finalConfig) - } + // startDebugging on VS Code goes through the whole resolution chain + await vscode.debug.startDebugging(folder, finalConfig) } public async getLaunchConfigQuickPickItems( launchConfig: LaunchConfiguration, @@ -426,9 +413,9 @@ export class SamInvokeWebview extends VueWebview { const WebviewPanel = VueWebview.compilePanel(SamInvokeWebview) -export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposable { +export function registerSamInvokeVueCommand(context: vscode.ExtensionContext): vscode.Disposable { return Commands.register('aws.launchConfigForm', async (launchConfig?: AwsSamDebuggerConfiguration) => { - const webview = new WebviewPanel(context.extensionContext, context, launchConfig) + const webview = new WebviewPanel(context, launchConfig) await telemetry.sam_openConfigUi.run(async (span) => { await webview.show({ title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'), @@ -439,11 +426,14 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa }) } -export async function registerSamDebugInvokeVueCommand(context: ExtContext, params: { resource: ResourceNode }) { +export async function registerSamDebugInvokeVueCommand( + context: vscode.ExtensionContext, + params: { resource: ResourceNode } +) { const launchConfig: AwsSamDebuggerConfiguration | undefined = undefined const resource = params?.resource.resource const source = 'AppBuilderLocalInvoke' - const webview = new WebviewPanel(context.extensionContext, context, launchConfig, { + const webview = new WebviewPanel(context, launchConfig, { logicalId: resource.resource.Id ?? '', region: resource.region ?? '', location: resource.location.fsPath, diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index 501304c1a94..38b3700719c 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -219,7 +219,8 @@ export class RemoteInvokeWebview extends VueWebview { return } const sampleUrl = `${sampleRequestPath}${pickerResponse.filename}` - const sample = (await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get()) ?? '' + const resp = await new HttpResourceFetcher(sampleUrl, { showUrl: true }).get() + const sample = (await resp?.text()) ?? '' return sample } catch (err) { diff --git a/packages/core/src/login/webview/commonAuthViewProvider.ts b/packages/core/src/login/webview/commonAuthViewProvider.ts index d5f94748e07..8f641acdb8d 100644 --- a/packages/core/src/login/webview/commonAuthViewProvider.ts +++ b/packages/core/src/login/webview/commonAuthViewProvider.ts @@ -11,7 +11,7 @@ "type": "webview", "id": "aws.AmazonCommonAuth", "name": "%AWS.amazonq.login%", -"when": "!isCloud9 && !aws.isSageMaker && !aws.amazonq.showView" +"when": "!aws.isSageMaker && !aws.amazonq.showView" }, * 2. Assign when clause context to this view. Manage the state of when clause context. diff --git a/packages/core/src/notifications/controller.ts b/packages/core/src/notifications/controller.ts index 83886a14c1a..41ec81f8500 100644 --- a/packages/core/src/notifications/controller.ts +++ b/packages/core/src/notifications/controller.ts @@ -24,11 +24,11 @@ import { NotificationsNode } from './panelNode' import { Commands } from '../shared/vscode/commands2' import { RuleEngine } from './rules' import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' -import { withRetries } from '../shared/utilities/functionUtils' import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher' import { isAmazonQ } from '../shared/extensionUtilities' import { telemetry } from '../shared/telemetry/telemetry' import { randomUUID } from '../shared/crypto' +import { waitUntil } from '../shared/utilities/timeoutUtils' const logger = getLogger('notifications') @@ -266,8 +266,8 @@ export interface NotificationFetcher { } export class RemoteFetcher implements NotificationFetcher { - public static readonly retryNumber = 5 public static readonly retryIntervalMs = 30000 + public static readonly retryTimeout = RemoteFetcher.retryIntervalMs * 5 private readonly startUpEndpoint: string = 'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/startup/1.x.json' @@ -286,7 +286,7 @@ export class RemoteFetcher implements NotificationFetcher { }) logger.verbose('Attempting to fetch notifications for category: %s at endpoint: %s', category, endpoint) - return withRetries( + return waitUntil( async () => { try { return await fetcher.getNewETagContent(versionTag) @@ -296,8 +296,9 @@ export class RemoteFetcher implements NotificationFetcher { } }, { - maxRetries: RemoteFetcher.retryNumber, - delay: RemoteFetcher.retryIntervalMs, + interval: RemoteFetcher.retryIntervalMs, + timeout: RemoteFetcher.retryTimeout, + retryOnFail: true, // No exponential backoff - necessary? } ) diff --git a/packages/core/src/shared/cloudformation/cloudformation.ts b/packages/core/src/shared/cloudformation/cloudformation.ts index f845a73ebc4..a22ff6d75b2 100644 --- a/packages/core/src/shared/cloudformation/cloudformation.ts +++ b/packages/core/src/shared/cloudformation/cloudformation.ts @@ -11,7 +11,6 @@ import * as filesystemUtilities from '../filesystemUtilities' import fs from '../../shared/fs/fs' import { getLogger } from '../logger' import { lambdaPackageTypeImage } from '../constants' -import { isCloud9 } from '../extensionUtilities' import { isUntitledScheme, normalizeVSCodeUri } from '../utilities/vsCodeUtils' export const SERVERLESS_API_TYPE = 'AWS::Serverless::Api' // eslint-disable-line @typescript-eslint/naming-convention @@ -893,20 +892,18 @@ export async function createStarterTemplateFile(isSam?: boolean): Promise /** * Creates a boilerplate CFN or SAM template that is complete enough to be picked up for JSON schema assignment - * TODO: Remove `isCloud9` when Cloud9 gets YAML code completion * @param isSam Create a SAM or CFN template */ function createStarterTemplateYaml(isSam?: boolean): string { return `AWSTemplateFormatVersion: '2010-09-09' ${isSam ? 'Transform: AWS::Serverless-2016-10-31\n' : ''} Description: -${isCloud9() ? '' : '\n# Available top-level fields are listed in code completion\n'} +# Available top-level fields are listed in code completion # Add Resources Here: uncomment the following lines # Resources: # : -# Type: # resource type here${isCloud9() ? '' : ' - available resources are listed in code completion'} -# # # Properties: ` diff --git a/packages/core/src/shared/crashMonitoring.ts b/packages/core/src/shared/crashMonitoring.ts index 5638ad875f0..662a7907875 100644 --- a/packages/core/src/shared/crashMonitoring.ts +++ b/packages/core/src/shared/crashMonitoring.ts @@ -17,8 +17,8 @@ import fs from './fs/fs' import { getLogger } from './logger/logger' import { crashMonitoringDirName } from './constants' import { throwOnUnstableFileSystem } from './filesystemUtilities' -import { withRetries } from './utilities/functionUtils' import { truncateUuid } from './crypto' +import { waitUntil } from './utilities/timeoutUtils' const className = 'CrashMonitoring' @@ -489,7 +489,12 @@ export class FileSystemState { this.deps.devLogger?.debug(`HEARTBEAT sent for ${truncateUuid(this.ext.sessionId)}`) } const funcWithCtx = () => withFailCtx('sendHeartbeatState', func) - const funcWithRetries = withRetries(funcWithCtx, { maxRetries: 6, delay: 100, backoff: 2 }) + const funcWithRetries = waitUntil(funcWithCtx, { + timeout: 15_000, + interval: 100, + backoff: 2, + retryOnFail: true, + }) return funcWithRetries } catch (e) { @@ -542,7 +547,12 @@ export class FileSystemState { await nodeFs.rm(filePath, { force: true }) } const funcWithCtx = () => withFailCtx(ctx, func) - const funcWithRetries = withRetries(funcWithCtx, { maxRetries: 6, delay: 100, backoff: 2 }) + const funcWithRetries = waitUntil(funcWithCtx, { + timeout: 15_000, + interval: 100, + backoff: 2, + retryOnFail: true, + }) await funcWithRetries } @@ -609,7 +619,7 @@ export class FileSystemState { } const funcWithIgnoreBadFile = () => ignoreBadFileError(loadExtFromDisk) const funcWithRetries = () => - withRetries(funcWithIgnoreBadFile, { maxRetries: 6, delay: 100, backoff: 2 }) + waitUntil(funcWithIgnoreBadFile, { timeout: 15_000, interval: 100, backoff: 2, retryOnFail: true }) const funcWithCtx = () => withFailCtx('parseRunningExtFile', funcWithRetries) const ext: ExtInstanceHeartbeat | undefined = await funcWithCtx() diff --git a/packages/core/src/shared/datetime.ts b/packages/core/src/shared/datetime.ts index bc7f562f83f..6123421666a 100644 --- a/packages/core/src/shared/datetime.ts +++ b/packages/core/src/shared/datetime.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { isCloud9 } from './extensionUtilities' - // constants for working with milliseconds export const oneSecond = 1000 export const oneMinute = oneSecond * 60 @@ -119,7 +117,7 @@ export function getRelativeDate(from: Date, now: Date = new Date()): string { * US: Jan 5, 2020 5:30:20 PM GMT-0700 * GB: 5 Jan 2020 17:30:20 GMT+0100 */ -export function formatLocalized(d: Date = new Date(), cloud9 = isCloud9()): string { +export function formatLocalized(d: Date = new Date()): string { const dateFormat = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', @@ -129,7 +127,7 @@ export function formatLocalized(d: Date = new Date(), cloud9 = isCloud9()): stri hour: 'numeric', minute: 'numeric', second: 'numeric', - timeZoneName: cloud9 ? 'short' : 'shortOffset', + timeZoneName: 'shortOffset', }) return `${dateFormat.format(d)} ${timeFormat.format(d)}` diff --git a/packages/core/src/shared/extensionStartup.ts b/packages/core/src/shared/extensionStartup.ts index fd5aab755e7..740534e2177 100644 --- a/packages/core/src/shared/extensionStartup.ts +++ b/packages/core/src/shared/extensionStartup.ts @@ -4,21 +4,14 @@ */ import * as _ from 'lodash' -import * as path from 'path' import * as vscode from 'vscode' import * as semver from 'semver' -import * as nls from 'vscode-nls' - -import { BaseTemplates } from './templates/baseTemplates' -import { fs } from '../shared/fs/fs' -import { getIdeProperties, getIdeType, isAmazonQ, isCloud9, isCn, productName } from './extensionUtilities' +import { getIdeType, isAmazonQ, productName } from './extensionUtilities' import * as localizedText from './localizedText' import { AmazonQPromptSettings, ToolkitPromptSettings } from './settings' import { showMessage } from './utilities/messages' import { getTelemetryReasonDesc } from './errors' -const localize = nls.loadMessageBundle() - /** * Shows a (suppressible) warning if the current vscode version is older than `minVscode`. */ @@ -48,75 +41,3 @@ export async function maybeShowMinVscodeWarning(minVscode: string) { }) } } - -/** - * Helper function to show a webview containing the quick start page - * - * @param context VS Code Extension Context - */ -export async function showQuickStartWebview(context: vscode.ExtensionContext): Promise { - try { - const view = await createQuickStartWebview(context) - view.reveal() - } catch { - void vscode.window.showErrorMessage( - localize('AWS.command.quickStart.error', 'Error while loading Quick Start page') - ) - } -} - -/** - * Helper function to create a webview containing the quick start page - * Returns an unfocused vscode.WebviewPanel if the quick start page is renderable. - * - * @param context VS Code Extension Context - * @param page Page to load (use for testing) - */ -export async function createQuickStartWebview( - context: vscode.ExtensionContext, - page?: string -): Promise { - let actualPage: string - if (page) { - actualPage = page - } else if (isCloud9()) { - actualPage = `quickStartCloud9${isCn() ? '-cn' : ''}.html` - } else { - actualPage = 'quickStartVscode.html' - } - // create hidden webview, leave it up to the caller to show - const view = vscode.window.createWebviewPanel( - 'html', - localize('AWS.command.quickStart.title', '{0} Toolkit - Quick Start', getIdeProperties().company), - { viewColumn: vscode.ViewColumn.Active, preserveFocus: true }, - { enableScripts: true } - ) - - const baseTemplateFn = _.template(BaseTemplates.simpleHtml) - - const htmlBody = convertExtensionRootTokensToPath( - await fs.readFileText(path.join(context.extensionPath, actualPage)), - context.extensionPath, - view.webview - ) - - view.webview.html = baseTemplateFn({ - cspSource: view.webview.cspSource, - content: htmlBody, - }) - - return view -} - -/** - * Utility function to search for tokens in a string and convert them to relative paths parseable by VS Code - * Useful for converting HTML images to webview-usable images - * - * @param text Text to scan - * @param basePath Extension path (from extension context) - */ -function convertExtensionRootTokensToPath(text: string, basePath: string, webview: vscode.Webview): string { - return text.replace(/!!EXTENSIONROOT!!(?[-a-zA-Z0-9@:%_\+.~#?&//=]*)/g, (matchedString, restOfUrl) => { - return webview.asWebviewUri(vscode.Uri.file(`${basePath}${restOfUrl}`)).toString() - }) -} diff --git a/packages/core/src/shared/extensionUtilities.ts b/packages/core/src/shared/extensionUtilities.ts index 4d758182205..01d064f88b8 100644 --- a/packages/core/src/shared/extensionUtilities.ts +++ b/packages/core/src/shared/extensionUtilities.ts @@ -12,7 +12,6 @@ import { VSCODE_EXTENSION_ID, extensionAlphaVersion } from './extensions' import { Ec2MetadataClient } from './clients/ec2MetadataClient' import { DefaultEc2MetadataClient } from './clients/ec2MetadataClient' import { extensionVersion, getCodeCatalystDevEnvId } from './vscode/env' -import { DevSettings } from './settings' import globals from './extensionGlobals' import { once } from './utilities/functionUtils' import { @@ -61,12 +60,7 @@ export function commandsPrefix(): string { let computeRegion: string | undefined = notInitialized export function getIdeType(): 'vscode' | 'cloud9' | 'sagemaker' | 'unknown' { - const settings = DevSettings.instance - if ( - vscode.env.appName === cloud9Appname || - vscode.env.appName === cloud9CnAppname || - settings.get('forceCloud9', false) - ) { + if (vscode.env.appName === cloud9Appname || vscode.env.appName === cloud9CnAppname) { return 'cloud9' } diff --git a/packages/core/src/shared/extensions.ts b/packages/core/src/shared/extensions.ts index a4682cc3a18..d9b242e96a6 100644 --- a/packages/core/src/shared/extensions.ts +++ b/packages/core/src/shared/extensions.ts @@ -31,9 +31,7 @@ export const vscodeExtensionMinVersion = { remotessh: '0.74.0', } -/** - * Long-lived, extension-scoped, shared globals. - */ +/** @deprecated Use `extensionGlobals.ts:globals` instead. */ export interface ExtContext { extensionContext: vscode.ExtensionContext awsContext: AwsContext diff --git a/packages/core/src/shared/filesystemUtilities.ts b/packages/core/src/shared/filesystemUtilities.ts index 1c8cf40f11a..d462d85ac15 100644 --- a/packages/core/src/shared/filesystemUtilities.ts +++ b/packages/core/src/shared/filesystemUtilities.ts @@ -39,7 +39,6 @@ export async function getDirSize(dirPath: string, startTime: number, duration: n return getDirSize(filePath, startTime, duration) } if (type === vscode.FileType.File) { - // TODO: This is SLOW on Cloud9. const stat = await fs.stat(filePath) return stat.size } diff --git a/packages/core/src/shared/fs/fs.ts b/packages/core/src/shared/fs/fs.ts index 477b16c12ea..1c7132648bc 100644 --- a/packages/core/src/shared/fs/fs.ts +++ b/packages/core/src/shared/fs/fs.ts @@ -6,7 +6,6 @@ import vscode from 'vscode' import os from 'os' import { promises as nodefs, constants as nodeConstants, WriteFileOptions } from 'fs' // eslint-disable-line no-restricted-imports import { chmod } from 'fs/promises' -import { isCloud9 } from '../extensionUtilities' import _path from 'path' import { PermissionsError, @@ -93,15 +92,6 @@ export class FileSystem { const uri = toUri(path) const errHandler = createPermissionsErrorHandler(this.isWeb, vscode.Uri.joinPath(uri, '..'), '*wx') - // Certain URIs are not supported with vscode.workspace.fs in Cloud9 - // so revert to using `fs` which works. - if (isCloud9()) { - return nodefs - .mkdir(uri.fsPath, { recursive: true }) - .then(() => {}) - .catch(errHandler) - } - return vfs.createDirectory(uri).then(undefined, errHandler) } @@ -114,12 +104,9 @@ export class FileSystem { const uri = toUri(path) const errHandler = createPermissionsErrorHandler(this.isWeb, uri, 'r**') - if (isCloud9()) { - return await nodefs.readFile(uri.fsPath).catch(errHandler) - } - return vfs.readFile(uri).then(undefined, errHandler) } + /** * Read file and convert the resulting bytes to a string. * @param path uri or path to file. @@ -170,22 +157,6 @@ export class FileSystem { // Note: comparison is bitwise (&) because `FileType` enum is bitwise. const anyKind = fileType === undefined || fileType & vscode.FileType.Unknown - if (isCloud9()) { - // vscode.workspace.fs.stat() is SLOW. Avoid it on Cloud9. - try { - const stat = await nodefs.stat(uri.fsPath) - if (anyKind) { - return true - } else if (fileType & vscode.FileType.Directory) { - return stat.isDirectory() - } else if (fileType & vscode.FileType.File) { - return stat.isFile() - } - } catch { - return false - } - } - const r = await this.stat(uri).then( (r) => r, (err) => !isFileNotFoundError(err) @@ -240,16 +211,7 @@ export class FileSystem { // Node writeFile is the only way to set `writeOpts`, such as the `mode`, on a file . // When not in web we will use Node's writeFile() for all other scenarios. // It also has better error messages than VS Code's writeFile(). - let write = (u: Uri) => nodefs.writeFile(u.fsPath, content, opts).then(undefined, errHandler) - - if (isCloud9()) { - // In Cloud9 vscode.workspace.writeFile has limited functionality, e.g. cannot write outside - // of open workspace. - // - // This is future proofing in the scenario we switch the initial implementation of `write()` - // to something else, C9 will still use node fs. - write = (u: Uri) => nodefs.writeFile(u.fsPath, content, opts).then(undefined, errHandler) - } + const write = (u: Uri) => nodefs.writeFile(u.fsPath, content, opts).then(undefined, errHandler) // Node writeFile does NOT create parent folders by default, unlike VS Code FS writeFile() await fs.mkdir(_path.dirname(uri.fsPath)) @@ -317,10 +279,6 @@ export class FileSystem { const newUri = toUri(newPath) const errHandler = createPermissionsErrorHandler(this.isWeb, oldUri, 'rw*') - if (isCloud9()) { - return nodefs.rename(oldUri.fsPath, newUri.fsPath).catch(errHandler) - } - /** * We were seeing 'FileNotFound' errors during renames, even though we did a `writeFile()` right before the rename. * The error looks to be from here: https://github.com/microsoft/vscode/blob/09d5f4efc5089ce2fc5c8f6aeb51d728d7f4e758/src/vs/platform/files/node/diskFileSystemProvider.ts#L747 @@ -411,12 +369,6 @@ export class FileSystem { const parent = vscode.Uri.joinPath(uri, '..') const errHandler = createPermissionsErrorHandler(this.isWeb, parent, '*wx') - if (isCloud9()) { - // Cloud9 does not support vscode.workspace.fs.delete. - opt.force = !!opt.recursive - return nodefs.rm(uri.fsPath, opt).catch(errHandler) - } - if (opt.recursive) { // Error messages may be misleading if using the `recursive` option. // Need to implement our own recursive delete if we want detailed info. @@ -464,18 +416,9 @@ export class FileSystem { } async readdir(uri: vscode.Uri | string): Promise<[string, vscode.FileType][]> { - const path = toUri(uri) - - // readdir is not a supported vscode API in Cloud9 - if (isCloud9()) { - return (await nodefs.readdir(path.fsPath, { withFileTypes: true })).map((e) => [ - e.name, - e.isDirectory() ? vscode.FileType.Directory : vscode.FileType.File, - ]) - } - - return await vfs.readDirectory(path) + return await vfs.readDirectory(toUri(uri)) } + /** * Copy target file or directory * @param source diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index 645929b918f..defb7658f68 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -32,6 +32,7 @@ export type globalKey = | 'aws.amazonq.hasShownWalkthrough' | 'aws.amazonq.showTryChatCodeLens' | 'aws.amazonq.securityIssueFilters' + | 'aws.amazonq.codescan.groupingStrategy' | 'aws.amazonq.notifications' | 'aws.amazonq.welcomeChatShowCount' | 'aws.amazonq.disclaimerAcknowledged' diff --git a/packages/core/src/shared/icons.ts b/packages/core/src/shared/icons.ts index e4379bccf9a..76d81339fb0 100644 --- a/packages/core/src/shared/icons.ts +++ b/packages/core/src/shared/icons.ts @@ -9,7 +9,6 @@ import type * as packageJson from '../../package.json' import * as fs from 'fs' // eslint-disable-line no-restricted-imports import * as path from 'path' import { Uri, ThemeIcon, ThemeColor } from 'vscode' -import { isCloud9 } from './extensionUtilities' import { memoize } from './utilities/functionUtils' import { getLogger } from './logger/logger' @@ -45,7 +44,7 @@ export const getIcon = memoize(resolveIconId) * ``` */ export function codicon(parts: TemplateStringsArray, ...components: (string | IconPath)[]): string { - const canUse = (sub: string | IconPath) => typeof sub === 'string' || (!isCloud9() && sub instanceof Icon) + const canUse = (sub: string | IconPath) => typeof sub === 'string' || sub instanceof Icon const resolved = components.map((i) => (canUse(i) ? i : '')).map(String) return parts @@ -80,7 +79,7 @@ export class Icon extends ThemeIcon { * {@link https://code.visualstudio.com/api/references/contribution-points#contributes.colors here} */ export function addColor(icon: IconPath, color: string | ThemeColor): IconPath { - if (isCloud9() || !(icon instanceof Icon)) { + if (!(icon instanceof Icon)) { return icon } @@ -89,32 +88,20 @@ export function addColor(icon: IconPath, color: string | ThemeColor): IconPath { function resolveIconId( id: IconId, - shouldUseCloud9 = isCloud9(), iconsPath = globals.context.asAbsolutePath(path.join('resources', 'icons')) ): IconPath { const [namespace, ...rest] = id.split('-') const name = rest.join('-') - // This 'override' logic is to support legacy use-cases, though ideally we wouldn't need it at all - const cloud9Override = shouldUseCloud9 ? resolvePathsSync(path.join(iconsPath, 'cloud9'), id) : undefined - const override = cloud9Override ?? resolvePathsSync(path.join(iconsPath, namespace), name) + const override = resolvePathsSync(path.join(iconsPath, namespace), name) if (override) { getLogger().verbose(`icons: using override for "${id}"`) return override } - // TODO: remove when they support codicons + the contribution point - if (shouldUseCloud9) { - const generated = resolvePathsSync(path.join(iconsPath, 'cloud9', 'generated'), id) - - if (generated) { - return generated - } - } - // TODO: potentially embed the icon source in `package.json` to avoid this messy mapping // of course, doing that implies we must always bundle both the original icon files and the font file - const source = !['cloud9', 'vscode'].includes(namespace) + const source = !['vscode'].includes(namespace) ? Uri.joinPath(Uri.file(iconsPath), namespace, rest[0], `${rest.slice(1).join('-')}.svg`) : undefined diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index a4f35d525c6..ea73349e817 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -22,6 +22,7 @@ export { getMachineId } from './vscode/env' export { getLogger } from './logger/logger' export { activateExtension, openUrl } from './utilities/vsCodeUtils' export { waitUntil, sleep, Timeout } from './utilities/timeoutUtils' +export * as timeoutUtils from './utilities/timeoutUtils' export { Prompter } from './ui/prompter' export { VirtualFileSystem } from './virtualFilesystem' export { VirtualMemoryFile } from './virtualMemoryFile' diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index 9cc04aa6585..d5bf5f13380 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode' -export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'unknown' +export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'childProcess' | 'unknown' class ErrorLog { constructor( diff --git a/packages/core/src/shared/regions/regionProvider.ts b/packages/core/src/shared/regions/regionProvider.ts index c9c010a250a..00d13942196 100644 --- a/packages/core/src/shared/regions/regionProvider.ts +++ b/packages/core/src/shared/regions/regionProvider.ts @@ -12,7 +12,7 @@ import * as vscode from 'vscode' import { getLogger } from '../logger' import { Endpoints, loadEndpoints, Region } from './endpoints' import { AwsContext } from '../awsContext' -import { getIdeProperties, isAmazonQ, isCloud9 } from '../extensionUtilities' +import { getIdeProperties, isAmazonQ } from '../extensionUtilities' import { ResourceFetcher } from '../resourcefetcher/resourcefetcher' import { isSsoConnection } from '../../auth/connection' import { Auth } from '../../auth/auth' @@ -185,17 +185,10 @@ export class RegionProvider { 'AWS.error.endpoint.load.failure', 'The {0} Toolkit was unable to load endpoints data.', getIdeProperties().company - )} ${ - isCloud9() - ? localize( - 'AWS.error.impactedFunctionalityReset.cloud9', - 'Toolkit functionality may be impacted until the Cloud9 browser tab is refreshed.' - ) - : localize( - 'AWS.error.impactedFunctionalityReset.vscode', - 'Toolkit functionality may be impacted until VS Code is restarted.' - ) - }` + )} ${localize( + 'AWS.error.impactedFunctionalityReset.vscode', + 'Toolkit functionality may be impacted until VS Code is restarted.' + )}` ) }) @@ -203,8 +196,11 @@ export class RegionProvider { } } -export async function getEndpointsFromFetcher(fetcher: ResourceFetcher): Promise { - const endpointsJson = await fetcher.get() +export async function getEndpointsFromFetcher( + fetcher: ResourceFetcher | ResourceFetcher +): Promise { + const contents = await fetcher.get() + const endpointsJson = typeof contents === 'string' ? contents : await contents?.text() if (!endpointsJson) { throw new Error('Failed to get resource') } diff --git a/packages/core/src/shared/request.ts b/packages/core/src/shared/request.ts index 9f3bf50047f..827e3e3e445 100644 --- a/packages/core/src/shared/request.ts +++ b/packages/core/src/shared/request.ts @@ -99,7 +99,11 @@ class FetchRequest { } async #throwIfBadResponse(request: RequestParams, response: Response, url: string) { - if (response.ok) { + /** + * response.ok only returns true for 200-299. + * We need to explicitly allow 304 since it means the cached version is still valid + */ + if (response.ok || response.status === 304) { return } diff --git a/packages/core/src/shared/resourcefetcher/compositeResourceFetcher.ts b/packages/core/src/shared/resourcefetcher/compositeResourceFetcher.ts deleted file mode 100644 index d511b324502..00000000000 --- a/packages/core/src/shared/resourcefetcher/compositeResourceFetcher.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getLogger, Logger } from '../logger' -import { ResourceFetcher } from './resourcefetcher' - -// TODO: replace this with something more generic like Log.all(...) -export class CompositeResourceFetcher implements ResourceFetcher { - private readonly logger: Logger = getLogger() - private readonly fetchers: ResourceFetcher[] - - /** - * @param fetchers - resource load is attempted from provided fetchers until one succeeds - */ - public constructor(...fetchers: ResourceFetcher[]) { - this.fetchers = fetchers - } - - /** - * Returns the contents of the resource from the first fetcher that successfully retrieves it, or undefined if the resource could not be retrieved. - */ - public async get(): Promise { - for (const fetcher of this.fetchers) { - const contents = await fetcher.get().catch((err) => { - this.logger.debug('fetch failed: %s', (err as Error).message) - }) - if (contents) { - return contents - } - } - } -} diff --git a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts index f2da4ba98aa..e85e1ded70b 100644 --- a/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts +++ b/packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts @@ -3,42 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs' // eslint-disable-line no-restricted-imports -import * as http from 'http' -import * as https from 'https' -import * as stream from 'stream' -import got, { Response, RequestError, CancelError } from 'got' -import urlToOptions from 'got/dist/source/core/utils/url-to-options' -import Request from 'got/dist/source/core' import { VSCODE_EXTENSION_ID } from '../extensions' import { getLogger, Logger } from '../logger' import { ResourceFetcher } from './resourcefetcher' -import { Timeout, CancellationError, CancelEvent } from '../utilities/timeoutUtils' -import { isCloud9 } from '../extensionUtilities' -import { Headers } from 'got/dist/source/core' - -// XXX: patched Got module for compatability with older VS Code versions (e.g. Cloud9) -// `got` has also deprecated `urlToOptions` -const patchedGot = got.extend({ - request: (url, options, callback) => { - if (url.protocol === 'https:') { - return https.request({ ...options, ...urlToOptions(url) }, callback) - } - return http.request({ ...options, ...urlToOptions(url) }, callback) - }, -}) - -/** Promise that resolves/rejects when all streams close. Can also access streams directly. */ -type FetcherResult = Promise & { - /** Download stream piped to `fsStream`. */ - requestStream: Request // `got` doesn't add the correct types to 'on' for some reason - /** Stream writing to the file system. */ - fsStream: fs.WriteStream -} +import { Timeout, CancelEvent, waitUntil } from '../utilities/timeoutUtils' +import request, { RequestError } from '../request' type RequestHeaders = { eTag?: string; gZip?: boolean } -export class HttpResourceFetcher implements ResourceFetcher { +export class HttpResourceFetcher implements ResourceFetcher { private readonly logger: Logger = getLogger() /** @@ -47,38 +20,23 @@ export class HttpResourceFetcher implements ResourceFetcher { * @param params Additional params for the fetcher * @param {boolean} params.showUrl Whether or not to the URL in log statements. * @param {string} params.friendlyName If URL is not shown, replaces the URL with this text. - * @param {function} params.onSuccess Function to execute on successful request. No effect if piping to a location. * @param {Timeout} params.timeout Timeout token to abort/cancel the request. Similar to `AbortSignal`. + * @param {number} params.retries The number of retries a get request should make if one fails */ public constructor( private readonly url: string, private readonly params: { showUrl: boolean friendlyName?: string - onSuccess?(contents: string): void timeout?: Timeout } ) {} /** - * Returns the contents of the resource, or undefined if the resource could not be retrieved. - * - * @param pipeLocation Optionally pipe the download to a file system location + * Returns the response of the resource, or undefined if the response failed could not be retrieved. */ - public get(): Promise - public get(pipeLocation: string): FetcherResult - public get(pipeLocation?: string): Promise | FetcherResult { + public get(): Promise { this.logger.verbose(`downloading: ${this.logText()}`) - - if (pipeLocation) { - const result = this.pipeGetRequest(pipeLocation, this.params.timeout) - result.fsStream.on('exit', () => { - this.logger.verbose(`downloaded: ${this.logText()}`) - }) - - return result - } - return this.downloadRequest() } @@ -94,44 +52,35 @@ export class HttpResourceFetcher implements ResourceFetcher { public async getNewETagContent(eTag?: string): Promise<{ content?: string; eTag: string }> { const response = await this.getResponseFromGetRequest(this.params.timeout, { eTag, gZip: true }) - const eTagResponse = response.headers.etag + const eTagResponse = response.headers.get('etag') if (!eTagResponse) { throw new Error(`This URL does not support E-Tags. Cannot use this function for: ${this.url.toString()}`) } // NOTE: Even with use of `gzip` encoding header, the response content is uncompressed. // Most likely due to the http request library uncompressing it for us. - let contents: string | undefined = response.body.toString() - if (response.statusCode === 304) { + let contents: string | undefined = await response.text() + if (response.status === 304) { // Explanation: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match contents = undefined this.logger.verbose(`E-Tag, ${eTagResponse}, matched. No content downloaded from: ${this.url}`) } else { this.logger.verbose(`No E-Tag match. Downloaded content from: ${this.logText()}`) - if (this.params.onSuccess) { - this.params.onSuccess(contents) - } } return { content: contents, eTag: eTagResponse } } - private async downloadRequest(): Promise { + private async downloadRequest(): Promise { try { - // HACK(?): receiving JSON as a string without `toString` makes it so we can't deserialize later - const contents = (await this.getResponseFromGetRequest(this.params.timeout)).body.toString() - if (this.params.onSuccess) { - this.params.onSuccess(contents) - } - + const resp = await this.getResponseFromGetRequest(this.params.timeout) this.logger.verbose(`downloaded: ${this.logText()}`) - - return contents + return resp } catch (err) { - const error = err as CancelError | RequestError + const error = err as RequestError this.logger.verbose( `Error downloading ${this.logText()}: %s`, - error.message ?? error.code ?? error.response?.statusMessage ?? error.response?.statusCode + error.message ?? error.code ?? error.response.statusText ?? error.response.status ) return undefined } @@ -145,56 +94,40 @@ export class HttpResourceFetcher implements ResourceFetcher { getLogger().debug(`Download for "${this.logText()}" ${event.agent === 'user' ? 'cancelled' : 'timed out'}`) } - // TODO: make pipeLocation a vscode.Uri - private pipeGetRequest(pipeLocation: string, timeout?: Timeout): FetcherResult { - const requester = isCloud9() ? patchedGot : got - const requestStream = requester.stream(this.url, { headers: this.buildRequestHeaders() }) - const fsStream = fs.createWriteStream(pipeLocation) - - const done = new Promise((resolve, reject) => { - const pipe = stream.pipeline(requestStream, fsStream, (err) => { - if (err instanceof RequestError) { - return reject(Object.assign(new Error('Failed to download file'), { code: err.code })) - } - err ? reject(err) : resolve() - }) - - const cancelListener = timeout?.token.onCancellationRequested((event) => { - this.logCancellation(event) - pipe.destroy(new CancellationError(event.agent)) - }) - - pipe.on('close', () => cancelListener?.dispose()) - }) - - return Object.assign(done, { requestStream, fsStream }) - } - - private async getResponseFromGetRequest(timeout?: Timeout, headers?: RequestHeaders): Promise> { - const requester = isCloud9() ? patchedGot : got - const promise = requester(this.url, { - headers: this.buildRequestHeaders(headers), - }) - - const cancelListener = timeout?.token.onCancellationRequested((event) => { - this.logCancellation(event) - promise.cancel(new CancellationError(event.agent).message) - }) - - return promise.finally(() => cancelListener?.dispose()) + private async getResponseFromGetRequest(timeout?: Timeout, headers?: RequestHeaders): Promise { + return waitUntil( + () => { + const req = request.fetch('GET', this.url, { + headers: this.buildRequestHeaders(headers), + }) + + const cancelListener = timeout?.token.onCancellationRequested((event) => { + this.logCancellation(event) + req.cancel() + }) + + return req.response.finally(() => cancelListener?.dispose()) + }, + { + timeout: 3000, + interval: 100, + backoff: 2, + retryOnFail: true, + } + ) } private buildRequestHeaders(requestHeaders?: RequestHeaders): Headers { - const headers: Headers = {} + const headers = new Headers() - headers['User-Agent'] = VSCODE_EXTENSION_ID.awstoolkit + headers.set('User-Agent', VSCODE_EXTENSION_ID.awstoolkit) if (requestHeaders?.eTag !== undefined) { - headers['If-None-Match'] = requestHeaders.eTag + headers.set('If-None-Match', requestHeaders.eTag) } if (requestHeaders?.gZip) { - headers['Accept-Encoding'] = 'gzip' + headers.set('Accept-Encoding', 'gzip') } return headers @@ -214,7 +147,8 @@ export async function getPropertyFromJsonUrl( fetcher?: HttpResourceFetcher ): Promise { const resourceFetcher = fetcher ?? new HttpResourceFetcher(url, { showUrl: true }) - const result = await resourceFetcher.get() + const resp = await resourceFetcher.get() + const result = await resp?.text() if (result) { try { const json = JSON.parse(result) diff --git a/packages/core/src/shared/resourcefetcher/node/httpResourceFetcher.ts b/packages/core/src/shared/resourcefetcher/node/httpResourceFetcher.ts new file mode 100644 index 00000000000..d801e8c5027 --- /dev/null +++ b/packages/core/src/shared/resourcefetcher/node/httpResourceFetcher.ts @@ -0,0 +1,129 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs' // eslint-disable-line no-restricted-imports +import * as http from 'http' +import * as https from 'https' +import * as stream from 'stream' +import got, { RequestError } from 'got' +import urlToOptions from 'got/dist/source/core/utils/url-to-options' +import Request from 'got/dist/source/core' +import { VSCODE_EXTENSION_ID } from '../../extensions' +import { getLogger, Logger } from '../../logger' +import { Timeout, CancellationError, CancelEvent } from '../../utilities/timeoutUtils' +import { isCloud9 } from '../../extensionUtilities' +import { Headers } from 'got/dist/source/core' + +// XXX: patched Got module for compatability with older VS Code versions (e.g. Cloud9) +// `got` has also deprecated `urlToOptions` +const patchedGot = got.extend({ + request: (url, options, callback) => { + if (url.protocol === 'https:') { + return https.request({ ...options, ...urlToOptions(url) }, callback) + } + return http.request({ ...options, ...urlToOptions(url) }, callback) + }, +}) + +/** Promise that resolves/rejects when all streams close. Can also access streams directly. */ +type FetcherResult = Promise & { + /** Download stream piped to `fsStream`. */ + requestStream: Request // `got` doesn't add the correct types to 'on' for some reason + /** Stream writing to the file system. */ + fsStream: fs.WriteStream +} + +type RequestHeaders = { eTag?: string; gZip?: boolean } + +/** + * Legacy HTTP Resource Fetcher used specifically for streaming information. + * Only kept around until web streams are compatible with node streams + */ +export class HttpResourceFetcher { + private readonly logger: Logger = getLogger() + + /** + * + * @param url URL to fetch a response body from via the `get` call + * @param params Additional params for the fetcher + * @param {boolean} params.showUrl Whether or not to the URL in log statements. + * @param {string} params.friendlyName If URL is not shown, replaces the URL with this text. + * @param {function} params.onSuccess Function to execute on successful request. No effect if piping to a location. + * @param {Timeout} params.timeout Timeout token to abort/cancel the request. Similar to `AbortSignal`. + */ + public constructor( + private readonly url: string, + private readonly params: { + showUrl: boolean + friendlyName?: string + timeout?: Timeout + } + ) {} + + /** + * Returns the contents of the resource, or undefined if the resource could not be retrieved. + * + * @param pipeLocation Optionally pipe the download to a file system location + */ + public get(pipeLocation: string): FetcherResult { + this.logger.verbose(`downloading: ${this.logText()}`) + + const result = this.pipeGetRequest(pipeLocation, this.params.timeout) + result.fsStream.on('exit', () => { + this.logger.verbose(`downloaded: ${this.logText()}`) + }) + + return result + } + + private logText(): string { + return this.params.showUrl ? this.url : (this.params.friendlyName ?? 'resource from URL') + } + + private logCancellation(event: CancelEvent) { + getLogger().debug(`Download for "${this.logText()}" ${event.agent === 'user' ? 'cancelled' : 'timed out'}`) + } + + // TODO: make pipeLocation a vscode.Uri + private pipeGetRequest(pipeLocation: string, timeout?: Timeout): FetcherResult { + const requester = isCloud9() ? patchedGot : got + const requestStream = requester.stream(this.url, { headers: this.buildRequestHeaders() }) + const fsStream = fs.createWriteStream(pipeLocation) + + const done = new Promise((resolve, reject) => { + const pipe = stream.pipeline(requestStream, fsStream, (err) => { + if (err instanceof RequestError) { + return reject(Object.assign(new Error('Failed to download file'), { code: err.code })) + } + err ? reject(err) : resolve() + }) + + const cancelListener = timeout?.token.onCancellationRequested((event) => { + this.logCancellation(event) + pipe.destroy(new CancellationError(event.agent)) + }) + + pipe.on('close', () => cancelListener?.dispose()) + }) + + return Object.assign(done, { requestStream, fsStream }) + } + + private buildRequestHeaders(requestHeaders?: RequestHeaders): Headers { + const headers: Headers = {} + + headers['User-Agent'] = VSCODE_EXTENSION_ID.awstoolkit + + if (requestHeaders?.eTag !== undefined) { + headers['If-None-Match'] = requestHeaders.eTag + } + + if (requestHeaders?.gZip) { + headers['Accept-Encoding'] = 'gzip' + } + + return headers + } +} diff --git a/packages/core/src/shared/resourcefetcher/resourcefetcher.ts b/packages/core/src/shared/resourcefetcher/resourcefetcher.ts index de7f6837d2f..8374be22675 100644 --- a/packages/core/src/shared/resourcefetcher/resourcefetcher.ts +++ b/packages/core/src/shared/resourcefetcher/resourcefetcher.ts @@ -4,10 +4,10 @@ */ // TODO: this is just a "thunk". Replace it with something more generic. -export interface ResourceFetcher { +export interface ResourceFetcher { /** * Returns the contents of the resource, or undefined if the resource could not be retrieved. * Implementations are expected to handle Errors. */ - get(): Promise + get(): Promise } diff --git a/packages/core/src/shared/sam/activation.ts b/packages/core/src/shared/sam/activation.ts index 3f398968b19..855dde39a29 100644 --- a/packages/core/src/shared/sam/activation.ts +++ b/packages/core/src/shared/sam/activation.ts @@ -19,7 +19,7 @@ import * as goLensProvider from '../codelens/goCodeLensProvider' import { SamTemplateCodeLensProvider } from '../codelens/samTemplateCodeLensProvider' import * as jsLensProvider from '../codelens/typescriptCodeLensProvider' import { ExtContext, VSCODE_EXTENSION_ID } from '../extensions' -import { getIdeProperties, getIdeType, isCloud9 } from '../extensionUtilities' +import { getIdeProperties, getIdeType } from '../extensionUtilities' import { getLogger } from '../logger/logger' import { PerfLog } from '../logger/perfLogger' import { NoopWatcher } from '../fs/watchedFiles' @@ -28,7 +28,6 @@ import { CodelensRootRegistry } from '../fs/codelensRootRegistry' import { AWS_SAM_DEBUG_TYPE } from './debugger/awsSamDebugConfiguration' import { SamDebugConfigProvider } from './debugger/awsSamDebugger' import { addSamDebugConfiguration } from './debugger/commands/addSamDebugConfiguration' -import { lazyLoadSamTemplateStrings } from '../../lambda/models/samTemplates' import { ToolkitPromptSettings } from '../settings' import { shared } from '../utilities/functionUtils' import { SamCliSettings } from './cli/samCliSettings' @@ -125,7 +124,6 @@ export async function activate(ctx: ExtContext): Promise { } async function registerCommands(ctx: ExtContext, settings: SamCliSettings): Promise { - lazyLoadSamTemplateStrings() ctx.extensionContext.subscriptions.push( Commands.register({ id: 'aws.samcli.detect', autoconnect: false }, () => sharedDetectSamCli({ passive: false, showMessage: true }) @@ -273,13 +271,10 @@ async function activateCodefileOverlays( supportedLanguages[jsLensProvider.javascriptLanguage] = tsCodeLensProvider supportedLanguages[pyLensProvider.pythonLanguage] = pyCodeLensProvider - - if (!isCloud9()) { - supportedLanguages[javaLensProvider.javaLanguage] = javaCodeLensProvider - supportedLanguages[csLensProvider.csharpLanguage] = csCodeLensProvider - supportedLanguages[goLensProvider.goLanguage] = goCodeLensProvider - supportedLanguages[jsLensProvider.typescriptLanguage] = tsCodeLensProvider - } + supportedLanguages[javaLensProvider.javaLanguage] = javaCodeLensProvider + supportedLanguages[csLensProvider.csharpLanguage] = csCodeLensProvider + supportedLanguages[goLensProvider.goLanguage] = goCodeLensProvider + supportedLanguages[jsLensProvider.typescriptLanguage] = tsCodeLensProvider disposables.push(vscode.languages.registerCodeLensProvider(jsLensProvider.typescriptAllFiles, tsCodeLensProvider)) disposables.push(vscode.languages.registerCodeLensProvider(pyLensProvider.pythonAllfiles, pyCodeLensProvider)) diff --git a/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts b/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts index 9573ffac7e6..7905e80321e 100644 --- a/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts +++ b/packages/core/src/shared/sam/cli/samCliLocalInvoke.ts @@ -20,7 +20,6 @@ const localize = nls.loadMessageBundle() export const waitForDebuggerMessages = { PYTHON: 'Debugger waiting for client...', - PYTHON_IKPDB: 'IKP3db listening on', NODEJS: 'Debugger listening on', DOTNET: 'Waiting for the debugger to attach...', GO_DELVE: 'launching process with args', // Comes from https://github.com/go-delve/delve/blob/f5d2e132bca763d222680815ace98601c2396517/service/debugger/debugger.go#L187 diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts b/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts index c249295fd84..4bf40211fa5 100644 --- a/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts +++ b/packages/core/src/shared/sam/debugger/awsSamDebugConfiguration.ts @@ -14,7 +14,6 @@ import { TemplateTargetProperties, } from './awsSamDebugConfiguration.gen' import { getLogger } from '../../logger' -import { isCloud9 } from '../../extensionUtilities' export * from './awsSamDebugConfiguration.gen' @@ -245,11 +244,7 @@ export function createApiAwsSamDebugConfig( function makeWorkspaceRelativePath(folder: vscode.WorkspaceFolder | undefined, target: string): string { if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length <= 1) { - return folder - ? isCloud9() // TODO: remove when Cloud9 supports ${workspaceFolder}. - ? getNormalizedRelativePath(folder.uri.fsPath, target) - : `\${workspaceFolder}/${getNormalizedRelativePath(folder.uri.fsPath, target)}` - : target + return folder ? `\${workspaceFolder}/${getNormalizedRelativePath(folder.uri.fsPath, target)}` : target } return target diff --git a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts index 7e3eca9d53c..f0c277285ac 100644 --- a/packages/core/src/shared/sam/debugger/awsSamDebugger.ts +++ b/packages/core/src/shared/sam/debugger/awsSamDebugger.ts @@ -56,7 +56,7 @@ import { Credentials } from '@aws-sdk/types' import * as CloudFormation from '../../cloudformation/cloudformation' import { getSamCliContext, getSamCliVersion } from '../cli/samCliContext' import { minSamCliVersionForImageSupport, minSamCliVersionForGoSupport } from '../cli/samCliValidator' -import { getIdeProperties, isCloud9 } from '../../extensionUtilities' +import { getIdeProperties } from '../../extensionUtilities' import { resolve } from 'path' import globals from '../../extensionGlobals' import { Runtime, telemetry } from '../../telemetry/telemetry' @@ -205,11 +205,6 @@ export interface SamLaunchRequestArgs extends AwsSamDebuggerConfiguration { */ parameterOverrides?: string[] - /** - * HACK: Forces use of `ikp3db` python debugger in Cloud9 (and in tests). - */ - useIkpdb?: boolean - // // Invocation properties (for "execute" phase, after "config" phase). // Non-serializable... @@ -327,11 +322,6 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider token?: vscode.CancellationToken, source?: string ): Promise { - if (isCloud9()) { - // TODO: remove when Cloud9 supports ${workspaceFolder}. - await this.makeAndInvokeConfig(folder, config, token, source) - return undefined - } return config } @@ -562,19 +552,15 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider } else { const credentialsId = config.aws.credentials const getHelp = localize('AWS.generic.message.getHelp', 'Get Help...') - // TODO: getHelp page for Cloud9. - const extraButtons = isCloud9() - ? [] - : [ - { - label: getHelp, - onClick: () => openUrl(vscode.Uri.parse(credentialHelpUrl)), - }, - ] throw new SamLaunchRequestError(`Invalid credentials found in launch configuration: ${credentialsId}`, { code: 'InvalidCredentials', - extraButtons, + extraButtons: [ + { + label: getHelp, + onClick: () => openUrl(vscode.Uri.parse(credentialHelpUrl)), + }, + ], }) } } @@ -625,7 +611,6 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider region: region, awsCredentials: awsCredentials, parameterOverrides: parameterOverrideArr, - useIkpdb: isCloud9() || !!(config as any).useIkpdb, architecture: architecture, } diff --git a/packages/core/src/shared/sam/debugger/csharpSamDebug.ts b/packages/core/src/shared/sam/debugger/csharpSamDebug.ts index d502d72bbdf..e5c35e9146b 100644 --- a/packages/core/src/shared/sam/debugger/csharpSamDebug.ts +++ b/packages/core/src/shared/sam/debugger/csharpSamDebug.ts @@ -196,8 +196,8 @@ async function downloadInstallScript(debuggerPath: string): Promise { installScriptPath = path.join(debuggerPath, 'installVsdbgScript.sh') } - const installScriptFetcher = new HttpResourceFetcher(installScriptUrl, { showUrl: true }) - const installScript = await installScriptFetcher.get() + const installScriptFetcher = await new HttpResourceFetcher(installScriptUrl, { showUrl: true }).get() + const installScript = await installScriptFetcher?.text() if (!installScript) { throw Error(`Failed to download ${installScriptUrl}`) } diff --git a/packages/core/src/shared/sam/debugger/pythonSamDebug.ts b/packages/core/src/shared/sam/debugger/pythonSamDebug.ts index 45013eaaf96..b802876c1cf 100644 --- a/packages/core/src/shared/sam/debugger/pythonSamDebug.ts +++ b/packages/core/src/shared/sam/debugger/pythonSamDebug.ts @@ -8,7 +8,6 @@ import * as os from 'os' import * as path from 'path' import { isImageLambdaConfig, - PythonCloud9DebugConfiguration, PythonDebugConfiguration, PythonPathMapping, } from '../../../lambda/local/debugConfiguration' @@ -19,9 +18,6 @@ import { fileExists, readFileAsString } from '../../filesystemUtilities' import { getLogger } from '../../logger' import * as pathutil from '../../utilities/pathUtils' import { getLocalRootVariants } from '../../utilities/pathUtils' -import { sleep } from '../../utilities/timeoutUtils' -import { Timeout } from '../../utilities/timeoutUtils' -import { getWorkspaceRelativePath } from '../../utilities/workspaceUtils' import { DefaultSamLocalInvokeCommand, waitForDebuggerMessages } from '../cli/samCliLocalInvoke' import { runLambdaFunction } from '../localLambdaRunner' import { SamLaunchRequestArgs } from './awsSamDebugger' @@ -40,7 +36,6 @@ async function makePythonDebugManifest(params: { isImageLambda: boolean samProjectCodeRoot: string outputDir: string - useIkpdb: boolean }): Promise { let manifestText = '' const manfestPath = path.join(params.samProjectCodeRoot, 'requirements.txt') @@ -50,18 +45,9 @@ async function makePythonDebugManifest(params: { manifestText = await readFileAsString(manfestPath) } getLogger().debug(`pythonCodeLensProvider.makePythonDebugManifest params: %O`, params) - // TODO: If another module name includes the string "ikp3db", this will be skipped... - // HACK: Cloud9-created Lambdas hardcode ikp3db 1.1.4, which only functions with Python 3.6 (which we don't support) - // Remove any ikp3db dependency if it exists and manually add a non-pinned ikp3db dependency. - if (params.useIkpdb) { - manifestText = manifestText.replace(/[ \t]*ikp3db\b[^\r\n]*/, '') - manifestText += `${os.EOL}ikp3db` - await fs.writeFile(debugManifestPath, manifestText) - return debugManifestPath - } // TODO: If another module name includes the string "debugpy", this will be skipped... - if (!params.useIkpdb && !manifestText.includes('debugpy')) { + if (!manifestText.includes('debugpy')) { manifestText += `${os.EOL}debugpy>=1.0,<2` await fs.writeFile(debugManifestPath, manifestText) @@ -76,9 +62,7 @@ async function makePythonDebugManifest(params: { * * Does NOT execute/invoke SAM, docker, etc. */ -export async function makePythonDebugConfig( - config: SamLaunchRequestArgs -): Promise { +export async function makePythonDebugConfig(config: SamLaunchRequestArgs): Promise { if (!config.baseBuildDir) { throw Error('invalid state: config.baseBuildDir was not set') } @@ -97,63 +81,22 @@ export async function makePythonDebugConfig( if (!config.noDebug) { const isImageLambda = await isImageLambdaConfig(config) - if (!config.useIkpdb) { - // Mounted in the Docker container as: /tmp/lambci_debug_files - config.debuggerPath = globals.context.asAbsolutePath(path.join('resources', 'debugger')) - // NOTE: SAM CLI splits on each *single* space in `--debug-args`! - // Extra spaces will be passed as spurious "empty" arguments :( - const debugArgs = `${debugpyWrapperPath} --listen 0.0.0.0:${config.debugPort} --wait-for-client --log-to-stderr` - if (isImageLambda) { - const params = getPythonExeAndBootstrap(config.runtime) - config.debugArgs = [`${params.python} ${debugArgs} ${params.bootstrap}`] - } else { - config.debugArgs = [debugArgs] - } + // Mounted in the Docker container as: /tmp/lambci_debug_files + config.debuggerPath = globals.context.asAbsolutePath(path.join('resources', 'debugger')) + // NOTE: SAM CLI splits on each *single* space in `--debug-args`! + // Extra spaces will be passed as spurious "empty" arguments :( + const debugArgs = `${debugpyWrapperPath} --listen 0.0.0.0:${config.debugPort} --wait-for-client --log-to-stderr` + if (isImageLambda) { + const params = getPythonExeAndBootstrap(config.runtime) + config.debugArgs = [`${params.python} ${debugArgs} ${params.bootstrap}`] } else { - // -ikpdb-log: https://ikpdb.readthedocs.io/en/1.x/api.html?highlight=log#ikpdb.IKPdbLogger - // n,N: Network (noisy) - // b,B: Breakpoints - // e,E: Expression evaluation - // x,X: Execution - // f,F: Frame - // p,P: Path manipulation - // g,G: Global debugger - // - // Level "G" is not too noisy, and is required because it emits the - // "IKP3db listening on" string (`WAIT_FOR_DEBUGGER_MESSAGES`). - const logArg = getLogger().logLevelEnabled('debug') ? '--ikpdb-log=BEXFPG' : '--ikpdb-log=G' - const ccwd = pathutil.normalize( - getWorkspaceRelativePath(config.codeRoot, { workspaceFolders: [config.workspaceFolder] }) - ?.relativePath ?? 'error' - ) - - // NOTE: SAM CLI splits on each *single* space in `--debug-args`! - // Extra spaces will be passed as spurious "empty" arguments :( - // - // -u: (python arg) unbuffered binary stdout/stderr - // - // -ik_ccwd: Must be relative to /var/task, because ikpdb tries to - // resolve filepaths in the Docker container and produces - // nonsense as a "fallback". See `ikp3db.py:normalize_path_in()`: - // https://github.com/cmorisse/ikp3db/blob/eda176a1d4e0b1167466705a26ae4dd5c4188d36/ikp3db.py#L659 - // --ikpdb-protocol=vscode: - // For https://github.com/cmorisse/vscode-ikp3db - // Requires ikp3db 1.5 (unreleased): https://github.com/cmorisse/ikp3db/pull/12 - const debugArgs = `-m ikp3db --ikpdb-address=0.0.0.0 --ikpdb-port=${config.debugPort} -ik_ccwd=${ccwd} -ik_cwd=/var/task ${logArg}` - - if (isImageLambda) { - const params = getPythonExeAndBootstrap(config.runtime) - config.debugArgs = [`${params.python} ${debugArgs} ${params.bootstrap}`] - } else { - config.debugArgs = [debugArgs] - } + config.debugArgs = [debugArgs] } manifestPath = await makePythonDebugManifest({ isImageLambda: isImageLambda, samProjectCodeRoot: config.codeRoot, outputDir: config.baseBuildDir, - useIkpdb: !!config.useIkpdb, }) } @@ -169,30 +112,6 @@ export async function makePythonDebugConfig( }) } - if (config.useIkpdb) { - // Documentation: - // https://github.com/cmorisse/vscode-ikp3db/blob/master/documentation/debug_configurations_reference.md - return { - ...config, - type: 'ikp3db', - request: config.noDebug ? 'launch' : 'attach', - runtimeFamily: RuntimeFamily.Python, - manifestPath: manifestPath, - sam: { - ...config.sam, - // Needed to build ikp3db which has a C build step. - // https://github.com/aws/aws-sam-cli/issues/1840 - containerBuild: true, - }, - - // cloud9 debugger fields: - port: config.debugPort ?? -1, - localRoot: config.codeRoot, - remoteRoot: '/var/task', - address: 'localhost', - } - } - // Make debugpy output log information if our loglevel is at 'debug' if (!config.noDebug && getLogger().logLevelEnabled('debug')) { config.debugArgs![0] += ' --debug' @@ -224,29 +143,13 @@ export async function invokePythonLambda( ctx: ExtContext, config: PythonDebugConfiguration ): Promise { - config.samLocalInvokeCommand = new DefaultSamLocalInvokeCommand([ - waitForDebuggerMessages.PYTHON, - waitForDebuggerMessages.PYTHON_IKPDB, - ]) + config.samLocalInvokeCommand = new DefaultSamLocalInvokeCommand([waitForDebuggerMessages.PYTHON]) - // Must not used waitForPort() for ikpdb: the socket consumes - // ikpdb's initial message and ikpdb does not have a --wait-for-client - // mode, then cloud9 never sees the init message and waits forever. - // - // eslint-disable-next-line @typescript-eslint/unbound-method - config.onWillAttachDebugger = config.useIkpdb ? waitForIkpdb : undefined + config.onWillAttachDebugger = undefined const c = (await runLambdaFunction(ctx, config, async () => {})) as PythonDebugConfiguration return c } -async function waitForIkpdb(debugPort: number, timeout: Timeout) { - // HACK: - // - We cannot consumed the first message on the socket. - // - We must wait for the debugger to be ready, else cloud9 startDebugging() waits forever. - getLogger().info('waitForIkpdb: wait 2 seconds') - await sleep(2000) -} - function getPythonExeAndBootstrap(runtime: Runtime) { // unfortunately new 'Image'-base images did not standardize the paths // https://github.com/aws/aws-sam-cli/blob/7d5101a8edeb575b6925f9adecf28f47793c403c/samcli/local/docker/lambda_debug_settings.py diff --git a/packages/core/src/shared/sam/processTerminal.ts b/packages/core/src/shared/sam/processTerminal.ts index 1482db35d81..a12dccb83d5 100644 --- a/packages/core/src/shared/sam/processTerminal.ts +++ b/packages/core/src/shared/sam/processTerminal.ts @@ -5,12 +5,9 @@ import * as vscode from 'vscode' import { ToolkitError, UnknownError } from '../errors' -import globals from '../extensionGlobals' -import { isCloud9 } from '../extensionUtilities' import { ChildProcess, ChildProcessResult } from '../utilities/processUtils' import { CancellationError } from '../utilities/timeoutUtils' import { getLogger } from '../logger' -import { removeAnsi } from '../utilities/textUtilities' import { isAutomation } from '../vscode/env' import { throwIfErrorMatches } from './utils' @@ -29,19 +26,6 @@ export async function runInTerminal(proc: ChildProcess, cmd: string) { } } - // `createTerminal` doesn't work on C9 so we use the output channel instead - if (isCloud9()) { - globals.outputChannel.show() - - const result = proc.run({ - onStdout: (text) => globals.outputChannel.append(removeAnsi(text)), - onStderr: (text) => globals.outputChannel.append(removeAnsi(text)), - }) - await proc.send('\n') - - return handleResult(await result) - } - // The most recent terminal won't get garbage collected until the next run if (oldTerminal?.stopped === true) { oldTerminal.close() diff --git a/packages/core/src/shared/schemas.ts b/packages/core/src/shared/schemas.ts index d55262be1e0..c75916fde68 100644 --- a/packages/core/src/shared/schemas.ts +++ b/packages/core/src/shared/schemas.ts @@ -225,7 +225,8 @@ export async function updateSchemaFromRemote(params: { try { const httpFetcher = new HttpResourceFetcher(params.url, { showUrl: true }) - const content = await httpFetcher.get() + const resp = await httpFetcher.get() + const content = await resp?.text() if (!content) { throw new Error(`failed to resolve schema: ${params.destination}`) diff --git a/packages/core/src/shared/settings.ts b/packages/core/src/shared/settings.ts index 258daa99b5c..5e157ba4605 100644 --- a/packages/core/src/shared/settings.ts +++ b/packages/core/src/shared/settings.ts @@ -747,7 +747,6 @@ export class Experiments extends Settings.define( const devSettings = { crashCheckInterval: Number, logfile: String, - forceCloud9: Boolean, forceDevMode: Boolean, forceInstallTools: Boolean, forceResolveEnv: Boolean, diff --git a/packages/core/src/shared/telemetry/activation.ts b/packages/core/src/shared/telemetry/activation.ts index 5b1ba3e5a56..f266787681c 100644 --- a/packages/core/src/shared/telemetry/activation.ts +++ b/packages/core/src/shared/telemetry/activation.ts @@ -12,7 +12,7 @@ import * as vscode from 'vscode' import { AwsContext } from '../awsContext' import { DefaultTelemetryService } from './telemetryService' import { getLogger } from '../logger' -import { getComputeRegion, isAmazonQ, isCloud9, productName } from '../extensionUtilities' +import { getComputeRegion, isAmazonQ, productName } from '../extensionUtilities' import { openSettingsId, Settings } from '../settings' import { getSessionId, TelemetryConfig } from './util' import { isAutomation, isReleaseVersion } from '../vscode/env' @@ -70,7 +70,7 @@ export async function activate( ) // Prompt user about telemetry if they haven't been - if (!isCloud9() && !hasUserSeenTelemetryNotice()) { + if (!hasUserSeenTelemetryNotice()) { showTelemetryNotice() } diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index a81dd8fc12d..ce9342add56 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -245,7 +245,6 @@ export function getUserAgent( */ export type EnvType = | 'cloud9' - | 'cloud9-codecatalyst' | 'cloudDesktop-amzn' | 'codecatalyst' | 'local' @@ -260,10 +259,8 @@ export type EnvType = * Returns the identifier for the environment that the extension is running in. */ export async function getComputeEnvType(): Promise { - if (isCloud9('classic')) { + if (isCloud9()) { return 'cloud9' - } else if (isCloud9('codecatalyst')) { - return 'cloud9-codecatalyst' } else if (isInDevEnv()) { return 'codecatalyst' } else if (isSageMaker()) { diff --git a/packages/core/src/shared/treeview/nodes/awsTreeNodeBase.ts b/packages/core/src/shared/treeview/nodes/awsTreeNodeBase.ts index 4b72ed74d42..316634389d1 100644 --- a/packages/core/src/shared/treeview/nodes/awsTreeNodeBase.ts +++ b/packages/core/src/shared/treeview/nodes/awsTreeNodeBase.ts @@ -4,7 +4,6 @@ */ import { TreeItem, TreeItemCollapsibleState, commands } from 'vscode' -import { isCloud9 } from '../../extensionUtilities' export abstract class AWSTreeNodeBase extends TreeItem { public readonly regionCode?: string @@ -24,10 +23,6 @@ export abstract class AWSTreeNodeBase extends TreeItem { } public refresh(): void { - if (isCloud9()) { - void commands.executeCommand('aws.refreshAwsExplorer', true) - } else { - void commands.executeCommand('aws.refreshAwsExplorerNode', this) - } + void commands.executeCommand('aws.refreshAwsExplorerNode', this) } } diff --git a/packages/core/src/shared/ui/inputPrompter.ts b/packages/core/src/shared/ui/inputPrompter.ts index ad2fa2ac2c8..88292f9d43d 100644 --- a/packages/core/src/shared/ui/inputPrompter.ts +++ b/packages/core/src/shared/ui/inputPrompter.ts @@ -153,8 +153,6 @@ export class InputBoxPrompter extends Prompter { }) this.inputBox.show() }).finally(() => { - // TODO: remove the .hide() call when Cloud9 implements dispose - this.inputBox.hide() this.inputBox.dispose() }) diff --git a/packages/core/src/shared/utilities/cliUtils.ts b/packages/core/src/shared/utilities/cliUtils.ts index a247ed19489..b52ae10b023 100644 --- a/packages/core/src/shared/utilities/cliUtils.ts +++ b/packages/core/src/shared/utilities/cliUtils.ts @@ -11,7 +11,7 @@ import * as vscode from 'vscode' import { getIdeProperties } from '../extensionUtilities' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../filesystemUtilities' import { getLogger } from '../logger' -import { HttpResourceFetcher } from '../resourcefetcher/httpResourceFetcher' +import { HttpResourceFetcher } from '../resourcefetcher/node/httpResourceFetcher' import { ChildProcess } from './processUtils' import * as nls from 'vscode-nls' diff --git a/packages/core/src/shared/utilities/functionUtils.ts b/packages/core/src/shared/utilities/functionUtils.ts index f61a3abde34..d21727bac1d 100644 --- a/packages/core/src/shared/utilities/functionUtils.ts +++ b/packages/core/src/shared/utilities/functionUtils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { sleep, Timeout } from './timeoutUtils' +import { Timeout } from './timeoutUtils' /** * Creates a function that always returns a 'shared' Promise. @@ -145,34 +145,3 @@ export function cancellableDebounce( cancel: cancel, } } - -/** - * Executes the given function, retrying if it throws. - * - * @param opts - if no opts given, defaults are used - */ -export async function withRetries( - fn: () => Promise, - opts?: { maxRetries?: number; delay?: number; backoff?: number } -): Promise { - const maxRetries = opts?.maxRetries ?? 3 - const delay = opts?.delay ?? 0 - const backoff = opts?.backoff ?? 1 - - let retryCount = 0 - let latestDelay = delay - while (true) { - try { - return await fn() - } catch (err) { - retryCount++ - if (retryCount >= maxRetries) { - throw err - } - if (latestDelay > 0) { - await sleep(latestDelay) - latestDelay = latestDelay * backoff - } - } - } -} diff --git a/packages/core/src/shared/utilities/messages.ts b/packages/core/src/shared/utilities/messages.ts index a961e983745..54085407d65 100644 --- a/packages/core/src/shared/utilities/messages.ts +++ b/packages/core/src/shared/utilities/messages.ts @@ -8,7 +8,7 @@ import * as nls from 'vscode-nls' import * as localizedText from '../localizedText' import { getLogger } from '../../shared/logger' import { ProgressEntry } from '../../shared/vscode/window' -import { getIdeProperties, isCloud9 } from '../extensionUtilities' +import { getIdeProperties } from '../extensionUtilities' import { sleep } from './timeoutUtils' import { Timeout } from './timeoutUtils' import { addCodiconToString } from './textUtilities' @@ -247,11 +247,6 @@ async function showProgressWithTimeout( if (showAfterMs === 0) { showAfterMs = 1 // Show immediately. } - // Cloud9 doesn't support `ProgressLocation.Notification`. User won't be able to cancel. - if (isCloud9()) { - options.location = vscode.ProgressLocation.Window - } - // See also: codecatalyst.ts:LazyProgress const progressPromise: Promise> = new Promise( (resolve, reject) => { diff --git a/packages/core/src/shared/utilities/pollingSet.ts b/packages/core/src/shared/utilities/pollingSet.ts index d8c4f7c6ded..ea9873aa71e 100644 --- a/packages/core/src/shared/utilities/pollingSet.ts +++ b/packages/core/src/shared/utilities/pollingSet.ts @@ -45,8 +45,14 @@ export class PollingSet extends Set { } } - public start(id: T): void { - this.add(id) + public override add(id: T) { + super.add(id) this.pollTimer = this.pollTimer ?? globals.clock.setInterval(() => this.poll(), this.interval) + return this + } + + public override clear(): void { + this.clearTimer() + super.clear() } } diff --git a/packages/core/src/shared/utilities/processUtils.ts b/packages/core/src/shared/utilities/processUtils.ts index 2e179da98b8..488ebe14e7b 100644 --- a/packages/core/src/shared/utilities/processUtils.ts +++ b/packages/core/src/shared/utilities/processUtils.ts @@ -7,6 +7,8 @@ import * as proc from 'child_process' // eslint-disable-line no-restricted-impor import * as crossSpawn from 'cross-spawn' import * as logger from '../logger' import { Timeout, CancellationError, waitUntil } from './timeoutUtils' +import { PollingSet } from './pollingSet' +import { getLogger } from '../logger/logger' export interface RunParameterContext { /** Reports an error parsed from the stdin/stdout streams. */ @@ -61,6 +63,135 @@ export interface ChildProcessResult { export const eof = Symbol('EOF') +export interface ProcessStats { + memory: number + cpu: number +} +export class ChildProcessTracker { + static readonly pollingInterval: number = 10000 // Check usage every 10 seconds + static readonly thresholds: ProcessStats = { + memory: 100 * 1024 * 1024, // 100 MB + cpu: 50, + } + static readonly logger = getLogger('childProcess') + #processByPid: Map = new Map() + #pids: PollingSet + + public constructor() { + this.#pids = new PollingSet(ChildProcessTracker.pollingInterval, () => this.monitor()) + } + + private cleanUp() { + const terminatedProcesses = Array.from(this.#pids.values()).filter( + (pid: number) => this.#processByPid.get(pid)?.stopped + ) + for (const pid of terminatedProcesses) { + this.delete(pid) + } + } + + private async monitor() { + this.cleanUp() + ChildProcessTracker.logger.debug(`Active running processes size: ${this.#pids.size}`) + + for (const pid of this.#pids.values()) { + await this.checkProcessUsage(pid) + } + } + + private async checkProcessUsage(pid: number): Promise { + if (!this.#pids.has(pid)) { + ChildProcessTracker.logger.warn(`Missing process with id ${pid}`) + return + } + const stats = this.getUsage(pid) + if (stats) { + ChildProcessTracker.logger.debug(`Process ${pid} usage: %O`, stats) + if (stats.memory > ChildProcessTracker.thresholds.memory) { + ChildProcessTracker.logger.warn(`Process ${pid} exceeded memory threshold: ${stats.memory}`) + } + if (stats.cpu > ChildProcessTracker.thresholds.cpu) { + ChildProcessTracker.logger.warn(`Process ${pid} exceeded cpu threshold: ${stats.cpu}`) + } + } + } + + public add(childProcess: ChildProcess) { + const pid = childProcess.pid() + this.#processByPid.set(pid, childProcess) + this.#pids.add(pid) + } + + public delete(childProcessId: number) { + this.#processByPid.delete(childProcessId) + this.#pids.delete(childProcessId) + } + + public get size() { + return this.#pids.size + } + + public has(childProcess: ChildProcess) { + return this.#pids.has(childProcess.pid()) + } + + public clear() { + for (const childProcess of this.#processByPid.values()) { + childProcess.stop(true) + } + this.#pids.clear() + this.#processByPid.clear() + } + + public getUsage(pid: number): ProcessStats { + try { + // isWin() leads to circular dependency. + return process.platform === 'win32' ? getWindowsUsage() : getUnixUsage() + } catch (e) { + ChildProcessTracker.logger.warn(`Failed to get process stats for ${pid}: ${e}`) + return { cpu: 0, memory: 0 } + } + + function getWindowsUsage() { + const cpuOutput = proc + .execFileSync('wmic', [ + 'path', + 'Win32_PerfFormattedData_PerfProc_Process', + 'where', + `IDProcess=${pid}`, + 'get', + 'PercentProcessorTime', + ]) + .toString() + const memOutput = proc + .execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize']) + .toString() + + const cpuPercentage = parseFloat(cpuOutput.split('\n')[1]) + const memoryBytes = parseInt(memOutput.split('\n')[1]) * 1024 + + return { + cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage, + memory: memoryBytes, + } + } + + function getUnixUsage() { + const cpuMemOutput = proc.execFileSync('ps', ['-p', pid.toString(), '-o', '%cpu,%mem']).toString() + const rssOutput = proc.execFileSync('ps', ['-p', pid.toString(), '-o', 'rss']).toString() + + const cpuMemLines = cpuMemOutput.split('\n')[1].trim().split(/\s+/) + const cpuPercentage = parseFloat(cpuMemLines[0]) + const memoryBytes = parseInt(rssOutput.split('\n')[1]) * 1024 + + return { + cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage, + memory: memoryBytes, + } + } + } +} + /** * Convenience class to manage a child process * To use: @@ -68,7 +199,8 @@ export const eof = Symbol('EOF') * - call and await run to get the results (pass or fail) */ export class ChildProcess { - static #runningProcesses: Map = new Map() + static #runningProcesses = new ChildProcessTracker() + static stopTimeout = 3000 #childProcess: proc.ChildProcess | undefined #processErrors: Error[] = [] #processResult: ChildProcessResult | undefined @@ -285,7 +417,7 @@ export class ChildProcess { child.kill(signal) if (force === true) { - waitUntil(async () => this.stopped, { timeout: 3000, interval: 200, truthy: true }) + waitUntil(async () => this.stopped, { timeout: ChildProcess.stopTimeout, interval: 200, truthy: true }) .then((stopped) => { if (!stopped) { child.kill('SIGKILL') @@ -309,7 +441,7 @@ export class ChildProcess { if (pid === undefined) { return } - ChildProcess.#runningProcesses.set(pid, this) + ChildProcess.#runningProcesses.add(this) const timeoutListener = options?.timeout?.token.onCancellationRequested(({ agent }) => { const message = agent === 'user' ? 'Cancelled: ' : 'Timed out: ' @@ -319,7 +451,7 @@ export class ChildProcess { const dispose = () => { timeoutListener?.dispose() - ChildProcess.#runningProcesses.delete(pid) + ChildProcess.#runningProcesses.delete(this.pid()) } process.on('exit', dispose) diff --git a/packages/core/src/shared/utilities/textUtilities.ts b/packages/core/src/shared/utilities/textUtilities.ts index 95bfabe37d3..b17b79517b8 100644 --- a/packages/core/src/shared/utilities/textUtilities.ts +++ b/packages/core/src/shared/utilities/textUtilities.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode' import * as crypto from 'crypto' import * as fs from 'fs' // eslint-disable-line no-restricted-imports import { default as stripAnsi } from 'strip-ansi' -import { isCloud9 } from '../extensionUtilities' import { getLogger } from '../logger' /** @@ -120,10 +119,10 @@ export function getStringHash(text: string | Buffer): string { } /** - * Temporary util while Cloud9 does not have codicon support + * Previously used to add Cloud9 support (no icons). Might be useful in the future, so let's leave it here. */ export function addCodiconToString(codiconName: string, text: string): string { - return isCloud9() ? text : `$(${codiconName}) ${text}` + return `$(${codiconName}) ${text}` } /** diff --git a/packages/core/src/shared/utilities/timeoutUtils.ts b/packages/core/src/shared/utilities/timeoutUtils.ts index 8dc38cfef5c..7c1a6e521ca 100644 --- a/packages/core/src/shared/utilities/timeoutUtils.ts +++ b/packages/core/src/shared/utilities/timeoutUtils.ts @@ -219,40 +219,101 @@ interface WaitUntilOptions { readonly interval?: number /** Wait for "truthy" result, else wait for any defined result including `false` (default: true) */ readonly truthy?: boolean + /** A backoff multiplier for how long the next interval will be (default: None, i.e 1) */ + readonly backoff?: number + /** + * Only retries when an error is thrown, otherwise returning the immediate result. + * - 'truthy' arg is ignored + * - If the timeout is reached it throws the last error + * - default: false + */ + readonly retryOnFail?: boolean } +export const waitUntilDefaultTimeout = 2000 +export const waitUntilDefaultInterval = 500 + /** - * Invokes `fn()` until it returns a truthy value (or non-undefined if `truthy:false`). + * Invokes `fn()` on an interval based on the given arguments. This can be used for retries, or until + * an expected result is given. Read {@link WaitUntilOptions} carefully. * * @param fn Function whose result is checked * @param options See {@link WaitUntilOptions} * - * @returns Result of `fn()`, or `undefined` if timeout was reached. + * @returns Result of `fn()`, or possibly `undefined` depending on the arguments. */ +export async function waitUntil(fn: () => Promise, options: WaitUntilOptions & { retryOnFail: true }): Promise +export async function waitUntil( + fn: () => Promise, + options: WaitUntilOptions & { retryOnFail: false } +): Promise +export async function waitUntil( + fn: () => Promise, + options: Omit +): Promise export async function waitUntil(fn: () => Promise, options: WaitUntilOptions): Promise { - const opt = { timeout: 5000, interval: 500, truthy: true, ...options } + // set default opts + const opt = { + timeout: waitUntilDefaultTimeout, + interval: waitUntilDefaultInterval, + truthy: true, + backoff: 1, + retryOnFail: false, + ...options, + } + + let interval = opt.interval + let lastError: Error | undefined + let elapsed: number = 0 + let remaining = opt.timeout + for (let i = 0; true; i++) { const start: number = globals.clock.Date.now() let result: T - // Needed in case a caller uses a 0 timeout (function is only called once) - if (opt.timeout > 0) { - result = await Promise.race([fn(), new Promise((r) => globals.clock.setTimeout(r, opt.timeout))]) - } else { - result = await fn() + try { + // Needed in case a caller uses a 0 timeout (function is only called once) + if (remaining > 0) { + result = await Promise.race([fn(), new Promise((r) => globals.clock.setTimeout(r, remaining))]) + } else { + result = await fn() + } + + if (opt.retryOnFail || (opt.truthy && result) || (!opt.truthy && result !== undefined)) { + return result + } + } catch (e) { + if (!opt.retryOnFail) { + throw e + } + + // Unlikely to hit this, but exists for typing + if (!(e instanceof Error)) { + throw e + } + + lastError = e } // Ensures that we never overrun the timeout - opt.timeout -= globals.clock.Date.now() - start + remaining -= globals.clock.Date.now() - start + + // If the sleep will exceed the timeout, abort early + if (elapsed + interval >= remaining) { + if (!opt.retryOnFail) { + return undefined + } - if ((opt.truthy && result) || (!opt.truthy && result !== undefined)) { - return result + throw lastError } - if (i * opt.interval >= opt.timeout) { - return undefined + + // when testing, this avoids the need to progress the stubbed clock + if (interval > 0) { + await sleep(interval) } - await sleep(opt.interval) + elapsed += interval + interval = interval * opt.backoff } } diff --git a/packages/core/src/stepFunctions/utils.ts b/packages/core/src/stepFunctions/utils.ts index cf6a8ef6704..cfb89f5f162 100644 --- a/packages/core/src/stepFunctions/utils.ts +++ b/packages/core/src/stepFunctions/utils.ts @@ -132,13 +132,13 @@ export class StateMachineGraphCache { } if (!cssExists) { - // Help users setup on disconnected C9/VSCode instances. + // Help users setup on disconnected VSCode instances. this.logger.error( `Failed to locate cached State Machine Graph css assets. Expected to find: "${visualizationCssUrl}" at "${this.cssFilePath}"` ) } if (!jsExists) { - // Help users setup on disconnected C9/VSCode instances. + // Help users setup on disconnected VSCode instances. this.logger.error( `Failed to locate cached State Machine Graph js assets. Expected to find: "${visualizationScriptUrl}" at "${this.jsFilePath}"` ) @@ -179,8 +179,8 @@ async function httpsGetRequestWrapper(url: string): Promise { const logger = getLogger() logger.verbose('Step Functions is getting content...') - const fetcher = new HttpResourceFetcher(url, { showUrl: true }) - const val = await fetcher.get() + const fetcher = await new HttpResourceFetcher(url, { showUrl: true }).get() + const val = await fetcher?.text() if (!val) { const message = 'Step Functions was unable to get content.' diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index 97044d99338..a6d36942840 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -43,6 +43,7 @@ import { featureDevScheme, featureName, messageWithConversationId } from '../../ import { i18n } from '../../../../shared/i18n-helper' import { FollowUpTypes } from '../../../../amazonq/commons/types' import { ToolkitError } from '../../../../shared' +import { MessengerTypes } from '../../../../amazonqFeatureDev/controllers/chat/messenger/constants' let mockGetCodeGeneration: sinon.SinonStub describe('Controller', () => { @@ -526,10 +527,11 @@ describe('Controller', () => { await waitUntil(() => Promise.resolve(sendMetricDataTelemetrySpy.callCount >= 2), {}) } - async function verifyAddCodeMessage( - remainingIterations: number, - totalIterations: number, - expectedMessage: string + async function verifyMessage( + expectedMessage: string, + type: MessengerTypes, + remainingIterations?: number, + totalIterations?: number ) { sinon.stub(session, 'send').resolves() sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry @@ -542,7 +544,7 @@ describe('Controller', () => { assert.ok( sendAnswerSpy.calledWith({ - type: 'answer', + type, tabID, message: expectedMessage, }) @@ -595,7 +597,33 @@ describe('Controller', () => { return 'Would you like me to add this code to your project?' } })() - await verifyAddCodeMessage(remainingIterations, totalIterations, expectedMessage) + await verifyMessage(expectedMessage, 'answer', remainingIterations, totalIterations) + }) + } + + for (let remainingIterations = -1; remainingIterations <= 3; remainingIterations++) { + let remaining: number | undefined = remainingIterations + if (remainingIterations < 0) { + remaining = undefined + } + it(`verifies messages after cancellation for remaining iterations at ${remaining !== undefined ? remaining : 'undefined'}`, async () => { + const totalIterations = 10 + const expectedMessage = (() => { + if (remaining === undefined || remaining > 2) { + return 'I stopped generating your code. If you want to continue working on this task, provide another description.' + } else if (remaining > 0) { + return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remaining} out of ${totalIterations} code generations left.` + } else { + return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." + } + })() + session.state.tokenSource.cancel() + await verifyMessage( + expectedMessage, + 'answer-part', + remaining, + remaining === undefined ? undefined : totalIterations + ) }) } }) diff --git a/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts index b675b2452f8..74fafc20843 100644 --- a/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/detectSamProjects.test.ts @@ -83,7 +83,7 @@ describe('getFiles', () => { const templateFiles = await getFiles(workspaceFolder, '**/template.{yml,yaml}', '**/.aws-sam/**') assert.strictEqual(templateFiles.length, 0) - assertLogsContain('Failed to get files with pattern', false, 'error') + assertLogsContain('Failed to find files with pattern', false, 'error') sandbox.restore() }) }) diff --git a/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts index de7a82e126a..f959ef0e572 100644 --- a/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts +++ b/packages/core/src/test/awsService/appBuilder/explorer/samProject.test.ts @@ -48,7 +48,7 @@ describe('samProject', () => { assert.strictEqual(region, expectedRegion) }) - it('returns undefined give no stack name or region in samconfig file', async () => { + it('returns undefined given no stack name or region in samconfig file', async () => { await testFolder.write( 'samconfig.toml', generateSamconfigData({ @@ -71,24 +71,28 @@ describe('samProject', () => { const result = await wrapperCall(undefined) assert.deepStrictEqual(result, {}) - assertLogsContain('Error getting stack name or region information: No project folder found', false, 'warn') + assertLogsContain( + 'Error parsing stack name and/or region information: No project folder found', + false, + 'warn' + ) }) - it('returns empty object give no samconfig file found', async () => { + it('returns empty object given no samconfig file found', async () => { // simulate error when no samconfig.toml file in directory const result = await getStackName(projectRoot) assert.deepStrictEqual(result, {}) - assertLogsContain('No stack name or region information available in samconfig.toml', false, 'info') + assertLogsContain('Stack name and/or region information not found in samconfig.toml', false, 'info') }) - it('returns empty object give error parsing samconfig file', async () => { + it('returns empty object given error parsing samconfig file', async () => { // simulate error when parsinf samconfig.toml: missing quote or empty value await testFolder.write('samconfig.toml', samconfigInvalidData) const result = await getStackName(projectRoot) assert.deepStrictEqual(result, {}) - assertLogsContain('Error getting stack name or region information:', false, 'error') + assertLogsContain('Error parsing stack name and/or region information from samconfig.toml:', false, 'error') getTestWindow().getFirstMessage().assertError('Encountered an issue reading samconfig.toml') }) }) @@ -149,17 +153,6 @@ describe('samProject', () => { () => getApp(mockSamAppLocation), new ToolkitError(`Template at ${mockSamAppLocation.samTemplateUri.fsPath} is not valid`) ) - // try { - // await getApp(mockSamAppLocation) - // assert.fail('Test should not reach here. Expect ToolkitError thrown') - // } catch (error) { - // assert(cloudformationTryLoadSpy.calledOnce) - // assert(error instanceof ToolkitError) - // assert.strictEqual( - // error.message, - // `Template at ${mockSamAppLocation.samTemplateUri.fsPath} is not valid` - // ) - // } }) }) }) diff --git a/packages/core/src/test/awsService/appBuilder/utils.test.ts b/packages/core/src/test/awsService/appBuilder/utils.test.ts index 37ffac34863..d74cfc77802 100644 --- a/packages/core/src/test/awsService/appBuilder/utils.test.ts +++ b/packages/core/src/test/awsService/appBuilder/utils.test.ts @@ -294,9 +294,9 @@ describe('AppBuilder Utils', function () { } try { await runOpenTemplate(tNode as TreeNode) - assert.fail('No template provided') + assert.fail('SAM Template not found, cannot open template') } catch (err) { - assert.strictEqual((err as Error).message, 'No template provided') + assert.strictEqual((err as Error).message, 'SAM Template not found, cannot open template') } // Then assert(openCommand.neverCalledWith(sinon.match.has('fspath', sinon.match(/template.yaml/g)))) diff --git a/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts b/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts index 72dfb5c0ae2..b6a8d6d662a 100644 --- a/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts +++ b/packages/core/src/test/awsService/appBuilder/walkthrough.test.ts @@ -23,7 +23,7 @@ import { getTestWindow } from '../../shared/vscode/window' import { AwsClis, installCli } from '../../../shared/utilities/cliUtils' import { ChildProcess } from '../../../shared/utilities/processUtils' import { assertTelemetryCurried } from '../../testUtil' -import { HttpResourceFetcher } from '../../../shared/resourcefetcher/httpResourceFetcher' +import { HttpResourceFetcher } from '../../../shared/resourcefetcher/node/httpResourceFetcher' import { SamCliInfoInvocation } from '../../../shared/sam/cli/samCliInfo' import { CodeScansState } from '../../../codewhisperer' @@ -206,9 +206,9 @@ describe('AppBuilder Walkthrough', function () { try { // When await genWalkthroughProject('Visual', workspaceUri, undefined) - assert.fail('template.yaml already exist') + assert.fail('A file named template.yaml already exists in this path.') } catch (e) { - assert.equal((e as Error).message, 'template.yaml already exist') + assert.equal((e as Error).message, 'A file named template.yaml already exists in this path.') } // Then assert.equal(await fs.readFileText(vscode.Uri.joinPath(workspaceUri, 'template.yaml')), prevInfo) @@ -236,9 +236,9 @@ describe('AppBuilder Walkthrough', function () { try { // When await genWalkthroughProject('S3', workspaceUri, 'python') - assert.fail('template.yaml already exist') + assert.fail('A file named template.yaml already exists in this path.') } catch (e) { - assert.equal((e as Error).message, 'template.yaml already exist') + assert.equal((e as Error).message, 'A file named template.yaml already exists in this path.') } // Then no overwrite happens assert.equal(await fs.readFileText(vscode.Uri.joinPath(workspaceUri, 'template.yaml')), prevInfo) diff --git a/packages/core/src/test/awsService/ec2/activation.test.ts b/packages/core/src/test/awsService/ec2/activation.test.ts index f604ada56cb..7c19b78199c 100644 --- a/packages/core/src/test/awsService/ec2/activation.test.ts +++ b/packages/core/src/test/awsService/ec2/activation.test.ts @@ -19,7 +19,7 @@ describe('ec2 activation', function () { const testPartition = 'test-partition' // Don't want to be polling here, that is tested in ../ec2ParentNode.test.ts // disabled here for convenience (avoiding race conditions with timeout) - sinon.stub(PollingSet.prototype, 'start') + sinon.stub(PollingSet.prototype, 'add') const testClient = new Ec2Client(testRegion) const parentNode = new Ec2ParentNode(testRegion, testPartition, new Ec2Client(testRegion)) testNode = new Ec2InstanceNode(parentNode, testClient, testRegion, testPartition, { diff --git a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts index 5299d2a080d..fc0bd36d66b 100644 --- a/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts +++ b/packages/core/src/test/awsService/ec2/explorer/ec2InstanceNode.test.ts @@ -35,7 +35,7 @@ describe('ec2InstanceNode', function () { sinon.stub(Ec2InstanceNode.prototype, 'updateStatus') // Don't want to be polling here, that is tested in ../ec2ParentNode.test.ts // disabled here for convenience (avoiding race conditions with timeout) - sinon.stub(PollingSet.prototype, 'start') + sinon.stub(PollingSet.prototype, 'add') const testClient = new Ec2Client('') const testParentNode = new Ec2ParentNode(testRegion, testPartition, testClient) testNode = new Ec2InstanceNode(testParentNode, testClient, 'testRegion', 'testPartition', testInstance) diff --git a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts index 997b24b78f4..bc2dd2c4001 100644 --- a/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts +++ b/packages/core/src/test/codewhisperer/commands/basicCommands.test.ts @@ -30,7 +30,7 @@ import { testCommand } from '../../shared/vscode/testUtils' import { Command, placeholder } from '../../../shared/vscode/commands2' import { SecurityPanelViewProvider } from '../../../codewhisperer/views/securityPanelViewProvider' import { DefaultCodeWhispererClient } from '../../../codewhisperer/client/codewhisperer' -import { stub } from '../../utilities/stubber' +import { Stub, stub } from '../../utilities/stubber' import { AuthUtil } from '../../../codewhisperer/util/authUtil' import { getTestWindow } from '../../shared/vscode/window' import { ExtContext } from '../../../shared/extensions' @@ -67,6 +67,7 @@ import { SecurityIssueProvider } from '../../../codewhisperer/service/securityIs import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' import { confirm } from '../../../shared' import * as commentUtils from '../../../shared/utilities/commentUtils' +import * as startCodeFixGeneration from '../../../codewhisperer/commands/startCodeFixGeneration' describe('CodeWhisperer-basicCommands', function () { let targetCommand: Command & vscode.Disposable @@ -749,156 +750,159 @@ def execute_input_compliant(): }) }) - // describe('generateFix', function () { - // let sandbox: sinon.SinonSandbox - // let mockClient: Stub - // let filePath: string - // let codeScanIssue: CodeScanIssue - // let issueItem: IssueItem - // let updateSecurityIssueWebviewMock: sinon.SinonStub - // let updateIssueMock: sinon.SinonStub - // let refreshTreeViewMock: sinon.SinonStub - // let mockDocument: vscode.TextDocument - - // beforeEach(function () { - // sandbox = sinon.createSandbox() - // mockClient = stub(DefaultCodeWhispererClient) - // mockClient.generateCodeFix.resolves({ - // // TODO: Clean this up - // $response: {} as PromiseResult['$response'], - // suggestedRemediationDiff: 'diff', - // suggestedRemediationDescription: 'description', - // references: [], - // }) - // filePath = 'dummy/file.py' - // codeScanIssue = createCodeScanIssue({ - // findingId: randomUUID(), - // ruleId: 'dummy-rule-id', - // }) - // issueItem = new IssueItem(filePath, codeScanIssue) - // updateSecurityIssueWebviewMock = sinon.stub() - // updateIssueMock = sinon.stub() - // refreshTreeViewMock = sinon.stub() - // mockDocument = createMockDocument('dummy input') - // }) - - // afterEach(function () { - // sandbox.restore() - // }) - - // it('should call generateFix command successfully', async function () { - // sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview').value(updateSecurityIssueWebviewMock) - // sinon.stub(SecurityIssueProvider.instance, 'updateIssue').value(updateIssueMock) - // sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh').value(refreshTreeViewMock) - // sinon.stub(vscode.workspace, 'openTextDocument').resolves(mockDocument) - // targetCommand = testCommand(generateFix, mockClient) - // await targetCommand.execute(codeScanIssue, filePath, 'webview') - // assert.ok(updateSecurityIssueWebviewMock.calledWith({ isGenerateFixLoading: true })) - // assert.ok( - // mockClient.generateCodeFix.calledWith({ - // sourceCode: 'dummy input', - // ruleId: codeScanIssue.ruleId, - // startLine: codeScanIssue.startLine, - // endLine: codeScanIssue.endLine, - // findingDescription: codeScanIssue.description.text, - // }) - // ) - - // const expectedUpdatedIssue = { - // ...codeScanIssue, - // suggestedFixes: [{ code: 'diff', description: 'description', references: [] }], - // } - // assert.ok( - // updateSecurityIssueWebviewMock.calledWith({ issue: expectedUpdatedIssue, isGenerateFixLoading: false }) - // ) - // assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) - // assert.ok(refreshTreeViewMock.calledOnce) - - // assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - // detectorId: codeScanIssue.detectorId, - // findingId: codeScanIssue.findingId, - // ruleId: codeScanIssue.ruleId, - // component: 'webview', - // result: 'Succeeded', - // }) - // }) - - // it('should call generateFix from tree view item', async function () { - // sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview').value(updateSecurityIssueWebviewMock) - // sinon.stub(SecurityIssueProvider.instance, 'updateIssue').value(updateIssueMock) - // sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh').value(refreshTreeViewMock) - // sinon.stub(vscode.workspace, 'openTextDocument').resolves(mockDocument) - // const filePath = 'dummy/file.py' - // targetCommand = testCommand(generateFix, mockClient) - // await targetCommand.execute(issueItem, filePath, 'tree') - // assert.ok(updateSecurityIssueWebviewMock.calledWith({ isGenerateFixLoading: true })) - // assert.ok( - // mockClient.generateCodeFix.calledWith({ - // sourceCode: 'dummy input', - // ruleId: codeScanIssue.ruleId, - // startLine: codeScanIssue.startLine, - // endLine: codeScanIssue.endLine, - // findingDescription: codeScanIssue.description.text, - // }) - // ) - - // const expectedUpdatedIssue = { - // ...codeScanIssue, - // suggestedFixes: [{ code: 'diff', description: 'description', references: [] }], - // } - // assert.ok( - // updateSecurityIssueWebviewMock.calledWith({ issue: expectedUpdatedIssue, isGenerateFixLoading: false }) - // ) - // assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) - // assert.ok(refreshTreeViewMock.calledOnce) - - // assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - // detectorId: codeScanIssue.detectorId, - // findingId: codeScanIssue.findingId, - // ruleId: codeScanIssue.ruleId, - // component: 'tree', - // result: 'Succeeded', - // }) - // }) - - // it('should call generateFix with refresh=true to indicate fix regenerated', async function () { - // sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview').value(updateSecurityIssueWebviewMock) - // sinon.stub(SecurityIssueProvider.instance, 'updateIssue').value(updateIssueMock) - // sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh').value(refreshTreeViewMock) - // sinon.stub(vscode.workspace, 'openTextDocument').resolves(mockDocument) - // targetCommand = testCommand(generateFix, mockClient) - // await targetCommand.execute(codeScanIssue, filePath, 'webview', true) - // assert.ok(updateSecurityIssueWebviewMock.calledWith({ isGenerateFixLoading: true })) - // assert.ok( - // mockClient.generateCodeFix.calledWith({ - // sourceCode: 'dummy input', - // ruleId: codeScanIssue.ruleId, - // startLine: codeScanIssue.startLine, - // endLine: codeScanIssue.endLine, - // findingDescription: codeScanIssue.description.text, - // }) - // ) - - // const expectedUpdatedIssue = { - // ...codeScanIssue, - // suggestedFixes: [{ code: 'diff', description: 'description', references: [] }], - // } - // assert.ok( - // updateSecurityIssueWebviewMock.calledWith({ issue: expectedUpdatedIssue, isGenerateFixLoading: false }) - // ) - // assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) - // assert.ok(refreshTreeViewMock.calledOnce) - - // assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { - // detectorId: codeScanIssue.detectorId, - // findingId: codeScanIssue.findingId, - // ruleId: codeScanIssue.ruleId, - // component: 'webview', - // variant: 'refresh', - // result: 'Succeeded', - // }) - // }) - // }) + describe('generateFix', function () { + let sandbox: sinon.SinonSandbox + let mockClient: Stub + let startCodeFixGenerationStub: sinon.SinonStub + let filePath: string + let codeScanIssue: CodeScanIssue + let issueItem: IssueItem + let updateSecurityIssueWebviewMock: sinon.SinonStub + let updateIssueMock: sinon.SinonStub + let refreshTreeViewMock: sinon.SinonStub + let mockExtContext: ExtContext + + beforeEach(async function () { + sandbox = sinon.createSandbox() + mockClient = stub(DefaultCodeWhispererClient) + startCodeFixGenerationStub = sinon.stub(startCodeFixGeneration, 'startCodeFixGeneration') + filePath = 'dummy/file.py' + codeScanIssue = createCodeScanIssue({ + findingId: randomUUID(), + ruleId: 'dummy-rule-id', + }) + issueItem = new IssueItem(filePath, codeScanIssue) + updateSecurityIssueWebviewMock = sinon.stub(securityIssueWebview, 'updateSecurityIssueWebview') + updateIssueMock = sinon.stub(SecurityIssueProvider.instance, 'updateIssue') + refreshTreeViewMock = sinon.stub(SecurityIssueTreeViewProvider.instance, 'refresh') + mockExtContext = await FakeExtensionContext.getFakeExtContext() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should call generateFix command successfully', async function () { + startCodeFixGenerationStub.resolves({ + suggestedFix: { + codeDiff: 'codeDiff', + description: 'description', + references: [], + }, + jobId: 'jobId', + }) + + targetCommand = testCommand(generateFix, mockClient, mockExtContext) + await targetCommand.execute(codeScanIssue, filePath, 'webview') + + assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) + assert.ok( + startCodeFixGenerationStub.calledWith(mockClient, codeScanIssue, filePath, codeScanIssue.findingId) + ) + + const expectedUpdatedIssue = { + ...codeScanIssue, + fixJobId: 'jobId', + suggestedFixes: [{ code: 'codeDiff', description: 'description', references: [] }], + } + assert.ok( + updateSecurityIssueWebviewMock.calledWith( + sinon.match({ + issue: expectedUpdatedIssue, + isGenerateFixLoading: false, + filePath: filePath, + shouldRefreshView: true, + }) + ) + ) + assert.ok(updateIssueMock.calledWith(expectedUpdatedIssue, filePath)) + assert.ok(refreshTreeViewMock.calledOnce) + + assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { + detectorId: codeScanIssue.detectorId, + findingId: codeScanIssue.findingId, + ruleId: codeScanIssue.ruleId, + component: 'webview', + result: 'Succeeded', + }) + }) + + it('should call generateFix from tree view item', async function () { + startCodeFixGenerationStub.resolves({ + suggestedFix: { + codeDiff: 'codeDiff', + description: 'description', + references: [], + }, + jobId: 'jobId', + }) + + targetCommand = testCommand(generateFix, mockClient, mockExtContext) + await targetCommand.execute(issueItem, filePath, 'tree') + + assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { + detectorId: codeScanIssue.detectorId, + findingId: codeScanIssue.findingId, + ruleId: codeScanIssue.ruleId, + component: 'tree', + result: 'Succeeded', + }) + }) + + it('should call generateFix with refresh=true to indicate fix regenerated', async function () { + startCodeFixGenerationStub.resolves({ + suggestedFix: { + codeDiff: 'codeDiff', + description: 'description', + references: [], + }, + jobId: 'jobId', + }) + + targetCommand = testCommand(generateFix, mockClient, mockExtContext) + await targetCommand.execute(codeScanIssue, filePath, 'webview', true) + + assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { + detectorId: codeScanIssue.detectorId, + findingId: codeScanIssue.findingId, + ruleId: codeScanIssue.ruleId, + component: 'webview', + result: 'Succeeded', + variant: 'refresh', + }) + }) + + it('should handle generateFix error', async function () { + startCodeFixGenerationStub.throws(new Error('Unexpected error')) + + targetCommand = testCommand(generateFix, mockClient, mockExtContext) + await targetCommand.execute(codeScanIssue, filePath, 'webview') + + assert.ok(updateSecurityIssueWebviewMock.calledWith(sinon.match({ isGenerateFixLoading: true }))) + assert.ok( + updateSecurityIssueWebviewMock.calledWith( + sinon.match({ + issue: codeScanIssue, + isGenerateFixLoading: false, + generateFixError: 'Unexpected error', + shouldRefreshView: false, + }) + ) + ) + assert.ok(updateIssueMock.calledWith(codeScanIssue, filePath)) + assert.ok(refreshTreeViewMock.calledOnce) + + assertTelemetry('codewhisperer_codeScanIssueGenerateFix', { + detectorId: codeScanIssue.detectorId, + findingId: codeScanIssue.findingId, + ruleId: codeScanIssue.ruleId, + component: 'webview', + result: 'Failed', + reason: 'Error', + reasonDesc: 'Unexpected error', + }) + }) + }) describe('rejectFix', function () { let mockExtensionContext: vscode.ExtensionContext diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index 0e1e326b6c5..ce00b0c52c3 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -224,7 +224,7 @@ describe('transformByQ', function () { requestId: 'requestId', hasNextPage: () => false, error: undefined, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null redirectCount: 0, retryCount: 0, httpResponse: new HttpResponse(), diff --git a/packages/core/src/test/codewhisperer/testUtil.ts b/packages/core/src/test/codewhisperer/testUtil.ts index 027087a86b6..f3b82fd3850 100644 --- a/packages/core/src/test/codewhisperer/testUtil.ts +++ b/packages/core/src/test/codewhisperer/testUtil.ts @@ -226,7 +226,7 @@ export const mockGetCodeScanResponse = { requestId: 'requestId', hasNextPage: () => false, error: undefined, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null redirectCount: 0, retryCount: 0, httpResponse: new HttpResponse(), @@ -246,7 +246,7 @@ export function createClient() { requestId: 'requestId', hasNextPage: () => false, error: undefined, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null redirectCount: 0, retryCount: 0, httpResponse: new HttpResponse(), @@ -263,7 +263,7 @@ export function createClient() { requestId: 'requestId', hasNextPage: () => false, error: undefined, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null redirectCount: 0, retryCount: 0, httpResponse: new HttpResponse(), @@ -306,7 +306,7 @@ export function createClient() { requestId: 'requestId', hasNextPage: () => false, error: undefined, - nextPage: () => undefined, + nextPage: () => null, // eslint-disable-line unicorn/no-null redirectCount: 0, retryCount: 0, httpResponse: new HttpResponse(), diff --git a/packages/core/src/test/index.ts b/packages/core/src/test/index.ts index 282fdac2bfc..7e6d500a30c 100644 --- a/packages/core/src/test/index.ts +++ b/packages/core/src/test/index.ts @@ -24,3 +24,4 @@ export * from './credentials/testUtil' export * from './testUtil' export * from './amazonqFeatureDev/utils' export * from './fake/mockFeatureConfigData' +export * from './shared/ui/testUtils' diff --git a/packages/core/src/test/lambda/commands/uploadLambda.test.ts b/packages/core/src/test/lambda/commands/uploadLambda.test.ts index 6a76b08497c..e31fb9088fa 100644 --- a/packages/core/src/test/lambda/commands/uploadLambda.test.ts +++ b/packages/core/src/test/lambda/commands/uploadLambda.test.ts @@ -43,11 +43,6 @@ describe('uploadLambda', async function () { (await findApplicationJsonFile(folderUri))?.fsPath ?? '', path.join(tempFolder, '.application.json') ) - // Also test Cloud9 temporary workaround. - assertEqualPaths( - (await findApplicationJsonFile(folderUri, true))?.fsPath ?? '', - path.join(tempFolder, '.application.json') - ) }) it('finds application.json file from dir path - nested', async function () { @@ -56,8 +51,6 @@ describe('uploadLambda', async function () { await toFile('top secret data', appjsonPath) assertEqualPaths((await findApplicationJsonFile(folderUri))?.fsPath ?? '', appjsonPath) - // Also test Cloud9 temporary workaround. - assertEqualPaths((await findApplicationJsonFile(folderUri, true))?.fsPath ?? '', appjsonPath) }) it('finds application.json file from template file path', async function () { @@ -67,8 +60,6 @@ describe('uploadLambda', async function () { await toFile('top secret data', appjsonPath) assertEqualPaths((await findApplicationJsonFile(templateUri))?.fsPath ?? '', appjsonPath) - // Also test Cloud9 temporary workaround. - assertEqualPaths((await findApplicationJsonFile(templateUri, true))?.fsPath ?? '', appjsonPath) }) it('lists functions from .application.json', async function () { diff --git a/packages/core/src/test/lambda/models/samTemplates.test.ts b/packages/core/src/test/lambda/models/samTemplates.test.ts index e9105adf0b6..4abb3f87315 100644 --- a/packages/core/src/test/lambda/models/samTemplates.test.ts +++ b/packages/core/src/test/lambda/models/samTemplates.test.ts @@ -16,7 +16,6 @@ import { eventBridgeStarterAppTemplate, stepFunctionsSampleApp, typeScriptBackendTemplate, - lazyLoadSamTemplateStrings, } from '../../../lambda/models/samTemplates' import { Set } from 'immutable' @@ -29,8 +28,6 @@ let validGoTemplateOptions: Set let defaultTemplateOptions: Set before(function () { - lazyLoadSamTemplateStrings() - validTemplateOptions = Set([ helloWorldTemplate, eventBridgeHelloWorldTemplate, diff --git a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts index 8cdf2e0529f..2a0fcaa0e0d 100644 --- a/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts +++ b/packages/core/src/test/lambda/vue/remoteInvoke/invokeLambda.test.ts @@ -21,6 +21,7 @@ import { FakeExtensionContext } from '../../../fakeExtensionContext' import * as samCliRemoteTestEvent from '../../../../shared/sam/cli/samCliRemoteTestEvent' import { TestEventsOperation, SamCliRemoteTestEventsParameters } from '../../../../shared/sam/cli/samCliRemoteTestEvent' import { assertLogsContain } from '../../../globalSetup.test' +import { createResponse } from '../../../testUtil' describe('RemoteInvokeWebview', () => { let outputChannel: vscode.OutputChannel @@ -394,7 +395,7 @@ describe('RemoteInvokeWebview', () => { createQuickPickStub.returns({}) promptUserStub.resolves([{ label: 'testEvent', filename: 'testEvent.json' }]) verifySinglePickerOutputStub.returns({ label: 'testEvent', filename: 'testEvent.json' }) - httpFetcherStub.resolves(mockSampleContent) + httpFetcherStub.resolves(createResponse(mockSampleContent)) const result = await remoteInvokeWebview.getSamplePayload() diff --git a/packages/core/src/test/lambda/vue/samInvokeBackend.test.ts b/packages/core/src/test/lambda/vue/samInvokeBackend.test.ts index 89a9272df30..18ae33b67d0 100644 --- a/packages/core/src/test/lambda/vue/samInvokeBackend.test.ts +++ b/packages/core/src/test/lambda/vue/samInvokeBackend.test.ts @@ -10,7 +10,6 @@ import { SamInvokeWebview, finalizeConfig, } from '../../../lambda/vue/configEditor/samInvokeBackend' -import { ExtContext } from '../../../shared/extensions' import { AwsSamDebuggerConfiguration } from '../../../shared/sam/debugger/awsSamDebugConfiguration' import assert from 'assert' import * as picker from '../../../shared/ui/picker' @@ -22,12 +21,11 @@ import path from 'path' import { addCodiconToString, fs, makeTemporaryToolkitFolder } from '../../../shared' import { LaunchConfiguration } from '../../../shared/debug/launchConfiguration' import { getTestWindow } from '../..' -import * as extensionUtilities from '../../../shared/extensionUtilities' import * as samInvokeBackend from '../../../lambda/vue/configEditor/samInvokeBackend' -import { SamDebugConfigProvider } from '../../../shared/sam/debugger/awsSamDebugger' import sinon from 'sinon' import * as nls from 'vscode-nls' import { assertLogsContain } from '../../../test/globalSetup.test' +import { createResponse } from '../../testUtil' const localize = nls.loadMessageBundle() @@ -68,13 +66,11 @@ const mockConfig: AwsSamDebuggerConfigurationLoose = { describe('SamInvokeWebview', () => { let samInvokeWebview: SamInvokeWebview - let mockExtContext: ExtContext let sandbox: sinon.SinonSandbox beforeEach(() => { - mockExtContext = {} as ExtContext sandbox = sinon.createSandbox() - samInvokeWebview = new SamInvokeWebview(mockExtContext, mockConfig, mockResourceData) + samInvokeWebview = new SamInvokeWebview(mockConfig, mockResourceData) }) afterEach(() => { @@ -82,7 +78,7 @@ describe('SamInvokeWebview', () => { }) it('should return undefined when no resource data is provided', () => { - const noResourceWebview = new SamInvokeWebview(mockExtContext, mockConfig, undefined) + const noResourceWebview = new SamInvokeWebview(mockConfig, undefined) const data = noResourceWebview.getResourceData() // Using assert to check if the data is undefined @@ -166,7 +162,7 @@ describe('SamInvokeWebview', () => { createQuickPickStub.returns({}) promptUserStub.resolves([{ label: 'testEvent', filename: 'testEvent.json' }]) verifySinglePickerOutputStub.returns({ label: 'testEvent', filename: 'testEvent.json' }) - httpFetcherStub.resolves(mockSampleContent) + httpFetcherStub.resolves(createResponse(mockSampleContent)) const result = await samInvokeWebview.getSamplePayload() @@ -579,9 +575,8 @@ describe('SamInvokeWebview', () => { sandbox.restore() }) - it('should invoke launch config for non-Cloud9 environment', async () => { + it('should invoke launch config', async () => { workspaceFoldersStub.value([mockFolder]) - sandbox.stub(extensionUtilities, 'isCloud9').returns(false) sandbox.replace(samInvokeWebview as any, 'getUriFromLaunchConfig', getUriFromLaunchConfigStub) getUriFromLaunchConfigStub.resolves(mockUri) @@ -591,27 +586,6 @@ describe('SamInvokeWebview', () => { assert(startDebuggingStub.called) }) - - it('should invoke launch config for Cloud9 environment', async () => { - workspaceFoldersStub.value([mockFolder]) - sandbox.stub(extensionUtilities, 'isCloud9').returns(true) - sandbox.replace(samInvokeWebview as any, 'getUriFromLaunchConfig', getUriFromLaunchConfigStub) - getUriFromLaunchConfigStub.resolves(mockUri) - - const startDebuggingStub = sandbox.stub(vscode.debug, 'startDebugging').resolves(true) - - await samInvokeWebview.invokeLaunchConfig(mockConfig) - - assert(startDebuggingStub.notCalled) - }) - it('should use SamDebugConfigProvider for Cloud9 environment', async () => { - sandbox.stub(extensionUtilities, 'isCloud9').returns(true) - const SamDebugConfigProviderStub = sinon.stub(SamDebugConfigProvider.prototype, 'resolveDebugConfiguration') - - await samInvokeWebview.invokeLaunchConfig(mockConfig) - - assert(SamDebugConfigProviderStub.called) - }) }) describe('saveLaunchConfig', function () { let sandbox: sinon.SinonSandbox @@ -700,7 +674,6 @@ describe('SamInvokeWebview', () => { }) it('should not save launch config', async () => { workspaceFoldersStub.value([mockFolder]) - sandbox.stub(extensionUtilities, 'isCloud9').returns(false) sandbox.replace(samInvokeWebview as any, 'getUriFromLaunchConfig', getUriFromLaunchConfigStub) const launchConfigItems = launchConfigurationsStub.resolves([]) getUriFromLaunchConfigStub.resolves(mockUri) @@ -781,7 +754,6 @@ describe('SamInvokeWebview', () => { it('should save launch config', async () => { workspaceFoldersStub.value([mockFolder]) - sandbox.stub(extensionUtilities, 'isCloud9').returns(false) getUriFromLaunchConfigStub.resolves(mockUri) sandbox.replace(samInvokeWebview as any, 'getUriFromLaunchConfig', getUriFromLaunchConfigStub) const launchConfigItems = launchConfigurationsStub.resolves([ diff --git a/packages/core/src/test/notifications/controller.test.ts b/packages/core/src/test/notifications/controller.test.ts index e28f0271d19..761c6ce4bf2 100644 --- a/packages/core/src/test/notifications/controller.test.ts +++ b/packages/core/src/test/notifications/controller.test.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode' import * as FakeTimers from '@sinonjs/fake-timers' import assert from 'assert' -import sinon from 'sinon' +import sinon, { createSandbox } from 'sinon' import globals from '../../shared/extensionGlobals' import { randomUUID } from '../../shared/crypto' import { getContext } from '../../shared/vscode/setContext' @@ -27,6 +27,7 @@ import { import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher' import { NotificationsNode } from '../../notifications/panelNode' import { RuleEngine } from '../../notifications/rules' +import Sinon from 'sinon' // one test node to use across different tests export const panelNode: NotificationsNode = NotificationsNode.instance @@ -512,13 +513,16 @@ describe('Notifications Controller', function () { describe('RemoteFetcher', function () { let clock: FakeTimers.InstalledClock + let sandbox: Sinon.SinonSandbox before(function () { clock = installFakeClock() + sandbox = createSandbox() }) afterEach(function () { clock.reset() + sandbox.restore() }) after(function () { @@ -526,29 +530,29 @@ describe('RemoteFetcher', function () { }) it('retries and throws error', async function () { - const httpStub = sinon.stub(HttpResourceFetcher.prototype, 'getNewETagContent') + // Setup + const httpStub = sandbox.stub(HttpResourceFetcher.prototype, 'getNewETagContent') httpStub.throws(new Error('network error')) - const runClock = (async () => { - await clock.tickAsync(1) - for (let n = 1; n <= RemoteFetcher.retryNumber; n++) { - assert.equal(httpStub.callCount, n) - await clock.tickAsync(RemoteFetcher.retryIntervalMs) - } - - // Stop trying - await clock.tickAsync(RemoteFetcher.retryNumber) - assert.equal(httpStub.callCount, RemoteFetcher.retryNumber) - })() + // Start function under test + const fetcher = assert.rejects(new RemoteFetcher().fetch('startUp', 'any'), (e) => { + return e instanceof Error && e.message === 'last error' + }) - const fetcher = new RemoteFetcher() + // Progresses the clock, allowing the fetcher logic to break out of sleep for each iteration of withRetries() + assert.strictEqual(httpStub.callCount, 1) // 0 + await clock.tickAsync(RemoteFetcher.retryIntervalMs) + assert.strictEqual(httpStub.callCount, 2) // 30_000 + await clock.tickAsync(RemoteFetcher.retryIntervalMs) + assert.strictEqual(httpStub.callCount, 3) // 60_000 + await clock.tickAsync(RemoteFetcher.retryIntervalMs) + assert.strictEqual(httpStub.callCount, 4) // 120_000 + httpStub.throws(new Error('last error')) + await clock.tickAsync(RemoteFetcher.retryIntervalMs) + assert.strictEqual(httpStub.callCount, 5) // 150_000 + + // We hit timeout so the last error will be thrown await fetcher - .fetch('startUp', 'any') - .then(() => assert.ok(false, 'Did not throw exception.')) - .catch(() => assert.ok(true)) - await runClock - - httpStub.restore() }) }) diff --git a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/appNode.test.ts b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/appNode.test.ts index 80d83a8303c..d3c9b32555c 100644 --- a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/appNode.test.ts +++ b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/appNode.test.ts @@ -157,7 +157,7 @@ describe('AppNode', () => { assert.strictEqual(resourceNode.id, 'placeholder') assert.strictEqual( resourceNode.resource, - '[Unable to load Resource tree for this App. Update SAM template]' + '[Unable to load resource tree for this app. Ensure SAM template is correct.]' ) assert(getAppStub.calledOnce) assert(getStackNameStub.notCalled) diff --git a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts index 4917979fe1f..d9b5bd1c023 100644 --- a/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts +++ b/packages/core/src/test/shared/applicationBuilder/explorer/nodes/deployedNode.test.ts @@ -220,7 +220,10 @@ describe('generateDeployedNode', () => { // Check placeholder propertries const deployedResourceNode = deployedResourceNodes[0] as DeployedResourceNode assert.strictEqual(deployedResourceNode.id, 'placeholder') - assert.strictEqual(deployedResourceNode.resource, '[Failed to retrive deployed resource.]') + assert.strictEqual( + deployedResourceNode.resource, + '[Failed to retrieve deployed resource. Ensure correct stack name and region are in the samconfig.toml, and that your account is connected.]' + ) }) }) @@ -374,7 +377,7 @@ describe('generateDeployedNode', () => { // Check placeholder propertries const deployedResourceNode = deployedResourceNodes[0] as DeployedResourceNode assert.strictEqual(deployedResourceNode.id, 'placeholder') - assert.strictEqual(deployedResourceNode.resource, '[This resource is not yet supported.]') + assert.strictEqual(deployedResourceNode.resource, '[This resource is not yet supported in AppBuilder.]') }) }) }) diff --git a/packages/core/src/test/shared/datetime.test.ts b/packages/core/src/test/shared/datetime.test.ts index d7320e50ce0..1bac62cf078 100644 --- a/packages/core/src/test/shared/datetime.test.ts +++ b/packages/core/src/test/shared/datetime.test.ts @@ -11,8 +11,7 @@ import { globals } from '../../shared' describe('simple tests', () => { it('formatLocalized()', async function () { const d = new globals.clock.Date(globals.clock.Date.UTC(2013, 11, 17, 3, 24, 0)) - assert.deepStrictEqual(formatLocalized(d, false), 'Dec 16, 2013 7:24:00 PM GMT-8') - assert.deepStrictEqual(formatLocalized(d, true), 'Dec 16, 2013 7:24:00 PM PST') + assert.deepStrictEqual(formatLocalized(d), 'Dec 16, 2013 7:24:00 PM GMT-8') }) it('formatDateTimestamp()', async function () { diff --git a/packages/core/src/test/shared/extensionUtilities.test.ts b/packages/core/src/test/shared/extensionUtilities.test.ts index 0e29c9f7f06..0fc846f54ab 100644 --- a/packages/core/src/test/shared/extensionUtilities.test.ts +++ b/packages/core/src/test/shared/extensionUtilities.test.ts @@ -6,20 +6,16 @@ import assert from 'assert' import { AWSError } from 'aws-sdk' -import * as path from 'path' import * as sinon from 'sinon' import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient' import * as vscode from 'vscode' import { UserActivity, getComputeRegion, initializeComputeRegion } from '../../shared/extensionUtilities' import { isDifferentVersion, setMostRecentVersion } from '../../shared/extensionUtilities' -import * as filesystemUtilities from '../../shared/filesystemUtilities' -import { FakeExtensionContext } from '../fakeExtensionContext' import { InstanceIdentity } from '../../shared/clients/ec2MetadataClient' import { extensionVersion } from '../../shared/vscode/env' import { sleep } from '../../shared/utilities/timeoutUtils' import globals from '../../shared/extensionGlobals' -import { createQuickStartWebview, maybeShowMinVscodeWarning } from '../../shared/extensionStartup' -import { fs } from '../../shared' +import { maybeShowMinVscodeWarning } from '../../shared/extensionStartup' import { getTestWindow } from './vscode/window' import { assertTelemetry } from '../testUtil' @@ -34,53 +30,6 @@ describe('extensionUtilities', function () { assertTelemetry('toolkit_showNotification', []) }) - describe('createQuickStartWebview', async function () { - let context: FakeExtensionContext - let tempDir: string | undefined - - beforeEach(async function () { - context = await FakeExtensionContext.create() - tempDir = await filesystemUtilities.makeTemporaryToolkitFolder() - context.extensionPath = tempDir - }) - - afterEach(async function () { - if (tempDir) { - await fs.delete(tempDir, { recursive: true }) - } - }) - - it("throws error if a quick start page doesn't exist", async () => { - await assert.rejects(createQuickStartWebview(context, 'irresponsibly-named-file')) - }) - - it('returns a webview with unaltered text if a valid file is passed without tokens', async function () { - const filetext = 'this temp page does not have any tokens' - const filepath = 'tokenless' - await fs.writeFile(path.join(context.extensionPath, filepath), filetext) - const webview = await createQuickStartWebview(context, filepath) - - assert.strictEqual(typeof webview, 'object') - const forcedWebview = webview as vscode.WebviewPanel - assert.strictEqual(forcedWebview.webview.html.includes(filetext), true) - }) - - it('returns a webview with tokens replaced', async function () { - const token = '!!EXTENSIONROOT!!' - const basetext = 'this temp page has tokens: ' - const filetext = basetext + token - const filepath = 'tokenless' - await fs.writeFile(path.join(context.extensionPath, filepath), filetext) - const webview = await createQuickStartWebview(context, filepath) - - assert.strictEqual(typeof webview, 'object') - const forcedWebview = webview as vscode.WebviewPanel - - const pathAsVsCodeResource = forcedWebview.webview.asWebviewUri(vscode.Uri.file(context.extensionPath)) - assert.strictEqual(forcedWebview.webview.html.includes(`${basetext}${pathAsVsCodeResource}`), true) - }) - }) - describe('isDifferentVersion', function () { it('returns false if the version exists and matches the existing version exactly', async function () { const goodVersion = '1.2.3' diff --git a/packages/core/src/test/shared/fs/fs.test.ts b/packages/core/src/test/shared/fs/fs.test.ts index ad8d8777462..c816ecdde83 100644 --- a/packages/core/src/test/shared/fs/fs.test.ts +++ b/packages/core/src/test/shared/fs/fs.test.ts @@ -14,7 +14,6 @@ import fs, { FileSystem } from '../../../shared/fs/fs' import * as os from 'os' import { isMinVscode, isWin } from '../../../shared/vscode/env' import Sinon from 'sinon' -import * as extensionUtilities from '../../../shared/extensionUtilities' import { PermissionsError, formatError, isFileNotFoundError, scrubNames } from '../../../shared/errors' import { EnvironmentVariables } from '../../../shared/environmentVariables' import * as testutil from '../../testUtil' @@ -227,20 +226,6 @@ describe('FileSystem', function () { }) }) - // eslint-disable-next-line unicorn/no-array-for-each - paths.forEach(async function (p) { - it(`creates folder but uses the "fs" module if in Cloud9: '${p}'`, async function () { - sandbox.stub(extensionUtilities, 'isCloud9').returns(true) - const dirPath = testFolder.pathFrom(p) - const mkdirSpy = sandbox.spy(nodefs, 'mkdir') - - await fs.mkdir(dirPath) - - assert(existsSync(dirPath)) - assert.deepStrictEqual(mkdirSpy.args[0], [dirPath, { recursive: true }]) - }) - }) - it('does NOT throw if dir already exists', async function () { // We do not always want this behavior, but it seems that this is how the vsc implementation // does it. Look at the Node FS implementation instead as that throws if the dir already exists. @@ -281,32 +266,6 @@ describe('FileSystem', function () { function sorted(i: [string, vscode.FileType][]) { return i.sort((a, b) => a[0].localeCompare(b[0])) } - - it('uses the "fs" readdir implementation if in Cloud9', async function () { - sandbox.stub(extensionUtilities, 'isCloud9').returns(true) - const readdirSpy = sandbox.spy(nodefs, 'readdir') - - await testFolder.write('a.txt') - await testFolder.write('b.txt') - await testFolder.write('c.txt') - mkdirSync(testFolder.pathFrom('dirA')) - mkdirSync(testFolder.pathFrom('dirB')) - mkdirSync(testFolder.pathFrom('dirC')) - - const files = await fs.readdir(testFolder.path) - assert.deepStrictEqual( - sorted(files), - sorted([ - ['a.txt', vscode.FileType.File], - ['b.txt', vscode.FileType.File], - ['c.txt', vscode.FileType.File], - ['dirA', vscode.FileType.Directory], - ['dirB', vscode.FileType.Directory], - ['dirC', vscode.FileType.Directory], - ]) - ) - assert(readdirSpy.calledOnce) - }) }) describe('copy()', function () { @@ -360,19 +319,6 @@ describe('FileSystem', function () { assert(!existsSync(f)) await assert.rejects(() => fs.delete(f)) }) - - it('uses "node:fs" rm() if in Cloud9', async function () { - sandbox.stub(extensionUtilities, 'isCloud9').returns(true) - const rmdirSpy = sandbox.spy(nodefs, 'rm') - // Folder with subfolders - const dirPath = await testFolder.mkdir('a/b/deleteMe') - mkdirSync(dirPath, { recursive: true }) - - await fs.delete(dirPath, { recursive: true }) - - assert(rmdirSpy.calledOnce) - assert(!existsSync(dirPath)) - }) }) describe('stat()', function () { diff --git a/packages/core/src/test/shared/icons.test.ts b/packages/core/src/test/shared/icons.test.ts index 3fd1feaf373..edca2647d42 100644 --- a/packages/core/src/test/shared/icons.test.ts +++ b/packages/core/src/test/shared/icons.test.ts @@ -12,51 +12,12 @@ import { fs } from '../../shared' describe('getIcon', function () { it('returns a ThemeIcon for `vscode` codicons', function () { - const icon = getIcon('vscode-gear', false) + const icon = getIcon('vscode-gear') assert.ok(icon instanceof ThemeIcon) assert.strictEqual(icon.id, 'gear') }) - it('returns a ThemeIcon for `aws` icons', function () { - const icon = getIcon('aws-cdk-logo', false) - - assert.ok(icon instanceof ThemeIcon) - assert.strictEqual(icon.id, 'aws-cdk-logo') - }) - - it('returns icon URIs for non-codicon icons', function () { - const icon = getIcon('vscode-help', false) - - assert.ok(!(icon instanceof ThemeIcon)) - assert.ok(icon.dark.path.endsWith('/resources/icons/vscode/dark/help.svg')) - assert.ok(icon.light.path.endsWith('/resources/icons/vscode/light/help.svg')) - }) - - it('can use specific icons for Cloud9', function () { - const icon = getIcon('vscode-help', true) - - assert.ok(!(icon instanceof ThemeIcon)) - assert.ok(icon.dark.path.endsWith('/resources/icons/cloud9/dark/vscode-help.svg')) - assert.ok(icon.light.path.endsWith('/resources/icons/cloud9/light/vscode-help.svg')) - }) - - it('can use generated icons for Cloud9', function () { - const icon = getIcon('aws-cdk-logo', true) - - assert.ok(!(icon instanceof ThemeIcon)) - assert.ok(icon.dark.path.endsWith('/resources/icons/cloud9/generated/dark/aws-cdk-logo.svg')) - assert.ok(icon.light.path.endsWith('/resources/icons/cloud9/generated/light/aws-cdk-logo.svg')) - }) - - it('can use codicons for Cloud9', function () { - const icon = getIcon('vscode-gear', true) - - assert.ok(!(icon instanceof ThemeIcon)) - assert.ok(icon.dark.path.endsWith('/resources/icons/cloud9/generated/dark/vscode-gear.svg')) - assert.ok(icon.light.path.endsWith('/resources/icons/cloud9/generated/light/vscode-gear.svg')) - }) - it('can use overrides for contributed icons', async function () { const tempDir = await makeTemporaryToolkitFolder() @@ -72,7 +33,7 @@ describe('getIcon', function () { await fs.writeFile(p, '') } - const icon = getIcon('aws-cdk-logo', false, tempDir) + const icon = getIcon('aws-cdk-logo', tempDir) assert.ok(!(icon instanceof ThemeIcon)) assert.strictEqual(icon.dark.fsPath, Uri.file(paths[1]).fsPath) @@ -90,7 +51,7 @@ describe('getIcon', function () { await fs.mkdir(path.dirname(logoPath)) await fs.writeFile(logoPath, '') - const icon = getIcon('aws-cdk-logo', false, tempDir) + const icon = getIcon('aws-cdk-logo', tempDir) assert.ok(icon instanceof ThemeIcon) assert.strictEqual(icon.source?.fsPath, Uri.file(logoPath).fsPath) diff --git a/packages/core/src/test/shared/resourceFetcher/compositeResourceFetcher.test.ts b/packages/core/src/test/shared/resourceFetcher/compositeResourceFetcher.test.ts deleted file mode 100644 index 6387fcde9f9..00000000000 --- a/packages/core/src/test/shared/resourceFetcher/compositeResourceFetcher.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import { CompositeResourceFetcher } from '../../../shared/resourcefetcher/compositeResourceFetcher' - -describe('CompositeResourceFetcher', async function () { - const expectedContents = 'Hello World!\n12345' - - it('loads from a resource fetcher', async function () { - const fetcher = { - get: async () => expectedContents, - } - - const sut = new CompositeResourceFetcher(fetcher) - - const contents = await sut.get() - assert.strictEqual(contents, expectedContents) - }) - - it('loads from the first resource fetcher to return contents', async function () { - const fetcher1 = { - get: async () => undefined, - } - - const fetcher2 = { - get: async () => expectedContents, - } - - const fetcher3 = { - get: async () => { - assert.fail('This should never be called') - }, - } - - const sut = new CompositeResourceFetcher(fetcher1, fetcher2, fetcher3) - - const contents = await sut.get() - assert.strictEqual(contents, expectedContents) - }) - - it('tries to load from the next resource fetcher when one raises an error', async function () { - const fetcher1 = { - get: async () => { - assert.fail('Error, load from the next fetcher') - }, - } - - const fetcher2 = { - get: async () => expectedContents, - } - - const sut = new CompositeResourceFetcher(fetcher1, fetcher2) - - const contents = await sut.get() - assert.strictEqual(contents, expectedContents) - }) - - it('returns undefined if no resource fetcher returns contents', async function () { - let timesCalled = 0 - const fetcher = { - get: async () => { - timesCalled++ - - return undefined - }, - } - - const sut = new CompositeResourceFetcher(fetcher, fetcher) - - const contents = await sut.get() - assert.strictEqual(contents, undefined) - assert.strictEqual(timesCalled, 2, 'fetcher was not called the expected amount of times') - }) -}) diff --git a/packages/core/src/test/shared/resourceFetcher/httpResourceFetcher.test.ts b/packages/core/src/test/shared/resourceFetcher/httpResourceFetcher.test.ts index bf22f152fa6..18a5f0e9cbb 100644 --- a/packages/core/src/test/shared/resourceFetcher/httpResourceFetcher.test.ts +++ b/packages/core/src/test/shared/resourceFetcher/httpResourceFetcher.test.ts @@ -6,6 +6,7 @@ import assert from 'assert' import { HttpResourceFetcher, getPropertyFromJsonUrl } from '../../../shared/resourcefetcher/httpResourceFetcher' import { stub } from '../../utilities/stubber' +import { createResponse } from '../../testUtil' describe('getPropertyFromJsonUrl', function () { const dummyUrl = 'url' @@ -19,18 +20,18 @@ describe('getPropertyFromJsonUrl', function () { it('undefined if resource is not JSON', async function () { const mockFetcher = stub(HttpResourceFetcher) - mockFetcher.get.resolves('foo' as any) // horrible hack: this works without the declaration but the language server latches onto this using a FetcherResult return type + mockFetcher.get.resolves(createResponse('foo')) assert.strictEqual(await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher), undefined) }) it('undefined if property is not present', async function () { const mockFetcher = stub(HttpResourceFetcher) - mockFetcher.get.resolves('{"foo": "bar"}' as any) + mockFetcher.get.resolves(createResponse('{"foo": "bar"}')) assert.strictEqual(await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher), undefined) }) it('returns value if property is present', async function () { const mockFetcher = stub(HttpResourceFetcher) - mockFetcher.get.resolves('{"property": "111"}' as any) + mockFetcher.get.resolves(createResponse('{"property": "111"}')) mockFetcher.getNewETagContent.resolves({ content: 'foo', eTag: '' }) const value = await getPropertyFromJsonUrl(dummyUrl, dummyProperty, mockFetcher) assert.strictEqual(value, '111') diff --git a/packages/core/src/test/shared/sam/cli/samCliInit.test.ts b/packages/core/src/test/shared/sam/cli/samCliInit.test.ts index 9ee8d889368..854fdc91610 100644 --- a/packages/core/src/test/shared/sam/cli/samCliInit.test.ts +++ b/packages/core/src/test/shared/sam/cli/samCliInit.test.ts @@ -9,7 +9,6 @@ import { eventBridgeStarterAppTemplate, getSamCliTemplateParameter, helloWorldTemplate, - lazyLoadSamTemplateStrings, } from '../../../../lambda/models/samTemplates' import { SamCliContext } from '../../../../shared/sam/cli/samCliContext' import { runSamCliInit, SamCliInitArgs } from '../../../../shared/sam/cli/samCliInit' @@ -52,8 +51,6 @@ describe('runSamCliInit', async function () { let sampleSamInitArgs: SamCliInitArgs before(function () { - lazyLoadSamTemplateStrings() - sampleSamInitArgs = { name: 'qwerty', location: '/some/path/to/code.js', @@ -198,8 +195,6 @@ describe('runSamCliInit', async function () { let samInitArgsWithExtraContent: SamCliInitArgs before(function () { - lazyLoadSamTemplateStrings() - // eslint-disable-next-line @typescript-eslint/naming-convention extraContent = { AWS_Schema_registry: 'testRegistry', diff --git a/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts index cf9586c9a28..9fe7c76e842 100644 --- a/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts +++ b/packages/core/src/test/shared/sam/debugger/samDebugConfigProvider.test.ts @@ -555,7 +555,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'nodejs18.x', runtimeFamily: lambdaModel.RuntimeFamily.NodeJS, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -721,7 +720,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'nodejs18.x', runtimeFamily: lambdaModel.RuntimeFamily.NodeJS, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -881,7 +879,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'nodejs20.x', runtimeFamily: lambdaModel.RuntimeFamily.NodeJS, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -1008,7 +1005,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'nodejs18.x', runtimeFamily: lambdaModel.RuntimeFamily.NodeJS, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -1147,7 +1143,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'nodejs20.x', runtimeFamily: lambdaModel.RuntimeFamily.NodeJS, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -1228,7 +1223,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'java17', runtimeFamily: lambdaModel.RuntimeFamily.Java, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1331,7 +1325,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'java17', runtimeFamily: lambdaModel.RuntimeFamily.Java, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1444,7 +1437,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'java17', runtimeFamily: lambdaModel.RuntimeFamily.Java, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1543,7 +1535,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'java11', runtimeFamily: lambdaModel.RuntimeFamily.Java, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1640,7 +1631,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'dotnet6', // lambdaModel.dotNetRuntimes[0], runtimeFamily: lambdaModel.RuntimeFamily.DotNet, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1809,7 +1799,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'dotnet6', // lambdaModel.dotNetRuntimes[0], runtimeFamily: lambdaModel.RuntimeFamily.DotNet, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -1964,7 +1953,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'dotnet6', // lambdaModel.dotNetRuntimes[0], runtimeFamily: lambdaModel.RuntimeFamily.DotNet, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, workspaceFolder: { index: 0, @@ -2129,7 +2117,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'python3.7', runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', workspaceFolder: { @@ -2275,7 +2262,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'python3.7', runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', workspaceFolder: { @@ -2401,7 +2387,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'python3.7', runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, handlerName: 'app.lambda_handler', workspaceFolder: { @@ -2493,7 +2478,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'python3.7', runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: false, type: AWS_SAM_DEBUG_TYPE, handlerName: 'HelloWorldFunction', workspaceFolder: { @@ -2635,207 +2619,6 @@ describe('SamDebugConfigurationProvider', async function () { getLogger().setLogLevel('debug') }) - it('target=code: ikpdb, python 3.7', async function () { - const appDir = pathutil.normalize( - path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/python3.7-plain-sam-app') - ) - const folder = testutil.getWorkspaceFolder(appDir) - const input = { - type: AWS_SAM_DEBUG_TYPE, - name: 'test: ikpdb target=code', - request: DIRECT_INVOKE_TYPE, - invokeTarget: { - target: CODE_TARGET_TYPE, - lambdaHandler: 'app.lambda_handler', - projectRoot: 'hello_world', - }, - lambda: { - runtime: 'python3.7', - payload: { - path: `${appDir}/events/event.json`, - }, - }, - // Force ikpdb in non-cloud9 environment. - useIkpdb: true, - } - - // Invoke with noDebug=false (the default). - const actual = (await debugConfigProvider.makeConfig(folder, input))! - // Expected result with noDebug=false. - const expected: SamLaunchRequestArgs = { - awsCredentials: fakeCredentials, - request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', - runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: true, - type: AWS_SAM_DEBUG_TYPE, - handlerName: 'app.lambda_handler', - workspaceFolder: { - index: 0, - name: 'test-workspace-folder', - uri: vscode.Uri.file(appDir), - }, - baseBuildDir: actual.baseBuildDir, // Random, sanity-checked by assertEqualLaunchConfigs(). - envFile: undefined, - eventPayloadFile: `${actual.baseBuildDir}/event.json`, - codeRoot: pathutil.normalize(path.join(appDir, 'hello_world')), - debugArgs: [ - `-m ikp3db --ikpdb-address=0.0.0.0 --ikpdb-port=${actual.debugPort} -ik_ccwd=hello_world -ik_cwd=/var/task --ikpdb-log=BEXFPG`, - ], - apiPort: actual.apiPort, - debugPort: actual.debugPort, - documentUri: vscode.Uri.file(''), // TODO: remove or test. - invokeTarget: { ...input.invokeTarget }, - lambda: { - ...input.lambda, - environmentVariables: {}, - memoryMb: undefined, - timeoutSec: undefined, - }, - sam: { - containerBuild: true, - }, - name: input.name, - templatePath: pathutil.normalize(path.join(actual.baseBuildDir!, 'app___vsctk___template.yaml')), - parameterOverrides: undefined, - architecture: undefined, - region: 'us-west-2', - - // - // Python-ikpdb fields - // - port: actual.debugPort, - address: 'localhost', - localRoot: pathutil.normalize(path.join(appDir, 'hello_world')), - remoteRoot: '/var/task', - } - - assertEqualLaunchConfigs(actual, expected) - assert.strictEqual( - await fs.readFileText(actual.eventPayloadFile!), - await fs.readFileText(input.lambda.payload.path) - ) - await assertFileText( - expected.templatePath, - `Resources: - helloworld: - Type: AWS::Serverless::Function - Properties: - Handler: ${expected.handlerName} - CodeUri: >- - ${expected.codeRoot} - Runtime: python3.7 -` - ) - - // - // Test noDebug=true. - // - ;(input as any).noDebug = true - const actualNoDebug = (await debugConfigProvider.makeConfig(folder, input))! as SamLaunchRequestArgs - const expectedNoDebug: SamLaunchRequestArgs = { - ...expected, - noDebug: true, - request: 'launch', - debugPort: undefined, - port: -1, - handlerName: 'app.lambda_handler', - baseBuildDir: actualNoDebug.baseBuildDir, - envFile: undefined, - eventPayloadFile: `${actualNoDebug.baseBuildDir}/event.json`, - } - assertEqualLaunchConfigs(actualNoDebug, expectedNoDebug) - }) - - it('target=template: ikpdb, python 3.7 (deep project tree)', async function () { - // To test a deeper tree, use "testFixtures/workspaceFolder/" as the root. - const appDir = pathutil.normalize(path.join(testutil.getProjectDir(), 'testFixtures/workspaceFolder/')) - const folder = testutil.getWorkspaceFolder(appDir) - const input = { - type: AWS_SAM_DEBUG_TYPE, - name: 'test-py37-template', - request: DIRECT_INVOKE_TYPE, - invokeTarget: { - target: TEMPLATE_TARGET_TYPE, - templatePath: 'python3.7-plain-sam-app/template.yaml', - logicalId: 'HelloWorldFunction', - }, - // Force ikpdb in non-cloud9 environment. - useIkpdb: true, - } - const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml')) - - // Invoke with noDebug=false (the default). - const actual = (await debugConfigProvider.makeConfig(folder, input))! - // Expected result with noDebug=false. - const expected: SamLaunchRequestArgs = { - awsCredentials: fakeCredentials, - request: 'attach', // Input "direct-invoke", output "attach". - runtime: 'python3.7', - runtimeFamily: lambdaModel.RuntimeFamily.Python, - useIkpdb: true, - type: AWS_SAM_DEBUG_TYPE, - handlerName: 'app.lambda_handler', - workspaceFolder: { - index: 0, - name: 'test-workspace-folder', - uri: vscode.Uri.file(appDir), - }, - baseBuildDir: actual.baseBuildDir, // Random, sanity-checked by assertEqualLaunchConfigs(). - envFile: undefined, - eventPayloadFile: undefined, - codeRoot: pathutil.normalize(path.join(appDir, 'python3.7-plain-sam-app/hello_world')), - apiPort: undefined, - debugArgs: [ - `-m ikp3db --ikpdb-address=0.0.0.0 --ikpdb-port=${actual.debugPort} -ik_ccwd=python3.7-plain-sam-app/hello_world -ik_cwd=/var/task --ikpdb-log=BEXFPG`, - ], - debugPort: actual.debugPort, - documentUri: vscode.Uri.file(''), // TODO: remove or test. - invokeTarget: { ...input.invokeTarget }, - lambda: { - environmentVariables: {}, - memoryMb: undefined, - timeoutSec: 3, - }, - sam: { - containerBuild: true, - }, - name: input.name, - templatePath: pathutil.normalize(path.join(path.dirname(templatePath.fsPath), 'template.yaml')), - parameterOverrides: undefined, - architecture: undefined, - region: 'us-west-2', - - // - // Python-ikpdb fields - // - port: actual.debugPort, - address: 'localhost', - localRoot: pathutil.normalize(path.join(appDir, 'hello_world')), - remoteRoot: '/var/task', - } - - assertEqualLaunchConfigs(actual, expected) - - // - // Test noDebug=true. - // - ;(input as any).noDebug = true - const actualNoDebug = (await debugConfigProvider.makeConfig(folder, input))! - const expectedNoDebug: SamLaunchRequestArgs = { - ...expected, - noDebug: true, - request: 'launch', - debugPort: undefined, - port: -1, - handlerName: 'app.lambda_handler', - baseBuildDir: actualNoDebug.baseBuildDir, - envFile: undefined, - eventPayloadFile: undefined, - } - assertEqualLaunchConfigs(actualNoDebug, expectedNoDebug) - }) - it('debugconfig with "aws" section', async function () { // Simluates credentials in "aws.credentials" launch-config field. const configCredentials: Credentials = { @@ -2918,7 +2701,6 @@ describe('SamDebugConfigurationProvider', async function () { awsCredentials: configCredentials, ...awsSection, type: AWS_SAM_DEBUG_TYPE, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', @@ -3010,7 +2792,6 @@ describe('SamDebugConfigurationProvider', async function () { request: 'attach', // Input "direct-invoke", output "attach". runtime: 'go1.x', runtimeFamily: lambdaModel.RuntimeFamily.Go, - useIkpdb: false, workspaceFolder: { index: 0, name: 'test-workspace-folder', diff --git a/packages/core/src/test/shared/settings.test.ts b/packages/core/src/test/shared/settings.test.ts index da68ada2cde..a868d6fbe66 100644 --- a/packages/core/src/test/shared/settings.test.ts +++ b/packages/core/src/test/shared/settings.test.ts @@ -276,7 +276,7 @@ describe('Settings', function () { }) describe('DevSetting', function () { - const testSetting = 'forceCloud9' + const testSetting = 'renderDebugDetails' let settings: ClassToInterfaceType let sut: DevSettings diff --git a/packages/core/src/test/shared/utilities/functionUtils.test.ts b/packages/core/src/test/shared/utilities/functionUtils.test.ts index fb87cf3501e..43da4ebb619 100644 --- a/packages/core/src/test/shared/utilities/functionUtils.test.ts +++ b/packages/core/src/test/shared/utilities/functionUtils.test.ts @@ -4,10 +4,8 @@ */ import assert from 'assert' -import { once, onceChanged, debounce, withRetries } from '../../../shared/utilities/functionUtils' +import { once, onceChanged, debounce } from '../../../shared/utilities/functionUtils' import { installFakeClock } from '../../testUtil' -import { stub, SinonStub } from 'sinon' -import { InstalledClock } from '@sinonjs/fake-timers' describe('functionUtils', function () { it('once()', function () { @@ -109,82 +107,3 @@ describe('debounce', function () { }) }) }) - -// function to test the withRetries method. It passes in a stub function as the argument and has different tests that throw on different iterations -describe('withRetries', function () { - let clock: InstalledClock - let fn: SinonStub - - beforeEach(function () { - fn = stub() - clock = installFakeClock() - }) - - afterEach(function () { - clock.uninstall() - }) - - it('retries the function until it succeeds, using defaults', async function () { - fn.onCall(0).throws() - fn.onCall(1).throws() - fn.onCall(2).resolves('success') - assert.strictEqual(await withRetries(fn), 'success') - }) - - it('retries the function until it succeeds at the final try', async function () { - fn.onCall(0).throws() - fn.onCall(1).throws() - fn.onCall(2).throws() - fn.onCall(3).resolves('success') - assert.strictEqual(await withRetries(fn, { maxRetries: 4 }), 'success') - }) - - it('throws the last error if the function always fails, using defaults', async function () { - fn.onCall(0).throws() - fn.onCall(1).throws() - fn.onCall(2).throws() - fn.onCall(3).resolves('unreachable') - await assert.rejects(async () => { - await withRetries(fn) - }) - }) - - it('throws the last error if the function always fails', async function () { - fn.onCall(0).throws() - fn.onCall(1).throws() - fn.onCall(2).throws() - fn.onCall(3).throws() - fn.onCall(4).resolves('unreachable') - await assert.rejects(async () => { - await withRetries(fn, { maxRetries: 4 }) - }) - }) - - it('honors retry delay + backoff multiplier', async function () { - fn.onCall(0).throws() // 100ms - fn.onCall(1).throws() // 200ms - fn.onCall(2).throws() // 400ms - fn.onCall(3).resolves('success') - - const res = withRetries(fn, { maxRetries: 4, delay: 100, backoff: 2 }) - - // Check the call count after each iteration, ensuring the function is called - // after the correct delay between retries. - await clock.tickAsync(99) - assert.strictEqual(fn.callCount, 1) - await clock.tickAsync(1) - assert.strictEqual(fn.callCount, 2) - - await clock.tickAsync(199) - assert.strictEqual(fn.callCount, 2) - await clock.tickAsync(1) - assert.strictEqual(fn.callCount, 3) - - await clock.tickAsync(399) - assert.strictEqual(fn.callCount, 3) - await clock.tickAsync(1) - assert.strictEqual(fn.callCount, 4) - - assert.strictEqual(await res, 'success') - }) -}) diff --git a/packages/core/src/test/shared/utilities/pollingSet.test.ts b/packages/core/src/test/shared/utilities/pollingSet.test.ts index 55fb1e87a88..74a7ffbe284 100644 --- a/packages/core/src/test/shared/utilities/pollingSet.test.ts +++ b/packages/core/src/test/shared/utilities/pollingSet.test.ts @@ -56,7 +56,7 @@ describe('pollingSet', function () { const action = sinon.spy() pollingSet = new PollingSet(10, action) sinon.assert.notCalled(action) - pollingSet.start('item') + pollingSet.add('item') await clock.tickAsync(9) sinon.assert.notCalled(action) @@ -66,7 +66,7 @@ describe('pollingSet', function () { it('stops timer once polling set is empty', async function () { const pollingSet = new PollingSet(10, () => {}) - pollingSet.start('1') + pollingSet.add('1') pollingSet.add('2') const clearStub = sinon.stub(pollingSet, 'clearTimer') @@ -90,7 +90,7 @@ describe('pollingSet', function () { it('runs action once per interval', async function () { const action = sinon.spy() pollingSet = new PollingSet(10, action) - pollingSet.start('1') + pollingSet.add('1') pollingSet.add('2') sinon.assert.callCount(action, 0) diff --git a/packages/core/src/test/shared/utilities/processUtils.test.ts b/packages/core/src/test/shared/utilities/processUtils.test.ts index c6e3da722db..0e6f474ed10 100644 --- a/packages/core/src/test/shared/utilities/processUtils.test.ts +++ b/packages/core/src/test/shared/utilities/processUtils.test.ts @@ -6,11 +6,22 @@ import assert from 'assert' import * as os from 'os' import * as path from 'path' +import * as sinon from 'sinon' import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../../shared/filesystemUtilities' -import { ChildProcess, eof } from '../../../shared/utilities/processUtils' +import { + ChildProcess, + ChildProcessResult, + ChildProcessTracker, + eof, + ProcessStats, +} from '../../../shared/utilities/processUtils' import { sleep } from '../../../shared/utilities/timeoutUtils' import { Timeout, waitUntil } from '../../../shared/utilities/timeoutUtils' import { fs } from '../../../shared' +import * as FakeTimers from '@sinonjs/fake-timers' +import { installFakeClock } from '../../testUtil' +import { isWin } from '../../../shared/vscode/env' +import { assertLogsContain } from '../../globalSetup.test' describe('ChildProcess', async function () { let tempFolder: string @@ -350,3 +361,142 @@ describe('ChildProcess', async function () { await writeShellFile(filename, file) } }) + +interface RunningProcess { + childProcess: ChildProcess + result: Promise +} + +function getSleepCmd() { + return isWin() ? 'timeout' : 'sleep' +} + +async function stopAndWait(runningProcess: RunningProcess): Promise { + runningProcess.childProcess.stop(true) + await runningProcess.result +} + +function startSleepProcess(timeout: number = 90): RunningProcess { + const childProcess = new ChildProcess(getSleepCmd(), [timeout.toString()]) + const result = childProcess.run().catch(() => assert.fail('sleep command threw an error')) + return { childProcess, result } +} + +describe('ChildProcessTracker', function () { + let tracker: ChildProcessTracker + let clock: FakeTimers.InstalledClock + let usageMock: sinon.SinonStub + + before(function () { + clock = installFakeClock() + tracker = new ChildProcessTracker() + usageMock = sinon.stub(ChildProcessTracker.prototype, 'getUsage') + }) + + afterEach(function () { + tracker.clear() + usageMock.reset() + }) + + after(function () { + clock.uninstall() + }) + + it(`removes stopped processes every ${ChildProcessTracker.pollingInterval / 1000} seconds`, async function () { + // Start a 'sleep' command, check it only removes after we stop it. + const runningProcess = startSleepProcess() + tracker.add(runningProcess.childProcess) + assert.strictEqual(tracker.has(runningProcess.childProcess), true, 'failed to add sleep command') + + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assert.strictEqual(tracker.has(runningProcess.childProcess), true, 'process was mistakenly removed') + await stopAndWait(runningProcess) + + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assert.strictEqual(tracker.has(runningProcess.childProcess), false, 'process was not removed after stopping') + }) + + it('multiple processes from same command are tracked seperately', async function () { + const runningProcess1 = startSleepProcess() + const runningProcess2 = startSleepProcess() + tracker.add(runningProcess1.childProcess) + tracker.add(runningProcess2.childProcess) + + assert.strictEqual(tracker.has(runningProcess1.childProcess), true, 'Missing first process') + assert.strictEqual(tracker.has(runningProcess2.childProcess), true, 'Missing second process') + + await stopAndWait(runningProcess1) + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assert.strictEqual(tracker.has(runningProcess2.childProcess), true, 'second process was mistakenly removed') + assert.strictEqual( + tracker.has(runningProcess1.childProcess), + false, + 'first process was not removed after stopping it' + ) + + await stopAndWait(runningProcess2) + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assert.strictEqual( + tracker.has(runningProcess2.childProcess), + false, + 'second process was not removed after stopping it' + ) + + assert.strictEqual(tracker.size, 0, 'expected tracker to be empty') + }) + + it('logs a warning message when system usage exceeds threshold', async function () { + const runningProcess = startSleepProcess() + tracker.add(runningProcess.childProcess) + + const highCpu: ProcessStats = { + cpu: ChildProcessTracker.thresholds.cpu + 1, + memory: 0, + } + const highMemory: ProcessStats = { + cpu: 0, + memory: ChildProcessTracker.thresholds.memory + 1, + } + + usageMock.returns(highCpu) + + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assertLogsContain('exceeded cpu threshold', false, 'warn') + + usageMock.returns(highMemory) + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assertLogsContain('exceeded memory threshold', false, 'warn') + + await stopAndWait(runningProcess) + }) + + it('includes pid in logs', async function () { + const runningProcess = startSleepProcess() + tracker.add(runningProcess.childProcess) + + usageMock.returns({ + cpu: ChildProcessTracker.thresholds.cpu + 1, + memory: 0, + }) + + await clock.tickAsync(ChildProcessTracker.pollingInterval) + assertLogsContain(runningProcess.childProcess.pid().toString(), false, 'warn') + + await stopAndWait(runningProcess) + }) + + it('does not log for processes within threshold', async function () { + const runningProcess = startSleepProcess() + + usageMock.returns({ + cpu: ChildProcessTracker.thresholds.cpu - 1, + memory: ChildProcessTracker.thresholds.memory - 1, + }) + + await clock.tickAsync(ChildProcessTracker.pollingInterval) + + assert.throws(() => assertLogsContain(runningProcess.childProcess.pid().toString(), false, 'warn')) + + await stopAndWait(runningProcess) + }) +}) diff --git a/packages/core/src/test/shared/utilities/timeoutUtils.test.ts b/packages/core/src/test/shared/utilities/timeoutUtils.test.ts index c518b1cfae1..3ee5968132b 100644 --- a/packages/core/src/test/shared/utilities/timeoutUtils.test.ts +++ b/packages/core/src/test/shared/utilities/timeoutUtils.test.ts @@ -7,7 +7,7 @@ import assert from 'assert' import * as FakeTimers from '@sinonjs/fake-timers' import * as timeoutUtils from '../../../shared/utilities/timeoutUtils' import { installFakeClock, tickPromise } from '../../../test/testUtil' -import { sleep } from '../../../shared/utilities/timeoutUtils' +import { sleep, waitUntil } from '../../../shared/utilities/timeoutUtils' import { SinonStub, SinonSandbox, createSandbox } from 'sinon' // We export this describe() so it can be used in the web tests as well @@ -303,6 +303,17 @@ export const timeoutUtilsDescribe = describe('timeoutUtils', async function () { assert.strictEqual(returnValue, testSettings.callGoal) }) + it('returns value after multiple function calls WITH backoff', async function () { + testSettings.callGoal = 4 + const returnValue: number | undefined = await timeoutUtils.waitUntil(testFunction, { + timeout: 10000, + interval: 10, + truthy: false, + backoff: 2, + }) + assert.strictEqual(returnValue, testSettings.callGoal) + }) + it('timeout before function returns defined value', async function () { testSettings.callGoal = 7 const returnValue: number | undefined = await timeoutUtils.waitUntil(testFunction, { @@ -376,6 +387,90 @@ export const timeoutUtilsDescribe = describe('timeoutUtils', async function () { }) }) + describe('waitUntil w/ retries', function () { + let fn: SinonStub<[], Promise> + + beforeEach(function () { + fn = sandbox.stub() + }) + + it('retries the function until it succeeds', async function () { + fn.onCall(0).throws() + fn.onCall(1).throws() + fn.onCall(2).resolves('success') + + const res = waitUntil(fn, { retryOnFail: true }) + + await clock.tickAsync(timeoutUtils.waitUntilDefaultInterval) + assert.strictEqual(fn.callCount, 2) + await clock.tickAsync(timeoutUtils.waitUntilDefaultInterval) + assert.strictEqual(fn.callCount, 3) + assert.strictEqual(await res, 'success') + }) + + it('retryOnFail ignores truthiness', async function () { + fn.resolves(false) + const res = waitUntil(fn, { retryOnFail: true, truthy: true }) + assert.strictEqual(await res, false) + }) + + // This test causes the following error, cannot figure out why: + // `rejected promise not handled within 1 second: Error: last` + it('throws the last error if the function always fails, using defaults', async function () { + fn.onCall(0).throws() // 0 + fn.onCall(1).throws() // 500 + fn.onCall(2).throws(new Error('second last')) // 1000 + fn.onCall(3).throws(new Error('last')) // 1500 + fn.onCall(4).resolves('this is not hit') + + // We must wrap w/ assert.rejects() here instead of at the end, otherwise Mocha raise a + // `rejected promise not handled within 1 second: Error: last` + const res = assert.rejects( + waitUntil(fn, { retryOnFail: true }), + (e) => e instanceof Error && e.message === 'last' + ) + + await clock.tickAsync(timeoutUtils.waitUntilDefaultInterval) // 500 + assert.strictEqual(fn.callCount, 2) + await clock.tickAsync(timeoutUtils.waitUntilDefaultInterval) // 1000 + assert.strictEqual(fn.callCount, 3) + await clock.tickAsync(timeoutUtils.waitUntilDefaultInterval) // 1500 + assert.strictEqual(fn.callCount, 4) + + await res + }) + + it('honors retry delay + backoff multiplier', async function () { + fn.onCall(0).throws(Error('0')) // 0ms + fn.onCall(1).throws(Error('1')) // 100ms + fn.onCall(2).throws(Error('2')) // 200ms + fn.onCall(3).resolves('success') // 400ms + + // Note 701 instead of 700 for timeout. The 1 millisecond allows the final call to execute + // since the timeout condition is >= instead of > + const res = waitUntil(fn, { timeout: 701, interval: 100, backoff: 2, retryOnFail: true }) + + // Check the call count after each iteration, ensuring the function is called + // after the correct delay between retries. + await clock.tickAsync(99) + assert.strictEqual(fn.callCount, 1) + await clock.tickAsync(1) + assert.strictEqual(fn.callCount, 2) + + await clock.tickAsync(199) + assert.strictEqual(fn.callCount, 2) + await clock.tickAsync(1) + assert.strictEqual(fn.callCount, 3) + + await clock.tickAsync(399) + assert.strictEqual(fn.callCount, 3) + await clock.tickAsync(1) + assert.strictEqual(fn.callCount, 4) + + assert.strictEqual(await res, 'success') + }) + }) + describe('waitTimeout', async function () { async function testFunction(delay: number = 500, error?: Error) { await sleep(delay) diff --git a/packages/core/src/test/testUtil.ts b/packages/core/src/test/testUtil.ts index d5b9cc324c2..e2ff480ea27 100644 --- a/packages/core/src/test/testUtil.ts +++ b/packages/core/src/test/testUtil.ts @@ -18,7 +18,7 @@ import { DeclaredCommand } from '../shared/vscode/commands2' import { mkdirSync, existsSync } from 'fs' // eslint-disable-line no-restricted-imports import { randomBytes } from 'crypto' import request from '../shared/request' -import { stub } from 'sinon' +import { createStubInstance, stub } from 'sinon' const testTempDirs: string[] = [] @@ -634,3 +634,10 @@ export function getFetchStubWithResponse(response: Partial) { export function copyEnv(): NodeJS.ProcessEnv { return { ...process.env } } + +// Returns a stubbed response object +export function createResponse(text: string): Response { + const responseStub = createStubInstance(Response) + responseStub.text.resolves(text) + return responseStub +} diff --git a/packages/core/src/webviews/main.ts b/packages/core/src/webviews/main.ts index 379dc869fd8..1fa18d3c460 100644 --- a/packages/core/src/webviews/main.ts +++ b/packages/core/src/webviews/main.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import { isCloud9 } from '../shared/extensionUtilities' import { Protocol, registerWebviewServer } from './server' import { getIdeProperties } from '../shared/extensionUtilities' import { getFunctions } from '../shared/utilities/classUtils' @@ -345,11 +344,7 @@ export type ClassToProtocol = FilterUnknown & * Creates a brand new webview panel, setting some basic initial parameters and updating the webview. */ function createWebviewPanel(ctx: vscode.ExtensionContext, params: WebviewPanelParams): vscode.WebviewPanel { - // C9 doesn't support 'Beside'. The next best thing is always using the second column. - const viewColumn = - isCloud9() && params.viewColumn === vscode.ViewColumn.Beside - ? vscode.ViewColumn.Two - : (params.viewColumn ?? vscode.ViewColumn.Active) + const viewColumn = params.viewColumn ?? vscode.ViewColumn.Active const panel = vscode.window.createWebviewPanel( params.id, @@ -358,9 +353,10 @@ function createWebviewPanel(ctx: vscode.ExtensionContext, params: WebviewPanelPa { // The redundancy here is to correct a bug with Cloud9's Webview implementation // We need to assign certain things on instantiation, otherwise they'll never be applied to the view + // TODO: Comment is old, no cloud9 support anymore. Is this needed? enableScripts: true, enableCommandUris: true, - retainContextWhenHidden: isCloud9() || params.retainContextWhenHidden, + retainContextWhenHidden: params.retainContextWhenHidden, } ) updateWebview(ctx, panel.webview, params) @@ -392,7 +388,7 @@ function updateWebview(ctx: vscode.ExtensionContext, webview: vscode.Webview, pa ]) const css = resolveRelative(webview, vscode.Uri.joinPath(resources, 'css'), [ - isCloud9() ? 'base-cloud9.css' : 'base.css', + 'base.css', ...(params.cssFiles ?? []), ]) @@ -403,7 +399,7 @@ function updateWebview(ctx: vscode.ExtensionContext, webview: vscode.Webview, pa stylesheets: css.map((p) => `\n`).join('\n'), main: mainScript, webviewJs: params.webviewJs, - cspSource: updateCspSource(webview.cspSource), + cspSource: webview.cspSource, }) return webview @@ -455,12 +451,3 @@ function resolveWebviewHtml(params: { ` } - -/** - * Updates the CSP source for webviews with an allowed source for AWS endpoints when running in - * Cloud9 environments. Possible this can be further scoped to specific C9 CDNs or removed entirely - * if C9 injects this. - */ -export function updateCspSource(baseSource: string) { - return isCloud9() ? `https://*.amazonaws.com ${baseSource}` : baseSource -} diff --git a/packages/toolkit/.changes/3.42.0.json b/packages/toolkit/.changes/3.42.0.json new file mode 100644 index 00000000000..73b08afc915 --- /dev/null +++ b/packages/toolkit/.changes/3.42.0.json @@ -0,0 +1,10 @@ +{ + "date": "2025-01-15", + "version": "3.42.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Auth: Valid StartURL not accepted at login" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/3.43.0.json b/packages/toolkit/.changes/3.43.0.json new file mode 100644 index 00000000000..f8785bff22f --- /dev/null +++ b/packages/toolkit/.changes/3.43.0.json @@ -0,0 +1,18 @@ +{ + "date": "2025-01-23", + "version": "3.43.0", + "entries": [ + { + "type": "Bug Fix", + "description": "AppBuilder: Update error messaging to make more legible and actionable" + }, + { + "type": "Bug Fix", + "description": "Notifications: 'Dismiss' command visible in command palette." + }, + { + "type": "Removal", + "description": "Cloud9: remove special-case logic." + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/next-release/Bug Fix-29e6ef4c-536b-47bb-ae27-26b802ccdb65.json b/packages/toolkit/.changes/next-release/Bug Fix-29e6ef4c-536b-47bb-ae27-26b802ccdb65.json deleted file mode 100644 index e0c15b7f2dc..00000000000 --- a/packages/toolkit/.changes/next-release/Bug Fix-29e6ef4c-536b-47bb-ae27-26b802ccdb65.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Auth: Valid StartURL not accepted at login" -} diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 82d89b566cf..42a378df667 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,13 @@ +## 3.43.0 2025-01-23 + +- **Bug Fix** AppBuilder: Update error messaging to make more legible and actionable +- **Bug Fix** Notifications: 'Dismiss' command visible in command palette. +- **Removal** Cloud9: remove special-case logic. + +## 3.42.0 2025-01-15 + +- **Bug Fix** Auth: Valid StartURL not accepted at login + ## 3.41.0 2025-01-09 - **Removal** Amazon Q: No longer autoinstall Amazon Q if the user had used CodeWhisperer in old Toolkit versions. diff --git a/packages/toolkit/README.quickstart.cloud9.md b/packages/toolkit/README.quickstart.cloud9.md deleted file mode 100644 index 2fe1b3f9e36..00000000000 --- a/packages/toolkit/README.quickstart.cloud9.md +++ /dev/null @@ -1,146 +0,0 @@ -# AWS Toolkit - -The AWS Toolkit extension for AWS Cloud9 that enables you to interact with [Amazon Web Services (AWS)](https://aws.amazon.com/what-is-aws/). -See the [user guide](https://docs.aws.amazon.com/cloud9/latest/user-guide/toolkit-welcome.html) for complete documentation. - -Try the [AWS Code Sample Catalog](https://docs.aws.amazon.com/code-samples/latest/catalog/welcome.html) to start coding with the AWS SDK. - -# Features - -- [AWS Explorer](#ui-components-aws-expl) - - API Gateway - - App Runner - - CloudFormation stacks - - [CloudWatch Logs](https://docs.aws.amazon.com/cloud9/latest/user-guide/cloudwatch-logs-toolkit.html) - - ECR - - [ECS](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/ecs-exec.html) - - IoT explorer - - Lambda functions - - S3 explorer -- [Developer Tools](#ui-components-dev-tools) - - [CDK Explorer](#ui-components-cdk-expl) - - [CodeWhisperer](#codewhisperer) -- [AWS Serverless Applications (SAM)](#sam-and-lambda) -- [`AWS:` Commands](#aws-commands) - ---- - -## AWS Explorer - -The **AWS Explorer** provides access to the AWS services that you can work with when using the Toolkit. To see the **AWS Explorer**, choose the **AWS** icon in the **Activity bar**. - -## ![Overview, AWS Explorer](./resources/marketplace/cloud9/overview-aws-explorer-en.png) - -## Developer Tools - -The **Developer Tools** panel is a section for developer-focused tooling curated for working in an IDE. The **Developer Tools** panel can be found underneath the **AWS Explorer** when the **AWS icon** is selected in the **Activity bar**. - -## { [Return to Top](#top) } - -## CDK Explorer - -The **AWS CDK Explorer** enables you to work with [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk/) applications. It shows a top-level view of your CDK applications that have been sythesized in your workspace. - -With the CDK explorer, you can navigate the CDK application's infrastructure stacks, resources, and policies. - -For full details see the [AWS CDK Explorer](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/cdk-explorer.html) in the user guide. - -{ [Return to Top](#top) } - -## Amazon CodeWhisperer - -**Amazon CodeWhisperer** provides inline code suggestions using machine learning and natural language processing on the contents of your current file. Supported languages include: Java, Python and Javascript. - -Once enabled, CodeWhisperer will provide code suggestions automatically and can also be requested manually using option+c (mac) / alt+c (PC). To accept a suggestion and add it to your file, press Tab, Enter or click on it. To dismiss a suggestion, press escape or keep typing. - -For more information, see [Amazon CodeWhisperer](https://aws.amazon.com/codewhisperer) in our user guide. - -## { [Return to Top](#top) } - -## AWS Serverless Applications - -The AWS Toolkit enables you to develop [AWS serverless applications](https://aws.amazon.com/serverless/) locally. It also provides _Inline Actions_ in Cloud9 to do the following: - -- Use SAM (serverless application model) templates to build and debug your locally developed AWS serverless applications. -- Run selected [AWS Lambda](https://aws.amazon.com/lambda/) functions. - -To start debugging with a SAM template, click the `Add Debug Configuration` _Inline Action_ in the template file. - -![Add Debug Configuration Template](./resources/marketplace/cloud9/Codelens-YAML-template.png) - -###### The _Inline Action_ indicator in the SAM template allows you to add a debug configuration for the serverless application. - -Alternatively, you can run and debug just the AWS Lambda function and exclude other resources defined by the SAM template. Again, use an _Inline Action_ indicator for an AWS Lambda-function handler. (A _handler_ is a function that Lambda calls to start execution of a Lambda function.) - -![Add Debug Configuration Direct](./resources/marketplace/cloud9/Codelens-direct-function.png) - -###### The _Inline Action_ indicator in the application file lets you add a debug configuration for a selected AWS Lambda function. - -When you run a debug session, the status and results are shown in the **AWS Toolkit** output channel. If the toolkit does not have an open **AWS Toolkit** output channel, one can be created with the New Tab button. - -![Configure and Run](./resources/marketplace/cloud9/sam-configure-and-run-still-en.png) - -###### After a local run is complete, the output appears in the **OUTPUT** tab. - -When you're satisfied with performance, you can [deploy your serverless application](https://docs.aws.amazon.com/cloud9/latest/user-guide/deploy-serverless-app.html). The SAM template is converted to a CloudFormation template, which is then used to deploy all the application's assets to the AWS Cloud. - -### Supported runtimes - -The Toolkit _local SAM debugging_ feature supports these runtimes: - -- JavaScript (Node.js 16.x, Node.js 18.x, Node.js 20.x, Node.js 22.x,) -- Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) - -For more information see [Working with AWS Serverless Applications](https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html) in the user guide. - -{ [Return to Top](#top) } - ---- - -## `AWS:` Commands - -The Toolkit provides commands (prefixed with `AWS:`) to the AWS Cloud9 _Go to Anything panel_, available by clicking the search bar and typing "." or via hotkey. - -| OS | Hotkey | -| :------ | :------- | -| Windows | `CTRL-.` | -| macOS | `CMD-.` | - -![Go to Anything panel](./resources/marketplace/cloud9/open-commands-en.png) - -| AWS Command | Description | -| :---------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `AWS: About Toolkit` | Displays information about the AWS Toolkit. | -| `AWS: Add Local Invoke and Debug Configuration` | Creates an `aws-sam` Debug Configuration from a function in the given source file | -| `AWS: Connect to AWS` | Select or create a connection to AWS. | -| `AWS: Create a new Issue on Github` | Opens the AWS Toolkit's [New Issue page on Github](https://github.com/aws/aws-toolkit-vscode/issues/new/choose). | -| `AWS: Create Credentials Profile` | Creates an AWS credentials profile. | -| `AWS: Create Lambda SAM Application` | Generates code files for a new AWS serverless Lambda application. For more information, see [Creating a Serverless Application](https://docs.aws.amazon.com/cloud9/latest/user-guide/latest/user-guide/create-sam.html) in the user guide. | -| `AWS: Create new CloudFormation Template` | Creates a new starter Cloudformation Template | -| `AWS: Create new SAM Template` | Creates a new starter SAM Template | -| `AWS: Deploy SAM Application` | Deploys a local serverless application to an AWS account. For more information, see [Deploying a Serverless Application](https://docs.aws.amazon.com/cloud9/latest/user-guide/deploy-serverless-app.html) in the user guide. | -| `AWS: Edit Credentials` | Opens the `~/.aws/credentials` or `~/.aws/config` file for editing. | -| `AWS: Local Invoke and Debug Configuration` | Shows a tool that helps you create, edit, run, and debug a SAM _launch config_ (`type:aws-sam`). | -| `AWS: Detect SAM CLI` | Checks whether the Toolkit can communicate correctly with the AWS SAM CLI that is installed. | -| `AWS: Show or Hide Regions` | Adds or removes AWS Regions in the **AWS Explorer**. | -| `AWS: Sign out` | Disconnect the Toolkit from the current AWS connection. | -| `AWS: Submit Quick Feedback...` | Submit a private, one-way message and sentiment to the AWS Toolkit dev team. For larger issues that warrant conversations or bugfixes, please submit an issue in Github with the **AWS: Create a New Issue on Github** command. | -| `AWS: Toggle SAM hints in source files` | Toggles AWS SAM-related Inline Actions in source files | -| `AWS: View Toolkit Logs` | Displays log files that contain general Toolkit diagnostic information. | -| `AWS: View Quick Start` | Open this quick-start guide. | -| `AWS: View Toolkit Documentation` | Opens the [user guide](https://docs.aws.amazon.com/cloud9/latest/user-guide/toolkit-welcome.html) for the Toolkit. | -| `AWS: View Source on GitHub` | Opens the [GitHub repository](https://github.com/aws/aws-toolkit-vscode) for the Toolkit. | - -{ [Return to Top](#top) } - ---- - -# Get help - -For additional details on how to use the AWS Toolkit, see the [user guide](https://docs.aws.amazon.com/cloud9/latest/user-guide/toolkit-welcome.html). - -To report issues with the Toolkit or to propose Toolkit code changes, see the [aws/aws-toolkit-vscode](https://github.com/aws/aws-toolkit-vscode) repository on GitHub. - -You can also [contact AWS](https://aws.amazon.com/contact-us/) directly. - -{ [Return to Top](#top) } diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index e7b3333314e..b02ab7e28d0 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.42.0-SNAPSHOT", + "version": "3.44.0-SNAPSHOT", "extensionKind": [ "workspace" ], @@ -732,7 +732,7 @@ { "id": "aws.toolkit.notifications", "name": "%AWS.notifications.title%", - "when": "!isCloud9 && !aws.isSageMaker && aws.toolkit.notifications.show" + "when": "!(isCloud9 || aws.isSageMaker) && aws.toolkit.notifications.show" }, { "id": "aws.amazonq.codewhisperer", @@ -2231,7 +2231,7 @@ "command": "_aws.toolkit.notifications.dismiss", "title": "%AWS.generic.dismiss%", "category": "%AWS.title%", - "enablement": "isCloud9 || !aws.isWebExtHost", + "enablement": "view == aws.toolkit.notifications", "icon": "$(remove-close)" }, { diff --git a/scripts/generateIcons.ts b/scripts/generateIcons.ts index d920cb67f9f..e3f2d42da42 100644 --- a/scripts/generateIcons.ts +++ b/scripts/generateIcons.ts @@ -18,7 +18,7 @@ const iconSources = [ // Paths relative to packages/toolkit `resources/icons/**/*.svg`, `../../node_modules/@vscode/codicons/src/icons/**/*.svg`, - '!**/{cloud9,dark,light}/**', + '!**/{dark,light}/**', ] interface PackageIcon { @@ -73,27 +73,6 @@ async function updatePackage(fontPath: string, icons: [id: string, icon: Package console.log('Updated package.json') } -const themes = { - dark: '#C5C5C5', - light: '#424242', -} - -async function generateCloud9Icons(targets: { name: string; path: string }[], destination: string): Promise { - console.log('Generating icons for Cloud9') - - function replaceColor(file: string, color: string, dst: string): void { - const contents = nodefs.readFileSync(file, 'utf-8') - const replaced = contents.replace(/currentColor/g, color) - nodefs.writeFileSync(dst, replaced) - } - - for (const [theme, color] of Object.entries(themes)) { - const themeDest = path.join(destination, theme) - nodefs.mkdirSync(themeDest, { recursive: true }) - await Promise.all(targets.map((t) => replaceColor(t.path, color, path.join(themeDest, `${t.name}.svg`)))) - } -} - async function generate(mappings: Record = {}) { const dest = path.join(fontsDir, `${fontId}.woff`) const relativeDest = path.relative(projectDir, dest) @@ -166,7 +145,6 @@ ${result.template} `.trim() const stylesheetPath = path.join(stylesheetsDir, 'icons.css') - const cloud9Dest = path.join(iconsDir, 'cloud9', 'generated') const isValidIcon = (i: (typeof icons)[number]): i is Required => i.data !== undefined nodefs.mkdirSync(fontsDir, { recursive: true }) @@ -178,11 +156,9 @@ ${result.template} `./${relativeDest}`, icons.filter(isValidIcon).map((i) => [i.name, i.data]) ) - await generateCloud9Icons(icons, cloud9Dest) generated.addEntry(dest) generated.addEntry(stylesheetPath) - generated.addEntry(cloud9Dest) generated.emit(path.join(projectDir, 'dist')) } diff --git a/scripts/generateNonCodeFiles.ts b/scripts/generateNonCodeFiles.ts index 059ded8cbde..c165e9e6b36 100644 --- a/scripts/generateNonCodeFiles.ts +++ b/scripts/generateNonCodeFiles.ts @@ -63,8 +63,6 @@ function generateFileHash(root: string) { try { translateReadmeToHtml(projectRoot, 'README.md', 'quickStartVscode.html', true) - translateReadmeToHtml(projectRoot, 'README.quickstart.cloud9.md', 'quickStartCloud9.html', false) - translateReadmeToHtml(projectRoot, 'README.quickstart.cloud9.md', 'quickStartCloud9-cn.html', false, true) generateFileHash(projectRoot) } catch (error) { console.error(error)