diff --git a/extensions/positron-python/.eslintignore b/extensions/positron-python/.eslintignore index 7ba146c7d8e4..083b9d650d0c 100644 --- a/extensions/positron-python/.eslintignore +++ b/extensions/positron-python/.eslintignore @@ -108,17 +108,6 @@ src/test/common/interpreterPathService.unit.test.ts src/test/pythonFiles/formatting/dummy.ts -src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts -src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts -src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts -src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts -src/test/debugger/extension/configuration/resolvers/base.unit.test.ts -src/test/debugger/extension/configuration/resolvers/common.ts -src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts -src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts -src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts -src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts -src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts src/test/debugger/extension/banner.unit.test.ts src/test/debugger/extension/adapter/adapter.test.ts src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -183,7 +172,6 @@ src/client/formatters/baseFormatter.ts src/client/testing/serviceRegistry.ts src/client/testing/main.ts src/client/testing/configurationFactory.ts -src/client/testing/common/debugLauncher.ts src/client/testing/common/constants.ts src/client/testing/common/testUtils.ts src/client/testing/common/socketServer.ts @@ -272,38 +260,18 @@ src/client/common/process/pythonProcess.ts src/client/common/process/pythonEnvironment.ts src/client/common/process/decoder.ts -src/client/debugger/extension/configuration/providers/moduleLaunch.ts -src/client/debugger/extension/configuration/providers/fastapiLaunch.ts -src/client/debugger/extension/configuration/providers/flaskLaunch.ts -src/client/debugger/extension/configuration/providers/fileLaunch.ts -src/client/debugger/extension/configuration/providers/remoteAttach.ts -src/client/debugger/extension/configuration/providers/djangoLaunch.ts -src/client/debugger/extension/configuration/providers/providerFactory.ts -src/client/debugger/extension/configuration/providers/pyramidLaunch.ts -src/client/debugger/extension/configuration/providers/pidAttach.ts -src/client/debugger/extension/configuration/resolvers/base.ts -src/client/debugger/extension/configuration/resolvers/helper.ts -src/client/debugger/extension/configuration/resolvers/launch.ts -src/client/debugger/extension/configuration/resolvers/attach.ts -src/client/debugger/extension/configuration/debugConfigurationService.ts -src/client/debugger/extension/configuration/launch.json/updaterService.ts -src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts -src/client/debugger/extension/configuration/launch.json/completionProvider.ts -src/client/debugger/extension/banner.ts -src/client/debugger/extension/serviceRegistry.ts + src/client/debugger/extension/adapter/remoteLaunchers.ts src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts src/client/debugger/extension/adapter/factory.ts src/client/debugger/extension/adapter/activator.ts src/client/debugger/extension/adapter/logging.ts -src/client/debugger/extension/types.ts src/client/debugger/extension/hooks/eventHandlerDispatcher.ts src/client/debugger/extension/hooks/childProcessAttachService.ts src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts src/client/debugger/extension/attachQuickPick/factory.ts src/client/debugger/extension/attachQuickPick/psProcessParser.ts src/client/debugger/extension/attachQuickPick/picker.ts -src/client/debugger/extension/helpers/protocolParser.ts src/client/application/serviceRegistry.ts src/client/application/diagnostics/surceMapSupportService.ts diff --git a/extensions/positron-python/.github/ISSUE_TEMPLATE/config.yml b/extensions/positron-python/.github/ISSUE_TEMPLATE/config.yml index 82af056110ab..eaacc33b8d8d 100644 --- a/extensions/positron-python/.github/ISSUE_TEMPLATE/config.yml +++ b/extensions/positron-python/.github/ISSUE_TEMPLATE/config.yml @@ -15,6 +15,3 @@ contact_links: - name: Help/Support url: https://github.com/microsoft/vscode-python/discussions/categories/q-a about: 'Having trouble with the extension? Need help getting something to work?' - - name: 'Chat' - url: https://aka.ms/python-discord - about: 'You can ask for help or chat in the `#vscode` channel of our microsoft-python Discord server' diff --git a/extensions/positron-python/.github/release_plan.md b/extensions/positron-python/.github/release_plan.md index 095c6f8aed93..e02d7ee45abf 100644 --- a/extensions/positron-python/.github/release_plan.md +++ b/extensions/positron-python/.github/release_plan.md @@ -1,66 +1,85 @@ All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. -# Feature freeze (Monday @ 17:00 America/Vancouver, XXX XX) +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. + + +NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. -- [ ] Announce the feature freeze on both Teams and e-mail, leave enough time for teams to surface any last minute issues that need to get in before freeze. Make sure debugger and Language Server teams are looped in as well. # Release candidate (Monday, XXX XX) -NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. - -- [ ] Update `main` for the release - - [ ] Change the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) to the next **even** number and switch the `-dev` to `-rc` (🤖) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) is up-to-date, you should now see changes to the`package.json` and `package-lock.json` (🤖) - - [ ] Check [`pypi.org`](https://pypi.org/search/?q=debugpy) and update the version of `debugpy` in `install_debugpy.py` if necessary. - - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Repository.txt) as appropriate. This file is manually edited so you can check with the teams if anything needs to be added here. - - [ ] Get approval on PR then merge pull request into `main` -- [ ] Create the [`release` branch](https://github.com/microsoft/vscode-python/branches) - - [ ] If there are `release` branches that are two versions old you can delete them at this time - - [ ] Click `draft new release` then create a tag for this release matching the `release/YYYY.XX` format - - [ ] Click `generate release notes` - - [ ] Create a new `release/YYYY.XX` branch from `main` -- [ ] Create a draft [GitHub release](https://github.com/microsoft/vscode-python/releases) for the release notes (🤖) -- [ ] Update `main` post-release (🤖) - - [ ] Bump the minor version number to the next ("YYYY.[minor+1]") release in the `main` branch to an **odd** number and switch the `-rc` to `-dev`(🤖) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) is up-to-date, you should now see changes to the`package.json` and `package-lock.json` (🤖) - - [ ] Create a pull request against `main` - - [ ] Get approval on PR then merge pull request into `main` -- [ ] Announce the code freeze is over on the same channels, not required if this occurs on normal release cadence -- [ ] Update Component Governance (Notes are in the team OneNote under Python VS Code → Dev Process → Component Governance). - - [ ] Check pipeline on Azure DevOps under [`monacotools/Monaco/Compliance/Component Governance`](https://dev.azure.com/monacotools/Monaco/_componentGovernance/192726?_a=alerts&typeId=11825783&alerts-view-option=active) - - [ ] Make sure there are no active alerts - - [ ] Manually add any repository/embedded/CG-incompatible dependencies -- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) -- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post. Contact the PM team for this. +NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. + +### Step 1: +##### Bump the version of `main` to be a release candidate (also updating debugpy dependences, third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen 🥶) + +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] Create a new branch called **`bump-release-[YYYY.minor]`**. +- [ ] Change the version in `package.json` to the next **even** number and switch the `-dev` to `-rc`. (🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (🤖) +- [ ] Check [debugpy on PyPI](https://pypi.org/project/debugpy/) for a new release and update the version of debugpy in [`install_debugpy.py`](https://github.com/microsoft/vscode-python/blob/main/pythonFiles/install_debugpy.py) if necessary. +- [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. +- [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. + +NOTE: this PR will fail the test in our internal release pipeline called `VS Code (pre-release)` because the version specified in `main` is (temporarily) an invalid pre-release version. This is expected as this will be resolved below. + + +### Step 2: Creating your release branch ❄️ +- [ ] Create a release branch by creating a new branch called **`release/YYYY.minor`** branch from `main`. This branch is now the candidate for our release which will be the base from which we will release. + +NOTE: If there are release branches that are two versions old you can delete them at this time. + +### Step 3 Create a draft GitHub release for the release notes (🤖) ❄️ + +- [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). +- [ ] Specify a new tag called `YYYY.minor.0`. +- [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. +- [ ] Create the release notes by specifying the previous tag for the last stable release and click `Generate release notes`. Quickly check that it only contain notes from what is new in this release. +- [ ] Click `Save draft`. + +### Step 4: Return `main` to dev and unfreeze (❄️ ➡ 💧) +NOTE: The purpose of this step is ensuring that main always is on a dev version number for every night's 🌃 pre-release. Therefore it is imperative that you do this directly after the previous steps to reset the version in main to a dev version **before** a pre-release goes out. +- [ ] Create a branch called **`bump-dev-version-YYYY.[minor+1]`**. +- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and switch the `-rc` to `-dev`.(🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] Create a PR from this branch against `main` and merge it. + +NOTE: this PR should make all CI relating to `main` be passing again (such as the failures stemming from step 1). + +### Step 5: Notifications and Checks on External Release Factors +- [ ] Check [Component Governance](https://dev.azure.com/monacotools/Monaco/_componentGovernance/192726?_a=alerts&typeId=11825783&alerts-view-option=active) to make sure there are no active alerts. +- [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). +- [ ] Contact the PM team to begin drafting a blog post. + # Release (Wednesday, XXX XX) -## Preparation - -- [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready -- [ ] Final updates to the `release-YYYY.minor` branch - - [ ] Create a branch against `release-YYYY.minor` for a pull request - - [ ] Update the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) to remove the `-rc` (🤖) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) is up-to-date (the only update should be the version number if `package-lock.json` has been kept up-to-date) (🤖) - - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Repository.txt) manually if necessary - - [ ] Create pull request against `release/YYYY.minor` (🤖) - - [ ] Merge pull request into `release/YYYY.minor` - -## Release - -- [ ] Make sure [CI](https://github.com/microsoft/vscode-python/actions?query=workflow:%22Build%22) is passing for Release branch (🤖). -- [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the `release/yyyy.minor` branch. -- [ ] Check to ensure VS Code release has gone out before moving onto publishing the python extension - - [ ] Press the approve button if everything looks good to publish to market place. -- [ ] Create a [GitHub release](https://github.com/microsoft/vscode-python/releases) (🤖) - - [ ] Update the release notes - - [ ] Take the release out of draft -- [ ] Publish [documentation changes](https://github.com/Microsoft/vscode-docs/pulls?q=is%3Apr+is%3Aopen+label%3Apython) -- [ ] Publish the [blog](http://aka.ms/pythonblog) post -- [ ] Determine if a hotfix is needed -- [ ] Merge the release branch back into `main`. Don't overwrite the main branch version. (🤖) +### Step 6: Take the release branch from a candidate to the finalized release +- [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready. +- [ ] Check to make sure any final updates to the **`release/YYYY.minor`** branch have been merged. +- [ ] Create a branch against **`release/YYYY.minor`** called **`finalized-release-[YYYY.minor]`**. +- [ ] Update the version in `package.json` to remove the `-rc` (🤖) from the version. +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(the only update should be the version number if `package-lock.json` has been kept up-to-date)_. (🤖) +- [ ] Update `ThirdPartyNotices-Repository.txt` manually if necessary. +- [ ] Create a PR from **`finalized-release-[YYYY.minor]`** against `release/YYYY.minor` and merge it. + + +### Step 7: Execute the Release +- [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). +- [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - NOTE: Please opt to release the python extension close to when VS Code is released to align when release notes go out. When we bump the VS Code engine number, our extension will not go out to stable until the VS Code stable release but this only occurs when we bump the engine number. +- [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299). +- [ ] Click "approve" in the publish step of [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) to publish the release to the marketplace. 🎉 +- [ ] Take the Github release out of draft. +- [ ] Publish documentation changes. +- [ ] Contact the PM team to publish the blog post. +- [ ] Determine if a hotfix is needed. +- [ ] Merge the release branch **`release/YYYY.minor`** back into `main`. (This step is only required if changes were merged into the release branch. If the only change made on the release branch is the version, this is not necessary. Overall you need to ensure you DO NOT overwrite the version on the `main` branch.) ## Prep for the _next_ release -- [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md) (🤖) +- [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md). (🤖) - [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release%20plan) (🤖) diff --git a/extensions/positron-python/.github/workflows/build.yml b/extensions/positron-python/.github/workflows/build.yml index b0118c430bcf..f26f13ce50f4 100644 --- a/extensions/positron-python/.github/workflows/build.yml +++ b/extensions/positron-python/.github/workflows/build.yml @@ -18,7 +18,6 @@ env: # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). # Also enables a reporter which exits the process running the tests if it haven't already. MOCHA_REPORTER_JUNIT: true - DISABLE_TRANSLATIONS: true jobs: setup: diff --git a/extensions/positron-python/.github/workflows/lock-issues.yml b/extensions/positron-python/.github/workflows/lock-issues.yml index acc64b3f34b2..6417d415fcfe 100644 --- a/extensions/positron-python/.github/workflows/lock-issues.yml +++ b/extensions/positron-python/.github/workflows/lock-issues.yml @@ -15,7 +15,7 @@ jobs: lock-issues: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/extensions/positron-python/.github/workflows/pr-chat.yml b/extensions/positron-python/.github/workflows/pr-chat.yml deleted file mode 100644 index 586ae9c6ff2f..000000000000 --- a/extensions/positron-python/.github/workflows/pr-chat.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: PR Chat -on: - pull_request_target: - types: [opened, ready_for_review, closed] - -jobs: - main: - runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - steps: - - name: Checkout Actions - uses: actions/checkout@v3 - with: - repository: 'microsoft/vscode-github-triage-actions' - ref: stable - path: ./actions - - name: Install Actions - run: npm install --production --prefix ./actions - - name: Run Code Review Chat - uses: ./actions/code-review-chat - with: - token: ${{secrets.GITHUB_TOKEN}} - slack_token: ${{ secrets.SLACK_TOKEN }} - slack_bot_name: 'VSCodeBot' - notification_channel: codereview diff --git a/extensions/positron-python/.github/workflows/pr-check.yml b/extensions/positron-python/.github/workflows/pr-check.yml index e654cdc36f97..ab615f33b888 100644 --- a/extensions/positron-python/.github/workflows/pr-check.yml +++ b/extensions/positron-python/.github/workflows/pr-check.yml @@ -14,7 +14,6 @@ env: ARTIFACT_NAME_VSIX: ms-python-insiders-vsix VSIX_NAME: ms-python-insiders.vsix TEST_RESULTS_DIRECTORY: . - DISABLE_TRANSLATIONS: true # Force a path with spaces and to test extension works in these scenarios # Unicode characters are causing 2.7 failures so skip that for now. special-working-directory: './path with spaces' diff --git a/extensions/positron-python/.github/workflows/pr-labels.yml b/extensions/positron-python/.github/workflows/pr-labels.yml index 098492e1d042..7563d4d44dcc 100644 --- a/extensions/positron-python/.github/workflows/pr-labels.yml +++ b/extensions/positron-python/.github/workflows/pr-labels.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'PR impact specified' - uses: mheap/github-action-required-labels@v2 + uses: mheap/github-action-required-labels@v3 with: mode: exactly count: 1 diff --git a/extensions/positron-python/.github/workflows/telemetry.yml b/extensions/positron-python/.github/workflows/telemetry.yml deleted file mode 100644 index 95a014790d75..000000000000 --- a/extensions/positron-python/.github/workflows/telemetry.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: 'Telemetry' -on: - pull_request: - paths: - - 'src/client/telemetry/*.ts' - push: - branches-ignore: - - 'main' - - 'release*' - paths: - - 'src/client/telemetry/*.ts' - -jobs: - check-metdata: - name: 'Check metadata' - runs-on: 'ubuntu-latest' - - steps: - - uses: 'actions/checkout@v3' - - - uses: 'actions/setup-node@v3' - with: - node-version: 'lts/*' - - - name: 'Run vscode-telemetry-extractor' - run: 'npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s src/client/telemetry/' diff --git a/extensions/positron-python/.gitignore b/extensions/positron-python/.gitignore index 32831f712472..3d0c1e914126 100644 --- a/extensions/positron-python/.gitignore +++ b/extensions/positron-python/.gitignore @@ -46,4 +46,5 @@ dist/** *.xlf *.nls.*.json *.i18n.json +l10n/ tags diff --git a/extensions/positron-python/build/azure-pipeline.pre-release.yml b/extensions/positron-python/build/azure-pipeline.pre-release.yml index a609b8c58ead..a5b95c91af9a 100644 --- a/extensions/positron-python/build/azure-pipeline.pre-release.yml +++ b/extensions/positron-python/build/azure-pipeline.pre-release.yml @@ -21,8 +21,7 @@ resources: extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: - locTsConfigs: $(Build.SourcesDirectory)/tsconfig.json - locBundleDestination: $(Build.SourcesDirectory)/out/client + l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 inputs: diff --git a/extensions/positron-python/build/azure-pipeline.stable.yml b/extensions/positron-python/build/azure-pipeline.stable.yml index df249bb89c10..76e1e0061ebf 100644 --- a/extensions/positron-python/build/azure-pipeline.stable.yml +++ b/extensions/positron-python/build/azure-pipeline.stable.yml @@ -24,10 +24,7 @@ extends: template: azure-pipelines/extension/stable.yml@templates parameters: publishExtension: ${{ parameters.publishExtension }} - - locTsConfigs: $(Build.SourcesDirectory)/tsconfig.json - locBundleDestination: $(Build.SourcesDirectory)/out/client - + l10nSourcePaths: ./src/client buildSteps: - task: NodeTool@0 inputs: diff --git a/extensions/positron-python/build/webpack/common.js b/extensions/positron-python/build/webpack/common.js index 1b9758559e9f..81ba8ec04fba 100644 --- a/extensions/positron-python/build/webpack/common.js +++ b/extensions/positron-python/build/webpack/common.js @@ -58,20 +58,3 @@ function getListOfExistingModulesInOutDir() { return files.map((filePath) => `./${filePath.slice(0, -3)}`); } exports.getListOfExistingModulesInOutDir = getListOfExistingModulesInOutDir; -function getTranlationsLoader() { - const loaders = []; - if (process.env.DISABLE_TRANSLATIONS !== 'true') { - loaders.push({ - loader: 'vscode-nls-dev/lib/webpack-loader', - options: { - base: constants.ExtensionRootDir, - }, - }); - } - // --- Start Positron --- - // Do not use the vscode-nls-dev loader for Positron; it is incompatible with - // the way localization works in built-in extensions. - return []; - // --- End Positron --- -} -exports.getTranlationsLoader = getTranlationsLoader; diff --git a/extensions/positron-python/build/webpack/webpack.extension.config.js b/extensions/positron-python/build/webpack/webpack.extension.config.js index 88587806c4a6..b1b3922126d6 100644 --- a/extensions/positron-python/build/webpack/webpack.extension.config.js +++ b/extensions/positron-python/build/webpack/webpack.extension.config.js @@ -26,7 +26,6 @@ const config = { }, module: { rules: [ - ...common.getTranlationsLoader(), { test: /\.ts$/, use: [ diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 8e541b5b1cb8..63432ececb85 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -40,7 +40,7 @@ "theme": "dark" }, "engines": { - "vscode": "^1.68.0" + "vscode": "^1.75.0-20230123" }, "keywords": [ "python", @@ -64,30 +64,7 @@ "onLanguage:python", "onDebugDynamicConfigurations:python", "onDebugResolve:python", - "onCommand:python.execInTerminal", - "onCommand:python.debugInTerminal", - "onCommand:python.sortImports", - "onCommand:python.setInterpreter", - "onCommand:python.setShebangInterpreter", - "onCommand:python.viewLanguageServerOutput", - "onCommand:python.viewOutput", - "onCommand:python.execSelectionInTerminal", - "onCommand:python.execSelectionInDjangoShell", - "onCommand:python.startREPL", - "onCommand:python.goToPythonObject", - "onCommand:python.reportIssue", - "onCommand:python.setLinter", - "onCommand:python.enableLinting", - "onCommand:python.createTerminal", - "onCommand:python.configureTests", - "onCommand:python.clearWorkspaceInterpreter", - "onCommand:python.enableSourceMapSupport", - "onCommand:python.launchTensorBoard", - "onCommand:python.clearCacheAndReload", - "onCommand:python.createEnvironment", - "onCommand:python.createNewFile", "onWalkthrough:pythonWelcome", - "onWalkthrough:pythonWelcomeWithEnv", "onWalkthrough:pythonWelcomeWithDS", "onWalkthrough:pythonDataScienceWelcome", "workspaceContains:mspythonconfig.json", @@ -100,6 +77,7 @@ ], "main": "./out/client/extension", "browser": "./dist/extension.browser.js", + "l10n": "./l10n", "contributes": { "walkthroughs": [ { @@ -155,74 +133,7 @@ "svg": "resources/walkthrough/python-interpreter.svg", "altText": "Selecting a python interpreter from the status bar" }, - "when": "" - }, - { - "id": "python.runAndDebug", - "title": "Run and debug your Python file", - "description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", - "media": { - "svg": "resources/walkthrough/rundebug2.svg", - "altText": "How to run and debug in VS Code with F5 or the play button on the top right." - }, - "when": "" - }, - { - "id": "python.learnMoreWithDS", - "title": "Explore more resources", - "description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", - "media": { - "altText": "Image representing our documentation page and mailing list resources.", - "svg": "resources/walkthrough/learnmore.svg" - }, - "when": "" - } - ] - }, - { - "id": "pythonWelcomeWithEnv", - "title": "Get started with Python development", - "description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", - "when": "false", - "steps": [ - { - "id": "python.createPythonFile2", - "title": "Create a Python file", - "description": "[Open](command:toSide:workbench.action.files.openFile) or [create](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D) a Python file - make sure to save it as \".py\".\n[Create Python File](command:toSide:workbench.action.files.newUntitledFile?%7B%22languageId%22%3A%22python%22%7D)", - "media": { - "svg": "resources/walkthrough/open-folder.svg", - "altText": "Open a Python file or a folder with a Python project." - }, - "when": "" - }, - { - "id": "python.installPythonWin82", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python [from python.org](https://www.python.org/downloads).\n\n[Install Python](https://www.python.org/downloads)\n", - "media": { - "markdown": "resources/walkthrough/install-python-windows-8.md" - }, - "when": "workspacePlatform == windows && showInstallPythonTile" - }, - { - "id": "python.installPythonMac2", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via Brew](command:python.installPythonOnMac)\n", - "media": { - "markdown": "resources/walkthrough/install-python-macos.md" - }, - "when": "workspacePlatform == mac && showInstallPythonTile", - "command": "workbench.action.terminal.new" - }, - { - "id": "python.installPythonLinux2", - "title": "Install Python", - "description": "The Python Extension requires Python to be installed. Install Python 3 through the terminal.\n[Install Python via terminal](command:python.installPythonOnLinux)\n", - "media": { - "markdown": "resources/walkthrough/install-python-linux.md" - }, - "when": "workspacePlatform == linux && showInstallPythonTile", - "command": "workbench.action.terminal.new" + "when": "workspaceFolderCount == 0" }, { "id": "python.createEnvironment", @@ -235,17 +146,7 @@ "when": "workspaceFolderCount > 0" }, { - "id": "python.selectInterpreter2", - "title": "Select a Python Interpreter", - "description": "Choose which Python interpreter/environment you want to use for your Python project.\n[Select Python Interpreter](command:python.setInterpreter)\n**Tip**: Run the ``Python: Select Interpreter`` command in the [Command Palette](command:workbench.action.showCommands).", - "media": { - "svg": "resources/walkthrough/python-interpreter.svg", - "altText": "Selecting a python interpreter from the status bar" - }, - "when": "workspaceFolderCount == 0" - }, - { - "id": "python.runAndDebug2", + "id": "python.runAndDebug", "title": "Run and debug your Python file", "description": "Open your Python file and click on the play button on the top right of the editor, or press F5 when on the file and select \"Python File\" to run with the debugger. \n \n[Learn more](https://code.visualstudio.com/docs/python/python-tutorial#_run-hello-world)", "media": { @@ -255,7 +156,7 @@ "when": "" }, { - "id": "python.learnMoreWithDS2", + "id": "python.learnMoreWithDS", "title": "Explore more resources", "description": "🎨 Explore all the features the Python extension has to offer by looking for \"Python\" in the [Command Palette](command:workbench.action.showCommands). \n 📈 Learn more about getting started with [data science](command:workbench.action.openWalkthrough?%7B%22category%22%3A%22ms-python.python%23pythonDataScienceWelcome%22%2C%22step%22%3A%22ms-python.python%23python.createNewNotebook%22%7D) in Python. \n ✨ Take a look at our [Release Notes](https://aka.ms/AA8dxtb) to learn more about the latest features. \n \n[Learn More](https://aka.ms/AA8dqti)", "media": { @@ -486,7 +387,8 @@ "default": [], "description": "%python.autoComplete.extraPaths.description%", "scope": "resource", - "type": "array" + "type": "array", + "uniqueItems": true }, "python.condaPath": { "default": "", @@ -529,7 +431,8 @@ ] }, "scope": "machine", - "type": "array" + "type": "array", + "uniqueItems": true }, "python.experiments.optOutFrom": { "default": [], @@ -542,7 +445,8 @@ ] }, "scope": "machine", - "type": "array" + "type": "array", + "uniqueItems": true }, "python.formatting.autopep8Args": { "default": [], @@ -741,7 +645,8 @@ "type": "string" }, "scope": "resource", - "type": "array" + "type": "array", + "uniqueItems": true }, "python.linting.lintOnSave": { "default": true, @@ -1039,7 +944,7 @@ "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", - "scope": "application", + "scope": "resource", "type": "string" }, "python.terminal.activateEnvInCurrentTerminal": { @@ -1145,7 +1050,8 @@ "type": "string" }, "scope": "machine", - "type": "array" + "type": "array", + "uniqueItems": true }, "python.venvPath": { "default": "", @@ -1621,7 +1527,7 @@ "category": "Python", "command": "python.analysis.restartLanguageServer", "title": "%python.command.python.analysis.restartLanguageServer.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1633,7 +1539,7 @@ "category": "Python", "command": "python.clearWorkspaceInterpreter", "title": "%python.command.python.clearWorkspaceInterpreter.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1657,7 +1563,7 @@ "category": "Python", "command": "python.enableLinting", "title": "%python.command.python.enableLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1669,33 +1575,33 @@ "category": "Python", "command": "python.execInTerminal", "title": "%python.command.python.execInTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", "command": "python.execInTerminal-icon", "icon": "$(play)", "title": "%python.command.python.execInTerminalIcon.title%", - "when": "false" + "when": "false && editorLangId == python" }, { "category": "Python", "command": "python.debugInTerminal", "icon": "$(debug-alt)", "title": "%python.command.python.debugInTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", "command": "python.execSelectionInDjangoShell", "title": "%python.command.python.execSelectionInDjangoShell.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", "command": "python.execSelectionInTerminal", "title": "%python.command.python.execSelectionInTerminal.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1715,7 +1621,7 @@ "category": "Python", "command": "python.reportIssue", "title": "%python.command.python.reportIssue.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Test", @@ -1728,7 +1634,7 @@ "category": "Python", "command": "python.runLinting", "title": "%python.command.python.runLinting.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1740,13 +1646,13 @@ "category": "Python", "command": "python.setLinter", "title": "%python.command.python.setLinter.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python Refactor", "command": "python.sortImports", "title": "%python.command.python.sortImports.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, { "category": "Python", @@ -1893,8 +1799,9 @@ "webpack": "webpack" }, "dependencies": { + "@iarna/toml": "^2.2.5", + "@vscode/extension-telemetry": "^0.7.4-preview", "@vscode/jupyter-lsp-middleware": "^0.2.50", - "@vscode/extension-telemetry": "^0.6.2", "arch": "^2.1.0", "diff-match-patch": "^1.0.0", "fs-extra": "^10.0.1", @@ -1914,8 +1821,8 @@ "request-progress": "^3.0.0", "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", - "stack-trace": "0.0.10", "semver": "^5.5.0", + "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", "uint64be": "^3.0.0", @@ -1927,11 +1834,10 @@ "vscode-languageclient": "8.0.2-next.5", "vscode-languageserver": "8.0.2-next.5", "vscode-languageserver-protocol": "3.17.2-next.6", - "vscode-nls": "^5.0.1", "vscode-tas-client": "^0.1.63", + "which": "^2.0.2", "winreg": "^1.2.4", - "xml2js": "^0.4.19", - "which": "^2.0.2" + "xml2js": "^0.4.19" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", @@ -1959,7 +1865,6 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", - "@vscode/telemetry-extractor": ">=1.9.8", "@vscode/test-electron": "^2.1.3", "chai": "^4.1.2", "chai-arrays": "^2.0.0", @@ -1972,7 +1877,7 @@ "eslint": "^7.2.0", "eslint-config-airbnb": "^18.2.0", "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.22.0", + "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.0", @@ -2004,7 +1909,6 @@ "uuid": "^8.3.2", "vsce": "^2.6.6", "vscode-debugadapter-testsupport": "^1.27.0", - "vscode-nls-dev": "^4.0.0", "webpack": "^5.70.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index 65d610149125..0c2621b480f6 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -33,8 +33,8 @@ "python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.", "python.envFile.description": "Absolute path to a file containing environment variable definitions.", "python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.", - "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", - "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", + "python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/AB-Experiments for more details.", + "python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/AB-Experiments for more details.", "python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.formatting.autopep8Path.description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", "python.formatting.blackArgs.description": "Arguments passed in. Each argument is a separate item in the array.", @@ -102,7 +102,7 @@ "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in Terminal created using the Extension.", "python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", - "python.terminal.focusAfterLaunch.description": "When launching a python process, whether to focus on the terminal.", + "python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.", "python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.", "python.testing.autoTestDiscoverOnSaveEnabled.description": "Enable auto run test discovery when saving a test file.", "python.testing.cwd.description": "Optional working directory for tests.", diff --git a/extensions/positron-python/pythonFiles/create_venv.py b/extensions/positron-python/pythonFiles/create_venv.py index 1f31abc5cc87..24d4baa357c9 100644 --- a/extensions/positron-python/pythonFiles/create_venv.py +++ b/extensions/positron-python/pythonFiles/create_venv.py @@ -7,7 +7,7 @@ import pathlib import subprocess import sys -from typing import Optional, Sequence, Union +from typing import List, Optional, Sequence, Union VENV_NAME = ".venv" CWD = pathlib.PurePath(os.getcwd()) @@ -19,12 +19,27 @@ class VenvError(Exception): def parse_args(argv: Sequence[str]) -> argparse.Namespace: parser = argparse.ArgumentParser() + parser.add_argument( - "--install", - action="store_true", - default=False, - help="Install packages into the virtual environment.", + "--requirements", + action="append", + default=[], + help="Install additional dependencies into the virtual environment.", + ) + + parser.add_argument( + "--toml", + action="store", + default=None, + help="Install additional dependencies from sources like `pyproject.toml` into the virtual environment.", ) + parser.add_argument( + "--extras", + action="append", + default=[], + help="Install specific package groups from `pyproject.toml` into the virtual environment.", + ) + parser.add_argument( "--git-ignore", action="store_true", @@ -71,30 +86,36 @@ def get_venv_path(name: str) -> str: return os.fspath(CWD / name / "bin" / "python") -def install_packages(venv_path: str) -> None: - requirements = os.fspath(CWD / "requirements.txt") - pyproject = os.fspath(CWD / "pyproject.toml") +def install_requirements(venv_path: str, requirements: List[str]) -> None: + if not requirements: + return + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + args = [] + for requirement in requirements: + args += ["-r", requirement] + run_process( + [venv_path, "-m", "pip", "install"] + args, + "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", + ) + print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") + + +def install_toml(venv_path: str, extras: List[str]) -> None: + args = "." if len(extras) == 0 else f".[{','.join(extras)}]" + run_process( + [venv_path, "-m", "pip", "install", "-e", args], + "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", + ) + print("CREATE_VENV.PIP_INSTALLED_PYPROJECT") + + +def upgrade_pip(venv_path: str) -> None: run_process( [venv_path, "-m", "pip", "install", "--upgrade", "pip"], "CREATE_VENV.PIP_UPGRADE_FAILED", ) - if file_exists(requirements): - print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") - run_process( - [venv_path, "-m", "pip", "install", "-r", requirements], - "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", - ) - print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") - elif file_exists(pyproject): - print(f"VENV_INSTALLING_PYPROJECT: {pyproject}") - run_process( - [venv_path, "-m", "pip", "install", "-e", ".[extras]"], - "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", - ) - print("CREATE_VENV.PIP_INSTALLED_PYPROJECT") - def add_gitignore(name: str) -> None: git_ignore = CWD / name / ".gitignore" @@ -112,7 +133,9 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if not is_installed("venv"): raise VenvError("CREATE_VENV.VENV_NOT_FOUND") - if args.install and not is_installed("pip"): + pip_installed = is_installed("pip") + deps_needed = args.requirements or args.extras or args.toml + if deps_needed and not pip_installed: raise VenvError("CREATE_VENV.PIP_NOT_FOUND") if venv_exists(args.name): @@ -128,8 +151,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if args.git_ignore: add_gitignore(args.name) - if args.install: - install_packages(venv_path) + if pip_installed: + upgrade_pip(venv_path) + + if args.requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {args.requirements}") + install_requirements(venv_path, args.requirements) + + if args.toml: + print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") + install_toml(venv_path, args.extras) if __name__ == "__main__": diff --git a/extensions/positron-python/pythonFiles/install_debugpy.py b/extensions/positron-python/pythonFiles/install_debugpy.py index 2a8594b4b52e..12a9e47cc396 100644 --- a/extensions/positron-python/pythonFiles/install_debugpy.py +++ b/extensions/positron-python/pythonFiles/install_debugpy.py @@ -12,12 +12,12 @@ EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") DEBUGGER_PACKAGE = "debugpy" -DEBUGGER_PYTHON_ABI_VERSIONS = ("cp39",) -DEBUGGER_VERSION = "1.6.3" # can also be "latest" +DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",) +DEBUGGER_VERSION = "1.6.5" # can also be "latest" def _contains(s, parts=()): - return any(p for p in parts if p in s) + return any(p in s for p in parts) def _get_package_data(): diff --git a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt index 44a77c8e824c..0ab00132f463 100644 --- a/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt +++ b/extensions/positron-python/pythonFiles/jedilsp_requirements/requirements.txt @@ -16,43 +16,57 @@ jedi==0.18.1 \ --hash=sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d \ --hash=sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab # via jedi-language-server -jedi-language-server==0.36.0 \ - --hash=sha256:30f69eab674ed6b7e35316ea558d2bdd150f1b05ce7a9f65dd67388ef1fa0e35 \ - --hash=sha256:d60ce0927ad4e9c81f4545804b36a5dcd028070160a7225a04acb2ddd4f3d06c +jedi-language-server==0.38.0 \ + --hash=sha256:573f790267cd149fe356d974da5e9be5f219dea9f5659895487122f7fad120c0 \ + --hash=sha256:98f439926da1ce0410d6f52d0bc0ffbefdfe71fbbd2c7d009ef9e162175c2548 # via -r pythonFiles\jedilsp_requirements\requirements.in parso==0.8.3 \ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pydantic==1.8.2 \ - --hash=sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd \ - --hash=sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739 \ - --hash=sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f \ - --hash=sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840 \ - --hash=sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23 \ - --hash=sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287 \ - --hash=sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62 \ - --hash=sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b \ - --hash=sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb \ - --hash=sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820 \ - --hash=sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3 \ - --hash=sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b \ - --hash=sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e \ - --hash=sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3 \ - --hash=sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316 \ - --hash=sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b \ - --hash=sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4 \ - --hash=sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20 \ - --hash=sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e \ - --hash=sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505 \ - --hash=sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1 \ - --hash=sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833 +pydantic==1.10.2 \ + --hash=sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42 \ + --hash=sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624 \ + --hash=sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e \ + --hash=sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559 \ + --hash=sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709 \ + --hash=sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9 \ + --hash=sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d \ + --hash=sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52 \ + --hash=sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda \ + --hash=sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912 \ + --hash=sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c \ + --hash=sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525 \ + --hash=sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe \ + --hash=sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41 \ + --hash=sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b \ + --hash=sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283 \ + --hash=sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965 \ + --hash=sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c \ + --hash=sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410 \ + --hash=sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5 \ + --hash=sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116 \ + --hash=sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98 \ + --hash=sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f \ + --hash=sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644 \ + --hash=sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13 \ + --hash=sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd \ + --hash=sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254 \ + --hash=sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6 \ + --hash=sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488 \ + --hash=sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5 \ + --hash=sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c \ + --hash=sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1 \ + --hash=sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a \ + --hash=sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2 \ + --hash=sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d \ + --hash=sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236 # via # jedi-language-server # pygls -pygls==0.11.3 \ - --hash=sha256:4d86fc854e6d6613cd42bf7511e9c6aac947fc8d62ff973a705570b036d969f2 \ - --hash=sha256:5c925b182f2b0aa38d0ce83a9829ca5aed8eb9c7079cffc5bddff2da1033b58f +pygls==0.12.4 \ + --hash=sha256:1b96378452217a02f19d89d9e647a4256d8d445ab3c641a589b4f73bf11898b6 \ + --hash=sha256:63b859411307ed6f99fb9dd0e71be507a17ae9b3de5c5d07c497f5bddadcc46a # via # -r pythonFiles\jedilsp_requirements\requirements.in # jedi-language-server @@ -60,13 +74,13 @@ typeguard==2.13.3 \ --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 # via pygls -typing-extensions==4.2.0 \ - --hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \ - --hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via # importlib-metadata # pydantic -zipp==3.8.0 \ - --hash=sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad \ - --hash=sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099 +zipp==3.10.0 \ + --hash=sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1 \ + --hash=sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8 # via importlib-metadata diff --git a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py index 51c94527302d..34312dcc1997 100644 --- a/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ b/extensions/positron-python/pythonFiles/testing_tools/adapter/pytest/_discovery.py @@ -32,6 +32,9 @@ def discover( if ec == 5: # No tests were discovered. pass + elif ec == 1: + # Some tests where collected but with errors. + pass elif ec != 0: print( "equivalent command: {} -m pytest {}".format( diff --git a/extensions/positron-python/pythonFiles/tests/test_create_venv.py b/extensions/positron-python/pythonFiles/tests/test_create_venv.py index e002ad17ef95..e70a4d90c99b 100644 --- a/extensions/positron-python/pythonFiles/tests/test_create_venv.py +++ b/extensions/positron-python/pythonFiles/tests/test_create_venv.py @@ -16,38 +16,44 @@ def test_venv_not_installed(): assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND" -def test_pip_not_installed(): +@pytest.mark.parametrize("install", ["requirements", "toml"]) +def test_pip_not_installed(install): importlib.reload(create_venv) create_venv.venv_exists = lambda _n: True create_venv.is_installed = lambda module: module != "pip" create_venv.run_process = lambda _args, _error_message: None with pytest.raises(create_venv.VenvError) as e: - create_venv.main(["--install"]) + if install == "requirements": + create_venv.main(["--requirements", "requirements-for-test.txt"]) + elif install == "toml": + create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) assert str(e.value) == "CREATE_VENV.PIP_NOT_FOUND" -@pytest.mark.parametrize("env_exists", [True, False]) -@pytest.mark.parametrize("git_ignore", [True, False]) -@pytest.mark.parametrize("install", [True, False]) +@pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"]) +@pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore"]) +@pytest.mark.parametrize("install", ["requirements", "toml", "skipInstall"]) def test_create_env(env_exists, git_ignore, install): importlib.reload(create_venv) create_venv.is_installed = lambda _x: True - create_venv.venv_exists = lambda _n: env_exists + create_venv.venv_exists = lambda _n: env_exists == "hasEnv" + create_venv.upgrade_pip = lambda _x: None install_packages_called = False - def install_packages(_name): + def install_packages(_env, _name): nonlocal install_packages_called install_packages_called = True - create_venv.install_packages = install_packages + create_venv.install_requirements = install_packages + create_venv.install_toml = install_packages run_process_called = False def run_process(args, error_message): nonlocal run_process_called run_process_called = True - if not env_exists: + if env_exists == "noEnv": assert args == [sys.executable, "-m", "venv", create_venv.VENV_NAME] assert error_message == "CREATE_VENV.VENV_FAILED_CREATION" @@ -62,18 +68,23 @@ def add_gitignore(_name): create_venv.add_gitignore = add_gitignore args = [] - if git_ignore: - args.append("--git-ignore") - if install: - args.append("--install") + if git_ignore == "useGitIgnore": + args += ["--git-ignore"] + if install == "requirements": + args += ["--requirements", "requirements-for-test.txt"] + elif install == "toml": + args += ["--toml", "pyproject.toml", "--extras", "test"] + create_venv.main(args) - assert install_packages_called == install + assert install_packages_called == (install != "skipInstall") # run_process is called when the venv does not exist - assert run_process_called != env_exists + assert run_process_called == (env_exists == "noEnv") # add_gitignore is called when new venv is created and git_ignore is True - assert add_gitignore_called == (not env_exists and git_ignore) + assert add_gitignore_called == ( + (env_exists == "noEnv") and (git_ignore == "useGitIgnore") + ) @pytest.mark.parametrize("install_type", ["requirements", "pyproject"]) @@ -93,12 +104,79 @@ def run_process(args, error_message): elif args[1:-1] == ["-m", "pip", "install", "-r"]: installing = "requirements" assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" - elif args[1:] == ["-m", "pip", "install", "-e", ".[extras]"]: + elif args[1:] == ["-m", "pip", "install", "-e", ".[test]"]: installing = "pyproject" assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT" create_venv.run_process = run_process - create_venv.main(["--install"]) + if install_type == "requirements": + create_venv.main(["--requirements", "requirements-for-test.txt"]) + elif install_type == "pyproject": + create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) + assert pip_upgraded assert installing == install_type + + +@pytest.mark.parametrize( + ("extras", "expected"), + [ + ([], ["-m", "pip", "install", "-e", "."]), + (["test"], ["-m", "pip", "install", "-e", ".[test]"]), + (["test", "doc"], ["-m", "pip", "install", "-e", ".[test,doc]"]), + ], +) +def test_toml_args(extras, expected): + importlib.reload(create_venv) + + actual = [] + + def run_process(args, error_message): + nonlocal actual + actual = args[1:] + + create_venv.run_process = run_process + + create_venv.install_toml(sys.executable, extras) + + assert actual == expected + + +@pytest.mark.parametrize( + ("extras", "expected"), + [ + ([], None), + ( + ["requirements/test.txt"], + [sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"], + ), + ( + ["requirements/test.txt", "requirements/doc.txt"], + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + "requirements/test.txt", + "-r", + "requirements/doc.txt", + ], + ), + ], +) +def test_requirements_args(extras, expected): + importlib.reload(create_venv) + + actual = None + + def run_process(args, error_message): + nonlocal actual + actual = args + + create_venv.run_process = run_process + + create_venv.install_requirements(sys.executable, extras) + + assert actual == expected diff --git a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py index 1a75c4af863c..c3b8a2d679d1 100644 --- a/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py +++ b/extensions/positron-python/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py @@ -403,6 +403,31 @@ def test_no_tests_found(self): self.assertEqual(tests, expected) self.assertEqual(actual_calls, expected_calls) + def test_found_with_collection_error(self): + stub = util.Stub() + pytest = StubPyTest(stub) + pytest.return_main = 1 + plugin = StubPlugin(stub) + expected = [] + plugin.discovered = expected + calls = [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ("discovered.parents", None, None), + ("discovered.__len__", None, None), + ("discovered.__getitem__", (0,), None), + ] + + parents, tests = _discovery.discover( + [], _pytest_main=pytest.main, _plugin=plugin + ) + + actual_calls = unique(stub.calls, lambda k: k[0]) + expected_calls = unique(calls, lambda k: k[0]) + + self.assertEqual(parents, []) + self.assertEqual(tests, expected) + self.assertEqual(actual_calls, expected_calls) + def test_stdio_hidden_file(self): stub = util.Stub() diff --git a/extensions/positron-python/src/client/activation/activationManager.ts b/extensions/positron-python/src/client/activation/activationManager.ts index 062c7df51248..fac5cbeda648 100644 --- a/extensions/positron-python/src/client/activation/activationManager.ts +++ b/extensions/positron-python/src/client/activation/activationManager.ts @@ -9,7 +9,7 @@ import { IApplicationDiagnostics } from '../application/types'; import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PYTHON_LANGUAGE } from '../common/constants'; import { IFileSystem } from '../common/platform/types'; -import { IDisposable, Resource } from '../common/types'; +import { IDisposable, IInterpreterPathService, Resource } from '../common/types'; import { Deferred } from '../common/utils/async'; import { IInterpreterAutoSelectionService } from '../interpreter/autoSelection/types'; import { traceDecoratorError } from '../logging'; @@ -36,6 +36,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IFileSystem) private readonly fileSystem: IFileSystem, @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, ) {} private filterServices() { @@ -91,6 +92,7 @@ export class ExtensionActivationManager implements IExtensionActivationManager { if (this.workspaceService.isTrusted) { // Do not interact with interpreters in a untrusted workspace. await this.autoSelection.autoSelectInterpreter(resource); + await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); } await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); await Promise.all(this.activationServices.map((item) => item.activate(resource))); diff --git a/extensions/positron-python/src/client/activation/common/analysisOptions.ts b/extensions/positron-python/src/client/activation/common/analysisOptions.ts index f2839a25399d..18de19384fbf 100644 --- a/extensions/positron-python/src/client/activation/common/analysisOptions.ts +++ b/extensions/positron-python/src/client/activation/common/analysisOptions.ts @@ -42,7 +42,7 @@ export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServ documentSelector, workspaceFolder, synchronize: { - configurationSection: PYTHON_LANGUAGE, + configurationSection: this.getConfigSectionsToSynchronize(), }, outputChannel: this.output, revealOutputChannelOn: RevealOutputChannelOn.Never, @@ -58,6 +58,10 @@ export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServ return this.workspace.isVirtualWorkspace ? [{ language: PYTHON_LANGUAGE }] : PYTHON; } + protected getConfigSectionsToSynchronize(): string[] { + return [PYTHON_LANGUAGE]; + } + protected async getInitializationOptions(): Promise { return undefined; } diff --git a/extensions/positron-python/src/client/activation/node/analysisOptions.ts b/extensions/positron-python/src/client/activation/node/analysisOptions.ts index 815ca73ff7eb..4e2320b42331 100644 --- a/extensions/positron-python/src/client/activation/node/analysisOptions.ts +++ b/extensions/positron-python/src/client/activation/node/analysisOptions.ts @@ -11,6 +11,7 @@ import { IExperimentService } from '../../common/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; import { LspNotebooksExperiment } from './lspNotebooksExperiment'; +import { traceWarn } from '../../logging'; const EDITOR_CONFIG_SECTION = 'editor'; const FORMAT_ON_TYPE_CONFIG_SETTING = 'formatOnType'; @@ -26,6 +27,10 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt super(lsOutputChannel, workspace); } + protected getConfigSectionsToSynchronize(): string[] { + return [...super.getConfigSectionsToSynchronize(), 'jupyter.runStartupCommands']; + } + // eslint-disable-next-line class-methods-use-this protected async getInitializationOptions(): Promise { return ({ @@ -39,23 +44,19 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt private async isAutoIndentEnabled() { const editorConfig = this.getPythonSpecificEditorSection(); - let formatOnTypeEffectiveValue = editorConfig.get(FORMAT_ON_TYPE_CONFIG_SETTING); const formatOnTypeInspect = editorConfig.inspect(FORMAT_ON_TYPE_CONFIG_SETTING); const formatOnTypeSetForPython = formatOnTypeInspect?.globalLanguageValue !== undefined; const inExperiment = await this.isInAutoIndentExperiment(); - - if (inExperiment !== formatOnTypeSetForPython) { - if (inExperiment) { - await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true); - } else if (formatOnTypeInspect?.globalLanguageValue !== false) { - await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, undefined); - } - - formatOnTypeEffectiveValue = this.getPythonSpecificEditorSection().get(FORMAT_ON_TYPE_CONFIG_SETTING); + // only explicitly enable formatOnType for those who are in the experiment + // but have not explicitly given a value for the setting + if (!formatOnTypeSetForPython && inExperiment) { + await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true); } - return inExperiment && formatOnTypeEffectiveValue; + const formatOnTypeEffectiveValue = this.getPythonSpecificEditorSection().get(FORMAT_ON_TYPE_CONFIG_SETTING); + + return formatOnTypeEffectiveValue; } private async isInAutoIndentExperiment(): Promise { @@ -75,11 +76,15 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt editorConfig: WorkspaceConfiguration, value: boolean | undefined, ) { - await editorConfig.update( - FORMAT_ON_TYPE_CONFIG_SETTING, - value, - ConfigurationTarget.Global, - /* overrideInLanguage */ true, - ); + try { + await editorConfig.update( + FORMAT_ON_TYPE_CONFIG_SETTING, + value, + ConfigurationTarget.Global, + /* overrideInLanguage */ true, + ); + } catch (ex) { + traceWarn(`Failed to set formatOnType to ${value}`); + } } } diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts index d6ad3b259615..19ccc2f8beb9 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -3,9 +3,8 @@ // eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import * as nls from 'vscode-nls'; import { IPlatformService } from '../../../common/platform/types'; import { IConfigurationService, @@ -23,11 +22,8 @@ import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '. import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../types'; import { Common } from '../../../common/utils/localize'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - const messages = { - [DiagnosticCodes.MacInterpreterSelected]: localize( - 'DiagnosticCodes.MacInterpreterSelected', + [DiagnosticCodes.MacInterpreterSelected]: l10n.t( 'The selected macOS system install of Python is not recommended, some functionality in the extension will be limited. [Install another version of Python](https://www.python.org/downloads) or select a different interpreter for the best experience. [Learn more](https://aka.ms/AA7jfor).', ), }; diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/powerShellActivation.ts b/extensions/positron-python/src/client/application/diagnostics/checks/powerShellActivation.ts index 4ffdf21a9173..85f68db0d6a4 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/powerShellActivation.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/powerShellActivation.ts @@ -3,9 +3,8 @@ // eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import * as nls from 'vscode-nls'; import { useCommandPromptAsDefaultShell } from '../../../common/terminal/commandPrompt'; import { IConfigurationService, ICurrentProcess, IDisposableRegistry, Resource } from '../../../common/types'; import { Common } from '../../../common/utils/localize'; @@ -19,10 +18,7 @@ import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -const PowershellActivationNotSupportedWithBatchFilesMessage = localize( - 'powershelActivationMsg', +const PowershellActivationNotSupportedWithBatchFilesMessage = l10n.t( 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.', ); diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts b/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts index 87ba83cbbc66..45a758c9f283 100644 --- a/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/extensions/positron-python/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -3,9 +3,8 @@ // eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import * as nls from 'vscode-nls'; import * as path from 'path'; import { IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; import { IInterpreterService } from '../../../interpreter/contracts'; @@ -30,15 +29,11 @@ import { IExtensionSingleActivationService } from '../../../activation/types'; import { cache } from '../../../common/utils/decorators'; import { noop } from '../../../common/utils/misc'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - const messages = { - [DiagnosticCodes.NoPythonInterpretersDiagnostic]: localize( - 'DiagnosticCodes.NoPythonInterpretersDiagnostic', + [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( 'No Python interpreter is selected. Please select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', ), - [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: localize( - 'DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic', + [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t( 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging.', ), }; @@ -59,7 +54,7 @@ export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { // Specify folder name in case of multiroot scenarios const folder = workspaceService.getWorkspaceFolder(resource); if (folder) { - formatArg = ` ${localize('Common.forWorkspace', 'for workspace')} ${path.basename(folder.uri.fsPath)}`; + formatArg = ` ${l10n.t('for workspace')} ${path.basename(folder.uri.fsPath)}`; } } super(code, messages[code].format(formatArg), DiagnosticSeverity.Error, scope, resource, undefined, 'always'); diff --git a/extensions/positron-python/src/client/application/diagnostics/checks/pythonPathDeprecated.ts b/extensions/positron-python/src/client/application/diagnostics/checks/pythonPathDeprecated.ts deleted file mode 100644 index f88347f33fe4..000000000000 --- a/extensions/positron-python/src/client/application/diagnostics/checks/pythonPathDeprecated.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// eslint-disable-next-line max-classes-per-file -import { inject, named } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { IDisposableRegistry, Resource } from '../../../common/types'; -import { Common, Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { IDiagnosticsCommandFactory } from '../commands/types'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -export class PythonPathDeprecatedDiagnostic extends BaseDiagnostic { - constructor(message: string, resource: Resource) { - super( - DiagnosticCodes.PythonPathDeprecatedDiagnostic, - message, - DiagnosticSeverity.Information, - DiagnosticScope.WorkspaceFolder, - resource, - ); - } -} - -export const PythonPathDeprecatedDiagnosticServiceId = 'PythonPathDeprecatedDiagnosticServiceId'; - -export class PythonPathDeprecatedDiagnosticService extends BaseDiagnosticsService { - private workspaceService: IWorkspaceService; - - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - ) { - super([DiagnosticCodes.PythonPathDeprecatedDiagnostic], serviceContainer, disposableRegistry, true); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - } - - public async diagnose(resource: Resource): Promise { - const setting = this.workspaceService.getConfiguration('python', resource).inspect('pythonPath'); - if (!setting) { - return []; - } - const isCodeWorkspaceSettingSet = this.workspaceService.workspaceFile && setting.workspaceValue !== undefined; - const isSettingsJsonSettingSet = setting.workspaceFolderValue !== undefined; - if (isCodeWorkspaceSettingSet || isSettingsJsonSettingSet) { - return [new PythonPathDeprecatedDiagnostic(Diagnostics.removedPythonPathFromSettings, resource)]; - } - return []; - } - - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - if (diagnostics.length === 0 || !(await this.canHandle(diagnostics[0]))) { - return; - } - const diagnostic = diagnostics[0]; - if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { - return; - } - const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - const options = [ - { - prompt: Common.ok, - }, - ]; - const command = commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }); - await command.invoke(); - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } -} diff --git a/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts b/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts index edc9e3ffb7f8..8d9b765939c9 100644 --- a/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts +++ b/extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts @@ -33,10 +33,6 @@ import { } from './checks/powerShellActivation'; import { PylanceDefaultDiagnosticService, PylanceDefaultDiagnosticServiceId } from './checks/pylanceDefault'; import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter'; -import { - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId, -} from './checks/pythonPathDeprecated'; import { SwitchToDefaultLanguageServerDiagnosticService, SwitchToDefaultLanguageServerDiagnosticServiceId, @@ -92,11 +88,6 @@ export function registerTypes(serviceManager: IServiceManager): void { InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId, ); - serviceManager.addSingleton( - IDiagnosticsService, - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId, - ); serviceManager.addSingleton( IDiagnosticsService, diff --git a/extensions/positron-python/src/client/browser/extension.ts b/extensions/positron-python/src/client/browser/extension.ts index c8410aad3292..c9ecf8ac1212 100644 --- a/extensions/positron-python/src/client/browser/extension.ts +++ b/extensions/positron-python/src/client/browser/extension.ts @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import '../../setupNls'; import * as vscode from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { LanguageClientOptions } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/browser'; import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; import { LanguageServerType } from '../activation/types'; -import { AppinsightsKey, PVSC_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; import { EventName } from '../telemetry/constants'; import { createStatusItem } from './intellisenseStatus'; @@ -77,7 +76,7 @@ async function runPylance( ], synchronize: { // Synchronize the setting section to the server. - configurationSection: ['python'], + configurationSection: ['python', 'jupyter.runStartupCommands'], }, middleware, }; @@ -127,16 +126,10 @@ function getTelemetryReporter() { if (telemetryReporter) { return telemetryReporter; } - const extensionId = PVSC_EXTENSION_ID; - - // eslint-disable-next-line global-require - const { extensions } = require('vscode') as typeof import('vscode'); - const extension = extensions.getExtension(extensionId)!; - const extensionVersion = extension.packageJSON.version; // eslint-disable-next-line global-require const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; - telemetryReporter = new Reporter(extensionId, extensionVersion, AppinsightsKey, true, [ + telemetryReporter = new Reporter(AppinsightsKey, [ { lookup: /(errorName|errorMessage|errorStack)/g, }, diff --git a/extensions/positron-python/src/client/browser/localize.ts b/extensions/positron-python/src/client/browser/localize.ts index 24f2cde53c9a..fd50dbcc7093 100644 --- a/extensions/positron-python/src/client/browser/localize.ts +++ b/extensions/positron-python/src/client/browser/localize.ts @@ -3,21 +3,20 @@ 'use strict'; +import { l10n } from 'vscode'; + /* eslint-disable @typescript-eslint/no-namespace */ // IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export namespace LanguageService { export const statusItem = { - name: localize('LanguageService.statusItem.name', 'Python IntelliSense Status'), - text: localize('LanguageService.statusItem.text', 'Partial Mode'), - detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), }; } export namespace Common { - export const learnMore = localize('Common.learnMore', 'Learn more'); + export const learnMore = l10n.t('Learn more'); } diff --git a/extensions/positron-python/src/client/common/application/applicationShell.ts b/extensions/positron-python/src/client/common/application/applicationShell.ts index 62696c34524c..c1a5de51b7f6 100644 --- a/extensions/positron-python/src/client/common/application/applicationShell.ts +++ b/extensions/positron-python/src/client/common/application/applicationShell.ts @@ -122,8 +122,14 @@ export class ApplicationShell implements IApplicationShell { return window.setStatusBarMessage(text, arg); } - public createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem { - return window.createStatusBarItem(alignment, priority); + public createStatusBarItem( + alignment?: StatusBarAlignment, + priority?: number, + id?: string | undefined, + ): StatusBarItem { + return id + ? window.createStatusBarItem(id, alignment, priority) + : window.createStatusBarItem(alignment, priority); } public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { return window.showWorkspaceFolderPick(options); diff --git a/extensions/positron-python/src/client/common/application/extensions.ts b/extensions/positron-python/src/client/common/application/extensions.ts index 9d62e76d5da4..e4b8f5bce73d 100644 --- a/extensions/positron-python/src/client/common/application/extensions.ts +++ b/extensions/positron-python/src/client/common/application/extensions.ts @@ -17,6 +17,9 @@ import { EXTENSION_ROOT_DIR } from '../constants'; */ @injectable() export class Extensions implements IExtensions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _cachedExtensions?: readonly Extension[]; + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -32,6 +35,16 @@ export class Extensions implements IExtensions { return extensions.getExtension(extensionId); } + private get cachedExtensions() { + if (!this._cachedExtensions) { + this._cachedExtensions = extensions.all; + extensions.onDidChange(() => { + this._cachedExtensions = extensions.all; + }); + } + return this._cachedExtensions; + } + /** * Code borrowed from: * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts @@ -51,7 +64,8 @@ export class Extensions implements IExtensions { }) .filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot)) .filter((item) => - this.all.some( + // Use cached list of extensions as we need this to be fast. + this.cachedExtensions.some( (ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath), ), ) as string[]; diff --git a/extensions/positron-python/src/client/common/application/terminalManager.ts b/extensions/positron-python/src/client/common/application/terminalManager.ts index 8fe6c067d0e6..e5b758437393 100644 --- a/extensions/positron-python/src/client/common/application/terminalManager.ts +++ b/extensions/positron-python/src/client/common/application/terminalManager.ts @@ -2,18 +2,40 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { Event, Terminal, TerminalOptions, window } from 'vscode'; +import { Event, EventEmitter, Terminal, TerminalOptions, window } from 'vscode'; +import { traceLog } from '../../logging'; import { ITerminalManager } from './types'; @injectable() export class TerminalManager implements ITerminalManager { + private readonly didOpenTerminal = new EventEmitter(); + constructor() { + window.onDidOpenTerminal((terminal) => { + this.didOpenTerminal.fire(monkeyPatchTerminal(terminal)); + }); + } public get onDidCloseTerminal(): Event { return window.onDidCloseTerminal; } public get onDidOpenTerminal(): Event { - return window.onDidOpenTerminal; + return this.didOpenTerminal.event; } public createTerminal(options: TerminalOptions): Terminal { - return window.createTerminal(options); + return monkeyPatchTerminal(window.createTerminal(options)); + } +} + +/** + * Monkeypatch the terminal to log commands sent. + */ +function monkeyPatchTerminal(terminal: Terminal) { + if (!(terminal as any).isPatched) { + const oldSendText = terminal.sendText.bind(terminal); + terminal.sendText = (text: string, addNewLine: boolean = true) => { + traceLog(`Send text to terminal: ${text}`); + return oldSendText(text, addNewLine); + }; + (terminal as any).isPatched = true; } + return terminal; } diff --git a/extensions/positron-python/src/client/common/application/types.ts b/extensions/positron-python/src/client/common/application/types.ts index 256404810237..69caf30a261b 100644 --- a/extensions/positron-python/src/client/common/application/types.ts +++ b/extensions/positron-python/src/client/common/application/types.ts @@ -355,7 +355,7 @@ export interface IApplicationShell { * @param priority The priority of the item. Higher values mean the item should be shown more to the left. * @return A new status bar item. */ - createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + createStatusBarItem(alignment?: StatusBarAlignment, priority?: number, id?: string): StatusBarItem; /** * Shows a selection list of [workspace folders](#workspace.workspaceFolders) to pick from. * Returns `undefined` if no folder is open. diff --git a/extensions/positron-python/src/client/common/configSettings.ts b/extensions/positron-python/src/client/common/configSettings.ts index 93a3f631c67a..6c31de4361c8 100644 --- a/extensions/positron-python/src/client/common/configSettings.ts +++ b/extensions/positron-python/src/client/common/configSettings.ts @@ -504,7 +504,13 @@ export class PythonSettings implements IPythonSettings { optOutFrom: [], }; - this.tensorBoard = pythonSettings.get('tensorBoard'); + const tensorBoardSettings = systemVariables.resolveAny( + pythonSettings.get('tensorBoard'), + )!; + this.tensorBoard = tensorBoardSettings || { logDirectory: '' }; + if (this.tensorBoard.logDirectory) { + this.tensorBoard.logDirectory = getAbsolutePath(this.tensorBoard.logDirectory, workspaceRoot); + } } // eslint-disable-next-line class-methods-use-this diff --git a/extensions/positron-python/src/client/common/experiments/service.ts b/extensions/positron-python/src/client/common/experiments/service.ts index 9f76d8347046..39319ab2a363 100644 --- a/extensions/positron-python/src/client/common/experiments/service.ts +++ b/extensions/positron-python/src/client/common/experiments/service.ts @@ -4,8 +4,8 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { l10n } from 'vscode'; import { getExperimentationService, IExperimentationService } from 'vscode-tas-client'; -import * as nls from 'vscode-nls'; import { traceLog } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -14,8 +14,6 @@ import { PVSC_EXTENSION_ID } from '../constants'; import { IExperimentService, IPersistentStateFactory } from '../types'; import { ExperimentationTelemetry } from './telemetry'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - const EXP_MEMENTO_KEY = 'VSCode.ABExp.FeatureData'; const EXP_CONFIG_ID = 'vscode'; @@ -174,7 +172,7 @@ export class ExperimentService implements IExperimentService { if (this._optOutFrom.includes('All')) { // We prioritize opt out first - traceLog(localize('Experiments.optedOutOf', "Experiment '{0}' is inactive", 'All')); + traceLog(l10n.t("Experiment '{0}' is inactive", 'All')); // Since we are in the Opt Out all case, this means when checking for experiment we // short circuit and return. So, printing out additional experiment info might cause @@ -183,7 +181,7 @@ export class ExperimentService implements IExperimentService { } if (this._optInto.includes('All')) { // Only if 'All' is not in optOut then check if it is in Opt In. - traceLog(localize('Experiments.inGroup', "Experiment '{0}' is active", 'All')); + traceLog(l10n.t("Experiment '{0}' is active", 'All')); // Similar to the opt out case. If user is opting into to all experiments we short // circuit the experiment checks. So, skip printing any additional details to the logs. @@ -194,14 +192,14 @@ export class ExperimentService implements IExperimentService { this._optOutFrom .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) .forEach((exp) => { - traceLog(localize('Experiments.manuallyOptedOutOf', "Experiment '{0}' is inactive", exp)); + traceLog(l10n.t("Experiment '{0}' is inactive", exp)); }); // Log experiments that users manually opt into, these are experiments which are added using the exp framework. this._optInto .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) .forEach((exp) => { - traceLog(localize('Experiments.manuallyOptIntoExperiments', "Experiment '{0}' is active", exp)); + traceLog(l10n.t("Experiment '{0}' is active", exp)); }); if (!experimentsDisabled) { @@ -214,7 +212,7 @@ export class ExperimentService implements IExperimentService { !this._optOutFrom.includes(exp) && !this._optInto.includes(exp) ) { - traceLog(localize('Experiments.autoOptIntoExperiments', "Experiment '{0}' is active", exp)); + traceLog(l10n.t("Experiment '{0}' is active", exp)); } }); } diff --git a/extensions/positron-python/src/client/common/extensions.ts b/extensions/positron-python/src/client/common/extensions.ts index 5510d95069f7..962349583776 100644 --- a/extensions/positron-python/src/client/common/extensions.ts +++ b/extensions/positron-python/src/client/common/extensions.ts @@ -73,7 +73,9 @@ String.prototype.toCommandArgumentForPythonExt = function (this: string): string if (!this) { return this; } - return (this.indexOf(' ') >= 0 || this.indexOf('&') >= 0) && !this.startsWith('"') && !this.endsWith('"') + return (this.indexOf(' ') >= 0 || this.indexOf('&') >= 0 || this.indexOf('(') >= 0 || this.indexOf(')') >= 0) && + !this.startsWith('"') && + !this.endsWith('"') ? `"${this}"` : this.toString(); }; diff --git a/extensions/positron-python/src/client/common/installer/condaInstaller.ts b/extensions/positron-python/src/client/common/installer/condaInstaller.ts index b440e6cba798..774cade34457 100644 --- a/extensions/positron-python/src/client/common/installer/condaInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/condaInstaller.ts @@ -38,7 +38,7 @@ export class CondaInstaller extends ModuleInstaller { } public get priority(): number { - return 0; + return 10; } /** diff --git a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts index 1f600053d02e..4049edb8ec0d 100644 --- a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts @@ -3,7 +3,7 @@ import { injectable } from 'inversify'; import * as path from 'path'; -import { CancellationToken, ProgressLocation, ProgressOptions } from 'vscode'; +import { CancellationToken, l10n, ProgressLocation, ProgressOptions } from 'vscode'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { traceError, traceLog } from '../../logging'; @@ -21,9 +21,6 @@ import { ExecutionInfo, IConfigurationService, IOutputChannel, Product } from '. import { isResource } from '../utils/misc'; import { ProductNames } from './productNames'; import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); @injectable() export abstract class ModuleInstaller implements IModuleInstaller { @@ -139,7 +136,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { const options: ProgressOptions = { location: ProgressLocation.Notification, cancellable: true, - title: localize('products.installingModule', 'Installing {0}', name), + title: l10n.t('Installing {0}', name), }; await shell.withProgress(options, async (_, token: CancellationToken) => install(wrapCancellationTokens(token, cancel)), diff --git a/extensions/positron-python/src/client/common/installer/pipEnvInstaller.ts b/extensions/positron-python/src/client/common/installer/pipEnvInstaller.ts index 04fcdb6fc91e..2c7dece6a298 100644 --- a/extensions/positron-python/src/client/common/installer/pipEnvInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/pipEnvInstaller.ts @@ -62,9 +62,6 @@ export class PipEnvInstaller extends ModuleInstaller { flags & ModuleInstallFlags.updateDependencies || flags & ModuleInstallFlags.upgrade; const args = [update ? 'update' : 'install', moduleName, '--dev']; - if (moduleName === 'black') { - args.push('--pre'); - } return { args: args, execPath: pipenvName, diff --git a/extensions/positron-python/src/client/common/installer/poetryInstaller.ts b/extensions/positron-python/src/client/common/installer/poetryInstaller.ts index ea9de510cadd..5017d0813d98 100644 --- a/extensions/positron-python/src/client/common/installer/poetryInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/poetryInstaller.ts @@ -71,9 +71,6 @@ export class PoetryInstaller extends ModuleInstaller { protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { const execPath = this.configurationService.getSettings(isResource(resource) ? resource : undefined).poetryPath; const args = ['add', '--group', 'dev', moduleName]; - if (moduleName === 'black') { - args.push('--allow-prereleases'); - } return { args, execPath, diff --git a/extensions/positron-python/src/client/common/installer/productInstaller.ts b/extensions/positron-python/src/client/common/installer/productInstaller.ts index 223769842101..526369f9e9ad 100644 --- a/extensions/positron-python/src/client/common/installer/productInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/productInstaller.ts @@ -2,9 +2,8 @@ import { inject, injectable } from 'inversify'; import * as semver from 'semver'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, l10n, Uri } from 'vscode'; import '../extensions'; -import * as nls from 'vscode-nls'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { LinterId } from '../../linters/types'; @@ -42,8 +41,6 @@ import { isParentPath } from '../platform/fs-paths'; export { Product } from '../types'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - // Products which may not be available to install from certain package registries, keyed by product name // Installer implementations can check this to determine a suitable installation channel for a product // This is temporary and can be removed when https://github.com/microsoft/vscode-jupyter/issues/5034 is unblocked @@ -252,25 +249,16 @@ export class FormatterInstaller extends BaseInstaller { const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); const productName = ProductNames.get(product)!; formatterNames.splice(formatterNames.indexOf(productName), 1); - const useOptions = formatterNames.map((name) => localize('products.useFormatter', 'Use {0}', name)); + const useOptions = formatterNames.map((name) => l10n.t('Use {0}', name)); const yesChoice = Common.bannerLabelYes; const options = [...useOptions, Common.doNotShowAgain]; - let message = localize( - 'products.formatterNotInstalled', - 'Formatter {0} is not installed. Install?', - productName, - ); + let message = l10n.t('Formatter {0} is not installed. Install?', productName); if (this.isExecutableAModule(product, resource)) { options.splice(0, 0, yesChoice); } else { const executable = this.getExecutableNameFromSettings(product, resource); - message = localize( - 'products.invalidFormatterPath', - 'Path to the {0} formatter is invalid ({1})', - productName, - executable, - ); + message = l10n.t('Path to the {0} formatter is invalid ({1})', productName, executable); } const item = await this.appShell.showErrorMessage(message, ...options); @@ -339,17 +327,12 @@ export class LinterInstaller extends BaseInstaller { const options = [selectLinter, doNotShowAgain]; - let message = localize('Linter.notInstalled', 'Linter {0} is not installed.', productName); + let message = l10n.t('Linter {0} is not installed.', productName); if (this.isExecutableAModule(product, resource)) { options.splice(0, 0, install); } else { const executable = this.getExecutableNameFromSettings(product, resource); - message = localize( - 'Linter.invalidPath', - 'Path to the {0} linter is invalid ({1})', - productName, - executable, - ); + message = l10n.t('Path to the {0} linter is invalid ({1})', productName, executable); } const response = await this.appShell.showErrorMessage(message, ...options); if (response === install) { @@ -404,21 +387,12 @@ export class TestFrameworkInstaller extends BaseInstaller { const productName = ProductNames.get(product)!; const options: string[] = []; - let message = localize( - 'TestFramework.notIstalled', - 'Test framework {0} is not installed. Install?', - productName, - ); + let message = l10n.t('Test framework {0} is not installed. Install?', productName); if (this.isExecutableAModule(product, resource)) { options.push(...[Common.bannerLabelYes, Common.bannerLabelNo]); } else { const executable = this.getExecutableNameFromSettings(product, resource); - message = localize( - 'TestFramework.invalidPath', - 'Path to the {0} test framework is invalid ({1})', - productName, - executable, - ); + message = l10n.t('Path to the {0} test framework is invalid ({1})', productName, executable); } const item = await this.appShell.showErrorMessage(message, ...options); @@ -529,8 +503,7 @@ export class DataScienceInstaller extends BaseInstaller { if (!installerModule) { this.appShell .showErrorMessage( - localize( - 'Installer.couldNotInstallLibrary', + l10n.t( 'Could not install {0}. If pip is not available, please use the package manager of your choice to manually install this library into your Python environment.', moduleName, ), @@ -575,11 +548,7 @@ export class DataScienceInstaller extends BaseInstaller { ): Promise { const productName = ProductNames.get(product)!; const item = await this.appShell.showErrorMessage( - localize( - 'Installer.dataScienceInstallPrompt', - 'Data Science library {0} is not installed. Install?', - productName, - ), + l10n.t('Data Science library {0} is not installed. Install?', productName), Common.bannerLabelYes, Common.bannerLabelNo, ); diff --git a/extensions/positron-python/src/client/common/interpreterPathService.ts b/extensions/positron-python/src/client/common/interpreterPathService.ts index 9104b6a1a8d6..5b27fd5a9d72 100644 --- a/extensions/positron-python/src/client/common/interpreterPathService.ts +++ b/extensions/positron-python/src/client/common/interpreterPathService.ts @@ -7,7 +7,7 @@ import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; import { traceError } from '../logging'; -import { IWorkspaceService } from './application/types'; +import { IApplicationEnvironment, IWorkspaceService } from './application/types'; import { PythonSettings } from './configSettings'; import { isTestExecution } from './constants'; import { FileSystemPaths } from './platform/fs-paths'; @@ -24,6 +24,9 @@ import { } from './types'; import { SystemVariables } from './variables/systemVariables'; +export const remoteWorkspaceKeysForWhichTheCopyIsDone_Key = 'remoteWorkspaceKeysForWhichTheCopyIsDone_Key'; +export const remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key = 'remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key'; +export const isRemoteGlobalSettingCopiedKey = 'isRemoteGlobalSettingCopiedKey'; export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; const CI_PYTHON_PATH = getCIPythonPath(); @@ -44,6 +47,7 @@ export class InterpreterPathService implements IInterpreterPathService { @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IDisposableRegistry) disposables: IDisposable[], + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, ) { disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); this.fileSystemPaths = FileSystemPaths.withDefaults(); @@ -55,17 +59,17 @@ export class InterpreterPathService implements IInterpreterPathService { } } - public inspect(resource: Resource): InspectInterpreterSettingType { + public inspect(resource: Resource, useOldKey = false): InspectInterpreterSettingType { resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; let workspaceFolderSetting: IPersistentState | undefined; let workspaceSetting: IPersistentState | undefined; if (resource) { workspaceFolderSetting = this.persistentStateFactory.createGlobalPersistentState( - this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder), + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder, useOldKey), undefined, ); workspaceSetting = this.persistentStateFactory.createGlobalPersistentState( - this.getSettingKey(resource, ConfigurationTarget.Workspace), + this.getSettingKey(resource, ConfigurationTarget.Workspace, useOldKey), undefined, ); } @@ -73,8 +77,14 @@ export class InterpreterPathService implements IInterpreterPathService { this.workspaceService.getConfiguration('python', resource)?.inspect('defaultInterpreterPath') ?? {}; return { globalValue: defaultInterpreterPath.globalValue, - workspaceFolderValue: workspaceFolderSetting?.value || defaultInterpreterPath.workspaceFolderValue, - workspaceValue: workspaceSetting?.value || defaultInterpreterPath.workspaceValue, + workspaceFolderValue: + !workspaceFolderSetting?.value || workspaceFolderSetting?.value === 'python' + ? defaultInterpreterPath.workspaceFolderValue + : workspaceFolderSetting.value, + workspaceValue: + !workspaceSetting?.value || workspaceSetting?.value === 'python' + ? defaultInterpreterPath.workspaceValue + : workspaceSetting.value, }; } @@ -125,6 +135,7 @@ export class InterpreterPathService implements IInterpreterPathService { public getSettingKey( resource: Uri, configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder, + useOldKey = false, ): string { let settingKey: string; const folderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource); @@ -138,6 +149,71 @@ export class InterpreterPathService implements IInterpreterPathService { : // Only a single folder is opened, use fsPath of the folder as key `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; } + if (!useOldKey && this.appEnvironment.remoteName) { + return `${this.appEnvironment.remoteName}_${settingKey}`; + } return settingKey; } + + public async copyOldInterpreterStorageValuesToNew(resource: Resource): Promise { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + const oldSettings = this.inspect(resource, true); + await Promise.all([ + this._copyWorkspaceFolderValueToNewStorage(resource, oldSettings.workspaceFolderValue), + this._copyWorkspaceValueToNewStorage(resource, oldSettings.workspaceValue), + this._moveGlobalSettingValueToNewStorage(oldSettings.globalValue), + ]); + } + + public async _copyWorkspaceFolderValueToNewStorage(resource: Resource, value: string | undefined): Promise { + // Copy workspace folder setting into the new storage if it hasn't been copied already + const workspaceFolderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); + if (workspaceFolderKey === '') { + // No workspace folder is opened, simply return. + return; + } + const flaggedWorkspaceFolderKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key, + [], + ); + const flaggedWorkspaceFolderKeys = flaggedWorkspaceFolderKeysStorage.value; + const shouldUpdateWorkspaceFolderSetting = !flaggedWorkspaceFolderKeys.includes(workspaceFolderKey); + if (shouldUpdateWorkspaceFolderSetting) { + await this.update(resource, ConfigurationTarget.WorkspaceFolder, value); + await flaggedWorkspaceFolderKeysStorage.updateValue([workspaceFolderKey, ...flaggedWorkspaceFolderKeys]); + } + } + + public async _copyWorkspaceValueToNewStorage(resource: Resource, value: string | undefined): Promise { + // Copy workspace setting into the new storage if it hasn't been copied already + const workspaceKey = this.workspaceService.workspaceFile + ? this.fileSystemPaths.normCase(this.workspaceService.workspaceFile.fsPath) + : undefined; + if (!workspaceKey) { + return; + } + const flaggedWorkspaceKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + remoteWorkspaceKeysForWhichTheCopyIsDone_Key, + [], + ); + const flaggedWorkspaceKeys = flaggedWorkspaceKeysStorage.value; + const shouldUpdateWorkspaceSetting = !flaggedWorkspaceKeys.includes(workspaceKey); + if (shouldUpdateWorkspaceSetting) { + await this.update(resource, ConfigurationTarget.Workspace, value); + await flaggedWorkspaceKeysStorage.updateValue([workspaceKey, ...flaggedWorkspaceKeys]); + } + } + + public async _moveGlobalSettingValueToNewStorage(value: string | undefined) { + // Move global setting into the new storage if it hasn't been moved already + const isGlobalSettingCopiedStorage = this.persistentStateFactory.createGlobalPersistentState( + isRemoteGlobalSettingCopiedKey, + false, + ); + const shouldUpdateGlobalSetting = !isGlobalSettingCopiedStorage.value; + if (shouldUpdateGlobalSetting) { + await this.update(undefined, ConfigurationTarget.Global, value); + await isGlobalSettingCopiedStorage.updateValue(true); + } + } } diff --git a/extensions/positron-python/src/client/common/net/fileDownloader.ts b/extensions/positron-python/src/client/common/net/fileDownloader.ts index 5c54ae247911..6ddd06bcc940 100644 --- a/extensions/positron-python/src/client/common/net/fileDownloader.ts +++ b/extensions/positron-python/src/client/common/net/fileDownloader.ts @@ -4,9 +4,8 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import * as nls from 'vscode-nls'; import * as requestTypes from 'request'; -import { Progress } from 'vscode'; +import { l10n, Progress } from 'vscode'; import { traceLog } from '../../logging'; import { IApplicationShell } from '../application/types'; import { Octicons } from '../constants'; @@ -14,8 +13,6 @@ import { IFileSystem, WriteStream } from '../platform/types'; import { DownloadOptions, IFileDownloader, IHttpClient } from '../types'; import { noop } from '../utils/misc'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - @injectable() export class FileDownloader implements IFileDownloader { constructor( @@ -24,7 +21,7 @@ export class FileDownloader implements IFileDownloader { @inject(IApplicationShell) private readonly appShell: IApplicationShell, ) {} public async downloadFile(uri: string, options: DownloadOptions): Promise { - traceLog(localize('downloading.file', 'Downloading {0}...', uri)); + traceLog(l10n.t('Downloading {0}...', uri)); const tempFile = await this.fs.createTemporaryFile(options.extension); await this.downloadFileWithStatusBarProgress(uri, options.progressMessagePrefix, tempFile.filePath).then( @@ -99,8 +96,7 @@ function formatProgressMessageWithState(progressMessagePrefix: string, state: Re const total = Math.round(state.size.total / 1024); const percentage = Math.round(100 * state.percent); - return localize( - 'downloading.file.progress', + return l10n.t( '{0}{1} of {2} KB ({3}%)', progressMessagePrefix, received.toString(), diff --git a/extensions/positron-python/src/client/common/persistentState.ts b/extensions/positron-python/src/client/common/persistentState.ts index 0d397665d96d..48e885a676a2 100644 --- a/extensions/positron-python/src/client/common/persistentState.ts +++ b/extensions/positron-python/src/client/common/persistentState.ts @@ -6,7 +6,7 @@ import { inject, injectable, named } from 'inversify'; import { Memento } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; -import { traceError } from '../logging'; +import { traceError, traceVerbose, traceWarn } from '../logging'; import { ICommandManager } from './application/types'; import { Commands } from './constants'; import { @@ -41,13 +41,24 @@ export class PersistentState implements IPersistentState { } } - public async updateValue(newValue: T): Promise { + public async updateValue(newValue: T, retryOnce = true): Promise { try { if (this.expiryDurationMs) { await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); } else { await this.storage.update(this.key, newValue); } + if (retryOnce && JSON.stringify(this.value) != JSON.stringify(newValue)) { + // Due to a VSCode bug sometimes the changes are not reflected in the storage, atleast not immediately. + // It is noticed however that if we reset the storage first and then update it, it works. + // https://github.com/microsoft/vscode/issues/171827 + traceVerbose('Storage update failed for key', this.key, ' retrying by resetting first'); + await this.updateValue(undefined as any, false); + await this.updateValue(newValue, false); + if (JSON.stringify(this.value) != JSON.stringify(newValue)) { + traceWarn('Retry failed, storage update failed for key', this.key); + } + } } catch (ex) { traceError('Error while updating storage for key:', this.key, ex); } diff --git a/extensions/positron-python/src/client/common/process/internal/scripts/index.ts b/extensions/positron-python/src/client/common/process/internal/scripts/index.ts index d3f28097a5dc..33eaa4686e8f 100644 --- a/extensions/positron-python/src/client/common/process/internal/scripts/index.ts +++ b/extensions/positron-python/src/client/common/process/internal/scripts/index.ts @@ -43,18 +43,16 @@ export type InterpreterInfoJson = { export const OUTPUT_MARKER_SCRIPT = path.join(_SCRIPTS_DIR, 'get_output_via_markers.py'); -export function interpreterInfo(): [string[], (out: string) => InterpreterInfoJson | undefined] { +export function interpreterInfo(): [string[], (out: string) => InterpreterInfoJson] { const script = path.join(SCRIPTS_DIR, 'interpreterInfo.py'); const args = [script]; - function parse(out: string): InterpreterInfoJson | undefined { - let json: InterpreterInfoJson | undefined; + function parse(out: string): InterpreterInfoJson { try { - json = JSON.parse(out); + return JSON.parse(out); } catch (ex) { throw Error(`python ${args} returned bad JSON (${out}) (${ex})`); } - return json; } return [args, parse]; diff --git a/extensions/positron-python/src/client/common/process/pythonExecutionFactory.ts b/extensions/positron-python/src/client/common/process/pythonExecutionFactory.ts index 02c42beb1400..fc13e7f2346c 100644 --- a/extensions/positron-python/src/client/common/process/pythonExecutionFactory.ts +++ b/extensions/positron-python/src/client/common/process/pythonExecutionFactory.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import { IEnvironmentActivationService } from '../../interpreter/activation/types'; -import { IComponentAdapter } from '../../interpreter/contracts'; +import { IActivatedEnvironmentLaunch, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -51,7 +51,11 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { public async create(options: ExecutionFactoryCreationOptions): Promise { let { pythonPath } = options; - if (!pythonPath) { + if (!pythonPath || pythonPath === 'python') { + const activatedEnvLaunch = this.serviceContainer.get( + IActivatedEnvironmentLaunch, + ); + await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); // If python path wasn't passed in, we need to auto select it and then read it // from the configuration. const interpreterPath = this.interpreterPathExpHelper.get(options.resource); diff --git a/extensions/positron-python/src/client/common/process/rawProcessApis.ts b/extensions/positron-python/src/client/common/process/rawProcessApis.ts index 43f73e671e94..fb5108322751 100644 --- a/extensions/positron-python/src/client/common/process/rawProcessApis.ts +++ b/extensions/positron-python/src/client/common/process/rawProcessApis.ts @@ -11,6 +11,7 @@ import { DEFAULT_ENCODING } from './constants'; import { ExecutionResult, ObservableExecutionResult, Output, ShellOptions, SpawnOptions, StdErrError } from './types'; import { noop } from '../utils/misc'; import { decodeBuffer } from './decoder'; +import { traceVerbose } from '../../logging'; function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { const defaultOptions = { ...options }; @@ -51,6 +52,7 @@ export function shellExec( disposables?: Set, ): Promise> { const shellOptions = getDefaultOptions(options, defaultEnv); + traceVerbose(`Shell Exec: ${command} with options: ${JSON.stringify(shellOptions, null, 4)}`); return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const callback = (e: any, stdout: any, stderr: any) => { diff --git a/extensions/positron-python/src/client/common/terminal/shellDetector.ts b/extensions/positron-python/src/client/common/terminal/shellDetector.ts index aaf04f6d057b..98cda5953fe8 100644 --- a/extensions/positron-python/src/client/common/terminal/shellDetector.ts +++ b/extensions/positron-python/src/client/common/terminal/shellDetector.ts @@ -4,8 +4,8 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { Terminal } from 'vscode'; -import { traceVerbose } from '../../logging'; +import { Terminal, env } from 'vscode'; +import { traceError, traceVerbose } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import '../extensions'; @@ -70,6 +70,7 @@ export class ShellDetector { // If we could not identify the shell, use the defaults. if (shell === undefined || shell === TerminalShellType.other) { + traceError('Unable to identify shell', env.shell, ' for OS ', this.platform.osType); traceVerbose('Using default OS shell'); shell = defaultOSShells[this.platform.osType]; } diff --git a/extensions/positron-python/src/client/common/types.ts b/extensions/positron-python/src/client/common/types.ts index 18b6e1481746..886d34a1914b 100644 --- a/extensions/positron-python/src/client/common/types.ts +++ b/extensions/positron-python/src/client/common/types.ts @@ -205,7 +205,7 @@ export interface IPythonSettings { } export interface ITensorBoardSettings { - readonly logDirectory: string | undefined; + logDirectory: string | undefined; } export interface ISortImportSettings { readonly path: string; @@ -479,6 +479,7 @@ export interface IInterpreterPathService { get(resource: Resource): string; inspect(resource: Resource): InspectInterpreterSettingType; update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise; + copyOldInterpreterStorageValuesToNew(resource: Resource): Promise; } export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.Node; diff --git a/extensions/positron-python/src/client/common/utils/localize.ts b/extensions/positron-python/src/client/common/utils/localize.ts index 5d1eb52514c7..9680a30a460b 100644 --- a/extensions/positron-python/src/client/common/utils/localize.ts +++ b/extensions/positron-python/src/client/common/utils/localize.ts @@ -3,630 +3,467 @@ 'use strict'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +import { l10n } from 'vscode'; /* eslint-disable @typescript-eslint/no-namespace, no-shadow */ // External callers of localize use these tables to retrieve localized values. export namespace Diagnostics { - export const warnSourceMaps = localize( - 'diagnostics.warnSourceMaps', + export const warnSourceMaps = l10n.t( 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.', ); - export const disableSourceMaps = localize('diagnostics.disableSourceMaps', 'Disable Source Map Support'); - export const warnBeforeEnablingSourceMaps = localize( - 'diagnostics.warnBeforeEnablingSourceMaps', + export const disableSourceMaps = l10n.t('Disable Source Map Support'); + + export const warnBeforeEnablingSourceMaps = l10n.t( 'Enabling source map support in the Python Extension will adversely impact performance of the extension.', ); - export const enableSourceMapsAndReloadVSC = localize( - 'diagnostics.enableSourceMapsAndReloadVSC', - 'Enable and reload Window.', - ); - export const lsNotSupported = localize( - 'diagnostics.lsNotSupported', + export const enableSourceMapsAndReloadVSC = l10n.t('Enable and reload Window.'); + export const lsNotSupported = l10n.t( 'Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.', ); - export const removedPythonPathFromSettings = localize( - 'diagnostics.removedPythonPathFromSettings', - 'The "python.pythonPath" setting in your settings.json is no longer used by the Python extension. If you want, you can use a new setting called "python.defaultInterpreterPath" instead. Keep in mind that you need to change the value of this setting manually as the Python extension doesn\'t modify it when you change interpreters. [Learn more](https://aka.ms/AA7jfor).', - ); - export const invalidPythonPathInDebuggerSettings = localize( - 'diagnostics.invalidPythonPathInDebuggerSettings', + export const invalidPythonPathInDebuggerSettings = l10n.t( 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Interpreter" in the status bar.', ); - export const invalidPythonPathInDebuggerLaunch = localize( - 'diagnostics.invalidPythonPathInDebuggerLaunch', - 'The Python path in your debug configuration is invalid.', - ); - export const invalidDebuggerTypeDiagnostic = localize( - 'diagnostics.invalidDebuggerTypeDiagnostic', + export const invalidPythonPathInDebuggerLaunch = l10n.t('The Python path in your debug configuration is invalid.'); + export const invalidDebuggerTypeDiagnostic = l10n.t( 'Your launch.json file needs to be updated to change the "pythonExperimental" debug configurations to use the "python" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?', ); - export const consoleTypeDiagnostic = localize( - 'diagnostics.consoleTypeDiagnostic', + export const consoleTypeDiagnostic = l10n.t( 'Your launch.json file needs to be updated to change the console type string from "none" to "internalConsole", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?', ); - export const justMyCodeDiagnostic = localize( - 'diagnostics.justMyCodeDiagnostic', + export const justMyCodeDiagnostic = l10n.t( 'Configuration "debugStdLib" in launch.json is no longer supported. It\'s recommended to replace it with "justMyCode", which is the exact opposite of using "debugStdLib". Would you like to automatically update your launch.json file to do that?', ); - export const yesUpdateLaunch = localize('diagnostics.yesUpdateLaunch', 'Yes, update launch.json'); - export const invalidTestSettings = localize( - 'diagnostics.invalidTestSettings', + export const yesUpdateLaunch = l10n.t('Yes, update launch.json'); + export const invalidTestSettings = l10n.t( 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?', ); - export const updateSettings = localize('diagnostics.updateSettings', 'Yes, update settings'); - export const checkIsort5UpgradeGuide = localize( - 'diagnostics.checkIsort5UpgradeGuide', + export const updateSettings = l10n.t('Yes, update settings'); + export const checkIsort5UpgradeGuide = l10n.t( 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.', ); - export const pylanceDefaultMessage = localize( - 'diagnostics.pylanceDefaultMessage', + export const pylanceDefaultMessage = l10n.t( "The Python extension now includes Pylance to improve completions, code navigation, overall performance and much more! You can learn more about the update and learn how to change your language server [here](https://aka.ms/new-python-bundle).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); } export namespace Common { - export const bannerLabelYes = localize('Common.bannerLabelYes', 'Yes'); - export const bannerLabelNo = localize('Common.bannerLabelNo', 'No'); - export const yesPlease = localize('Common.yesPlease', 'Yes, please'); - export const canceled = localize('Common.canceled', 'Canceled'); - export const cancel = localize('Common.cancel', 'Cancel'); - export const ok = localize('Common.ok', 'Ok'); - export const error = localize('Common.error', 'Error'); - export const gotIt = localize('Common.gotIt', 'Got it!'); - export const install = localize('Common.install', 'Install'); - export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); - export const openOutputPanel = localize('Common.openOutputPanel', 'Show output'); - export const noIWillDoItLater = localize('Common.noIWillDoItLater', 'No, I will do it later'); - export const notNow = localize('Common.notNow', 'Not now'); - export const doNotShowAgain = localize('Common.doNotShowAgain', 'Do not show again'); - export const reload = localize('Common.reload', 'Reload'); - export const moreInfo = localize('Common.moreInfo', 'More Info'); - export const learnMore = localize('Common.learnMore', 'Learn more'); - export const and = localize('Common.and', 'and'); - export const reportThisIssue = localize('Common.reportThisIssue', 'Report this issue'); - export const recommended = localize('Common.recommended', 'Recommended'); - export const clearAll = localize('Common.clearAll', 'Clear all'); - export const alwaysIgnore = localize('Common.alwaysIgnore', 'Always Ignore'); - export const ignore = localize('Common.ignore', 'Ignore'); - export const selectPythonInterpreter = localize('Common.selectPythonInterpreter', 'Select Python Interpreter'); - export const openLaunch = localize('Common.openLaunch', 'Open launch.json'); - export const useCommandPrompt = localize('Common.useCommandPrompt', 'Use Command Prompt'); - export const download = localize('Common.download', 'Download'); - export const showLogs = localize('Common.showLogs', 'Show logs'); + export const bannerLabelYes = l10n.t('Yes'); + export const bannerLabelNo = l10n.t('No'); + export const yesPlease = l10n.t('Yes, please'); + export const canceled = l10n.t('Canceled'); + export const cancel = l10n.t('Cancel'); + export const ok = l10n.t('Ok'); + export const error = l10n.t('Error'); + export const gotIt = l10n.t('Got it!'); + export const install = l10n.t('Install'); + export const loadingExtension = l10n.t('Python extension loading...'); + export const openOutputPanel = l10n.t('Show output'); + export const noIWillDoItLater = l10n.t('No, I will do it later'); + export const notNow = l10n.t('Not now'); + export const doNotShowAgain = l10n.t('Do not show again'); + export const reload = l10n.t('Reload'); + export const moreInfo = l10n.t('More Info'); + export const learnMore = l10n.t('Learn more'); + export const and = l10n.t('and'); + export const reportThisIssue = l10n.t('Report this issue'); + export const recommended = l10n.t('Recommended'); + export const clearAll = l10n.t('Clear all'); + export const alwaysIgnore = l10n.t('Always Ignore'); + export const ignore = l10n.t('Ignore'); + export const selectPythonInterpreter = l10n.t('Select Python Interpreter'); + export const openLaunch = l10n.t('Open launch.json'); + export const useCommandPrompt = l10n.t('Use Command Prompt'); + export const download = l10n.t('Download'); + export const showLogs = l10n.t('Show logs'); + export const openFolder = l10n.t('Open Folder...'); } export namespace CommonSurvey { - export const remindMeLaterLabel = localize('CommonSurvey.remindMeLaterLabel', 'Remind me later'); - export const yesLabel = localize('CommonSurvey.yesLabel', 'Yes, take survey now'); - export const noLabel = localize('CommonSurvey.noLabel', 'No, thanks'); + export const remindMeLaterLabel = l10n.t('Remind me later'); + export const yesLabel = l10n.t('Yes, take survey now'); + export const noLabel = l10n.t('No, thanks'); } export namespace AttachProcess { - export const attachTitle = localize('AttachProcess.attachTitle', 'Attach to process'); - export const selectProcessPlaceholder = localize( - 'AttachProcess.selectProcessPlaceholder', - 'Select the process to attach to', - ); - export const noProcessSelected = localize('AttachProcess.noProcessSelected', 'No process selected'); - export const refreshList = localize('AttachProcess.refreshList', 'Refresh process list'); + export const attachTitle = l10n.t('Attach to process'); + export const selectProcessPlaceholder = l10n.t('Select the process to attach to'); + export const noProcessSelected = l10n.t('No process selected'); + export const refreshList = l10n.t('Refresh process list'); } export namespace Pylance { - export const remindMeLater = localize('Pylance.remindMeLater', 'Remind me later'); + export const remindMeLater = l10n.t('Remind me later'); - export const pylanceNotInstalledMessage = localize( - 'Pylance.pylanceNotInstalledMessage', - 'Pylance extension is not installed.', - ); - export const pylanceInstalledReloadPromptMessage = localize( - 'Pylance.pylanceInstalledReloadPromptMessage', + export const pylanceNotInstalledMessage = l10n.t('Pylance extension is not installed.'); + export const pylanceInstalledReloadPromptMessage = l10n.t( 'Pylance extension is now installed. Reload window to activate?', ); - export const pylanceRevertToJediPrompt = localize( - 'Pylance.pylanceRevertToJediPrompt', + export const pylanceRevertToJediPrompt = l10n.t( 'The Pylance extension is not installed but the python.languageServer value is set to "Pylance". Would you like to install the Pylance extension to use Pylance, or revert back to Jedi?', ); - export const pylanceInstallPylance = localize('Pylance.pylanceInstallPylance', 'Install Pylance'); - export const pylanceRevertToJedi = localize('Pylance.pylanceRevertToJedi', 'Revert to Jedi'); + export const pylanceInstallPylance = l10n.t('Install Pylance'); + export const pylanceRevertToJedi = l10n.t('Revert to Jedi'); } export namespace TensorBoard { - export const enterRemoteUrl = localize('TensorBoard.enterRemoteUrl', 'Enter remote URL'); - export const enterRemoteUrlDetail = localize( - 'TensorBoard.enterRemoteUrlDetail', + export const enterRemoteUrl = l10n.t('Enter remote URL'); + export const enterRemoteUrlDetail = l10n.t( 'Enter a URL pointing to a remote directory containing your TensorBoard log files', ); - export const useCurrentWorkingDirectoryDetail = localize( - 'TensorBoard.useCurrentWorkingDirectoryDetail', + export const useCurrentWorkingDirectoryDetail = l10n.t( 'TensorBoard will search for tfevent files in all subdirectories of the current working directory', ); - export const useCurrentWorkingDirectory = localize( - 'TensorBoard.useCurrentWorkingDirectory', - 'Use current working directory', - ); - export const logDirectoryPrompt = localize( - 'TensorBoard.logDirectoryPrompt', - 'Select a log directory to start TensorBoard with', - ); - export const progressMessage = localize('TensorBoard.progressMessage', 'Starting TensorBoard session...'); - export const nativeTensorBoardPrompt = localize( - 'TensorBoard.nativeTensorBoardPrompt', + export const useCurrentWorkingDirectory = l10n.t('Use current working directory'); + export const logDirectoryPrompt = l10n.t('Select a log directory to start TensorBoard with'); + export const progressMessage = l10n.t('Starting TensorBoard session...'); + export const nativeTensorBoardPrompt = l10n.t( 'VS Code now has integrated TensorBoard support. Would you like to launch TensorBoard? (Tip: Launch TensorBoard anytime by opening the command palette and searching for "Launch TensorBoard".)', ); - export const selectAFolder = localize('TensorBoard.selectAFolder', 'Select a folder'); - export const selectAFolderDetail = localize( - 'TensorBoard.selectAFolderDetail', - 'Select a log directory containing tfevent files', - ); - export const selectAnotherFolder = localize('TensorBoard.selectAnotherFolder', 'Select another folder'); - export const selectAnotherFolderDetail = localize( - 'TensorBoard.selectAnotherFolderDetail', - 'Use the file explorer to select another folder', - ); - export const installPrompt = localize( - 'TensorBoard.installPrompt', + export const selectAFolder = l10n.t('Select a folder'); + export const selectAFolderDetail = l10n.t('Select a log directory containing tfevent files'); + export const selectAnotherFolder = l10n.t('Select another folder'); + export const selectAnotherFolderDetail = l10n.t('Use the file explorer to select another folder'); + export const installPrompt = l10n.t( 'The package TensorBoard is required to launch a TensorBoard session. Would you like to install it?', ); - export const installTensorBoardAndProfilerPluginPrompt = localize( - 'TensorBoard.installTensorBoardAndProfilerPluginPrompt', + export const installTensorBoardAndProfilerPluginPrompt = l10n.t( 'TensorBoard >= 2.4.1 and the PyTorch Profiler TensorBoard plugin >= 0.2.0 are required. Would you like to install these packages?', ); - export const installProfilerPluginPrompt = localize( - 'TensorBoard.installProfilerPluginPrompt', + export const installProfilerPluginPrompt = l10n.t( 'We recommend installing version >= 0.2.0 of the PyTorch Profiler TensorBoard plugin. Would you like to install the package?', ); - export const upgradePrompt = localize( - 'TensorBoard.upgradePrompt', + export const upgradePrompt = l10n.t( 'Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?', ); - export const launchNativeTensorBoardSessionCodeLens = localize( - 'TensorBoard.launchNativeTensorBoardSessionCodeLens', - '▶ Launch TensorBoard Session', - ); - export const launchNativeTensorBoardSessionCodeAction = localize( - 'TensorBoard.launchNativeTensorBoardSessionCodeAction', - 'Launch TensorBoard session', - ); - export const missingSourceFile = localize( - 'TensorBoard.missingSourceFile', + export const launchNativeTensorBoardSessionCodeLens = l10n.t('▶ Launch TensorBoard Session'); + export const launchNativeTensorBoardSessionCodeAction = l10n.t('Launch TensorBoard session'); + export const missingSourceFile = l10n.t( 'We could not locate the requested source file on disk. Please manually specify the file.', ); - export const selectMissingSourceFile = localize('TensorBoard.selectMissingSourceFile', 'Choose File'); - export const selectMissingSourceFileDescription = localize( - 'TensorBoard.selectMissingSourceFileDescription', + export const selectMissingSourceFile = l10n.t('Choose File'); + export const selectMissingSourceFileDescription = l10n.t( "The source file's contents may not match the original contents in the trace.", ); } export namespace LanguageService { export const virtualWorkspaceStatusItem = { - detail: localize( - 'LanguageService.virtualWorkspaceStatusItem.detail', - 'Limited IntelliSense supported by Jedi and Pylance', - ), + detail: l10n.t('Limited IntelliSense supported by Jedi and Pylance'), }; export const statusItem = { - name: localize('LanguageService.statusItem.name', 'Python IntelliSense Status'), - text: localize('LanguageService.statusItem.text', 'Partial Mode'), - detail: localize('LanguageService.statusItem.detail', 'Limited IntelliSense provided by Pylance'), + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), }; - export const startingPylance = localize('LanguageService.startingPylance', 'Starting Pylance language server.'); - export const startingNone = localize( - 'LanguageService.startingNone', - 'Editor support is inactive since language server is set to None.', - ); - export const untrustedWorkspaceMessage = localize( - 'LanguageService.untrustedWorkspaceMessage', + export const startingPylance = l10n.t('Starting Pylance language server.'); + export const startingNone = l10n.t('Editor support is inactive since language server is set to None.'); + export const untrustedWorkspaceMessage = l10n.t( 'Only Pylance is supported in untrusted workspaces, setting language server to None.', ); - export const reloadAfterLanguageServerChange = localize( - 'LanguageService.reloadAfterLanguageServerChange', + export const reloadAfterLanguageServerChange = l10n.t( 'Please reload the window switching between language servers.', ); - export const lsFailedToStart = localize( - 'LanguageService.lsFailedToStart', + export const lsFailedToStart = l10n.t( 'We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - export const lsFailedToDownload = localize( - 'LanguageService.lsFailedToDownload', + export const lsFailedToDownload = l10n.t( 'We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - export const lsFailedToExtract = localize( - 'LanguageService.lsFailedToExtract', + export const lsFailedToExtract = l10n.t( 'We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.', ); - export const downloadFailedOutputMessage = localize( - 'LanguageService.downloadFailedOutputMessage', - 'Language server download failed.', - ); - export const extractionFailedOutputMessage = localize( - 'LanguageService.extractionFailedOutputMessage', - 'Language server extraction failed.', - ); - export const extractionCompletedOutputMessage = localize( - 'LanguageService.extractionCompletedOutputMessage', - 'Language server download complete.', - ); - export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); - export const reloadVSCodeIfSeachPathHasChanged = localize( - 'LanguageService.reloadVSCodeIfSeachPathHasChanged', + export const downloadFailedOutputMessage = l10n.t('Language server download failed.'); + export const extractionFailedOutputMessage = l10n.t('Language server extraction failed.'); + export const extractionCompletedOutputMessage = l10n.t('Language server download complete.'); + export const extractionDoneOutputMessage = l10n.t('done.'); + export const reloadVSCodeIfSeachPathHasChanged = l10n.t( 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.', ); } export namespace Interpreters { - export const installingPython = localize('Interpreters.installingPython', 'Installing Python into Environment...'); - export const discovering = localize('Interpreters.DiscoveringInterpreters', 'Discovering Python Interpreters'); - export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); - export const condaInheritEnvMessage = localize( - 'Interpreters.condaInheritEnvMessage', + export const installingPython = l10n.t('Installing Python into Environment...'); + export const discovering = l10n.t('Discovering Python Interpreters'); + export const refreshing = l10n.t('Refreshing Python Interpreters'); + export const condaInheritEnvMessage = l10n.t( 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.', ); - export const environmentPromptMessage = localize( - 'Interpreters.environmentPromptMessage', + export const activatedCondaEnvLaunch = l10n.t( + 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', + ); + export const environmentPromptMessage = l10n.t( 'We noticed a new environment has been created. Do you want to select it for the workspace folder?', ); - export const entireWorkspace = localize('Interpreters.entireWorkspace', 'Select at workspace level'); - export const clearAtWorkspace = localize('Interpreters.clearAtWorkspace', 'Clear at workspace level'); - export const selectInterpreterTip = localize( - 'Interpreters.selectInterpreterTip', + export const entireWorkspace = l10n.t('Select at workspace level'); + export const clearAtWorkspace = l10n.t('Clear at workspace level'); + export const selectInterpreterTip = l10n.t( 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar', ); - export const installPythonTerminalMessage = localize( - 'Interpreters.installPythonTerminalMessage', + export const installPythonTerminalMessage = l10n.t( '💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); + export const changePythonInterpreter = l10n.t('Change Python Interpreter'); + export const selectedPythonInterpreter = l10n.t('Selected Python Interpreter'); } export namespace InterpreterQuickPickList { - export const noPythonInstalled = localize( - 'InterpreterQuickPickList.noPythonInstalled', - 'Python is not installed, please download and install it', - ); - export const clickForInstructions = localize( - 'InterpreterQuickPickList.clickForInstructions', - 'Click for instructions...', - ); - export const globalGroupName = localize('InterpreterQuickPickList.globalGroupName', 'Global'); - export const workspaceGroupName = localize('InterpreterQuickPickList.workspaceGroupName', 'Workspace'); + export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); + export const clickForInstructions = l10n.t('Click for instructions...'); + export const globalGroupName = l10n.t('Global'); + export const workspaceGroupName = l10n.t('Workspace'); export const enterPath = { - label: localize('InterpreterQuickPickList.enterPath.label', 'Enter interpreter path...'), - placeholder: localize('InterpreterQuickPickList.enterPath.placeholder', 'Enter path to a Python interpreter.'), + label: l10n.t('Enter interpreter path...'), + placeholder: l10n.t('Enter path to a Python interpreter.'), }; export const defaultInterpreterPath = { - label: localize( - 'InterpreterQuickPickList.defaultInterpreterPath.label', - 'Use Python from `python.defaultInterpreterPath` setting', - ), + label: l10n.t('Use Python from `python.defaultInterpreterPath` setting'), }; export const browsePath = { - label: localize('InterpreterQuickPickList.browsePath.label', 'Find...'), - detail: localize( - 'InterpreterQuickPickList.browsePath.detail', - 'Browse your file system to find a Python interpreter.', - ), - openButtonLabel: localize('python.command.python.setInterpreter.title', 'Select Interpreter'), - title: localize('InterpreterQuickPickList.browsePath.title', 'Select Python interpreter'), + label: l10n.t('Find...'), + detail: l10n.t('Browse your file system to find a Python interpreter.'), + openButtonLabel: l10n.t('Select Interpreter'), + title: l10n.t('Select Python interpreter'), }; - export const refreshInterpreterList = localize( - 'InterpreterQuickPickList.refreshInterpreterList', - 'Refresh Interpreter list', - ); - export const refreshingInterpreterList = localize( - 'InterpreterQuickPickList.refreshingInterpreterList', - 'Refreshing Interpreter list...', - ); + export const refreshInterpreterList = l10n.t('Refresh Interpreter list'); + export const refreshingInterpreterList = l10n.t('Refreshing Interpreter list...'); } export namespace OutputChannelNames { - export const languageServer = localize('OutputChannelNames.languageServer', 'Python Language Server'); - export const python = localize('OutputChannelNames.python', 'Python'); - export const pythonTest = localize('OutputChannelNames.pythonTest', 'Python Test Log'); + export const languageServer = l10n.t('Python Language Server'); + export const python = l10n.t('Python'); + export const pythonTest = l10n.t('Python Test Log'); } export namespace Logging { - export const currentWorkingDirectory = localize('Logging.CurrentWorkingDirectory', 'cwd:'); + export const currentWorkingDirectory = l10n.t('cwd:'); } export namespace Linters { - export const selectLinter = localize('Linter.selectLinter', 'Select Linter'); + export const selectLinter = l10n.t('Select Linter'); } export namespace Installer { - export const noCondaOrPipInstaller = localize( - 'Installer.noCondaOrPipInstaller', + export const noCondaOrPipInstaller = l10n.t( 'There is no Conda or Pip installer available in the selected environment.', ); - export const noPipInstaller = localize( - 'Installer.noPipInstaller', - 'There is no Pip installer available in the selected environment.', - ); - export const searchForHelp = localize('Installer.searchForHelp', 'Search for help'); + export const noPipInstaller = l10n.t('There is no Pip installer available in the selected environment.'); + export const searchForHelp = l10n.t('Search for help'); } export namespace ExtensionSurveyBanner { - export const bannerMessage = localize( - 'ExtensionSurveyBanner.bannerMessage', + export const bannerMessage = l10n.t( 'Can you please take 2 minutes to tell us how the Python extension is working for you?', ); - export const bannerLabelYes = localize('ExtensionSurveyBanner.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('ExtensionSurveyBanner.bannerLabelNo', 'No, thanks'); - export const maybeLater = localize('ExtensionSurveyBanner.maybeLater', 'Maybe later'); + export const bannerLabelYes = l10n.t('Yes, take survey now'); + export const bannerLabelNo = l10n.t('No, thanks'); + export const maybeLater = l10n.t('Maybe later'); } export namespace DebugConfigStrings { export const selectConfiguration = { - title: localize('debug.selectConfigurationTitle', 'Select a debug configuration'), - placeholder: localize('debug.selectConfigurationPlaceholder', 'Debug Configuration'), + title: l10n.t('Select a debug configuration'), + placeholder: l10n.t('Debug Configuration'), }; export const launchJsonCompletions = { - label: localize('debug.launchJsonConfigurationsCompletionLabel', 'Python'), - description: localize( - 'debug.launchJsonConfigurationsCompletionDescription', - 'Select a Python debug configuration', - ), + label: l10n.t('Python'), + description: l10n.t('Select a Python debug configuration'), }; export namespace file { export const snippet = { - name: localize('python.snippet.launch.standard.label', 'Python: Current File'), + name: l10n.t('Python: Current File'), }; export const selectConfiguration = { - label: localize('debug.debugFileConfigurationLabel', 'Python File'), - description: localize('debug.debugFileConfigurationDescription', 'Debug the currently active Python file'), + label: l10n.t('Python File'), + description: l10n.t('Debug the currently active Python file'), }; } export namespace module { export const snippet = { - name: localize('python.snippet.launch.module.label', 'Python: Module'), - default: localize('python.snippet.launch.module.default', 'enter-your-module-name'), + name: l10n.t('Python: Module'), + default: l10n.t('enter-your-module-name'), }; export const selectConfiguration = { - label: localize('debug.debugModuleConfigurationLabel', 'Module'), - description: localize( - 'debug.debugModuleConfigurationDescription', - "Debug a Python module by invoking it with '-m'", - ), + label: l10n.t('Module'), + description: l10n.t("Debug a Python module by invoking it with '-m'"), }; export const enterModule = { - title: localize('debug.moduleEnterModuleTitle', 'Debug Module'), - prompt: localize('debug.moduleEnterModulePrompt', 'Enter a Python module/package name'), - default: localize('debug.moduleEnterModuleDefault', 'enter-your-module-name'), - invalid: localize('debug.moduleEnterModuleInvalidNameError', 'Enter a valid module name'), + title: l10n.t('Debug Module'), + prompt: l10n.t('Enter a Python module/package name'), + default: l10n.t('enter-your-module-name'), + invalid: l10n.t('Enter a valid module name'), }; } export namespace attach { export const snippet = { - name: localize('python.snippet.launch.attach.label', 'Python: Remote Attach'), + name: l10n.t('Python: Remote Attach'), }; export const selectConfiguration = { - label: localize('debug.remoteAttachConfigurationLabel', 'Remote Attach'), - description: localize('debug.remoteAttachConfigurationDescription', 'Attach to a remote debug server'), + label: l10n.t('Remote Attach'), + description: l10n.t('Attach to a remote debug server'), }; export const enterRemoteHost = { - title: localize('debug.attachRemoteHostTitle', 'Remote Debugging'), - prompt: localize('debug.attachRemoteHostPrompt', 'Enter a valid host name or IP address'), - invalid: localize('debug.attachRemoteHostValidationError', 'Enter a valid host name or IP address'), + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter a valid host name or IP address'), + invalid: l10n.t('Enter a valid host name or IP address'), }; export const enterRemotePort = { - title: localize('debug.attachRemotePortTitle', 'Remote Debugging'), - prompt: localize( - 'debug.attachRemotePortPrompt', - 'Enter the port number that the debug server is listening on', - ), - invalid: localize('debug.attachRemotePortValidationError', 'Enter a valid port number'), + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter the port number that the debug server is listening on'), + invalid: l10n.t('Enter a valid port number'), }; } export namespace attachPid { export const snippet = { - name: localize('python.snippet.launch.attachpid.label', 'Python: Attach using Process Id'), + name: l10n.t('Python: Attach using Process Id'), }; export const selectConfiguration = { - label: localize('debug.attachPidConfigurationLabel', 'Attach using Process ID'), - description: localize('debug.attachPidConfigurationDescription', 'Attach to a local process'), + label: l10n.t('Attach using Process ID'), + description: l10n.t('Attach to a local process'), }; } export namespace django { export const snippet = { - name: localize('python.snippet.launch.django.label', 'Python: Django'), + name: l10n.t('Python: Django'), }; export const selectConfiguration = { - label: localize('debug.debugDjangoConfigurationLabel', 'Django'), - description: localize( - 'debug.debugDjangoConfigurationDescription', - 'Launch and debug a Django web application', - ), + label: l10n.t('Django'), + description: l10n.t('Launch and debug a Django web application'), }; export const enterManagePyPath = { - title: localize('debug.djangoEnterManagePyPathTitle', 'Debug Django'), - prompt: localize( - 'debug.djangoEnterManagePyPathPrompt', + title: l10n.t('Debug Django'), + prompt: l10n.t( "Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)", ), - invalid: localize('debug.djangoEnterManagePyPathInvalidFilePathError', 'Enter a valid Python file path'), + invalid: l10n.t('Enter a valid Python file path'), }; } export namespace fastapi { export const snippet = { - name: localize('python.snippet.launch.fastapi.label', 'Python: FastAPI'), + name: l10n.t('Python: FastAPI'), }; export const selectConfiguration = { - label: localize('debug.debugFastAPIConfigurationLabel', 'FastAPI'), - description: localize( - 'debug.debugFastAPIConfigurationDescription', - 'Launch and debug a FastAPI web application', - ), + label: l10n.t('FastAPI'), + description: l10n.t('Launch and debug a FastAPI web application'), }; export const enterAppPathOrNamePath = { - title: localize('debug.fastapiEnterAppPathOrNamePathTitle', 'Debug FastAPI'), - prompt: localize( - 'debug.fastapiEnterAppPathOrNamePathPrompt', - "Enter the path to the application, e.g. 'main.py' or 'main'", - ), - invalid: localize('debug.fastapiEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'), + title: l10n.t('Debug FastAPI'), + prompt: l10n.t("Enter the path to the application, e.g. 'main.py' or 'main'"), + invalid: l10n.t('Enter a valid name'), }; } export namespace flask { export const snippet = { - name: localize('python.snippet.launch.flask.label', 'Python: Flask'), + name: l10n.t('Python: Flask'), }; export const selectConfiguration = { - label: localize('debug.debugFlaskConfigurationLabel', 'Flask'), - description: localize( - 'debug.debugFlaskConfigurationDescription', - 'Launch and debug a Flask web application', - ), + label: l10n.t('Flask'), + description: l10n.t('Launch and debug a Flask web application'), }; export const enterAppPathOrNamePath = { - title: localize('debug.flaskEnterAppPathOrNamePathTitle', 'Debug Flask'), - prompt: localize('debug.flaskEnterAppPathOrNamePathPrompt', 'Python: Flask'), - invalid: localize('debug.flaskEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'), + title: l10n.t('Debug Flask'), + prompt: l10n.t('Python: Flask'), + invalid: l10n.t('Enter a valid name'), }; } export namespace pyramid { export const snippet = { - name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application'), + name: l10n.t('Python: Pyramid Application'), }; export const selectConfiguration = { - label: localize('debug.debugPyramidConfigurationLabel', 'Pyramid'), - description: localize( - 'debug.debugPyramidConfigurationDescription', - 'Launch and debug a Pyramid web application', - ), + label: l10n.t('Pyramid'), + description: l10n.t('Launch and debug a Pyramid web application'), }; export const enterDevelopmentIniPath = { - title: localize('debug.pyramidEnterDevelopmentIniPathTitle', 'Debug Pyramid'), - invalid: localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError', 'Enter a valid file path'), + title: l10n.t('Debug Pyramid'), + invalid: l10n.t('Enter a valid file path'), }; } } export namespace Testing { - export const configureTests = localize('Testing.configureTests', 'Configure Test Framework'); - export const testNotConfigured = localize('Testing.testNotConfigured', 'No test framework configured.'); - export const cancelUnittestDiscovery = localize( - 'Testing.cancelUnittestDiscovery', - 'Canceled unittest test discovery', - ); - export const errorUnittestDiscovery = localize('Testing.errorUnittestDiscovery', 'Unittest test discovery error'); - export const seePythonOutput = localize('Testing.seePythonOutput', '(see Output > Python)'); - export const cancelUnittestExecution = localize( - 'Testing.cancelUnittestExecution', - 'Canceled unittest test execution', - ); - export const errorUnittestExecution = localize('Testing.errorUnittestExecution', 'Unittest test execution error'); + export const configureTests = l10n.t('Configure Test Framework'); + export const testNotConfigured = l10n.t('No test framework configured.'); + export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); + export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); + export const seePythonOutput = l10n.t('(see Output > Python)'); + export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); + export const errorUnittestExecution = l10n.t('Unittest test execution error'); } export namespace OutdatedDebugger { - export const outdatedDebuggerMessage = localize( - 'OutdatedDebugger.updateDebuggerMessage', + export const outdatedDebuggerMessage = l10n.t( 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).', ); } export namespace Python27Support { - export const jediMessage = localize( - 'Python27Support.jediMessage', + export const jediMessage = l10n.t( 'IntelliSense with Jedi for Python 2.7 is no longer supported. [Learn more](https://aka.ms/python-27-support).', ); } export namespace SwitchToDefaultLS { - export const bannerMessage = localize( - 'SwitchToDefaultLS.bannerMessage', + export const bannerMessage = l10n.t( "The Microsoft Python Language Server has reached end of life. Your language server has been set to the default for Python in VS Code, Pylance.\n\nIf you'd like to change your language server, you can learn about how to do so [here](https://devblogs.microsoft.com/python/python-in-visual-studio-code-may-2021-release/#configuring-your-language-server).\n\nRead Pylance's license [here](https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license).", ); } export namespace CreateEnv { - export const informEnvCreation = localize( - 'createEnv.informEnvCreation', - 'We have selected the following environment:', - ); - export const statusTitle = localize('createEnv.statusTitle', 'Creating environment'); - export const statusStarting = localize('createEnv.statusStarting', 'Starting...'); + export const informEnvCreation = l10n.t('We have selected the following environment:'); + export const statusTitle = l10n.t('Creating environment'); + export const statusStarting = l10n.t('Starting...'); - export const hasVirtualEnv = localize('createEnv.hasVirtualEnv', 'Workspace folder contains a virtual environment'); + export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); - export const noWorkspace = localize( - 'createEnv.noWorkspace', - 'Please open a directory when creating an environment using venv.', - ); + export const noWorkspace = l10n.t('Please open a folder when creating an environment using venv.'); - export const pickWorkspacePlaceholder = localize( - 'createEnv.workspaceQuickPick.placeholder', - 'Select a workspace to create environment', - ); + export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); - export const providersQuickPickPlaceholder = localize( - 'createEnv.providersQuickPick.placeholder', - 'Select an environment type', - ); + export const providersQuickPickPlaceholder = l10n.t('Select an environment type'); export namespace Venv { - export const creating = localize('createEnv.venv.creating', 'Creating venv...'); - export const created = localize('createEnv.venv.created', 'Environment created...'); - export const installingPackages = localize('createEnv.venv.installingPackages', 'Installing packages...'); - export const errorCreatingEnvironment = localize( - 'createEnv.venv.errorCreatingEnvironment', - 'Error while creating virtual environment.', - ); - export const selectPythonQuickPickTitle = localize( - 'createEnv.venv.basePython.title', - 'Select a python to use for environment creation', - ); - export const providerDescription = localize( - 'createEnv.venv.description', - 'Creates a `.venv` virtual environment in the current workspace', - ); - export const error = localize('createEnv.venv.error', 'Creating virtual environment failed with error.'); + export const creating = l10n.t('Creating venv...'); + export const created = l10n.t('Environment created...'); + export const installingPackages = l10n.t('Installing packages...'); + export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.'); + export const selectPythonQuickPickTitle = l10n.t('Select a python to use for environment creation'); + export const providerDescription = l10n.t('Creates a `.venv` virtual environment in the current workspace'); + export const error = l10n.t('Creating virtual environment failed with error.'); + export const tomlExtrasQuickPickTitle = l10n.t('Select optional dependencies to install from pyproject.toml'); + export const requirementsQuickPickTitle = l10n.t('Select dependencies to install'); } export namespace Conda { - export const condaMissing = localize( - 'createEnv.conda.missing', - 'Please install `conda` to create conda environments.', - ); - export const created = localize('createEnv.conda.created', 'Environment created...'); - export const installingPackages = localize('createEnv.conda.installingPackages', 'Installing packages...'); - export const errorCreatingEnvironment = localize( - 'createEnv.conda.errorCreatingEnvironment', - 'Error while creating conda environment.', - ); - export const selectPythonQuickPickPlaceholder = localize( - 'createEnv.conda.pythonSelection.placeholder', + export const condaMissing = l10n.t('Please install `conda` to create conda environments.'); + export const created = l10n.t('Environment created...'); + export const installingPackages = l10n.t('Installing packages...'); + export const errorCreatingEnvironment = l10n.t('Error while creating conda environment.'); + export const selectPythonQuickPickPlaceholder = l10n.t( 'Please select the version of Python to install in the environment', ); - export const creating = localize('createEnv.conda.creating', 'Creating conda environment...'); - export const providerDescription = localize( - 'createEnv.conda.description', - 'Creates a `.conda` Conda environment in the current workspace', - ); + export const creating = l10n.t('Creating conda environment...'); + export const providerDescription = l10n.t('Creates a `.conda` Conda environment in the current workspace'); } } export namespace ToolsExtensions { - export const flake8PromptMessage = localize( - 'toolsExt.flake8.message', + export const flake8PromptMessage = l10n.t( 'Use the Flake8 extension to enable easier configuration and new features such as quick fixes.', ); - export const pylintPromptMessage = localize( - 'toolsExt.pylint.message', + export const pylintPromptMessage = l10n.t( 'Use the Pylint extension to enable easier configuration and new features such as quick fixes.', ); - export const installPylintExtension = localize('toolsExt.install.pylint', 'Install Pylint extension'); - export const installFlake8Extension = localize('toolsExt.install.flake8', 'Install Flake8 extension'); + export const installPylintExtension = l10n.t('Install Pylint extension'); + export const installFlake8Extension = l10n.t('Install Flake8 extension'); } diff --git a/extensions/positron-python/src/client/common/variables/environmentVariablesProvider.ts b/extensions/positron-python/src/client/common/variables/environmentVariablesProvider.ts index 0cfd6687d0c7..2524aac21017 100644 --- a/extensions/positron-python/src/client/common/variables/environmentVariablesProvider.ts +++ b/extensions/positron-python/src/client/common/variables/environmentVariablesProvider.ts @@ -2,9 +2,9 @@ // Licensed under the MIT License. import { inject, injectable, optional } from 'inversify'; -import path from 'path'; +import * as path from 'path'; import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { sendFileCreationTelemetry } from '../../telemetry/envFileTelemetry'; import { IWorkspaceService } from '../application/types'; import { PythonSettings } from '../configSettings'; @@ -61,6 +61,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } const vars = await this._getEnvironmentVariables(resource); this.setCachedEnvironmentVariables(resource, vars); + traceVerbose('Dump environment variables', JSON.stringify(vars, null, 4)); return vars; } @@ -136,7 +137,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid const workspaceFolderUri = this.getWorkspaceFolderUri(resource); const envFileSetting = this.workspaceService.getConfiguration('python', resource).get('envFile'); const envFile = systemVariables.resolveAny(envFileSetting); - if (!envFile) { + if (envFile === undefined) { traceError('Unable to read `python.envFile` setting for resource', JSON.stringify(resource)); return workspaceFolderUri?.fsPath ? path.join(workspaceFolderUri?.fsPath, '.env') : ''; } diff --git a/extensions/positron-python/src/client/common/vscodeApis/browserApis.ts b/extensions/positron-python/src/client/common/vscodeApis/browserApis.ts new file mode 100644 index 000000000000..ccf51bd07ec8 --- /dev/null +++ b/extensions/positron-python/src/client/common/vscodeApis/browserApis.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { env, Uri } from 'vscode'; + +export function launch(url: string): void { + env.openExternal(Uri.parse(url)); +} diff --git a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts index 07a4b4c4acc6..be2c362a4b45 100644 --- a/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts +++ b/extensions/positron-python/src/client/common/vscodeApis/windowApis.ts @@ -9,6 +9,7 @@ import { ProgressOptions, QuickPickItem, QuickPickOptions, + TextEditor, window, } from 'vscode'; @@ -61,3 +62,8 @@ export function withProgress( ): Thenable { return window.withProgress(options, task); } + +export function getActiveTextEditor(): TextEditor | undefined { + const { activeTextEditor } = window; + return activeTextEditor; +} diff --git a/extensions/positron-python/src/client/common/vscodeApis/workspaceApis.ts b/extensions/positron-python/src/client/common/vscodeApis/workspaceApis.ts index 5172cc1593ce..fda05e2477af 100644 --- a/extensions/positron-python/src/client/common/vscodeApis/workspaceApis.ts +++ b/extensions/positron-python/src/client/common/vscodeApis/workspaceApis.ts @@ -1,7 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationScope, workspace, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { + CancellationToken, + ConfigurationScope, + GlobPattern, + Uri, + workspace, + WorkspaceConfiguration, + WorkspaceEdit, + WorkspaceFolder, +} from 'vscode'; import { Resource } from '../types'; export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined { @@ -19,3 +28,16 @@ export function getWorkspaceFolderPaths(): string[] { export function getConfiguration(section?: string, scope?: ConfigurationScope | null): WorkspaceConfiguration { return workspace.getConfiguration(section, scope); } + +export function applyEdit(edit: WorkspaceEdit): Thenable { + return workspace.applyEdit(edit); +} + +export function findFiles( + include: GlobPattern, + exclude?: GlobPattern | null, + maxResults?: number, + token?: CancellationToken, +): Thenable { + return workspace.findFiles(include, exclude, maxResults, token); +} diff --git a/extensions/positron-python/src/client/debugger/extension/adapter/factory.ts b/extensions/positron-python/src/client/debugger/extension/adapter/factory.ts index 5a228ca289ff..fc9232729eae 100644 --- a/extensions/positron-python/src/client/debugger/extension/adapter/factory.ts +++ b/extensions/positron-python/src/client/debugger/extension/adapter/factory.ts @@ -10,9 +10,9 @@ import { DebugAdapterExecutable, DebugAdapterServer, DebugSession, + l10n, WorkspaceFolder, } from 'vscode'; -import { IApplicationShell } from '../../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { IInterpreterService } from '../../../interpreter/contracts'; import { traceLog, traceVerbose } from '../../../logging'; @@ -21,15 +21,23 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; import { IDebugAdapterDescriptorFactory } from '../types'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { IPersistentStateFactory } from '../../../common/types'; +import { Commands } from '../../../common/constants'; +import { ICommandManager } from '../../../common/application/types'; + +// persistent state names, exported to make use of in testing +export enum debugStateKeys { + doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain', +} @injectable() export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, ) {} public async createDebugAdapterDescriptor( @@ -145,8 +153,39 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac return this.getExecutableCommand(interpreters[0]); } + private async showDeprecatedPythonMessage() { + const notificationPromptEnabled = this.persistentState.createGlobalPersistentState( + debugStateKeys.doNotShowAgain, + false, + ); + if (notificationPromptEnabled.value) { + return; + } + const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain]; + const selection = await showErrorMessage( + l10n.t('The debugger in the python extension no longer supports python versions minor than 3.7.'), + { modal: true }, + ...prompts, + ); + if (!selection) { + return; + } + if (selection == Interpreters.changePythonInterpreter) { + await this.commandManager.executeCommand(Commands.Set_Interpreter); + } + if (selection == Common.doNotShowAgain) { + // Never show the message again + await this.persistentState + .createGlobalPersistentState(debugStateKeys.doNotShowAgain, false) + .updateValue(true); + } + } + private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { + if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) { + this.showDeprecatedPythonMessage(); + } return interpreter.path.length > 0 ? [interpreter.path] : []; } return []; @@ -161,8 +200,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac * @memberof DebugAdapterDescriptorFactory */ private async notifySelectInterpreter() { - await this.appShell.showErrorMessage( - localize('interpreterError', 'Please install Python or select a Python Interpreter to use the debugger.'), - ); + await showErrorMessage(l10n.t('Please install Python or select a Python Interpreter to use the debugger.')); } } diff --git a/extensions/positron-python/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/extensions/positron-python/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts index c86f3a9ef206..04117e9838d1 100644 --- a/extensions/positron-python/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +++ b/extensions/positron-python/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -2,34 +2,27 @@ // Licensed under the MIT License. 'use strict'; - -import { inject, injectable } from 'inversify'; +import { injectable } from 'inversify'; import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { IApplicationShell } from '../../../common/application/types'; -import { IBrowserService } from '../../../common/types'; import { Common, OutdatedDebugger } from '../../../common/utils/localize'; +import { launch } from '../../../common/vscodeApis/browserApis'; +import { showInformationMessage } from '../../../common/vscodeApis/windowApis'; import { IPromptShowState } from './types'; // This situation occurs when user connects to old containers or server where // the debugger they had installed was ptvsd. We should show a prompt to ask them to update. class OutdatedDebuggerPrompt implements DebugAdapterTracker { - constructor( - private promptCheck: IPromptShowState, - private appShell: IApplicationShell, - private browserService: IBrowserService, - ) {} + constructor(private promptCheck: IPromptShowState) {} public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { const prompts = [Common.moreInfo]; - this.appShell - .showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts) - .then((selection) => { - if (selection === prompts[0]) { - this.browserService.launch('https://aka.ms/migrateToDebugpy'); - } - }); + showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts).then((selection) => { + if (selection === prompts[0]) { + launch('https://aka.ms/migrateToDebugpy'); + } + }); } } @@ -71,13 +64,10 @@ class OutdatedDebuggerPromptState implements IPromptShowState { @injectable() export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { private readonly promptCheck: OutdatedDebuggerPromptState; - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IBrowserService) private browserService: IBrowserService, - ) { + constructor() { this.promptCheck = new OutdatedDebuggerPromptState(); } public createDebugAdapterTracker(_session: DebugSession): ProviderResult { - return new OutdatedDebuggerPrompt(this.promptCheck, this.appShell, this.browserService); + return new OutdatedDebuggerPrompt(this.promptCheck); } } diff --git a/extensions/positron-python/src/client/debugger/extension/attachQuickPick/provider.ts b/extensions/positron-python/src/client/debugger/extension/attachQuickPick/provider.ts index 26bb80067f93..3626d8dfb8ce 100644 --- a/extensions/positron-python/src/client/debugger/extension/attachQuickPick/provider.ts +++ b/extensions/positron-python/src/client/debugger/extension/attachQuickPick/provider.ts @@ -4,15 +4,13 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import * as nls from 'vscode-nls'; +import { l10n } from 'vscode'; import { IPlatformService } from '../../../common/platform/types'; import { IProcessServiceFactory } from '../../../common/process/types'; import { PsProcessParser } from './psProcessParser'; import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; import { WmicProcessParser } from './wmicProcessParser'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - @injectable() export class AttachProcessProvider implements IAttachProcessProvider { constructor( @@ -71,13 +69,7 @@ export class AttachProcessProvider implements IAttachProcessProvider { } else if (this.platformService.isWindows) { processCmd = WmicProcessParser.wmicCommand; } else { - throw new Error( - localize( - 'AttachProcess.unsupportedOS', - "Operating system '{0}' not supported.", - this.platformService.osType, - ), - ); + throw new Error(l10n.t("Operating system '{0}' not supported.", this.platformService.osType)); } const processService = await this.processServiceFactory.create(); diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts index c5413b3662e0..ea0c84f7c3de 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -28,6 +28,7 @@ import { IDebugConfigurationResolver } from './types'; @injectable() export class PythonDebugConfigurationService implements IDebugConfigurationService { private cacheDebugConfig: DebugConfiguration | undefined = undefined; + constructor( @inject(IDebugConfigurationResolver) @named('attach') @@ -47,13 +48,12 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi // Disabled until configuration issues are addressed by VS Code. See #4007 const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); + await multiStep.run((input, s) => PythonDebugConfigurationService.pickDebugConfiguration(input, s), state); - if (Object.keys(state.config).length === 0) { - return; - } else { + if (Object.keys(state.config).length !== 0) { return [state.config as DebugConfiguration]; } + return undefined; } public async resolveDebugConfiguration( @@ -67,7 +67,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi debugConfiguration as AttachRequestArguments, token, ); - } else if (debugConfiguration.request === 'test') { + } + if (debugConfiguration.request === 'test') { // `"request": "test"` is now deprecated. But some users might have it in their // launch config. We get here if they triggered it using F5 or start with debugger. throw Error( @@ -80,9 +81,10 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi } else { const configs = await this.provideDebugConfigurations(folder, token); if (configs === undefined) { - return; + return undefined; } if (Array.isArray(configs) && configs.length === 1) { + // eslint-disable-next-line prefer-destructuring debugConfiguration = configs[0]; } this.cacheDebugConfig = cloneDeep(debugConfiguration); @@ -107,7 +109,8 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); } - protected async pickDebugConfiguration( + // eslint-disable-next-line consistent-return + protected static async pickDebugConfiguration( input: MultiStepInput, state: DebugConfigurationState, ): Promise | void> { @@ -178,7 +181,7 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi title: DebugConfigStrings.selectConfiguration.title, placeholder: DebugConfigStrings.selectConfiguration.placeholder, activeItem: items[0], - items: items, + items, }); if (pick) { const pickedDebugConfiguration = debugConfigurations.get(pick.type)!; diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts index 4fb2e7e0c225..d13cf48c2a6d 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/dynamicdebugConfigurationService.ts @@ -4,11 +4,10 @@ 'use strict'; import * as path from 'path'; -import { inject, injectable } from 'inversify'; +import * as fs from 'fs-extra'; +import { injectable } from 'inversify'; import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; import { IDynamicDebugConfigurationService } from '../types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPathUtils } from '../../../common/types'; import { DebuggerTypeName } from '../../constants'; import { asyncFilter } from '../../../common/utils/arrayUtils'; @@ -16,8 +15,7 @@ const workspaceFolderToken = '${workspaceFolder}'; @injectable() export class DynamicPythonDebugConfigurationService implements IDynamicDebugConfigurationService { - constructor(@inject(IFileSystem) private fs: IFileSystem, @inject(IPathUtils) private pathUtils: IPathUtils) {} - + // eslint-disable-next-line class-methods-use-this public async provideDebugConfigurations( folder: WorkspaceFolder, _token?: CancellationToken, @@ -32,20 +30,20 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf justMyCode: true, }); - const djangoManagePath = await this.getDjangoPath(folder); + const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder); if (djangoManagePath) { providers.push({ name: 'Python: Django', type: DebuggerTypeName, request: 'launch', - program: `${workspaceFolderToken}${this.pathUtils.separator}${djangoManagePath}`, + program: `${workspaceFolderToken}${path.sep}${djangoManagePath}`, args: ['runserver'], django: true, justMyCode: true, }); } - const flaskPath = await this.getFlaskPath(folder); + const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder); if (flaskPath) { providers.push({ name: 'Python: Flask', @@ -62,12 +60,9 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf }); } - let fastApiPath = await this.getFastApiPath(folder); + let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder); if (fastApiPath) { - fastApiPath = path - .relative(folder.uri.fsPath, fastApiPath) - .replaceAll(this.pathUtils.separator, '.') - .replace('.py', ''); + fastApiPath = path.relative(folder.uri.fsPath, fastApiPath).replaceAll(path.sep, '.').replace('.py', ''); providers.push({ name: 'Python: FastAPI', type: DebuggerTypeName, @@ -82,9 +77,9 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf return providers; } - private async getDjangoPath(folder: WorkspaceFolder) { + private static async getDjangoPath(folder: WorkspaceFolder) { const regExpression = /execute_from_command_line\(/; - const possiblePaths = await this.getPossiblePaths( + const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( folder, ['manage.py', '*/manage.py', 'app.py', '*/app.py'], regExpression, @@ -92,9 +87,9 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null; } - private async getFastApiPath(folder: WorkspaceFolder) { + private static async getFastApiPath(folder: WorkspaceFolder) { const regExpression = /app\s*=\s*FastAPI\(/; - const fastApiPaths = await this.getPossiblePaths( + const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( folder, ['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'], regExpression, @@ -103,9 +98,9 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf return fastApiPaths.length ? fastApiPaths[0] : null; } - private async getFlaskPath(folder: WorkspaceFolder) { + private static async getFlaskPath(folder: WorkspaceFolder) { const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/; - const flaskPaths = await this.getPossiblePaths( + const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths( folder, ['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'], regExpression, @@ -114,16 +109,23 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf return flaskPaths.length ? flaskPaths[0] : null; } - private async getPossiblePaths(folder: WorkspaceFolder, globPatterns: string[], regex: RegExp): Promise { + private static async getPossiblePaths( + folder: WorkspaceFolder, + globPatterns: string[], + regex: RegExp, + ): Promise { const foundPathsPromises = (await Promise.allSettled( globPatterns.map( - async (pattern): Promise => this.fs.search(path.join(folder.uri.fsPath, pattern)), + async (pattern): Promise => + (await fs.pathExists(path.join(folder.uri.fsPath, pattern))) + ? [path.join(folder.uri.fsPath, pattern)] + : [], ), )) as { status: string; value: [] }[]; const possiblePaths: string[] = []; foundPathsPromises.forEach((result) => possiblePaths.push(...result.value)); const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) => - regex.exec((await this.fs.readFile(possiblePath)).toString()), + regex.exec((await fs.readFile(possiblePath)).toString()), ); return finalPaths; diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts index d89201b940ee..c3b243fe9065 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/completionProvider.ts @@ -29,10 +29,12 @@ enum JsonLanguages { @injectable() export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + constructor( @inject(ILanguageService) private readonly languageService: ILanguageService, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, ) {} + public async activate(): Promise { this.disposableRegistry.push( this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this), @@ -41,12 +43,14 @@ export class LaunchJsonCompletionProvider implements CompletionItemProvider, IEx this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this), ); } + + // eslint-disable-next-line class-methods-use-this public async provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, ): Promise { - if (!this.canProvideCompletions(document, position)) { + if (!LaunchJsonCompletionProvider.canProvideCompletions(document, position)) { return []; } @@ -66,7 +70,8 @@ export class LaunchJsonCompletionProvider implements CompletionItemProvider, IEx }, ]; } - public canProvideCompletions(document: TextDocument, position: Position) { + + public static canProvideCompletions(document: TextDocument, position: Position): boolean { if (path.basename(document.uri.fsPath) !== 'launch.json') { return false; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts index 6137d20b1d1e..e4c14de407c9 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -6,32 +6,39 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ICommandManager } from '../../../../common/application/types'; import { Commands } from '../../../../common/constants'; import { IDisposable, IDisposableRegistry } from '../../../../common/types'; +import { registerCommand } from '../../../../common/vscodeApis/commandApis'; import { IInterpreterService } from '../../../../interpreter/contracts'; @injectable() export class InterpreterPathCommand implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IDisposableRegistry) private readonly disposables: IDisposable[], ) {} - public async activate() { + public async activate(): Promise { this.disposables.push( - this.commandManager.registerCommand(Commands.GetSelectedInterpreterPath, (args) => { - return this._getSelectedInterpreterPath(args); - }), + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), ); } public async _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): Promise { // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder - const workspaceFolder = 'workspaceFolder' in args ? args.workspaceFolder : args[1] ? args[1] : undefined; + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else { + workspaceFolder = undefined; + } + let workspaceFolderUri; try { workspaceFolderUri = workspaceFolder ? Uri.parse(workspaceFolder) : undefined; diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts index d600a37665b1..789dda510e37 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -2,44 +2,36 @@ // Licensed under the MIT License. import * as path from 'path'; +import * as fs from 'fs-extra'; import { parse } from 'jsonc-parser'; -import { inject, injectable } from 'inversify'; import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { IFileSystem } from '../../../../common/platform/types'; -import { ILaunchJsonReader } from '../types'; -import { IWorkspaceService } from '../../../../common/application/types'; +import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; -@injectable() -export class LaunchJsonReader implements ILaunchJsonReader { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - ) {} +export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { + const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); - public async getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { - const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); - - if (!(await this.fs.fileExists(filename))) { - return []; - } + if (!(await fs.pathExists(filename))) { + return []; + } - const text = await this.fs.readFile(filename); - const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); - if (!parsed.configurations || !Array.isArray(parsed.configurations)) { - throw Error('Missing field in launch.json: configurations'); - } - if (!parsed.version) { - throw Error('Missing field in launch.json: version'); - } - // We do not bother ensuring each item is a DebugConfiguration... - return parsed.configurations; + const text = await fs.readFile(filename, 'utf-8'); + const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); + if (!parsed.configurations || !Array.isArray(parsed.configurations)) { + throw Error('Missing field in launch.json: configurations'); + } + if (!parsed.version) { + throw Error('Missing field in launch.json: version'); } + // We do not bother ensuring each item is a DebugConfiguration... + return parsed.configurations; +} - public async getConfigurationsByUri(uri: Uri): Promise { - const workspace = this.workspaceService.getWorkspaceFolder(uri); +export async function getConfigurationsByUri(uri?: Uri): Promise { + if (uri) { + const workspace = getWorkspaceFolder(uri); if (workspace) { - return this.getConfigurationsForWorkspace(workspace); + return getConfigurationsForWorkspace(workspace); } - return []; } + return []; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts index 232d068cc47b..b95749040f3c 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterService.ts @@ -4,169 +4,25 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; -import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; import { IExtensionSingleActivationService } from '../../../../activation/types'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; import { IDisposableRegistry } from '../../../../common/types'; -import { noop } from '../../../../common/utils/misc'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; +import { registerCommand } from '../../../../common/vscodeApis/commandApis'; import { IDebugConfigurationService } from '../../types'; - -type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; -type PositionOfComma = 'BeforeCursor'; - -export class LaunchJsonUpdaterServiceHelper { - constructor( - private readonly commandManager: ICommandManager, - private readonly workspace: IWorkspaceService, - private readonly documentManager: IDocumentManager, - private readonly configurationProvider: IDebugConfigurationService, - ) {} - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) - public async selectAndInsertDebugConfig( - document: TextDocument, - position: Position, - token: CancellationToken, - ): Promise { - if (this.documentManager.activeTextEditor && this.documentManager.activeTextEditor.document === document) { - const folder = this.workspace.getWorkspaceFolder(document.uri); - const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); - - if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { - // Always use the first available debug configuration. - await this.insertDebugConfiguration(document, position, configs[0]); - } - } - } - /** - * Inserts the debug configuration into the document. - * Invokes the document formatter to ensure JSON is formatted nicely. - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns {Promise} - * @memberof LaunchJsonCompletionItemProvider - */ - public async insertDebugConfiguration( - document: TextDocument, - position: Position, - config: DebugConfiguration, - ): Promise { - const cursorPosition = this.getCursorPositionInConfigurationsArray(document, position); - if (!cursorPosition) { - return; - } - const commaPosition = this.isCommaImmediatelyBeforeCursor(document, position) ? 'BeforeCursor' : undefined; - const formattedJson = this.getTextForInsertion(config, cursorPosition, commaPosition); - const workspaceEdit = new WorkspaceEdit(); - workspaceEdit.insert(document.uri, position, formattedJson); - await this.documentManager.applyEdit(workspaceEdit); - this.commandManager.executeCommand('editor.action.formatDocument').then(noop, noop); - } - /** - * Gets the string representation of the debug config for insertion in the document. - * Adds necessary leading or trailing commas (remember the text is added into an array). - * @param {DebugConfiguration} config - * @param {PositionOfCursor} cursorPosition - * @param {PositionOfComma} [commaPosition] - * @returns - * @memberof LaunchJsonCompletionItemProvider - */ - public getTextForInsertion( - config: DebugConfiguration, - cursorPosition: PositionOfCursor, - commaPosition?: PositionOfComma, - ) { - const json = JSON.stringify(config); - if (cursorPosition === 'AfterItem') { - // If we already have a comma immediatley before the cursor, then no need of adding a comma. - return commaPosition === 'BeforeCursor' ? json : `,${json}`; - } - if (cursorPosition === 'BeforeItem') { - return `${json},`; - } - return json; - } - public getCursorPositionInConfigurationsArray( - document: TextDocument, - position: Position, - ): PositionOfCursor | undefined { - if (this.isConfigurationArrayEmpty(document)) { - return 'InsideEmptyArray'; - } - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); - const nextToken = scanner.scan(); - if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { - return 'AfterItem'; - } - if (nextToken === SyntaxKind.OpenBraceToken) { - return 'BeforeItem'; - } - } - public isConfigurationArrayEmpty(document: TextDocument): boolean { - const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { - configurations: []; - }; - return ( - !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 - ); - } - public isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position) { - const line = document.lineAt(position.line); - // Get text from start of line until the cursor. - const currentLine = document.getText(new Range(line.range.start, position)); - if (currentLine.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (currentLine.trim().length !== 0) { - return false; - } - - // Keep walking backwards until we hit a non-comma character or a comm character. - let startLineNumber = position.line - 1; - while (startLineNumber > 0) { - const lineText = document.lineAt(startLineNumber).text; - if (lineText.trim().endsWith(',')) { - return true; - } - // If there are other characters, then don't bother. - if (lineText.trim().length !== 0) { - return false; - } - startLineNumber -= 1; - continue; - } - return false; - } -} +import { LaunchJsonUpdaterServiceHelper } from './updaterServiceHelper'; @injectable() export class LaunchJsonUpdaterService implements IExtensionSingleActivationService { public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService, ) {} + public async activate(): Promise { - const handler = new LaunchJsonUpdaterServiceHelper( - this.commandManager, - this.workspace, - this.documentManager, - this.configurationProvider, - ); + const handler = new LaunchJsonUpdaterServiceHelper(this.configurationProvider); this.disposableRegistry.push( - this.commandManager.registerCommand( - 'python.SelectAndInsertDebugConfiguration', - handler.selectAndInsertDebugConfig, - handler, - ), + registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler), ); } } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts new file mode 100644 index 000000000000..bc0820fa188f --- /dev/null +++ b/extensions/positron-python/src/client/debugger/extension/configuration/launch.json/updaterServiceHelper.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; +import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; +import { noop } from '../../../../common/utils/misc'; +import { executeCommand } from '../../../../common/vscodeApis/commandApis'; +import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; +import { applyEdit, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { captureTelemetry } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { IDebugConfigurationService } from '../../types'; + +type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; +type PositionOfComma = 'BeforeCursor'; + +export class LaunchJsonUpdaterServiceHelper { + constructor(private readonly configurationProvider: IDebugConfigurationService) {} + + @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) + public async selectAndInsertDebugConfig( + document: TextDocument, + position: Position, + token: CancellationToken, + ): Promise { + const activeTextEditor = getActiveTextEditor(); + if (activeTextEditor && activeTextEditor.document === document) { + const folder = getWorkspaceFolder(document.uri); + const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); + + if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { + // Always use the first available debug configuration. + await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document, position, configs[0]); + } + } + } + + /** + * Inserts the debug configuration into the document. + * Invokes the document formatter to ensure JSON is formatted nicely. + * @param {TextDocument} document + * @param {Position} position + * @param {DebugConfiguration} config + * @returns {Promise} + * @memberof LaunchJsonCompletionItemProvider + */ + public static async insertDebugConfiguration( + document: TextDocument, + position: Position, + config: DebugConfiguration, + ): Promise { + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document, + position, + ); + if (!cursorPosition) { + return; + } + const commaPosition = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document, position) + ? 'BeforeCursor' + : undefined; + const formattedJson = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, cursorPosition, commaPosition); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.insert(document.uri, position, formattedJson); + await applyEdit(workspaceEdit); + executeCommand('editor.action.formatDocument').then(noop, noop); + } + + /** + * Gets the string representation of the debug config for insertion in the document. + * Adds necessary leading or trailing commas (remember the text is added into an array). + * @param {DebugConfiguration} config + * @param {PositionOfCursor} cursorPosition + * @param {PositionOfComma} [commaPosition] + * @returns + * @memberof LaunchJsonCompletionItemProvider + */ + public static getTextForInsertion( + config: DebugConfiguration, + cursorPosition: PositionOfCursor, + commaPosition?: PositionOfComma, + ): string { + const json = JSON.stringify(config); + if (cursorPosition === 'AfterItem') { + // If we already have a comma immediatley before the cursor, then no need of adding a comma. + return commaPosition === 'BeforeCursor' ? json : `,${json}`; + } + if (cursorPosition === 'BeforeItem') { + return `${json},`; + } + return json; + } + + public static getCursorPositionInConfigurationsArray( + document: TextDocument, + position: Position, + ): PositionOfCursor | undefined { + if (LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document)) { + return 'InsideEmptyArray'; + } + const scanner = createScanner(document.getText(), true); + scanner.setPosition(document.offsetAt(position)); + const nextToken = scanner.scan(); + if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { + return 'AfterItem'; + } + if (nextToken === SyntaxKind.OpenBraceToken) { + return 'BeforeItem'; + } + return undefined; + } + + public static isConfigurationArrayEmpty(document: TextDocument): boolean { + const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { + configurations: []; + }; + return ( + !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 + ); + } + + public static isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position): boolean { + const line = document.lineAt(position.line); + // Get text from start of line until the cursor. + const currentLine = document.getText(new Range(line.range.start, position)); + if (currentLine.trim().endsWith(',')) { + return true; + } + // If there are other characters, then don't bother. + if (currentLine.trim().length !== 0) { + return false; + } + + // Keep walking backwards until we hit a non-comma character or a comm character. + let startLineNumber = position.line - 1; + while (startLineNumber > 0) { + const lineText = document.lineAt(startLineNumber).text; + if (lineText.trim().endsWith(',')) { + return true; + } + // If there are other characters, then don't bother. + if (lineText.trim().length !== 0) { + return false; + } + startLineNumber -= 1; + } + return false; + } +} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts index 6f31fab4d106..4e1513ccb1ea 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -20,7 +20,7 @@ const workspaceFolderToken = '${workspaceFolder}'; export async function buildDjangoLaunchDebugConfiguration( input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const program = await getManagePyPath(state.folder); let manuallyEnteredAValue: boolean | undefined; const defaultProgram = `${workspaceFolderToken}${path.sep}manage.py`; @@ -73,15 +73,16 @@ export async function validateManagePy( return error; } } - return; + return undefined; } export async function getManagePyPath(folder: vscode.WorkspaceFolder | undefined): Promise { if (!folder) { - return; + return undefined; } const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); if (await fs.pathExists(defaultLocationOfManagePy)) { return `${workspaceFolderToken}${path.sep}manage.py`; } + return undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts index 6a4d3676ccab..25aaf3d25c08 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fastapiLaunch.ts @@ -17,7 +17,7 @@ import { DebugConfigurationState, DebugConfigurationType } from '../../types'; export async function buildFastAPILaunchDebugConfiguration( input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const application = await getApplicationPath(state.folder); let manuallyEnteredAValue: boolean | undefined; const config: Partial = { @@ -57,10 +57,11 @@ export async function buildFastAPILaunchDebugConfiguration( } export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { if (!folder) { - return; + return undefined; } const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'main.py'); if (await fs.pathExists(defaultLocationOfManagePy)) { return 'main.py'; } + return undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts index 6fcd9d671aad..746e33ea86fd 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -14,7 +14,7 @@ import { DebugConfigurationState, DebugConfigurationType } from '../../types'; export async function buildFileLaunchDebugConfiguration( _input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const config: Partial = { name: DebugConfigStrings.file.snippet.name, type: DebuggerTypeName, diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts index 4433caa6138a..d85258c800c6 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -17,7 +17,7 @@ import { DebugConfigurationState, DebugConfigurationType } from '../../types'; export async function buildFlaskLaunchDebugConfiguration( input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const application = await getApplicationPath(state.folder); let manuallyEnteredAValue: boolean | undefined; const config: Partial = { @@ -61,10 +61,11 @@ export async function buildFlaskLaunchDebugConfiguration( } export async function getApplicationPath(folder: WorkspaceFolder | undefined): Promise { if (!folder) { - return; + return undefined; } const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); if (await fs.pathExists(defaultLocationOfManagePy)) { return 'app.py'; } + return undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts index 1b644833f593..16787296ce7c 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -14,7 +14,7 @@ import { DebugConfigurationState, DebugConfigurationType } from '../../types'; export async function buildModuleLaunchConfiguration( input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { let manuallyEnteredAValue: boolean | undefined; const config: Partial = { name: DebugConfigStrings.module.snippet.name, diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts index c9bb7656d6ca..fc0d66874470 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pidAttach.ts @@ -14,7 +14,7 @@ import { DebugConfigurationState, DebugConfigurationType } from '../../types'; export async function buildPidAttachConfiguration( _input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const config: Partial = { name: DebugConfigStrings.attachPid.snippet.name, type: DebuggerTypeName, diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts index 514bf0641347..315e204e7bf8 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; -import { WorkspaceFolder } from 'vscode'; +import { l10n, WorkspaceFolder } from 'vscode'; import { DebugConfigStrings } from '../../../../common/utils/localize'; import { MultiStepInput } from '../../../../common/utils/multiStepInput'; import { sendTelemetryEvent } from '../../../../telemetry'; @@ -13,17 +13,14 @@ import { EventName } from '../../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import * as nls from 'vscode-nls'; import { resolveVariables } from '../utils/common'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - const workspaceFolderToken = '${workspaceFolder}'; export async function buildPyramidLaunchConfiguration( input: MultiStepInput, state: DebugConfigurationState, -) { +): Promise { const iniPath = await getDevelopmentIniPath(state.folder); const defaultIni = `${workspaceFolderToken}${path.sep}development.ini`; let manuallyEnteredAValue: boolean | undefined; @@ -43,8 +40,7 @@ export async function buildPyramidLaunchConfiguration( const selectedIniPath = await input.showInputBox({ title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title, value: defaultIni, - prompt: localize( - 'debug.pyramidEnterDevelopmentIniPathPrompt', + prompt: l10n.t( 'Enter the path to development.ini ({0} points to the root of the current workspace folder)', workspaceFolderToken, ), @@ -70,7 +66,7 @@ export async function validateIniPath( selected?: string, ): Promise { if (!folder) { - return; + return undefined; } const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid; if (!selected || selected.trim().length === 0) { @@ -85,14 +81,16 @@ export async function validateIniPath( return error; } } + return undefined; } export async function getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { if (!folder) { - return; + return undefined; } const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); if (await fs.pathExists(defaultLocationOfManagePy)) { return `${workspaceFolderToken}${path.sep}development.ini`; } + return undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/attach.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/attach.ts index 4264f1e1875a..635dc88dbac4 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -16,7 +16,7 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver { - const workspaceFolder = this.getWorkspaceFolder(folder); + const workspaceFolder = AttachConfigurationResolver.getWorkspaceFolder(folder); await this.provideAttachDefaults(workspaceFolder, debugConfiguration as AttachRequestArguments); @@ -49,41 +49,41 @@ export class AttachConfigurationResolver extends BaseConfigurationResolver 0 ? pathMappings : undefined; } diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/base.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/base.ts index 63dabfa430cc..06b233eb1ad1 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/base.ts @@ -6,9 +6,12 @@ import { injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { PYTHON_LANGUAGE } from '../../../../common/constants'; import { IConfigurationService } from '../../../../common/types'; import { getOSType, OSType } from '../../../../common/utils/platform'; +import { + getWorkspaceFolder as getVSCodeWorkspaceFolder, + getWorkspaceFolders, +} from '../../../../common/vscodeApis/workspaceApis'; import { IInterpreterService } from '../../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../../telemetry'; import { EventName } from '../../../../telemetry/constants'; @@ -16,8 +19,8 @@ import { DebuggerTelemetry } from '../../../../telemetry/types'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; -import { getActiveTextEditor, resolveVariables } from '../utils/common'; -import { getWorkspaceFolder as getVSCodeWorkspaceFolder, getWorkspaceFolders } from '../utils/workspaceFolder'; +import { resolveVariables } from '../utils/common'; +import { getProgram } from './helper'; @injectable() export abstract class BaseConfigurationResolver @@ -37,6 +40,7 @@ export abstract class BaseConfigurationResolver // and validation of debug configuration in derived classes should be performed in // resolveDebugConfigurationWithSubstitutedVariables() instead, where all variables // are already substituted. + // eslint-disable-next-line class-methods-use-this public async resolveDebugConfiguration( _folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, @@ -51,12 +55,12 @@ export abstract class BaseConfigurationResolver token?: CancellationToken, ): Promise; - protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + protected static getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { if (folder) { return folder.uri; } - const program = this.getProgram(); - let workspaceFolders = getWorkspaceFolders(); + const program = getProgram(); + const workspaceFolders = getWorkspaceFolders(); if (!Array.isArray(workspaceFolders) || workspaceFolders.length === 0) { return program ? Uri.file(path.dirname(program)) : undefined; @@ -70,24 +74,18 @@ export abstract class BaseConfigurationResolver return workspaceFolder.uri; } } - } - - protected getProgram(): string | undefined { - const activeTextEditor = getActiveTextEditor(); - if (activeTextEditor && activeTextEditor.document.languageId === PYTHON_LANGUAGE) { - return activeTextEditor.document.fileName; - } + return undefined; } protected async resolveAndUpdatePaths( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments, ): Promise { - this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); + BaseConfigurationResolver.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); await this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); } - protected resolveAndUpdateEnvFilePath( + protected static resolveAndUpdateEnvFilePath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments, ): void { @@ -134,19 +132,19 @@ export abstract class BaseConfigurationResolver ); } - protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + protected static debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions): void { if (debugOptions.indexOf(debugOption) >= 0) { return; } debugOptions.push(debugOption); } - protected isLocalHost(hostName?: string) { + protected static isLocalHost(hostName?: string): boolean { const LocalHosts = ['localhost', '127.0.0.1', '::1']; - return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false; + return !!(hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0); } - protected fixUpPathMappings( + protected static fixUpPathMappings( pathMappings: PathMapping[], defaultLocalRoot?: string, defaultRemoteRoot?: string, @@ -168,9 +166,9 @@ export abstract class BaseConfigurationResolver } else { // Expand ${workspaceFolder} variable first if necessary. pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => { - let resolvedLocalRoot = resolveVariables(mappedLocalRoot, defaultLocalRoot, undefined); + const resolvedLocalRoot = resolveVariables(mappedLocalRoot, defaultLocalRoot, undefined); return { - localRoot: resolvedLocalRoot ? resolvedLocalRoot : '', + localRoot: resolvedLocalRoot || '', // TODO: Apply to remoteRoot too? remoteRoot, }; @@ -179,7 +177,7 @@ export abstract class BaseConfigurationResolver // If on Windows, lowercase the drive letter for path mappings. // TODO: Apply even if no localRoot? - if (getOSType() == OSType.Windows) { + if (getOSType() === OSType.Windows) { // TODO: Apply to remoteRoot too? pathMappings = pathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { let localRoot = windowsLocalRoot; @@ -193,18 +191,22 @@ export abstract class BaseConfigurationResolver return pathMappings; } - protected isDebuggingFastAPI(debugConfiguration: Partial) { - return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI' ? true : false; + protected static isDebuggingFastAPI( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } - protected isDebuggingFlask(debugConfiguration: Partial) { - return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false; + protected static isDebuggingFlask( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); } - protected sendTelemetry( + protected static sendTelemetry( trigger: 'launch' | 'attach' | 'test', debugConfiguration: Partial, - ) { + ): void { const name = debugConfiguration.name || ''; const moduleName = debugConfiguration.module || ''; const telemetryProps: DebuggerTelemetry = { @@ -212,10 +214,10 @@ export abstract class BaseConfigurationResolver console: debugConfiguration.console, hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, django: !!debugConfiguration.django, - fastapi: this.isDebuggingFastAPI(debugConfiguration), - flask: this.isDebuggingFlask(debugConfiguration), + fastapi: BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration), + flask: BaseConfigurationResolver.isDebuggingFlask(debugConfiguration), hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, - isLocalhost: this.isLocalHost(debugConfiguration.host), + isLocalhost: BaseConfigurationResolver.isLocalHost(debugConfiguration.host), isModule: moduleName.length > 0, isSudo: !!debugConfiguration.sudo, jinja: !!debugConfiguration.jinja, diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/helper.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/helper.ts index 711d1c3ce4fc..781b25a26510 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/helper.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -4,9 +4,12 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { ICurrentProcess, IPathUtils } from '../../../../common/types'; +import { ICurrentProcess } from '../../../../common/types'; import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../../common/variables/types'; import { LaunchRequestArguments } from '../../../types'; +import { PYTHON_LANGUAGE } from '../../../../common/constants'; +import { getActiveTextEditor } from '../../../../common/vscodeApis/windowApis'; +import { getSearchPathEnvVarNames } from '../../../../common/utils/exec'; export const IDebugEnvironmentVariablesService = Symbol('IDebugEnvironmentVariablesService'); export interface IDebugEnvironmentVariablesService { @@ -17,15 +20,17 @@ export interface IDebugEnvironmentVariablesService { export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariablesService { constructor( @inject(IEnvironmentVariablesService) private envParser: IEnvironmentVariablesService, - @inject(IPathUtils) private pathUtils: IPathUtils, @inject(ICurrentProcess) private process: ICurrentProcess, ) {} + public async getEnvironmentVariables(args: LaunchRequestArguments): Promise { - const pathVariableName = this.pathUtils.getPathVariableName(); + const pathVariableName = getSearchPathEnvVarNames()[0]; // Merge variables from both .env file and env json variables. const debugLaunchEnvVars: Record = - args.env && Object.keys(args.env).length > 0 ? ({ ...args.env } as any) : ({} as any); + args.env && Object.keys(args.env).length > 0 + ? ({ ...args.env } as Record) + : ({} as Record); const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); const env = envFileVars ? { ...envFileVars } : {}; @@ -77,3 +82,11 @@ export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariabl return env; } } + +export function getProgram(): string | undefined { + const activeTextEditor = getActiveTextEditor(); + if (activeTextEditor && activeTextEditor.document.languageId === PYTHON_LANGUAGE) { + return activeTextEditor.document.fileName; + } + return undefined; +} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts index 603074b249d7..d5cb419e031d 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -13,7 +13,7 @@ import { IInterpreterService } from '../../../../interpreter/contracts'; import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; import { BaseConfigurationResolver } from './base'; -import { IDebugEnvironmentVariablesService } from './helper'; +import { getProgram, IDebugEnvironmentVariablesService } from './helper'; @injectable() export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -40,7 +40,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - const workspaceFolder = this.getWorkspaceFolder(folder); + const workspaceFolder = LaunchConfigurationResolver.getWorkspaceFolder(folder); await this.provideLaunchDefaults(workspaceFolder, debugConfiguration); const isValid = await this.validateLaunchConfiguration(folder, debugConfiguration); if (!isValid) { - return; + return undefined; } if (Array.isArray(debugConfiguration.debugOptions)) { @@ -123,50 +123,50 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0) { - pathMappings = this.fixUpPathMappings( + pathMappings = LaunchConfigurationResolver.fixUpPathMappings( pathMappings || [], workspaceFolder ? workspaceFolder.fsPath : '', ); @@ -177,7 +177,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { @@ -19,9 +19,3 @@ export interface IDebugConfigurationResolver { token?: CancellationToken, ): Promise; } - -export const ILaunchJsonReader = Symbol('ILaunchJsonReader'); -export interface ILaunchJsonReader { - getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise; - getConfigurationsByUri(uri?: Uri): Promise; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/utils/common.ts b/extensions/positron-python/src/client/debugger/extension/configuration/utils/common.ts index 36572419908f..3643a0c49c5d 100644 --- a/extensions/positron-python/src/client/debugger/extension/configuration/utils/common.ts +++ b/extensions/positron-python/src/client/debugger/extension/configuration/utils/common.ts @@ -6,8 +6,8 @@ 'use strict'; -import { WorkspaceFolder, window, TextEditor } from 'vscode'; -import { getWorkspaceFolder } from './workspaceFolder'; +import { WorkspaceFolder } from 'vscode'; +import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; /** * @returns whether the provided parameter is a JavaScript String or not. @@ -26,9 +26,9 @@ export function resolveVariables( folder: WorkspaceFolder | undefined, ): string | undefined { if (value) { - const workspace = folder ? getWorkspaceFolder(folder.uri) : undefined; + const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined; const variablesObject: { [key: string]: any } = {}; - variablesObject.workspaceFolder = workspace ? workspace.uri.fsPath : rootFolder; + variablesObject.workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder; const regexp = /\$\{(.*?)\}/g; return value.replace(regexp, (match: string, name: string) => { @@ -41,8 +41,3 @@ export function resolveVariables( } return value; } - -export function getActiveTextEditor(): TextEditor | undefined { - const { activeTextEditor } = window; - return activeTextEditor; -} diff --git a/extensions/positron-python/src/client/debugger/extension/configuration/utils/workspaceFolder.ts b/extensions/positron-python/src/client/debugger/extension/configuration/utils/workspaceFolder.ts deleted file mode 100644 index 162f8b435837..000000000000 --- a/extensions/positron-python/src/client/debugger/extension/configuration/utils/workspaceFolder.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as vscode from 'vscode'; - -export function getWorkspaceFolder(uri: vscode.Uri): vscode.WorkspaceFolder | undefined { - return vscode.workspace.getWorkspaceFolder(uri); -} - -export function getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { - return vscode.workspace.workspaceFolders; -} diff --git a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts index 2980bde046f8..14a108d27793 100644 --- a/extensions/positron-python/src/client/debugger/extension/debugCommands.ts +++ b/extensions/positron-python/src/client/debugger/extension/debugCommands.ts @@ -10,10 +10,10 @@ import { Commands } from '../../common/constants'; import { IDisposableRegistry } from '../../common/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ILaunchJsonReader } from './configuration/types'; import { DebugPurpose, LaunchRequestArguments } from '../types'; import { IInterpreterService } from '../../interpreter/contracts'; import { noop } from '../../common/utils/misc'; +import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; @injectable() export class DebugCommands implements IExtensionSingleActivationService { @@ -22,7 +22,6 @@ export class DebugCommands implements IExtensionSingleActivationService { constructor( @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IDebugService) private readonly debugService: IDebugService, - @inject(ILaunchJsonReader) private readonly launchJsonReader: ILaunchJsonReader, @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, ) {} @@ -36,15 +35,15 @@ export class DebugCommands implements IExtensionSingleActivationService { this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); return; } - const config = await this.getDebugConfiguration(file); + const config = await DebugCommands.getDebugConfiguration(file); this.debugService.startDebugging(undefined, config); }), ); return Promise.resolve(); } - private async getDebugConfiguration(uri?: Uri): Promise { - const configs = (await this.launchJsonReader.getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); + private static async getDebugConfiguration(uri?: Uri): Promise { + const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); for (const config of configs) { if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { if (!config.program && !config.module && !config.code) { diff --git a/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts b/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts index 0b1410133af9..c0d1306a841b 100644 --- a/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts +++ b/extensions/positron-python/src/client/debugger/extension/helpers/protocolParser.ts @@ -9,7 +9,7 @@ import { IProtocolParser } from '../types'; const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; -type Listener = (...args: any[]) => void; +type Listener = (...args: unknown[]) => void; /** * Parsers the debugger Protocol messages and raises the following events: @@ -26,34 +26,45 @@ type Listener = (...args: any[]) => void; @injectable() export class ProtocolParser implements IProtocolParser { private rawData = Buffer.alloc(0); - private contentLength: number = -1; - private disposed: boolean = false; + + private contentLength = -1; + + private disposed = false; + private stream?: Readable; + private events: EventEmitter; + constructor() { this.events = new EventEmitter(); } - public dispose() { + + public dispose(): void { if (this.stream) { this.stream.removeListener('data', this.dataCallbackHandler); this.stream = undefined; } } - public connect(stream: Readable) { + + public connect(stream: Readable): void { this.stream = stream; stream.addListener('data', this.dataCallbackHandler); } + public on(event: string | symbol, listener: Listener): this { this.events.on(event, listener); return this; } + public once(event: string | symbol, listener: Listener): this { this.events.once(event, listener); return this; } + private dataCallbackHandler = (data: string | Buffer) => { this.handleData(data as Buffer); }; + private dispatch(body: string): void { const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; @@ -86,12 +97,14 @@ export class ProtocolParser implements IProtocolParser { this.events.emit('data', message); } + private handleData(data: Buffer): void { if (this.disposed) { return; } this.rawData = Buffer.concat([this.rawData, data]); + // eslint-disable-next-line no-constant-condition while (true) { if (this.contentLength >= 0) { if (this.rawData.length >= this.contentLength) { @@ -102,6 +115,7 @@ export class ProtocolParser implements IProtocolParser { this.dispatch(message); } // there may be more complete messages to process. + // eslint-disable-next-line no-continue continue; } } else { @@ -116,6 +130,7 @@ export class ProtocolParser implements IProtocolParser { } } this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); + // eslint-disable-next-line no-continue continue; } } diff --git a/extensions/positron-python/src/client/debugger/extension/hooks/childProcessAttachService.ts b/extensions/positron-python/src/client/debugger/extension/hooks/childProcessAttachService.ts index 4493c3b7145e..c6556a62eaa1 100644 --- a/extensions/positron-python/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/extensions/positron-python/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -4,16 +4,15 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../common/application/types'; +import { IDebugService } from '../../../common/application/types'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder } from 'vscode'; import { noop } from '../../../common/utils/misc'; import { captureTelemetry } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments } from '../../types'; import { IChildProcessAttachService } from './types'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; /** * This class is responsible for attaching the debugger to any @@ -24,11 +23,7 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); */ @injectable() export class ChildProcessAttachService implements IChildProcessAttachService { - constructor( - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - ) {} + constructor(@inject(IDebugService) private readonly debugService: IDebugService) {} @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { @@ -37,11 +32,7 @@ export class ChildProcessAttachService implements IChildProcessAttachService { const folder = this.getRelatedWorkspaceFolder(debugConfig); const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); if (!launched) { - this.appShell - .showErrorMessage( - localize('debuggerError', 'Failed to launch debugger for child process {0}', processId), - ) - .then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', processId)).then(noop, noop); } } @@ -49,10 +40,11 @@ export class ChildProcessAttachService implements IChildProcessAttachService { config: AttachRequestArguments & DebugConfiguration, ): WorkspaceFolder | undefined { const workspaceFolder = config.workspaceFolder; - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; if (!hasWorkspaceFolders || !workspaceFolder) { return; } - return this.workspaceService.workspaceFolders!.find((ws) => ws.uri.fsPath === workspaceFolder); + return getWorkspaceFolders()!.find((ws) => ws.uri.fsPath === workspaceFolder); } } diff --git a/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts b/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts index 5a779428cb76..a8c5ae7bbfcc 100644 --- a/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts +++ b/extensions/positron-python/src/client/debugger/extension/serviceRegistry.ts @@ -16,12 +16,11 @@ import { PythonDebugConfigurationService } from './configuration/debugConfigurat import { DynamicPythonDebugConfigurationService } from './configuration/dynamicdebugConfigurationService'; import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonReader } from './configuration/launch.json/launchJsonReader'; import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from './configuration/resolvers/attach'; import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; -import { IDebugConfigurationResolver, ILaunchJsonReader } from './configuration/types'; +import { IDebugConfigurationResolver } from './configuration/types'; import { DebugCommands } from './debugCommands'; import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; @@ -34,7 +33,7 @@ import { IOutdatedDebuggerPromptFactory, } from './types'; -export function registerTypes(serviceManager: IServiceManager) { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton( IExtensionSingleActivationService, LaunchJsonCompletionProvider, @@ -89,5 +88,4 @@ export function registerTypes(serviceManager: IServiceManager) { AttachProcessProviderFactory, ); serviceManager.addSingleton(IExtensionSingleActivationService, DebugCommands); - serviceManager.addSingleton(ILaunchJsonReader, LaunchJsonReader); } diff --git a/extensions/positron-python/src/client/debugger/extension/types.ts b/extensions/positron-python/src/client/debugger/extension/types.ts index bb01239c9769..2a304efae918 100644 --- a/extensions/positron-python/src/client/debugger/extension/types.ts +++ b/extensions/positron-python/src/client/debugger/extension/types.ts @@ -57,6 +57,6 @@ export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFacto export const IProtocolParser = Symbol('IProtocolParser'); export interface IProtocolParser extends Disposable { connect(stream: Readable): void; - once(event: string | symbol, listener: Function): this; - on(event: string | symbol, listener: Function): this; + once(event: string | symbol, listener: (...args: unknown[]) => void): this; + on(event: string | symbol, listener: (...args: unknown[]) => void): this; } diff --git a/extensions/positron-python/src/client/extension.ts b/extensions/positron-python/src/client/extension.ts index 04ad91f80c84..67710b73c8b0 100644 --- a/extensions/positron-python/src/client/extension.ts +++ b/extensions/positron-python/src/client/extension.ts @@ -28,7 +28,6 @@ initializeFileLogging(logDispose); //=============================================== // loading starts here -import '../setupNls'; import { ProgressLocation, ProgressOptions, window } from 'vscode'; import { buildApi } from './api'; import { IApplicationShell, IWorkspaceService } from './common/application/types'; diff --git a/extensions/positron-python/src/client/extensionActivation.ts b/extensions/positron-python/src/client/extensionActivation.ts index e1387f55f0e3..1f2e3e94cefe 100644 --- a/extensions/positron-python/src/client/extensionActivation.ts +++ b/extensions/positron-python/src/client/extensionActivation.ts @@ -26,6 +26,7 @@ import { IExtensions, IInterpreterPathService, IOutputChannel, + IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; import { DebuggerTypeName } from './debugger/constants'; @@ -46,7 +47,6 @@ import { registerTypes as tensorBoardRegisterTypes } from './tensorBoard/service import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; -import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; // components import * as pythonEnvironments from './pythonEnvironments'; @@ -103,7 +103,8 @@ export function activateFeatures(ext: ExtensionState, _components: Components): const interpreterPathService: IInterpreterPathService = ext.legacyIOC.serviceContainer.get( IInterpreterPathService, ); - registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService); + const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); + registerCreateEnvironmentFeatures(ext.disposables, interpreterQuickPick, interpreterPathService, pathUtils); } /// ////////////////////////// @@ -131,7 +132,6 @@ async function activateLegacy(ext: ExtensionState): Promise { // Feature specific registrations. unitTestsRegisterTypes(serviceManager); lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); formattersRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); commonRegisterTerminalTypes(serviceManager); diff --git a/extensions/positron-python/src/client/extensionInit.ts b/extensions/positron-python/src/client/extensionInit.ts index 249dfd9e969f..4ee5f099f15d 100644 --- a/extensions/positron-python/src/client/extensionInit.ts +++ b/extensions/positron-python/src/client/extensionInit.ts @@ -10,6 +10,7 @@ import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; +import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { GLOBAL_MEMENTO, IDisposableRegistry, @@ -85,6 +86,7 @@ export function initializeStandard(ext: ExtensionState): void { variableRegisterTypes(serviceManager); platformRegisterTypes(serviceManager); processRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); // We will be pulling other code over from activateLegacy(). } diff --git a/extensions/positron-python/src/client/formatters/blackFormatter.ts b/extensions/positron-python/src/client/formatters/blackFormatter.ts index 4ca741842b55..0a8109e163e0 100644 --- a/extensions/positron-python/src/client/formatters/blackFormatter.ts +++ b/extensions/positron-python/src/client/formatters/blackFormatter.ts @@ -5,7 +5,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; import { IApplicationShell } from '../common/application/types'; import { Product } from '../common/installer/productInstaller'; import { IConfigurationService } from '../common/types'; @@ -16,8 +15,6 @@ import { sendTelemetryWhenDone } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { BaseFormatter } from './baseFormatter'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - export class BlackFormatter extends BaseFormatter { constructor(serviceContainer: IServiceContainer) { super('black', Product.black, serviceContainer); @@ -40,9 +37,7 @@ export class BlackFormatter extends BaseFormatter { const shell = this.serviceContainer.get(IApplicationShell); // Black does not support partial formatting on purpose. shell - .showErrorMessage( - localize('formatSelectionError', 'Black does not support the "Format Selection" command'), - ) + .showErrorMessage(vscode.l10n.t('Black does not support the "Format Selection" command')) .then(noop, noop); return []; } diff --git a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 5a8c72cd117d..39965adec0a0 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -6,8 +6,7 @@ import { inject, injectable } from 'inversify'; import { cloneDeep } from 'lodash'; import * as path from 'path'; -import { QuickPick, QuickPickItem, QuickPickItemKind, ThemeIcon } from 'vscode'; -import * as nls from 'vscode-nls'; +import { l10n, QuickPick, QuickPickItem, QuickPickItemKind, ThemeIcon } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; import { Commands, Octicons, ThemeIcons } from '../../../../common/constants'; import { isParentPath } from '../../../../common/platform/fs-paths'; @@ -38,7 +37,6 @@ import { } from '../../types'; import { BaseInterpreterSelectorCommand } from './base'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); const untildify = require('untildify'); export type InterpreterStateArgs = { path?: string; workspace: Resource }; @@ -143,12 +141,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem const placeholder = params?.placeholder === null ? undefined - : params?.placeholder ?? - localize( - 'InterpreterQuickPickList.quickPickListPlaceholder', - 'Selected Interpreter: {0}', - currentInterpreterPathDisplay, - ); + : params?.placeholder ?? l10n.t('Selected Interpreter: {0}', currentInterpreterPathDisplay); const title = params?.title === null ? undefined : params?.title ?? InterpreterQuickPickList.browsePath.openButtonLabel; const selection = await input.showQuickPick>({ diff --git a/extensions/positron-python/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/extensions/positron-python/src/client/interpreter/configuration/pythonPathUpdaterService.ts index 64b6d03fddf0..ae2c92eada5b 100644 --- a/extensions/positron-python/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/extensions/positron-python/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,7 +1,6 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { ConfigurationTarget, Uri, window } from 'vscode'; -import * as nls from 'vscode-nls'; +import { ConfigurationTarget, l10n, Uri, window } from 'vscode'; import { StopWatch } from '../../common/utils/stopWatch'; import { SystemVariables } from '../../common/variables/systemVariables'; import { traceError } from '../../logging'; @@ -11,8 +10,6 @@ import { PythonInterpreterTelemetry } from '../../telemetry/types'; import { IComponentAdapter } from '../contracts'; import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - @injectable() export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { constructor( @@ -36,9 +33,7 @@ export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManage failed = true; const reason = err as Error; const message = reason && typeof reason.message === 'string' ? (reason.message as string) : ''; - window.showErrorMessage( - localize('setInterpreterError', 'Failed to set interpreter path. Error: {0}', message), - ); + window.showErrorMessage(l10n.t('Failed to set interpreter path. Error: {0}', message)); traceError(reason); } // do not wait for this to complete diff --git a/extensions/positron-python/src/client/interpreter/contracts.ts b/extensions/positron-python/src/client/interpreter/contracts.ts index a79a5250ec99..ec504802bcfc 100644 --- a/extensions/positron-python/src/client/interpreter/contracts.ts +++ b/extensions/positron-python/src/client/interpreter/contracts.ts @@ -122,3 +122,8 @@ export type WorkspacePythonPath = { folderUri: Uri; configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; }; + +export const IActivatedEnvironmentLaunch = Symbol('IActivatedEnvironmentLaunch'); +export interface IActivatedEnvironmentLaunch { + selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection?: boolean): Promise; +} diff --git a/extensions/positron-python/src/client/interpreter/display/index.ts b/extensions/positron-python/src/client/interpreter/display/index.ts index 437418304bb9..aabb9f86f6d1 100644 --- a/extensions/positron-python/src/client/interpreter/display/index.ts +++ b/extensions/positron-python/src/client/interpreter/display/index.ts @@ -1,6 +1,7 @@ import { inject, injectable } from 'inversify'; import { Disposable, + l10n, LanguageStatusItem, LanguageStatusSeverity, StatusBarAlignment, @@ -13,7 +14,7 @@ import { IApplicationShell, IWorkspaceService } from '../../common/application/t import { Commands, PYTHON_LANGUAGE } from '../../common/constants'; import '../../common/extensions'; import { IDisposableRegistry, IPathUtils, Resource } from '../../common/types'; -import { InterpreterQuickPickList } from '../../common/utils/localize'; +import { InterpreterQuickPickList, Interpreters } from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; import { traceLog } from '../../logging'; import { PythonEnvironment } from '../../pythonEnvironments/info'; @@ -23,15 +24,13 @@ import { IInterpreterService, IInterpreterStatusbarVisibilityFilter, } from '../contracts'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); /** * Based on https://github.com/microsoft/vscode-python/issues/18040#issuecomment-992567670. * This is to ensure the item appears right after the Python language status item. */ const STATUS_BAR_ITEM_PRIORITY = 100.09999; + @injectable() export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingleActivationService { public supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { @@ -81,9 +80,10 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle this.disposableRegistry.push(this.languageStatus); } else { const [alignment, priority] = [StatusBarAlignment.Right, STATUS_BAR_ITEM_PRIORITY]; - this.statusBar = application.createStatusBarItem(alignment, priority); + this.statusBar = application.createStatusBarItem(alignment, priority, 'python.selectedInterpreterDisplay'); this.statusBar.command = Commands.Set_Interpreter; this.disposableRegistry.push(this.statusBar); + this.statusBar.name = Interpreters.selectedPythonInterpreter; } } @@ -106,7 +106,7 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle } } private onDidChangeInterpreterInformation(info: PythonEnvironment) { - if (!this.currentlySelectedInterpreterPath || this.currentlySelectedInterpreterPath === info.path) { + if (this.currentlySelectedInterpreterPath === info.path) { this.updateDisplay(this.currentlySelectedWorkspaceFolder).ignoreErrors(); } } @@ -126,8 +126,7 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); if (this.currentlySelectedInterpreterPath !== interpreter.path) { traceLog( - localize( - 'Interpreters.sttausBarPythonInterpreterPath', + l10n.t( 'Python interpreter path: {0}', this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), ), @@ -151,8 +150,7 @@ export class InterpreterDisplay implements IInterpreterDisplay, IExtensionSingle this.languageStatus.detail = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); if (this.currentlySelectedInterpreterPath !== interpreter.path) { traceLog( - localize( - 'Interpreters.pythonInterpreterPath', + l10n.t( 'Python interpreter path: {0}', this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), ), diff --git a/extensions/positron-python/src/client/interpreter/interpreterService.ts b/extensions/positron-python/src/client/interpreter/interpreterService.ts index 50545558d721..59ce435bb4d8 100644 --- a/extensions/positron-python/src/client/interpreter/interpreterService.ts +++ b/extensions/positron-python/src/client/interpreter/interpreterService.ts @@ -22,6 +22,7 @@ import { import { IServiceContainer } from '../ioc/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; import { + IActivatedEnvironmentLaunch, IComponentAdapter, IInterpreterDisplay, IInterpreterService, @@ -179,7 +180,13 @@ export class InterpreterService implements Disposable, IInterpreterService { } public async getActiveInterpreter(resource?: Uri): Promise { - let path = this.configService.getSettings(resource).pythonPath; + const activatedEnvLaunch = this.serviceContainer.get(IActivatedEnvironmentLaunch); + let path = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(true); + // This is being set as interpreter in background, after which it'll show up in `.pythonPath` config. + // However we need not wait on the update to take place, as we can use the value directly. + if (!path) { + path = this.configService.getSettings(resource).pythonPath; + } if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. // Note the following triggers autoselection if no interpreter is explictly diff --git a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts index cdcd8718fd1d..422776bd5e43 100644 --- a/extensions/positron-python/src/client/interpreter/serviceRegistry.ts +++ b/extensions/positron-python/src/client/interpreter/serviceRegistry.ts @@ -25,11 +25,12 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from './configuration/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; +import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; import { InterpreterDisplay } from './display'; import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; import { InterpreterService } from './interpreterService'; +import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt'; @@ -90,6 +91,7 @@ export function registerInterpreterTypes(serviceManager: IServiceManager): void ); serviceManager.addSingleton(IExtensionActivationService, CondaInheritEnvPrompt); + serviceManager.addSingleton(IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch); } export function registerTypes(serviceManager: IServiceManager): void { diff --git a/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts new file mode 100644 index 000000000000..01b4829df4ac --- /dev/null +++ b/extensions/positron-python/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, optional } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import * as path from 'path'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { sleep } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { traceError, traceLog, traceWarn } from '../../logging'; +import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IPythonPathUpdaterServiceManager } from '../configuration/types'; +import { IActivatedEnvironmentLaunch, IInterpreterService } from '../contracts'; + +@injectable() +export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private inMemorySelection: string | undefined; + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @optional() public wasSelected: boolean = false, + ) {} + + @cache(-1, true) + public async _promptIfApplicable(): Promise { + const baseCondaPrefix = getPrefixOfActivatedCondaEnv(); + if (!baseCondaPrefix) { + return; + } + const info = await this.interpreterService.getInterpreterDetails(baseCondaPrefix); + if (info?.envName !== 'base') { + // Only show prompt for base conda environments, as we need to check config for such envs which can be slow. + return; + } + const conda = await Conda.getConda(); + if (!conda) { + traceWarn('Conda not found even though activated environment vars are set'); + return; + } + const service = await this.processServiceFactory.create(); + const autoActivateBaseConfig = await service + .shellExec(`${conda.shellCommand} config --get auto_activate_base`) + .catch((ex) => { + traceError(ex); + return { stdout: '' }; + }); + if (autoActivateBaseConfig.stdout.trim().toLowerCase().endsWith('false')) { + await this.promptAndUpdate(baseCondaPrefix); + } + } + + private async promptAndUpdate(prefix: string) { + this.wasSelected = true; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.activatedCondaEnvLaunch, ...prompts); + sendTelemetryEvent(EventName.ACTIVATED_CONDA_ENV_LAUNCH, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.setInterpeterInStorage(prefix); + } + } + + public async selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (this.wasSelected) { + return this.inMemorySelection; + } + return this._selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection); + } + + @cache(-1, true) + private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (this.workspaceService.workspaceFile) { + // Assuming multiroot workspaces cannot be directly launched via `code .` command. + return undefined; + } + const prefix = await this.getPrefixOfSelectedActivatedEnv(); + if (!prefix) { + this._promptIfApplicable().ignoreErrors(); + return undefined; + } + this.wasSelected = true; + this.inMemorySelection = prefix; + traceLog( + `VS Code was launched from an activated environment: '${path.basename( + prefix, + )}', selecting it as the interpreter for workspace.`, + ); + if (doNotBlockOnSelection) { + this.setInterpeterInStorage(prefix).ignoreErrors(); + } else { + await this.setInterpeterInStorage(prefix); + await sleep(1); // Yield control so config service can update itself. + } + this.inMemorySelection = undefined; // Once we have set the prefix in storage, clear the in memory selection. + return prefix; + } + + private async setInterpeterInStorage(prefix: string) { + const { workspaceFolders } = this.workspaceService; + if (!workspaceFolders || workspaceFolders.length === 0) { + await this.pythonPathUpdaterService.updatePythonPath(prefix, ConfigurationTarget.Global, 'load'); + } else { + await this.pythonPathUpdaterService.updatePythonPath( + prefix, + ConfigurationTarget.WorkspaceFolder, + 'load', + workspaceFolders[0].uri, + ); + } + } + + private async getPrefixOfSelectedActivatedEnv(): Promise { + const virtualEnvVar = process.env.VIRTUAL_ENV; + if (virtualEnvVar !== undefined && virtualEnvVar.length > 0) { + return virtualEnvVar; + } + const condaPrefixVar = getPrefixOfActivatedCondaEnv(); + if (!condaPrefixVar) { + return undefined; + } + const info = await this.interpreterService.getInterpreterDetails(condaPrefixVar); + if (info?.envName !== 'base') { + return condaPrefixVar; + } + // Ignoring base conda environments, as they could be automatically set by conda. + if (process.env.CONDA_AUTO_ACTIVATE_BASE !== undefined) { + if (process.env.CONDA_AUTO_ACTIVATE_BASE.toLowerCase() === 'false') { + return condaPrefixVar; + } + } + return undefined; + } +} + +function getPrefixOfActivatedCondaEnv() { + const condaPrefixVar = process.env.CONDA_PREFIX; + if (condaPrefixVar && condaPrefixVar.length > 0) { + const condaShlvl = process.env.CONDA_SHLVL; + if (condaShlvl !== undefined && condaShlvl.length > 0 && condaShlvl > '0') { + return condaPrefixVar; + } + } + return undefined; +} diff --git a/extensions/positron-python/src/client/jupyter/jupyterIntegration.ts b/extensions/positron-python/src/client/jupyter/jupyterIntegration.ts index 692bef74214d..556ff93f240a 100644 --- a/extensions/positron-python/src/client/jupyter/jupyterIntegration.ts +++ b/extensions/positron-python/src/client/jupyter/jupyterIntegration.ts @@ -203,6 +203,7 @@ export class JupyterExtensionIntegration { public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi)); return undefined; } // Forward python parts diff --git a/extensions/positron-python/src/client/languageServer/watcher.ts b/extensions/positron-python/src/client/languageServer/watcher.ts index f7c4193bde90..c71c33b7ad28 100644 --- a/extensions/positron-python/src/client/languageServer/watcher.ts +++ b/extensions/positron-python/src/client/languageServer/watcher.ts @@ -3,8 +3,7 @@ import * as path from 'path'; import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; -import * as nls from 'vscode-nls'; +import { ConfigurationChangeEvent, l10n, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; @@ -30,8 +29,6 @@ import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; import { LspNotebooksExperiment } from '../activation/node/lspNotebooksExperiment'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - @injectable() /** * The Language Server Watcher class implements the ILanguageServerWatcher interface, which is the one-stop shop for language server activation. @@ -380,7 +377,7 @@ function logStartup(languageServerType: LanguageServerType, resource: Uri): void switch (languageServerType) { case LanguageServerType.Jedi: - outputLine = localize('LanguageService.startingJedi', 'Starting Jedi language server for {0}.', basename); + outputLine = l10n.t('Starting Jedi language server for {0}.', basename); break; case LanguageServerType.Node: outputLine = LanguageService.startingPylance; diff --git a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts index 62aac7dffa47..6bd2d3c8e115 100644 --- a/extensions/positron-python/src/client/linters/errorHandlers/standard.ts +++ b/extensions/positron-python/src/client/linters/errorHandlers/standard.ts @@ -1,5 +1,4 @@ -import { Uri } from 'vscode'; -import * as nls from 'vscode-nls'; +import { l10n, Uri } from 'vscode'; import { IApplicationShell } from '../../common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; import { ExecutionInfo, IOutputChannel } from '../../common/types'; @@ -7,7 +6,6 @@ import { traceError, traceLog } from '../../logging'; import { ILinterManager, LinterId } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export class StandardErrorHandler extends BaseErrorHandler { public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { if ( @@ -29,7 +27,7 @@ export class StandardErrorHandler extends BaseErrorHandler { } private async displayLinterError(linterId: LinterId) { - const message = localize('linterError', "There was an error in running the linter '{0}'", linterId); + const message = l10n.t("There was an error in running the linter '{0}'", linterId); const appShell = this.serviceContainer.get(IApplicationShell); const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); const action = await appShell.showErrorMessage(message, 'View Errors'); diff --git a/extensions/positron-python/src/client/linters/linterCommands.ts b/extensions/positron-python/src/client/linters/linterCommands.ts index 5c9cb42bb460..cc35e80f26b1 100644 --- a/extensions/positron-python/src/client/linters/linterCommands.ts +++ b/extensions/positron-python/src/client/linters/linterCommands.ts @@ -3,8 +3,7 @@ 'use strict'; -import { DiagnosticCollection, Disposable, QuickPickOptions, Uri } from 'vscode'; -import * as nls from 'vscode-nls'; +import { DiagnosticCollection, Disposable, l10n, QuickPickOptions, Uri } from 'vscode'; import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; import { Commands } from '../common/constants'; import { IDisposable } from '../common/types'; @@ -14,8 +13,6 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { ILinterManager, ILintingEngine, LinterId } from './types'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - export class LinterCommands implements IDisposable { private disposables: Disposable[] = []; @@ -74,11 +71,7 @@ export class LinterCommands implements IDisposable { const index = linters.findIndex((x) => x.id === selection); if (activeLinters.length > 1) { const response = await this.appShell.showWarningMessage( - localize( - 'Linter.replaceWithSelectedLinter', - "Multiple linters are enabled in settings. Replace with '{0}'?", - selection, - ), + l10n.t("Multiple linters are enabled in settings. Replace with '{0}'?", selection), Common.bannerLabelYes, Common.bannerLabelNo, ); diff --git a/extensions/positron-python/src/client/linters/linterManager.ts b/extensions/positron-python/src/client/linters/linterManager.ts index 01a4c4ca38ea..72c92aa1c77d 100644 --- a/extensions/positron-python/src/client/linters/linterManager.ts +++ b/extensions/positron-python/src/client/linters/linterManager.ts @@ -13,8 +13,8 @@ import { Bandit } from './bandit'; import { Flake8 } from './flake8'; import { LinterInfo } from './linterInfo'; import { MyPy } from './mypy'; -import { Flake8ExtensionPrompt } from './prompts/flake8Prompt'; -import { PylintExtensionPrompt } from './prompts/pylintPrompt'; +import { getOrCreateFlake8Prompt } from './prompts/flake8Prompt'; +import { getOrCreatePylintPrompt } from './prompts/pylintPrompt'; import { Prospector } from './prospector'; import { Pycodestyle } from './pycodestyle'; import { PyDocStyle } from './pydocstyle'; @@ -112,9 +112,9 @@ export class LinterManager implements ILinterManager { case Product.bandit: return new Bandit(serviceContainer); case Product.flake8: - return new Flake8(serviceContainer, new Flake8ExtensionPrompt(serviceContainer)); + return new Flake8(serviceContainer, getOrCreateFlake8Prompt(serviceContainer)); case Product.pylint: - return new Pylint(serviceContainer, new PylintExtensionPrompt(serviceContainer)); + return new Pylint(serviceContainer, getOrCreatePylintPrompt(serviceContainer)); case Product.mypy: return new MyPy(serviceContainer); case Product.prospector: diff --git a/extensions/positron-python/src/client/linters/prompts/common.ts b/extensions/positron-python/src/client/linters/prompts/common.ts index 509baaf4ee89..bf459f895ead 100644 --- a/extensions/positron-python/src/client/linters/prompts/common.ts +++ b/extensions/positron-python/src/client/linters/prompts/common.ts @@ -1,13 +1,38 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs-extra'; +import * as path from 'path'; import { ShowToolsExtensionPrompt } from '../../common/experiments/groups'; import { IExperimentService, IExtensions, IPersistentState, IPersistentStateFactory } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { traceLog } from '../../logging'; + +function isExtensionInstalledButDisabled(extensions: IExtensions, extensionId: string): boolean { + // When debugging the python extension this `extensionPath` below will point to your repo. + // If you are debugging this feature then set the `extensionPath` to right location after + // the next line. + const pythonExt = extensions.getExtension('ms-python.python'); + if (pythonExt) { + let found = false; + traceLog(`Extension search path: ${path.dirname(pythonExt.extensionPath)}`); + fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { + if (s.toString().startsWith(extensionId)) { + found = true; + } + }); + return found; + } + return false; +} export function isExtensionInstalled(serviceContainer: IServiceContainer, extensionId: string): boolean { const extensions: IExtensions = serviceContainer.get(IExtensions); const extension = extensions.getExtension(extensionId); + if (!extension) { + // The extension you are looking for might be disabled. + return isExtensionInstalledButDisabled(extensions, extensionId); + } return extension !== undefined; } diff --git a/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts b/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts index 547644ba0383..c60767f2fa8d 100644 --- a/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts +++ b/extensions/positron-python/src/client/linters/prompts/flake8Prompt.ts @@ -61,3 +61,11 @@ export class Flake8ExtensionPrompt implements IToolsExtensionPrompt { return false; } } + +let _prompt: IToolsExtensionPrompt | undefined; +export function getOrCreateFlake8Prompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { + if (!_prompt) { + _prompt = new Flake8ExtensionPrompt(serviceContainer); + } + return _prompt; +} diff --git a/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts b/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts index 7c1ce30f3f8e..7a0693740b32 100644 --- a/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts +++ b/extensions/positron-python/src/client/linters/prompts/pylintPrompt.ts @@ -74,3 +74,11 @@ export class PylintExtensionPrompt implements IToolsExtensionPrompt { return false; } } + +let _prompt: IToolsExtensionPrompt | undefined; +export function getOrCreatePylintPrompt(serviceContainer: IServiceContainer): IToolsExtensionPrompt { + if (!_prompt) { + _prompt = new PylintExtensionPrompt(serviceContainer); + } + return _prompt; +} diff --git a/extensions/positron-python/src/client/proposedApi.ts b/extensions/positron-python/src/client/proposedApi.ts index 4a5a28a46e00..5f40fcf263db 100644 --- a/extensions/positron-python/src/client/proposedApi.ts +++ b/extensions/positron-python/src/client/proposedApi.ts @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; +import { ConfigurationTarget, EventEmitter, Uri, workspace, WorkspaceFolder } from 'vscode'; import * as pathUtils from 'path'; import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; @@ -124,17 +124,21 @@ export function buildProposedApi( const disposables = serviceContainer.get(IDisposableRegistry); const extensions = serviceContainer.get(IExtensions); const envVarsProvider = serviceContainer.get(IEnvironmentVariablesProvider); - function sendApiTelemetry(apiName: string) { - extensions - .determineExtensionFromCallStack() - .then((info) => { - sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { - apiName, - extensionId: info.extensionId, - }); - traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); - }) - .ignoreErrors(); + function sendApiTelemetry(apiName: string, args?: unknown) { + setTimeout(() => + extensions + .determineExtensionFromCallStack() + .then((info) => { + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + traceVerbose( + `Extension ${info.extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`, + ); + }) + .ignoreErrors(), + ); } disposables.push( discoveryApi.onChanged((e) => { @@ -229,6 +233,9 @@ export function buildProposedApi( return onDidActiveInterpreterChangedEvent.event; }, resolveEnvironment: async (env: Environment | EnvironmentPath | string) => { + if (!workspace.isTrusted) { + throw new Error('Not allowed to resolve environment in an untrusted workspace'); + } let path = typeof env !== 'string' ? env.path : env; if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. @@ -247,7 +254,7 @@ export function buildProposedApi( } path = fullyQualifiedPath; } - sendApiTelemetry('resolveEnvironment'); + sendApiTelemetry('resolveEnvironment', env); return resolveEnvironment(path, discoveryApi); }, get known(): Environment[] { @@ -258,6 +265,10 @@ export function buildProposedApi( .map((e) => convertEnvInfoAndGetReference(e)); }, async refreshEnvironments(options?: RefreshOptions) { + if (!workspace.isTrusted) { + traceError('Not allowed to refresh environments in an untrusted workspace'); + return; + } await discoveryApi.triggerRefresh(undefined, { ifNotTriggerredAlready: !options?.forceRefresh, }); @@ -277,7 +288,11 @@ async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Pr if (!env) { return undefined; } - return getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; + const resolvedEnv = getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; + if (resolvedEnv.version?.major === -1 || resolvedEnv.version?.minor === -1 || resolvedEnv.version?.micro === -1) { + traceError(`Invalid version for ${path}: ${JSON.stringify(env)}`); + } + return resolvedEnv; } export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { diff --git a/extensions/positron-python/src/client/proposedApiTypes.ts b/extensions/positron-python/src/client/proposedApiTypes.ts index 7e08e1eede82..a40ad3312d02 100644 --- a/extensions/positron-python/src/client/proposedApiTypes.ts +++ b/extensions/positron-python/src/client/proposedApiTypes.ts @@ -17,8 +17,8 @@ export interface ProposedExtensionAPI { /** * Sets the active environment path for the python extension for the resource. Configuration target will always * be the workspace folder. - * @param environment : Full path to environment folder or python executable for the environment. Can also pass - * the environment itself. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ updateActiveEnvironmentPath( @@ -55,8 +55,8 @@ export interface ProposedExtensionAPI { refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; /** * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Full path to environment folder or python executable for the environment. Can also pass - * the environment itself. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. */ resolveEnvironment( environment: Environment | EnvironmentPath | string, diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts index b24f86f75e07..565be30acf94 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -3,7 +3,7 @@ import { Uri } from 'vscode'; import { IDisposableRegistry } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; +import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; import { createRunningWorkerPool, IWorkerPool, QueuePosition } from '../../../common/utils/workerPool'; import { getInterpreterInfo, InterpreterInformation } from './interpreter'; import { buildPythonExecInfo } from '../../exec'; @@ -38,7 +38,7 @@ export interface IEnvironmentInfoService { } async function buildEnvironmentInfo(env: PythonEnvInfo): Promise { - const python = [env.executable.filename, OUTPUT_MARKER_SCRIPT]; + const python = [env.executable.filename, '-I', OUTPUT_MARKER_SCRIPT]; const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(python, undefined, env.executable.filename)); return interpreterInfo; } @@ -50,7 +50,7 @@ async function buildEnvironmentInfoUsingCondaRun(env: PythonEnvInfo): Promise { if (env.kind === PythonEnvKind.Conda && env.executable.filename === 'python') { const emptyInterpreterInfo: InterpreterInformation = { @@ -163,6 +164,12 @@ class EnvironmentInfoService implements IEnvironmentInfoService { traceError(reason); } } + if (r === undefined && retryOnce) { + // Retry once, in case the environment was not fully populated. Also observed in CI: + // https://github.com/microsoft/vscode-python/issues/20147 where running environment the first time + // failed due to unknown reasons. + return sleep(2000).then(() => this._getEnvironmentInfo(env, priority, false)); + } return r; } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/info/interpreter.ts b/extensions/positron-python/src/client/pythonEnvironments/base/info/interpreter.ts index 94c686370174..f696bd40d173 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/info/interpreter.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/info/interpreter.ts @@ -89,13 +89,16 @@ export async function getInterpreterInfo( const result = await shellExecute(quoted, { timeout: timeout ?? 15000 }); if (result.stderr) { traceError( - `Stderr when executing script with ${argv} stderr: ${result.stderr}, still attempting to parse output`, + `Stderr when executing script with >> ${quoted} << stderr: ${result.stderr}, still attempting to parse output`, ); } - const json = parse(result.stdout); - if (!json) { + let json: InterpreterInfoJson; + try { + json = parse(result.stdout); + } catch (ex) { + traceError(`Failed to parse interpreter information for >> ${quoted} << with ${ex}`); return undefined; } - traceInfo(`Found interpreter for ${argv}`); + traceInfo(`Found interpreter for >> ${quoted} <<: ${JSON.stringify(json)}`); return extractInterpreterInfo(python.pythonExecutable, json); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index b40f63fe9d27..b8ae5bcf4cd2 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -3,7 +3,7 @@ import { Event } from 'vscode'; import { isTestExecution } from '../../../../common/constants'; -import { traceInfo } from '../../../../logging'; +import { traceInfo, traceVerbose } from '../../../../logging'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; import { PythonEnvInfo } from '../../info'; import { areEnvsDeepEqual, areSameEnv, getEnvPath } from '../../info/env'; @@ -99,8 +99,9 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher cachedEnv.id === env.id)) { return true; } + if (Array.from(this.validatedEnvs.keys()).some((envId) => cachedEnv.id === envId)) { + // These envs are provided by the consumer themselves, consider them valid. + return true; + } } else { return true; } @@ -141,6 +146,7 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher areSameEnv(e, env)); if (hasLatestInfo) { + traceVerbose(`Flushing env to cache ${env.id}`); this.validatedEnvs.add(env.id!); this.flush(env).ignoreErrors(); // If we have latest info, flush it so it can be saved. } @@ -172,13 +178,16 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher arePathsSame(e.location, path)) ?? this.envs.find((e) => areSameEnv(e, path)); if (env) { if (this.validatedEnvs.has(env.id!)) { + traceVerbose(`Found cached env for ${path}`); return env; } - if (await validateInfo(env)) { + if (await this.validateInfo(env)) { + traceVerbose(`Needed to validate ${path} with latest info`); this.validatedEnvs.add(env.id!); return env; } } + traceVerbose(`No cached env found for ${path}`); return undefined; } @@ -207,16 +216,24 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { traceError(`Failed to resolve ${path}`, ex); return undefined; }); + traceVerbose(`Resolved ${path} to ${JSON.stringify(resolved)}`); if (resolved) { this.cache.addEnv(resolved, true); } diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index c3d159d2ff84..4d68370ea262 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -52,6 +52,9 @@ export class PythonEnvsResolver implements IResolvingLocator { const kind = await identifyEnvironment(path); const environment = await resolveBasicEnv({ kind, executablePath, envPath }); const info = await this.environmentInfoService.getEnvironmentInfo(environment); + traceVerbose( + `Environment resolver resolved ${path} for ${JSON.stringify(environment)} to ${JSON.stringify(info)}`, + ); if (!info) { return undefined; } @@ -177,7 +180,6 @@ function checkIfFinishedAndNotify( function getResolvedEnv(interpreterInfo: InterpreterInformation, environment: PythonEnvInfo) { // Deep copy into a new object const resolvedEnv = cloneDeep(environment); - resolvedEnv.executable.filename = interpreterInfo.executable.filename; resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; const isEnvLackingPython = getEnvPath(resolvedEnv.executable.filename, resolvedEnv.location).pathType === 'envFolderPath'; diff --git a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts index 5df4f366e86d..0b210c05b6a1 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts @@ -7,13 +7,13 @@ import * as path from 'path'; import { chain, iterable } from '../../../../common/utils/async'; import { PythonEnvKind } from '../../info'; import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; -import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; import { getInterpreterPathFromDir } from '../../../common/commonUtils'; import { pathExists } from '../../../common/externalDependencies'; import { isPoetryEnvironment, localPoetryEnvDirName, Poetry } from '../../../common/environmentManagers/poetry'; import '../../../../common/extensions'; import { asyncFilter } from '../../../../common/utils/arrayUtils'; import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; /** * Gets all default virtual environment locations to look for in a workspace. @@ -28,27 +28,6 @@ async function getVirtualEnvDirs(root: string): Promise { return asyncFilter(envDirs, pathExists); } -async function getRootVirtualEnvDir(root: string): Promise { - const rootDirs = []; - const poetry = await Poetry.getPoetry(root); - /** - * We can infer the directory in which the existing poetry environments are created to determine - * the root virtual env dir. If no virtual envs are created yet, then fetch the setting value to - * get the root directory instead. We prefer to use 'poetry env list' command first because the - * result of that command is already cached when getting poetry. - */ - const virtualenvs = await poetry?.getEnvList(); - if (virtualenvs?.length) { - rootDirs.push(path.dirname(virtualenvs[0])); - } else { - const setting = await poetry?.getVirtualenvsPathSetting(); - if (setting) { - rootDirs.push(setting); - } - } - return rootDirs; -} - async function getVirtualEnvKind(interpreterPath: string): Promise { if (await isPoetryEnvironment(interpreterPath)) { return PythonEnvKind.Poetry; @@ -60,16 +39,11 @@ async function getVirtualEnvKind(interpreterPath: string): Promise getRootVirtualEnvDir(root), - async () => PythonEnvKind.Poetry, - undefined, - FSWatcherKind.Workspace, - ); + super(); } protected doIterEnvs(): IPythonEnvsIterator { diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts index 7325c4cd69de..bdbcb7ea3ac5 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -435,7 +435,7 @@ export class Conda { // eslint-disable-next-line class-methods-use-this private async getInfoImpl(command: string): Promise { const result = await exec(command, ['info', '--json'], { timeout: CONDA_GENERAL_TIMEOUT }); - traceVerbose(`conda info --json: ${result.stdout}`); + traceVerbose(`${command} info --json: ${result.stdout}`); return JSON.parse(result.stdout); } @@ -506,7 +506,11 @@ export class Conda { return 'python'; } - public async getRunPythonArgs(env: CondaEnvInfo, forShellExecution?: boolean): Promise { + public async getRunPythonArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + isolatedFlag = false, + ): Promise { const condaVersion = await this.getCondaVersion(); if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { traceError('`conda run` is not supported for conda version', condaVersion.raw); @@ -518,14 +522,17 @@ export class Conda { } else { args.push('-p', env.prefix); } - return [ + const python = [ forShellExecution ? this.shellCommand : this.command, 'run', ...args, '--no-capture-output', 'python', - OUTPUT_MARKER_SCRIPT, ]; + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; } /** diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts index 4128c3fe6108..19e3bd80af59 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -142,10 +142,14 @@ export class Poetry { traceVerbose(`Getting poetry for cwd ${cwd}`); // Produce a list of candidate binaries to be probed by exec'ing them. function* getCandidates() { - const customPoetryPath = getPythonSetting('poetryPath'); - if (customPoetryPath && customPoetryPath !== 'poetry') { - // If user has specified a custom poetry path, use it first. - yield customPoetryPath; + try { + const customPoetryPath = getPythonSetting('poetryPath'); + if (customPoetryPath && customPoetryPath !== 'poetry') { + // If user has specified a custom poetry path, use it first. + yield customPoetryPath; + } + } catch (ex) { + traceError(`Failed to get poetry setting`, ex); } // Check unqualified filename, in case it's on PATH. yield 'poetry'; diff --git a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts index cd44cf00efce..54f614ebdd49 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/common/externalDependencies.ts @@ -10,6 +10,7 @@ import { IDisposable, IConfigurationService } from '../../common/types'; import { chain, iterable } from '../../common/utils/async'; import { getOSType, OSType } from '../../common/utils/platform'; import { IServiceContainer } from '../../ioc/types'; +import { traceVerbose } from '../../logging'; let internalServiceContainer: IServiceContainer; export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { @@ -116,6 +117,7 @@ export async function getFileInfo(filePath: string): Promise<{ ctime: number; mt } catch (ex) { // This can fail on some cases, such as, `reparse points` on windows. So, return the // time as -1. Which we treat as not set in the extension. + traceVerbose(`Failed to get file info for ${filePath}`, ex); return { ctime: -1, mtime: -1 }; } } diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/common/workspaceSelection.ts index b2dc97882e23..625aaa5c265e 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/common/workspaceSelection.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -6,7 +6,8 @@ import * as path from 'path'; import { CancellationToken, QuickPickItem, WorkspaceFolder } from 'vscode'; import { showErrorMessage, showQuickPick } from '../../../common/vscodeApis/windowApis'; import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; -import { CreateEnv } from '../../../common/utils/localize'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; function hasVirtualEnv(workspace: WorkspaceFolder): Promise { return Promise.race([ @@ -39,7 +40,10 @@ export async function pickWorkspaceFolder( const workspaces = getWorkspaceFolders(); if (!workspaces || workspaces.length === 0) { - showErrorMessage(CreateEnv.noWorkspace); + const result = await showErrorMessage(CreateEnv.noWorkspace, Common.openFolder); + if (result === Common.openFolder) { + await executeCommand('vscode.openFolder'); + } return undefined; } diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvApi.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvApi.ts index 76263dd9315a..b05670feae02 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvApi.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -3,7 +3,7 @@ import { ConfigurationTarget, Disposable } from 'vscode'; import { Commands } from '../../common/constants'; -import { IDisposableRegistry, IInterpreterPathService } from '../../common/types'; +import { IDisposableRegistry, IInterpreterPathService, IPathUtils } from '../../common/types'; import { registerCommand } from '../../common/vscodeApis/commandApis'; import { IInterpreterQuickPick } from '../../interpreter/configuration/types'; import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; @@ -48,6 +48,7 @@ export function registerCreateEnvironmentFeatures( disposables: IDisposableRegistry, interpreterQuickPick: IInterpreterQuickPick, interpreterPathService: IInterpreterPathService, + pathUtils: IPathUtils, ): void { disposables.push( registerCommand( @@ -64,7 +65,7 @@ export function registerCreateEnvironmentFeatures( onCreateEnvironmentExited(async (e: CreateEnvironmentResult | undefined) => { if (e && e.path) { await interpreterPathService.update(e.uri, ConfigurationTarget.WorkspaceFolder, e.path); - showInformationMessage(`${CreateEnv.informEnvCreation} ${e.path}`); + showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); } }), ); diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index cf0ac950410c..3bc927d898e9 100644 --- a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -8,7 +8,7 @@ import { createVenvScript } from '../../../common/process/internal/scripts'; import { execObservable } from '../../../common/process/rawProcessApis'; import { createDeferred } from '../../../common/utils/async'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { traceError, traceLog } from '../../../logging'; +import { traceError, traceInfo, traceLog } from '../../../logging'; import { CreateEnvironmentOptions, CreateEnvironmentProgress, @@ -23,23 +23,22 @@ import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; import { showErrorMessageWithLogs } from '../common/commonUtils'; +import { IPackageInstallSelection, pickPackagesToInstall } from './venvUtils'; -function generateCommandArgs(options?: CreateEnvironmentOptions): string[] { - let addGitIgnore = true; - let installPackages = true; - if (options) { - addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; - installPackages = options?.installPackages !== undefined ? options.installPackages : true; - } - +function generateCommandArgs(installInfo?: IPackageInstallSelection, addGitIgnore?: boolean): string[] { const command: string[] = [createVenvScript()]; if (addGitIgnore) { command.push('--git-ignore'); } - if (installPackages) { - command.push('--install'); + if (installInfo) { + if (installInfo?.installType === 'toml') { + command.push('--toml', installInfo.source?.fileToCommandArgumentForPythonExt() || 'pyproject.toml'); + installInfo.installList?.forEach((i) => command.push('--extras', i)); + } else if (installInfo?.installType === 'requirements') { + installInfo.installList?.forEach((i) => command.push('--requirements', i)); + } } return command; @@ -128,51 +127,62 @@ export class VenvCreationProvider implements CreateEnvironmentProvider { [EnvironmentType.System, EnvironmentType.MicrosoftStore, EnvironmentType.Global].includes(i.envType), ); - if (interpreter) { - return withProgress( - { - location: ProgressLocation.Notification, - title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, - cancellable: true, - }, - async ( - progress: CreateEnvironmentProgress, - token: CancellationToken, - ): Promise => { - let hasError = false; - - progress.report({ - message: CreateEnv.statusStarting, - }); - - let envPath: string | undefined; - try { - if (interpreter) { - envPath = await createVenv( - workspace, - interpreter, - generateCommandArgs(options), - progress, - token, - ); - } - } catch (ex) { - traceError(ex); - hasError = true; - throw ex; - } finally { - if (hasError) { - showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); - } - } + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + let installInfo: IPackageInstallSelection | undefined; + if (installPackages) { + installInfo = await pickPackagesToInstall(workspace); + } + const args = generateCommandArgs(installInfo, addGitIgnore); - return { path: envPath, uri: workspace.uri }; - }, - ); + if (!interpreter) { + traceError('Virtual env creation requires an interpreter.'); + return undefined; } - traceError('Virtual env creation requires an interpreter.'); - return undefined; + if (!installInfo) { + traceInfo('Virtual env creation exited during dependencies selection.'); + return undefined; + } + + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => { + let hasError = false; + + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter) { + envPath = await createVenv(workspace, interpreter, args, progress, token); + } + } catch (ex) { + traceError(ex); + hasError = true; + throw ex; + } finally { + if (hasError) { + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + } + } + + return { path: envPath, uri: workspace.uri }; + }, + ); } name = 'Venv'; diff --git a/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts new file mode 100644 index 000000000000..f1644c8597fa --- /dev/null +++ b/extensions/positron-python/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as tomljs from '@iarna/toml'; +import * as fs from 'fs-extra'; +import { flatten, isArray } from 'lodash'; +import * as path from 'path'; +import { CancellationToken, QuickPickItem, RelativePattern, WorkspaceFolder } from 'vscode'; +import { CreateEnv } from '../../../common/utils/localize'; +import { showQuickPick } from '../../../common/vscodeApis/windowApis'; +import { findFiles } from '../../../common/vscodeApis/workspaceApis'; +import { traceError, traceVerbose } from '../../../logging'; + +const exclude = '**/{.venv*,.git,.nox,.tox,.conda}/**'; +async function getPipRequirementsFiles( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const files = flatten( + await Promise.all([ + findFiles(new RelativePattern(workspaceFolder, '**/*requirement*.txt'), exclude, undefined, token), + findFiles(new RelativePattern(workspaceFolder, '**/requirements/*.txt'), exclude, undefined, token), + ]), + ).map((u) => u.fsPath); + return files; +} + +async function getTomlOptionalDeps(tomlPath: string): Promise { + const content = await fs.readFile(tomlPath, 'utf-8'); + const extras: string[] = []; + try { + const toml = tomljs.parse(content); + if (toml.project && (toml.project as Record>)['optional-dependencies']) { + const deps = (toml.project as Record>>)['optional-dependencies']; + for (const key of Object.keys(deps)) { + extras.push(key); + } + } + } catch (err) { + traceError('Failed to parse `pyproject.toml`:', err); + } + return extras; +} + +async function pickTomlExtras(extras: string[], token?: CancellationToken): Promise { + const items: QuickPickItem[] = extras.map((e) => ({ label: e })); + + const selection = await showQuickPick( + items, + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + canPickMany: true, + ignoreFocusOut: true, + }, + token, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +async function pickRequirementsFiles(files: string[], token?: CancellationToken): Promise { + const items: QuickPickItem[] = files + .sort((a, b) => { + const al: number = a.split(/[\\\/]/).length; + const bl: number = b.split(/[\\\/]/).length; + if (al === bl) { + if (a.length === b.length) { + return a.localeCompare(b); + } + return a.length - b.length; + } + return al - bl; + }) + .map((e) => ({ label: e })); + + const selection = await showQuickPick( + items, + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + token, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +export interface IPackageInstallSelection { + installType: 'toml' | 'requirements' | 'none'; + installList: string[]; + source?: string; +} + +export async function pickPackagesToInstall( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const tomlPath = path.join(workspaceFolder.uri.fsPath, 'pyproject.toml'); + traceVerbose(`Looking for toml pyproject.toml with optional dependencies at: ${tomlPath}`); + + let extras: string[] = []; + let tomlExists = false; + if (await fs.pathExists(tomlPath)) { + tomlExists = true; + extras = await getTomlOptionalDeps(tomlPath); + } + + if (tomlExists) { + if (extras.length === 0) { + return { installType: 'toml', installList: [], source: tomlPath }; + } + traceVerbose('Found toml with optional dependencies.'); + const installList = await pickTomlExtras(extras, token); + if (installList) { + return { installType: 'toml', installList, source: tomlPath }; + } + return undefined; + } + + traceVerbose('Looking for pip requirements.'); + const requirementFiles = (await getPipRequirementsFiles(workspaceFolder, token))?.map((p) => + path.relative(workspaceFolder.uri.fsPath, p), + ); + + if (requirementFiles && requirementFiles.length > 0) { + traceVerbose('Found pip requirements.'); + const installList = (await pickRequirementsFiles(requirementFiles, token))?.map((p) => + path.join(workspaceFolder.uri.fsPath, p), + ); + if (installList) { + return { installType: 'requirements', installList }; + } + return undefined; + } + + return { installType: 'none', installList: [] }; +} diff --git a/extensions/positron-python/src/client/telemetry/constants.ts b/extensions/positron-python/src/client/telemetry/constants.ts index de97d817cb88..4a895ab8a9ff 100644 --- a/extensions/positron-python/src/client/telemetry/constants.ts +++ b/extensions/positron-python/src/client/telemetry/constants.ts @@ -30,6 +30,7 @@ export enum EventName { PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', EXECUTION_CODE = 'EXECUTION_CODE', diff --git a/extensions/positron-python/src/client/telemetry/index.ts b/extensions/positron-python/src/client/telemetry/index.ts index 08e1b7b72aaa..c833922ace30 100644 --- a/extensions/positron-python/src/client/telemetry/index.ts +++ b/extensions/positron-python/src/client/telemetry/index.ts @@ -2,11 +2,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import TelemetryReporter from '@vscode/extension-telemetry/lib/telemetryReporter'; +import TelemetryReporter from '@vscode/extension-telemetry'; import { DiagnosticCodes } from '../application/diagnostics/constants'; import { IWorkspaceService } from '../common/application/types'; -import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; +import { AppinsightsKey, isTestExecution, isUnitTestExecution } from '../common/constants'; import type { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; import { isPromise } from '../common/utils/async'; @@ -80,14 +80,9 @@ function getTelemetryReporter() { if (!isTestExecution() && telemetryReporter) { return telemetryReporter; } - const extensionId = PVSC_EXTENSION_ID; - - const { extensions } = require('vscode') as typeof import('vscode'); - const extension = extensions.getExtension(extensionId)!; - const extensionVersion = extension.packageJSON.version; const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; - telemetryReporter = new Reporter(extensionId, extensionVersion, AppinsightsKey, true, [ + telemetryReporter = new Reporter(AppinsightsKey, [ { lookup: /(errorName|errorMessage|errorStack)/g, }, @@ -1304,8 +1299,9 @@ export interface IEventNamePropertyMapping { environmentsWithoutPython?: number; }; /** - * Telemetry event sent with details when user clicks the prompt with the following message - * `Prompt message` :- 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?' + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?' */ /* __GDPR__ "conda_inherit_env_prompt" : { @@ -1320,6 +1316,23 @@ export interface IEventNamePropertyMapping { */ selection: 'Yes' | 'No' | 'More Info' | undefined; }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message: + * + * 'We noticed VS Code was launched from an activated conda environment, would you like to select it?' + */ + /* __GDPR__ + "activated_conda_env_launch" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ + [EventName.ACTIVATED_CONDA_ENV_LAUNCH]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + */ + selection: 'Yes' | 'No' | undefined; + }; /** * Telemetry event sent with details when user clicks a button in the virtual environment prompt. * `Prompt message` :- 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' @@ -2050,7 +2063,7 @@ export interface IEventNamePropertyMapping { * Telemetry event sent if installing packages failed. */ /* __GDPR__ - "environment.installing_packages" : { + "environment.installing_packages_failed" : { "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } } diff --git a/extensions/positron-python/src/client/telemetry/pylance.ts b/extensions/positron-python/src/client/telemetry/pylance.ts index dd107dbdd5f1..5fae11fe4a47 100644 --- a/extensions/positron-python/src/client/telemetry/pylance.ts +++ b/extensions/positron-python/src/client/telemetry/pylance.ts @@ -64,7 +64,6 @@ "autoimport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "dictionarykey" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "memberaccess" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "keyword" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -205,7 +204,7 @@ */ /* __GDPR__ "language_server/installed_packages" : { - "packages" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "packagesbitarray" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "packageslowercase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "resolverid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts index 132547d6a4b7..5d5b9cd1bb35 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardSession.ts @@ -9,6 +9,7 @@ import { env, Event, EventEmitter, + l10n, Position, Progress, ProgressLocation, @@ -23,7 +24,6 @@ import { window, workspace, } from 'vscode'; -import * as nls from 'vscode-nls'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { createPromiseFromCancellation } from '../common/cancellation'; import { tensorboardLauncher } from '../common/process/internal/scripts'; @@ -35,6 +35,7 @@ import { ProductInstallStatus, Product, IPersistentState, + IConfigurationService, } from '../common/types'; import { createDeferred, sleep } from '../common/utils/async'; import { Common, TensorBoard } from '../common/utils/localize'; @@ -48,8 +49,6 @@ import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { ModuleInstallFlags } from '../common/installer/types'; import { traceError, traceInfo } from '../logging'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - enum Messages { JumpToSource = 'jump_to_source', } @@ -100,6 +99,7 @@ export class TensorBoardSession { private readonly applicationShell: IApplicationShell, private readonly globalMemento: IPersistentState, private readonly multiStepFactory: IMultiStepInputFactory, + private readonly configurationService: IConfigurationService, ) {} public get onDidDispose(): Event { @@ -341,10 +341,10 @@ export class TensorBoardSession { // the editor, if any, then the directory that the active text editor is in, if any. private async getLogDirectory(): Promise { // See if the user told us to always use a specific log directory - const setting = this.workspaceService.getConfiguration('python.tensorBoard'); - const settingValue = setting.get('logDirectory'); + const settings = this.configurationService.getSettings(); + const settingValue = settings.tensorBoard?.logDirectory; if (settingValue) { - traceInfo(`Using log directory specified by python.tensorBoard.logDirectory setting: ${settingValue}`); + traceInfo(`Using log directory resolved by python.tensorBoard.logDirectory setting: ${settingValue}`); return settingValue; } // No log directory in settings. Ask the user which directory to use @@ -357,7 +357,7 @@ export class TensorBoardSession { const item = await this.applicationShell.showQuickPick(items, { canPickMany: false, ignoreFocusOut: false, - placeHolder: logDir ? localize('TensorBoard.currentDirectory', 'Current: {0}', logDir) : undefined, + placeHolder: logDir ? l10n.t('Current: {0}', logDir) : undefined, }); switch (item?.label) { case useCurrentWorkingDirectory: diff --git a/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts b/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts index 1039aa1167d7..f689dab6a6ba 100644 --- a/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/extensions/positron-python/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -2,14 +2,19 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ViewColumn } from 'vscode'; -import * as nls from 'vscode-nls'; +import { l10n, ViewColumn } from 'vscode'; import { IExtensionSingleActivationService } from '../activation/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { Commands } from '../common/constants'; import { ContextKey } from '../common/contextKey'; import { IPythonExecutionFactory } from '../common/process/types'; -import { IDisposableRegistry, IInstaller, IPersistentState, IPersistentStateFactory } from '../common/types'; +import { + IDisposableRegistry, + IInstaller, + IPersistentState, + IPersistentStateFactory, + IConfigurationService, +} from '../common/types'; import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; import { IInterpreterService } from '../interpreter/contracts'; import { traceError, traceInfo } from '../logging'; @@ -18,8 +23,6 @@ import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @injectable() @@ -42,6 +45,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, ) { this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( PREFERRED_VIEWGROUP, @@ -102,6 +106,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer this.applicationShell, this.preferredViewGroupMemento, this.multiStepFactory, + this.configurationService, ); newSession.onDidChangeViewState(() => this.updateTensorBoardSessionContext(), this, this.disposables); newSession.onDidDispose((e) => this.didDisposeSession(e), this, this.disposables); @@ -111,8 +116,7 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer } catch (e) { traceError(`Encountered error while starting new TensorBoard session: ${e}`); await this.applicationShell.showErrorMessage( - localize( - 'TensorBoard.failedToStartSessionError', + l10n.t( 'We failed to start a TensorBoard session due to the following error: {0}', (e as Error).message, ), diff --git a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts index 6d4fbf9e92cd..d4f205883cef 100644 --- a/extensions/positron-python/src/client/terminals/codeExecution/helper.ts +++ b/extensions/positron-python/src/client/terminals/codeExecution/helper.ts @@ -3,9 +3,8 @@ import '../../common/extensions'; import { inject, injectable } from 'inversify'; -import { Position, Range, TextEditor, Uri } from 'vscode'; +import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; -import * as nls from 'vscode-nls'; import { IApplicationShell, IDocumentManager } from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; @@ -16,8 +15,6 @@ import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; import { traceError } from '../../logging'; -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly documentManager: IDocumentManager; @@ -86,19 +83,15 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { public async getFileToExecute(): Promise { const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { - this.applicationShell.showErrorMessage(localize('documentError.Empty', 'No open file to run in terminal')); + this.applicationShell.showErrorMessage(l10n.t('No open file to run in terminal')); return undefined; } if (activeEditor.document.isUntitled) { - this.applicationShell.showErrorMessage( - localize('documentError.NoSaved', 'The active file needs to be saved before it can be run'), - ); + this.applicationShell.showErrorMessage(l10n.t('The active file needs to be saved before it can be run')); return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage( - localize('documentError.NotPythonFile', 'The active file is not a Python source file)'), - ); + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file)')); return undefined; } if (activeEditor.document.isDirty) { diff --git a/extensions/positron-python/src/client/testing/common/debugLauncher.ts b/extensions/positron-python/src/client/testing/common/debugLauncher.ts index 5c8bfd537f79..36432c0bd831 100644 --- a/extensions/positron-python/src/client/testing/common/debugLauncher.ts +++ b/extensions/positron-python/src/client/testing/common/debugLauncher.ts @@ -1,84 +1,70 @@ import { inject, injectable, named } from 'inversify'; - import * as path from 'path'; -import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../common/application/types'; +import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; import * as internalScripts from '../../common/process/internal/scripts'; import { IConfigurationService, IPythonSettings } from '../../common/types'; import { DebuggerTypeName } from '../../debugger/constants'; -import { IDebugConfigurationResolver, ILaunchJsonReader } from '../../debugger/extension/configuration/types'; +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; import { IServiceContainer } from '../../ioc/types'; import { traceError } from '../../logging'; import { TestProvider } from '../types'; import { ITestDebugLauncher, LaunchOptions } from './types'; -import * as nls from 'vscode-nls'; - -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { showErrorMessage } from '../../common/vscodeApis/windowApis'; @injectable() export class DebugLauncher implements ITestDebugLauncher { private readonly configService: IConfigurationService; - private readonly workspaceService: IWorkspaceService; + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @inject(ILaunchJsonReader) private readonly launchJsonReader: ILaunchJsonReader, ) { this.configService = this.serviceContainer.get(IConfigurationService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); } - public async launchDebugger(options: LaunchOptions) { + public async launchDebugger(options: LaunchOptions): Promise { if (options.token && options.token.isCancellationRequested) { - return; + return undefined; } - const workspaceFolder = this.resolveWorkspaceFolder(options.cwd); + const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); const launchArgs = await this.getLaunchArgs( options, workspaceFolder, this.configService.getSettings(workspaceFolder.uri), ); const debugManager = this.serviceContainer.get(IDebugService); + return debugManager.startDebugging(workspaceFolder, launchArgs).then( // Wait for debug session to be complete. - () => { - return new Promise((resolve) => { + () => + new Promise((resolve) => { debugManager.onDidTerminateDebugSession(() => { resolve(); }); - }); - }, + }), (ex) => traceError('Failed to start debugging tests', ex), ); } - public async readAllDebugConfigs(workspace: WorkspaceFolder): Promise { - try { - const configs = await this.launchJsonReader.getConfigurationsForWorkspace(workspace); - return configs; - } catch (exc) { - traceError('could not get debug config', exc); - const appShell = this.serviceContainer.get(IApplicationShell); - await appShell.showErrorMessage( - localize('readDebugError', 'Could not load unit test config from launch.json as it is missing a field'), - ); - return []; - } - } - private resolveWorkspaceFolder(cwd: string): WorkspaceFolder { - const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + + private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; if (!hasWorkspaceFolders) { throw new Error('Please open a workspace'); } const cwdUri = cwd ? Uri.file(cwd) : undefined; - let workspaceFolder = this.workspaceService.getWorkspaceFolder(cwdUri); + let workspaceFolder = getWorkspaceFolder(cwdUri); if (!workspaceFolder) { - workspaceFolder = this.workspaceService.workspaceFolders![0]; + const [first] = getWorkspaceFolders()!; + workspaceFolder = first; } return workspaceFolder; } @@ -88,7 +74,7 @@ export class DebugLauncher implements ITestDebugLauncher { workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings, ): Promise { - let debugConfig = await this.readDebugConfig(workspaceFolder); + let debugConfig = await DebugLauncher.readDebugConfig(workspaceFolder); if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', @@ -104,27 +90,50 @@ export class DebugLauncher implements ITestDebugLauncher { path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false, }); - this.applyDefaults(debugConfig!, workspaceFolder, configSettings); + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); } - private async readDebugConfig(workspaceFolder: WorkspaceFolder): Promise { - const configs = await this.readAllDebugConfigs(workspaceFolder); - for (const cfg of configs) { - if (cfg.name && cfg.type === DebuggerTypeName) { + public async readAllDebugConfigs(workspace: WorkspaceFolder): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspace); + return configs; + } catch (exc) { + traceError('could not get debug config', exc); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showErrorMessage( + l10n.t('Could not load unit test config from launch.json as it is missing a field'), + ); + return []; + } + } + + private static async readDebugConfig( + workspaceFolder: WorkspaceFolder, + ): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspaceFolder); + for (const cfg of configs) { if ( - cfg.request === 'test' || - (cfg as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugTest) + cfg.name && + cfg.type === DebuggerTypeName && + (cfg.request === 'test' || + (cfg as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugTest)) ) { // Return the first one. return cfg as LaunchRequestArguments; } } + return undefined; + } catch (exc) { + traceError('could not get debug config', exc); + await showErrorMessage(l10n.t('Could not load unit test config from launch.json as it is missing a field')); + return undefined; } - return undefined; } - private applyDefaults( + + private static applyDefaults( cfg: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings, @@ -145,7 +154,6 @@ export class DebugLauncher implements ITestDebugLauncher { if (!cfg.envFile) { cfg.envFile = configSettings.envFile; } - if (cfg.stopOnEntry === undefined) { cfg.stopOnEntry = false; } @@ -167,11 +175,12 @@ export class DebugLauncher implements ITestDebugLauncher { options: LaunchOptions, ): Promise { const configArgs = debugConfig as LaunchRequestArguments; - - const testArgs = this.fixArgs(options.args, options.testProvider); - const script = this.getTestLauncherScript(options.testProvider); + const testArgs = + options.testProvider === 'unittest' ? options.args.filter((item) => item !== '--debug') : options.args; + const script = DebugLauncher.getTestLauncherScript(options.testProvider); const args = script(testArgs); - configArgs.program = args[0]; + const [program] = args; + configArgs.program = program; configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. @@ -200,15 +209,7 @@ export class DebugLauncher implements ITestDebugLauncher { return launchArgs; } - private fixArgs(args: string[], testProvider: TestProvider): string[] { - if (testProvider === 'unittest') { - return args.filter((item) => item !== '--debug'); - } else { - return args; - } - } - - private getTestLauncherScript(testProvider: TestProvider) { + private static getTestLauncherScript(testProvider: TestProvider) { switch (testProvider) { case 'unittest': { return internalScripts.visualstudio_py_testlauncher; // old way unittest execution, debugger diff --git a/extensions/positron-python/src/setupNls.ts b/extensions/positron-python/src/setupNls.ts deleted file mode 100644 index 22ece182e5ae..000000000000 --- a/extensions/positron-python/src/setupNls.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vscode-nls'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone }); diff --git a/extensions/positron-python/src/test/activation/activationManager.unit.test.ts b/extensions/positron-python/src/test/activation/activationManager.unit.test.ts index 8cc15400b6e5..2b8d54f12ee9 100644 --- a/extensions/positron-python/src/test/activation/activationManager.unit.test.ts +++ b/extensions/positron-python/src/test/activation/activationManager.unit.test.ts @@ -46,6 +46,9 @@ suite('Activation Manager', () => { let fileSystem: IFileSystem; setup(() => { interpreterPathService = typemoq.Mock.ofType(); + interpreterPathService + .setup((i) => i.copyOldInterpreterStorageValuesToNew(typemoq.It.isAny())) + .returns(() => Promise.resolve()); workspaceService = mock(WorkspaceService); activeResourceService = mock(ActiveResourceService); appDiagnostics = typemoq.Mock.ofType(); @@ -66,6 +69,7 @@ suite('Activation Manager', () => { instance(workspaceService), instance(fileSystem), instance(activeResourceService), + interpreterPathService.object, ); sinon.stub(EnvFileTelemetry, 'sendActivationTelemetry').resolves(); @@ -97,6 +101,7 @@ suite('Activation Manager', () => { instance(workspaceService), instance(fileSystem), instance(activeResourceService), + interpreterPathService.object, ); await managerTest.activateWorkspace(resource); @@ -126,6 +131,7 @@ suite('Activation Manager', () => { instance(workspaceService), instance(fileSystem), instance(activeResourceService), + interpreterPathService.object, ); await managerTest.activateWorkspace(resource); diff --git a/extensions/positron-python/src/test/api.test.ts b/extensions/positron-python/src/test/api.test.ts new file mode 100644 index 000000000000..24eb78c11bf0 --- /dev/null +++ b/extensions/positron-python/src/test/api.test.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { IExtensionApi } from '../client/apiTypes'; +import { ProposedExtensionAPI } from '../client/proposedApiTypes'; +import { initialize } from './initialize'; + +suite('Python API tests', () => { + let api: IExtensionApi & ProposedExtensionAPI; + suiteSetup(async () => { + api = await initialize(); + }); + test('Active environment is defined', async () => { + const environmentPath = api.environments.getActiveEnvironmentPath(); + const environment = await api.environments.resolveEnvironment(environmentPath); + expect(environment).to.not.equal( + undefined, + `Active environment is not defined, envPath: ${JSON.stringify(environmentPath)}, env: ${JSON.stringify( + environment, + )}`, + ); + }); +}); diff --git a/extensions/positron-python/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts deleted file mode 100644 index 9feab57af375..000000000000 --- a/extensions/positron-python/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert, expect } from 'chai'; -import * as sinon from 'sinon'; -import * as typemoq from 'typemoq'; -import { DiagnosticSeverity, Uri, WorkspaceConfiguration } from 'vscode'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { - PythonPathDeprecatedDiagnostic, - PythonPathDeprecatedDiagnosticService, -} from '../../../../client/application/diagnostics/checks/pythonPathDeprecated'; -import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { - DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt, -} from '../../../../client/application/diagnostics/promptHandler'; -import { - DiagnosticScope, - IDiagnostic, - IDiagnosticCommand, - IDiagnosticFilterService, - IDiagnosticHandlerService, -} from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IDisposableRegistry, IExperimentService, Resource } from '../../../../client/common/types'; -import { Common, Diagnostics } from '../../../../client/common/utils/localize'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Python Path Deprecated', () => { - const resource = Uri.parse('a'); - let diagnosticService: PythonPathDeprecatedDiagnosticService; - let messageHandler: typemoq.IMock>; - let commandFactory: typemoq.IMock; - let workspaceService: typemoq.IMock; - let filterService: typemoq.IMock; - let experimentsManager: typemoq.IMock; - let serviceContainer: typemoq.IMock; - function createContainer() { - serviceContainer = typemoq.Mock.ofType(); - filterService = typemoq.Mock.ofType(); - experimentsManager = typemoq.Mock.ofType(); - messageHandler = typemoq.Mock.ofType>(); - serviceContainer - .setup((s) => - s.get( - typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), - ), - ) - .returns(() => messageHandler.object); - commandFactory = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) - .returns(() => filterService.object); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IExperimentService))) - .returns(() => experimentsManager.object); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) - .returns(() => commandFactory.object); - workspaceService = typemoq.Mock.ofType(); - serviceContainer - .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); - return serviceContainer.object; - } - suite('Diagnostics', () => { - setup(() => { - diagnosticService = new (class extends PythonPathDeprecatedDiagnosticService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - })(createContainer(), messageHandler.object, []); - (diagnosticService as any)._clear(); - }); - - teardown(() => { - sinon.restore(); - }); - - test('Can handle PythonPathDeprecatedDiagnostic diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.PythonPathDeprecatedDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal( - true, - `Should be able to handle ${DiagnosticCodes.PythonPathDeprecatedDiagnostic}`, - ); - diagnostic.verifyAll(); - }); - test('Can not handle non-PythonPathDeprecatedDiagnostic diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType(); - diagnostic - .setup((d) => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should not display a message if the diagnostic code has been ignored', async () => { - const diagnostic = typemoq.Mock.ofType(); - - filterService - .setup((f) => - f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PythonPathDeprecatedDiagnostic)), - ) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - diagnostic - .setup((d) => d.code) - .returns(() => DiagnosticCodes.PythonPathDeprecatedDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - commandFactory - .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - messageHandler - .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - - filterService.verifyAll(); - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('Python Path Deprecated Diagnostic is handled as expected', async () => { - let invoked = false; - const diagnostic = new PythonPathDeprecatedDiagnostic('message', resource); - const ignoreCmd = ({ - invoke: () => { - invoked = true; - }, - } as any) as IDiagnosticCommand; - filterService - .setup((f) => - f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PythonPathDeprecatedDiagnostic)), - ) - .returns(() => Promise.resolve(false)); - let messagePrompt: MessageCommandPrompt | undefined; - messageHandler - .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - commandFactory - .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((a, b) => { - expect(a).to.be.deep.equal(diagnostic); - expect(b).to.be.deep.equal({ - type: 'ignore', - options: DiagnosticScope.Global, - }); - }) - .returns(() => ignoreCmd) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - expect(invoked).to.equal(true, 'Command should be invoked'); - messageHandler.verifyAll(); - commandFactory.verifyAll(); - expect(messagePrompt).to.be.deep.equal({ - commandPrompts: [ - { - prompt: Common.ok, - }, - ], - }); - }); - test('Handling an empty diagnostic should not show a message nor return a command', async () => { - const diagnostics: IDiagnostic[] = []; - - messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); - commandFactory - .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - await diagnosticService.handle(diagnostics); - - messageHandler.verifyAll(); - commandFactory.verifyAll(); - }); - test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { - const diagnostic = new (class SomeRandomDiagnostic extends BaseDiagnostic { - constructor(message: string, uri: Resource) { - super( - 'SomeRandomDiagnostic' as any, - message, - DiagnosticSeverity.Information, - DiagnosticScope.WorkspaceFolder, - uri, - ); - } - })('message', undefined); - messageHandler - .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.never()); - commandFactory - .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - await diagnosticService.handle([diagnostic]); - - messageHandler.verifyAll(); - commandFactory.verifyAll(); - }); - - test('If a workspace is opened and only workspace value is set, diagnostic with appropriate message is returned', async () => { - const workspaceConfig = typemoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); - workspaceService - .setup((w) => w.getConfiguration('python', resource)) - .returns(() => workspaceConfig.object) - .verifiable(typemoq.Times.once()); - workspaceConfig - .setup((w) => w.inspect('pythonPath')) - .returns( - () => - ({ - workspaceValue: 'workspaceValue', - } as any), - ); - - const diagnostics = await diagnosticService.diagnose(resource); - expect(diagnostics.length).to.equal(1); - expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings); - expect(diagnostics[0].resource).to.equal(resource); - - workspaceService.verifyAll(); - }); - - test('If folder is directly opened and workspace folder value is set, diagnostic with appropriate message is returned', async () => { - const workspaceConfig = typemoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); - workspaceService - .setup((w) => w.getConfiguration('python', resource)) - .returns(() => workspaceConfig.object) - .verifiable(typemoq.Times.once()); - workspaceConfig - .setup((w) => w.inspect('pythonPath')) - .returns( - () => - ({ - workspaceValue: 'workspaceValue', - workspaceFolderValue: 'workspaceFolderValue', - } as any), - ); - - const diagnostics = await diagnosticService.diagnose(resource); - expect(diagnostics.length).to.equal(1); - expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings); - expect(diagnostics[0].resource).to.equal(resource); - - workspaceService.verifyAll(); - }); - - test('If a workspace is opened and both workspace folder value & workspace value is set, diagnostic with appropriate message is returned', async () => { - const workspaceConfig = typemoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); - workspaceService - .setup((w) => w.getConfiguration('python', resource)) - .returns(() => workspaceConfig.object) - .verifiable(typemoq.Times.once()); - workspaceConfig - .setup((w) => w.inspect('pythonPath')) - .returns( - () => - ({ - workspaceValue: 'workspaceValue', - workspaceFolderValue: 'workspaceFolderValue', - } as any), - ); - - const diagnostics = await diagnosticService.diagnose(resource); - expect(diagnostics.length).to.equal(1); - expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings); - expect(diagnostics[0].resource).to.equal(resource); - - workspaceService.verifyAll(); - }); - - test('Otherwise an empty diagnostic is returned', async () => { - const workspaceConfig = typemoq.Mock.ofType(); - workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); - workspaceService - .setup((w) => w.getConfiguration('python', resource)) - .returns(() => workspaceConfig.object) - .verifiable(typemoq.Times.once()); - workspaceConfig.setup((w) => w.inspect('pythonPath')).returns(() => ({} as any)); - - const diagnostics = await diagnosticService.diagnose(resource); - assert.deepEqual(diagnostics, []); - - workspaceService.verifyAll(); - }); - }); -}); diff --git a/extensions/positron-python/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/application/diagnostics/serviceRegistry.unit.test.ts index 1c5ae0a43463..dcff47b2b7e7 100644 --- a/extensions/positron-python/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -30,10 +30,6 @@ import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId, } from '../../../client/application/diagnostics/checks/pythonInterpreter'; -import { - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId, -} from '../../../client/application/diagnostics/checks/pythonPathDeprecated'; import { SwitchToDefaultLanguageServerDiagnosticService, SwitchToDefaultLanguageServerDiagnosticServiceId, @@ -129,13 +125,6 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { InvalidMacPythonInterpreterServiceId, ), ); - verify( - serviceManager.addSingleton( - IDiagnosticsService, - PythonPathDeprecatedDiagnosticService, - PythonPathDeprecatedDiagnosticServiceId, - ), - ); verify( serviceManager.addSingleton( IDiagnosticsService, diff --git a/extensions/positron-python/src/test/common.ts b/extensions/positron-python/src/test/common.ts index e58830b7f236..0a76c495830a 100644 --- a/extensions/positron-python/src/test/common.ts +++ b/extensions/positron-python/src/test/common.ts @@ -14,6 +14,7 @@ import type { IExtensionApi } from '../client/apiTypes'; import { IProcessService } from '../client/common/process/types'; import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; +import { ProposedExtensionAPI } from '../client/proposedApiTypes'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_MULTI_ROOT_TEST, IS_PERF_TEST, IS_SMOKE_TEST } from './constants'; import { noop, sleep } from './core'; @@ -437,7 +438,7 @@ export async function isPythonVersion(...versions: string[]): Promise { } } -export interface IExtensionTestApi extends IExtensionApi { +export interface IExtensionTestApi extends IExtensionApi, ProposedExtensionAPI { serviceContainer: IServiceContainer; serviceManager: IServiceManager; } diff --git a/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts b/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts index c3c94e5411e4..7d2d6230f05b 100644 --- a/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts +++ b/extensions/positron-python/src/test/common/configSettings/configSettings.unit.test.ts @@ -11,6 +11,7 @@ import * as TypeMoq from 'typemoq'; import untildify = require('untildify'); import { WorkspaceConfiguration } from 'vscode'; import { LanguageServerType } from '../../../client/activation/types'; +import { IApplicationEnvironment } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; import { PythonSettings } from '../../../client/common/configSettings'; import { InterpreterPathService } from '../../../client/common/interpreterPathService'; @@ -54,14 +55,18 @@ suite('Python Settings', async () => { undefined, new MockAutoSelectionService(), workspaceService, - new InterpreterPathService(persistentStateFactory, workspaceService, []), + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), undefined, ); settings = new CustomPythonSettings( undefined, new MockAutoSelectionService(), workspaceService, - new InterpreterPathService(persistentStateFactory, workspaceService, []), + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), undefined, ); expected.defaultInterpreterPath = 'python'; diff --git a/extensions/positron-python/src/test/common/extensions.unit.test.ts b/extensions/positron-python/src/test/common/extensions.unit.test.ts index 2e282cfc7d43..ffa75515fd9c 100644 --- a/extensions/positron-python/src/test/common/extensions.unit.test.ts +++ b/extensions/positron-python/src/test/common/extensions.unit.test.ts @@ -20,6 +20,21 @@ suite('String Extensions', () => { const argTotest = 'one two three'; expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); }); + test('Should quote file paths containing one of the parentheses: ( ', () => { + const fileToTest = 'user/code(1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + + test('Should quote file paths containing one of the parentheses: ) ', () => { + const fileToTest = 'user)/code1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + + test('Should quote file paths containing both of the parentheses: () ', () => { + const fileToTest = '(user)/code1.py'; + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); + }); + test('Should quote command arguments containing ampersand', () => { const argTotest = 'one&twothree'; expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); diff --git a/extensions/positron-python/src/test/common/installer.test.ts b/extensions/positron-python/src/test/common/installer.test.ts index 90378fa4e959..6c1a6383c2b6 100644 --- a/extensions/positron-python/src/test/common/installer.test.ts +++ b/extensions/positron-python/src/test/common/installer.test.ts @@ -100,6 +100,14 @@ import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; import { UnitTestIocContainer } from '../testing/serviceRegistry'; import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize'; +import { IActivatedEnvironmentLaunch } from '../../client/interpreter/contracts'; +import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; +import { + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager, +} from '../../client/interpreter/configuration/types'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; suite('Installer', () => { let ioc: UnitTestIocContainer; @@ -169,6 +177,18 @@ suite('Installer', () => { TestFrameworkProductPathService, ProductType.TestFramework, ); + ioc.serviceManager.addSingleton( + IActivatedEnvironmentLaunch, + ActivatedEnvironmentLaunch, + ); + ioc.serviceManager.addSingleton( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService, + ); + ioc.serviceManager.addSingleton( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory, + ); ioc.serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); ioc.serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); ioc.serviceManager.addSingleton(IExtensions, Extensions); diff --git a/extensions/positron-python/src/test/common/installer/condaInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/condaInstaller.unit.test.ts index 0124de62034b..64a4a35539e4 100644 --- a/extensions/positron-python/src/test/common/installer/condaInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/condaInstaller.unit.test.ts @@ -41,7 +41,7 @@ suite('Common - Conda Installer', () => { test('Name and priority', async () => { assert.strictEqual(installer.displayName, 'Conda'); assert.strictEqual(installer.name, 'Conda'); - assert.strictEqual(installer.priority, 0); + assert.strictEqual(installer.priority, 10); }); test('Installer is not supported when conda is available variable is set to false', async () => { const uri = Uri.file(__filename); diff --git a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts index af0562b5ba13..5c77f8165492 100644 --- a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts @@ -641,9 +641,6 @@ suite('Module Installer', () => { moduleName, '--dev', ]; - if (moduleName === 'black') { - expectedArgs.push('--pre'); - } await installModuleAndVerifyCommand( pipenvName, expectedArgs, diff --git a/extensions/positron-python/src/test/common/installer/poetryInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/poetryInstaller.unit.test.ts index 9046ce59d1b0..8c2c3614d184 100644 --- a/extensions/positron-python/src/test/common/installer/poetryInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/poetryInstaller.unit.test.ts @@ -115,7 +115,7 @@ suite('Module Installer - Poetry', () => { const info = await poetryInstaller.getExecutionInfo('black', uri); assert.deepEqual(info, { - args: ['add', '--group', 'dev', 'black', '--allow-prereleases'], + args: ['add', '--group', 'dev', 'black'], execPath: 'poetry path', }); }); diff --git a/extensions/positron-python/src/test/common/interpreterPathService.unit.test.ts b/extensions/positron-python/src/test/common/interpreterPathService.unit.test.ts index 4dbd6577b0a3..6ba63d9d663d 100644 --- a/extensions/positron-python/src/test/common/interpreterPathService.unit.test.ts +++ b/extensions/positron-python/src/test/common/interpreterPathService.unit.test.ts @@ -14,7 +14,7 @@ import { Uri, WorkspaceConfiguration, } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; import { defaultInterpreterPathSetting, InterpreterPathService } from '../../client/common/interpreterPathService'; import { FileSystemPaths } from '../../client/common/platform/fs-paths'; import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; @@ -24,6 +24,7 @@ suite('Interpreter Path Service', async () => { let interpreterPathService: InterpreterPathService; let persistentStateFactory: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; + let appEnvironment: TypeMoq.IMock; const resource = Uri.parse('a'); const resourceOutsideOfWorkspace = Uri.parse('b'); const interpreterPath = 'path/to/interpreter'; @@ -31,6 +32,8 @@ suite('Interpreter Path Service', async () => { setup(() => { const event = TypeMoq.Mock.ofType>(); workspaceService = TypeMoq.Mock.ofType(); + appEnvironment = TypeMoq.Mock.ofType(); + appEnvironment.setup((a) => a.remoteName).returns(() => undefined); workspaceService .setup((w) => w.getWorkspaceFolder(resource)) .returns(() => ({ @@ -41,7 +44,12 @@ suite('Interpreter Path Service', async () => { workspaceService.setup((w) => w.getWorkspaceFolder(resourceOutsideOfWorkspace)).returns(() => undefined); persistentStateFactory = TypeMoq.Mock.ofType(); workspaceService.setup((w) => w.onDidChangeConfiguration).returns(() => event.object); - interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + interpreterPathService = new InterpreterPathService( + persistentStateFactory.object, + workspaceService.object, + [], + appEnvironment.object, + ); }); teardown(() => { diff --git a/extensions/positron-python/src/test/common/moduleInstaller.test.ts b/extensions/positron-python/src/test/common/moduleInstaller.test.ts index ef5f0aeb3d80..302587902c16 100644 --- a/extensions/positron-python/src/test/common/moduleInstaller.test.ts +++ b/extensions/positron-python/src/test/common/moduleInstaller.test.ts @@ -1,7 +1,7 @@ import { expect, should as chaiShould, use as chaiUse } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; -import { instance, mock } from 'ts-mockito'; +import { instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../client/activation/types'; @@ -90,7 +90,12 @@ import { import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Architecture } from '../../client/common/utils/platform'; import { Random } from '../../client/common/utils/random'; -import { ICondaService, IInterpreterService, IComponentAdapter } from '../../client/interpreter/contracts'; +import { + ICondaService, + IInterpreterService, + IComponentAdapter, + IActivatedEnvironmentLaunch, +} from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { JupyterExtensionDependencyManager } from '../../client/jupyter/jupyterExtensionDependencyManager'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; @@ -163,7 +168,12 @@ suite('Module Installer', () => { ITerminalServiceFactory, mockTerminalFactory.object, ); - + const activatedEnvironmentLaunch = mock(); + when(activatedEnvironmentLaunch.selectIfLaunchedViaActivatedEnv()).thenResolve(undefined); + ioc.serviceManager.addSingletonInstance( + IActivatedEnvironmentLaunch, + instance(activatedEnvironmentLaunch), + ); ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); ioc.serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); diff --git a/extensions/positron-python/src/test/common/process/pythonExecutionFactory.unit.test.ts b/extensions/positron-python/src/test/common/process/pythonExecutionFactory.unit.test.ts index 8035c676c188..e31a9e4d900e 100644 --- a/extensions/positron-python/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/extensions/positron-python/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -26,7 +26,11 @@ import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } f import { Architecture } from '../../../client/common/utils/platform'; import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IComponentAdapter, IInterpreterService } from '../../../client/interpreter/contracts'; +import { + IActivatedEnvironmentLaunch, + IComponentAdapter, + IInterpreterService, +} from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { ServiceContainer } from '../../../client/ioc/container'; import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; @@ -73,6 +77,7 @@ suite('Process - PythonExecutionFactory', () => { suite(title(resource, interpreter), () => { let factory: PythonExecutionFactory; let activationHelper: IEnvironmentActivationService; + let activatedEnvironmentLaunch: IActivatedEnvironmentLaunch; let processFactory: IProcessServiceFactory; let configService: IConfigurationService; let processLogger: IProcessLogger; @@ -122,6 +127,11 @@ suite('Process - PythonExecutionFactory', () => { when(serviceContainer.get(IInterpreterService)).thenReturn( instance(interpreterService), ); + activatedEnvironmentLaunch = mock(); + when(activatedEnvironmentLaunch.selectIfLaunchedViaActivatedEnv()).thenResolve(); + when(serviceContainer.get(IActivatedEnvironmentLaunch)).thenReturn( + instance(activatedEnvironmentLaunch), + ); when(serviceContainer.get(IComponentAdapter)).thenReturn(instance(pyenvs)); when(serviceContainer.tryGet(IInterpreterService)).thenReturn( instance(interpreterService), @@ -153,7 +163,22 @@ suite('Process - PythonExecutionFactory', () => { verify(pythonSettings.pythonPath).once(); }); - test('If interpreter is explicitly set, ensure we use it', async () => { + test('If interpreter is explicitly set to `python`, ensure we use it', async () => { + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); + reset(interpreterPathExpHelper); + when(interpreterPathExpHelper.get(anything())).thenReturn('python'); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource, pythonPath: 'python' }); + + expect(service).to.not.equal(undefined); + verify(autoSelection.autoSelectInterpreter(anything())).once(); + }); + + test('Otherwise if interpreter is explicitly set, ensure we use it', async () => { const pythonSettings = mock(PythonSettings); when(processFactory.create(resource)).thenResolve(processService.object); when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); diff --git a/extensions/positron-python/src/test/debugger/envVars.test.ts b/extensions/positron-python/src/test/debugger/envVars.test.ts index e7b251b08a5f..71c5b8e62650 100644 --- a/extensions/positron-python/src/test/debugger/envVars.test.ts +++ b/extensions/positron-python/src/test/debugger/envVars.test.ts @@ -37,7 +37,7 @@ suite('Resolving Environment Variables when Debugging', () => { const envParser = ioc.serviceContainer.get(IEnvironmentVariablesService); const pathUtils = ioc.serviceContainer.get(IPathUtils); mockProcess = ioc.serviceContainer.get(ICurrentProcess); - debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, pathUtils, mockProcess); + debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, mockProcess); pathVariableName = pathUtils.getPathVariableName(); }); suiteTeardown(closeActiveWindows); diff --git a/extensions/positron-python/src/test/debugger/extension/adapter/factory.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/adapter/factory.unit.test.ts index f23e8aa12704..a1e57d44f6cb 100644 --- a/extensions/positron-python/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -7,31 +7,36 @@ import * as assert from 'assert'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; - +import * as sinon from 'sinon'; import rewiremock from 'rewiremock'; import { SemVer } from 'semver'; -import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../../client/common/application/applicationShell'; -import { IApplicationShell } from '../../../../client/common/application/types'; import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IPythonSettings } from '../../../../client/common/types'; +import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; import { Architecture } from '../../../../client/common/utils/platform'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; +import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../../client/debugger/extension/adapter/factory'; import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; import { clearTelemetryReporter } from '../../../../client/telemetry'; import { EventName } from '../../../../client/telemetry/constants'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; use(chaiAsPromised); suite('Debugging - Adapter Factory', () => { let factory: IDebugAdapterDescriptorFactory; let interpreterService: IInterpreterService; - let appShell: IApplicationShell; + let stateFactory: IPersistentStateFactory; + let state: PersistentState; + let showErrorMessageStub: sinon.SinonStub; + let commandManager: ICommandManager; const nodeExecutable = undefined; const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); @@ -63,6 +68,15 @@ suite('Debugging - Adapter Factory', () => { process.env.VSC_PYTHON_CI_TEST = undefined; rewiremock.enable(); rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState; + commandManager = mock(CommandManager); + + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + + when( + stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); const configurationService = mock(ConfigurationService); when(configurationService.getSettings(undefined)).thenReturn(({ @@ -70,12 +84,15 @@ suite('Debugging - Adapter Factory', () => { } as any) as IPythonSettings); interpreterService = mock(InterpreterService); - appShell = mock(ApplicationShell); when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); - factory = new DebugAdapterDescriptorFactory(instance(interpreterService), instance(appShell)); + factory = new DebugAdapterDescriptorFactory( + instance(commandManager), + instance(interpreterService), + instance(stateFactory), + ); }); teardown(() => { @@ -86,6 +103,7 @@ suite('Debugging - Adapter Factory', () => { Reporter.measures = []; rewiremock.disable(); clearTelemetryReporter(); + sinon.restore(); }); function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { @@ -136,7 +154,26 @@ suite('Debugging - Adapter Factory', () => { const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided'); - verify(appShell.showErrorMessage(anyString())).once(); + sinon.assert.calledOnce(showErrorMessageStub); + }); + + test('Display a message if python version is less than 3.7', async () => { + when(interpreterService.getInterpreters(anything())).thenReturn([]); + const session = createSession({}); + const deprecatedInterpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.6.12-test'), + }; + when(state.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + sinon.assert.calledOnce(showErrorMessageStub); }); test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { diff --git a/extensions/positron-python/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts index e17b80ab34f7..0ab094119a5c 100644 --- a/extensions/positron-python/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -4,23 +4,23 @@ 'use strict'; import * as assert from 'assert'; -import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; +import { anyString, anything, mock, when } from 'ts-mockito'; import { DebugSession, WorkspaceFolder } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { ApplicationShell } from '../../../../client/common/application/applicationShell'; -import { IApplicationShell } from '../../../../client/common/application/types'; import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { BrowserService } from '../../../../client/common/net/browser'; -import { IBrowserService, IPythonSettings } from '../../../../client/common/types'; import { createDeferred, sleep } from '../../../../client/common/utils/async'; import { Common } from '../../../../client/common/utils/localize'; import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; import { clearTelemetryReporter } from '../../../../client/telemetry'; +import * as browserApis from '../../../../client/common/vscodeApis/browserApis'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { IPythonSettings } from '../../../../client/common/types'; suite('Debugging - Outdated Debugger Prompt tests.', () => { let promptFactory: OutdatedDebuggerPromptFactory; - let appShell: IApplicationShell; - let browserService: IBrowserService; + let showInformationMessageStub: sinon.SinonStub; + let browserLaunchStub: sinon.SinonStub; const ptvsdOutputEvent: DebugProtocol.OutputEvent = { seq: 1, @@ -42,12 +42,14 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { experiments: { enabled: true }, } as any) as IPythonSettings); - appShell = mock(ApplicationShell); - browserService = mock(BrowserService); - promptFactory = new OutdatedDebuggerPromptFactory(instance(appShell), instance(browserService)); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + browserLaunchStub = sinon.stub(browserApis, 'launch'); + + promptFactory = new OutdatedDebuggerPromptFactory(); }); teardown(() => { + sinon.restore(); clearTelemetryReporter(); }); @@ -68,32 +70,37 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { } test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); - + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); if (prompter) { prompter.onDidSendMessage!(ptvsdOutputEvent); } - verify(browserService.launch(anyString())).never(); + browserLaunchStub.neverCalledWith(anyString()); + // First call should show info once - verify(appShell.showInformationMessage(anything(), anything())).once(); + + sinon.assert.calledOnce(showInformationMessageStub); assert(prompter); prompter!.onDidSendMessage!(ptvsdOutputEvent); // Can't use deferred promise here await sleep(1); - verify(browserService.launch(anyString())).never(); + browserLaunchStub.neverCalledWith(anyString()); // Second time it should not be called, so overall count is one. - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); }); test('Show prompt when attaching to ptvsd, more info is clicked', async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(Common.moreInfo)); + showInformationMessageStub.returns(Promise.resolve(Common.moreInfo)); + const deferred = createDeferred(); - when(browserService.launch(anything())).thenCall(() => deferred.resolve()); + browserLaunchStub.callsFake(() => deferred.resolve()); + browserLaunchStub.onCall(1).callsFake(() => { + return new Promise(() => deferred.resolve()); + }); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); @@ -102,22 +109,24 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { prompter!.onDidSendMessage!(ptvsdOutputEvent); await deferred.promise; - verify(browserService.launch(anything())).once(); + sinon.assert.calledOnce(browserLaunchStub); + // First call should show info once - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); prompter!.onDidSendMessage!(ptvsdOutputEvent); // The second call does not go through the same path. So we just give enough time for the // operation to complete. await sleep(1); - verify(browserService.launch(anyString())).once(); + sinon.assert.calledOnce(browserLaunchStub); + // Second time it should not be called, so overall count is one. - verify(appShell.showInformationMessage(anything(), anything())).once(); + sinon.assert.calledOnce(showInformationMessageStub); }); test("Don't show prompt attaching to debugpy", async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); @@ -127,7 +136,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { // Can't use deferred promise here await sleep(1); - verify(appShell.showInformationMessage(anything(), anything())).never(); + showInformationMessageStub.neverCalledWith(anything(), anything()); }); const someRequest: DebugProtocol.RunInTerminalRequest = { @@ -155,7 +164,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { [someRequest, someEvent, someOutputEvent].forEach((message) => { test(`Don't show prompt when non-telemetry events are seen: ${JSON.stringify(message)}`, async () => { - when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + showInformationMessageStub.returns(Promise.resolve(undefined)); const session = createSession(); const prompter = await promptFactory.createDebugAdapterTracker(session); @@ -165,7 +174,7 @@ suite('Debugging - Outdated Debugger Prompt tests.', () => { // Can't use deferred promise here await sleep(1); - verify(appShell.showInformationMessage(anything(), anything())).never(); + showInformationMessageStub.neverCalledWith(anything(), anything()); }); }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 85c45407a137..7c7977ab8480 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; +import { DebugConfiguration, Uri } from 'vscode'; import { IMultiStepInputFactory, MultiStepInput } from '../../../../client/common/utils/multiStepInput'; import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; @@ -19,11 +19,11 @@ suite('Debugging - Configuration Service', () => { let multiStepFactory: typemoq.IMock; class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - public async pickDebugConfiguration( + public static async pickDebugConfiguration( input: MultiStepInput, state: DebugConfigurationState, ) { - return super.pickDebugConfiguration(input, state); + return PythonDebugConfigurationService.pickDebugConfiguration(input, state); } } setup(() => { @@ -40,7 +40,7 @@ suite('Debugging - Configuration Service', () => { test('Should use attach resolver when passing attach config', async () => { const config = ({ request: 'attach', - } as any) as AttachRequestArguments; + } as DebugConfiguration) as AttachRequestArguments; const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; const expectedConfig = { yay: 1 }; @@ -48,13 +48,13 @@ suite('Debugging - Configuration Service', () => { .setup((a) => a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config), typemoq.It.isAny()), ) - .returns(() => Promise.resolve(expectedConfig as any)) + .returns(() => Promise.resolve((expectedConfig as unknown) as AttachRequestArguments)) .verifiable(typemoq.Times.once()); launchResolver .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); - const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as DebugConfiguration); expect(resolvedConfig).to.deep.equal(expectedConfig); attachResolver.verifyAll(); @@ -69,17 +69,17 @@ suite('Debugging - Configuration Service', () => { .setup((a) => a.resolveDebugConfiguration( typemoq.It.isValue(folder), - typemoq.It.isValue((config as any) as LaunchRequestArguments), + typemoq.It.isValue((config as DebugConfiguration) as LaunchRequestArguments), typemoq.It.isAny(), ), ) - .returns(() => Promise.resolve(expectedConfig as any)) + .returns(() => Promise.resolve((expectedConfig as unknown) as LaunchRequestArguments)) .verifiable(typemoq.Times.once()); attachResolver .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); - const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as DebugConfiguration); expect(resolvedConfig).to.deep.equal(expectedConfig); attachResolver.verifyAll(); @@ -87,26 +87,26 @@ suite('Debugging - Configuration Service', () => { }); }); test('Picker should be displayed', async () => { - const state = ({ configs: [], folder: {}, token: undefined } as any) as DebugConfigurationState; + const state = ({ configs: [], folder: {}, token: undefined } as unknown) as DebugConfigurationState; const multiStepInput = typemoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) + .returns(() => Promise.resolve(undefined)) .verifiable(typemoq.Times.once()); - await configService.pickDebugConfiguration(multiStepInput.object, state); + await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); multiStepInput.verifyAll(); }); test('Existing Configuration items must be removed before displaying picker', async () => { - const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as any) as DebugConfigurationState; + const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as unknown) as DebugConfigurationState; const multiStepInput = typemoq.Mock.ofType>(); multiStepInput .setup((i) => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) + .returns(() => Promise.resolve(undefined)) .verifiable(typemoq.Times.once()); - await configService.pickDebugConfiguration(multiStepInput.object, state); + await TestPythonDebugConfigurationService.pickDebugConfiguration(multiStepInput.object, state); multiStepInput.verifyAll(); expect(Object.keys(state.config)).to.be.lengthOf(0); @@ -114,34 +114,34 @@ suite('Debugging - Configuration Service', () => { test('Ensure generated config is returned', async () => { const expectedConfig = { yes: 'Updated' }; const multiStepInput = { - run: (_: any, state: any) => { + run: (_: unknown, state: DebugConfiguration) => { Object.assign(state.config, expectedConfig); return Promise.resolve(); }, }; multiStepFactory .setup((f) => f.create()) - .returns(() => multiStepInput as any) + .returns(() => multiStepInput as MultiStepInput) .verifiable(typemoq.Times.once()); - configService.pickDebugConfiguration = (_, state) => { + TestPythonDebugConfigurationService.pickDebugConfiguration = (_, state) => { Object.assign(state.config, expectedConfig); return Promise.resolve(); }; - const config = await configService.provideDebugConfigurations!({} as any); + const config = await configService.provideDebugConfigurations!(({} as unknown) as undefined); multiStepFactory.verifyAll(); expect(config).to.deep.equal([expectedConfig]); }); test('Ensure `undefined` is returned if QuickPick is cancelled', async () => { const multiStepInput = { - run: () => Promise.resolve(), + run: (_: unknown, _state: DebugConfiguration) => Promise.resolve(), }; const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; multiStepFactory .setup((f) => f.create()) - .returns(() => multiStepInput as any) + .returns(() => multiStepInput as MultiStepInput) .verifiable(typemoq.Times.once()); - const config = await configService.resolveDebugConfiguration(folder, {} as any); + const config = await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); multiStepFactory.verifyAll(); @@ -157,23 +157,23 @@ suite('Debugging - Configuration Service', () => { console: 'integratedTerminal', }; const multiStepInput = { - run: (_: any, state: any) => { + run: (_: unknown, state: DebugConfiguration) => { Object.assign(state.config, expectedConfig); return Promise.resolve(); }, }; multiStepFactory .setup((f) => f.create()) - .returns(() => multiStepInput as any) + .returns(() => multiStepInput as MultiStepInput) .verifiable(typemoq.Times.once()); // this should be called only once. launchResolver .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(expectedConfig as any)) + .returns(() => Promise.resolve(expectedConfig as LaunchRequestArguments)) .verifiable(typemoq.Times.exactly(2)); // this should be called twice with the same config. - await configService.resolveDebugConfiguration(folder, {} as any); - await configService.resolveDebugConfiguration(folder, {} as any); + await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); + await configService.resolveDebugConfiguration(folder, {} as DebugConfiguration); multiStepFactory.verifyAll(); launchResolver.verifyAll(); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts index c331efbd28b2..a850d50150ae 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts @@ -41,18 +41,18 @@ suite('Debugging - launch.json Completion Provider', () => { const document = typemoq.Mock.ofType(); const position = new Position(0, 0); document.setup((doc) => doc.uri).returns(() => Uri.file(__filename)); - assert.strictEqual(completionProvider.canProvideCompletions(document.object, position), false); + assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); document.reset(); document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - assert.strictEqual(completionProvider.canProvideCompletions(document.object, position), false); + assert.strictEqual(LaunchJsonCompletionProvider.canProvideCompletions(document.object, position), false); }); function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { const document = typemoq.Mock.ofType(); document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); - const canProvideCompletions = completionProvider.canProvideCompletions(document.object, position); + const canProvideCompletions = LaunchJsonCompletionProvider.canProvideCompletions(document.object, position); assert.strictEqual(canProvideCompletions, expectedValue); } test('Cannot provide completions when there is no configurations section in json', () => { @@ -60,7 +60,7 @@ suite('Debugging - launch.json Completion Provider', () => { const config = `{ "version": "0.1.0" }`; - testCanProvideCompletions(position, 1, config as any, false); + testCanProvideCompletions(position, 1, config as string, false); }); test('Cannot provide completions when cursor position is not in configurations array', () => { const position = new Position(0, 0); @@ -83,7 +83,7 @@ suite('Debugging - launch.json Completion Provider', () => { test('No Completions for non launch.json', async () => { const document = typemoq.Mock.ofType(); document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); - const token = new CancellationTokenSource().token; + const { token } = new CancellationTokenSource(); const position = new Position(0, 0); const completions = await completionProvider.provideCompletionItems(document.object, position, token); @@ -93,7 +93,7 @@ suite('Debugging - launch.json Completion Provider', () => { test('No Completions for files ending with launch.json', async () => { const document = typemoq.Mock.ofType(); document.setup((doc) => doc.uri).returns(() => Uri.file('x-launch.json')); - const token = new CancellationTokenSource().token; + const { token } = new CancellationTokenSource(); const position = new Position(0, 0); const completions = await completionProvider.provideCompletionItems(document.object, position, token); @@ -113,7 +113,7 @@ suite('Debugging - launch.json Completion Provider', () => { document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); const position = new Position(0, 0); - const token = new CancellationTokenSource().token; + const { token } = new CancellationTokenSource(); const completions = await completionProvider.provideCompletionItems(document.object, position, token); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts index b727e1bded89..c773e1cbd5bc 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -5,24 +5,23 @@ import { assert, expect } from 'chai'; import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; -import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../../../client/common/application/types'; -import { Commands } from '../../../../../client/common/constants'; import { IDisposable } from '../../../../../client/common/types'; +import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; import { IInterpreterService } from '../../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; suite('Interpreter Path Command', () => { - let cmdManager: ICommandManager; let interpreterService: IInterpreterService; let interpreterPathCommand: InterpreterPathCommand; + let registerCommandStub: sinon.SinonStub; setup(() => { - cmdManager = mock(CommandManager); interpreterService = mock(); - interpreterPathCommand = new InterpreterPathCommand(instance(cmdManager), instance(interpreterService), []); + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterPathCommand = new InterpreterPathCommand(instance(interpreterService), []); }); teardown(() => { @@ -30,16 +29,14 @@ suite('Interpreter Path Command', () => { }); test('Ensure command is registered with the correct callback handler', async () => { - let getInterpreterPathHandler!: Function; - when(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).thenCall((_, cb) => { + let getInterpreterPathHandler = (_param: unknown) => undefined; + registerCommandStub.callsFake((_, cb) => { getInterpreterPathHandler = cb; return TypeMoq.Mock.ofType().object; }); - await interpreterPathCommand.activate(); - verify(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).once(); - + sinon.assert.calledOnce(registerCommandStub); const getSelectedInterpreterPath = sinon.stub(InterpreterPathCommand.prototype, '_getSelectedInterpreterPath'); getInterpreterPathHandler([]); assert(getSelectedInterpreterPath.calledOnceWith([])); @@ -50,7 +47,7 @@ suite('Interpreter Path Command', () => { when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { assert.deepEqual(arg, Uri.parse('folderPath')); - return Promise.resolve({ path: 'settingValue' }) as any; + return Promise.resolve({ path: 'settingValue' }) as unknown; }); const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); @@ -61,7 +58,7 @@ suite('Interpreter Path Command', () => { when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { assert.deepEqual(arg, Uri.parse('folderPath')); - return Promise.resolve({ path: 'settingValue' }) as any; + return Promise.resolve({ path: 'settingValue' }) as unknown; }); const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); @@ -71,7 +68,7 @@ suite('Interpreter Path Command', () => { const args = ['command']; when(interpreterService.getActiveInterpreter(undefined)).thenReturn( - Promise.resolve({ path: 'settingValue' }) as any, + Promise.resolve({ path: 'settingValue' }) as Promise, ); const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); @@ -81,7 +78,7 @@ suite('Interpreter Path Command', () => { const args = ['command', '${input:some_input}']; when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { assert.deepEqual(arg, undefined); - return Promise.resolve({ path: 'settingValue' }) as any; + return Promise.resolve({ path: 'settingValue' }) as unknown; }); const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); expect(setting).to.equal('settingValue'); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts index afb5dac381ee..b2addd24267b 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts @@ -3,56 +3,25 @@ 'use strict'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, DebugConfiguration, Position, Range, TextDocument, TextEditor, Uri } from 'vscode'; +import { instance, mock, verify } from 'ts-mockito'; import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { DocumentManager } from '../../../../../client/common/application/documentManager'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { ICommandManager } from '../../../../../client/common/application/types'; import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { - LaunchJsonUpdaterService, - LaunchJsonUpdaterServiceHelper, -} from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; +import { LaunchJsonUpdaterService } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; +import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; -type LaunchJsonSchema = { - version: string; - configurations: DebugConfiguration[]; -}; - suite('Debugging - launch.json Updater Service', () => { let helper: LaunchJsonUpdaterServiceHelper; let commandManager: ICommandManager; - let workspace: IWorkspaceService; - let documentManager: IDocumentManager; let debugConfigService: IDebugConfigurationService; - const sandbox = sinon.createSandbox(); setup(() => { commandManager = mock(CommandManager); - workspace = mock(WorkspaceService); - documentManager = mock(DocumentManager); debugConfigService = mock(PythonDebugConfigurationService); - sandbox.stub(LaunchJsonUpdaterServiceHelper.prototype, 'isCommaImmediatelyBeforeCursor').returns(false); - helper = new LaunchJsonUpdaterServiceHelper( - instance(commandManager), - instance(workspace), - instance(documentManager), - instance(debugConfigService), - ); + helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); }); - teardown(() => sandbox.restore()); test('Activation will register the required commands', async () => { - const service = new LaunchJsonUpdaterService( - instance(commandManager), - [], - instance(workspace), - instance(documentManager), - instance(debugConfigService), - ); + const service = new LaunchJsonUpdaterService([], instance(debugConfigService)); await service.activate(); verify( commandManager.registerCommand( @@ -62,427 +31,4 @@ suite('Debugging - launch.json Updater Service', () => { ), ); }); - - test('Configuration Array is detected as being empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = helper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, true); - }); - test('Configuration Array is not empty', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = helper.isConfigurationArrayEmpty(document.object); - assert.strictEqual(isEmpty, false); - }); - test('Cursor is not positioned in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python', - }, - ], - }; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, undefined); - }); - test('Cursor is positioned in the empty configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] - }`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, 'InsideEmptyArray'); - }); - test('Cursor is positioned before an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned before an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned after an item in the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - }] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Cursor is positioned after an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.strictEqual(cursorPosition, 'AfterItem'); - }); - test('Text to be inserted must be prefixed with a comma', async () => { - const config = {} as any; - const expectedText = `,${JSON.stringify(config)}`; - - const textToInsert = helper.getTextForInsertion(config, 'AfterItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { - const config = {} as any; - const expectedText = JSON.stringify(config); - - const textToInsert = helper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must be suffixed with a comma', async () => { - const config = {} as any; - const expectedText = `${JSON.stringify(config)},`; - - const textToInsert = helper.getTextForInsertion(config, 'BeforeItem'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { - const config = {} as any; - const expectedText = JSON.stringify(config); - - const textToInsert = helper.getTextForInsertion(config, 'InsideEmptyArray'); - - assert.strictEqual(textToInsert, expectedText); - }); - test('When inserting the debug config into the json file format the document', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - const config = {} as any; - const document = typemoq.Mock.ofType(); - document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - when(documentManager.applyEdit(anything())).thenResolve(); - when(commandManager.executeCommand('editor.action.formatDocument')).thenResolve(); - - await helper.insertDebugConfiguration(document.object, new Position(0, 0), config); - - verify(documentManager.applyEdit(anything())).once(); - verify(commandManager.executeCommand('editor.action.formatDocument')).once(); - }); - test('No changes to configuration if there is not active document', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const token = new CancellationTokenSource().token; - when(documentManager.activeTextEditor).thenReturn(); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(anything())).never(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if the active document is not same as the document passed in', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const token = new CancellationTokenSource().token; - const textEditor = typemoq.Mock.ofType(); - textEditor - .setup((t) => t.document) - .returns(() => 'x' as any) - .verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(anything())).never(); - textEditor.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if cancellation token has been cancelled', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([''] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('No changes to configuration if no configuration items are returned', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, false); - }); - test('Changes are made to the configuration', async () => { - const document = typemoq.Mock.ofType(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document - .setup((doc) => doc.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - textEditor - .setup((t) => t.document) - .returns(() => document.object) - .verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(['config'] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.strictEqual(debugConfigInserted, true); - }); - test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(1, 0); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 1) } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after some text (not a comma) then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 1, 5) } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned after a comma then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3) } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => '}, ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(isBeforeCursor); - document.verifyAll(); - }); - test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { - sandbox.restore(); - const document = typemoq.Mock.ofType(); - const position = new Position(2, 2); - document - .setup((doc) => doc.lineAt(1)) - .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.lineAt(2)) - .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as any)) - .verifiable(typemoq.Times.atLeastOnce()); - document - .setup((doc) => doc.getText(typemoq.It.isAny())) - .returns(() => ' ') - .verifiable(typemoq.Times.atLeastOnce()); - - const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); - - assert.ok(!isBeforeCursor); - document.verifyAll(); - }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts new file mode 100644 index 000000000000..53118d68025e --- /dev/null +++ b/extensions/positron-python/src/test/debugger/extension/configuration/launch.json/updaterServerHelper.unit.test.ts @@ -0,0 +1,496 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { + CancellationTokenSource, + DebugConfiguration, + Position, + Range, + TextDocument, + TextEditor, + TextLine, + Uri, +} from 'vscode'; +import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; +import { LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterServiceHelper'; +import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../../../client/common/vscodeApis/commandApis'; + +type LaunchJsonSchema = { + version: string; + configurations: DebugConfiguration[]; +}; + +suite('Debugging - launch.json Updater Service', () => { + let helper: LaunchJsonUpdaterServiceHelper; + let getWorkspaceFolderStub: sinon.SinonStub; + let getActiveTextEditorStub: sinon.SinonStub; + let applyEditStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let debugConfigService: IDebugConfigurationService; + + const sandbox = sinon.createSandbox(); + setup(() => { + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + applyEditStub = sinon.stub(workspaceApis, 'applyEdit'); + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + + debugConfigService = mock(PythonDebugConfigurationService); + sandbox.stub(LaunchJsonUpdaterServiceHelper, 'isCommaImmediatelyBeforeCursor').returns(false); + helper = new LaunchJsonUpdaterServiceHelper(instance(debugConfigService)); + }); + teardown(() => { + sandbox.restore(); + sinon.restore(); + }); + + test('Configuration Array is detected as being empty', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [], + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); + assert.strictEqual(isEmpty, true); + }); + test('Configuration Array is not empty', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python', + }, + ], + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = LaunchJsonUpdaterServiceHelper.isConfigurationArrayEmpty(document.object); + assert.strictEqual(isEmpty, false); + }); + test('Cursor is not positioned in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python', + }, + ], + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, undefined); + }); + test('Cursor is positioned in the empty configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] + }`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, 'InsideEmptyArray'); + }); + test('Cursor is positioned before an item in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned before an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned after an item in the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + }] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, 'AfterItem'); + }); + test('Cursor is positioned after an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + + const cursorPosition = LaunchJsonUpdaterServiceHelper.getCursorPositionInConfigurationsArray( + document.object, + new Position(0, 0), + ); + assert.strictEqual(cursorPosition, 'AfterItem'); + }); + test('Text to be inserted must be prefixed with a comma', async () => { + const config = {} as DebugConfiguration; + const expectedText = `,${JSON.stringify(config)}`; + + const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem'); + + assert.strictEqual(textToInsert, expectedText); + }); + test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { + const config = {} as DebugConfiguration; + const expectedText = JSON.stringify(config); + + const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); + + assert.strictEqual(textToInsert, expectedText); + }); + test('Text to be inserted must be suffixed with a comma', async () => { + const config = {} as DebugConfiguration; + const expectedText = `${JSON.stringify(config)},`; + + const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'BeforeItem'); + + assert.strictEqual(textToInsert, expectedText); + }); + test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { + const config = {} as DebugConfiguration; + const expectedText = JSON.stringify(config); + + const textToInsert = LaunchJsonUpdaterServiceHelper.getTextForInsertion(config, 'InsideEmptyArray'); + + assert.strictEqual(textToInsert, expectedText); + }); + test('When inserting the debug config into the json file format the document', async () => { + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + const config = {} as DebugConfiguration; + const document = typemoq.Mock.ofType(); + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + applyEditStub.returns(undefined); + executeCommandStub.withArgs('editor.action.formatDocument').resolves(); + + await LaunchJsonUpdaterServiceHelper.insertDebugConfiguration(document.object, new Position(0, 0), config); + + sinon.assert.calledOnce(applyEditStub); + sinon.assert.calledOnce(executeCommandStub.withArgs('editor.action.formatDocument')); + }); + test('No changes to configuration if there is not active document', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const { token } = new CancellationTokenSource(); + getActiveTextEditorStub.returns(undefined); + let debugConfigInserted = false; + LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + sinon.assert.calledOnce(getActiveTextEditorStub); + sinon.assert.notCalled(getWorkspaceFolderStub); + assert.strictEqual(debugConfigInserted, false); + }); + test('No changes to configuration if the active document is not same as the document passed in', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const { token } = new CancellationTokenSource(); + const textEditor = typemoq.Mock.ofType(); + textEditor + .setup((t) => t.document) + .returns(() => ('x' as unknown) as TextDocument) + .verifiable(typemoq.Times.atLeastOnce()); + getActiveTextEditorStub.returns(textEditor.object); + let debugConfigInserted = false; + LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + sinon.assert.calledOnce(getActiveTextEditorStub); + sinon.assert.notCalled(getWorkspaceFolderStub); + textEditor.verifyAll(); + assert.strictEqual(debugConfigInserted, false); + }); + test('No changes to configuration if cancellation token has been cancelled', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + const { token } = tokenSource; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + getActiveTextEditorStub.returns(textEditor.object); + getWorkspaceFolderStub.returns(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([''] as unknown) as void); + let debugConfigInserted = false; + LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + sinon.assert.calledOnce(getActiveTextEditorStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + textEditor.verifyAll(); + document.verifyAll(); + assert.strictEqual(debugConfigInserted, false); + }); + test('No changes to configuration if no configuration items are returned', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const { token } = tokenSource; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + + getActiveTextEditorStub.returns(textEditor.object); + getWorkspaceFolderStub.returns(folder); + + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([] as unknown) as void); + let debugConfigInserted = false; + LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + sinon.assert.calledOnce(getActiveTextEditorStub); + sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); + textEditor.verifyAll(); + document.verifyAll(); + assert.strictEqual(debugConfigInserted, false); + }); + test('Changes are made to the configuration', async () => { + const document = typemoq.Mock.ofType(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const { token } = tokenSource; + const textEditor = typemoq.Mock.ofType(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + getActiveTextEditorStub.returns(textEditor.object); + getWorkspaceFolderStub.withArgs(docUri).returns(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(([ + 'config', + ] as unknown) as void); + let debugConfigInserted = false; + LaunchJsonUpdaterServiceHelper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + sinon.assert.called(getActiveTextEditorStub); + sinon.assert.calledOnce(getWorkspaceFolderStub.withArgs(docUri)); + textEditor.verifyAll(); + document.verifyAll(); + assert.strictEqual(debugConfigInserted, true); + }); + test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType(); + const position = new Position(1, 0); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 1) } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => '') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned after some text (not a comma) then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 1, 5) } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned after a comma then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3) } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => '}, ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => ' ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as TextLine)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => ' ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = LaunchJsonUpdaterServiceHelper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); +}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts index 479fdfebb53e..8a5898611c82 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts @@ -14,8 +14,8 @@ import { MultiStepInput } from '../../../../../client/common/utils/multiStepInpu import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; import * as djangoLaunch from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Debugging - Configuration Provider Django', () => { let pathExistsStub: sinon.SinonStub; @@ -27,7 +27,7 @@ suite('Debugging - Configuration Provider Django', () => { input = mock>(MultiStepInput); pathExistsStub = sinon.stub(fs, 'pathExists'); pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceFolder, 'getWorkspaceFolder'); + workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); }); teardown(() => { sinon.restore(); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts index 60f2b199bbd2..f627c7558c51 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts @@ -7,15 +7,20 @@ import { expect } from 'chai'; import * as path from 'path'; import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { buildFileLaunchDebugConfiguration } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; suite('Debugging - Configuration Provider File', () => { test('Launch JSON with default managepy path', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - await buildFileLaunchDebugConfiguration(undefined as any, state); + await buildFileLaunchDebugConfiguration( + (undefined as unknown) as MultiStepInput, + state, + ); const config = { name: DebugConfigStrings.file.snippet.name, diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts index db9de7e0f58c..8217e150aa01 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts @@ -7,15 +7,17 @@ import { expect } from 'chai'; import * as path from 'path'; import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { buildPidAttachConfiguration } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; suite('Debugging - Configuration Provider File', () => { test('Launch JSON with default process id', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; const state = { config: {}, folder }; - await buildPidAttachConfiguration(undefined as any, state); + await buildPidAttachConfiguration((undefined as unknown) as MultiStepInput, state); const config = { name: DebugConfigStrings.attachPid.snippet.name, diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts index 8a5d29206180..688215259a2f 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts @@ -13,9 +13,9 @@ import { DebugConfigStrings } from '../../../../../client/common/utils/localize' import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { resolveVariables } from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; import * as pyramidLaunch from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; suite('Debugging - Configuration Provider Pyramid', () => { let input: MultiStepInput; @@ -27,7 +27,7 @@ suite('Debugging - Configuration Provider Pyramid', () => { input = mock>(MultiStepInput); pathExistsStub = sinon.stub(fs, 'pathExists'); pathSeparatorStub = sinon.stub(path, 'sep'); - workspaceStub = sinon.stub(workspaceFolder, 'getWorkspaceFolder'); + workspaceStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); }); teardown(() => { sinon.restore(); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts index 3bb672833e0a..9d271b7acd90 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -13,9 +13,9 @@ import { AttachConfigurationResolver } from '../../../../../client/debugger/exte import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; import { IInterpreterService } from '../../../../../client/interpreter/contracts'; import { getInfoPerOS } from './common'; -import * as common from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -47,9 +47,9 @@ getInfoPerOS().forEach(([osName, osType, path]) => { configurationService = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); debugProvider = new AttachConfigurationResolver(configurationService.object, interpreterService.object); - getActiveTextEditorStub = sinon.stub(common, 'getActiveTextEditor'); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); - getWorkspaceFoldersStub = sinon.stub(workspaceFolder, 'getWorkspaceFolders'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); }); @@ -245,7 +245,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; expect(pathMappings).to.be.lengthOf(1); expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); @@ -267,11 +267,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { localRoot, host, }); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path')).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + + return undefined; }); test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { @@ -290,11 +292,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { localRoot, host, }); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path')).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + + return undefined; }); test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async function () { @@ -317,11 +321,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { pathMappings: debugPathMappings, host, }); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path', localRoot)).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + + return undefined; }); test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}' and with existing path mappings`, async function () { @@ -345,11 +351,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { pathMappings: debugPathMappings, host, }); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; expect(Uri.file(pathMappings![0].localRoot).fsPath).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + + return undefined; }); test(`Ensure local path mappings are not modified when not pointing to a local drive when host is '${host}'`, async () => { @@ -365,7 +373,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { localRoot, host, }); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); @@ -388,7 +396,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + const { pathMappings } = debugConfig as AttachRequestArguments; expect(pathMappings || []).to.be.lengthOf(0); }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts index 3ff4815ecae8..b4478f7c3f0a 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -7,17 +8,16 @@ import { expect } from 'chai'; import * as path from 'path'; import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { DebugConfiguration, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { ConfigurationService } from '../../../../../client/common/configuration/service'; -import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; import { IConfigurationService } from '../../../../../client/common/types'; import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; import { IInterpreterService } from '../../../../../client/interpreter/contracts'; -import * as workspaceFolder from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; -import * as common from '../../../../../client/debugger/extension/configuration/utils/common'; +import { PythonEnvironment } from '../../../../../client/pythonEnvironments/info'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; +import * as helper from '../../../../../client/debugger/extension/configuration/resolvers/helper'; suite('Debugging - Config Resolver', () => { class BaseResolver extends BaseConfigurationResolver { @@ -38,99 +38,51 @@ suite('Debugging - Config Resolver', () => { } public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { - return super.getWorkspaceFolder(folder); - } - - public getProgram(): string | undefined { - return super.getProgram(); + return BaseConfigurationResolver.getWorkspaceFolder(folder); } public resolveAndUpdatePythonPath( - workspaceFolder: Uri | undefined, + workspaceFolderUri: Uri | undefined, debugConfiguration: LaunchRequestArguments, ) { - return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); } public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { - return super.debugOption(debugOptions, debugOption); + return BaseConfigurationResolver.debugOption(debugOptions, debugOption); } public isLocalHost(hostName?: string) { - return super.isLocalHost(hostName); + return BaseConfigurationResolver.isLocalHost(hostName); } public isDebuggingFastAPI(debugConfiguration: Partial) { - return super.isDebuggingFastAPI(debugConfiguration); + return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); } public isDebuggingFlask(debugConfiguration: Partial) { - return super.isDebuggingFlask(debugConfiguration); + return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); } } let resolver: BaseResolver; let configurationService: IConfigurationService; let interpreterService: IInterpreterService; let getWorkspaceFoldersStub: sinon.SinonStub; - let workspaceStub: sinon.SinonStub; - let getActiveTextEditorStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let getProgramStub: sinon.SinonStub; setup(() => { configurationService = mock(ConfigurationService); interpreterService = mock(); resolver = new BaseResolver(instance(configurationService), instance(interpreterService)); - getWorkspaceFoldersStub = sinon.stub(workspaceFolder, 'getWorkspaceFolders'); - workspaceStub = sinon.stub(workspaceFolder, 'getWorkspaceFolder'); - getActiveTextEditorStub = sinon.stub(common, 'getActiveTextEditor'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getProgramStub = sinon.stub(helper, 'getProgram'); }); teardown(() => { sinon.restore(); }); - test('Program should return filepath of active editor if file is python', () => { - const expectedFileName = 'my.py'; - const editor = typemoq.Mock.ofType(); - const doc = typemoq.Mock.ofType(); - - editor - .setup((e) => e.document) - .returns(() => doc.object) - .verifiable(typemoq.Times.once()); - doc.setup((d) => d.languageId) - .returns(() => PYTHON_LANGUAGE) - .verifiable(typemoq.Times.once()); - doc.setup((d) => d.fileName) - .returns(() => expectedFileName) - .verifiable(typemoq.Times.once()); - getActiveTextEditorStub.returns(editor.object); - - const program = resolver.getProgram(); - - expect(program).to.be.equal(expectedFileName); - }); - test('Program should return undefined if active file is not python', () => { - const editor = typemoq.Mock.ofType(); - const doc = typemoq.Mock.ofType(); - - editor - .setup((e) => e.document) - .returns(() => doc.object) - .verifiable(typemoq.Times.once()); - doc.setup((d) => d.languageId) - .returns(() => 'C#') - .verifiable(typemoq.Times.once()); - getActiveTextEditorStub.returns(editor.object); - - const program = resolver.getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); - test('Program should return undefined if there is no active editor', () => { - getActiveTextEditorStub.returns(undefined); - const program = resolver.getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); test('Should get workspace folder when workspace folder is provided', () => { const expectedUri = Uri.parse('mock'); const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; @@ -149,7 +101,7 @@ suite('Debugging - Config Resolver', () => { test(item.title, () => { const programPath = path.join('one', 'two', 'three.xyz'); - resolver.getProgram = () => programPath; + getProgramStub.returns(programPath); getWorkspaceFoldersStub.returns(item.workspaceFolders); const uri = resolver.getWorkspaceFolder(undefined); @@ -162,9 +114,9 @@ suite('Debugging - Config Resolver', () => { const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; const folders: WorkspaceFolder[] = [folder]; - resolver.getProgram = () => undefined; + getProgramStub.returns(undefined); - workspaceStub.returns(folder); + getWorkspaceFolderStub.returns(folder); getWorkspaceFoldersStub.returns(folders); @@ -178,10 +130,11 @@ suite('Debugging - Config Resolver', () => { const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; const folders: WorkspaceFolder[] = [folder1, folder2]; - resolver.getProgram = () => programPath; + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(folders); - workspaceStub.returns(folder2); + getWorkspaceFolderStub.returns(folder2); const uri = resolver.getWorkspaceFolder(undefined); @@ -193,25 +146,27 @@ suite('Debugging - Config Resolver', () => { const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; const folders: WorkspaceFolder[] = [folder1, folder2]; - resolver.getProgram = () => programPath; + getProgramStub.returns(programPath); getWorkspaceFoldersStub.returns(folders); - workspaceStub.returns(undefined); + getWorkspaceFolderStub.returns(undefined); const uri = resolver.getWorkspaceFolder(undefined); expect(uri).to.be.deep.equal(undefined, 'not undefined'); }); test('Do nothing if debug configuration is undefined', async () => { - await resolver.resolveAndUpdatePythonPath(undefined, undefined as any); + await resolver.resolveAndUpdatePythonPath(undefined, (undefined as unknown) as LaunchRequestArguments); }); test('pythonPath in debug config must point to pythonPath in settings if pythonPath in config is not set', async () => { const config = {}; const pythonPath = path.join('1', '2', '3'); - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ path: pythonPath } as any); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); - await resolver.resolveAndUpdatePythonPath(undefined, config as any); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); expect(config).to.have.property('pythonPath', pythonPath); }); @@ -221,9 +176,11 @@ suite('Debugging - Config Resolver', () => { }; const pythonPath = path.join('1', '2', '3'); - when(interpreterService.getActiveInterpreter(anything())).thenResolve({ path: pythonPath } as any); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: pythonPath, + } as PythonEnvironment); - await resolver.resolveAndUpdatePythonPath(undefined, config as any); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); expect(config.pythonPath).to.equal(pythonPath); }); @@ -244,32 +201,32 @@ suite('Debugging - Config Resolver', () => { }); test('Is debugging fastapi=true', () => { const config = { module: 'fastapi' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as any); + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); expect(isFastAPI).to.equal(true, 'not fastapi'); }); test('Is debugging fastapi=false', () => { const config = { module: 'fastapi2' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as any); + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); expect(isFastAPI).to.equal(false, 'fastapi'); }); test('Is debugging fastapi=false when not defined', () => { const config = {}; - const isFastAPI = resolver.isDebuggingFastAPI(config as any); + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); expect(isFastAPI).to.equal(false, 'fastapi'); }); test('Is debugging flask=true', () => { const config = { module: 'flask' }; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(true, 'not flask'); }); test('Is debugging flask=false', () => { const config = { module: 'flask2' }; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(false, 'flask'); }); test('Is debugging flask=false when not defined', () => { const config = {}; - const isFlask = resolver.isDebuggingFlask(config as any); + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); expect(isFlask).to.equal(false, 'flask'); }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts new file mode 100644 index 000000000000..01205fd0c87c --- /dev/null +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/helper.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TextDocument, TextEditor } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import { getProgram } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; + +suite('Debugging - Helpers', () => { + let getActiveTextEditorStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + }); + teardown(() => { + sinon.restore(); + }); + + test('Program should return filepath of active editor if file is python', () => { + const expectedFileName = 'my.py'; + const editor = typemoq.Mock.ofType(); + const doc = typemoq.Mock.ofType(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => PYTHON_LANGUAGE) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.fileName) + .returns(() => expectedFileName) + .verifiable(typemoq.Times.once()); + + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(expectedFileName); + }); + test('Program should return undefined if active file is not python', () => { + const editor = typemoq.Mock.ofType(); + const doc = typemoq.Mock.ofType(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => 'C#') + .verifiable(typemoq.Times.once()); + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + getActiveTextEditorStub.returns(undefined); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); +}); diff --git a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 912a2f662431..e7e256468f84 100644 --- a/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -15,12 +15,12 @@ import { DebuggerTypeName } from '../../../../../client/debugger/constants'; import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; import { PythonPathSource } from '../../../../../client/debugger/extension/types'; -import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { ConsoleType, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; import { IInterpreterHelper, IInterpreterService } from '../../../../../client/interpreter/contracts'; import { getInfoPerOS } from './common'; -import * as common from '../../../../../client/debugger/extension/configuration/utils/common'; -import * as workspaceFolders from '../../../../../client/debugger/extension/configuration/utils/workspaceFolder'; import * as platform from '../../../../../client/common/utils/platform'; +import * as windowApis from '../../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Unknown) { @@ -42,9 +42,9 @@ getInfoPerOS().forEach(([osName, osType, path]) => { let getWorkspaceFolderStub: sinon.SinonStub; setup(() => { - getActiveTextEditorStub = sinon.stub(common, 'getActiveTextEditor'); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); getOSTypeStub = sinon.stub(platform, 'getOSType'); - getWorkspaceFolderStub = sinon.stub(workspaceFolders, 'getWorkspaceFolders'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); getOSTypeStub.returns(osType); }); @@ -64,7 +64,6 @@ getInfoPerOS().forEach(([osName, osType, path]) => { debugEnvHelper = TypeMoq.Mock.ofType(); pythonExecutionService = TypeMoq.Mock.ofType(); helper = TypeMoq.Mock.ofType(); - pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); const factory = TypeMoq.Mock.ofType(); factory .setup((f) => f.create(TypeMoq.It.isAny())) @@ -76,9 +75,9 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const settings = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); - interpreterService - .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ path: pythonPath } as any)); + // interpreterService + // .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + // .returns(() => Promise.resolve({ path: pythonPath } as any)); settings.setup((s) => s.pythonPath).returns(() => pythonPath); if (workspaceFolder) { settings.setup((s) => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); @@ -172,7 +171,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test("Defaults should be returned when an object with 'noDebug' property is passed with a Workspace Folder and active file", async () => { @@ -200,7 +199,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { @@ -227,7 +226,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { @@ -250,7 +249,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).not.to.have.property('envFile'); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { @@ -274,7 +273,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).not.to.have.property('envFile'); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { @@ -302,7 +301,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); expect(debugConfig).to.have.property('env'); - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); test("Ensure 'port' is left unaltered", async () => { @@ -381,7 +380,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { pathMappings: [expected], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.deep.equal([expected]); }); @@ -401,7 +400,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { ], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.deep.equal([ { localRoot: `${workspaceFolder.uri.fsPath}/spam`, @@ -419,10 +418,10 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, - localRoot: localRoot, + localRoot, }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); }); @@ -435,11 +434,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, - localRoot: localRoot, + localRoot, pathMappings: [], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); }); @@ -452,7 +451,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, - localRoot: localRoot, + localRoot, pathMappings: [ { localRoot: '/spam', @@ -462,7 +461,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.be.deep.equal([ { localRoot: '/spam', @@ -491,7 +490,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { ], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; expect(pathMappings).to.deep.equal([ { @@ -499,6 +498,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { remoteRoot: '/app/', }, ]); + return undefined; }); test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { @@ -521,13 +521,14 @@ getInfoPerOS().forEach(([osName, osType, path]) => { ], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.deep.equal([ { localRoot, remoteRoot: '/app/', }, ]); + return undefined; }); test('Ensure local path mappings are not modified when not pointing to a local drive', async () => { @@ -546,7 +547,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { ], }); - const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const { pathMappings } = debugConfig as LaunchRequestArguments; expect(pathMappings).to.deep.equal([ { localRoot: '/spam', @@ -698,11 +699,11 @@ getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Windows) { expectedOptions.push(DebugOptions.FixFilePathCase); } - expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); }); test('Test defaults of python debugger', async () => { - if ('python' === DebuggerTypeName) { + if (DebuggerTypeName === 'python') { return; } const pythonPath = `PythonPath_${new Date().toString()}`; @@ -718,7 +719,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(debugConfig).to.have.property('stopOnEntry', false); expect(debugConfig).to.have.property('showReturnValue', true); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([]); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal([]); }); test('Test overriding defaults of debugger', async () => { @@ -748,7 +749,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { if (osType === platform.OSType.Windows) { expectedOptions.push(DebugOptions.FixFilePathCase); } - expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); }); const testsForJustMyCode = [ @@ -870,13 +871,13 @@ getInfoPerOS().forEach(([osName, osType, path]) => { testsForRedirectOutput.forEach(async (testParams) => { const debugConfig = await resolveDebugConfiguration(workspaceFolder, { ...launch, - console: testParams.console as any, + console: testParams.console as ConsoleType, redirectOutput: testParams.redirectOutput, }); expect(debugConfig).to.have.property('redirectOutput', testParams.expectedRedirectOutput); if (testParams.expectedRedirectOutput) { expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.contain(DebugOptions.RedirectOutput); + expect((debugConfig as DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); } }); }); @@ -914,7 +915,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); }); test('Auto detect flask debugging', async () => { @@ -930,7 +931,7 @@ getInfoPerOS().forEach(([osName, osType, path]) => { }); expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); }); test('Test validation of Python Path when launching debugger (with invalid "python")', async () => { diff --git a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts index 8432f9356561..3c023f3f1450 100644 --- a/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/debugCommands.unit.test.ts @@ -12,7 +12,6 @@ import { IDisposableRegistry } from '../../../client/common/types'; import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; import * as telemetry from '../../../client/telemetry'; -import { ILaunchJsonReader } from '../../../client/debugger/extension/configuration/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; @@ -20,7 +19,6 @@ suite('Debugging - commands', () => { let commandManager: typemoq.IMock; let debugService: typemoq.IMock; let disposables: typemoq.IMock; - let launchJsonReader: typemoq.IMock; let interpreterService: typemoq.IMock; let debugCommands: IExtensionSingleActivationService; @@ -30,12 +28,6 @@ suite('Debugging - commands', () => { .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve()); debugService = typemoq.Mock.ofType(); - launchJsonReader = typemoq.Mock.ofType(); - launchJsonReader - .setup((l) => l.getConfigurationsByUri(typemoq.It.isAny())) - .returns(() => Promise.resolve([])) - .verifiable(typemoq.Times.once()); - disposables = typemoq.Mock.ofType(); interpreterService = typemoq.Mock.ofType(); interpreterService @@ -61,7 +53,6 @@ suite('Debugging - commands', () => { debugCommands = new DebugCommands( commandManager.object, debugService.object, - launchJsonReader.object, disposables.object, interpreterService.object, ); @@ -83,7 +74,6 @@ suite('Debugging - commands', () => { debugCommands = new DebugCommands( commandManager.object, debugService.object, - launchJsonReader.object, disposables.object, interpreterService.object, ); @@ -92,6 +82,5 @@ suite('Debugging - commands', () => { await callback(Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'test.py'))); commandManager.verifyAll(); debugService.verifyAll(); - launchJsonReader.verifyAll(); }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index b4056d0c04f2..2ab9d3e30d2c 100644 --- a/extensions/positron-python/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -4,30 +4,30 @@ 'use strict'; import { expect } from 'chai'; +import * as sinon from 'sinon'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { Uri, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../../../client/common/application/applicationShell'; import { DebugService } from '../../../../client/common/application/debugService'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { IDebugService } from '../../../../client/common/application/types'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; suite('Debug - Attach to Child Process', () => { - let shell: IApplicationShell; let debugService: IDebugService; - let workspaceService: IWorkspaceService; let attachService: ChildProcessAttachService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; setup(() => { - shell = mock(ApplicationShell); debugService = mock(DebugService); - workspaceService = mock(WorkspaceService); - attachService = new ChildProcessAttachService( - instance(shell), - instance(debugService), - instance(workspaceService), - ); + attachService = new ChildProcessAttachService(instance(debugService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + teardown(() => { + sinon.restore(); }); test('Message is not displayed if debugger is launched', async () => { @@ -39,15 +39,15 @@ suite('Debug - Attach to Child Process', () => { subProcessId: 2, }; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn(undefined); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); - when(shell.showErrorMessage(anything())).thenResolve(); + showErrorMessageStub.returns(undefined); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(anything(), anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Message is displayed if debugger is not launched', async () => { const data: AttachRequestArguments = { @@ -59,15 +59,15 @@ suite('Debug - Attach to Child Process', () => { }; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn(undefined); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); - when(shell.showErrorMessage(anything())).thenResolve(); + showErrorMessageStub.resolves(() => {}); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(anything(), anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).once(); + sinon.assert.calledOnce(showErrorMessageStub); }); test('Use correct workspace folder', async () => { const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; @@ -84,14 +84,14 @@ suite('Debug - Attach to Child Process', () => { }; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn([wkspace1, rightWorkspaceFolder, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).atLeast(1); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Use empty workspace folder if right one is not found', async () => { const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; @@ -108,14 +108,14 @@ suite('Debug - Attach to Child Process', () => { }; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn([wkspace1, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).atLeast(1); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config is passed as is', async () => { const data: LaunchRequestArguments | AttachRequestArguments = { @@ -131,17 +131,17 @@ suite('Debug - Attach to Child Process', () => { debugConfig.host = 'localhost'; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn(undefined); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); expect(thirdArg).to.deep.equal(session); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Pass data as is if data is attach debug configuration', async () => { const data: AttachRequestArguments = { @@ -152,17 +152,17 @@ suite('Debug - Attach to Child Process', () => { const session: any = {}; const debugConfig = JSON.parse(JSON.stringify(data)); - when(workspaceService.workspaceFolders).thenReturn(undefined); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); expect(thirdArg).to.deep.equal(session); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Validate debug config when parent/root parent was attached', async () => { const data: AttachRequestArguments = { @@ -180,16 +180,16 @@ suite('Debug - Attach to Child Process', () => { debugConfig.request = 'attach'; const session: any = {}; - when(workspaceService.workspaceFolders).thenReturn(undefined); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); await attachService.attach(data, session); - verify(workspaceService.workspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); expect(thirdArg).to.deep.equal(session); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); }); diff --git a/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts index 73390b28c627..43d81bbe1385 100644 --- a/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -14,11 +14,10 @@ import { IAttachProcessProviderFactory } from '../../../client/debugger/extensio import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider'; import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; -import { LaunchJsonReader } from '../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { IDebugConfigurationResolver, ILaunchJsonReader } from '../../../client/debugger/extension/configuration/types'; +import { IDebugConfigurationResolver } from '../../../client/debugger/extension/configuration/types'; import { DebugCommands } from '../../../client/debugger/extension/debugCommands'; import { ChildProcessAttachEventHandler } from '../../../client/debugger/extension/hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; @@ -141,6 +140,5 @@ suite('Debugging - Service Registry', () => { DebugCommands, ), ).once(); - verify(serviceManager.addSingleton(ILaunchJsonReader, LaunchJsonReader)).once(); }); }); diff --git a/extensions/positron-python/src/test/debuggerTest.ts b/extensions/positron-python/src/test/debuggerTest.ts index 9217b85e391c..949f14caee3d 100644 --- a/extensions/positron-python/src/test/debuggerTest.ts +++ b/extensions/positron-python/src/test/debuggerTest.ts @@ -4,11 +4,11 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; process.env.VSC_PYTHON_CI_TEST = '1'; -const channel = process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL || 'stable'; function start() { console.log('*'.repeat(100)); @@ -17,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: channel, + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Debugger tests (with errors)', ex); diff --git a/extensions/positron-python/src/test/extensionSettings.ts b/extensions/positron-python/src/test/extensionSettings.ts index d3e96c030a49..66a77589a770 100644 --- a/extensions/positron-python/src/test/extensionSettings.ts +++ b/extensions/positron-python/src/test/extensionSettings.ts @@ -6,6 +6,7 @@ 'use strict'; import { Event, Uri } from 'vscode'; +import { IApplicationEnvironment } from '../client/common/application/types'; import { WorkspaceService } from '../client/common/application/workspace'; import { InterpreterPathService } from '../client/common/interpreterPathService'; import { PersistentStateFactory } from '../client/common/persistentState'; @@ -44,7 +45,9 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings resource, new AutoSelectionService(), workspaceService, - new InterpreterPathService(persistentStateFactory, workspaceService, []), + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), undefined, ); } diff --git a/extensions/positron-python/src/test/insiders/languageServer.insiders.test.ts b/extensions/positron-python/src/test/insiders/languageServer.insiders.test.ts deleted file mode 100644 index 852c7d976733..000000000000 --- a/extensions/positron-python/src/test/insiders/languageServer.insiders.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { updateSetting } from '../common'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; -import { sleep } from '../core'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { openFileAndWaitForLS } from '../smoke/common'; - -const fileDefinitions = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'testMultiRootWkspc', - 'smokeTests', - 'definitions.py', -); - -// const notebookDefinitions = path.join( -// EXTENSION_ROOT_DIR_FOR_TESTS, -// 'src', -// 'testMultiRootWkspc', -// 'smokeTests', -// 'definitions.ipynb', -// ); - -suite('Insiders Test: Language Server', () => { - suiteSetup(async function () { - // This test should only run in the insiders build - if (vscode.env.appName.includes('Insider')) { - await updateSetting( - 'linting.ignorePatterns', - ['**/dir1/**'], - vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder, - ); - await initialize(); - } else { - this.skip(); - } - }); - setup(async () => { - await initializeTest(); - await closeActiveWindows(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await updateSetting( - 'linting.ignorePatterns', - undefined, - vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder, - ); - }); - teardown(closeActiveWindows); - - test('Definitions', async () => { - const pythonVersion = process.env.CI_PYTHON_VERSION ? parseFloat(process.env.CI_PYTHON_VERSION) : undefined; - if (pythonVersion && pythonVersion < 3) { - // Skip test for v2.7 - return; - } - - const startPosition = new vscode.Position(13, 6); - const textDocument = await openFileAndWaitForLS(fileDefinitions); - let tested = false; - for (let i = 0; i < 5; i += 1) { - const locations = await vscode.commands.executeCommand( - 'vscode.executeDefinitionProvider', - textDocument.uri, - startPosition, - ); - if (locations && locations.length > 0) { - expect(locations![0].uri.fsPath).to.contain(path.basename(fileDefinitions)); - tested = true; - break; - } else { - // Wait for LS to start. - await sleep(5_000); - } - } - if (!tested) { - assert.fail('Failed to test definitions'); - } - }); - // Commented the test out as `.edit` functionality is no longer available on `NotebookEditor` interface. - - // test('Notebooks', async () => { - // const startPosition = new vscode.Position(0, 6); - // const notebookDocument = await openNotebookAndWaitForLS(notebookDefinitions); - // let tested = false; - // for (let i = 0; i < 5; i += 1) { - // const locations = await vscode.commands.executeCommand( - // 'vscode.executeDefinitionProvider', - // notebookDocument.cellAt(2).document.uri, // Second cell should have a function with the decorator on it - // startPosition, - // ); - // if (locations && locations.length > 0) { - // expect(locations![0].uri.fsPath).to.contain(path.basename(notebookDefinitions)); - - // // Insert a new cell - // const activeEditor = vscode.window.activeNotebookEditor; - // expect(activeEditor).not.to.be.equal(undefined, 'Active editor not found in notebook'); - // await activeEditor!.edit((edit) => { - // edit.replaceCells(0, 0, [ - // new vscode.NotebookCellData(vscode.NotebookCellKind.Code, PYTHON_LANGUAGE, 'x = 4'), - // ]); - // }); - - // // Wait a bit to get diagnostics - // await sleep(1_000); - - // // Make sure no error diagnostics - // let diagnostics = vscode.languages.getDiagnostics(activeEditor!.document.uri); - // expect(diagnostics).to.have.lengthOf(0, 'Diagnostics found when shouldnt be'); - - // // Move the cell - // await activeEditor!.edit((edit) => { - // edit.replaceCells(0, 1, []); - // edit.replaceCells(1, 0, [ - // new vscode.NotebookCellData(vscode.NotebookCellKind.Code, PYTHON_LANGUAGE, 'x = 4'), - // ]); - // }); - - // // Wait a bit to get diagnostics - // await sleep(1_000); - - // // Make sure no error diagnostics - // diagnostics = vscode.languages.getDiagnostics(activeEditor!.document.uri); - // expect(diagnostics).to.have.lengthOf(0, 'Diagnostics found when shouldnt be after move'); - - // // Delete the cell - // await activeEditor!.edit((edit) => { - // edit.replaceCells(1, 1, []); - // }); - - // // Wait a bit to get diagnostics - // await sleep(1_000); - - // // Make sure no error diagnostics - // diagnostics = vscode.languages.getDiagnostics(activeEditor!.document.uri); - // expect(diagnostics).to.have.lengthOf(0, 'Diagnostics found when shouldnt be after delete'); - // tested = true; - - // break; - // } else { - // // Wait for LS to start. - // await sleep(5_000); - // } - // } - // if (!tested) { - // assert.fail('Failled to test notebooks'); - // } - // }); -}); diff --git a/extensions/positron-python/src/test/interpreters/display.unit.test.ts b/extensions/positron-python/src/test/interpreters/display.unit.test.ts index 63a3d2efa4b0..3537425f2efc 100644 --- a/extensions/positron-python/src/test/interpreters/display.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/display.unit.test.ts @@ -60,7 +60,9 @@ suite('Interpreters Display', () => { let traceLogStub: sinon.SinonStub; async function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { interpreterDisplay = new InterpreterDisplay(serviceContainer.object); - await interpreterDisplay.activate(); + try { + await interpreterDisplay.activate(); + } catch {} filters.forEach((f) => interpreterDisplay.registerVisibilityFilter(f)); } @@ -73,6 +75,7 @@ suite('Interpreters Display', () => { interpreterHelper = TypeMoq.Mock.ofType(); disposableRegistry = []; statusBar = TypeMoq.Mock.ofType(); + statusBar.setup((s) => s.name).returns(() => ''); languageStatusItem = TypeMoq.Mock.ofType(); pathUtils = TypeMoq.Mock.ofType(); @@ -95,7 +98,13 @@ suite('Interpreters Display', () => { serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); if (!useLanguageStatus) { applicationShell - .setup((a) => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Right), TypeMoq.It.isAny())) + .setup((a) => + a.createStatusBarItem( + TypeMoq.It.isValue(StatusBarAlignment.Right), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) .returns(() => statusBar.object); } else { applicationShell @@ -144,10 +153,7 @@ suite('Interpreters Display', () => { ); expect(disposableRegistry).contain(languageStatusItem.object); } else { - statusBar.verify( - (s) => (s.command = TypeMoq.It.isValue('python.setInterpreter')), - TypeMoq.Times.once(), - ); + statusBar.verify((s) => (s.command = TypeMoq.It.isAny()), TypeMoq.Times.once()); expect(disposableRegistry).contain(statusBar.object); } expect(disposableRegistry).to.be.lengthOf.above(0); diff --git a/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts b/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts index dff756cd3e64..00090eb4b6e9 100644 --- a/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts +++ b/extensions/positron-python/src/test/interpreters/serviceRegistry.unit.test.ts @@ -28,12 +28,18 @@ import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager, } from '../../client/interpreter/configuration/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from '../../client/interpreter/contracts'; +import { + IActivatedEnvironmentLaunch, + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, +} from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { InterpreterLocatorProgressStatubarHandler } from '../../client/interpreter/display/progressDisplay'; import { InterpreterHelper } from '../../client/interpreter/helpers'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import { registerTypes } from '../../client/interpreter/serviceRegistry'; +import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -69,6 +75,7 @@ suite('Interpreters - Service Registry', () => { [EnvironmentActivationService, EnvironmentActivationService], [IEnvironmentActivationService, EnvironmentActivationService], [IExtensionActivationService, CondaInheritEnvPrompt], + [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], ].forEach((mapping) => { // eslint-disable-next-line prefer-spread verify(serviceManager.addSingleton.apply(serviceManager, mapping as never)).once(); diff --git a/extensions/positron-python/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts b/extensions/positron-python/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts new file mode 100644 index 000000000000..04a5d3c95de1 --- /dev/null +++ b/extensions/positron-python/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { ExecutionResult, IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Common } from '../../../client/common/utils/localize'; +import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { ActivatedEnvironmentLaunch } from '../../../client/interpreter/virtualEnvs/activatedEnvLaunch'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Activated Env Launch', async () => { + const uri = Uri.file('a'); + const condaPrefix = 'path/to/conda/env'; + const virtualEnvPrefix = 'path/to/virtual/env'; + let workspaceService: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let pythonPathUpdaterService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let processServiceFactory: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let activatedEnvLaunch: ActivatedEnvironmentLaunch; + let _promptIfApplicable: sinon.SinonStub; + + suite('Method getPrefixOfSelectedActivatedEnv()', () => { + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const oldVirtualEnv = process.env.VIRTUAL_ENV; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + pythonPathUpdaterService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + _promptIfApplicable = sinon.stub(ActivatedEnvironmentLaunch.prototype, '_promptIfApplicable'); + _promptIfApplicable.returns(Promise.resolve()); + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + if (oldVirtualEnv) { + process.env.VIRTUAL_ENV = oldVirtualEnv; + } else { + delete process.env.VIRTUAL_ENV; + } + sinon.restore(); + }); + + test('Updates interpreter path with the non-base conda prefix if activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to not auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'false'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'true'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.calledOnce).to.equal(true, 'Prompt not displayed'); + }); + + test('Updates interpreter path with virtual env prefix if activated', async () => { + process.env.VIRTUAL_ENV = virtualEnvPrefix; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(virtualEnvPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(virtualEnvPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path in global scope if no workspace is opened', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('load'), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.notCalled).to.equal(true, 'Prompt should not be displayed'); + }); + + test('Does not update interpreter path if a multiroot workspace is opened', async () => { + process.env.VIRTUAL_ENV = virtualEnvPrefix; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => uri); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(virtualEnvPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Returns `undefined` if env was already selected', async () => { + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + true, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + }); + }); + + suite('Method _promptIfApplicable()', () => { + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + pythonPathUpdaterService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((p) => (p as any).then).returns(() => undefined); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + sinon.restore(); + }); + + test('Shows prompt if base conda environment is activated and auto activate configuration is disabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('If user chooses yes, update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('If user chooses no, do not update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('Do not show prompt if base conda environment is activated but auto activate configuration is enabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base True' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if non-base conda environment is activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'nonbase' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if conda environment is not activated', async () => { + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + }); +}); diff --git a/extensions/positron-python/src/test/linters/lint.functional.test.ts b/extensions/positron-python/src/test/linters/lint.functional.test.ts index e46b4217ec1f..4c06a26067a5 100644 --- a/extensions/positron-python/src/test/linters/lint.functional.test.ts +++ b/extensions/positron-python/src/test/linters/lint.functional.test.ts @@ -32,7 +32,11 @@ import { } from '../../client/common/types'; import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; -import { IComponentAdapter, IInterpreterService } from '../../client/interpreter/contracts'; +import { + IActivatedEnvironmentLaunch, + IComponentAdapter, + IInterpreterService, +} from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; import { ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; @@ -650,7 +654,13 @@ class TestFixture extends BaseTestFixture { serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(IComponentAdapter), TypeMoq.It.isAny())) .returns(() => componentAdapter.object); - + const activatedEnvironmentLaunch = TypeMoq.Mock.ofType(); + activatedEnvironmentLaunch + .setup((a) => a.selectIfLaunchedViaActivatedEnv()) + .returns(() => Promise.resolve(undefined)); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IActivatedEnvironmentLaunch), TypeMoq.It.isAny())) + .returns(() => activatedEnvironmentLaunch.object); const platformService = new PlatformService(); super( diff --git a/extensions/positron-python/src/test/mocks/vsc/extHostedTypes.ts b/extensions/positron-python/src/test/mocks/vsc/extHostedTypes.ts index bb60cfd0c698..4921b24629d1 100644 --- a/extensions/positron-python/src/test/mocks/vsc/extHostedTypes.ts +++ b/extensions/positron-python/src/test/mocks/vsc/extHostedTypes.ts @@ -7,14 +7,14 @@ 'use strict'; import { relative } from 'path'; -import type * as vscode from 'vscode'; +import * as vscode from 'vscode'; import * as vscMockHtmlContent from './htmlContent'; import * as vscMockStrings from './strings'; import * as vscUri from './uri'; import { generateUuid } from './uuid'; export enum NotebookCellKind { - Markdown = 1, + Markup = 1, Code = 2, } @@ -493,11 +493,11 @@ export class TextEdit { return ret; } - protected _range: Range = new Range(new Position(0, 0), new Position(0, 0)); + _range: Range = new Range(new Position(0, 0), new Position(0, 0)); - protected _newText = ''; + newText = ''; - protected _newEol: EndOfLine = EndOfLine.LF; + _newEol: EndOfLine = EndOfLine.LF; get range(): Range { return this._range; @@ -510,17 +510,6 @@ export class TextEdit { this._range = value; } - get newText(): string { - return this._newText || ''; - } - - set newText(value: string) { - if (value && typeof value !== 'string') { - throw illegalArgument('newText'); - } - this._newText = value; - } - get newEol(): EndOfLine { return this._newEol; } @@ -536,14 +525,6 @@ export class TextEdit { this.range = range; this.newText = newText; } - - toJSON(): { range: Range; newText: string; newEol: EndOfLine } { - return { - range: this.range, - newText: this.newText, - newEol: this._newEol, - }; - } } export class WorkspaceEdit implements vscode.WorkspaceEdit { @@ -664,7 +645,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { return this._textEdits.has(uri.toString()); } - set(uri: vscUri.URI, edits: TextEdit[]): void { + set(uri: vscUri.URI, edits: readonly unknown[]): void { let data = this._textEdits.get(uri.toString()); if (!data) { data = { seq: this._seqPool += 1, uri, edits: [] }; @@ -673,7 +654,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { if (!edits) { data.edits = []; } else { - data.edits = edits.slice(0); + data.edits = edits.slice(0) as TextEdit[]; } } @@ -913,19 +894,16 @@ export class Diagnostic { } export class Hover { - public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + public contents: vscode.MarkdownString[]; public range: Range; - constructor( - contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], - range?: Range, - ) { + constructor(contents: vscode.MarkdownString | vscode.MarkdownString[], range?: Range) { if (!contents) { throw new Error('Illegal argument, contents must be defined'); } if (Array.isArray(contents)) { - this.contents = contents; + this.contents = contents; } else if (vscMockHtmlContent.isMarkdownString(contents)) { this.contents = [contents]; } else { diff --git a/extensions/positron-python/src/test/mocks/vsc/index.ts b/extensions/positron-python/src/test/mocks/vsc/index.ts index bbd3d23091b1..e6ea57a88673 100644 --- a/extensions/positron-python/src/test/mocks/vsc/index.ts +++ b/extensions/positron-python/src/test/mocks/vsc/index.ts @@ -62,6 +62,43 @@ export class Disposable { } } +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace l10n { + export function t(message: string, ...args: unknown[]): string; + export function t(options: { + message: string; + args?: Array | Record; + comment: string | string[]; + }): string; + + export function t( + message: + | string + | { + message: string; + args?: Array | Record; + comment: string | string[]; + }, + ...args: unknown[] + ): string { + let _message = message; + let _args: unknown[] | Record | undefined = args; + if (typeof message !== 'string') { + _message = message.message; + _args = message.args ?? args; + } + + if ((_args as Array).length > 0) { + return (_message as string).replace(/{(\d+)}/g, (match, number) => + (_args as Array)[number] === undefined ? match : (_args as Array)[number], + ); + } + return _message as string; + } + export const bundle: { [key: string]: string } | undefined = undefined; + export const uri: vscode.Uri | undefined = undefined; +} + export class EventEmitter implements vscode.EventEmitter { public event: vscode.Event; @@ -302,6 +339,8 @@ export class CodeActionKind { public static readonly RefactorInline: CodeActionKind = new CodeActionKind('refactor.inline'); + public static readonly RefactorMove: CodeActionKind = new CodeActionKind('refactor.move'); + public static readonly RefactorRewrite: CodeActionKind = new CodeActionKind('refactor.rewrite'); public static readonly Source: CodeActionKind = new CodeActionKind('source'); diff --git a/extensions/positron-python/src/test/multiRootTest.ts b/extensions/positron-python/src/test/multiRootTest.ts index 089b10a46d46..c8c63b6dabe5 100644 --- a/extensions/positron-python/src/test/multiRootTest.ts +++ b/extensions/positron-python/src/test/multiRootTest.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { runTests } from '@vscode/test-electron'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; import { initializeLogger } from './testLogger'; +import { getChannel } from './utils/vscode'; const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; @@ -9,8 +10,6 @@ process.env.VSC_PYTHON_CI_TEST = '1'; initializeLogger(); -const channel = process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL || 'stable'; - function start() { console.log('*'.repeat(100)); console.log('Start Multiroot tests'); @@ -18,7 +17,7 @@ function start() { extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), launchArgs: [workspacePath], - version: channel, + version: getChannel(), extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, }).catch((ex) => { console.error('End Multiroot tests (with errors)', ex); diff --git a/extensions/positron-python/src/test/proposedApi.unit.test.ts b/extensions/positron-python/src/test/proposedApi.unit.test.ts index 1a834d62a6a5..80db62f4814b 100644 --- a/extensions/positron-python/src/test/proposedApi.unit.test.ts +++ b/extensions/positron-python/src/test/proposedApi.unit.test.ts @@ -99,8 +99,6 @@ suite('Proposed Extension API', () => { }); teardown(() => { - // Verify each API method sends telemetry regarding who called the API. - extensions.verifyAll(); sinon.restore(); }); diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index c0915bfa4aa9..ac7157365f02 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -420,17 +420,20 @@ suite('Python envs locator - Environments Collection', async () => { assertEnvEqual(resolved, env); }); - test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { - const cachedEnvs = getCachedEnvs(); - const env = cachedEnvs[0]; - const resolvedViaLocator = buildEnvInfo({ - executable: env.executable.filename, - sysPrefix: 'Resolved via locator', - }); - env.executable.ctime = 101; - env.executable.mtime = 90; + test('resolveEnv() does not use cache if complete info is not available', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const deferred = createDeferred(); + const waitDeferred = createDeferred(); + const locatedEnvs = getLocatorEnvs(); + const env = locatedEnvs[0]; + env.executable.ctime = 100; + env.executable.mtime = 100; sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); - const parentLocator = new SimpleLocator([], { + const parentLocator = new SimpleLocator(locatedEnvs, { + after: async () => { + waitDeferred.resolve(); + await deferred.promise; + }, resolve: async (e: PythonEnvInfo) => { if (env.executable.filename === e.executable.filename) { return resolvedViaLocator; @@ -439,18 +442,27 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - get: () => cachedEnvs, + get: () => [], store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator); + collectionService.triggerRefresh().ignoreErrors(); + await waitDeferred.promise; // Cache should already contain `env` at this point, although it is not complete. + collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); }); - test('resolveEnv() uses underlying locator if cache does not have complete info for env', async () => { - const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { const cachedEnvs = getCachedEnvs(); const env = cachedEnvs[0]; + const resolvedViaLocator = buildEnvInfo({ + executable: env.executable.filename, + sysPrefix: 'Resolved via locator', + }); + env.executable.ctime = 101; + env.executable.mtime = 90; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); const parentLocator = new SimpleLocator([], { resolve: async (e: PythonEnvInfo) => { if (env.executable.filename === e.executable.filename) { diff --git a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.testvirtualenvs.ts b/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.testvirtualenvs.ts deleted file mode 100644 index d8472013db04..000000000000 --- a/extensions/positron-python/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.testvirtualenvs.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { ExecutionResult, ShellOptions } from '../../../../../client/common/process/types'; -import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; -import { BasicEnvInfo, ILocator } from '../../../../../client/pythonEnvironments/base/locator'; -import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; -import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; -import { PoetryLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/poetryLocator'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../../constants'; -import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; -import { testLocatorWatcher } from './watcherTestUtils'; - -suite('Poetry Watcher', async () => { - let shellExecute: sinon.SinonStub; - const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); - const project1 = path.join(testPoetryDir, 'project1'); - suiteSetup(async function () { - // Skipping these test see https://github.com/microsoft/vscode-python/issues/17087 - this.skip(); - - shellExecute = sinon.stub(externalDependencies, 'shellExecute'); - shellExecute.callsFake((command: string, options: ShellOptions) => { - // eslint-disable-next-line default-case - if (command === 'poetry env list --full-path') { - return Promise.resolve>({ stdout: '' }); - } - if (command === 'poetry config virtualenvs.path') { - if (options.cwd && externalDependencies.arePathsSame(options.cwd, project1)) { - return Promise.resolve>({ - stdout: `${testPoetryDir} \n`, - }); - } - } - return Promise.reject(new Error('Command failed')); - }); - }); - testLocatorWatcher(testPoetryDir, async () => new PoetryLocator(project1), { - kind: PythonEnvKind.Poetry, - doNotVerifyIfLocated: true, - }); - - suiteTeardown(() => sinon.restore()); -}); - -suite('Poetry Locator', async () => { - let locator: ILocator; - suiteSetup(async function () { - if (process.env.CI_PYTHON_VERSION && process.env.CI_PYTHON_VERSION.startsWith('2.')) { - // Poetry is soon to be deprecated for Python2.7, and tests do not pass - // as it is with pip installation of poetry, hence skip. - this.skip(); - } - locator = new PoetryLocator(EXTENSION_ROOT_DIR_FOR_TESTS); - }); - - test('Discovers existing poetry environments', async () => { - const items = await getEnvs(locator.iterEnvs()); - const isLocated = items.some( - (item) => item.kind === PythonEnvKind.Poetry && item.executablePath.includes('poetry-tutorial-project'), - ); - expect(isLocated).to.equal(true); - }); -}); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 6b6187ed3e4a..856733b29a1c 100644 --- a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -22,6 +22,7 @@ import { createDeferred } from '../../../../client/common/utils/async'; import { Output } from '../../../../client/common/process/types'; import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; import { CreateEnv } from '../../../../client/common/utils/localize'; +import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; chaiUse(chaiAsPromised); @@ -33,12 +34,20 @@ suite('venv Creation provider tests', () => { let execObservableStub: sinon.SinonStub; let withProgressStub: sinon.SinonStub; let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickPackagesToInstallStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); interpreterQuickPick = typemoq.Mock.ofType(); withProgressStub = sinon.stub(windowApis, 'withProgress'); + pickPackagesToInstallStub = sinon.stub(venvUtils, 'pickPackagesToInstall'); showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); showErrorMessageWithLogsStub.resolves(); @@ -59,11 +68,7 @@ suite('venv Creation provider tests', () => { }); test('No Python selected', async () => { - pickWorkspaceFolderStub.resolves({ - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }); + pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) @@ -74,12 +79,21 @@ suite('venv Creation provider tests', () => { interpreterQuickPick.verifyAll(); }); + test('User pressed Esc while selecting dependencies', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves(undefined); + + assert.isUndefined(await venvProvider.createEnvironment()); + assert.isTrue(pickPackagesToInstallStub.calledOnce); + }); + test('Create venv with python selected by user', async () => { - const workspace1 = { - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }; pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick @@ -87,6 +101,11 @@ suite('venv Creation provider tests', () => { .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); + pickPackagesToInstallStub.resolves({ + installType: 'none', + installList: [], + }); + const deferred = createDeferred(); let _next: undefined | ((value: Output) => void); let _complete: undefined | (() => void); @@ -138,17 +157,18 @@ suite('venv Creation provider tests', () => { }); test('Create venv failed', async () => { - pickWorkspaceFolderStub.resolves({ - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }); + pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); + pickPackagesToInstallStub.resolves({ + installType: 'none', + installList: [], + }); + const deferred = createDeferred(); let _error: undefined | ((error: unknown) => void); let _complete: undefined | (() => void); @@ -194,11 +214,6 @@ suite('venv Creation provider tests', () => { }); test('Create venv failed (non-zero exit code)', async () => { - const workspace1 = { - uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), - name: 'workspace1', - index: 0, - }; pickWorkspaceFolderStub.resolves(workspace1); interpreterQuickPick @@ -206,6 +221,11 @@ suite('venv Creation provider tests', () => { .returns(() => Promise.resolve('/usr/bin/python')) .verifiable(typemoq.Times.once()); + pickPackagesToInstallStub.resolves({ + installType: 'none', + installList: [], + }); + const deferred = createDeferred(); let _next: undefined | ((value: Output) => void); let _complete: undefined | (() => void); diff --git a/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts new file mode 100644 index 000000000000..5627feee598d --- /dev/null +++ b/extensions/positron-python/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { pickPackagesToInstall } from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Venv Utils test', () => { + let findFilesStub: sinon.SinonStub; + let showQuickPickStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + findFilesStub = sinon.stub(workspaceApis, 'findFiles'); + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No requirements or toml found', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(false); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickStub.notCalled); + assert.deepStrictEqual(actual, { + installType: 'none', + installList: [], + }); + }); + + test('Toml found with no optional deps', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickStub.notCalled); + assert.deepStrictEqual(actual, { + installType: 'toml', + installList: [], + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }); + }); + + test('Toml found with deps, but user presses escape', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickStub.resolves(undefined); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, undefined); + }); + + test('Toml found with dependencies and user selects None', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'toml', + installList: [], + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }); + }); + + test('Toml found with dependencies and user selects One', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickStub.resolves([{ label: 'doc' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'toml', + installList: ['doc'], + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }); + }); + + test('Toml found with dependencies and user selects Few', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]\ncov = ["pytest-cov"]', + ); + + showQuickPickStub.resolves([{ label: 'test' }, { label: 'cov' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }, { label: 'cov' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'toml', + installList: ['test', 'cov'], + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }); + }); + + test('Requirements found, but user presses escape', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickStub.resolves(undefined); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, undefined); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects None', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'requirements', + installList: [], + }); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects One', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickStub.resolves([{ label: 'requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'requirements', + installList: [path.join(workspace1.uri.fsPath, 'requirements.txt')], + }); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects Few', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickStub.resolves([{ label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickStub.calledWithExactly( + [{ label: 'requirements.txt' }, { label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, { + installType: 'requirements', + installList: [ + path.join(workspace1.uri.fsPath, 'dev-requirements.txt'), + path.join(workspace1.uri.fsPath, 'test-requirements.txt'), + ], + }); + assert.isTrue(readFileStub.notCalled); + }); +}); diff --git a/extensions/positron-python/src/test/standardTest.ts b/extensions/positron-python/src/test/standardTest.ts index 95b1e1cf42ee..0562d1adf431 100644 --- a/extensions/positron-python/src/test/standardTest.ts +++ b/extensions/positron-python/src/test/standardTest.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../client/common/constants'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { getChannel } from './utils/vscode'; // If running smoke tests, we don't have access to this. if (process.env.TEST_FILES_SUFFIX !== 'smoke.test') { @@ -27,8 +28,6 @@ const extensionDevelopmentPath = process.env.CODE_EXTENSIONS_PATH ? process.env.CODE_EXTENSIONS_PATH : EXTENSION_ROOT_DIR_FOR_TESTS; -const channel = process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL || 'stable'; - /** * Smoke tests & tests running in VSCode require Jupyter extension to be installed. */ @@ -77,6 +76,8 @@ async function installPylanceExtension(vscodeExecutablePath: string) { async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); + const channel = getChannel(); + console.log(`Using ${channel} build of VS Code.`); const vscodeExecutablePath = await downloadAndUnzipVSCode(channel); const baseLaunchArgs = requiresJupyterExtensionToBeInstalled() || requiresPylanceExtensionToBeInstalled() diff --git a/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.insiders.test.ts b/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.insiders.test.ts deleted file mode 100644 index 086317ad021e..000000000000 --- a/extensions/positron-python/src/test/tensorBoard/nbextensionCodeLensProvider.insiders.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { CodeLens, commands, env, window } from 'vscode'; -import { IExperimentService } from '../../client/common/types'; -import { IServiceManager } from '../../client/ioc/types'; -import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; -import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; -import { - closeActiveNotebooks, - closeActiveWindows, - EXTENSION_ROOT_DIR_FOR_TESTS, - initialize, - initializeTest, -} from '../initialize'; -import { openFile, waitForCondition } from '../common'; -import { openNotebook } from '../smoke/common'; - -suite('TensorBoard code lens provider', () => { - suiteSetup(async function () { - // This test should only run in the insiders build because it relies on - // being able to open native notebooks - if (!env.appName.includes('Insider')) { - this.skip(); - } - }); - suiteTeardown(closeActiveWindows); - suite('Nbextension', () => { - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let experimentService: IExperimentService; - let serviceManager: IServiceManager; - setup(async () => { - ({ serviceManager } = await initialize()); - await initializeTest(); - await closeActiveWindows(); - experimentService = serviceManager.get(IExperimentService); - sandbox.stub(experimentService, 'inExperiment').resolves(true); - codeLensProvider = serviceManager.get( - TensorBoardNbextensionCodeLensProvider, - ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (codeLensProvider as any).activateInternal(); - }); - teardown(async () => { - sandbox.restore(); - await closeActiveWindows(); - await closeActiveNotebooks(); - }); - test('Does not provide codelenses for Python file loading tensorboard nbextension', async () => { - const spy = sandbox.spy(codeLensProvider, 'provideCodeLenses'); - await openFile( - path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'pythonFiles', - 'tensorBoard', - 'tensorboard_launch.py', - ), - ); - assert.ok(spy.notCalled, 'Called provideCodeLens for Python file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook loading and launching tensorboard nbextension', async () => { - const filePath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'pythonFiles', - 'tensorBoard', - 'tensorboard_nbextension.ipynb', - ); - const notebook = await openNotebook(filePath); - assert(window.activeTextEditor, 'No active editor'); - const codeLenses = await commands.executeCommand( - 'vscode.executeCodeLensProvider', - notebook.cellAt(0).document.uri, - ); - assert.ok(codeLenses?.length && codeLenses.length > 0, 'Code lens provider did not provide codelenses'); - }); - }); - suite('Imports', () => { - let codeLensProvider: TensorBoardImportCodeLensProvider; - let experimentService: IExperimentService; - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let serviceManager: IServiceManager; - let spy: sinon.SinonSpy; - setup(async () => { - ({ serviceManager } = await initialize()); - await initializeTest(); - await closeActiveWindows(); - experimentService = serviceManager.get(IExperimentService); - sandbox.stub(experimentService, 'inExperiment').resolves(true); - codeLensProvider = serviceManager.get(TensorBoardImportCodeLensProvider); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (codeLensProvider as any).activateInternal(); - spy = sandbox.spy(codeLensProvider, 'provideCodeLenses'); - }); - teardown(() => { - sandbox.restore(); - }); - test('Provides code lens for Python file importing tensorboard', async () => { - await openFile( - path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'pythonFiles', - 'tensorBoard', - 'tensorboard_imports.py', - ), - ); - await waitForCondition( - async () => spy.called, - 5000, - 'provideCodeLenses not called for Python file loading tensorboard nbextension', - ); - assert.ok(spy.returnValues.length > 0, 'No return values recorded for provideCodeLens'); - assert.ok(spy.returnValues[0].length === 1, 'provideCodeLenses did not return codelenses'); - }); - test('Provide code lens for Python notebook importing tensorboard', async () => { - const filePath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'pythonFiles', - 'tensorBoard', - 'tensorboard_import.ipynb', - ); - const notebook = await openNotebook(filePath); - assert(window.activeTextEditor, 'No active editor'); - const codeLenses = await commands.executeCommand( - 'vscode.executeCodeLensProvider', - notebook.cellAt(0).document.uri, - ); - assert.ok(codeLenses?.length && codeLenses.length > 0, 'Code lens provider did not provide codelenses'); - }); - test('Does not provide code lens if no matching import', async () => { - await openFile( - path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'pythonFiles', 'tensorBoard', 'noMatch.py'), - ); - assert.ok(spy.notCalled, 'Called provideCodeLens for Python file loading tensorboard nbextension'); - }); - }); -}); diff --git a/extensions/positron-python/src/test/testing/common/debugLauncher.unit.test.ts b/extensions/positron-python/src/test/testing/common/debugLauncher.unit.test.ts index 44ff5478922b..dfe9e8ce5e99 100644 --- a/extensions/positron-python/src/test/testing/common/debugLauncher.unit.test.ts +++ b/extensions/positron-python/src/test/testing/common/debugLauncher.unit.test.ts @@ -6,19 +6,19 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; +import * as fs from 'fs-extra'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; import { CancellationTokenSource, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; import { IInvalidPythonPathInDebuggerService } from '../../../client/application/diagnostics/types'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../client/common/application/types'; +import { IApplicationShell, IDebugService } from '../../../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; import '../../../client/common/extensions'; -import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; import { DebuggerTypeName } from '../../../client/debugger/constants'; -import { LaunchJsonReader } from '../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { ILaunchJsonReader } from '../../../client/debugger/extension/configuration/types'; import { DebugOptions } from '../../../client/debugger/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -36,13 +36,13 @@ suite('Unit Tests - Debug Launcher', () => { let unitTestSettings: TypeMoq.IMock; let debugLauncher: DebugLauncher; let debugService: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let filesystem: TypeMoq.IMock; let settings: TypeMoq.IMock; let debugEnvHelper: TypeMoq.IMock; - let launchJsonReader: ILaunchJsonReader; let interpreterService: TypeMoq.IMock; + let getWorkspaceFolderStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; setup(async () => { interpreterService = TypeMoq.Mock.ofType(); @@ -54,19 +54,10 @@ suite('Unit Tests - Debug Launcher', () => { debugService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDebugService))).returns(() => debugService.object); - - workspaceService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - platformService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - - filesystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => filesystem.object); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); const appShell = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); @@ -83,14 +74,13 @@ suite('Unit Tests - Debug Launcher', () => { .setup((c) => c.get(TypeMoq.It.isValue(IDebugEnvironmentVariablesService))) .returns(() => debugEnvHelper.object); - launchJsonReader = new LaunchJsonReader(filesystem.object, workspaceService.object); + debugLauncher = new DebugLauncher(serviceContainer.object, getNewResolver(configService.object)); + }); - debugLauncher = new DebugLauncher( - serviceContainer.object, - getNewResolver(configService.object), - launchJsonReader, - ); + teardown(() => { + sinon.restore(); }); + function getNewResolver(configService: IConfigurationService) { const validator = TypeMoq.Mock.ofType( undefined, @@ -111,7 +101,6 @@ suite('Unit Tests - Debug Launcher', () => { expected: DebugConfiguration, testProvider: TestProvider, ) { - platformService.setup((p) => p.isWindows).returns(() => /^win/.test(process.platform)); interpreterService .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); @@ -184,22 +173,21 @@ suite('Unit Tests - Debug Launcher', () => { const testLaunchScript = getTestLauncherScript(testProvider); const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; - workspaceService.setup((u) => u.workspaceFolders).returns(() => workspaceFolders); - workspaceService.setup((u) => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); if (!debugConfigs) { - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + pathExistsStub.resolves(false); } else { - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pathExistsStub.resolves(true); + if (typeof debugConfigs !== 'string') { debugConfigs = JSON.stringify({ version: '0.1.0', configurations: debugConfigs, }); } - filesystem - .setup((fs) => fs.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(debugConfigs as string)); + readFileStub.resolves(debugConfigs as string); } if (!expected) { @@ -298,7 +286,7 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); test(`Must throw an exception if there are no workspaces ${testTitleSuffix}`, async () => { - workspaceService.setup((u) => u.workspaceFolders).returns(() => undefined); + getWorkspaceFoldersStub.returns(undefined); debugService .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined as any)) @@ -588,8 +576,8 @@ suite('Unit Tests - Debug Launcher', () => { const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); const jsonc = '{"version":"1234", "configurations":[1,2,],}'; - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); - filesystem.setup((fs) => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); @@ -600,8 +588,8 @@ suite('Unit Tests - Debug Launcher', () => { const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); const jsonc = '{"version":"1234"'; - filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); - filesystem.setup((fs) => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); diff --git a/extensions/positron-python/src/test/utils/vscode.ts b/extensions/positron-python/src/test/utils/vscode.ts new file mode 100644 index 000000000000..a10ceb2e8881 --- /dev/null +++ b/extensions/positron-python/src/test/utils/vscode.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; + +const insidersVersion = /^\^(\d+\.\d+\.\d+)-(insider|\d{8})$/; + +export function getChannel(): string { + if (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL) { + return process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL; + } + const packageJsonPath = path.join(EXTENSION_ROOT_DIR, 'package.json'); + if (fs.pathExistsSync(packageJsonPath)) { + const packageJson = fs.readJSONSync(packageJsonPath); + const engineVersion = packageJson.engines.vscode; + if (insidersVersion.test(engineVersion)) { + // Can't pass in the version number for an insiders build; + // https://github.com/microsoft/vscode-test/issues/176 + return 'insiders'; + } + return engineVersion.replace('^', ''); + } + return 'stable'; +} diff --git a/extensions/positron-python/src/test/vscode-mock.ts b/extensions/positron-python/src/test/vscode-mock.ts index 8e1f319bee08..9fb4bbec329b 100644 --- a/extensions/positron-python/src/test/vscode-mock.ts +++ b/extensions/positron-python/src/test/vscode-mock.ts @@ -63,6 +63,7 @@ export function initialize() { } mockedVSCode.ThemeIcon = vscodeMocks.ThemeIcon; +mockedVSCode.l10n = vscodeMocks.l10n; mockedVSCode.ThemeColor = vscodeMocks.ThemeColor; mockedVSCode.MarkdownString = vscodeMocks.MarkdownString; mockedVSCode.Hover = vscodeMocks.Hover; diff --git a/extensions/positron-python/yarn.lock b/extensions/positron-python/yarn.lock index 1fa0732a5738..25096d3d6639 100644 --- a/extensions/positron-python/yarn.lock +++ b/extensions/positron-python/yarn.lock @@ -10,6 +10,65 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@azure/abort-controller@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.1.0.tgz#788ee78457a55af8a1ad342acb182383d2119249" + integrity sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw== + dependencies: + tslib "^2.2.0" + +"@azure/core-auth@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.4.0.tgz#6fa9661c1705857820dbc216df5ba5665ac36a9e" + integrity sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + +"@azure/core-http@^2.2.3": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-2.3.1.tgz#eed8a7d012ba8c576c557828f66af0fc4e52b23a" + integrity sha512-cur03BUwV0Tbv81bQBOLafFB02B6G++K6F2O3IMl8pSE2QlXm3cu11bfyBNlDUKi5U+xnB3GC63ae3athhkx6Q== + dependencies: + "@azure/abort-controller" "^1.0.0" + "@azure/core-auth" "^1.3.0" + "@azure/core-tracing" "1.0.0-preview.13" + "@azure/core-util" "^1.1.1" + "@azure/logger" "^1.0.0" + "@types/node-fetch" "^2.5.0" + "@types/tunnel" "^0.0.3" + form-data "^4.0.0" + node-fetch "^2.6.7" + process "^0.11.10" + tough-cookie "^4.0.0" + tslib "^2.2.0" + tunnel "^0.0.6" + uuid "^8.3.0" + xml2js "^0.4.19" + +"@azure/core-tracing@1.0.0-preview.13": + version "1.0.0-preview.13" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz#55883d40ae2042f6f1e12b17dd0c0d34c536d644" + integrity sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ== + dependencies: + "@opentelemetry/api" "^1.0.1" + tslib "^2.2.0" + +"@azure/core-util@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.1.1.tgz#8f87b3dd468795df0f0849d9f096c3e7b29452c1" + integrity sha512-A4TBYVQCtHOigFb2ETiiKFDocBoI1Zk2Ui1KpI42aJSIDexF7DHQFpnjonltXAIU/ceH+1fsZAWWgvX6/AKzog== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + +"@azure/logger@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@azure/logger/-/logger-1.0.3.tgz#6e36704aa51be7d4a1bae24731ea580836293c96" + integrity sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g== + dependencies: + tslib "^2.2.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -252,6 +311,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@iarna/toml@^2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" + integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -331,7 +395,7 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@microsoft/1ds-core-js@3.2.8", "@microsoft/1ds-core-js@^3.2.3": +"@microsoft/1ds-core-js@3.2.8", "@microsoft/1ds-core-js@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.8.tgz#1b6b7d9bb858238c818ccf4e4b58ece7aeae5760" integrity sha512-9o9SUAamJiTXIYwpkQDuueYt83uZfXp8zp8YFix1IwVPwC9RmE36T2CX9gXOeq1nDckOuOduYpA8qHvdh5BGfQ== @@ -340,7 +404,7 @@ "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" -"@microsoft/1ds-post-js@^3.2.3": +"@microsoft/1ds-post-js@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.8.tgz#46793842cca161bf7a2a5b6053c349f429e55110" integrity sha512-SjlRoNcXcXBH6WQD/5SkkaCHIVqldH3gDu+bI7YagrOVJ5APxwT1Duw9gm3L1FjFa9S2i81fvJ3EVSKpp9wULA== @@ -349,6 +413,25 @@ "@microsoft/applicationinsights-shims" "^2.0.2" "@microsoft/dynamicproto-js" "^1.1.7" +"@microsoft/applicationinsights-channel-js@2.8.9": + version "2.8.9" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.8.9.tgz#840656f3c716de8b3eb0a98c122aa1b92bb8ebfb" + integrity sha512-fMBsAEB7pWtPn43y72q9Xy5E5y55r6gMuDQqRRccccVoQDPXyS57VCj5IdATblctru0C6A8XpL2vRyNmEsu0Vg== + dependencies: + "@microsoft/applicationinsights-common" "2.8.9" + "@microsoft/applicationinsights-core-js" "2.8.9" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/applicationinsights-common@2.8.9": + version "2.8.9" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-common/-/applicationinsights-common-2.8.9.tgz#a75e4a3143a7fd797687830c0ddd2069fd900827" + integrity sha512-mObn1moElyxZaGIRF/IU3cOaeKMgxghXnYEoHNUCA2e+rNwBIgxjyKkblFIpmGuHf4X7Oz3o3yBWpaC6AoMpig== + dependencies: + "@microsoft/applicationinsights-core-js" "2.8.9" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + "@microsoft/applicationinsights-core-js@2.8.9": version "2.8.9" resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.9.tgz#0e5d207acfae6986a6fc97249eeb6117e523bf1b" @@ -362,6 +445,22 @@ resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== +"@microsoft/applicationinsights-web-basic@^2.8.9": + version "2.8.9" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-2.8.9.tgz#eed2f3d1e19069962ed2155915c1656e6936e1d5" + integrity sha512-CH0J8JFOy7MjK8JO4pXXU+EML+Ilix+94PMZTX5EJlBU1in+mrik74/8qSg3UC4ekPi12KwrXaHCQSVC3WseXQ== + dependencies: + "@microsoft/applicationinsights-channel-js" "2.8.9" + "@microsoft/applicationinsights-common" "2.8.9" + "@microsoft/applicationinsights-core-js" "2.8.9" + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/applicationinsights-web-snippet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz#6bb788b2902e48bf5d460c38c6bb7fedd686ddd7" + integrity sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ== + "@microsoft/dynamicproto-js@^1.1.7": version "1.1.7" resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.7.tgz#ede48dd3f85af14ee369c805e5ed5b84222b9fe2" @@ -388,6 +487,40 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@^1.0.1", "@opentelemetry/api@^1.0.4": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" + integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== + +"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.0.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" + integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== + dependencies: + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/resources@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" + integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-trace-base@^1.0.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" + integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" + integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== + "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" @@ -431,16 +564,6 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@ts-morph/common@~0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.16.0.tgz#57e27d4b3fd65a4cd72cb36679ed08acb40fa3ba" - integrity sha512-SgJpzkTgZKLKqQniCjLaE3c2L2sdL7UShvmTmPBejAKd2OKV/yfMpQ2IWpAuA+VY5wy7PkSUaEObIqEK6afFuw== - dependencies: - fast-glob "^3.2.11" - minimatch "^5.1.0" - mkdirp "^1.0.4" - path-browserify "^1.0.1" - "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -596,6 +719,14 @@ dependencies: "@types/node" "*" +"@types/node-fetch@^2.5.0": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" + integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "18.11.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.7.tgz#8ccef136f240770c1379d50100796a6952f01f94" @@ -653,6 +784,13 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/tunnel@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.3.tgz#f109e730b072b3136347561fc558c9358bb8c6e9" + integrity sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA== + dependencies: + "@types/node" "*" + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -740,13 +878,15 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@vscode/extension-telemetry@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.6.2.tgz#b86814ee680615730da94220c2b03ea9c3c14a8e" - integrity sha512-yb/wxLuaaCRcBAZtDCjNYSisAXz3FWsSqAha5nhHcYxx2ZPdQdWuZqVXGKq0ZpHVndBWWtK6XqtpCN2/HB4S1w== +"@vscode/extension-telemetry@^0.7.4-preview": + version "0.7.4-preview" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.7.4-preview.tgz#318d6bc54064e5f443b25dfb42fec724d888c36b" + integrity sha512-6OkvjCc+DaC9B26t3hj7vuAxf1ONm/p4LrVvFrapa+jBCKxXXUaV1Asz6+QxYaPfd4Ws/MlnFfCvlgvv3uYRwQ== dependencies: - "@microsoft/1ds-core-js" "^3.2.3" - "@microsoft/1ds-post-js" "^3.2.3" + "@microsoft/1ds-core-js" "^3.2.8" + "@microsoft/1ds-post-js" "^3.2.8" + "@microsoft/applicationinsights-web-basic" "^2.8.9" + applicationinsights "2.3.6" "@vscode/jupyter-lsp-middleware@^0.2.50": version "0.2.50" @@ -769,23 +909,6 @@ vscode-languageserver-protocol "^3.17.3-next.1" vscode-uri "^3.0.2" -"@vscode/ripgrep@^1.14.2": - version "1.14.2" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34" - integrity sha512-KDaehS8Jfdg1dqStaIPDKYh66jzKd5jy5aYEPzIv0JYFLADPsCSQPBUdsJVXnr0t72OlDcj96W05xt/rSnNFFQ== - dependencies: - https-proxy-agent "^5.0.0" - proxy-from-env "^1.1.0" - -"@vscode/telemetry-extractor@>=1.9.8": - version "1.9.8" - resolved "https://registry.yarnpkg.com/@vscode/telemetry-extractor/-/telemetry-extractor-1.9.8.tgz#ffc000720ea2b9cd3421ba8a7bd172972c398b06" - integrity sha512-L27/fgC/gM7AY6AXriFGrznnX1M4Nc7VmHabYinDPoJDQYLjbSEDDVjjlSS6BiVkzc3OrFQStqXpHBhImis2eQ== - dependencies: - "@vscode/ripgrep" "^1.14.2" - command-line-args "^5.2.1" - ts-morph "^15.1.0" - "@vscode/test-electron@^2.1.3": version "2.2.0" resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.2.0.tgz#b90c76b8f076f9cf2d885f14b3c5fc3f586b9419" @@ -1097,6 +1220,22 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" +applicationinsights@2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-2.3.6.tgz#91277ce44e5f6d2f85336922c05d90f8699c2e70" + integrity sha512-ZzXXpZpDRGcy6Pp5V319nDF9/+Ey7jNknEXZyaBajtC5onN0dcBem6ng5jcb3MPH2AjYWRI8XgyNEuzP/6Y5/A== + dependencies: + "@azure/core-http" "^2.2.3" + "@microsoft/applicationinsights-web-snippet" "^1.0.1" + "@opentelemetry/api" "^1.0.4" + "@opentelemetry/core" "^1.0.1" + "@opentelemetry/sdk-trace-base" "^1.0.1" + "@opentelemetry/semantic-conventions" "^1.0.1" + cls-hooked "^4.2.2" + continuation-local-storage "^3.2.1" + diagnostic-channel "1.1.0" + diagnostic-channel-publishers "1.0.5" + arch@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" @@ -1168,17 +1307,12 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== -array-back@^3.0.1, array-back@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - array-each@^1.0.0, array-each@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA== -array-includes@^3.1.4, array-includes@^3.1.5: +array-includes@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb" integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== @@ -1189,6 +1323,17 @@ array-includes@^3.1.4, array-includes@^3.1.5: get-intrinsic "^1.1.1" is-string "^1.0.7" +array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + array-initial@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" @@ -1228,14 +1373,14 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== -array.prototype.flat@^1.2.5: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" - integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" array.prototype.flatmap@^1.3.0: @@ -1248,6 +1393,16 @@ array.prototype.flatmap@^1.3.0: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -1323,6 +1478,21 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-hook-jl@^1.7.6: + version "1.7.6" + resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68" + integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg== + dependencies: + stack-chain "^1.3.7" + +async-listener@^0.6.0: + version "0.6.10" + resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc" + integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw== + dependencies: + semver "^5.3.0" + shimmer "^1.1.0" + async-settle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" @@ -1973,15 +2143,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -2008,7 +2169,7 @@ clone-stats@^1.0.0: resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag== -clone@^2.1.1, clone@^2.1.2: +clone@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== @@ -2022,10 +2183,14 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -code-block-writer@^11.0.0: - version "11.0.3" - resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.3.tgz#9eec2993edfb79bfae845fbc093758c0a0b73b76" - integrity sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw== +cls-hooked@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908" + integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw== + dependencies: + async-hook-jl "^1.7.6" + emitter-listener "^1.0.1" + semver "^5.4.1" code-point-at@^1.0.0: version "1.1.0" @@ -2083,23 +2248,13 @@ colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -command-line-args@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" - integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== - dependencies: - array-back "^3.1.0" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2167,6 +2322,14 @@ content-disposition@^0.5.2: dependencies: safe-buffer "5.2.1" +continuation-local-storage@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb" + integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA== + dependencies: + async-listener "^0.6.0" + emitter-listener "^1.1.1" + convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -2345,7 +2508,7 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -2556,6 +2719,18 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== +diagnostic-channel-publishers@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-1.0.5.tgz#df8c317086c50f5727fdfb5d2fce214d2e4130ae" + integrity sha512-dJwUS0915pkjjimPJVDnS/QQHsH0aOYhnZsLJdnZIMOrB+csj8RnZhWTuwnm8R5v3Z7OZs+ksv5luC14DGB7eg== + +diagnostic-channel@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-1.1.0.tgz#6985e9dfedfbc072d91dc4388477e4087147756e" + integrity sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ== + dependencies: + semver "^5.3.0" + diff-match-patch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" @@ -2675,7 +2850,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== -duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: +duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -2724,6 +2899,13 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emitter-listener@^1.0.1, emitter-listener@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" + integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== + dependencies: + shimmer "^1.2.0" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2813,11 +2995,59 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" +es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -2927,38 +3157,41 @@ eslint-config-prettier@^8.3.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== -eslint-import-resolver-node@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== +eslint-import-resolver-node@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7" + integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== dependencies: debug "^3.2.7" - resolve "^1.20.0" + is-core-module "^2.11.0" + resolve "^1.22.1" -eslint-module-utils@^2.7.3: +eslint-module-utils@^2.7.4: version "2.7.4" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.22.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" - integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== +eslint-plugin-import@^2.25.4: + version "2.27.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" + integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.3" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.7.4" has "^1.0.3" - is-core-module "^2.8.1" + is-core-module "^2.11.0" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.5" - resolve "^1.22.0" + object.values "^1.1.6" + resolve "^1.22.1" + semver "^6.3.0" tsconfig-paths "^3.14.1" eslint-plugin-jsx-a11y@^6.3.1: @@ -3119,19 +3352,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-stream@^3.3.4: - version "3.3.5" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.5.tgz#e5dd8989543630d94c6cf4d657120341fa31636b" - integrity sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g== - dependencies: - duplexer "^0.1.1" - from "^0.1.7" - map-stream "0.0.7" - pause-stream "^0.0.11" - split "^1.0.1" - stream-combiner "^0.2.2" - through "^2.3.8" - events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3241,7 +3461,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== -fancy-log@^1.3.2, fancy-log@^1.3.3: +fancy-log@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== @@ -3256,7 +3476,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -3388,13 +3608,6 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3527,6 +3740,24 @@ form-data@^2.5.0: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3551,11 +3782,6 @@ from2@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.0" -from@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== - fromentries@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" @@ -3839,6 +4065,13 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globby@^11.0.1, globby@^11.0.3: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -3858,6 +4091,13 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^8.3.1: version "8.3.2" resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" @@ -3986,6 +4226,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" @@ -4242,6 +4487,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -4300,6 +4554,15 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4344,7 +4607,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.11.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -4575,6 +4838,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.10: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array@^1.1.3, is-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67" @@ -4625,11 +4899,6 @@ is-windows@^1.0.1, is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== -is@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" - integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5021,11 +5290,6 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -5149,11 +5413,6 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== -map-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" - integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -5301,7 +5560,7 @@ minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.0: +minimatch@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== @@ -5522,6 +5781,13 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-fetch@^2.6.7: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + node-has-native-dependencies@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz#3152ec9753b6641e4d322d185dd4930649ada3da" @@ -5823,6 +6089,15 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -6118,13 +6393,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pause-stream@^0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== - dependencies: - through "~2.3" - pbkdf2@^3.0.3: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -6293,12 +6561,7 @@ propagate@^1.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709" integrity sha512-T/rqCJJaIPYObiLSmaDsIf4PGA7y+pkgYFHmwoXQyOHiDDSO1YCxcztNiRBmV4EZha4QIbID3vQIHkqKu5k0Xg== -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -psl@^1.1.28: +psl@^1.1.28, psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -6386,6 +6649,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -6640,6 +6908,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -6677,7 +6950,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.4.0, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.22.1, resolve@^1.4.0, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -6826,7 +7099,7 @@ semver-greatest-satisfied-range@^1.1.0: dependencies: sver-compat "^1.5.0" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -6909,6 +7182,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shimmer@^1.1.0, shimmer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + shortid@^2.2.8: version "2.2.16" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" @@ -7119,13 +7397,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -7146,6 +7417,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stack-chain@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" + integrity sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug== + stack-trace@0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -7175,14 +7451,6 @@ stream-browserify@^3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" -stream-combiner@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" - integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== - dependencies: - duplexer "~0.1.1" - through "~2.3.4" - stream-exhaust@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" @@ -7260,6 +7528,15 @@ string.prototype.trimend@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string.prototype.trimstart@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" @@ -7269,6 +7546,15 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -7498,7 +7784,7 @@ through2@^3.0.0: inherits "^2.0.4" readable-stream "2 || 3" -through@2, through@^2.3.8, through@~2.3, through@~2.3.4: +through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -7601,6 +7887,16 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +tough-cookie@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -7609,6 +7905,11 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -7638,14 +7939,6 @@ ts-mockito@^2.5.0: dependencies: lodash "^4.17.5" -ts-morph@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-15.1.0.tgz#53deea5296d967ff6eba8f15f99d378aa7074a4e" - integrity sha512-RBsGE2sDzUXFTnv8Ba22QfeuKbgvAGJFuTN7HfmIRUkgT/NaVLfDM/8OFm2NlFkGlWEXdpW5OaFIp1jvqdDuOg== - dependencies: - "@ts-morph/common" "~0.16.0" - code-block-writer "^11.0.0" - ts-node@^10.7.0: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -7689,6 +7982,11 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.17.1: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -7713,7 +8011,7 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tunnel@0.0.6: +tunnel@0.0.6, tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== @@ -7755,6 +8053,15 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typed-rest-client@^1.8.4: version "1.8.9" resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.9.tgz#e560226bcadfe71b0fb5c416b587f8da3b8f92d8" @@ -7790,16 +8097,6 @@ typescript@4.5.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== -typescript@^4.5.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== - -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== - uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -7882,6 +8179,11 @@ unique-stream@^2.0.2: json-stable-stringify-without-jsonify "^1.0.1" through2-filter "^3.0.0" +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -7953,6 +8255,14 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" @@ -8006,7 +8316,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -8086,7 +8396,7 @@ vinyl-sourcemap@^1.1.0: remove-bom-buffer "^3.0.0" vinyl "^2.0.0" -vinyl@^2.0.0, vinyl@^2.1.0, vinyl@^2.2.1: +vinyl@^2.0.0, vinyl@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== @@ -8223,29 +8533,6 @@ vscode-languageserver@8.0.2-next.5: dependencies: vscode-languageserver-protocol "3.17.2-next.6" -vscode-nls-dev@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-4.0.3.tgz#e55e8a7bd35c719454af17c0c5c3516ff2971d15" - integrity sha512-Y+wUSLwmBwLD8uN+6hBbR4MICuURQ1DoW5k3Z34kPyDlr9ZRQxCPIWaK64IFzPpjloMbb0ORIHkpI8qjMd4L8w== - dependencies: - ansi-colors "^4.1.1" - clone "^2.1.2" - event-stream "^3.3.4" - fancy-log "^1.3.3" - glob "^7.2.0" - iconv-lite "^0.6.3" - is "^3.3.0" - source-map "^0.6.1" - typescript "^4.5.4" - vinyl "^2.2.1" - xml2js "^0.4.23" - yargs "^17.3.0" - -vscode-nls@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" - integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== - vscode-tas-client@^0.1.63: version "0.1.63" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz#df89e67e9bf7ecb46471a0fb8a4a522d2aafad65" @@ -8266,6 +8553,11 @@ watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webpack-bundle-analyzer@^4.5.0: version "4.7.0" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#33c1c485a7fcae8627c547b5c3328b46de733c66" @@ -8357,6 +8649,14 @@ webpack@^5.70.0: watchpack "^2.4.0" webpack-sources "^3.2.3" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -8390,6 +8690,18 @@ which-typed-array@^1.1.2: has-tostringtag "^1.0.0" is-typed-array "^1.1.9" +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -8543,11 +8855,6 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - yargs-parser@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" @@ -8596,19 +8903,6 @@ yargs@^15.0.2, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.3.0: - version "17.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c" - integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - yargs@^7.1.0: version "7.1.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db"