diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a16f010a30a2..ff281705335f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,7 +4,7 @@ on:
push:
branches:
# Run the CI workflow only on master for microsoft/vscode-python for now.
- - master
+ - main
env:
PYTHON_VERSION: 3.8
@@ -24,7 +24,7 @@ jobs:
python-deps:
name: Install Python Requirements
runs-on: ubuntu-latest
- if: github.repository == 'microsoft/vscode-python'
+ if: github.repository == 'sunix/vscode-python'
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -73,11 +73,14 @@ jobs:
js-ts-deps:
name: Install npm dependencies
runs-on: ubuntu-latest
- if: github.repository == 'microsoft/vscode-python'
+ if: github.repository == 'sunix/vscode-python'
steps:
- name: Checkout
uses: actions/checkout@v2
-
+ - name: Setup node
+ uses: actions/setup-node@v2
+ with:
+ node-version: '12'
- name: Install dependencies (npm ci)
run: npm ci --prefer-offline
@@ -87,71 +90,20 @@ jobs:
path: ~/.npm
key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}}
- ### Hygiene + VSIX upload
- compile-hygiene:
- name: Compile, lint, check for errors
- runs-on: ubuntu-latest
- if: github.repository == 'microsoft/vscode-python'
- needs: [js-ts-deps, python-deps]
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Show all env vars
- run: |
- printenv
- shell: bash
-
- - name: Retrieve cached npm files
- uses: actions/cache@v1
- with:
- path: ~/.npm
- key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}}
-
- - name: Install dependencies (npm ci)
- run: npm ci --prefer-offline
-
- - name: Run gulp prePublishNonBundle
- run: npx gulp prePublishNonBundle
-
- - name: Cache the out/ directory
- uses: actions/cache@v1
- with:
- path: ./out
- key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}}
-
- - name: Check dependencies
- run: npm run checkDependencies
-
- - name: Run linting on TypeScript code
- run: npx tslint --project tsconfig.json
-
- - name: Run prettier on TypeScript code
- run: npx prettier 'src/**/*.ts*' --check
-
- - name: Run prettier on JavaScript code
- run: npx prettier 'build/**/*.js' --check
-
- - name: Use Python ${{env.PYTHON_VERSION}}
- uses: actions/setup-python@v2
- with:
- python-version: ${{env.PYTHON_VERSION}}
-
- - name: Run Black on Python code
- run: |
- python -m pip install -U black
- python -m black . --check
- working-directory: pythonFiles
-
build-vsix:
name: Build VSIX
runs-on: ubuntu-latest
- if: github.repository == 'microsoft/vscode-python'
+ if: github.repository == 'sunix/vscode-python'
needs: [python-deps, js-ts-deps]
steps:
- name: Checkout
uses: actions/checkout@v2
+ - name: Setup node
+ uses: actions/setup-node@v2
+ with:
+ node-version: '12'
+
- name: Retrieve cached npm files
uses: actions/cache@v1
with:
@@ -187,378 +139,3 @@ jobs:
name: ${{env.ARTIFACT_NAME_VSIX}}
path: ms-python-insiders.vsix
- ### Non-smoke tests
- tests:
- name: Tests
- # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded.
- runs-on: ${{ matrix.os }}
- if: github.repository == 'microsoft/vscode-python'
- needs: [python-deps, js-ts-deps]
- strategy:
- fail-fast: false
- matrix:
- # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used,
- # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case.
- os: [ubuntu-latest, windows-latest]
- # Run the tests on the oldest and most recent versions of Python.
- python: [2.7, 3.8]
- test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional]
- env:
- # Something in Node 12.16.0 breaks the TS debug adapter, and ubuntu-latest bundles Node 12.16.1.
- # We can remove this when we switch over to the python-based DA in https://github.com/microsoft/vscode-python/issues/7136.
- # See https://github.com/microsoft/ptvsd/issues/2068
- # At this point pinning is only needed for consistency. We no longer have TS debug adapter.
- NODE_VERSION: 12.15.0
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Show all env vars
- run: |
- printenv
- shell: bash
-
- - name: Retrieve cached npm files
- uses: actions/cache@v1
- with:
- path: ~/.npm
- key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}}
-
- - name: Install dependencies (npm ci)
- run: npm ci
-
- - name: Retrieve cached compile output directory
- # Use an id for this step so that its cache-hit output can be accessed and checked in the next step.
- id: out-cache
- uses: actions/cache@v1
- with:
- path: ./out
- key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}}
-
- - name: Compile if not cached
- run: npx gulp prePublishNonBundle
- if: steps.out-cache.outputs.cache-hit == false
-
- - name: Use Python ${{matrix.python}}
- uses: actions/setup-python@v2
- with:
- python-version: ${{matrix.python}}
-
- - name: Use Node ${{env.NODE_VERSION}}
- uses: actions/setup-node@v1
- with:
- node-version: ${{env.NODE_VERSION}}
-
- - name: curl PyPI to get debugpy versions
- run: curl --output debugpy.json https://pypi.org/pypi/debugpy/json
-
- # We're intentionally not retrieving cached Python requirements installation, as it appears that pulling the cache pulls in some extra libraries as well,
- # which causes problems with the tests. Also, running the installation seems much faster than retrieving it from cache.
- - name: Install Python requirements
- run: |
- python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt
- # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase.
- python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy
-
- - name: Install test requirements
- run: python -m pip install --upgrade -r build/test-requirements.txt
-
- - name: pip install ipython requirements
- run: |
- python -m pip install numpy
- python -m pip install --upgrade -r ./build/ipython-test-requirements.txt
- if: matrix.test-suite == 'python-unit'
-
- - name: Install debugpy wheels (python 3.8)
- run: |
- python -m pip install wheel
- python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt
- python ./pythonFiles/install_debugpy.py
- shell: bash
- if: matrix.test-suite == 'debugger' && matrix.python == 3.8
-
- - name: Install debugpy (python 2.7)
- run: |
- python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy
- shell: bash
- if: matrix.test-suite == 'debugger' && matrix.python == 2.7
-
- - name: Install functional test requirements
- run: |
- python -m pip install numpy
- python -m pip install --upgrade -r ./build/functional-test-requirements.txt
- if: matrix.test-suite == 'functional'
-
- - name: Prepare pipenv for venv tests
- env:
- TEST_FILES_SUFFIX: testvirtualenvs
- PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json'
- shell: pwsh
- if: matrix.test-suite == 'venv'
- run: |
- python -m pip install pipenv
- python -m pipenv run python ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} pipenvPath
-
- - name: Prepare virtualenv for venv tests
- env:
- TEST_FILES_SUFFIX: testvirtualenvs
- PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json'
- shell: pwsh
- if: matrix.test-suite == 'venv'
- run: |
- python -m pip install virtualenv
- python -m virtualenv .virtualenv/
- if ('${{matrix.os}}' -match 'windows-latest') {
- & ".virtualenv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath
- } else {
- & ".virtualenv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath
- }
-
- - name: Prepare venv for venv tests
- env:
- TEST_FILES_SUFFIX: testvirtualenvs
- PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json'
- shell: pwsh
- if: matrix.test-suite == 'venv' && startsWith(matrix.python, 3.)
- run: |
- python -m venv .venv
- if ('${{matrix.os}}' -match 'windows-latest') {
- & ".venv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath
- } else {
- & ".venv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath
- }
-
- - name: Prepare conda for venv tests
- env:
- TEST_FILES_SUFFIX: testvirtualenvs
- PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json'
- shell: pwsh
- if: matrix.test-suite == 'venv'
- run: |
- # 1. For `terminalActivation.testvirtualenvs.test.ts`
- if ('${{matrix.os}}' -match 'windows-latest') {
- $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe
- $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda
- } else{
- $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath python
- $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath conda
- }
- & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaExecPath $condaExecPath
- & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaPath
-
- # 2. For `interpreterLocatorService.testvirtualenvs.ts`
-
- & $condaExecPath create -n "test_env1" -y python
- & $condaExecPath create -p "./test_env2" -y python
- & $condaExecPath create -p "~/test_env3" -y python
-
- - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION
- run: |
- echo "::set-env name=CI_PYTHON_PATH::python"
- echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1"
- shell: bash
- if: matrix.test-suite != 'ts-unit'
-
- # Run TypeScript unit tests only for Python 3.X.
- - name: Run TypeScript unit tests
- run: npm run test:unittests:cover
- if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.)
-
- # Upload unit test coverage reports for later use in the "reports" job.
- - name: Upload unit test coverage reports
- uses: actions/upload-artifact@v1
- with:
- name: ${{runner.os}}-${{env.COVERAGE_REPORTS}}
- path: .nyc_output
- if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.)
-
- # Run the Python and IPython tests in our codebase.
- - name: Run Python and IPython unit tests
- run: |
- python pythonFiles/tests/run_all.py
- python -m IPython pythonFiles/tests/run_all.py
- if: matrix.test-suite == 'python-unit'
-
- # The virtual environment based tests use the `testSingleWorkspace` set of tests
- # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`,
- # which is set in the "Prepare environment for venv tests" step.
- # We also use a third-party GitHub Action to install xvfb on Linux,
- # run tests and then clean up the process once the tests ran.
- # See https://github.com/GabrielBB/xvfb-action
- - name: Run venv tests
- env:
- TEST_FILES_SUFFIX: testvirtualenvs
- CI_PYTHON_VERSION: ${{matrix.python}}
- uses: GabrielBB/xvfb-action@v1.0
- with:
- run: npm run testSingleWorkspace
- if: matrix.test-suite == 'venv'
-
- - name: Run single-workspace tests
- uses: GabrielBB/xvfb-action@v1.0
- with:
- run: npm run testSingleWorkspace
- if: matrix.test-suite == 'single-workspace'
-
- - name: Run multi-workspace tests
- uses: GabrielBB/xvfb-action@v1.0
- with:
- run: npm run testMultiWorkspace
- if: matrix.test-suite == 'multi-workspace'
-
- - name: Run debugger tests
- uses: GabrielBB/xvfb-action@v1.0
- with:
- run: npm run testDebugger
- if: matrix.test-suite == 'debugger'
-
- - name: Run functional tests
- run: npm run test:functional
- if: matrix.test-suite == 'functional'
-
- smoke-tests:
- name: Smoke tests
- # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded.
- runs-on: ${{ matrix.os }}
- needs: [build-vsix]
- if: github.repository == 'microsoft/vscode-python'
- strategy:
- fail-fast: false
- matrix:
- # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used,
- # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case.
- os: [ubuntu-latest, windows-latest]
- python: [3.8]
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Show all env vars
- run: |
- printenv
- shell: bash
-
- - name: Use Python ${{matrix.python}}
- uses: actions/setup-python@v2
- with:
- python-version: ${{matrix.python}}
-
- - name: Retrieve cached npm files
- uses: actions/cache@v1
- with:
- path: ~/.npm
- key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}}
-
- - name: Install dependencies (npm ci)
- run: npm ci --prefer-offline
-
- - name: pip install system test requirements
- run: |
- python -m pip install --upgrade -r build/test-requirements.txt
- python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt
- python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy
- shell: bash
-
- - name: pip install ipython requirements
- run: |
- python -m pip install numpy
- python -m pip install --upgrade -r ./build/ipython-test-requirements.txt
-
- - name: pip install jupyter
- run: |
- python -m pip install --upgrade jupyter
-
- - name: Download VSIX
- uses: actions/download-artifact@v1
- with:
- name: ${{env.ARTIFACT_NAME_VSIX}}
-
- # Extract the artifact from its download folder (./${{env.ARTIFACT_NAME_VSIX}}) to the repo root,
- # then delete the download folder and compile the source code.
- - name: Prepare for smoke tests
- run: |
- mv ${{env.ARTIFACT_NAME_VSIX}}/* .
- rm -r ${{env.ARTIFACT_NAME_VSIX}}
- npx tsc -p ./
- shell: bash
-
- - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION
- run: |
- echo "::set-env name=CI_PYTHON_PATH::python"
- echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1"
- shell: bash
-
- - name: Run smoke tests
- env:
- DISPLAY: 10
- uses: GabrielBB/xvfb-action@v1.0
- with:
- run: node --no-force-async-hooks-checks ./out/test/smokeTest.js
-
- coverage:
- name: Coverage reports upload
- runs-on: ubuntu-latest
- if: github.repository == 'microsoft/vscode-python'
- needs: [tests, smoke-tests]
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Retrieve cached npm files
- uses: actions/cache@v1
- with:
- path: ~/.npm
- key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}}
-
- - name: Install dependencies (npm ci)
- run: npm ci --prefer-offline
-
- # It isn't possible to specify a regex for artifact names, so we have to download each artifact manually.
- # The name pattern is ${{runner.os}}-${{env.COVERAGE_REPORTS}}, and possible values for runner.os are `Linux`, `Windows`, or `macOS`.
- # See https://help.github.com/en/actions/reference/contexts-and-expression-syntax-for-github-actions#runner-context
- - name: Download Ubuntu test coverage artifacts
- uses: actions/download-artifact@v1
- with:
- name: Linux-${{env.COVERAGE_REPORTS}}
-
- - name: Extract Ubuntu coverage artifacts to ./nyc_output
- run: |
- mkdir .nyc_output
- mv Linux-${{env.COVERAGE_REPORTS}}/* .nyc_output
- rm -r Linux-${{env.COVERAGE_REPORTS}}
-
- - name: Generate coverage reports
- run: npm run test:cover:report
- continue-on-error: true
-
- - name: Upload coverage to codecov
- uses: codecov/codecov-action@v1
- with:
- CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}}
- file: ./coverage/cobertura-coverage.xml
-
- lkg-tag:
- # LKG = last known good
- name: Tag successful build as CI LKG
- runs-on: ubuntu-latest
- needs: [tests, smoke-tests]
- if: github.repository == 'microsoft/vscode-python'
- steps:
- - name: Delete existing tag
- run: |
- curl -s -X DELETE https://api.github.com/repos/microsoft/vscode-python/git/refs/tags/${{env.LKG_TAG}} \
- -H "Authorization: token ${{secrets.GITHUB_TOKEN}}"
-
- # We only need to create a tag reference for lightweight tags.
- # See https://developer.github.com/v3/git/tags/#create-a-tag-object
- # And https://developer.github.com/v3/git/refs/#create-a-reference
- - name: Create a tag reference
- run: |
- curl -s -X POST "https://api.github.com/repos/microsoft/vscode-python/git/refs" \
- -H "Authorization: token ${{secrets.GITHUB_TOKEN}}" \
- -d @- << EOF
- {
- "ref": "refs/tags/${{env.LKG_TAG}}",
- "sha": "${{github.sha}}"
- }
- EOF
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99392e6969c7..471dec73356f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
# Changelog
-## 2020.7.0-rc (8 July 2020)
+## 2020.7.1 (22 July 2020)
+
+1. Fix language server setting when provided an invalid value, send config event more consistently.
+ ([#13064](https://github.com/Microsoft/vscode-python/pull/13064))
+1. Add banner for pylance, and remove old LS experiment.
+ ([#12817](https://github.com/microsoft/vscode-python/pull/12817))
+
+## 2020.7.0 (16 July 2020)
### Enhancements
@@ -9,7 +16,8 @@
([#9679](https://github.com/Microsoft/vscode-python/issues/9679))
1. Added "argsExpansion" to debugpy launch.json schema.
([#11678](https://github.com/Microsoft/vscode-python/issues/11678))
-1. The extension will now automatically load if a `pyproject.toml` file is present in the workspace root directory. (@BrandonLWhite)
+1. The extension will now automatically load if a `pyproject.toml` file is present in the workspace root directory.
+ (thanks [Brandon White](https://github.com/BrandonLWhite))
([#12056](https://github.com/Microsoft/vscode-python/issues/12056))
1. Add ability to check and update whether a notebook is trusted.
([#12146](https://github.com/Microsoft/vscode-python/issues/12146))
@@ -29,10 +37,12 @@
([#12611](https://github.com/Microsoft/vscode-python/issues/12611))
1. Include the JUPYTER_PATH environment variable when searching the disk for kernels.
([#12694](https://github.com/Microsoft/vscode-python/issues/12694))
-1. Added exporting to python, HTML and PDF from the interative window.
+1. Added exporting to python, HTML and PDF from the interactive window.
([#12732](https://github.com/Microsoft/vscode-python/issues/12732))
1. Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment.
([#12764](https://github.com/Microsoft/vscode-python/issues/12764))
+1. Opening notebooks in the preview Notebook editor for [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/).
+ ([#10496](https://github.com/Microsoft/vscode-python/issues/10496))
### Fixes
@@ -40,6 +50,8 @@
([#10579](https://github.com/Microsoft/vscode-python/issues/10579))
1. Provided a method for external partners to participate in jupyter server URI picking/authentication.
([#10993](https://github.com/Microsoft/vscode-python/issues/10993))
+1. Check for hideFromUser before activating current terminal.
+ ([#11122](https://github.com/Microsoft/vscode-python/issues/11122))
1. In Markdown cells, turn HTML links to markdown links so that nteract renders them.
([#11254](https://github.com/Microsoft/vscode-python/issues/11254))
1. Prevent incorrect ipywidget display (double plots) due to synchronization issues.
@@ -85,6 +97,10 @@
([#12588](https://github.com/Microsoft/vscode-python/issues/12588))
1. Open variable explorer when opening variable explorer during debugging.
([#12773](https://github.com/Microsoft/vscode-python/issues/12773))
+1. Use the given interpreter for launching the non-daemon python
+ ([#12821](https://github.com/Microsoft/vscode-python/issues/12821))
+1. Correct the color of the 'Collapse All' button in the Interactive Window
+ ([#12838](https://github.com/microsoft/vscode-python/issues/12838))
### Code Health
@@ -115,6 +131,10 @@
([#12656](https://github.com/Microsoft/vscode-python/issues/12656))
1. Add more telemetry for "Select Interpreter" command.
([#12722](https://github.com/Microsoft/vscode-python/issues/12722))
+1. Add tests for trusted notebooks.
+ ([#12554](https://github.com/Microsoft/vscode-python/issues/12554))
+1. Update categories in `package.json`.
+ ([#12844](https://github.com/Microsoft/vscode-python/issues/12844))
### Thanks
diff --git a/StartPageReleaseNotes.md b/StartPageReleaseNotes.md
deleted file mode 100644
index 5fb96d33988a..000000000000
--- a/StartPageReleaseNotes.md
+++ /dev/null
@@ -1,3 +0,0 @@
-Added a start page for the extension. Launched in experimental mode such that it opens to new users or when there is a new release. It can be disabled with the setting 'Python: Show Start Page' and it can be opened at any time with the command 'Python: Open Start Page'.
-Removed `python.jediEnabled` setting in favor of `python.languageServer`. Instead of `"python.jediEnabled": true` please use `"python.languageServer": "Jedi"`.
-Made the variable explorer in the Notebook editor resizable.
diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml
index 1d60dd550c82..f46d58cbef18 100644
--- a/build/ci/vscode-python-pr-validation.yaml
+++ b/build/ci/vscode-python-pr-validation.yaml
@@ -56,6 +56,7 @@ stages:
'DataScience':
TestsToRun: 'testDataScience'
NeedsPythonTestReqs: true
+ NeedsPythonFunctionalReqs: true
'Smoke':
TestsToRun: 'testSmoke'
NeedsPythonTestReqs: true
diff --git a/experiments.json b/experiments.json
index 992b023270b0..02d843e969bc 100644
--- a/experiments.json
+++ b/experiments.json
@@ -65,18 +65,6 @@
"min": 0,
"max": 0
},
- {
- "name": "LS - enabled",
- "salt": "LS",
- "min": 0,
- "max": 4
- },
- {
- "name": "LS - control",
- "salt": "LS",
- "min": 20,
- "max": 24
- },
{
"name": "UseTerminalToGetActivatedEnvVars - experiment",
"salt": "UseTerminalToGetActivatedEnvVars",
diff --git a/gulpfile.js b/gulpfile.js
index 85bed2d5b04b..96a2a6459be8 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -177,6 +177,7 @@ gulp.task('webpack', async () => {
await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-notebooks.config.js', 'production');
await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-viewers.config.js', 'production');
await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.config.js', 'extension');
+ await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-renderers.config.js', 'production');
});
gulp.task('updateBuildNumber', async () => {
diff --git a/news/3 Code Health/12554.md b/news/3 Code Health/12554.md
deleted file mode 100644
index 926f9f320057..000000000000
--- a/news/3 Code Health/12554.md
+++ /dev/null
@@ -1 +0,0 @@
-Add tests for trusted notebooks.
diff --git a/package-lock.json b/package-lock.json
index 00dca13130b6..fcd4b734e2e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "python",
- "version": "2020.7.0-rc",
+ "version": "2020.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index b18fd8ed816d..e60bfb589563 100644
--- a/package.json
+++ b/package.json
@@ -2,13 +2,13 @@
"name": "python",
"displayName": "Python",
"description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, snippets, and more.",
- "version": "2020.7.0-rc",
+ "version": "2020.7.1",
"featureFlags": {
"usingNewInterpreterStorage": true
},
"languageServerVersion": "0.5.30",
"publisher": "ms-python",
- "enableProposedApi": false,
+ "enableProposedApi": true,
"author": {
"name": "Microsoft Corporation"
},
@@ -28,7 +28,7 @@
"theme": "dark"
},
"engines": {
- "vscode": "^1.46.0"
+ "vscode": "^1.47.0"
},
"keywords": [
"python",
@@ -43,7 +43,10 @@
"Snippets",
"Formatters",
"Other",
- "Extension Packs"
+ "Extension Packs",
+ "Data Science",
+ "Machine Learning",
+ "Notebooks"
],
"activationEvents": [
"onLanguage:python",
@@ -60,6 +63,7 @@
"onCommand:python.viewTestOutput",
"onCommand:python.viewOutput",
"onCommand:python.datascience.viewJupyterOutput",
+ "onCOmmand:python.datascience.export",
"onCommand:python.datascience.exportAsPythonScript",
"onCommand:python.datascience.exportToHTML",
"onCommand:python.datascience.exportToPDF",
@@ -358,6 +362,16 @@
"title": "%python.command.python.datascience.viewJupyterOutput.title%",
"category": "Python"
},
+ {
+ "command": "python.datascience.export",
+ "title": "%DataScience.notebookExportAs%",
+ "category": "Python",
+ "icon": {
+ "light": "resources/light/export_to_python.svg",
+ "dark": "resources/dark/export_to_python.svg"
+ },
+ "enablement": "notebookViewType == jupyter-notebook && python.datascience.isnotebooktrusted"
+ },
{
"command": "python.datascience.exportAsPythonScript",
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
@@ -681,6 +695,16 @@
},
"enablement": "python.datascience.notebookeditor.canrestartNotebookkernel"
},
+ {
+ "command": "python.datascience.notebookeditor.trust",
+ "title": "%DataScience.trustNotebookCommandTitle%",
+ "category": "Python",
+ "icon": {
+ "light": "resources/light/un-trusted.svg",
+ "dark": "resources/dark/un-trusted.svg"
+ },
+ "enablement": "notebookViewType == jupyter-notebook && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled"
+ },
{
"command": "python.datascience.notebookeditor.runallcells",
"title": "%python.command.python.datascience.notebookeditor.runallcells.title%",
@@ -741,6 +765,11 @@
"command": "python.datascience.gatherquality",
"title": "DataScience.gatherQuality",
"category": "Python"
+ },
+ {
+ "command": "python.datascience.latestExtension",
+ "title": "DataScience.latestExtension",
+ "category": "Python"
}
],
"menus": {
@@ -845,7 +874,19 @@
"command": "python.datascience.notebookeditor.restartkernel",
"title": "%python.command.python.datascience.restartkernel.title%",
"group": "navigation",
- "when": "notebookEditorFocused"
+ "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook'"
+ },
+ {
+ "command": "python.datascience.notebookeditor.trust",
+ "title": "%DataScience.trustNotebookCommandTitle%",
+ "group": "navigation@1",
+ "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook' && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled"
+ },
+ {
+ "command": "python.datascience.export",
+ "title": "%DataScience.notebookExportAs%",
+ "group": "navigation",
+ "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook'"
}
],
"explorer/context": [
@@ -890,19 +931,19 @@
"command": "python.datascience.exportAsPythonScript",
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
"category": "Python",
- "when": "python.datascience.isnativeactive && python.datascience.featureenabled"
+ "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted"
},
{
"command": "python.datascience.exportToHTML",
"title": "%python.command.python.datascience.exportToHTML.title%",
"category": "Python",
- "when": "python.datascience.isnativeactive && python.datascience.featureenabled"
+ "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted"
},
{
"command": "python.datascience.exportToPDF",
"title": "%python.command.python.datascience.exportToPDF.title%",
"category": "Python",
- "when": "python.datascience.isnativeactive && python.datascience.featureenabled"
+ "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted"
},
{
"command": "python.switchOffInsidersChannel",
@@ -1119,6 +1160,12 @@
"category": "Python",
"when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted"
},
+ {
+ "command": "python.datascience.notebookeditor.trust",
+ "title": "%DataScience.trustNotebookCommandTitle%",
+ "category": "Python",
+ "when": "python.datascience.featureenabled && notebookEditorFocused && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled"
+ },
{
"command": "python.datascience.notebookeditor.runallcells",
"title": "%python.command.python.datascience.notebookeditor.runallcells.title%",
@@ -1233,6 +1280,18 @@
"title": "%DataScience.gatherQuality%",
"category": "Python",
"when": "false"
+ },
+ {
+ "command": "python.datascience.latestExtension",
+ "title": "%DataScience.latestExtension%",
+ "category": "Python",
+ "when": "false"
+ },
+ {
+ "command": "python.datascience.export",
+ "title": "%DataScience.notebookExportAs%",
+ "category": "Python",
+ "when": "false"
}
],
"view/title": [
@@ -1774,7 +1833,6 @@
"default": [],
"items": {
"enum": [
- "LS - enabled",
"AlwaysDisplayTestExplorer - experiment",
"ShowExtensionSurveyPrompt - enabled",
"Reload - experiment",
@@ -1788,6 +1846,7 @@
"DeprecatePythonPath - experiment",
"RunByLine - experiment",
"EnableTrustedNotebooks",
+ "tryPylance",
"All"
]
},
@@ -1799,7 +1858,6 @@
"default": [],
"items": {
"enum": [
- "LS - enabled",
"AlwaysDisplayTestExplorer - experiment",
"ShowExtensionSurveyPrompt - enabled",
"Reload - experiment",
@@ -1813,6 +1871,7 @@
"DeprecatePythonPath - experiment",
"RunByLine - experiment",
"EnableTrustedNotebooks",
+ "tryPylance",
"All"
]
},
@@ -3085,7 +3144,43 @@
"when": "testsDiscovered"
}
]
- }
+ },
+ "notebookOutputRenderer": [
+ {
+ "viewType": "jupyter-notebook-renderer",
+ "displayName": "Jupyter Notebook Renderer",
+ "mimeTypes": [
+ "application/geo+json",
+ "application/vdom.v1+json",
+ "application/vnd.dataresource+json",
+ "application/vnd.plotly.v1+json",
+ "application/vnd.vega.v2+json",
+ "application/vnd.vega.v3+json",
+ "application/vnd.vega.v4+json",
+ "application/vnd.vega.v5+json",
+ "application/vnd.vegalite.v1+json",
+ "application/vnd.vegalite.v2+json",
+ "application/vnd.vegalite.v3+json",
+ "application/vnd.vegalite.v4+json",
+ "application/x-nteract-model-debug+json",
+ "image/gif",
+ "text/latex",
+ "text/vnd.plotly.v1+html"
+ ]
+ }
+ ],
+ "notebookProvider": [
+ {
+ "viewType": "jupyter-notebook",
+ "displayName": "Jupyter Notebook (preview)",
+ "selector": [
+ {
+ "filenamePattern": "*.ipynb"
+ }
+ ],
+ "priority": "option"
+ }
+ ]
},
"scripts": {
"package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix",
diff --git a/package.nls.json b/package.nls.json
index c3638945435e..2bef35ccb426 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -23,7 +23,7 @@
"DataScience.checkingIfImportIsSupported": "Checking if import is supported",
"DataScience.installingMissingDependencies": "Installing missing dependencies",
"DataScience.exportNotebookToPython": "Exporting Notebook to Python",
- "DataScience.performingExport": "Performing export",
+ "DataScience.performingExport": "Performing Export",
"DataScience.convertingToPDF": "Converting to PDF",
"DataScience.exportHTMLQuickPickLabel": "HTML",
"DataScience.exportPDFQuickPickLabel": "PDF",
@@ -32,9 +32,10 @@
"DataScience.openExportFileNo": "No",
"DataScience.failedExportMessage": "Export failed.",
"DataScience.exportToPDFDependencyMessage": "If you have not installed xelatex (TeX) you will need to do so before you can export to PDF, for further instructions please look [here](https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex). \r\nTo avoid installing xelatex (TeX) you might want to try exporting to HTML and using your browsers \"Print to PDF\" feature.",
- "DataScience.launchNotebookTrustPrompt": "A notebook could execute harmful code when opened. Some cells & outputs have been hidden. Do you trust this notebook? (To trust all notebooks by default, click [here](command:workbench.action.openSettings?%5B%22python.dataScience.alwaysTrustNotebooks%22%5D).) [Learn more.](https://aka.ms/trusted-notebooks)",
+ "DataScience.launchNotebookTrustPrompt": "A notebook could execute harmful code when opened. Some outputs have been hidden. Do you trust this notebook? [Learn more.](https://aka.ms/trusted-notebooks)",
"DataScience.launchNotebookTrustPrompt.yes": "Trust",
"DataScience.launchNotebookTrustPrompt.no": "Do not trust",
+ "DataScience.launchNotebookTrustPrompt.trustAllNotebooks": "Trust all notebooks",
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
"python.command.python.selectAndRunTestMethod.title": "Run Test Method ...",
"python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...",
@@ -106,6 +107,9 @@
"python.snippet.launch.django.label": "Python: Django",
"python.snippet.launch.flask.label": "Python: Flask",
"python.snippet.launch.pyramid.label": "Python: Pyramid Application",
+ "LanguageService.proposePylanceMessage": "Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.",
+ "LanguageService.tryItNow": "Try it now",
+ "LanguageService.remindMeLater": "Remind me later",
"LanguageService.bannerLabelYes": "Yes, take survey now",
"LanguageService.bannerLabelNo": "No, thanks",
"LanguageService.lsFailedToStart": "We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.",
@@ -369,6 +373,7 @@
"DataScience.fetchingDataViewer": "Fetching data ...",
"DataScience.noRowsInDataViewer": "No rows match current filter",
"DataScience.jupyterServer": "Jupyter Server",
+ "DataScience.trustNotebookCommandTitle": "Trust notebook",
"DataScience.notebookIsTrusted": "Trusted",
"DataScience.notebookIsNotTrusted": "Not Trusted",
"DataScience.noKernel": "No Kernel",
@@ -470,8 +475,8 @@
"DataScience.findJupyterCommandProgressCheckInterpreter": "Checking {0}.",
"DataScience.findJupyterCommandProgressSearchCurrentPath": "Searching current path.",
"DataScience.gatherError": "Gather internal error",
- "DataScience.gatheredScriptDescription": "# This file was generated by the Gather Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gathersurvey\n\n",
- "DataScience.gatheredNotebookDescriptionInMarkdown": "## Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|  |This notebook was generated by the Gather Extension. The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)",
+ "DataScience.gatheredScriptDescription": "# This file was generated by the Gather Extension.\n# It requires version 2020.7.94776 (or newer) of the Python Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gathersurvey\n\n",
+ "DataScience.gatheredNotebookDescriptionInMarkdown": "## Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|  |This notebook was generated by the Gather Extension. It requires version 2020.7.94776 (or newer) of the Python Extension, please update [here](https://command:python.datascience.latestExtension). The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)",
"DataScience.savePngTitle": "Save Image",
"DataScience.jupyterSelectURIQuickPickTitle": "Pick how to connect to Jupyter",
"DataScience.jupyterSelectURIQuickPickPlaceholder": "Choose an option",
@@ -510,6 +515,7 @@
"DataScience.jupyterSelectURIQuickPickTitleRemoteOnly": "Pick an already running jupyter server",
"DataScience.jupyterSelectURIRemoteDetail": "Specify the URI of an existing server",
"DataScience.gatherQuality": "Did gather work as desired?",
+ "DataScience.latestExtension": "Download the latest version of the Python Extension",
"DataScience.loadClassFailedWithNoInternet": "Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.",
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
@@ -526,7 +532,7 @@
"StartPage.pythonFileDescription": "- Create a
new file
with a .py extension",
"StartPage.openInteractiveWindow": "Use the Interactive Window to develop Python Scripts",
"StartPage.interactiveWindowDesc": "- You can create cells on a Python file by typing \"#%%\" - Use \"Shift + Enter
\" to run a cell, the output will be shown in the interactive window",
- "StartPage.releaseNotes": "Take a look at our Release Notes to learn more about the latest features",
+ "StartPage.releaseNotes": "Take a look at our Release Notes to learn more about the latest features.",
"StartPage.tutorialAndDoc": "Explore more features in our Tutorials or check Documentation for tips and troubleshooting.",
"StartPage.dontShowAgain": "Don't show this page again",
"StartPage.helloWorld": "Hello world",
@@ -542,7 +548,7 @@
"DataScience.reloadCustomEditor": "Please reload VS Code to use the custom editor API",
"DataScience.reloadVSCodeNotebookEditor": "Please reload VS Code to use the Notebook Editor",
"DataScience.step": "Run next line (F10)",
- "DataScience.usingPreviewNotebookWithOtherNotebookWarning": "Using the Preview Notebook Editor along with the stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.",
+ "DataScience.usingPreviewNotebookWithOtherNotebookWarning": "Opening the same file in the Preview Notebook Editor and stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.",
"DataScience.previewNotebookOnlySupportedInVSCInsiders": "The Preview Notebook Editor is supported only in the Insiders version of Visual Studio Code.",
"DataScienceNotebookSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Preview Notebook Editor is working for you?",
"DataScience.unknownServerUri": "Server URI cannot be used. Did you uninstall an extension that provided a Jupyter server connection?",
@@ -552,5 +558,10 @@
"DataScienceRendererExtension.downloadingMessage": "Downloading Notebook Renderers Extension...",
"DataScienceRendererExtension.downloadCompletedOutputMessage": "Notebook Renderers extension download complete.",
"DataScience.uriProviderDescriptionFormat": "{0} (From {1} extension)",
- "DataScience.unknownPackage": "unknown"
+ "DataScience.unknownPackage": "unknown",
+ "DataScience.interactiveWindowTitleFormat": "Python Interactive - {0}",
+ "DataScience.interactiveWindowModeBannerTitle": "Do you want to open a new Python Interactive window for this file? [More Information](command:workbench.action.openSettings?%5B%22python.dataScience.interactiveWindowMode%22%5D).",
+ "DataScience.interactiveWindowModeBannerSwitchYes": "Yes",
+ "DataScience.interactiveWindowModeBannerSwitchAlways": "Always",
+ "DataScience.interactiveWindowModeBannerSwitchNo": "No"
}
diff --git a/resources/dark/export_to_python.svg b/resources/dark/export_to_python.svg
new file mode 100644
index 000000000000..a68ca2942cb7
--- /dev/null
+++ b/resources/dark/export_to_python.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/resources/dark/trusted.svg b/resources/dark/trusted.svg
new file mode 100644
index 000000000000..a841903129b0
--- /dev/null
+++ b/resources/dark/trusted.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/resources/dark/un-trusted.svg b/resources/dark/un-trusted.svg
new file mode 100644
index 000000000000..f97f5f561d9e
--- /dev/null
+++ b/resources/dark/un-trusted.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/resources/light/export_to_python.svg b/resources/light/export_to_python.svg
new file mode 100644
index 000000000000..873383aaeb21
--- /dev/null
+++ b/resources/light/export_to_python.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/resources/light/run-file.svg b/resources/light/run-file.svg
index f41a5c8fa2d8..0adc9e411b03 100644
--- a/resources/light/run-file.svg
+++ b/resources/light/run-file.svg
@@ -1,3 +1,4 @@
-
-
+
+
diff --git a/resources/light/trusted.svg b/resources/light/trusted.svg
new file mode 100644
index 000000000000..82e41b9ff47b
--- /dev/null
+++ b/resources/light/trusted.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/resources/light/un-trusted.svg b/resources/light/un-trusted.svg
new file mode 100644
index 000000000000..c9be7cd88dda
--- /dev/null
+++ b/resources/light/un-trusted.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts
index f825a5235dc5..872c38c81b27 100644
--- a/src/client/activation/activationService.ts
+++ b/src/client/activation/activationService.ts
@@ -9,12 +9,10 @@ import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/ch
import { IDiagnosticsService } from '../application/diagnostics/types';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
-import { LSControl, LSEnabled } from '../common/experiments/groups';
import { traceError } from '../common/logger';
import {
IConfigurationService,
IDisposableRegistry,
- IExperimentsManager,
IOutputChannel,
IPersistentStateFactory,
IPythonSettings,
@@ -58,8 +56,7 @@ export class LanguageServerExtensionActivationService
constructor(
@inject(IServiceContainer) private serviceContainer: IServiceContainer,
- @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory,
- @inject(IExperimentsManager) private readonly abExperiments: IExperimentsManager
+ @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory
) {
this.workspaceService = this.serviceContainer.get(IWorkspaceService);
this.interpreterService = this.serviceContainer.get(IInterpreterService);
@@ -175,20 +172,8 @@ export class LanguageServerExtensionActivationService
* @returns `true` if user is using jedi, `false` if user is using language server
*/
public useJedi(): boolean {
- // Check if `languageServer` setting is missing (default configuration).
- if (this.isJediUsingDefaultConfiguration(this.resource)) {
- // If user is assigned to an experiment (i.e. use LS), return false.
- if (this.abExperiments.inExperiment(LSEnabled)) {
- return false;
- }
- // Send telemetry if user is in control group
- this.abExperiments.sendTelemetryIfInExperiment(LSControl);
- return true; // Do use Jedi as it is default.
- }
- // Configuration is non-default, so `languageServer` should be present.
const configurationService = this.serviceContainer.get(IConfigurationService);
const lstType = configurationService.getSettings(this.resource).languageServer;
- this.sendTelemetryForChosenLanguageServer(lstType).ignoreErrors();
return lstType === LanguageServerType.Jedi;
}
@@ -253,6 +238,8 @@ export class LanguageServerExtensionActivationService
break;
}
+ this.sendTelemetryForChosenLanguageServer(serverType).ignoreErrors();
+
await this.logStartup(serverType);
let server = this.serviceContainer.get(ILanguageServerActivator, serverType);
try {
diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts
index 42194f36adbe..cc4bb9be9678 100644
--- a/src/client/activation/languageServer/activator.ts
+++ b/src/client/activation/languageServer/activator.ts
@@ -1,13 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-import { inject, injectable } from 'inversify';
+import { inject, injectable, named } from 'inversify';
import * as path from 'path';
import { IWorkspaceService } from '../../common/application/types';
+import { isTestExecution } from '../../common/constants';
import { traceDecorators } from '../../common/logger';
import { IFileSystem } from '../../common/platform/types';
-import { IConfigurationService, Resource } from '../../common/types';
+import { BANNER_NAME_PROPOSE_LS, IConfigurationService, IPythonExtensionBanner, Resource } from '../../common/types';
+import { PythonInterpreter } from '../../pythonEnvironments/info';
import { LanguageServerActivatorBase } from '../common/activatorBase';
import { ILanguageServerDownloader, ILanguageServerFolderService, ILanguageServerManager } from '../types';
@@ -27,11 +29,21 @@ export class DotNetLanguageServerActivator extends LanguageServerActivatorBase {
@inject(IFileSystem) fs: IFileSystem,
@inject(ILanguageServerDownloader) lsDownloader: ILanguageServerDownloader,
@inject(ILanguageServerFolderService) languageServerFolderService: ILanguageServerFolderService,
- @inject(IConfigurationService) configurationService: IConfigurationService
+ @inject(IConfigurationService) configurationService: IConfigurationService,
+ @inject(IPythonExtensionBanner)
+ @named(BANNER_NAME_PROPOSE_LS)
+ private proposePylancePopup: IPythonExtensionBanner
) {
super(manager, workspace, fs, lsDownloader, languageServerFolderService, configurationService);
}
+ public async start(resource: Resource, interpreter?: PythonInterpreter): Promise {
+ if (!isTestExecution()) {
+ this.proposePylancePopup.showBanner().ignoreErrors();
+ }
+ return super.start(resource, interpreter);
+ }
+
@traceDecorators.error('Failed to ensure language server is available')
public async ensureLanguageServerIsAvailable(resource: Resource): Promise {
const languageServerFolderPath = await this.ensureLanguageServerFileIsAvailable(resource, 'mscorlib.dll');
diff --git a/src/client/activation/languageServer/languageServerProxy.ts b/src/client/activation/languageServer/languageServerProxy.ts
index c0ae13b03557..1d7c0cf8d33a 100644
--- a/src/client/activation/languageServer/languageServerProxy.ts
+++ b/src/client/activation/languageServer/languageServerProxy.ts
@@ -102,9 +102,13 @@ export class DotNetLanguageServerProxy implements ILanguageServerProxy {
}
@captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_READY, undefined, true)
protected async serverReady(): Promise {
- while (this.languageClient && !this.languageClient!.initializeResult) {
+ // languageClient can be disposed in awaits.
+ while (this.languageClient && !this.languageClient.initializeResult) {
await sleep(100);
}
+ if (this.languageClient) {
+ await this.languageClient.onReady();
+ }
this.startupCompleted.resolve();
}
@swallowExceptions('Activating Unit Tests Manager for Language Server')
diff --git a/src/client/activation/node/languageServerFolderService.ts b/src/client/activation/node/languageServerFolderService.ts
index 44922a052970..0ccb89b28fc3 100644
--- a/src/client/activation/node/languageServerFolderService.ts
+++ b/src/client/activation/node/languageServerFolderService.ts
@@ -8,14 +8,13 @@ import { inject, injectable } from 'inversify';
import * as path from 'path';
import { SemVer } from 'semver';
import { IWorkspaceService } from '../../common/application/types';
+import { PYLANCE_EXTENSION_ID } from '../../common/constants';
import { NugetPackage } from '../../common/nuget/types';
import { IConfigurationService, IExtensions, Resource } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { LanguageServerFolderService } from '../common/languageServerFolderService';
import { FolderVersionPair, ILanguageServerFolderService, NodeLanguageServerFolder } from '../types';
-export const PylanceExtensionName = 'ms-python.vscode-pylance';
-
class FallbackNodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(serviceContainer: IServiceContainer) {
super(serviceContainer, NodeLanguageServerFolder);
@@ -101,7 +100,7 @@ export class NodeLanguageServerFolderService implements ILanguageServerFolderSer
return undefined;
}
- const extension = this.extensions.getExtension(PylanceExtensionName);
+ const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID);
if (!extension) {
return undefined;
}
diff --git a/src/client/activation/node/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts
index 0e02adb97eae..4d5bfc9429cd 100644
--- a/src/client/activation/node/languageServerProxy.ts
+++ b/src/client/activation/node/languageServerProxy.ts
@@ -146,9 +146,12 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy {
NodeLanguageServerProxy.versionTelemetryProps
)
protected async serverReady(): Promise {
- while (this.languageClient && !this.languageClient!.initializeResult) {
+ while (this.languageClient && !this.languageClient.initializeResult) {
await sleep(100);
}
+ if (this.languageClient) {
+ await this.languageClient.onReady();
+ }
this.startupCompleted.resolve();
}
diff --git a/src/client/activation/none/activator.ts b/src/client/activation/none/activator.ts
index 8d808ae87ce0..5266e8ab39c1 100644
--- a/src/client/activation/none/activator.ts
+++ b/src/client/activation/none/activator.ts
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-import { injectable } from 'inversify';
+import { inject, injectable, named } from 'inversify';
import {
CancellationToken,
CodeLens,
@@ -20,7 +20,8 @@ import {
TextDocument,
WorkspaceEdit
} from 'vscode';
-import { Resource } from '../../common/types';
+import { isTestExecution } from '../../common/constants';
+import { BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner, Resource } from '../../common/types';
import { PythonInterpreter } from '../../pythonEnvironments/info';
import { ILanguageServerActivator } from '../types';
@@ -33,8 +34,16 @@ import { ILanguageServerActivator } from '../types';
*/
@injectable()
export class NoLanguageServerExtensionActivator implements ILanguageServerActivator {
- // tslint:disable-next-line: no-empty
- public async start(_resource: Resource, _interpreter?: PythonInterpreter): Promise {}
+ constructor(
+ @inject(IPythonExtensionBanner)
+ @named(BANNER_NAME_PROPOSE_LS)
+ private proposePylancePopup: IPythonExtensionBanner
+ ) {}
+ public async start(_resource: Resource, _interpreter?: PythonInterpreter): Promise {
+ if (!isTestExecution()) {
+ this.proposePylancePopup.showBanner().ignoreErrors();
+ }
+ }
// tslint:disable-next-line: no-empty
public dispose(): void {}
// tslint:disable-next-line: no-empty
diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts
index f5bec7c65f35..0f1fa5497f67 100644
--- a/src/client/activation/serviceRegistry.ts
+++ b/src/client/activation/serviceRegistry.ts
@@ -12,7 +12,7 @@ import {
import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner';
import { InteractiveShiftEnterBanner } from '../datascience/shiftEnterBanner';
import { IServiceManager } from '../ioc/types';
-import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner';
+import { ProposePylanceBanner } from '../languageServices/proposeLanguageServerBanner';
import { AATesting } from './aaTesting';
import { ExtensionActivationManager } from './activationManager';
import { LanguageServerExtensionActivationService } from './activationService';
@@ -86,7 +86,7 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp
serviceManager.addSingleton(
IPythonExtensionBanner,
- ProposeLanguageServerBanner,
+ ProposePylanceBanner,
BANNER_NAME_PROPOSE_LS
);
serviceManager.addSingleton(
diff --git a/src/client/common/application/applicationEnvironment.ts b/src/client/common/application/applicationEnvironment.ts
index a16082e0b848..3d01d85f4b35 100644
--- a/src/client/common/application/applicationEnvironment.ts
+++ b/src/client/common/application/applicationEnvironment.ts
@@ -88,4 +88,7 @@ export class ApplicationEnvironment implements IApplicationEnvironment {
const version = parse(this.packageJson.version);
return !version || version.prerelease.length > 0 ? 'insiders' : 'stable';
}
+ public get uriScheme(): string {
+ return vscode.env.uriScheme;
+ }
}
diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts
index d18b5cf737a5..3a5985e972aa 100644
--- a/src/client/common/application/commands.ts
+++ b/src/client/common/application/commands.ts
@@ -173,14 +173,16 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[DSCommands.ScrollToCell]: [string, string];
[DSCommands.ViewJupyterOutput]: [];
[DSCommands.ExportAsPythonScript]: [INotebookModel];
- [DSCommands.ExportToHTML]: [INotebookModel];
- [DSCommands.ExportToPDF]: [INotebookModel];
- [DSCommands.Export]: [INotebookModel];
+ [DSCommands.ExportToHTML]: [INotebookModel, string | undefined];
+ [DSCommands.ExportToPDF]: [INotebookModel, string | undefined];
+ [DSCommands.Export]: [Uri | INotebookModel, string | undefined];
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined, 'raw' | 'jupyter'];
[DSCommands.SelectJupyterCommandLine]: [undefined | Uri];
[DSCommands.SaveNotebookNonCustomEditor]: [Uri];
[DSCommands.SaveAsNotebookNonCustomEditor]: [Uri, Uri];
[DSCommands.OpenNotebookNonCustomEditor]: [Uri];
[DSCommands.GatherQuality]: [string];
+ [DSCommands.LatestExtension]: [string];
[DSCommands.EnableLoadingWidgetsFrom3rdPartySource]: [undefined | never];
+ [DSCommands.TrustNotebook]: [undefined | never | Uri];
}
diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts
index f1d7f38900c6..2459ca1aded7 100644
--- a/src/client/common/application/types.ts
+++ b/src/client/common/application/types.ts
@@ -1024,6 +1024,10 @@ export interface IApplicationEnvironment {
* The version of the editor.
*/
readonly vscodeVersion: string;
+ /**
+ * The custom uri scheme the editor registers to in the operating system.
+ */
+ readonly uriScheme: string;
}
export const IWebPanelMessageListener = Symbol('IWebPanelMessageListener');
diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts
index 57c865fccfa2..d0066f58b378 100644
--- a/src/client/common/configSettings.ts
+++ b/src/client/common/configSettings.ts
@@ -231,11 +231,12 @@ export class PythonSettings implements IPythonSettings {
pythonSettings.get('autoUpdateLanguageServer', true)
)!;
- let ls = pythonSettings.get('languageServer');
- if (!ls) {
+ let ls = pythonSettings.get('languageServer') ?? LanguageServerType.Jedi;
+ ls = systemVariables.resolveAny(ls);
+ if (!Object.values(LanguageServerType).includes(ls)) {
ls = LanguageServerType.Jedi;
}
- this.languageServer = systemVariables.resolveAny(ls)!;
+ this.languageServer = ls;
// tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion
this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!;
diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts
index fb4ccf0e9ef9..6096285fa170 100644
--- a/src/client/common/constants.ts
+++ b/src/client/common/constants.ts
@@ -15,6 +15,7 @@ export const PYTHON_ALLFILES = [{ language: PYTHON_LANGUAGE }];
export const PVSC_EXTENSION_ID = 'ms-python.python';
export const CODE_RUNNER_EXTENSION_ID = 'formulahendry.code-runner';
+export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';
export const AppinsightsKey = 'AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217';
export namespace Commands {
diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts
index 3757a03d54a0..6daf346c23f8 100644
--- a/src/client/common/experiments/groups.ts
+++ b/src/client/common/experiments/groups.ts
@@ -1,6 +1,3 @@
-export const LSControl = 'LS - control';
-export const LSEnabled = 'LS - enabled';
-
// Experiment to check whether to always display the test explorer.
export enum AlwaysDisplayTestExplorerGroups {
control = 'AlwaysDisplayTestExplorer - control',
@@ -99,3 +96,8 @@ export enum RemoveKernelToolbarInInteractiveWindow {
export enum EnableTrustedNotebooks {
experiment = 'EnableTrustedNotebooks'
}
+
+// Experiment to offer switch to Pylance language server
+export enum TryPylance {
+ experiment = 'tryPylance'
+}
diff --git a/src/client/common/startPage/startPage.ts b/src/client/common/startPage/startPage.ts
index 6ebb7fe62165..8a6ad587ed4b 100644
--- a/src/client/common/startPage/startPage.ts
+++ b/src/client/common/startPage/startPage.ts
@@ -120,11 +120,9 @@ export class StartPage extends WebViewHost implements IStartP
case StartPageMessages.Started:
this.webviewDidLoad = true;
break;
- case StartPageMessages.RequestReleaseNotesAndShowAgainSetting:
+ case StartPageMessages.RequestShowAgainSetting:
const settings = this.configuration.getSettings();
- const filteredNotes = await this.handleReleaseNotesRequest();
- await this.postMessage(StartPageMessages.SendReleaseNotes, {
- notes: filteredNotes,
+ await this.postMessage(StartPageMessages.SendSetting, {
showAgainSetting: settings.showStartPage
});
break;
@@ -245,12 +243,6 @@ export class StartPage extends WebViewHost implements IStartP
return shouldShowStartPage;
}
- // This gets the release notes from StartPageReleaseNotes.md
- private async handleReleaseNotesRequest(): Promise {
- const releaseNotes = await this.file.readFile(path.join(EXTENSION_ROOT_DIR, 'StartPageReleaseNotes.md'));
- return releaseNotes.splitLines();
- }
-
private async activateBackground(): Promise {
const enabled = await this.expService.inExperiment(EnableStartPage.experiment);
const settings = this.configuration.getSettings();
diff --git a/src/client/common/startPage/types.ts b/src/client/common/startPage/types.ts
index 0e724d7bff2d..a147085fe426 100644
--- a/src/client/common/startPage/types.ts
+++ b/src/client/common/startPage/types.ts
@@ -8,16 +8,15 @@ export interface IStartPage {
extensionVersionChanged(): Promise;
}
-export interface IReleaseNotesPackage {
- notes: string[];
+export interface ISettingPackage {
showAgainSetting: boolean;
}
export namespace StartPageMessages {
export const Started = SharedMessages.Started;
export const UpdateSettings = SharedMessages.UpdateSettings;
- export const RequestReleaseNotesAndShowAgainSetting = 'RequestReleaseNotesAndShowAgainSetting';
- export const SendReleaseNotes = 'SendReleaseNotes';
+ export const RequestShowAgainSetting = 'RequestShowAgainSetting';
+ export const SendSetting = 'SendSetting';
export const OpenBlankNotebook = 'OpenBlankNotebook';
export const OpenBlankPythonFile = 'OpenBlankPythonFile';
export const OpenInteractiveWindow = 'OpenInteractiveWindow';
@@ -30,8 +29,8 @@ export namespace StartPageMessages {
}
export class IStartPageMapping {
- public [StartPageMessages.RequestReleaseNotesAndShowAgainSetting]: IReleaseNotesPackage;
- public [StartPageMessages.SendReleaseNotes]: IReleaseNotesPackage;
+ public [StartPageMessages.RequestShowAgainSetting]: ISettingPackage;
+ public [StartPageMessages.SendSetting]: ISettingPackage;
public [StartPageMessages.Started]: never | undefined;
public [StartPageMessages.UpdateSettings]: boolean;
public [StartPageMessages.OpenBlankNotebook]: never | undefined;
diff --git a/src/client/common/types.ts b/src/client/common/types.ts
index 99557821e85c..d7fab4ddc24e 100644
--- a/src/client/common/types.ts
+++ b/src/client/common/types.ts
@@ -525,7 +525,7 @@ export interface IPythonExtensionBanner {
readonly enabled: boolean;
showBanner(): Promise;
}
-export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS';
+export const BANNER_NAME_PROPOSE_LS: string = 'ProposePylance';
export const BANNER_NAME_DS_SURVEY: string = 'DSSurveyBanner';
export const BANNER_NAME_INTERACTIVE_SHIFTENTER: string = 'InteractiveShiftEnterBanner';
diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts
index 455069417e30..ba07eddce146 100644
--- a/src/client/common/utils/localize.ts
+++ b/src/client/common/utils/localize.ts
@@ -101,6 +101,13 @@ export namespace AttachProcess {
}
export namespace LanguageService {
+ export const proposePylanceMessage = localize(
+ 'LanguageService.proposePylanceMessage',
+ 'Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.'
+ );
+ export const tryItNow = localize('LanguageService.tryItNow', 'Try it now');
+ export const remindMeLater = localize('LanguageService.remindMeLater', 'Remind me later');
+
export const bannerLabelYes = localize('LanguageService.bannerLabelYes', 'Yes, take survey now');
export const bannerLabelNo = localize('LanguageService.bannerLabelNo', 'No, thanks');
export const lsFailedToStart = localize(
@@ -331,7 +338,7 @@ export namespace DataScience {
'DataScience.installingMissingDependencies',
'Installing missing dependencies'
);
- export const performingExport = localize('DataScience.performingExport', 'Performing export');
+ export const performingExport = localize('DataScience.performingExport', 'Performing Export');
export const convertingToPDF = localize('DataScience.convertingToPDF', 'Converting to PDF');
export const exportNotebookToPython = localize(
'DataScience.exportNotebookToPython',
@@ -863,11 +870,11 @@ export namespace DataScience {
export const gatherError = localize('DataScience.gatherError', 'Gather internal error');
export const gatheredScriptDescription = localize(
'DataScience.gatheredScriptDescription',
- '# This file was generated by the Gather Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gathersurvey\n\n'
+ '# This file was generated by the Gather Extension.\n# It requires version 2020.7.94776 (or newer) of the Python Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gathersurvey\n\n'
);
export const gatheredNotebookDescriptionInMarkdown = localize(
'DataScience.gatheredNotebookDescriptionInMarkdown',
- '# Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|  |This notebook was generated by the Gather Extension. The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)'
+ '# Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|  |This notebook was generated by the Gather Extension. It requires version 2020.7.94776 (or newer) of the Python Extension, please update [here](https://command:python.datascience.latestExtension). The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)'
);
export const savePngTitle = localize('DataScience.savePngTitle', 'Save Image');
export const fallbackToUseActiveInterpeterAsKernel = localize(
@@ -1020,14 +1027,18 @@ export namespace DataScience {
);
export const usingPreviewNotebookWithOtherNotebookWarning = localize(
'DataScience.usingPreviewNotebookWithOtherNotebookWarning',
- 'Using the Preview Notebook Editor along with the stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.'
+ 'Opening the same file in the Preview Notebook Editor and stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.'
);
export const launchNotebookTrustPrompt = localize(
'DataScience.launchNotebookTrustPrompt',
- 'A notebook could execute harmful code when opened. Some cells & outputs have been hidden. Do you trust this notebook? (To trust all notebooks by default, click [here](command:workbench.action.openSettings?%5B%22python.dataScience.alwaysTrustNotebooks%22%5D).) [Learn more.](https://aka.ms/trusted-notebooks)'
+ 'A notebook could execute harmful code when opened. Some outputs have been hidden. Do you trust this notebook? [Learn more.](https://aka.ms/trusted-notebooks)'
);
export const trustNotebook = localize('DataScience.launchNotebookTrustPrompt.yes', 'Trust');
export const doNotTrustNotebook = localize('DataScience.launchNotebookTrustPrompt.no', 'Do not trust');
+ export const trustAllNotebooks = localize(
+ 'DataScience.launchNotebookTrustPrompt.trustAllNotebooks',
+ 'Trust all notebooks'
+ );
export const previewNotebookOnlySupportedInVSCInsiders = localize(
'DataScience.previewNotebookOnlySupportedInVSCInsiders',
'The Preview Notebook Editor is supported only in the Insiders version of Visual Studio Code.'
@@ -1058,7 +1069,7 @@ export namespace StartPage {
export const releaseNotes = localize(
'StartPage.releaseNotes',
- 'Take a look at our Release Notes to learn more about the latest features'
+ 'Take a look at our Release Notes to learn more about the latest features.'
);
export const tutorialAndDoc = localize(
'StartPage.tutorialAndDoc',
diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts
index d474d5175ec4..5bf2ded5f2f9 100644
--- a/src/client/datascience/commands/commandRegistry.ts
+++ b/src/client/datascience/commands/commandRegistry.ts
@@ -78,6 +78,7 @@ export class CommandRegistry implements IDisposable {
this.registerCommand(Commands.CreateNewNotebook, this.createNewNotebook);
this.registerCommand(Commands.ViewJupyterOutput, this.viewJupyterOutput);
this.registerCommand(Commands.GatherQuality, this.reportGatherQuality);
+ this.registerCommand(Commands.LatestExtension, this.openPythonExtensionPage);
this.registerCommand(
Commands.EnableLoadingWidgetsFrom3rdPartySource,
this.enableLoadingWidgetScriptsFromThirdParty
@@ -388,7 +389,11 @@ export class CommandRegistry implements IDisposable {
}
private reportGatherQuality(val: string) {
- sendTelemetryEvent(Telemetry.GatherQualityReport, undefined, { result: val === 'no' ? 'no' : 'yes' });
- env.openExternal(Uri.parse(`https://aka.ms/gathersurvey?succeed=${val}`));
+ sendTelemetryEvent(Telemetry.GatherQualityReport, undefined, { result: val[0] === 'no' ? 'no' : 'yes' });
+ env.openExternal(Uri.parse(`https://aka.ms/gathersurvey?succeed=${val[0]}`));
+ }
+
+ private openPythonExtensionPage() {
+ env.openExternal(Uri.parse(`https://marketplace.visualstudio.com/items?itemName=ms-python.python`));
}
}
diff --git a/src/client/datascience/commands/exportCommands.ts b/src/client/datascience/commands/exportCommands.ts
index c82c4d2b58cb..e936e60d3ec3 100644
--- a/src/client/datascience/commands/exportCommands.ts
+++ b/src/client/datascience/commands/exportCommands.ts
@@ -4,12 +4,14 @@
'use strict';
import { inject, injectable } from 'inversify';
-import { QuickPickItem, QuickPickOptions } from 'vscode';
+import { QuickPickItem, QuickPickOptions, Uri } from 'vscode';
import { getLocString } from '../../../datascience-ui/react-common/locReactSide';
import { ICommandNameArgumentTypeMapping } from '../../common/application/commands';
import { IApplicationShell, ICommandManager } from '../../common/application/types';
+import { IFileSystem } from '../../common/platform/types';
import { IDisposable } from '../../common/types';
import { DataScience } from '../../common/utils/localize';
+import { isUri } from '../../common/utils/misc';
import { sendTelemetryEvent } from '../../telemetry';
import { Commands, Telemetry } from '../constants';
import { ExportManager } from '../export/exportManager';
@@ -27,13 +29,20 @@ export class ExportCommands implements IDisposable {
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(IExportManager) private exportManager: ExportManager,
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
- @inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider
+ @inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider,
+ @inject(IFileSystem) private readonly fs: IFileSystem
) {}
public register() {
this.registerCommand(Commands.ExportAsPythonScript, (model) => this.export(model, ExportFormat.python));
- this.registerCommand(Commands.ExportToHTML, (model) => this.export(model, ExportFormat.html));
- this.registerCommand(Commands.ExportToPDF, (model) => this.export(model, ExportFormat.pdf));
- this.registerCommand(Commands.Export, (model) => this.export(model));
+ this.registerCommand(Commands.ExportToHTML, (model, defaultFileName?) =>
+ this.export(model, ExportFormat.html, defaultFileName)
+ );
+ this.registerCommand(Commands.ExportToPDF, (model, defaultFileName?) =>
+ this.export(model, ExportFormat.pdf, defaultFileName)
+ );
+ this.registerCommand(Commands.Export, (model, defaultFileName?) =>
+ this.export(model, undefined, defaultFileName)
+ );
}
public dispose() {
@@ -49,9 +58,22 @@ export class ExportCommands implements IDisposable {
this.disposables.push(disposable);
}
- private async export(model: INotebookModel, exportMethod?: ExportFormat) {
+ private async export(modelOrUri: Uri | INotebookModel, exportMethod?: ExportFormat, defaultFileName?: string) {
+ defaultFileName = typeof defaultFileName === 'string' ? defaultFileName : undefined;
+ let model: INotebookModel | undefined;
+ if (modelOrUri && isUri(modelOrUri)) {
+ const uri = modelOrUri;
+ const editor = this.notebookProvider.editors.find((item) =>
+ this.fs.arePathsSame(item.file.fsPath, uri.fsPath)
+ );
+ if (editor && editor.model) {
+ model = editor.model;
+ }
+ } else {
+ model = modelOrUri;
+ }
if (!model) {
- // if no model was passed then this was called from the command pallete,
+ // if no model was passed then this was called from the command palette,
// so we need to get the active editor
const activeEditor = this.notebookProvider.activeEditor;
if (!activeEditor || !activeEditor.model) {
@@ -65,11 +87,11 @@ export class ExportCommands implements IDisposable {
}
if (exportMethod) {
- await this.exportManager.export(exportMethod, model);
+ await this.exportManager.export(exportMethod, model, defaultFileName);
} else {
// if we don't have an export method we need to ask for one and display the
// quickpick menu
- const pickedItem = await this.showExportQuickPickMenu(model).then((item) => item);
+ const pickedItem = await this.showExportQuickPickMenu(model, defaultFileName).then((item) => item);
if (pickedItem !== undefined) {
pickedItem.handler();
} else {
@@ -78,7 +100,7 @@ export class ExportCommands implements IDisposable {
}
}
- private getExportQuickPickItems(model: INotebookModel): IExportQuickPickItem[] {
+ private getExportQuickPickItems(model: INotebookModel, defaultFileName?: string): IExportQuickPickItem[] {
return [
{
label: DataScience.exportPythonQuickPickLabel(),
@@ -97,7 +119,7 @@ export class ExportCommands implements IDisposable {
sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick, undefined, {
format: ExportFormat.html
});
- this.commandManager.executeCommand(Commands.ExportToHTML, model);
+ this.commandManager.executeCommand(Commands.ExportToHTML, model, defaultFileName);
}
},
{
@@ -107,14 +129,17 @@ export class ExportCommands implements IDisposable {
sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick, undefined, {
format: ExportFormat.pdf
});
- this.commandManager.executeCommand(Commands.ExportToPDF, model);
+ this.commandManager.executeCommand(Commands.ExportToPDF, model, defaultFileName);
}
}
];
}
- private async showExportQuickPickMenu(model: INotebookModel): Promise {
- const items = this.getExportQuickPickItems(model);
+ private async showExportQuickPickMenu(
+ model: INotebookModel,
+ defaultFileName?: string
+ ): Promise {
+ const items = this.getExportQuickPickItems(model, defaultFileName);
const options: QuickPickOptions = {
ignoreFocusOut: false,
diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts
index 7967ba05ce8b..43b550f1db4c 100644
--- a/src/client/datascience/constants.ts
+++ b/src/client/datascience/constants.ts
@@ -90,6 +90,8 @@ export namespace Commands {
export const SaveAsNotebookNonCustomEditor = 'python.datascience.notebookeditor.saveAs';
export const OpenNotebookNonCustomEditor = 'python.datascience.notebookeditor.open';
export const GatherQuality = 'python.datascience.gatherquality';
+ export const LatestExtension = 'python.datascience.latestExtension';
+ export const TrustNotebook = 'python.datascience.notebookeditor.trust';
export const EnableLoadingWidgetsFrom3rdPartySource =
'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource';
}
@@ -319,6 +321,7 @@ export enum Telemetry {
KernelInvalid = 'DS_INTERNAL.INVALID_KERNEL_USED',
GatherIsInstalled = 'DS_INTERNAL.GATHER_IS_INSTALLED',
GatherCompleted = 'DATASCIENCE.GATHER_COMPLETED',
+ GatherStats = 'DS_INTERNAL.GATHER_STATS',
GatheredNotebookSaved = 'DATASCIENCE.GATHERED_NOTEBOOK_SAVED',
GatherQualityReport = 'DS_INTERNAL.GATHER_QUALITY_REPORT',
ZMQSupported = 'DS_INTERNAL.ZMQ_NATIVE_BINARIES_LOADING',
@@ -367,7 +370,11 @@ export enum Telemetry {
RunByLineStart = 'DATASCIENCE.RUN_BY_LINE',
RunByLineStep = 'DATASCIENCE.RUN_BY_LINE_STEP',
RunByLineStop = 'DATASCIENCE.RUN_BY_LINE_STOP',
- RunByLineVariableHover = 'DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER'
+ RunByLineVariableHover = 'DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER',
+ TrustAllNotebooks = 'DATASCIENCE.TRUST_ALL_NOTEBOOKS',
+ TrustNotebook = 'DATASCIENCE.TRUST_NOTEBOOK',
+ DoNotTrustNotebook = 'DATASCIENCE.DO_NOT_TRUST_NOTEBOOK',
+ NotebookTrustPromptShown = 'DATASCIENCE.NOTEBOOK_TRUST_PROMPT_SHOWN'
}
export enum NativeKeyboardCommandTelemetry {
diff --git a/src/client/datascience/context/activeEditorContext.ts b/src/client/datascience/context/activeEditorContext.ts
index 39d7ad76cdcf..103021f29481 100644
--- a/src/client/datascience/context/activeEditorContext.ts
+++ b/src/client/datascience/context/activeEditorContext.ts
@@ -122,7 +122,7 @@ export class ActiveEditorContextService implements IExtensionSingleActivationSer
.getOrCreateNotebook({ identity: activeEditor.file, getOnly: true })
.then((nb) => {
if (activeEditor === this.notebookEditorProvider.activeEditor) {
- const canStart = nb && nb.status !== ServerStatus.NotStarted;
+ const canStart = nb && nb.status !== ServerStatus.NotStarted && activeEditor.model?.isTrusted;
this.canRestartNotebookKernelContext.set(!!canStart).ignoreErrors();
}
})
diff --git a/src/client/datascience/export/exportBase.ts b/src/client/datascience/export/exportBase.ts
index 7bf957085fed..2614433f68e6 100644
--- a/src/client/datascience/export/exportBase.ts
+++ b/src/client/datascience/export/exportBase.ts
@@ -1,11 +1,12 @@
import { inject, injectable } from 'inversify';
-import { Uri } from 'vscode';
+import * as path from 'path';
+import { CancellationToken, Uri } from 'vscode';
import { IFileSystem } from '../../common/platform/types';
import { IPythonExecutionFactory, IPythonExecutionService } from '../../common/process/types';
import { reportAction } from '../progress/decorator';
import { ReportableAction } from '../progress/types';
import { IJupyterSubCommandExecutionService, INotebookImporter } from '../types';
-import { IExport } from './types';
+import { ExportFormat, IExport } from './types';
@injectable()
export class ExportBase implements IExport {
@@ -18,43 +19,66 @@ export class ExportBase implements IExport {
) {}
// tslint:disable-next-line: no-empty
- public async export(_source: Uri, _target: Uri): Promise {}
+ public async export(_source: Uri, _target: Uri, _token: CancellationToken): Promise {}
@reportAction(ReportableAction.PerformingExport)
- public async executeCommand(source: Uri, target: Uri, args: string[]): Promise {
+ public async executeCommand(
+ source: Uri,
+ target: Uri,
+ format: ExportFormat,
+ token: CancellationToken
+ ): Promise {
+ if (token.isCancellationRequested) {
+ return;
+ }
+
const service = await this.getExecutionService(source);
if (!service) {
return;
}
- const oldFileExists = await this.fileSystem.fileExists(target.fsPath);
- let oldFileTime;
- if (oldFileExists) {
- oldFileTime = (await this.fileSystem.stat(target.fsPath)).mtime;
+ if (token.isCancellationRequested) {
+ return;
}
+ const tempTarget = await this.fileSystem.createTemporaryFile(path.extname(target.fsPath));
+ const args = [
+ source.fsPath,
+ '--to',
+ format,
+ '--output',
+ path.basename(tempTarget.filePath),
+ '--output-dir',
+ path.dirname(tempTarget.filePath)
+ ];
const result = await service.execModule('jupyter', ['nbconvert'].concat(args), {
throwOnStdErr: false,
- encoding: 'utf8'
+ encoding: 'utf8',
+ token: token
});
- // Need to check if export failed, since throwOnStdErr is not an
- // indicator of a failed export.
- if (!(await this.fileSystem.fileExists(target.fsPath))) {
+ if (token.isCancellationRequested) {
+ tempTarget.dispose();
+ return;
+ }
+
+ try {
+ await this.fileSystem.copyFile(tempTarget.filePath, target.fsPath);
+ } catch {
throw new Error(result.stderr);
- } else if (oldFileExists) {
- // If we exported to a file that already exists we need to check that
- // this file was actually overriden during export
- const newFileTime = (await this.fileSystem.stat(target.fsPath)).mtime;
- if (newFileTime === oldFileTime) {
- throw new Error(result.stderr);
- }
+ } finally {
+ tempTarget.dispose();
}
}
protected async getExecutionService(source: Uri): Promise {
+ const interpreter = await this.jupyterService.getSelectedInterpreter();
+ if (!interpreter) {
+ return;
+ }
return this.pythonExecutionFactory.createActivatedEnvironment({
resource: source,
+ interpreter,
allowEnvironmentFetchExceptions: false,
bypassCondaExecution: true
});
diff --git a/src/client/datascience/export/exportManager.ts b/src/client/datascience/export/exportManager.ts
index 41eab797fe66..d58367975728 100644
--- a/src/client/datascience/export/exportManager.ts
+++ b/src/client/datascience/export/exportManager.ts
@@ -19,11 +19,15 @@ export class ExportManager implements IExportManager {
@inject(ExportUtil) private readonly exportUtil: ExportUtil
) {}
- public async export(format: ExportFormat, model: INotebookModel): Promise {
+ public async export(
+ format: ExportFormat,
+ model: INotebookModel,
+ defaultFileName?: string
+ ): Promise {
let target;
if (format !== ExportFormat.python) {
- target = await this.filePicker.getExportFileLocation(format, model.file);
+ target = await this.filePicker.getExportFileLocation(format, model.file, defaultFileName);
if (!target) {
return;
}
@@ -38,7 +42,7 @@ export class ExportManager implements IExportManager {
// exporting to certain formats the filename is used within the exported document as the title.
const fileName = path.basename(target.fsPath, path.extname(target.fsPath));
const tempDir = await this.exportUtil.generateTempDir();
- const sourceFilePath = await this.exportUtil.makeFileInDirectory(model, fileName, tempDir.path);
+ const sourceFilePath = await this.exportUtil.makeFileInDirectory(model, `${fileName}.ipynb`, tempDir.path);
const source = Uri.file(sourceFilePath);
if (format === ExportFormat.pdf) {
@@ -47,27 +51,31 @@ export class ExportManager implements IExportManager {
await this.exportUtil.removeSvgs(source);
}
- const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`);
+ const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`, true);
try {
switch (format) {
case ExportFormat.python:
- await this.exportToPython.export(source, target);
+ await this.exportToPython.export(source, target, reporter.token);
break;
case ExportFormat.pdf:
- await this.exportToPDF.export(source, target);
+ await this.exportToPDF.export(source, target, reporter.token);
break;
case ExportFormat.html:
- await this.exportToHTML.export(source, target);
+ await this.exportToHTML.export(source, target, reporter.token);
break;
default:
break;
}
} finally {
- reporter.dispose();
tempDir.dispose();
+ reporter.dispose();
+ }
+
+ if (reporter.token.isCancellationRequested) {
+ return;
}
return target;
diff --git a/src/client/datascience/export/exportManagerDependencyChecker.ts b/src/client/datascience/export/exportManagerDependencyChecker.ts
index 8d3d0b99a6f7..daf8e44a453a 100644
--- a/src/client/datascience/export/exportManagerDependencyChecker.ts
+++ b/src/client/datascience/export/exportManagerDependencyChecker.ts
@@ -16,7 +16,11 @@ export class ExportManagerDependencyChecker implements IExportManager {
@inject(ProgressReporter) private readonly progressReporter: ProgressReporter
) {}
- public async export(format: ExportFormat, model: INotebookModel): Promise {
+ public async export(
+ format: ExportFormat,
+ model: INotebookModel,
+ defaultFileName?: string
+ ): Promise {
// Before we try the import, see if we don't support it, if we don't give a chance to install dependencies
const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`);
try {
@@ -29,6 +33,6 @@ export class ExportManagerDependencyChecker implements IExportManager {
} finally {
reporter.dispose();
}
- return this.manager.export(format, model);
+ return this.manager.export(format, model, defaultFileName);
}
}
diff --git a/src/client/datascience/export/exportManagerFileOpener.ts b/src/client/datascience/export/exportManagerFileOpener.ts
index afc9c9310e43..5373d1924260 100644
--- a/src/client/datascience/export/exportManagerFileOpener.ts
+++ b/src/client/datascience/export/exportManagerFileOpener.ts
@@ -22,10 +22,14 @@ export class ExportManagerFileOpener implements IExportManager {
@inject(IBrowserService) private readonly browserService: IBrowserService
) {}
- public async export(format: ExportFormat, model: INotebookModel): Promise {
+ public async export(
+ format: ExportFormat,
+ model: INotebookModel,
+ defaultFileName?: string
+ ): Promise {
let uri: Uri | undefined;
try {
- uri = await this.manager.export(format, model);
+ uri = await this.manager.export(format, model, defaultFileName);
} catch (e) {
let msg = e;
traceError('Export failed', e);
diff --git a/src/client/datascience/export/exportManagerFilePicker.ts b/src/client/datascience/export/exportManagerFilePicker.ts
index 6dbe8ca58458..c92436c295dd 100644
--- a/src/client/datascience/export/exportManagerFilePicker.ts
+++ b/src/client/datascience/export/exportManagerFilePicker.ts
@@ -20,19 +20,27 @@ export class ExportManagerFilePicker implements IExportManagerFilePicker {
@inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceStorage: Memento
) {}
- public async getExportFileLocation(format: ExportFormat, source: Uri): Promise {
+ public async getExportFileLocation(
+ format: ExportFormat,
+ source: Uri,
+ defaultFileName?: string
+ ): Promise {
// map each export method to a set of file extensions
let fileExtensions;
+ let extension: string | undefined;
switch (format) {
case ExportFormat.python:
fileExtensions = PythonExtensions;
+ extension = '.py';
break;
case ExportFormat.pdf:
+ extension = '.pdf';
fileExtensions = PDFExtensions;
break;
case ExportFormat.html:
+ extension = '.html';
fileExtensions = HTMLExtensions;
break;
@@ -40,8 +48,11 @@ export class ExportManagerFilePicker implements IExportManagerFilePicker {
return;
}
- const notebookFileName = path.basename(source.fsPath, path.extname(source.fsPath));
- const dialogUri = Uri.file(path.join(this.getLastFileSaveLocation().fsPath, notebookFileName));
+ const targetFileName = defaultFileName
+ ? defaultFileName
+ : `${path.basename(source.fsPath, path.extname(source.fsPath))}${extension}`;
+
+ const dialogUri = Uri.file(path.join(this.getLastFileSaveLocation().fsPath, targetFileName));
const options: SaveDialogOptions = {
defaultUri: dialogUri,
saveLabel: 'Export',
diff --git a/src/client/datascience/export/exportToHTML.ts b/src/client/datascience/export/exportToHTML.ts
index 005addcc4cfc..d12c92502402 100644
--- a/src/client/datascience/export/exportToHTML.ts
+++ b/src/client/datascience/export/exportToHTML.ts
@@ -1,20 +1,11 @@
import { injectable } from 'inversify';
-import * as path from 'path';
-import { Uri } from 'vscode';
+import { CancellationToken, Uri } from 'vscode';
import { ExportBase } from './exportBase';
+import { ExportFormat } from './types';
@injectable()
export class ExportToHTML extends ExportBase {
- public async export(source: Uri, target: Uri): Promise {
- const args = [
- source.fsPath,
- '--to',
- 'html',
- '--output',
- path.basename(target.fsPath),
- '--output-dir',
- path.dirname(target.fsPath)
- ];
- await this.executeCommand(source, target, args);
+ public async export(source: Uri, target: Uri, token: CancellationToken): Promise {
+ await this.executeCommand(source, target, ExportFormat.html, token);
}
}
diff --git a/src/client/datascience/export/exportToPDF.ts b/src/client/datascience/export/exportToPDF.ts
index 550e4b1c9e67..0b4b6afba335 100644
--- a/src/client/datascience/export/exportToPDF.ts
+++ b/src/client/datascience/export/exportToPDF.ts
@@ -1,33 +1,11 @@
-import { inject, injectable } from 'inversify';
-import * as path from 'path';
-import { Uri } from 'vscode';
-import { IFileSystem } from '../../common/platform/types';
-import { IPythonExecutionFactory } from '../../common/process/types';
-import { IJupyterSubCommandExecutionService, INotebookImporter } from '../types';
+import { injectable } from 'inversify';
+import { CancellationToken, Uri } from 'vscode';
import { ExportBase } from './exportBase';
+import { ExportFormat } from './types';
@injectable()
export class ExportToPDF extends ExportBase {
- constructor(
- @inject(IPythonExecutionFactory) protected readonly pythonExecutionFactory: IPythonExecutionFactory,
- @inject(IJupyterSubCommandExecutionService)
- protected jupyterService: IJupyterSubCommandExecutionService,
- @inject(IFileSystem) protected readonly fileSystem: IFileSystem,
- @inject(INotebookImporter) protected readonly importer: INotebookImporter
- ) {
- super(pythonExecutionFactory, jupyterService, fileSystem, importer);
- }
-
- public async export(source: Uri, target: Uri): Promise {
- const args = [
- source.fsPath,
- '--to',
- 'pdf',
- '--output',
- path.basename(target.fsPath),
- '--output-dir',
- path.dirname(target.fsPath)
- ];
- await this.executeCommand(source, target, args);
+ public async export(source: Uri, target: Uri, token: CancellationToken): Promise {
+ await this.executeCommand(source, target, ExportFormat.pdf, token);
}
}
diff --git a/src/client/datascience/export/exportToPython.ts b/src/client/datascience/export/exportToPython.ts
index 607026982710..58e010ead4f9 100644
--- a/src/client/datascience/export/exportToPython.ts
+++ b/src/client/datascience/export/exportToPython.ts
@@ -1,10 +1,13 @@
import { injectable } from 'inversify';
-import { Uri } from 'vscode';
+import { CancellationToken, Uri } from 'vscode';
import { ExportBase } from './exportBase';
@injectable()
export class ExportToPython extends ExportBase {
- public async export(source: Uri, target: Uri): Promise {
+ public async export(source: Uri, target: Uri, token: CancellationToken): Promise {
+ if (token.isCancellationRequested) {
+ return;
+ }
const contents = await this.importer.importFromFile(source.fsPath);
await this.fileSystem.writeFile(target.fsPath, contents);
}
diff --git a/src/client/datascience/export/exportUtil.ts b/src/client/datascience/export/exportUtil.ts
index d7fe774d9923..48bcf44f1535 100644
--- a/src/client/datascience/export/exportUtil.ts
+++ b/src/client/datascience/export/exportUtil.ts
@@ -6,13 +6,12 @@ import * as uuid from 'uuid/v4';
import { CancellationTokenSource, Uri } from 'vscode';
import { IFileSystem, TemporaryDirectory } from '../../common/platform/types';
import { sleep } from '../../common/utils/async';
-import { ICell, IDataScienceErrorHandler, INotebookExporter, INotebookModel, INotebookStorage } from '../types';
+import { ICell, INotebookExporter, INotebookModel, INotebookStorage } from '../types';
@injectable()
export class ExportUtil {
constructor(
@inject(IFileSystem) private fileSystem: IFileSystem,
- @inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler,
@inject(INotebookStorage) private notebookStorage: INotebookStorage,
@inject(INotebookExporter) private jupyterExporter: INotebookExporter
) {}
@@ -44,12 +43,7 @@ export class ExportUtil {
public async makeFileInDirectory(model: INotebookModel, fileName: string, dirPath: string): Promise {
const newFilePath = path.join(dirPath, fileName);
- try {
- const content = model ? model.getContent() : '';
- await this.fileSystem.writeFile(newFilePath, content, 'utf-8');
- } catch (e) {
- await this.errorHandler.handleError(e);
- }
+ await this.fileSystem.writeFile(newFilePath, model.getContent(), 'utf-8');
return newFilePath;
}
@@ -63,7 +57,7 @@ export class ExportUtil {
await this.jupyterExporter.exportToFile(cells, tempFile.filePath, false);
const newPath = path.join(tempDir.path, '.ipynb');
await this.fileSystem.copyFile(tempFile.filePath, newPath);
- model = await this.notebookStorage.load(Uri.file(newPath));
+ model = await this.notebookStorage.get(Uri.file(newPath));
} finally {
tempFile.dispose();
tempDir.dispose();
@@ -73,7 +67,7 @@ export class ExportUtil {
}
public async removeSvgs(source: Uri) {
- const model = await this.notebookStorage.load(source);
+ const model = await this.notebookStorage.get(source);
const newCells: ICell[] = [];
for (const cell of model.cells) {
diff --git a/src/client/datascience/export/types.ts b/src/client/datascience/export/types.ts
index a8de0fa1cf95..07ff4eee7f61 100644
--- a/src/client/datascience/export/types.ts
+++ b/src/client/datascience/export/types.ts
@@ -1,4 +1,4 @@
-import { Uri } from 'vscode';
+import { CancellationToken, Uri } from 'vscode';
import { INotebookModel } from '../types';
export enum ExportFormat {
@@ -9,15 +9,15 @@ export enum ExportFormat {
export const IExportManager = Symbol('IExportManager');
export interface IExportManager {
- export(format: ExportFormat, model: INotebookModel): Promise;
+ export(format: ExportFormat, model: INotebookModel, defaultFileName?: string): Promise;
}
export const IExport = Symbol('IExport');
export interface IExport {
- export(source: Uri, target: Uri): Promise;
+ export(source: Uri, target: Uri, token: CancellationToken): Promise;
}
export const IExportManagerFilePicker = Symbol('IExportManagerFilePicker');
export interface IExportManagerFilePicker {
- getExportFileLocation(format: ExportFormat, source: Uri): Promise;
+ getExportFileLocation(format: ExportFormat, source: Uri, defaultFileName?: string): Promise;
}
diff --git a/src/client/datascience/gather/gatherListener.ts b/src/client/datascience/gather/gatherListener.ts
index 470e7ef477a3..95b6215b5e31 100644
--- a/src/client/datascience/gather/gatherListener.ts
+++ b/src/client/datascience/gather/gatherListener.ts
@@ -42,6 +42,8 @@ export class GatherListener implements IInteractiveWindowListener {
private notebookUri: Uri | undefined;
private gatherProvider: IGatherProvider | undefined;
private gatherTimer: StopWatch | undefined;
+ private linesSubmitted: number = 0;
+ private cellsSubmitted: number = 0;
constructor(
@inject(IApplicationShell) private applicationShell: IApplicationShell,
@@ -78,11 +80,22 @@ export class GatherListener implements IInteractiveWindowListener {
break;
case InteractiveWindowMessages.RestartKernel:
+ this.linesSubmitted = 0;
+ this.cellsSubmitted = 0;
if (this.gatherProvider) {
this.gatherProvider.resetLog();
}
break;
+ case InteractiveWindowMessages.FinishCell:
+ const cell = payload as ICell;
+ if (cell && cell.data && cell.data.source) {
+ const lineCount: number = cell.data.source.length as number;
+ this.linesSubmitted += lineCount;
+ this.cellsSubmitted += 1;
+ }
+ break;
+
default:
break;
}
@@ -155,6 +168,13 @@ export class GatherListener implements IInteractiveWindowListener {
await this.showNotebook(slicedProgram, cell);
sendTelemetryEvent(Telemetry.GatherCompleted, this.gatherTimer?.elapsedTime, { result: 'notebook' });
}
+
+ sendTelemetryEvent(Telemetry.GatherStats, undefined, {
+ linesSubmitted: this.linesSubmitted,
+ cellsSubmitted: this.cellsSubmitted,
+ linesGathered: slicedProgram.splitLines().length,
+ cellsGathered: generateCellsFromString(slicedProgram).length
+ });
}
};
diff --git a/src/client/datascience/interactive-common/linkProvider.ts b/src/client/datascience/interactive-common/linkProvider.ts
index ea6461b66114..b8096ad616a6 100644
--- a/src/client/datascience/interactive-common/linkProvider.ts
+++ b/src/client/datascience/interactive-common/linkProvider.ts
@@ -19,6 +19,7 @@ const LineQueryRegex = /line=(\d+)/;
// in a markdown cell using the syntax: https://command:[my.vscode.command].
const linkCommandWhitelist = [
'python.datascience.gatherquality',
+ 'python.datascience.latestExtension',
'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'
];
@@ -52,8 +53,7 @@ export class LinkProvider implements IInteractiveWindowListener {
this.openFile(href);
} else if (href.startsWith('https://command:')) {
const temp: string = href.split(':')[2];
- const params: string[] =
- temp.includes('/?') && temp.includes(',') ? temp.split('/?')[1].split(',') : [];
+ const params: string[] = temp.includes('/?') ? temp.split('/?')[1].split(',') : [];
let command = temp.split('/?')[0];
if (command.endsWith('/')) {
command = command.substring(0, command.length - 1);
diff --git a/src/client/datascience/interactive-ipynb/digestStorage.ts b/src/client/datascience/interactive-ipynb/digestStorage.ts
index cb6887c81725..620953bff53d 100644
--- a/src/client/datascience/interactive-ipynb/digestStorage.ts
+++ b/src/client/datascience/interactive-ipynb/digestStorage.ts
@@ -3,17 +3,17 @@ import { inject, injectable } from 'inversify';
import * as os from 'os';
import * as path from 'path';
import { Uri } from 'vscode';
-import { traceError } from '../../common/logger';
+import { traceError, traceInfo } from '../../common/logger';
import { isFileNotFoundError } from '../../common/platform/errors';
import { IFileSystem } from '../../common/platform/types';
import { IExtensionContext } from '../../common/types';
import { IDigestStorage } from '../types';
-// NB: still need to implement automatic culling of least recently used entries
@injectable()
export class DigestStorage implements IDigestStorage {
public readonly key: Promise;
private digestDir: Promise;
+ private loggedFileLocations = new Set();
constructor(
@inject(IFileSystem) private fs: IFileSystem,
@@ -26,7 +26,21 @@ export class DigestStorage implements IDigestStorage {
public async saveDigest(uri: Uri, signature: string) {
const fileLocation = await this.getFileLocation(uri);
// Since the signature is a hex digest, the character 'z' is being used to delimit the start and end of a single digest
- await this.fs.appendFile(fileLocation, `z${signature}z\n`);
+ try {
+ await this.saveDigestInner(uri, fileLocation, signature);
+ } catch (err) {
+ // The nbsignatures dir is only initialized on extension activation.
+ // If the user deletes it to reset trust, the next attempt to trust
+ // an untrusted notebook in the same session will fail because the parent
+ // directory does not exist.
+ if (isFileNotFoundError(err)) {
+ // Gracefully recover from such errors by reinitializing directory and retrying
+ await this.initDir();
+ await this.saveDigestInner(uri, fileLocation, signature);
+ } else {
+ traceError(err);
+ }
+ }
}
public async containsDigest(uri: Uri, signature: string) {
@@ -42,6 +56,14 @@ export class DigestStorage implements IDigestStorage {
}
}
+ private async saveDigestInner(uri: Uri, fileLocation: string, signature: string) {
+ await this.fs.appendFile(fileLocation, `z${signature}z\n`);
+ if (!this.loggedFileLocations.has(fileLocation)) {
+ traceInfo(`Wrote trust for ${uri.toString()} to ${fileLocation}`);
+ this.loggedFileLocations.add(fileLocation);
+ }
+ }
+
private async getFileLocation(uri: Uri): Promise {
const normalizedName = os.platform() === 'win32' ? uri.fsPath.toLowerCase() : uri.fsPath;
const hashedName = createHash('sha256').update(normalizedName).digest('hex');
diff --git a/src/client/datascience/interactive-ipynb/nativeEditor.ts b/src/client/datascience/interactive-ipynb/nativeEditor.ts
index ef299a60b1ed..7eeb259016fc 100644
--- a/src/client/datascience/interactive-ipynb/nativeEditor.ts
+++ b/src/client/datascience/interactive-ipynb/nativeEditor.ts
@@ -97,7 +97,6 @@ import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher';
const nativeEditorDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'notebook');
@injectable()
export class NativeEditor extends InteractiveBase implements INotebookEditor {
- public readonly type: 'old' | 'custom' = 'custom';
public get onDidChangeViewState(): Event {
return this._onDidChangeViewState.event;
}
@@ -139,6 +138,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
public get isDirty(): boolean {
return this.model ? this.model.isDirty : false;
}
+ public readonly type: 'old' | 'custom' = 'custom';
public model: Readonly | undefined;
protected savedEvent: EventEmitter = new EventEmitter();
protected closedEvent: EventEmitter = new EventEmitter();
@@ -151,6 +151,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
private startupTimer: StopWatch = new StopWatch();
private loadedAllCells: boolean = false;
private executeCancelTokens = new Set();
+ private previouslyNotTrusted: boolean = false;
constructor(
@multiInject(IInteractiveWindowListener) listeners: IInteractiveWindowListener[],
@@ -226,6 +227,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
);
asyncRegistry.push(this);
+ asyncRegistry.push(this.trustService.onDidSetNotebookTrust(this.monitorChangesToTrust, this));
this.synchronizer.subscribeToUserActions(this, this.postMessage.bind(this));
}
@@ -250,6 +252,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
// Sign up for dirty events
model.changed(this.modelChanged.bind(this));
+ this.previouslyNotTrusted = !model.isTrusted;
}
// tslint:disable-next-line: no-any
@@ -595,7 +598,13 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
}
}
}
-
+ private async monitorChangesToTrust() {
+ if (this.previouslyNotTrusted && this.model?.isTrusted) {
+ this.previouslyNotTrusted = false;
+ // Tell UI to update main state
+ this.postMessage(InteractiveWindowMessages.TrustNotebookComplete).ignoreErrors();
+ }
+ }
private renameVariableExplorerHeights(name: string, updatedName: string) {
// Updates the workspace storage to reflect the updated name of the notebook
// should be called if the name of the notebook changes
@@ -611,31 +620,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
}
private async launchNotebookTrustPrompt() {
- const prompts = [localize.DataScience.trustNotebook(), localize.DataScience.doNotTrustNotebook()];
- const selection = await this.applicationShell.showErrorMessage(
- localize.DataScience.launchNotebookTrustPrompt(),
- ...prompts
- );
- if (!selection) {
- return;
- }
- if (this.model && selection === localize.DataScience.trustNotebook() && !this.model.isTrusted) {
- try {
- const contents = this.model.getContent();
- await this.trustService.trustNotebook(this.model.file, contents);
- // Update model trust
- this.model.update({
- source: 'user',
- kind: 'updateTrust',
- oldDirty: this.model.isDirty,
- newDirty: this.model.isDirty,
- isNotebookTrusted: true
- });
- // Tell UI to update main state
- await this.postMessage(InteractiveWindowMessages.TrustNotebookComplete);
- } catch (err) {
- traceError(err);
- }
+ if (this.model && !this.model.isTrusted) {
+ await this.commandManager.executeCommand(Commands.TrustNotebook, this.model.file);
}
}
@@ -730,7 +716,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
if (!activeEditor || !activeEditor.model) {
return;
}
- this.commandManager.executeCommand(Commands.Export, activeEditor.model);
+ this.commandManager.executeCommand(Commands.Export, activeEditor.model, undefined);
}
private logNativeCommand(args: INativeCommand) {
diff --git a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts
index 53e1a39506b7..2b0445be4cf5 100644
--- a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts
+++ b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts
@@ -184,7 +184,7 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter);
// Load our model from our storage object.
- const model = await this.storage.load(file, contents, options);
+ const model = await this.storage.get(file, contents, options);
// Make sure to listen to events on the model
this.trackModel(model);
diff --git a/src/client/datascience/interactive-ipynb/nativeEditorStorage.ts b/src/client/datascience/interactive-ipynb/nativeEditorStorage.ts
index 0586697ef17b..d9600bf8f483 100644
--- a/src/client/datascience/interactive-ipynb/nativeEditorStorage.ts
+++ b/src/client/datascience/interactive-ipynb/nativeEditorStorage.ts
@@ -74,20 +74,20 @@ export class NativeEditorStorage implements INotebookStorage {
return `${path.basename(model.file.fsPath)}-${uuid()}`;
}
- public load(
+ public get(
file: Uri,
possibleContents?: string,
backupId?: string,
forVSCodeNotebook?: boolean
): Promise;
- public load(
+ public get(
file: Uri,
possibleContents?: string,
// tslint:disable-next-line: unified-signatures
skipDirtyContents?: boolean,
forVSCodeNotebook?: boolean
): Promise;
- public load(
+ public get(
file: Uri,
possibleContents?: string,
// tslint:disable-next-line: no-any
@@ -282,10 +282,10 @@ export class NativeEditorStorage implements INotebookStorage {
const dirtyContents = skipDirtyContents ? undefined : await this.getStoredContents(file, backupId);
if (dirtyContents) {
// This means we're dirty. Indicate dirty and load from this content
- return this.loadContents(file, dirtyContents, true, contents, forVSCodeNotebook);
+ return this.loadContents(file, dirtyContents, true, forVSCodeNotebook);
} else {
// Load without setting dirty
- return this.loadContents(file, contents, undefined, undefined, forVSCodeNotebook);
+ return this.loadContents(file, contents, undefined, forVSCodeNotebook);
}
} catch (ex) {
// May not exist at this time. Should always have a single cell though
@@ -308,7 +308,6 @@ export class NativeEditorStorage implements INotebookStorage {
file: Uri,
contents: string | undefined,
isInitiallyDirty = false,
- trueContents?: string,
forVSCodeNotebook?: boolean
) {
// tslint:disable-next-line: no-any
@@ -348,18 +347,9 @@ export class NativeEditorStorage implements INotebookStorage {
}
const pythonNumber = json ? await this.extractPythonMainVersion(json) : 3;
- /* As an optimization, we don't call trustNotebook for hot exit, since our hot exit backup code gets called by VS
- Code whenever the notebook model changes. This means it's called very often, perhaps even as often as autosave.
- Instead, when loading a file that is dirty, we check if the actual file contents on disk are trusted. If so, we treat
- the dirty contents as trusted as well. */
- const contentsToCheck = isInitiallyDirty && trueContents !== undefined ? trueContents : contents;
- const isTrusted =
- contents === undefined || isUntitledFile(file)
- ? true // If no contents or untitled, this is a newly created file, so it should be trusted
- : await this.trustService.isNotebookTrusted(file, contentsToCheck!);
- return this.factory.createModel(
+ const model = this.factory.createModel(
{
- trusted: isTrusted,
+ trusted: true,
file,
cells: remapped,
notebookJson: json,
@@ -369,6 +359,23 @@ export class NativeEditorStorage implements INotebookStorage {
},
forVSCodeNotebook
);
+
+ // If no contents or untitled, this is a newly created file
+ // If dirty, that means it's been edited before in our extension
+ if (contents !== undefined && !isUntitledFile(file) && !isInitiallyDirty) {
+ const isNotebookTrusted = await this.trustService.isNotebookTrusted(file, model.getContent());
+ if (isNotebookTrusted !== model.isTrusted) {
+ model.update({
+ source: 'user',
+ kind: 'updateTrust',
+ oldDirty: model.isDirty,
+ newDirty: model.isDirty,
+ isNotebookTrusted
+ });
+ }
+ }
+
+ return model;
}
private getStaticStorageKey(file: Uri): string {
diff --git a/src/client/datascience/interactive-ipynb/notebookStorageProvider.ts b/src/client/datascience/interactive-ipynb/notebookStorageProvider.ts
index 0461c7e5f6ea..5ffb2814f4f4 100644
--- a/src/client/datascience/interactive-ipynb/notebookStorageProvider.ts
+++ b/src/client/datascience/interactive-ipynb/notebookStorageProvider.ts
@@ -56,8 +56,8 @@ export class NotebookStorageProvider implements INotebookStorageProvider {
public deleteBackup(model: INotebookModel, backupId?: string) {
return this.storage.deleteBackup(model, backupId);
}
- public load(file: Uri, contents?: string, backupId?: string, forVSCodeNotebook?: boolean): Promise;
- public load(
+ public get(file: Uri, contents?: string, backupId?: string, forVSCodeNotebook?: boolean): Promise;
+ public get(
file: Uri,
contents?: string,
// tslint:disable-next-line: unified-signatures
@@ -66,7 +66,7 @@ export class NotebookStorageProvider implements INotebookStorageProvider {
): Promise;
// tslint:disable-next-line: no-any
- public load(file: Uri, contents?: string, options?: any, forVSCodeNotebook?: boolean): Promise {
+ public get(file: Uri, contents?: string, options?: any, forVSCodeNotebook?: boolean): Promise {
const key = file.toString();
if (!this.storageAndModels.has(key)) {
// Every time we load a new untitled file, up the counter past the max value for this counter
@@ -74,7 +74,7 @@ export class NotebookStorageProvider implements INotebookStorageProvider {
file,
NotebookStorageProvider.untitledCounter
);
- const promise = this.storage.load(file, contents, options, forVSCodeNotebook);
+ const promise = this.storage.get(file, contents, options, forVSCodeNotebook);
this.storageAndModels.set(key, promise.then(this.trackModel.bind(this)));
}
return this.storageAndModels.get(key)!;
@@ -90,7 +90,7 @@ export class NotebookStorageProvider implements INotebookStorageProvider {
const uri = this.getNextNewNotebookUri(forVSCodeNotebooks);
// Always skip loading from the hot exit file. When creating a new file we want a new file.
- return this.load(uri, contents, true);
+ return this.get(uri, contents, true);
}
private getNextNewNotebookUri(forVSCodeNotebooks?: boolean): Uri {
diff --git a/src/client/datascience/interactive-ipynb/trustCommandHandler.ts b/src/client/datascience/interactive-ipynb/trustCommandHandler.ts
new file mode 100644
index 000000000000..b9a002749cbb
--- /dev/null
+++ b/src/client/datascience/interactive-ipynb/trustCommandHandler.ts
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+'use strict';
+
+import { inject, injectable } from 'inversify';
+import { commands, Uri } from 'vscode';
+import { IExtensionSingleActivationService } from '../../activation/types';
+import { IApplicationShell, ICommandManager } from '../../common/application/types';
+import { ContextKey } from '../../common/contextKey';
+import { EnableTrustedNotebooks } from '../../common/experiments/groups';
+import '../../common/extensions';
+import { IDisposableRegistry, IExperimentService } from '../../common/types';
+import { swallowExceptions } from '../../common/utils/decorators';
+import { DataScience } from '../../common/utils/localize';
+import { sendTelemetryEvent } from '../../telemetry';
+import { Commands, Telemetry } from '../constants';
+import { INotebookStorageProvider } from '../interactive-ipynb/notebookStorageProvider';
+import { INotebookEditorProvider, ITrustService } from '../types';
+
+@injectable()
+export class TrustCommandHandler implements IExtensionSingleActivationService {
+ constructor(
+ @inject(ITrustService) private readonly trustService: ITrustService,
+ @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider,
+ @inject(INotebookStorageProvider) private readonly storageProvider: INotebookStorageProvider,
+ @inject(ICommandManager) private readonly commandManager: ICommandManager,
+ @inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
+ @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
+ @inject(IExperimentService) private readonly experiments: IExperimentService
+ ) {}
+ public async activate(): Promise {
+ this.activateInBackground().ignoreErrors();
+ }
+ public async activateInBackground(): Promise {
+ if (!(await this.experiments.inExperiment(EnableTrustedNotebooks.experiment))) {
+ return;
+ }
+ const context = new ContextKey('python.datascience.trustfeatureenabled', this.commandManager);
+ context.set(true).ignoreErrors();
+ this.disposables.push(this.commandManager.registerCommand(Commands.TrustNotebook, this.onTrustNotebook, this));
+ }
+ @swallowExceptions('Trusting notebook')
+ private async onTrustNotebook(uri?: Uri) {
+ uri = uri ?? this.editorProvider.activeEditor?.file;
+ if (!uri) {
+ return;
+ }
+
+ const model = await this.storageProvider.get(uri);
+ if (model.isTrusted) {
+ return;
+ }
+
+ const selection = await this.applicationShell.showErrorMessage(
+ DataScience.launchNotebookTrustPrompt(),
+ DataScience.trustNotebook(),
+ DataScience.doNotTrustNotebook(),
+ DataScience.trustAllNotebooks()
+ );
+ sendTelemetryEvent(Telemetry.NotebookTrustPromptShown);
+
+ switch (selection) {
+ case DataScience.trustAllNotebooks():
+ commands.executeCommand('workbench.action.openSettings', 'python.dataScience.alwaysTrustNotebooks');
+ sendTelemetryEvent(Telemetry.TrustAllNotebooks);
+ break;
+ case DataScience.trustNotebook():
+ // Update model trust
+ model.update({
+ source: 'user',
+ kind: 'updateTrust',
+ oldDirty: model.isDirty,
+ newDirty: model.isDirty,
+ isNotebookTrusted: true
+ });
+ const contents = model.getContent();
+ await this.trustService.trustNotebook(model.file, contents);
+ sendTelemetryEvent(Telemetry.TrustNotebook);
+ break;
+ case DataScience.doNotTrustNotebook():
+ sendTelemetryEvent(Telemetry.DoNotTrustNotebook);
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts
index b5e8f6536d34..ca5a209ecde1 100644
--- a/src/client/datascience/interactive-window/interactiveWindow.ts
+++ b/src/client/datascience/interactive-window/interactiveWindow.ts
@@ -437,7 +437,11 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi
this.stopProgress();
}
if (model) {
- this.commandManager.executeCommand(Commands.Export, model);
+ let defaultFileName;
+ if (this.lastFile) {
+ defaultFileName = path.basename(this.lastFile, path.extname(this.lastFile));
+ }
+ this.commandManager.executeCommand(Commands.Export, model, defaultFileName);
}
}
diff --git a/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts b/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts
index 1ef80d368189..09955c7ef402 100644
--- a/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts
+++ b/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts
@@ -6,7 +6,7 @@
import { ChildProcess } from 'child_process';
import { inject, injectable } from 'inversify';
import { IDisposable } from 'monaco-editor';
-import { ObservableExecutionResult } from '../../common/process/types';
+import { IPythonExecutionService, ObservableExecutionResult } from '../../common/process/types';
import { Resource } from '../../common/types';
import { noop } from '../../common/utils/misc';
import { PythonInterpreter } from '../../pythonEnvironments/info';
@@ -27,15 +27,10 @@ export class PythonKernelLauncherDaemon implements IDisposable {
resource: Resource,
kernelSpec: IJupyterKernelSpec,
interpreter?: PythonInterpreter
- ): Promise<{ observableOutput: ObservableExecutionResult; daemon: IPythonKernelDaemon } | undefined> {
+ ): Promise<{ observableOutput: ObservableExecutionResult; daemon: IPythonKernelDaemon | undefined }> {
const daemon = await this.daemonPool.get(resource, kernelSpec, interpreter);
- // The daemon pool can return back a non-IPythonKernelDaemon if daemon service is not supported or for Python 2.
- // Use a check for the daemon.start function here before we call it.
- if (!daemon.start) {
- return undefined;
- }
-
+ // Check to see if we have the type of kernelspec that we expect
const args = kernelSpec.argv.slice();
const modulePrefixIndex = args.findIndex((item) => item === '-m');
if (modulePrefixIndex === -1) {
@@ -49,11 +44,26 @@ export class PythonKernelLauncherDaemon implements IDisposable {
const moduleArgs = args.slice(modulePrefixIndex + 2);
const env = kernelSpec.env && Object.keys(kernelSpec.env).length > 0 ? kernelSpec.env : undefined;
- const observableOutput = await daemon.start(moduleName, moduleArgs, { env });
- if (observableOutput.proc) {
- this.processesToDispose.push(observableOutput.proc);
+ // The daemon pool can return back a non-IPythonKernelDaemon if daemon service is not supported or for Python 2.
+ // Use a check for the daemon.start function here before we call it.
+ if (!daemon.start) {
+ // If we don't have a KernelDaemon here then we have an execution service and should use that to launch
+ // Typing is a bit funk here, as createDaemon can return an execution service instead of the requested
+ // daemon class
+ // tslint:disable-next-line:no-any
+ const executionService = (daemon as any) as IPythonExecutionService;
+
+ const observableOutput = executionService.execModuleObservable(moduleName, moduleArgs, { env });
+
+ return { observableOutput, daemon: undefined };
+ } else {
+ // In the case that we do have a kernel deamon, just return it
+ const observableOutput = await daemon.start(moduleName, moduleArgs, { env });
+ if (observableOutput.proc) {
+ this.processesToDispose.push(observableOutput.proc);
+ }
+ return { observableOutput, daemon };
}
- return { observableOutput, daemon };
}
public dispose() {
while (this.processesToDispose.length) {
diff --git a/src/client/datascience/kernel-launcher/kernelProcess.ts b/src/client/datascience/kernel-launcher/kernelProcess.ts
index 9b88f0e57947..3996d63ee20b 100644
--- a/src/client/datascience/kernel-launcher/kernelProcess.ts
+++ b/src/client/datascience/kernel-launcher/kernelProcess.ts
@@ -224,13 +224,11 @@ export class KernelProcess implements IKernelProcess {
this.interpreter
);
- if (kernelDaemonLaunch) {
- this.kernelDaemon = kernelDaemonLaunch.daemon;
- exeObs = kernelDaemonLaunch.observableOutput;
- }
+ this.kernelDaemon = kernelDaemonLaunch.daemon;
+ exeObs = kernelDaemonLaunch.observableOutput;
}
- // If we are not python or if we failed with our daemon launch just use the ProcessExecutionFactory
+ // If we are not python just use the ProcessExecutionFactory
if (!exeObs) {
// First part of argument is always the executable.
const executable = this._kernelSpec.argv[0];
diff --git a/src/client/datascience/notebook/contentProvider.ts b/src/client/datascience/notebook/contentProvider.ts
index 3e48f4c88e84..622298bf7cd1 100644
--- a/src/client/datascience/notebook/contentProvider.ts
+++ b/src/client/datascience/notebook/contentProvider.ts
@@ -72,8 +72,8 @@ export class NotebookContentProvider implements INotebookContentProvider {
}
// If there's no backup id, then skip loading dirty contents.
const model = await (openContext.backupId
- ? this.notebookStorage.load(uri, undefined, openContext.backupId, true)
- : this.notebookStorage.load(uri, undefined, true, true));
+ ? this.notebookStorage.get(uri, undefined, openContext.backupId, true)
+ : this.notebookStorage.get(uri, undefined, true, true));
setSharedProperty('ds_notebookeditor', 'native');
sendTelemetryEvent(Telemetry.CellCount, undefined, { count: model.cells.length });
@@ -81,7 +81,7 @@ export class NotebookContentProvider implements INotebookContentProvider {
}
@captureTelemetry(Telemetry.Save, undefined, true)
public async saveNotebook(document: NotebookDocument, cancellation: CancellationToken) {
- const model = await this.notebookStorage.load(document.uri, undefined, undefined, true);
+ const model = await this.notebookStorage.get(document.uri, undefined, undefined, true);
if (cancellation.isCancellationRequested) {
return;
}
@@ -97,7 +97,7 @@ export class NotebookContentProvider implements INotebookContentProvider {
document: NotebookDocument,
cancellation: CancellationToken
): Promise {
- const model = await this.notebookStorage.load(document.uri, undefined, undefined, true);
+ const model = await this.notebookStorage.get(document.uri, undefined, undefined, true);
if (!cancellation.isCancellationRequested) {
await this.notebookStorage.saveAs(model, targetResource);
}
@@ -107,7 +107,7 @@ export class NotebookContentProvider implements INotebookContentProvider {
_context: NotebookDocumentBackupContext,
cancellation: CancellationToken
): Promise {
- const model = await this.notebookStorage.load(document.uri, undefined, undefined, true);
+ const model = await this.notebookStorage.get(document.uri, undefined, undefined, true);
const id = this.notebookStorage.generateBackupId(model);
await this.notebookStorage.backup(model, cancellation, id);
return {
diff --git a/src/client/datascience/notebook/executionService.ts b/src/client/datascience/notebook/executionService.ts
index 95b2bc3fdba6..613c29a4e767 100644
--- a/src/client/datascience/notebook/executionService.ts
+++ b/src/client/datascience/notebook/executionService.ts
@@ -144,7 +144,7 @@ export class NotebookExecutionService implements INotebookExecutionService {
private async getNotebookAndModel(
document: NotebookDocument
): Promise<{ model: VSCodeNotebookModel; nb: INotebook }> {
- const model = await this.notebookStorage.load(document.uri, undefined, undefined, true);
+ const model = await this.notebookStorage.get(document.uri, undefined, undefined, true);
const nb = await this.notebookProvider.getOrCreateNotebook({
identity: document.uri,
resource: document.uri,
diff --git a/src/client/datascience/notebook/helpers/cellUpdateHelpers.ts b/src/client/datascience/notebook/helpers/cellUpdateHelpers.ts
index c0d9274e2813..eb91df59f389 100644
--- a/src/client/datascience/notebook/helpers/cellUpdateHelpers.ts
+++ b/src/client/datascience/notebook/helpers/cellUpdateHelpers.ts
@@ -10,7 +10,7 @@
*/
import * as assert from 'assert';
-import { NotebookCell } from '../../../../../types/vscode-proposed';
+import { NotebookCell, NotebookDocument } from '../../../../../types/vscode-proposed';
import {
NotebookCellLanguageChangeEvent,
NotebookCellOutputsChangeEvent,
@@ -20,8 +20,13 @@ import { traceError } from '../../../common/logger';
import { sendTelemetryEvent } from '../../../telemetry';
import { VSCodeNativeTelemetry } from '../../constants';
import { VSCodeNotebookModel } from '../../notebookStorage/vscNotebookModel';
+import { INotebookModel } from '../../types';
import { findMappedNotebookCellModel } from './cellMappers';
-import { createCellFromVSCNotebookCell, updateVSCNotebookCellMetadata } from './helpers';
+import {
+ createCellFromVSCNotebookCell,
+ createVSCCellOutputsFromOutputs,
+ updateVSCNotebookCellMetadata
+} from './helpers';
// tslint:disable-next-line: no-var-requires no-require-imports
const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed');
@@ -51,6 +56,48 @@ export function updateCellModelWithChangesToVSCCell(
}
}
+/**
+ * Updates a notebook document as a result of trusting it.
+ */
+export function updateVSCNotebookAfterTrustingNotebook(document: NotebookDocument, model: INotebookModel) {
+ const areAllCellsEditableAndRunnable = document.cells.every((cell) => {
+ if (cell.cellKind === vscodeNotebookEnums.CellKind.Markdown) {
+ return cell.metadata.editable;
+ } else {
+ return cell.metadata.editable && cell.metadata.runnable;
+ }
+ });
+ const isDocumentEditableAndRunnable =
+ document.metadata.cellEditable &&
+ document.metadata.cellRunnable &&
+ document.metadata.editable &&
+ document.metadata.runnable;
+
+ // If already trusted, then nothing to do.
+ if (isDocumentEditableAndRunnable && areAllCellsEditableAndRunnable) {
+ return;
+ }
+
+ document.metadata.cellEditable = true;
+ document.metadata.cellRunnable = true;
+ document.metadata.editable = true;
+ document.metadata.runnable = true;
+
+ document.cells.forEach((cell) => {
+ cell.metadata.editable = true;
+ if (cell.cellKind !== vscodeNotebookEnums.CellKind.Markdown) {
+ cell.metadata.runnable = true;
+ }
+
+ // Restore the output once we trust the notebook.
+ const cellModel = findMappedNotebookCellModel(cell, model.cells);
+ if (cellModel) {
+ // tslint:disable-next-line: no-any
+ cell.outputs = createVSCCellOutputsFromOutputs(cellModel.data.outputs as any);
+ }
+ });
+}
+
export function clearCellForExecution(vscCell: NotebookCell, model: VSCodeNotebookModel) {
vscCell.metadata.statusMessage = undefined;
vscCell.metadata.executionOrder = undefined;
diff --git a/src/client/datascience/notebook/helpers/helpers.ts b/src/client/datascience/notebook/helpers/helpers.ts
index 99487d2f94b4..40bfc4085468 100644
--- a/src/client/datascience/notebook/helpers/helpers.ts
+++ b/src/client/datascience/notebook/helpers/helpers.ts
@@ -26,12 +26,12 @@ import { createCodeCell, createMarkdownCell } from '../../../../datascience-ui/c
import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../common/constants';
import { traceError, traceWarning } from '../../../common/logger';
import { CellState, ICell, INotebookModel } from '../../types';
+import { JupyterNotebookView } from '../constants';
import { mapVSCNotebookCellToCellModel } from './cellMappers';
// tslint:disable-next-line: no-var-requires no-require-imports
const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed');
// tslint:disable-next-line: no-require-imports
import cloneDeep = require('lodash/cloneDeep');
-import { JupyterNotebookView } from '../constants';
// This is the custom type we are adding into nbformat.IBaseCellMetadata
interface IBaseCellVSCodeMetadata {
@@ -61,11 +61,11 @@ export function notebookModelToVSCNotebookData(model: INotebookModel): NotebookD
cells,
languages: [defaultLanguage],
metadata: {
- cellEditable: true,
- cellRunnable: true,
- editable: true,
+ cellEditable: model.isTrusted,
+ cellRunnable: model.isTrusted,
+ editable: model.isTrusted,
cellHasExecutionOrder: true,
- runnable: true,
+ runnable: model.isTrusted,
displayOrder: [
'application/vnd.*',
'application/vdom.*',
@@ -172,23 +172,16 @@ export function createVSCNotebookCellDataFromCell(model: INotebookModel, cell: I
runState = vscodeNotebookEnums.NotebookCellRunState.Success;
}
- const notebookCellData: NotebookCellData = {
- cellKind:
- cell.data.cell_type === 'code' ? vscodeNotebookEnums.CellKind.Code : vscodeNotebookEnums.CellKind.Markdown,
- language: cell.data.cell_type === 'code' ? defaultCodeLanguage : MARKDOWN_LANGUAGE,
- metadata: {
- editable: true,
- executionOrder: typeof cell.data.execution_count === 'number' ? cell.data.execution_count : undefined,
- hasExecutionOrder: cell.data.cell_type === 'code',
- runState,
- runnable: cell.data.cell_type === 'code'
- },
- source: concatMultilineStringInput(cell.data.source),
- outputs
+ const notebookCellMetadata: NotebookCellMetadata = {
+ editable: model.isTrusted,
+ executionOrder: typeof cell.data.execution_count === 'number' ? cell.data.execution_count : undefined,
+ hasExecutionOrder: cell.data.cell_type === 'code',
+ runState,
+ runnable: cell.data.cell_type === 'code' && model.isTrusted
};
if (statusMessage) {
- notebookCellData.metadata.statusMessage = statusMessage;
+ notebookCellMetadata.statusMessage = statusMessage;
}
const vscodeMetadata = (cell.data.metadata.vscode as unknown) as IBaseCellVSCodeMetadata | undefined;
const startExecutionTime = vscodeMetadata?.start_execution_time
@@ -199,12 +192,27 @@ export function createVSCNotebookCellDataFromCell(model: INotebookModel, cell: I
: undefined;
if (startExecutionTime && typeof endExecutionTime === 'number') {
- notebookCellData.metadata.runStartTime = startExecutionTime;
- notebookCellData.metadata.lastRunDuration = endExecutionTime - startExecutionTime;
+ notebookCellMetadata.runStartTime = startExecutionTime;
+ notebookCellMetadata.lastRunDuration = endExecutionTime - startExecutionTime;
}
- updateVSCNotebookCellMetadata(notebookCellData.metadata, cell);
- return notebookCellData;
+ updateVSCNotebookCellMetadata(notebookCellMetadata, cell);
+
+ // If not trusted, then clear the output in VSC Cell.
+ // At this point we have the original output in the ICell.
+ if (!model.isTrusted) {
+ while (outputs.length) {
+ outputs.shift();
+ }
+ }
+ return {
+ cellKind:
+ cell.data.cell_type === 'code' ? vscodeNotebookEnums.CellKind.Code : vscodeNotebookEnums.CellKind.Markdown,
+ language: cell.data.cell_type === 'code' ? defaultCodeLanguage : MARKDOWN_LANGUAGE,
+ metadata: notebookCellMetadata,
+ source: concatMultilineStringInput(cell.data.source),
+ outputs
+ };
}
export function createVSCCellOutputsFromOutputs(outputs?: nbformat.IOutput[]): CellOutput[] {
diff --git a/src/client/datascience/notebook/integration.ts b/src/client/datascience/notebook/integration.ts
index 2ba2c55ff379..e9a073f1c2bf 100644
--- a/src/client/datascience/notebook/integration.ts
+++ b/src/client/datascience/notebook/integration.ts
@@ -2,17 +2,16 @@
// Licensed under the MIT License.
'use strict';
import { inject, injectable } from 'inversify';
-import * as path from 'path';
+import { ConfigurationTarget } from 'vscode';
import { IExtensionSingleActivationService } from '../../activation/types';
import {
IApplicationEnvironment,
IApplicationShell,
- ICommandManager,
- IVSCodeNotebook
+ IVSCodeNotebook,
+ IWorkspaceService
} from '../../common/application/types';
import { NotebookEditorSupport } from '../../common/experiments/groups';
import { traceError } from '../../common/logger';
-import { IFileSystem } from '../../common/platform/types';
import { IDisposableRegistry, IExperimentsManager, IExtensionContext } from '../../common/types';
import { DataScience } from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
@@ -21,6 +20,8 @@ import { NotebookKernel } from './notebookKernel';
import { NotebookOutputRenderer } from './renderer';
import { INotebookContentProvider } from './types';
+const EditorAssociationUpdatedKey = 'EditorAssociationUpdatedToUseNotebooks';
+
/**
* This class basically registers the necessary providers and the like with VSC.
* I.e. this is where we integrate our stuff with VS Code via their extension endpoints.
@@ -33,36 +34,23 @@ export class NotebookIntegration implements IExtensionSingleActivationService {
@inject(IExperimentsManager) private readonly experiment: IExperimentsManager,
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
@inject(INotebookContentProvider) private readonly notebookContentProvider: INotebookContentProvider,
- @inject(IExtensionContext) private readonly context: IExtensionContext,
- @inject(IFileSystem) private readonly fs: IFileSystem,
- @inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(NotebookKernel) private readonly notebookKernel: NotebookKernel,
@inject(NotebookOutputRenderer) private readonly renderer: NotebookOutputRenderer,
@inject(IApplicationEnvironment) private readonly env: IApplicationEnvironment,
- @inject(IApplicationShell) private readonly shell: IApplicationShell
+ @inject(IApplicationShell) private readonly shell: IApplicationShell,
+ @inject(IWorkspaceService) private readonly workspace: IWorkspaceService,
+ @inject(IExtensionContext) private readonly extensionContext: IExtensionContext
) {}
- public get isEnabled() {
- const packageJsonFile = path.join(this.context.extensionPath, 'package.json');
- const content = JSON.parse(this.fs.readFileSync(packageJsonFile));
-
- // This code is temporary.
- return (
- content.enableProposedApi &&
- Array.isArray(content.contributes.notebookOutputRenderer) &&
- (content.contributes.notebookOutputRenderer as []).length > 0 &&
- Array.isArray(content.contributes.notebookProvider) &&
- (content.contributes.notebookProvider as []).length > 0
- );
- }
- public async enableSideBySideUsage() {
- await this.enableNotebooks(false);
- }
public async activate(): Promise {
// This condition is temporary.
// If user belongs to the experiment, then make the necessary changes to package.json.
// Once the API is final, we won't need to modify the package.json.
if (this.experiment.inExperiment(NotebookEditorSupport.nativeNotebookExperiment)) {
- await this.enableNotebooks(true);
+ await this.enableNotebooks();
+ } else {
+ // Possible user was in experiment, then they opted out. In this case we need to revert the changes made to the settings file.
+ // Again, this is temporary code.
+ await this.disableNotebooks();
}
if (this.env.channel !== 'insiders') {
return;
@@ -108,62 +96,61 @@ export class NotebookIntegration implements IExtensionSingleActivationService {
}
}
}
- private async enableNotebooks(useVSCodeNotebookAsDefaultEditor: boolean) {
+ private async enableNotebooks() {
if (this.env.channel === 'stable') {
this.shell.showErrorMessage(DataScience.previewNotebookOnlySupportedInVSCInsiders()).then(noop, noop);
return;
}
- const packageJsonFile = path.join(this.context.extensionPath, 'package.json');
- const content = JSON.parse(this.fs.readFileSync(packageJsonFile));
+ await this.enableDisableEditorAssociation(true);
+ }
+ private async enableDisableEditorAssociation(enable: boolean) {
// This code is temporary.
+ const settings = this.workspace.getConfiguration('workbench', undefined);
+ const editorAssociations = settings.get('editorAssociations') as {
+ viewType: string;
+ filenamePattern: string;
+ }[];
+
+ // Update the settings.
if (
- !content.enableProposedApi ||
- !Array.isArray(content.contributes.notebookOutputRenderer) ||
- !Array.isArray(content.contributes.notebookProvider)
+ enable &&
+ (!Array.isArray(editorAssociations) ||
+ editorAssociations.length === 0 ||
+ !editorAssociations.find((item) => item.viewType === JupyterNotebookView))
) {
- content.enableProposedApi = true;
- content.contributes.notebookOutputRenderer = [
- {
- viewType: 'jupyter-notebook-renderer',
- displayName: 'Jupyter Notebook Renderer',
- mimeTypes: [
- 'application/geo+json',
- 'application/vdom.v1+json',
- 'application/vnd.dataresource+json',
- 'application/vnd.plotly.v1+json',
- 'application/vnd.vega.v2+json',
- 'application/vnd.vega.v3+json',
- 'application/vnd.vega.v4+json',
- 'application/vnd.vega.v5+json',
- 'application/vnd.vegalite.v1+json',
- 'application/vnd.vegalite.v2+json',
- 'application/vnd.vegalite.v3+json',
- 'application/vnd.vegalite.v4+json',
- 'application/x-nteract-model-debug+json',
- 'image/gif',
- 'text/latex',
- 'text/vnd.plotly.v1+html'
- ]
- }
- ];
- content.contributes.notebookProvider = [
- {
- viewType: JupyterNotebookView,
- displayName: 'Jupyter Notebook (preview)',
- selector: [
- {
- filenamePattern: '*.ipynb'
- }
- ],
- priority: useVSCodeNotebookAsDefaultEditor ? 'default' : 'option'
- }
- ];
+ editorAssociations.push({
+ viewType: 'jupyter-notebook',
+ filenamePattern: '*.ipynb'
+ });
+ await Promise.all([
+ this.extensionContext.globalState.update(EditorAssociationUpdatedKey, true),
+ settings.update('editorAssociations', editorAssociations, ConfigurationTarget.Global)
+ ]);
+ }
- await this.fs.writeFile(packageJsonFile, JSON.stringify(content, undefined, 4));
- await this.commandManager
- .executeCommand('python.reloadVSCode', DataScience.reloadVSCodeNotebookEditor())
- .then(noop, noop);
+ // Revert the settings.
+ if (
+ !enable &&
+ this.extensionContext.globalState.get(EditorAssociationUpdatedKey, false) &&
+ Array.isArray(editorAssociations) &&
+ editorAssociations.find((item) => item.viewType === JupyterNotebookView)
+ ) {
+ const updatedSettings = editorAssociations.filter((item) => item.viewType !== JupyterNotebookView);
+ await Promise.all([
+ this.extensionContext.globalState.update(EditorAssociationUpdatedKey, false),
+ settings.update('editorAssociations', updatedSettings, ConfigurationTarget.Global)
+ ]);
+ }
+ }
+ private async disableNotebooks() {
+ if (this.env.channel === 'stable') {
+ return;
+ }
+ // If we never modified the settings, then nothing to do.
+ if (!this.extensionContext.globalState.get(EditorAssociationUpdatedKey, false)) {
+ return;
}
+ await this.enableDisableEditorAssociation(false);
}
}
diff --git a/src/client/datascience/notebook/notebookEditorProvider.ts b/src/client/datascience/notebook/notebookEditorProvider.ts
index a5a88521b6d2..bfe618d46d71 100644
--- a/src/client/datascience/notebook/notebookEditorProvider.ts
+++ b/src/client/datascience/notebook/notebookEditorProvider.ts
@@ -28,7 +28,6 @@ import { JupyterNotebookView } from './constants';
import { mapVSCNotebookCellsToNotebookCellModels } from './helpers/cellMappers';
import { updateCellModelWithChangesToVSCCell } from './helpers/cellUpdateHelpers';
import { isJupyterNotebook } from './helpers/helpers';
-import { NotebookIntegration } from './integration';
import { NotebookEditor } from './notebookEditor';
import { INotebookContentProvider, INotebookExecutionService } from './types';
@@ -85,13 +84,6 @@ export class NotebookEditorProvider implements INotebookEditorProvider {
if (uri) {
setSharedProperty('ds_notebookeditor', 'native');
captureTelemetry(Telemetry.OpenNotebook, { scope: 'command' }, false);
- const integration = serviceContainer.get(NotebookIntegration);
- // If user is not meant to be using VSC Notebooks, and it is not enabled,
- // then enable it for side by side usage.
- if (!integration.isEnabled && !useVSCodeNotebookEditorApi) {
- // At this point we need to reload VS Code, hence return and do not try to load nb, else it will fail.
- return integration.enableSideBySideUsage();
- }
this.open(uri).ignoreErrors();
}
})
@@ -160,7 +152,7 @@ export class NotebookEditorProvider implements INotebookEditorProvider {
return;
}
const uri = doc.uri;
- const model = await this.storage.load(uri, undefined, undefined, true);
+ const model = await this.storage.get(uri, undefined, undefined, true);
mapVSCNotebookCellsToNotebookCellModels(doc, model);
// In open method we might be waiting.
let editor = this.notebookEditorsByUri.get(uri.toString());
@@ -188,6 +180,9 @@ export class NotebookEditorProvider implements INotebookEditorProvider {
const deferred = this.notebooksWaitingToBeOpenedByUri.get(uri.toString())!;
deferred.resolve(editor);
this.notebookEditorsByUri.set(uri.toString(), editor);
+ if (!model.isTrusted) {
+ await this.commandManager.executeCommand(Commands.TrustNotebook, model.file);
+ }
}
private onDidChangeActiveVsCodeNotebookEditor(editor: VSCodeNotebookEditor | undefined) {
if (!editor) {
@@ -237,7 +232,7 @@ export class NotebookEditorProvider implements INotebookEditorProvider {
if (!isJupyterNotebook(e.document)) {
return;
}
- const model = await this.storage.load(e.document.uri, undefined, undefined, true);
+ const model = await this.storage.get(e.document.uri, undefined, undefined, true);
if (!(model instanceof VSCodeNotebookModel)) {
throw new Error('NotebookModel not of type VSCodeNotebookModel');
}
diff --git a/src/client/datascience/notebook/notebookTrustHandler.ts b/src/client/datascience/notebook/notebookTrustHandler.ts
new file mode 100644
index 000000000000..06b2a864ef33
--- /dev/null
+++ b/src/client/datascience/notebook/notebookTrustHandler.ts
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+'use strict';
+
+import { inject, injectable } from 'inversify';
+import { IExtensionSingleActivationService } from '../../activation/types';
+import { IVSCodeNotebook } from '../../common/application/types';
+import { IFileSystem } from '../../common/platform/types';
+import { IDisposableRegistry } from '../../common/types';
+import { INotebookEditorProvider, ITrustService } from '../types';
+import { updateVSCNotebookAfterTrustingNotebook } from './helpers/cellUpdateHelpers';
+import { isJupyterNotebook } from './helpers/helpers';
+
+@injectable()
+export class NotebookTrustHandler implements IExtensionSingleActivationService {
+ constructor(
+ @inject(ITrustService) private readonly trustService: ITrustService,
+ @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook,
+ @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider,
+ @inject(IFileSystem) private readonly fs: IFileSystem,
+ @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry
+ ) {}
+ public async activate(): Promise {
+ this.trustService.onDidSetNotebookTrust(this.onDidTrustNotebook, this, this.disposables);
+ }
+ private onDidTrustNotebook() {
+ this.vscNotebook.notebookDocuments.forEach((doc) => {
+ if (!isJupyterNotebook(doc)) {
+ return;
+ }
+ const editor = this.editorProvider.editors.find((e) => this.fs.arePathsSame(e.file.fsPath, doc.uri.fsPath));
+ if (editor && editor.model?.isTrusted) {
+ updateVSCNotebookAfterTrustingNotebook(doc, editor.model);
+ }
+ });
+ }
+}
diff --git a/src/client/datascience/notebook/serviceRegistry.ts b/src/client/datascience/notebook/serviceRegistry.ts
index d8b1cea92b4f..6e53197efcb7 100644
--- a/src/client/datascience/notebook/serviceRegistry.ts
+++ b/src/client/datascience/notebook/serviceRegistry.ts
@@ -10,6 +10,7 @@ import { NotebookContentProvider } from './contentProvider';
import { NotebookExecutionService } from './executionService';
import { NotebookIntegration } from './integration';
import { NotebookKernel } from './notebookKernel';
+import { NotebookTrustHandler } from './notebookTrustHandler';
import { NotebookOutputRenderer } from './renderer';
import { NotebookSurveyBanner, NotebookSurveyDataLogger } from './survey';
import { INotebookContentProvider, INotebookExecutionService } from './types';
@@ -33,4 +34,8 @@ export function registerTypes(serviceManager: IServiceManager) {
IExtensionSingleActivationService,
NotebookSurveyDataLogger
);
+ serviceManager.addSingleton(
+ IExtensionSingleActivationService,
+ NotebookTrustHandler
+ );
}
diff --git a/src/client/datascience/notebook/survey.ts b/src/client/datascience/notebook/survey.ts
index 8a631c97ab4e..aabac9de00b6 100644
--- a/src/client/datascience/notebook/survey.ts
+++ b/src/client/datascience/notebook/survey.ts
@@ -81,6 +81,11 @@ export class NotebookSurveyBanner {
if (!this.enabled || this.disabledInCurrentSession) {
return false;
}
+ const currentDate = new Date();
+ if (currentDate.getMonth() < 7 && currentDate.getFullYear() <= 2020) {
+ return false;
+ }
+
const data = this.persistentState.createGlobalPersistentState(storageKey, {});
const totalActionsInPreviousSessions =
diff --git a/src/client/datascience/notebookStorage/baseModel.ts b/src/client/datascience/notebookStorage/baseModel.ts
index b9a95660b34c..452534c162e5 100644
--- a/src/client/datascience/notebookStorage/baseModel.ts
+++ b/src/client/datascience/notebookStorage/baseModel.ts
@@ -78,6 +78,9 @@ export abstract class BaseNotebookModel implements INotebookModel {
case 'version':
changed = this.updateVersionInfo(change.interpreter, change.kernelSpec);
break;
+ case 'updateTrust':
+ this._isTrusted = change.isNotebookTrusted;
+ break;
default:
break;
}
@@ -193,12 +196,8 @@ export abstract class BaseNotebookModel implements INotebookModel {
this.ensureNotebookJson();
// Reuse our original json except for the cells.
- const json = {
- cells: this.cells.map((c) => pruneCell(c.data)),
- metadata: this.notebookJson.metadata,
- nbformat: this.notebookJson.nbformat,
- nbformat_minor: this.notebookJson.nbformat_minor
- };
+ const json = { ...this.notebookJson };
+ json.cells = this.cells.map((c) => pruneCell(c.data));
return JSON.stringify(json, null, this.indentAmount);
}
}
diff --git a/src/client/datascience/notebookStorage/notebookModel.ts b/src/client/datascience/notebookStorage/notebookModel.ts
index 0bbe75705836..3b031870d81d 100644
--- a/src/client/datascience/notebookStorage/notebookModel.ts
+++ b/src/client/datascience/notebookStorage/notebookModel.ts
@@ -90,7 +90,7 @@ export class NativeEditorNotebookModel extends BaseNotebookModel {
// Dirty state comes from undo. At least VS code will track it that way. However
// skip file changes as we don't forward those to VS code
- if (change.kind !== 'save' && change.kind !== 'saveAs') {
+ if (change.kind !== 'save' && change.kind !== 'saveAs' && change.kind !== 'updateTrust') {
this.changeCount += 1;
}
diff --git a/src/client/datascience/progress/progressReporter.ts b/src/client/datascience/progress/progressReporter.ts
index 45098c9a0ff8..87ccadf9f450 100644
--- a/src/client/datascience/progress/progressReporter.ts
+++ b/src/client/datascience/progress/progressReporter.ts
@@ -33,15 +33,15 @@ export class ProgressReporter implements IProgressReporter {
* @returns {(IDisposable & { token: CancellationToken })}
* @memberof ProgressReporter
*/
- public createProgressIndicator(message: string): IDisposable & { token: CancellationToken } {
+ public createProgressIndicator(message: string, cancellable = false): IDisposable & { token: CancellationToken } {
const cancellation = new CancellationTokenSource();
const deferred = createDeferred();
- const options = { location: ProgressLocation.Notification, cancellable: false, title: message };
+ const options = { location: ProgressLocation.Notification, cancellable: cancellable, title: message };
this.appShell
.withProgress(options, async (progress, cancelToken) => {
cancelToken.onCancellationRequested(() => {
- if (!cancelToken.isCancellationRequested) {
+ if (cancelToken.isCancellationRequested) {
cancellation.cancel();
}
deferred.resolve();
diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts
index 8fa61793f59b..31bfd9a717b3 100644
--- a/src/client/datascience/serviceRegistry.ts
+++ b/src/client/datascience/serviceRegistry.ts
@@ -66,6 +66,7 @@ import { NativeEditorStorage } from './interactive-ipynb/nativeEditorStorage';
import { NativeEditorSynchronizer } from './interactive-ipynb/nativeEditorSynchronizer';
import { NativeEditorViewTracker } from './interactive-ipynb/nativeEditorViewTracker';
import { INotebookStorageProvider, NotebookStorageProvider } from './interactive-ipynb/notebookStorageProvider';
+import { TrustCommandHandler } from './interactive-ipynb/trustCommandHandler';
import { TrustService } from './interactive-ipynb/trustService';
import { InteractiveWindow } from './interactive-window/interactiveWindow';
import { InteractiveWindowCommandListener } from './interactive-window/interactiveWindowCommandListener';
@@ -246,6 +247,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton(IExtensionSingleActivationService, ServerPreload);
serviceManager.addSingleton(IExtensionSingleActivationService, NativeEditorViewTracker);
serviceManager.addSingleton(IExtensionSingleActivationService, NotebookUsageTracker);
+ serviceManager.addSingleton(IExtensionSingleActivationService, TrustCommandHandler);
serviceManager.addSingleton(IInteractiveWindowListener, DataScienceSurveyBannerLogger);
serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider);
serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger, undefined, [ICellHashListener]);
diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts
index 65259318741f..d2b4142ee7f6 100644
--- a/src/client/datascience/types.ts
+++ b/src/client/datascience/types.ts
@@ -1043,8 +1043,8 @@ export interface INotebookStorage {
save(model: INotebookModel, cancellation: CancellationToken): Promise;
saveAs(model: INotebookModel, targetResource: Uri): Promise;
backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string): Promise;
- load(file: Uri, contents?: string, backupId?: string, forVSCodeNotebook?: boolean): Promise;
- load(
+ get(file: Uri, contents?: string, backupId?: string, forVSCodeNotebook?: boolean): Promise;
+ get(
file: Uri,
contents?: string,
// tslint:disable-next-line: unified-signatures
diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts
index 55645f90f220..dfc792c48e47 100644
--- a/src/client/languageServices/proposeLanguageServerBanner.ts
+++ b/src/client/languageServices/proposeLanguageServerBanner.ts
@@ -4,22 +4,27 @@
'use strict';
import { inject, injectable } from 'inversify';
-import { ConfigurationTarget } from 'vscode';
import { LanguageServerType } from '../activation/types';
-import { IApplicationShell } from '../common/application/types';
+import { IApplicationEnvironment, IApplicationShell } from '../common/application/types';
+import { PYLANCE_EXTENSION_ID } from '../common/constants';
+import { TryPylance } from '../common/experiments/groups';
import '../common/extensions';
-import { IConfigurationService, IPersistentStateFactory, IPythonExtensionBanner } from '../common/types';
-import { getRandomBetween } from '../common/utils/random';
+import {
+ IConfigurationService,
+ IExperimentService,
+ IExtensions,
+ IPersistentStateFactory,
+ IPythonExtensionBanner
+} from '../common/types';
+import { LanguageService } from '../common/utils/localize';
-// persistent state names, exported to make use of in testing
-export enum ProposeLSStateKeys {
- ShowBanner = 'ProposeLSBanner'
+export function getPylanceExtensionUri(appEnv: IApplicationEnvironment): string {
+ return `${appEnv.uriScheme}:extension/${PYLANCE_EXTENSION_ID}`;
}
-enum ProposeLSLabelIndex {
- Yes,
- No,
- Later
+// persistent state names, exported to make use of in testing
+export enum ProposeLSStateKeys {
+ ShowBanner = 'TryPylanceBanner'
}
/*
@@ -30,46 +35,24 @@ and will show as soon as it is instructed to do so, if a random sample
function enables the popup for this user.
*/
@injectable()
-export class ProposeLanguageServerBanner implements IPythonExtensionBanner {
- private initialized?: boolean;
+export class ProposePylanceBanner implements IPythonExtensionBanner {
private disabledInCurrentSession: boolean = false;
- private sampleSizePerHundred: number;
- private bannerMessage: string =
- 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.';
- private bannerLabels: string[] = ['Try it now', 'No thanks', 'Remind me Later'];
constructor(
@inject(IApplicationShell) private appShell: IApplicationShell,
+ @inject(IApplicationEnvironment) private appEnv: IApplicationEnvironment,
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
@inject(IConfigurationService) private configuration: IConfigurationService,
- sampleSizePerOneHundredUsers: number = 10
- ) {
- this.sampleSizePerHundred = sampleSizePerOneHundredUsers;
- this.initialize();
- }
-
- public initialize() {
- if (this.initialized) {
- return;
- }
- this.initialized = true;
-
- // Don't even bother adding handlers if banner has been turned off.
- if (!this.enabled) {
- return;
- }
+ @inject(IExperimentService) private experiments: IExperimentService,
+ @inject(IExtensions) readonly extensions: IExtensions
+ ) {}
- // we only want 10% of folks that use Jedi to see this survey.
- const randomSample: number = getRandomBetween(0, 100);
- if (randomSample >= this.sampleSizePerHundred) {
- this.disable().ignoreErrors();
- return;
- }
- }
public get enabled(): boolean {
- // Banner is deactivated.
- return false;
- // return this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, true).value;
+ const lsType = this.configuration.getSettings().languageServer ?? LanguageServerType.Jedi;
+ if (lsType === LanguageServerType.Jedi || lsType === LanguageServerType.Node) {
+ return false;
+ }
+ return this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, true).value;
}
public async showBanner(): Promise {
@@ -82,30 +65,31 @@ export class ProposeLanguageServerBanner implements IPythonExtensionBanner {
return;
}
- const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels);
- switch (response) {
- case this.bannerLabels[ProposeLSLabelIndex.Yes]: {
- await this.enableLanguageServer();
- await this.disable();
- break;
- }
- case this.bannerLabels[ProposeLSLabelIndex.No]: {
- await this.disable();
- break;
- }
- case this.bannerLabels[ProposeLSLabelIndex.Later]: {
- this.disabledInCurrentSession = true;
- break;
- }
- default: {
- // Disable for the current session.
- this.disabledInCurrentSession = true;
- }
+ const response = await this.appShell.showInformationMessage(
+ LanguageService.proposePylanceMessage(),
+ LanguageService.tryItNow(),
+ LanguageService.bannerLabelNo(),
+ LanguageService.remindMeLater()
+ );
+
+ if (response === LanguageService.tryItNow()) {
+ this.appShell.openUrl(getPylanceExtensionUri(this.appEnv));
+ await this.disable();
+ } else if (response === LanguageService.bannerLabelNo()) {
+ await this.disable();
+ } else {
+ this.disabledInCurrentSession = true;
}
}
public async shouldShowBanner(): Promise {
- return Promise.resolve(this.enabled && !this.disabledInCurrentSession);
+ // Do not prompt if Pylance is already installed.
+ if (this.extensions.getExtension(PYLANCE_EXTENSION_ID)) {
+ return false;
+ }
+ // Only prompt for users in experiment.
+ const inExperiment = await this.experiments.inExperiment(TryPylance.experiment);
+ return inExperiment && this.enabled && !this.disabledInCurrentSession;
}
public async disable(): Promise {
@@ -113,13 +97,4 @@ export class ProposeLanguageServerBanner implements IPythonExtensionBanner {
.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, false)
.updateValue(false);
}
-
- public async enableLanguageServer(): Promise {
- await this.configuration.updateSetting(
- 'languageServer',
- LanguageServerType.Microsoft,
- undefined,
- ConfigurationTarget.Global
- );
- }
}
diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts
index c72a700f03f7..add675bb2e76 100644
--- a/src/client/providers/jediProxy.ts
+++ b/src/client/providers/jediProxy.ts
@@ -7,19 +7,13 @@ import * as path from 'path';
// @ts-ignore
import * as pidusage from 'pidusage';
import { CancellationToken, CancellationTokenSource, CompletionItemKind, Disposable, SymbolKind, Uri } from 'vscode';
-import { isTestExecution } from '../common/constants';
import '../common/extensions';
import { IS_WINDOWS } from '../common/platform/constants';
import { IFileSystem } from '../common/platform/types';
import * as internalPython from '../common/process/internal/python';
import * as internalScripts from '../common/process/internal/scripts';
import { IPythonExecutionFactory } from '../common/process/types';
-import {
- BANNER_NAME_PROPOSE_LS,
- IConfigurationService,
- IPythonExtensionBanner,
- IPythonSettings
-} from '../common/types';
+import { IConfigurationService, IPythonSettings } from '../common/types';
import { createDeferred, Deferred } from '../common/utils/async';
import { swallowExceptions } from '../common/utils/decorators';
import { StopWatch } from '../common/utils/stopWatch';
@@ -157,7 +151,6 @@ export class JediProxy implements Disposable {
private pidUsageFailures = { timer: new StopWatch(), counter: 0 };
private lastCmdIdProcessed?: number;
private lastCmdIdProcessedForPidUsage?: number;
- private proposeNewLanguageServerPopup: IPythonExtensionBanner;
private readonly disposables: Disposable[] = [];
private timer?: NodeJS.Timer | number;
@@ -174,12 +167,6 @@ export class JediProxy implements Disposable {
this.startLanguageServer()
.then(() => this.initialized.resolve())
.ignoreErrors();
-
- this.proposeNewLanguageServerPopup = serviceContainer.get(
- IPythonExtensionBanner,
- BANNER_NAME_PROPOSE_LS
- );
-
this.checkJediMemoryFootprint().ignoreErrors();
}
@@ -332,9 +319,6 @@ export class JediProxy implements Disposable {
private async startLanguageServer(): Promise {
const newAutoComletePaths = await this.buildAutoCompletePaths();
this.additionalAutoCompletePaths = newAutoComletePaths;
- if (!isTestExecution()) {
- await this.proposeNewLanguageServerPopup.showBanner();
- }
return this.restartLanguageServer();
}
private restartLanguageServer(): Promise {
diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts
index 628bad8fa022..15f1241d88ce 100644
--- a/src/client/providers/terminalProvider.ts
+++ b/src/client/providers/terminalProvider.ts
@@ -24,13 +24,15 @@ export class TerminalProvider implements Disposable {
const configuration = this.serviceContainer.get(IConfigurationService);
const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource());
- if (pythonSettings.terminal.activateEnvInCurrentTerminal) {
- if (currentTerminal) {
+ if (currentTerminal && pythonSettings.terminal.activateEnvInCurrentTerminal) {
+ const hideFromUser =
+ 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser;
+ if (!hideFromUser) {
const terminalActivator = this.serviceContainer.get(ITerminalActivator);
await terminalActivator.activateEnvironmentInTerminal(currentTerminal, { preserveFocus: true });
}
sendTelemetryEvent(EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL, undefined, {
- isTerminalVisible: currentTerminal ? true : false
+ isTerminalVisible: !hideFromUser
});
}
}
diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts
index 0f76ead0ec48..6be34d0f427e 100644
--- a/src/client/telemetry/index.ts
+++ b/src/client/telemetry/index.ts
@@ -2041,6 +2041,12 @@ export interface IEventNamePropertyMapping {
*/
result: 'err' | 'script' | 'notebook' | 'unavailable';
};
+ [Telemetry.GatherStats]: {
+ linesSubmitted: number;
+ cellsSubmitted: number;
+ linesGathered: number;
+ cellsGathered: number;
+ };
/**
* Telemetry event sent when a gathered notebook has been saved by the user.
*/
@@ -2192,10 +2198,20 @@ export interface IEventNamePropertyMapping {
[Telemetry.StartPageOpenFileBrowser]: never | undefined;
[Telemetry.StartPageOpenFolder]: never | undefined;
[Telemetry.StartPageOpenWorkspace]: never | undefined;
+
+ // Run by line events
[Telemetry.RunByLineStart]: never | undefined;
[Telemetry.RunByLineStep]: never | undefined;
[Telemetry.RunByLineStop]: never | undefined;
[Telemetry.RunByLineVariableHover]: never | undefined;
+
+ // Trusted notebooks events
+ [Telemetry.NotebookTrustPromptShown]: never | undefined;
+ [Telemetry.TrustNotebook]: never | undefined;
+ [Telemetry.TrustAllNotebooks]: never | undefined;
+ [Telemetry.DoNotTrustNotebook]: never | undefined;
+
+ // Native notebooks events
[VSCodeNativeTelemetry.AddCell]: never | undefined;
[VSCodeNativeTelemetry.DeleteCell]: never | undefined;
[VSCodeNativeTelemetry.MoveCell]: never | undefined;
diff --git a/src/datascience-ui/history-react/interactiveCell.tsx b/src/datascience-ui/history-react/interactiveCell.tsx
index b9e38b64fa1f..558258ebb099 100644
--- a/src/datascience-ui/history-react/interactiveCell.tsx
+++ b/src/datascience-ui/history-react/interactiveCell.tsx
@@ -189,6 +189,7 @@ export class InteractiveCell extends React.Component {
onClick={gatherCode}
hidden={
this.props.cellVM.cell.state === CellState.error ||
+ this.props.cellVM.cell.state === CellState.executing ||
this.props.cellVM.cell.data.cell_type === 'markdown' ||
!this.props.settings.gatherIsInstalled
}
diff --git a/src/datascience-ui/interactive-common/trustMessage.tsx b/src/datascience-ui/interactive-common/trustMessage.tsx
index a5d55993082b..c97a97e6c6cb 100644
--- a/src/datascience-ui/interactive-common/trustMessage.tsx
+++ b/src/datascience-ui/interactive-common/trustMessage.tsx
@@ -22,7 +22,9 @@ export class TrustMessage extends React.PureComponent {
};
const dynamicStyle: React.CSSProperties = {
maxWidth: getMaxWidth(textSize),
- color: this.props.isNotebookTrusted ? undefined : 'var(--vscode-editorError-foreground)',
+ color: this.props.isNotebookTrusted
+ ? 'var(--vscode-editor-foreground)'
+ : 'var(--vscode-editorError-foreground)',
cursor: this.props.isNotebookTrusted ? undefined : 'pointer'
};
diff --git a/src/datascience-ui/native-editor/addCellLine.tsx b/src/datascience-ui/native-editor/addCellLine.tsx
index 8f275cc072ce..f5513aafecc9 100644
--- a/src/datascience-ui/native-editor/addCellLine.tsx
+++ b/src/datascience-ui/native-editor/addCellLine.tsx
@@ -25,19 +25,23 @@ export class AddCellLine extends React.Component {
const plus = this.props.includePlus ? (
) : null;
+ const disabled = !this.props.isNotebookTrusted;
+ const innerFilter = disabled ? 'image-button-inner-disabled-filter' : '';
return (
- {plus}
-
+
+ {plus}
+
+
);
diff --git a/src/datascience-ui/native-editor/nativeCell.tsx b/src/datascience-ui/native-editor/nativeCell.tsx
index 39f97b3f7090..cda8de9462b0 100644
--- a/src/datascience-ui/native-editor/nativeCell.tsx
+++ b/src/datascience-ui/native-editor/nativeCell.tsx
@@ -281,6 +281,9 @@ export class NativeCell extends React.Component {
// tslint:disable-next-line: cyclomatic-complexity max-func-body-length
private keyDownInput = (cellId: string, e: IKeyboardEvent) => {
+ if (!this.isNotebookTrusted() && !isCellNavigationKeyboardEvent(e)) {
+ return;
+ }
const isFocusedWhenNotSuggesting = this.isFocused() && e.editorInfo && !e.editorInfo.isSuggesting;
switch (e.code) {
case 'ArrowUp':
@@ -595,6 +598,7 @@ export class NativeCell extends React.Component {
this.props.cellVM.cell.data.execution_count === null ||
this.props.cellVM.hasBeenRun === null ||
this.props.cellVM.hasBeenRun === false ||
+ this.props.cellVM.cell.state === CellState.executing ||
this.isError() ||
this.isMarkdownCell() ||
!this.props.gatherIsInstalled;
@@ -886,3 +890,14 @@ export class NativeCell extends React.Component {
export function getConnectedNativeCell() {
return connect(null, actionCreators)(NativeCell);
}
+
+function isCellNavigationKeyboardEvent(e: IKeyboardEvent) {
+ return (
+ ((e.code === 'Enter' || e.code === 'NumpadEnter') && !e.shiftKey && !e.ctrlKey && !e.altKey) ||
+ e.code === 'ArrowUp' ||
+ e.code === 'k' ||
+ e.code === 'ArrowDown' ||
+ e.code === 'j' ||
+ e.code === 'Escape'
+ );
+}
diff --git a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg
index ffddd4cbdb95..11e1f39ad968 100644
--- a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg
+++ b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg
@@ -1,4 +1,4 @@
-
-
+
+
diff --git a/src/datascience-ui/startPage/startPage.css b/src/datascience-ui/startPage/startPage.css
index de64fbe8538a..035610dcebf8 100644
--- a/src/datascience-ui/startPage/startPage.css
+++ b/src/datascience-ui/startPage/startPage.css
@@ -53,6 +53,12 @@
white-space: nowrap;
}
+.releaseNotesRow {
+ display: block;
+ min-height: 50px;
+ white-space: nowrap;
+}
+
.link {
display: inline;
color: var(--vscode-debugIcon-continueForeground);
diff --git a/src/datascience-ui/startPage/startPage.tsx b/src/datascience-ui/startPage/startPage.tsx
index 84f1ad076062..3794c22919fe 100644
--- a/src/datascience-ui/startPage/startPage.tsx
+++ b/src/datascience-ui/startPage/startPage.tsx
@@ -4,7 +4,7 @@
import * as React from 'react';
import '../../client/common/extensions';
-import { IReleaseNotesPackage, IStartPageMapping, StartPageMessages } from '../../client/common/startPage/types';
+import { ISettingPackage, IStartPageMapping, StartPageMessages } from '../../client/common/startPage/types';
import { Image, ImageName } from '../react-common/image';
import { getLocString } from '../react-common/locReactSide';
import { IMessageHandler, PostOffice } from '../react-common/postOffice';
@@ -19,8 +19,7 @@ export interface IStartPageProps {
// Front end of the Python extension start page.
// In general it consists of its render method and methods that send and receive messages.
export class StartPage extends React.Component implements IMessageHandler {
- private releaseNotes: IReleaseNotesPackage = {
- notes: [],
+ private releaseNotes: ISettingPackage = {
showAgainSetting: false
};
private postOffice: PostOffice = new PostOffice();
@@ -30,7 +29,7 @@ export class StartPage extends React.Component implements IMess
}
public componentDidMount() {
- this.postOffice.sendMessage(StartPageMessages.RequestReleaseNotesAndShowAgainSetting);
+ this.postOffice.sendMessage(StartPageMessages.RequestShowAgainSetting);
}
// tslint:disable: no-any
@@ -129,9 +128,8 @@ export class StartPage extends React.Component implements IMess
{this.renderInteractiveWindowDescription()}
-
+
{this.renderReleaseNotesLink()}
- {this.renderReleaseNotes()}
{this.renderTutorialAndDoc()}
@@ -151,8 +149,7 @@ export class StartPage extends React.Component
implements IMess
// tslint:disable-next-line: no-any
public handleMessage = (msg: string, payload?: any) => {
- if (msg === StartPageMessages.SendReleaseNotes) {
- this.releaseNotes.notes = payload.notes;
+ if (msg === StartPageMessages.SendSetting) {
this.releaseNotes.showAgainSetting = payload.showAgainSetting;
this.setState({});
}
@@ -240,21 +237,13 @@ export class StartPage extends React.Component implements IMess
dangerouslySetInnerHTML={{
__html: getLocString(
'StartPage.releaseNotes',
- 'Take a look at our Release Notes to learn more about the latest features'
+ 'Take a look at our Release Notes to learn more about the latest features.'
).format('https://aka.ms/AA8dxtb')
}}
/>
);
}
- private renderReleaseNotes(): JSX.Element {
- const notes: JSX.Element[] = [];
- this.releaseNotes.notes.forEach((rel, index) => {
- notes.push({rel} );
- });
- return ;
- }
-
private renderTutorialAndDoc(): JSX.Element {
// tslint:disable: react-no-dangerous-html
return (
diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts
index ae0c34aa5a6d..fea8949e050e 100644
--- a/src/test/activation/activationService.unit.test.ts
+++ b/src/test/activation/activationService.unit.test.ts
@@ -16,13 +16,11 @@ import {
import { LSNotSupportedDiagnosticServiceId } from '../../client/application/diagnostics/checks/lsNotSupported';
import { IDiagnostic, IDiagnosticsService } from '../../client/application/diagnostics/types';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types';
-import { LSControl, LSEnabled } from '../../client/common/experiments/groups';
import { IPlatformService } from '../../client/common/platform/types';
import {
IConfigurationService,
IDisposable,
IDisposableRegistry,
- IExperimentsManager,
IOutputChannel,
IPersistentState,
IPersistentStateFactory,
@@ -53,7 +51,6 @@ suite('Language Server Activation - ActivationService', () => {
let lsNotSupportedDiagnosticService: TypeMoq.IMock;
let stateFactory: TypeMoq.IMock;
let state: TypeMoq.IMock>;
- let experiments: TypeMoq.IMock;
let workspaceConfig: TypeMoq.IMock;
let interpreterService: TypeMoq.IMock;
let interpreterChangedHandler!: Function;
@@ -67,7 +64,6 @@ suite('Language Server Activation - ActivationService', () => {
state = TypeMoq.Mock.ofType>();
const configService = TypeMoq.Mock.ofType();
pythonSettings = TypeMoq.Mock.ofType();
- experiments = TypeMoq.Mock.ofType();
const langFolderServiceMock = TypeMoq.Mock.ofType();
const folderVer: FolderVersionPair = {
path: '',
@@ -179,16 +175,10 @@ suite('Language Server Activation - ActivationService', () => {
.returns(() => activator.object)
.verifiable(TypeMoq.Times.once());
- experiments
- .setup((ex) => ex.inExperiment(TypeMoq.It.isAny()))
- .returns(() => false)
- .verifiable(TypeMoq.Times.never());
-
await activationService.activate(undefined);
activator.verifyAll();
serviceContainer.verifyAll();
- experiments.verifyAll();
}
async function testReloadMessage(settingName: string): Promise {
@@ -205,8 +195,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
workspaceService.verifyAll();
@@ -244,8 +233,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await testActivation(activationService, activator, true);
@@ -255,8 +243,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await testActivation(activationService, activator, false);
@@ -267,8 +254,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await testActivation(activationService, activator);
@@ -278,8 +264,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await testActivation(activationService, activator);
@@ -294,8 +279,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await testActivation(activationService, activator, false, LanguageServerType.None);
});
@@ -316,8 +300,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
workspaceService.verifyAll();
@@ -392,8 +375,7 @@ suite('Language Server Activation - ActivationService', () => {
pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft);
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
const ls1 = await activationService.get(folder1.uri, interpreter1);
const ls2 = await activationService.get(folder1.uri, interpreter2);
@@ -463,8 +445,7 @@ suite('Language Server Activation - ActivationService', () => {
pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft);
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await activationService.activate(folder1.uri);
await interpreterChangedHandler();
@@ -486,8 +467,7 @@ suite('Language Server Activation - ActivationService', () => {
const activatorJedi = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
const diagnostics: IDiagnostic[] = [];
lsNotSupportedDiagnosticService
@@ -562,17 +542,12 @@ suite('Language Server Activation - ActivationService', () => {
.setup((w) => w.getWorkspaceFolderIdentifier(resource, ''))
.returns(() => resource!.fsPath)
.verifiable(TypeMoq.Times.atLeastOnce());
- experiments
- .setup((ex) => ex.inExperiment(TypeMoq.It.isAny()))
- .returns(() => false)
- .verifiable(TypeMoq.Times.never());
await activationService.activate(resource);
activator.verifyAll();
serviceContainer.verifyAll();
workspaceService.verifyAll();
- experiments.verifyAll();
}
test('Activator is disposed if activated workspace is removed and LS is "Microsoft"', async () => {
pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft);
@@ -584,8 +559,7 @@ suite('Language Server Activation - ActivationService', () => {
.verifiable(TypeMoq.Times.once());
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
workspaceService.verifyAll();
expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set');
@@ -624,8 +598,7 @@ suite('Language Server Activation - ActivationService', () => {
const activator1 = TypeMoq.Mock.ofType();
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 };
const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 };
@@ -642,15 +615,10 @@ suite('Language Server Activation - ActivationService', () => {
.setup((a) => a.start(folder1.uri, undefined))
.returns(() => Promise.resolve())
.verifiable(TypeMoq.Times.once());
- experiments
- .setup((ex) => ex.inExperiment(TypeMoq.It.isAny()))
- .returns(() => false)
- .verifiable(TypeMoq.Times.never());
await activationService.activate(folder1.uri);
activator1.verifyAll();
activator1.verify((a) => a.activate(), TypeMoq.Times.once());
serviceContainer.verifyAll();
- experiments.verifyAll();
const activator2 = TypeMoq.Mock.ofType();
serviceContainer
@@ -667,16 +635,11 @@ suite('Language Server Activation - ActivationService', () => {
.returns(() => Promise.resolve())
.verifiable(TypeMoq.Times.never());
activator2.setup((a) => a.activate()).verifiable(TypeMoq.Times.never());
- experiments
- .setup((ex) => ex.inExperiment(TypeMoq.It.isAny()))
- .returns(() => false)
- .verifiable(TypeMoq.Times.never());
await activationService.activate(folder2.uri);
serviceContainer.verifyAll();
activator1.verifyAll();
activator1.verify((a) => a.activate(), TypeMoq.Times.exactly(2));
activator2.verifyAll();
- experiments.verifyAll();
});
}
}
@@ -693,7 +656,6 @@ suite('Language Server Activation - ActivationService', () => {
let lsNotSupportedDiagnosticService: TypeMoq.IMock;
let stateFactory: TypeMoq.IMock;
let state: TypeMoq.IMock>;
- let experiments: TypeMoq.IMock;
let workspaceConfig: TypeMoq.IMock;
let interpreterService: TypeMoq.IMock;
setup(() => {
@@ -706,7 +668,6 @@ suite('Language Server Activation - ActivationService', () => {
state = TypeMoq.Mock.ofType>();
const configService = TypeMoq.Mock.ofType();
pythonSettings = TypeMoq.Mock.ofType();
- experiments = TypeMoq.Mock.ofType();
interpreterService = TypeMoq.Mock.ofType();
const e = new EventEmitter();
interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event);
@@ -785,8 +746,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi);
@@ -805,8 +765,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Microsoft);
@@ -825,8 +784,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi);
@@ -845,8 +803,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi);
@@ -854,204 +811,6 @@ suite('Language Server Activation - ActivationService', () => {
});
});
- suite('Test useJedi()', () => {
- let serviceContainer: TypeMoq.IMock;
- let pythonSettings: TypeMoq.IMock;
- let appShell: TypeMoq.IMock;
- let cmdManager: TypeMoq.IMock;
- let workspaceService: TypeMoq.IMock;
- let platformService: TypeMoq.IMock;
- let lsNotSupportedDiagnosticService: TypeMoq.IMock;
- let stateFactory: TypeMoq.IMock;
- let state: TypeMoq.IMock>;
- let experiments: TypeMoq.IMock;
- let workspaceConfig: TypeMoq.IMock;
- let interpreterService: TypeMoq.IMock;
- setup(() => {
- serviceContainer = TypeMoq.Mock.ofType();
- appShell = TypeMoq.Mock.ofType();
- workspaceService = TypeMoq.Mock.ofType();
- cmdManager = TypeMoq.Mock.ofType();
- platformService = TypeMoq.Mock.ofType();
- stateFactory = TypeMoq.Mock.ofType();
- state = TypeMoq.Mock.ofType>();
- const configService = TypeMoq.Mock.ofType();
- pythonSettings = TypeMoq.Mock.ofType();
- experiments = TypeMoq.Mock.ofType();
- interpreterService = TypeMoq.Mock.ofType();
- const e = new EventEmitter();
- interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event);
- const langFolderServiceMock = TypeMoq.Mock.ofType();
- const folderVer: FolderVersionPair = {
- path: '',
- version: new SemVer('1.2.3')
- };
- lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType();
- workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false);
- workspaceService.setup((w) => w.workspaceFolders).returns(() => []);
- configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object);
- langFolderServiceMock
- .setup((l) => l.getCurrentLanguageServerDirectory())
- .returns(() => Promise.resolve(folderVer));
- stateFactory
- .setup((f) =>
- f.createGlobalPersistentState(
- TypeMoq.It.isValue('SWITCH_LS'),
- TypeMoq.It.isAny(),
- TypeMoq.It.isAny()
- )
- )
- .returns(() => state.object);
- state.setup((s) => s.value).returns(() => undefined);
- state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve());
- workspaceConfig = TypeMoq.Mock.ofType();
- workspaceService
- .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny()))
- .returns(() => workspaceConfig.object);
- const output = TypeMoq.Mock.ofType();
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny()))
- .returns(() => output.object);
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService)))
- .returns(() => workspaceService.object);
- serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object);
- serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []);
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService)))
- .returns(() => configService.object);
- serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object);
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService)))
- .returns(() => platformService.object);
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService)))
- .returns(() => interpreterService.object);
- serviceContainer
- .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService)))
- .returns(() => langFolderServiceMock.object);
- serviceContainer
- .setup((s) =>
- s.get(
- TypeMoq.It.isValue(IDiagnosticsService),
- TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId)
- )
- )
- .returns(() => lsNotSupportedDiagnosticService.object);
- });
-
- test('If default value (Jedi) of language server is being used, and LSEnabled experiment is enabled, then return false', async () => {
- const settings = {};
- experiments
- .setup((ex) => ex.inExperiment(LSEnabled))
- .returns(() => true)
- .verifiable(TypeMoq.Times.once());
- experiments
- .setup((ex) => ex.sendTelemetryIfInExperiment(TypeMoq.It.isAny()))
- .returns(() => undefined)
- .verifiable(TypeMoq.Times.never());
- workspaceConfig
- .setup((c) => c.inspect('languageServer'))
- .returns(() => settings as any)
- .verifiable(TypeMoq.Times.once());
-
- const activationService = new LanguageServerExtensionActivationService(
- serviceContainer.object,
- stateFactory.object,
- experiments.object
- );
- const result = activationService.useJedi();
- expect(result).to.equal(false, 'LS should be enabled');
-
- workspaceService.verifyAll();
- workspaceConfig.verifyAll();
- experiments.verifyAll();
- });
-
- test('If default value of languageServer (Jedi) is being used, and LSEnabled experiment is disabled, then send telemetry if user is in Experiment LSControl and useJedi() should be true)', async () => {
- const settings = {};
- experiments
- .setup((ex) => ex.inExperiment(LSEnabled))
- .returns(() => false)
- .verifiable(TypeMoq.Times.once());
- experiments
- .setup((ex) => ex.sendTelemetryIfInExperiment(LSControl))
- .returns(() => undefined)
- .verifiable(TypeMoq.Times.once());
- workspaceConfig
- .setup((c) => c.inspect('languageServer'))
- .returns(() => settings as any)
- .verifiable(TypeMoq.Times.once());
-
- const activationService = new LanguageServerExtensionActivationService(
- serviceContainer.object,
- stateFactory.object,
- experiments.object
- );
- const result = activationService.useJedi();
- expect(result).to.equal(true, 'Return value should be true');
-
- pythonSettings.verifyAll();
- experiments.verifyAll();
- workspaceService.verifyAll();
- workspaceConfig.verifyAll();
- });
-
- suite(
- 'If default value of languageServer (Jedi) is not being used, then no experiments are used, and python settings value is returned',
- async () => {
- [
- {
- testName: 'Returns false when python settings value is Microsoft',
- pythonSettingsValue: LanguageServerType.Microsoft,
- expectedResult: false
- },
- {
- testName: 'Returns true when python settings value is Jedi',
- pythonSettingsValue: LanguageServerType.Jedi,
- expectedResult: true
- }
- ].forEach((testParams) => {
- test(testParams.testName, async () => {
- const settings = { workspaceFolderValue: true };
- experiments
- .setup((ex) => ex.inExperiment(LSEnabled))
- .returns(() => false)
- .verifiable(TypeMoq.Times.never());
- experiments
- .setup((ex) => ex.sendTelemetryIfInExperiment(LSControl))
- .returns(() => undefined)
- .verifiable(TypeMoq.Times.never());
- workspaceConfig
- .setup((c) => c.inspect('languageServer'))
- .returns(() => settings as any)
- .verifiable(TypeMoq.Times.once());
- pythonSettings
- .setup((p) => p.languageServer)
- .returns(() => testParams.pythonSettingsValue)
- .verifiable(TypeMoq.Times.once());
-
- const activationService = new LanguageServerExtensionActivationService(
- serviceContainer.object,
- stateFactory.object,
- experiments.object
- );
- const result = activationService.useJedi();
- expect(result).to.equal(
- testParams.expectedResult,
- `Return value should be ${testParams.expectedResult}`
- );
-
- pythonSettings.verifyAll();
- experiments.verifyAll();
- workspaceService.verifyAll();
- workspaceConfig.verifyAll();
- });
- });
- }
- );
- });
-
suite('Function isJediUsingDefaultConfiguration()', () => {
let serviceContainer: TypeMoq.IMock;
let pythonSettings: TypeMoq.IMock;
@@ -1062,7 +821,6 @@ suite('Language Server Activation - ActivationService', () => {
let lsNotSupportedDiagnosticService: TypeMoq.IMock;
let stateFactory: TypeMoq.IMock;
let state: TypeMoq.IMock>;
- let experiments: TypeMoq.IMock;
let workspaceConfig: TypeMoq.IMock;
let interpreterService: TypeMoq.IMock;
setup(() => {
@@ -1075,7 +833,6 @@ suite('Language Server Activation - ActivationService', () => {
state = TypeMoq.Mock.ofType>();
const configService = TypeMoq.Mock.ofType();
pythonSettings = TypeMoq.Mock.ofType();
- experiments = TypeMoq.Mock.ofType();
interpreterService = TypeMoq.Mock.ofType();
const e = new EventEmitter();
interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event);
@@ -1166,8 +923,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a'));
expect(result).to.equal(expectedResult);
@@ -1187,8 +943,7 @@ suite('Language Server Activation - ActivationService', () => {
const activationService = new LanguageServerExtensionActivationService(
serviceContainer.object,
- stateFactory.object,
- experiments.object
+ stateFactory.object
);
const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a'));
expect(result).to.equal(false, 'Return value should be false');
diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts
index 87fa327568a7..1b393fa95488 100644
--- a/src/test/activation/languageServer/activator.unit.test.ts
+++ b/src/test/activation/languageServer/activator.unit.test.ts
@@ -21,9 +21,10 @@ import { PythonSettings } from '../../../client/common/configSettings';
import { ConfigurationService } from '../../../client/common/configuration/service';
import { FileSystem } from '../../../client/common/platform/fileSystem';
import { IFileSystem } from '../../../client/common/platform/types';
-import { IConfigurationService, IPythonSettings } from '../../../client/common/types';
+import { IConfigurationService, IPythonExtensionBanner, IPythonSettings } from '../../../client/common/types';
import { createDeferred } from '../../../client/common/utils/async';
import { EXTENSION_ROOT_DIR } from '../../../client/constants';
+import { ProposePylanceBanner } from '../../../client/languageServices/proposeLanguageServerBanner';
import { sleep } from '../../core';
// tslint:disable:max-func-body-length
@@ -37,6 +38,7 @@ suite('Language Server - Activator', () => {
let lsFolderService: ILanguageServerFolderService;
let configuration: IConfigurationService;
let settings: IPythonSettings;
+ let banner: IPythonExtensionBanner;
setup(() => {
manager = mock(DotNetLanguageServerManager);
workspaceService = mock(WorkspaceService);
@@ -45,6 +47,7 @@ suite('Language Server - Activator', () => {
lsFolderService = mock(DotNetLanguageServerFolderService);
configuration = mock(ConfigurationService);
settings = mock(PythonSettings);
+ banner = mock(ProposePylanceBanner);
when(configuration.getSettings(anything())).thenReturn(instance(settings));
activator = new DotNetLanguageServerActivator(
instance(manager),
@@ -52,7 +55,8 @@ suite('Language Server - Activator', () => {
instance(fs),
instance(lsDownloader),
instance(lsFolderService),
- instance(configuration)
+ instance(configuration),
+ instance(banner)
);
});
test('Manager must be started without any workspace', async () => {
diff --git a/src/test/activation/node/languageServerFolderService.unit.test.ts b/src/test/activation/node/languageServerFolderService.unit.test.ts
index 115bdecc7eda..b944dde8ffb6 100644
--- a/src/test/activation/node/languageServerFolderService.unit.test.ts
+++ b/src/test/activation/node/languageServerFolderService.unit.test.ts
@@ -9,10 +9,10 @@ import { Extension, Uri, WorkspaceConfiguration } from 'vscode';
import {
ILanguageServerFolder,
ILSExtensionApi,
- NodeLanguageServerFolderService,
- PylanceExtensionName
+ NodeLanguageServerFolderService
} from '../../../client/activation/node/languageServerFolderService';
import { IWorkspaceService } from '../../../client/common/application/types';
+import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants';
import { IConfigurationService, IExtensions, IPythonSettings } from '../../../client/common/types';
import { IServiceContainer } from '../../../client/ioc/types';
@@ -115,7 +115,7 @@ suite('Node Language Server Folder Service', () => {
test('lsExtension not installed', async () => {
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
- extensions.setup((e) => e.getExtension(PylanceExtensionName)).returns(() => undefined);
+ extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => undefined);
const folderService = new TestService(
serviceContainer.object,
@@ -148,7 +148,7 @@ suite('Node Language Server Folder Service', () => {
extension.setup((e) => e.exports).returns(() => extensionApi);
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
- extensions.setup((e) => e.getExtension(PylanceExtensionName)).returns(() => extension.object);
+ extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => extension.object);
folderService = new TestService(
serviceContainer.object,
configService.object,
diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts
index 18e2bc853ba3..51219665453e 100644
--- a/src/test/activation/serviceRegistry.unit.test.ts
+++ b/src/test/activation/serviceRegistry.unit.test.ts
@@ -57,7 +57,7 @@ import { DataScienceSurveyBanner } from '../../client/datascience/dataScienceSur
import { InteractiveShiftEnterBanner } from '../../client/datascience/shiftEnterBanner';
import { ServiceManager } from '../../client/ioc/serviceManager';
import { IServiceManager } from '../../client/ioc/types';
-import { ProposeLanguageServerBanner } from '../../client/languageServices/proposeLanguageServerBanner';
+import { ProposePylanceBanner } from '../../client/languageServices/proposeLanguageServerBanner';
// tslint:disable:max-func-body-length
@@ -101,7 +101,7 @@ suite('Unit Tests - Language Server Activation Service Registry', () => {
verify(
serviceManager.addSingleton(
IPythonExtensionBanner,
- ProposeLanguageServerBanner,
+ ProposePylanceBanner,
BANNER_NAME_PROPOSE_LS
)
).once();
diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts
index a07d1082fb8b..919459206484 100644
--- a/src/test/common/configSettings/configSettings.unit.test.ts
+++ b/src/test/common/configSettings/configSettings.unit.test.ts
@@ -193,6 +193,31 @@ suite('Python Settings', async () => {
config.verifyAll();
});
+ function testLanguageServer(languageServer: LanguageServerType, expectedValue: LanguageServerType) {
+ test(languageServer, () => {
+ expected.pythonPath = 'python3';
+ expected.languageServer = languageServer;
+ initializeConfig(expected);
+ config
+ .setup((c) => c.get('languageServer'))
+ .returns(() => expected.languageServer)
+ .verifiable(TypeMoq.Times.once());
+
+ settings.update(config.object);
+
+ expect(settings.languageServer).to.be.equal(expectedValue);
+ config.verifyAll();
+ });
+ }
+
+ suite('languageServer settings', async () => {
+ Object.values(LanguageServerType).forEach(async (languageServer) => {
+ testLanguageServer(languageServer, languageServer);
+ });
+
+ testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi);
+ });
+
function testExperiments(enabled: boolean) {
expected.pythonPath = 'python3';
// tslint:disable-next-line:no-any
diff --git a/src/test/datascience/.vscode/settings.json b/src/test/datascience/.vscode/settings.json
index 84a3ffb84a43..90f4ac4dc843 100644
--- a/src/test/datascience/.vscode/settings.json
+++ b/src/test/datascience/.vscode/settings.json
@@ -24,5 +24,6 @@
"files.autoSave": "off",
// We don't want jupyter to start when testing (DS functionality or anything else).
"python.dataScience.disableJupyterAutoStart": true,
- "python.logging.level": "debug"
+ "python.logging.level": "debug",
+ "python.dataScience.alwaysTrustNotebooks": true // In UI tests we don't want prompts for `Do you want to trust thie nb...`
}
diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts
index b0ac5a5220e6..974befd219b0 100644
--- a/src/test/datascience/dataScienceIocContainer.ts
+++ b/src/test/datascience/dataScienceIocContainer.ts
@@ -151,6 +151,7 @@ import {
TerminalActivationProviders
} from '../../client/common/terminal/types';
import {
+ BANNER_NAME_PROPOSE_LS,
GLOBAL_MEMENTO,
IAsyncDisposableRegistry,
IBrowserService,
@@ -170,6 +171,7 @@ import {
IOutputChannel,
IPathUtils,
IPersistentStateFactory,
+ IPythonExtensionBanner,
IPythonSettings,
IsWindows,
ProductType,
@@ -365,6 +367,7 @@ import { InterpreterVersionService } from '../../client/interpreter/interpreterV
import { registerInterpreterTypes } from '../../client/interpreter/serviceRegistry';
import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs';
import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types';
+import { ProposePylanceBanner } from '../../client/languageServices/proposeLanguageServerBanner';
import { CacheableLocatorPromiseCache } from '../../client/pythonEnvironments/discovery/locators/services/cacheableLocatorService';
import { InterpreterType, PythonInterpreter } from '../../client/pythonEnvironments/info';
import { registerForIOC } from '../../client/pythonEnvironments/legacyIOC';
@@ -761,6 +764,11 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
LanguageServerType.Microsoft
);
this.serviceManager.add(ILanguageServerManager, DotNetLanguageServerManager);
+ this.serviceManager.add(
+ IPythonExtensionBanner,
+ ProposePylanceBanner,
+ BANNER_NAME_PROPOSE_LS
+ );
} else if (languageServerType === LanguageServerType.Node) {
this.serviceManager.add(
ILanguageServerActivator,
diff --git a/src/test/datascience/export/exportManager.test.ts b/src/test/datascience/export/exportManager.test.ts
index 862f3e06192c..e98ef9d932aa 100644
--- a/src/test/datascience/export/exportManager.test.ts
+++ b/src/test/datascience/export/exportManager.test.ts
@@ -33,17 +33,17 @@ suite('Data Science - Export Manager', () => {
exportPdf = mock();
// tslint:disable-next-line: no-any
- when(filePicker.getExportFileLocation(anything(), anything())).thenReturn(
+ when(filePicker.getExportFileLocation(anything(), anything(), anything())).thenReturn(
Promise.resolve(Uri.file('test.pdf'))
);
// tslint:disable-next-line: no-empty
when(exportUtil.generateTempDir()).thenResolve({ path: 'test', dispose: () => {} });
when(exportUtil.makeFileInDirectory(anything(), anything(), anything())).thenResolve('foo');
when(fileSystem.createTemporaryFile(anything())).thenResolve(instance(tempFile));
- when(exportPdf.export(anything(), anything())).thenResolve();
+ when(exportPdf.export(anything(), anything(), anything())).thenResolve();
when(filePicker.getExportFileLocation(anything(), anything())).thenResolve(Uri.file('foo'));
// tslint:disable-next-line: no-any
- when(reporter.createProgressIndicator(anything())).thenReturn(instance(mock()) as any);
+ when(reporter.createProgressIndicator(anything(), anything())).thenReturn(instance(mock()) as any);
exporter = new ExportManager(
instance(exportPdf),
instance(exportHtml),
diff --git a/src/test/datascience/export/exportManagerFileOpener.unit.test.ts b/src/test/datascience/export/exportManagerFileOpener.unit.test.ts
index 87dadc8a5592..174a9921141a 100644
--- a/src/test/datascience/export/exportManagerFileOpener.unit.test.ts
+++ b/src/test/datascience/export/exportManagerFileOpener.unit.test.ts
@@ -48,20 +48,20 @@ suite('Data Science - Export File Opener', () => {
test('No file is opened if nothing is exported', async () => {
when(exporter.export(anything(), anything())).thenResolve();
- await fileOpener.export(ExportFormat.python, model);
+ await fileOpener.export(ExportFormat.python, model, undefined);
verify(documentManager.showTextDocument(anything())).never();
});
test('Python File is opened if exported', async () => {
const uri = Uri.file('test.python');
- when(exporter.export(anything(), anything())).thenResolve(uri);
- await fileOpener.export(ExportFormat.python, model);
+ when(exporter.export(anything(), anything(), anything())).thenResolve(uri);
+ await fileOpener.export(ExportFormat.python, model, undefined);
verify(documentManager.showTextDocument(anything())).once();
});
test('HTML File opened if yes button pressed', async () => {
const uri = Uri.file('test.html');
- when(exporter.export(anything(), anything())).thenResolve(uri);
+ when(exporter.export(anything(), anything(), anything())).thenResolve(uri);
when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(
Promise.resolve(getLocString('DataScience.openExportFileYes', 'Yes'))
);
@@ -82,14 +82,14 @@ suite('Data Science - Export File Opener', () => {
verify(browserService.launch(anything())).never();
});
test('Exporting to PDF displays message if operation fails', async () => {
- when(exporter.export(anything(), anything())).thenThrow(new Error('Export failed...'));
+ when(exporter.export(anything(), anything(), anything())).thenThrow(new Error('Export failed...'));
when(applicationShell.showErrorMessage(anything())).thenResolve();
await fileOpener.export(ExportFormat.pdf, model);
verify(applicationShell.showErrorMessage(anything())).once();
});
test('PDF File opened if yes button pressed', async () => {
const uri = Uri.file('test.pdf');
- when(exporter.export(anything(), anything())).thenResolve(uri);
+ when(exporter.export(anything(), anything(), anything())).thenResolve(uri);
when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(
Promise.resolve(getLocString('DataScience.openExportFileYes', 'Yes'))
);
diff --git a/src/test/datascience/export/exportToHTML.test.ts b/src/test/datascience/export/exportToHTML.test.ts
index ec1b0b225543..4412881dea0e 100644
--- a/src/test/datascience/export/exportToHTML.test.ts
+++ b/src/test/datascience/export/exportToHTML.test.ts
@@ -4,7 +4,7 @@
// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any
import { assert } from 'chai';
import * as path from 'path';
-import { Uri } from 'vscode';
+import { CancellationTokenSource, Uri } from 'vscode';
import { IFileSystem } from '../../../client/common/platform/types';
import { ExportFormat, IExport } from '../../../client/datascience/export/types';
import { IExtensionTestApi } from '../../common';
@@ -34,9 +34,11 @@ suite('DataScience - Export HTML', () => {
const file = await fileSystem.createTemporaryFile('.html');
const target = Uri.file(file.filePath);
await file.dispose();
+ const token = new CancellationTokenSource();
await exportToHTML.export(
Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')),
- target
+ target,
+ token.token
);
assert.equal(await fileSystem.fileExists(target.fsPath), true);
diff --git a/src/test/datascience/export/exportToPython.test.ts b/src/test/datascience/export/exportToPython.test.ts
index 0bcf74ab912e..d7d130f141e5 100644
--- a/src/test/datascience/export/exportToPython.test.ts
+++ b/src/test/datascience/export/exportToPython.test.ts
@@ -4,7 +4,7 @@
// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any
import { assert } from 'chai';
import * as path from 'path';
-import { Uri } from 'vscode';
+import { CancellationTokenSource, Uri } from 'vscode';
import { IDocumentManager } from '../../../client/common/application/types';
import { IFileSystem } from '../../../client/common/platform/types';
import { ExportFormat, IExport } from '../../../client/datascience/export/types';
@@ -33,9 +33,11 @@ suite('DataScience - Export Python', () => {
const fileSystem = api.serviceContainer.get(IFileSystem);
const exportToPython = api.serviceContainer.get(IExport, ExportFormat.python);
const target = Uri.file((await fileSystem.createTemporaryFile('.py')).filePath);
+ const token = new CancellationTokenSource();
await exportToPython.export(
Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')),
- target
+ target,
+ token.token
);
const documentManager = api.serviceContainer.get(IDocumentManager);
diff --git a/src/test/datascience/export/exportUtil.test.ts b/src/test/datascience/export/exportUtil.test.ts
index 340828976e7e..a287b67ac84e 100644
--- a/src/test/datascience/export/exportUtil.test.ts
+++ b/src/test/datascience/export/exportUtil.test.ts
@@ -37,7 +37,7 @@ suite('DataScience - Export Util', () => {
);
await exportUtil.removeSvgs(file);
- const model = await notebookStorage.load(file);
+ const model = await notebookStorage.get(file);
// make sure no svg exists in model
const SVG = 'image/svg+xml';
diff --git a/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts b/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts
new file mode 100644
index 000000000000..3bb98ca5c813
--- /dev/null
+++ b/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+
+import * as fakeTimers from '@sinonjs/fake-timers';
+import { assert } from 'chai';
+import * as sinon from 'sinon';
+import { anything, instance, mock, verify, when } from 'ts-mockito';
+import { Uri } from 'vscode';
+import { IExtensionSingleActivationService } from '../../../client/activation/types';
+import { IApplicationShell, ICommandManager } from '../../../client/common/application/types';
+import { ContextKey } from '../../../client/common/contextKey';
+import { EnableTrustedNotebooks } from '../../../client/common/experiments/groups';
+import { IDisposable, IExperimentService } from '../../../client/common/types';
+import { DataScience } from '../../../client/common/utils/localize';
+import { Commands } from '../../../client/datascience/constants';
+import { INotebookStorageProvider } from '../../../client/datascience/interactive-ipynb/notebookStorageProvider';
+import { TrustCommandHandler } from '../../../client/datascience/interactive-ipynb/trustCommandHandler';
+import { TrustService } from '../../../client/datascience/interactive-ipynb/trustService';
+import { INotebookEditorProvider, INotebookModel, ITrustService } from '../../../client/datascience/types';
+import { noop } from '../../core';
+import { createNotebookModel, disposeAllDisposables } from '../notebook/helper';
+
+// tslint:disable: no-any
+
+suite('DataScience - Trust Command Handler', () => {
+ let trustCommandHandler: IExtensionSingleActivationService;
+ let trustService: ITrustService;
+ let editorProvider: INotebookEditorProvider;
+ let storageProvider: INotebookStorageProvider;
+ let commandManager: ICommandManager;
+ let applicationShell: IApplicationShell;
+ let disposables: IDisposable[] = [];
+ let clock: fakeTimers.InstalledClock;
+ let contextKeySet: sinon.SinonStub<[boolean], Promise>;
+ let experiments: IExperimentService;
+ let model: INotebookModel;
+ let trustNotebookCommandCallback: (uri: Uri) => Promise;
+ setup(() => {
+ trustService = mock();
+ editorProvider = mock();
+ storageProvider = mock();
+ commandManager = mock();
+ applicationShell = mock();
+ model = createNotebookModel(false, Uri.file('a'));
+ when(storageProvider.get(anything())).thenResolve(model);
+ disposables = [];
+
+ experiments = mock();
+
+ when(trustService.trustNotebook(anything(), anything())).thenResolve();
+ when(experiments.inExperiment(anything())).thenCall((exp) =>
+ Promise.resolve(exp === EnableTrustedNotebooks.experiment)
+ );
+ when(commandManager.registerCommand(anything(), anything(), anything())).thenCall(() => ({ dispose: noop }));
+ when(commandManager.registerCommand(Commands.TrustNotebook, anything(), anything())).thenCall((_, cb) => {
+ trustNotebookCommandCallback = cb.bind(trustCommandHandler);
+ return { dispose: noop };
+ });
+
+ trustCommandHandler = new TrustCommandHandler(
+ instance(trustService),
+ instance(editorProvider),
+ instance(storageProvider),
+ instance(commandManager),
+ instance(applicationShell),
+ disposables,
+ instance(experiments)
+ );
+
+ clock = fakeTimers.install();
+
+ contextKeySet = sinon.stub(ContextKey.prototype, 'set');
+ contextKeySet.resolves();
+ });
+ teardown(() => {
+ sinon.restore();
+ disposeAllDisposables(disposables);
+ clock.uninstall();
+ });
+
+ test('Context not set if not in experiment', async () => {
+ when(experiments.inExperiment(anything())).thenResolve(false);
+
+ await trustCommandHandler.activate();
+ await clock.runAllAsync();
+
+ assert.equal(contextKeySet.callCount, 0);
+ });
+ test('Context set if in experiment', async () => {
+ when(experiments.inExperiment(anything())).thenCall((exp) =>
+ Promise.resolve(exp === EnableTrustedNotebooks.experiment)
+ );
+
+ await trustCommandHandler.activate();
+ await clock.runAllAsync();
+
+ assert.equal(contextKeySet.callCount, 1);
+ });
+ test('Executing command will not update trust after dismissing the prompt', async () => {
+ when(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve(
+ undefined as any
+ );
+
+ await trustCommandHandler.activate();
+ await clock.runAllAsync();
+ await trustNotebookCommandCallback(Uri.file('a'));
+
+ verify(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).once();
+ verify(trustService.trustNotebook(anything(), anything())).never();
+ assert.isFalse(model.isTrusted);
+ });
+ test('Executing command will update trust', async () => {
+ when(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve(
+ DataScience.trustNotebook() as any
+ );
+
+ assert.isFalse(model.isTrusted);
+ await trustCommandHandler.activate();
+ await clock.runAllAsync();
+ await trustNotebookCommandCallback(Uri.file('a'));
+
+ verify(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).once();
+ verify(trustService.trustNotebook(anything(), anything())).once();
+ assert.isTrue(model.isTrusted);
+ });
+});
diff --git a/src/test/datascience/trustService.unit.test.ts b/src/test/datascience/interactive-common/trustService.unit.test.ts
similarity index 84%
rename from src/test/datascience/trustService.unit.test.ts
rename to src/test/datascience/interactive-common/trustService.unit.test.ts
index 2ff31348abf6..beb3c7c88fad 100644
--- a/src/test/datascience/trustService.unit.test.ts
+++ b/src/test/datascience/interactive-common/trustService.unit.test.ts
@@ -8,12 +8,12 @@ import * as os from 'os';
import { anything, instance, mock, when } from 'ts-mockito';
import * as typemoq from 'typemoq';
import { Uri } from 'vscode';
-import { ConfigurationService } from '../../client/common/configuration/service';
-import { ExperimentService } from '../../client/common/experiments/service';
-import { FileSystem } from '../../client/common/platform/fileSystem';
-import { IExtensionContext } from '../../client/common/types';
-import { DigestStorage } from '../../client/datascience/interactive-ipynb/digestStorage';
-import { TrustService } from '../../client/datascience/interactive-ipynb/trustService';
+import { ConfigurationService } from '../../../client/common/configuration/service';
+import { ExperimentService } from '../../../client/common/experiments/service';
+import { FileSystem } from '../../../client/common/platform/fileSystem';
+import { IExtensionContext } from '../../../client/common/types';
+import { DigestStorage } from '../../../client/datascience/interactive-ipynb/digestStorage';
+import { TrustService } from '../../../client/datascience/interactive-ipynb/trustService';
suite('DataScience - TrustService', () => {
let trustService: TrustService;
diff --git a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts
index 98b7a9fae312..3db2eecd0ee4 100644
--- a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts
+++ b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts
@@ -409,7 +409,7 @@ suite('DataScience - Native Editor Storage', () => {
}
test('Create new editor and add some cells', async () => {
- model = await storage.load(baseUri);
+ model = await storage.get(baseUri);
insertCell(0, '1');
const cells = model.cells;
expect(cells).to.be.lengthOf(4);
@@ -418,7 +418,7 @@ suite('DataScience - Native Editor Storage', () => {
});
test('Move cells around', async () => {
- model = await storage.load(baseUri);
+ model = await storage.get(baseUri);
swapCells('NotebookImport#0', 'NotebookImport#1');
const cells = model.cells;
expect(cells).to.be.lengthOf(3);
@@ -427,7 +427,7 @@ suite('DataScience - Native Editor Storage', () => {
});
test('Edit/delete cells', async () => {
- model = await storage.load(baseUri);
+ model = await storage.get(baseUri);
expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty');
editCell(
[
@@ -467,7 +467,7 @@ suite('DataScience - Native Editor Storage', () => {
test('Editing a file and closing will keep contents', async () => {
await filesConfig?.update('autoSave', 'off');
- model = await storage.load(baseUri);
+ model = await storage.get(baseUri);
expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty');
editCell(
[
@@ -496,7 +496,7 @@ suite('DataScience - Native Editor Storage', () => {
// Recreate
storage = createStorage();
- model = await storage.load(baseUri);
+ model = await storage.get(baseUri);
const cells = model.cells;
expect(cells).to.be.lengthOf(3);
@@ -506,7 +506,7 @@ suite('DataScience - Native Editor Storage', () => {
});
test('Editing a new file and closing will keep contents', async () => {
- model = await storage.load(untiledUri, undefined, true);
+ model = await storage.get(untiledUri, undefined, true);
expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty');
insertCell(0, 'a=1');
@@ -515,7 +515,7 @@ suite('DataScience - Native Editor Storage', () => {
// Recreate
storage = createStorage();
- model = await storage.load(untiledUri);
+ model = await storage.get(untiledUri);
const cells = model.cells;
expect(cells).to.be.lengthOf(2);
@@ -534,7 +534,7 @@ suite('DataScience - Native Editor Storage', () => {
// Put the regular file into the local storage
await localMemento.update(`notebook-storage-${file.toString()}`, differentFile);
- model = await storage.load(file);
+ model = await storage.get(file);
// It should load with that value
const cells = model.cells;
@@ -555,7 +555,7 @@ suite('DataScience - Native Editor Storage', () => {
contents: differentFile,
lastModifiedTimeMs: Date.now()
});
- model = await storage.load(file);
+ model = await storage.get(file);
// It should load with that value
const cells = model.cells;
@@ -585,7 +585,7 @@ suite('DataScience - Native Editor Storage', () => {
lastModifiedTimeMs: Date.now()
});
- model = await storage.load(file);
+ model = await storage.get(file);
// It should load with that value
const cells = model.cells;
diff --git a/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts b/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts
index c03aa732d229..eaef32c25385 100644
--- a/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts
+++ b/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts
@@ -3,7 +3,7 @@
import { assert } from 'chai';
import { anything, deepEqual, instance, mock, when } from 'ts-mockito';
-import { ObservableExecutionResult } from '../../../client/common/process/types';
+import { IPythonExecutionService, ObservableExecutionResult } from '../../../client/common/process/types';
import { ReadWrite } from '../../../client/common/types';
import { KernelDaemonPool } from '../../../client/datascience/kernel-launcher/kernelDaemonPool';
import { PythonKernelLauncherDaemon } from '../../../client/datascience/kernel-launcher/kernelLauncherDaemon';
@@ -61,10 +61,23 @@ suite('Data Science - Kernel Launcher Daemon', () => {
assert.equal(daemonCreationOutput.daemon, instance(kernelDaemon));
}
});
- test('If our daemon pool return does not support start, return undefined', async () => {
- when(daemonPool.get(anything(), anything(), anything())).thenResolve({} as any);
+ test('If our daemon pool returns an execution service, then use it and return the daemon as undefined', async () => {
+ const executionService = mock();
+ when(
+ executionService.execModuleObservable(
+ 'ipkernel_launcher',
+ deepEqual(['-f', 'file.json']),
+ deepEqual({ env: kernelSpec.env })
+ )
+ ).thenReturn(instance(observableOutputForDaemon));
+ // Make sure that it doesn't have a start function. Normally the proxy will pretend to have a start function, which we are checking for non-existance
+ when((executionService as any).start).thenReturn(false);
+ // Else ts-mockit doesn't allow us to return an instance of a mock as a return value from an async function.
+ (instance(executionService) as any).then = undefined;
+ when(daemonPool.get(anything(), anything(), anything())).thenResolve(instance(executionService) as any);
const daemonCreationOutput = await launcher.launch(undefined, kernelSpec, interpreter);
- assert.isUndefined(daemonCreationOutput);
+ assert.equal(daemonCreationOutput.observableOutput, instance(observableOutputForDaemon));
+ assert.isUndefined(daemonCreationOutput.daemon);
});
});
diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx
index 87b3e8a1116e..659602c257b4 100644
--- a/src/test/datascience/nativeEditor.functional.test.tsx
+++ b/src/test/datascience/nativeEditor.functional.test.tsx
@@ -630,7 +630,7 @@ df.head()`;
const model = editor!.model!;
ioc.serviceManager.rebindInstance(ICommandManager, commandManager.object);
commandManager
- .setup((cmd) => cmd.executeCommand(Commands.Export, model))
+ .setup((cmd) => cmd.executeCommand(Commands.Export, model, undefined))
.returns(() => {
commandFired.resolve();
return Promise.resolve();
diff --git a/src/test/datascience/notebook/contentProvider.unit.test.ts b/src/test/datascience/notebook/contentProvider.unit.test.ts
index b7c27604c456..b85a4715ebcf 100644
--- a/src/test/datascience/notebook/contentProvider.unit.test.ts
+++ b/src/test/datascience/notebook/contentProvider.unit.test.ts
@@ -33,157 +33,173 @@ suite('Data Science - NativeNotebook ContentProvider', () => {
instance(compatSupport)
);
});
+ [true, false].forEach((isNotebookTrusted) => {
+ suite(isNotebookTrusted ? 'Trusted Notebook' : 'Un-trusted notebook', () => {
+ test('Return notebook with 2 cells', async () => {
+ const model: Partial = {
+ cells: [
+ {
+ data: {
+ cell_type: 'code',
+ execution_count: 10,
+ hasExecutionOrder: true,
+ outputs: [],
+ source: 'print(1)',
+ metadata: {}
+ },
+ file: 'a.ipynb',
+ id: 'MyCellId1',
+ line: 0,
+ state: CellState.init
+ },
+ {
+ data: {
+ cell_type: 'markdown',
+ hasExecutionOrder: false,
+ source: '# HEAD',
+ metadata: {}
+ },
+ file: 'a.ipynb',
+ id: 'MyCellId2',
+ line: 0,
+ state: CellState.init
+ }
+ ],
+ isTrusted: isNotebookTrusted
+ };
+ when(storageProvider.get(anything(), anything(), anything(), anything())).thenResolve(
+ (model as unknown) as INotebookModel
+ );
- test('Return notebook with 2 cells', async () => {
- const model: Partial = {
- cells: [
- {
- data: {
- cell_type: 'code',
- execution_count: 10,
- hasExecutionOrder: true,
+ const notebook = await contentProvider.openNotebook(fileUri, {});
+
+ assert.isOk(notebook);
+ assert.deepEqual(notebook.languages, [PYTHON_LANGUAGE]);
+ // ignore metadata we add.
+ notebook.cells.forEach((cell) => delete cell.metadata.custom);
+
+ assert.equal(notebook.metadata.cellEditable, isNotebookTrusted);
+ assert.equal(notebook.metadata.cellRunnable, isNotebookTrusted);
+ assert.equal(notebook.metadata.editable, isNotebookTrusted);
+ assert.equal(notebook.metadata.runnable, isNotebookTrusted);
+
+ assert.deepEqual(notebook.cells, [
+ {
+ cellKind: (vscodeNotebookEnums as any).CellKind.Code,
+ language: PYTHON_LANGUAGE,
outputs: [],
source: 'print(1)',
- metadata: {}
+ metadata: {
+ editable: isNotebookTrusted,
+ executionOrder: 10,
+ hasExecutionOrder: true,
+ runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success,
+ runnable: isNotebookTrusted
+ }
},
- file: 'a.ipynb',
- id: 'MyCellId1',
- line: 0,
- state: CellState.init
- },
- {
- data: {
- cell_type: 'markdown',
- hasExecutionOrder: false,
+ {
+ cellKind: (vscodeNotebookEnums as any).CellKind.Markdown,
+ language: MARKDOWN_LANGUAGE,
+ outputs: [],
source: '# HEAD',
- metadata: {}
+ metadata: {
+ editable: isNotebookTrusted,
+ executionOrder: undefined,
+ hasExecutionOrder: false,
+ runState: (vscodeNotebookEnums as any).NotebookCellRunState.Idle,
+ runnable: false
+ }
+ }
+ ]);
+ });
+
+ test('Return notebook with csharp language', async () => {
+ const model: Partial = {
+ metadata: {
+ language_info: {
+ name: 'csharp'
+ },
+ orig_nbformat: 5
},
- file: 'a.ipynb',
- id: 'MyCellId2',
- line: 0,
- state: CellState.init
- }
- ]
- };
- when(storageProvider.load(anything(), anything(), anything(), anything())).thenResolve(
- (model as unknown) as INotebookModel
- );
+ cells: [
+ {
+ data: {
+ cell_type: 'code',
+ execution_count: 10,
+ hasExecutionOrder: true,
+ outputs: [],
+ source: 'Console.WriteLine("1")',
+ metadata: {}
+ },
+ file: 'a.ipynb',
+ id: 'MyCellId1',
+ line: 0,
+ state: CellState.init
+ },
+ {
+ data: {
+ cell_type: 'markdown',
+ hasExecutionOrder: false,
+ source: '# HEAD',
+ metadata: {}
+ },
+ file: 'a.ipynb',
+ id: 'MyCellId2',
+ line: 0,
+ state: CellState.init
+ }
+ ],
+ isTrusted: isNotebookTrusted
+ };
+ when(storageProvider.get(anything(), anything(), anything(), anything())).thenResolve(
+ (model as unknown) as INotebookModel
+ );
- const notebook = await contentProvider.openNotebook(fileUri, {});
+ const notebook = await contentProvider.openNotebook(fileUri, {});
- assert.isOk(notebook);
- assert.deepEqual(notebook.languages, [PYTHON_LANGUAGE]);
- // ignore metadata we add.
- notebook.cells.forEach((cell) => delete cell.metadata.custom);
+ assert.isOk(notebook);
+ assert.deepEqual(notebook.languages, ['csharp']);
- assert.deepEqual(notebook.cells, [
- {
- cellKind: (vscodeNotebookEnums as any).CellKind.Code,
- language: PYTHON_LANGUAGE,
- outputs: [],
- source: 'print(1)',
- metadata: {
- editable: true,
- executionOrder: 10,
- hasExecutionOrder: true,
- runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success,
- runnable: true
- }
- },
- {
- cellKind: (vscodeNotebookEnums as any).CellKind.Markdown,
- language: MARKDOWN_LANGUAGE,
- outputs: [],
- source: '# HEAD',
- metadata: {
- editable: true,
- executionOrder: undefined,
- hasExecutionOrder: false,
- runState: (vscodeNotebookEnums as any).NotebookCellRunState.Idle,
- runnable: false
- }
- }
- ]);
- });
+ assert.equal(notebook.metadata.cellEditable, isNotebookTrusted);
+ assert.equal(notebook.metadata.cellRunnable, isNotebookTrusted);
+ assert.equal(notebook.metadata.editable, isNotebookTrusted);
+ assert.equal(notebook.metadata.runnable, isNotebookTrusted);
+
+ // ignore metadata we add.
+ notebook.cells.forEach((cell: NotebookCellData) => delete cell.metadata.custom);
- test('Return notebook with csharp language', async () => {
- const model: Partial = {
- metadata: {
- language_info: {
- name: 'csharp'
- },
- orig_nbformat: 5
- },
- cells: [
- {
- data: {
- cell_type: 'code',
- execution_count: 10,
- hasExecutionOrder: true,
+ assert.deepEqual(notebook.cells, [
+ {
+ cellKind: (vscodeNotebookEnums as any).CellKind.Code,
+ language: 'csharp',
outputs: [],
source: 'Console.WriteLine("1")',
- metadata: {}
+ metadata: {
+ editable: isNotebookTrusted,
+ executionOrder: 10,
+ hasExecutionOrder: true,
+ runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success,
+ runnable: isNotebookTrusted
+ }
},
- file: 'a.ipynb',
- id: 'MyCellId1',
- line: 0,
- state: CellState.init
- },
- {
- data: {
- cell_type: 'markdown',
- hasExecutionOrder: false,
+ {
+ cellKind: (vscodeNotebookEnums as any).CellKind.Markdown,
+ language: MARKDOWN_LANGUAGE,
+ outputs: [],
source: '# HEAD',
- metadata: {}
- },
- file: 'a.ipynb',
- id: 'MyCellId2',
- line: 0,
- state: CellState.init
- }
- ]
- };
- when(storageProvider.load(anything(), anything(), anything(), anything())).thenResolve(
- (model as unknown) as INotebookModel
- );
-
- const notebook = await contentProvider.openNotebook(fileUri, {});
-
- assert.isOk(notebook);
- assert.deepEqual(notebook.languages, ['csharp']);
- // ignore metadata we add.
- notebook.cells.forEach((cell: NotebookCellData) => delete cell.metadata.custom);
-
- assert.deepEqual(notebook.cells, [
- {
- cellKind: (vscodeNotebookEnums as any).CellKind.Code,
- language: 'csharp',
- outputs: [],
- source: 'Console.WriteLine("1")',
- metadata: {
- editable: true,
- executionOrder: 10,
- hasExecutionOrder: true,
- runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success,
- runnable: true
- }
- },
- {
- cellKind: (vscodeNotebookEnums as any).CellKind.Markdown,
- language: MARKDOWN_LANGUAGE,
- outputs: [],
- source: '# HEAD',
- metadata: {
- editable: true,
- executionOrder: undefined,
- hasExecutionOrder: false,
- runState: (vscodeNotebookEnums as any).NotebookCellRunState.Idle,
- runnable: false
- }
- }
- ]);
- });
- test('Verify mime types and order', () => {
- // https://github.com/microsoft/vscode-python/issues/11880
+ metadata: {
+ editable: isNotebookTrusted,
+ executionOrder: undefined,
+ hasExecutionOrder: false,
+ runState: (vscodeNotebookEnums as any).NotebookCellRunState.Idle,
+ runnable: false
+ }
+ }
+ ]);
+ });
+ test('Verify mime types and order', () => {
+ // https://github.com/microsoft/vscode-python/issues/11880
+ });
+ });
});
});
diff --git a/src/test/datascience/notebook/helper.ts b/src/test/datascience/notebook/helper.ts
index 8f1d66148b59..cb77fdb87749 100644
--- a/src/test/datascience/notebook/helper.ts
+++ b/src/test/datascience/notebook/helper.ts
@@ -9,16 +9,30 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import * as sinon from 'sinon';
import * as tmp from 'tmp';
-import { commands, Uri } from 'vscode';
-import { NotebookCell } from '../../../../types/vscode-proposed';
+import { instance, mock } from 'ts-mockito';
+import { commands, TextDocument, Uri } from 'vscode';
+import { NotebookCell, NotebookDocument } from '../../../../types/vscode-proposed';
import { CellDisplayOutput } from '../../../../typings/vscode-proposed';
import { IApplicationEnvironment, IVSCodeNotebook } from '../../../client/common/application/types';
import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/constants';
import { IDisposable } from '../../../client/common/types';
import { noop, swallowExceptions } from '../../../client/common/utils/misc';
-import { findMappedNotebookCellModel } from '../../../client/datascience/notebook/helpers/cellMappers';
+import { Identifiers } from '../../../client/datascience/constants';
+import { JupyterNotebookView } from '../../../client/datascience/notebook/constants';
+import {
+ findMappedNotebookCellModel,
+ mapVSCNotebookCellsToNotebookCellModels
+} from '../../../client/datascience/notebook/helpers/cellMappers';
+import { createVSCNotebookCellDataFromCell } from '../../../client/datascience/notebook/helpers/helpers';
import { INotebookContentProvider } from '../../../client/datascience/notebook/types';
-import { ICell, INotebookEditorProvider, INotebookModel, INotebookProvider } from '../../../client/datascience/types';
+import { VSCodeNotebookModel } from '../../../client/datascience/notebookStorage/vscNotebookModel';
+import {
+ CellState,
+ ICell,
+ INotebookEditorProvider,
+ INotebookModel,
+ INotebookProvider
+} from '../../../client/datascience/types';
import { createEventHandler, waitForCondition } from '../../common';
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants';
import { closeActiveWindows, initialize } from '../../initialize';
@@ -365,3 +379,61 @@ export async function saveActiveNotebook(disposables: IDisposable[]) {
await waitForCondition(async () => savedEvent.all.some((e) => e.kind === 'save'), 5_000, 'Not saved');
}
+
+export function createNotebookModel(trusted: boolean, uri: Uri, nb?: Partial) {
+ const nbJson: nbformat.INotebookContent = {
+ cells: [],
+ metadata: {
+ orig_nbformat: 4
+ },
+ nbformat: 4,
+ nbformat_minor: 4,
+ ...(nb || {})
+ };
+
+ const cells = nbJson.cells.map((c, index) => {
+ return {
+ id: `NotebookImport#${index}`,
+ file: Identifiers.EmptyFileName,
+ line: 0,
+ state: CellState.finished,
+ data: c
+ };
+ });
+ return new VSCodeNotebookModel(trusted, uri, JSON.parse(JSON.stringify(cells)));
+}
+
+export function createNotebookDocument(
+ model: INotebookModel,
+ viewType: string = JupyterNotebookView
+): NotebookDocument {
+ const doc: NotebookDocument = {
+ cells: [],
+ fileName: model.file.fsPath,
+ isDirty: false,
+ languages: [],
+ uri: model.file,
+ viewType,
+ metadata: {
+ cellEditable: model.isTrusted,
+ cellHasExecutionOrder: true,
+ cellRunnable: model.isTrusted,
+ editable: model.isTrusted,
+ runnable: model.isTrusted
+ }
+ };
+ model.cells.forEach((cell, index) => {
+ const vscCell = createVSCNotebookCellDataFromCell(model, cell)!;
+ const vscDocumentCell: NotebookCell = {
+ ...vscCell,
+ uri: model.file.with({ fragment: `cell${index}` }),
+ notebook: doc,
+ document: instance(mock())
+ };
+ doc.cells.push(vscDocumentCell);
+ });
+ if (viewType === JupyterNotebookView) {
+ mapVSCNotebookCellsToNotebookCellModels(doc, model);
+ }
+ return doc;
+}
diff --git a/src/test/datascience/notebook/helpers.unit.test.ts b/src/test/datascience/notebook/helpers.unit.test.ts
index 63174224c3b3..bb48bc6c30c2 100644
--- a/src/test/datascience/notebook/helpers.unit.test.ts
+++ b/src/test/datascience/notebook/helpers.unit.test.ts
@@ -40,7 +40,8 @@ suite('Data Science - NativeNotebook helpers', () => {
line: 0,
state: CellState.init
}
- ]
+ ],
+ isTrusted: true
};
const notebook = notebookModelToVSCNotebookData((model as unknown) as INotebookModel);
@@ -95,7 +96,8 @@ suite('Data Science - NativeNotebook helpers', () => {
line: 0,
state: CellState.init
}
- ]
+ ],
+ isTrusted: true
};
const notebook = notebookModelToVSCNotebookData((model as unknown) as INotebookModel);
diff --git a/src/test/datascience/notebook/notebookTrustHandler.unit.test.ts b/src/test/datascience/notebook/notebookTrustHandler.unit.test.ts
new file mode 100644
index 000000000000..7d3f6e821b4e
--- /dev/null
+++ b/src/test/datascience/notebook/notebookTrustHandler.unit.test.ts
@@ -0,0 +1,142 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+'use strict';
+
+import { nbformat } from '@jupyterlab/coreutils';
+import { assert } from 'chai';
+import { teardown } from 'mocha';
+import { anything, instance, mock, when } from 'ts-mockito';
+import { EventEmitter, Uri } from 'vscode';
+import { NotebookDocument } from '../../../../types/vscode-proposed';
+import { IExtensionSingleActivationService } from '../../../client/activation/types';
+import { IVSCodeNotebook } from '../../../client/common/application/types';
+import { IFileSystem } from '../../../client/common/platform/types';
+import { IDisposable } from '../../../client/common/types';
+import { NotebookTrustHandler } from '../../../client/datascience/notebook/notebookTrustHandler';
+import { INotebookEditor, INotebookEditorProvider, ITrustService } from '../../../client/datascience/types';
+import { createNotebookDocument, createNotebookModel, disposeAllDisposables } from './helper';
+// tslint:disable-next-line: no-var-requires no-require-imports
+const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed');
+
+// tslint:disable: no-any
+suite('Data Science - NativeNotebook TrustHandler', () => {
+ let trustHandler: IExtensionSingleActivationService;
+ let trustService: ITrustService;
+ let vscNotebook: IVSCodeNotebook;
+ let editorProvider: INotebookEditorProvider;
+ let fs: IFileSystem;
+ let disposables: IDisposable[];
+ let onDidTrustNotebook: EventEmitter;
+ setup(async () => {
+ disposables = [];
+ trustService = mock();
+ vscNotebook = mock();
+ editorProvider = mock();
+ fs = mock();
+ onDidTrustNotebook = new EventEmitter();
+ when(trustService.onDidSetNotebookTrust).thenReturn(onDidTrustNotebook.event);
+ when(fs.arePathsSame(anything(), anything())).thenCall((a, b) => a === b); // Dirty simple file compare.
+ trustHandler = new NotebookTrustHandler(
+ instance(trustService),
+ instance(vscNotebook),
+ instance(editorProvider),
+ instance(fs),
+ disposables
+ );
+
+ await trustHandler.activate();
+ });
+ teardown(() => disposeAllDisposables(disposables));
+ function assertDocumentTrust(document: NotebookDocument, trusted: boolean) {
+ assert.equal(document.metadata.cellEditable, trusted);
+ assert.equal(document.metadata.cellRunnable, trusted);
+ assert.equal(document.metadata.editable, trusted);
+ assert.equal(document.metadata.runnable, trusted);
+
+ document.cells.forEach((cell) => {
+ assert.equal(cell.metadata.editable, trusted);
+ if (cell.cellKind === vscodeNotebookEnums.CellKind.Code) {
+ assert.equal(cell.metadata.runnable, trusted);
+ // In our test all code cells have outputs.
+ if (trusted) {
+ assert.ok(cell.outputs.length, 'No output in trusted cell');
+ } else {
+ assert.lengthOf(cell.outputs, 0, 'Cannot have output in non-trusted notebook');
+ }
+ }
+ });
+ }
+ function createModels() {
+ const nbJson: Partial = {
+ cells: [
+ {
+ cell_type: 'markdown',
+ source: [],
+ metadata: {}
+ },
+ {
+ cell_type: 'code',
+ source: [],
+ metadata: {},
+ execution_count: 1,
+ outputs: [
+ {
+ output_type: 'stream',
+ name: 'stdout',
+ text: 'Hello World'
+ }
+ ]
+ }
+ ]
+ };
+
+ return [createNotebookModel(false, Uri.file('a'), nbJson), createNotebookModel(false, Uri.file('b'), nbJson)];
+ }
+ test('When a notebook is trusted, the Notebook document is updated accordingly', async () => {
+ const [model1, model2] = createModels();
+ const [nb1, nb2, nbAnotherExtension] = [
+ createNotebookDocument(model1),
+ createNotebookDocument(model2),
+ createNotebookDocument(model2, 'AnotherExtensionNotebookEditorForIpynbFile')
+ ];
+
+ // Initially un-trusted.
+ assertDocumentTrust(nb1, false);
+ assertDocumentTrust(nb2, false);
+ assertDocumentTrust(nbAnotherExtension, false);
+
+ when(vscNotebook.notebookDocuments).thenReturn([nb1, nb2]);
+ const editor1 = mock();
+ const editor2 = mock();
+ when(editor1.file).thenReturn(model1.file);
+ when(editor2.file).thenReturn(model2.file);
+ when(editor1.model).thenReturn(model1);
+ when(editor2.model).thenReturn(model2);
+ when(editorProvider.editors).thenReturn([instance(editor1), instance(editor2)]);
+
+ // Trigger a change, even though none of the models are still trusted.
+ onDidTrustNotebook.fire();
+
+ // Still un-trusted.
+ assertDocumentTrust(nb1, false);
+ assertDocumentTrust(nb2, false);
+ assertDocumentTrust(nbAnotherExtension, false);
+
+ // Trigger a change, after trusting second nb/model.
+ model2.update({
+ source: 'user',
+ kind: 'updateTrust',
+ oldDirty: model2.isDirty,
+ newDirty: model2.isDirty,
+ isNotebookTrusted: true
+ });
+
+ onDidTrustNotebook.fire();
+
+ // Nb1 is still un-trusted and nb1 is trusted.
+ assertDocumentTrust(nb1, false);
+ assertDocumentTrust(nb2, true);
+ assertDocumentTrust(nbAnotherExtension, false); // This is a document from a different content provider, we should modify this.
+ });
+});
diff --git a/src/test/datascience/notebook/survey.unit.test.ts b/src/test/datascience/notebook/survey.unit.test.ts
index 5a7889548286..634882127978 100644
--- a/src/test/datascience/notebook/survey.unit.test.ts
+++ b/src/test/datascience/notebook/survey.unit.test.ts
@@ -57,6 +57,7 @@ suite('Data Science - NativeNotebook Survey', () => {
shell = mock();
browser = mock();
clock = fakeTimers.install();
+ clock.setSystemTime(new Date(2020, 7, 1)); // Survey will work only after 1st August.
});
async function loadAndActivateExtension() {
const surveyBanner = new NotebookSurveyBanner(instance(shell), instance(stateFactory), instance(browser));
diff --git a/src/test/providers/terminal.unit.test.ts b/src/test/providers/terminal.unit.test.ts
index 3e852b63dc2d..aa1fc837cfd1 100644
--- a/src/test/providers/terminal.unit.test.ts
+++ b/src/test/providers/terminal.unit.test.ts
@@ -122,6 +122,11 @@ suite('Terminal Provider', () => {
.returns(() => activeResourceService.object);
terminal = TypeMoq.Mock.ofType();
+ terminal
+ .setup((c) => c.creationOptions)
+ .returns(() => {
+ return { hideFromUser: false };
+ });
});
test('If terminal.activateCurrentTerminal setting is set, provided terminal should be activated', async () => {
@@ -168,6 +173,34 @@ suite('Terminal Provider', () => {
configService.verifyAll();
});
+ test('If terminal.activateCurrentTerminal setting is set, but hideFromUser is true, provided terminal should not be activated', async () => {
+ terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true);
+ configService
+ .setup((c) => c.getSettings(resource))
+ .returns(() => pythonSettings.object)
+ .verifiable(TypeMoq.Times.once());
+ activeResourceService
+ .setup((a) => a.getActiveResource())
+ .returns(() => resource)
+ .verifiable(TypeMoq.Times.once());
+
+ terminal
+ .setup((c) => c.creationOptions)
+ .returns(() => {
+ return { hideFromUser: true };
+ });
+
+ terminalProvider = new TerminalProvider(serviceContainer.object);
+ await terminalProvider.initialize(terminal.object);
+
+ terminalActivator.verify(
+ (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()),
+ TypeMoq.Times.never()
+ );
+ activeResourceService.verifyAll();
+ configService.verifyAll();
+ });
+
test('terminal.activateCurrentTerminal setting is set but provided terminal is undefined', async () => {
terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true);
configService
diff --git a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts
index 42e34a2106a0..db4911814901 100644
--- a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts
+++ b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts
@@ -3,103 +3,202 @@
'use strict';
-// tslint:disable:no-any max-func-body-length
+import { expect } from 'chai';
+import * as typemoq from 'typemoq';
+import { Extension } from 'vscode';
+import { LanguageServerType } from '../../../client/activation/types';
+import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types';
+import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants';
+import { TryPylance } from '../../../client/common/experiments/groups';
+import {
+ IConfigurationService,
+ IExperimentService,
+ IExtensions,
+ IPersistentState,
+ IPersistentStateFactory,
+ IPythonSettings
+} from '../../../client/common/types';
+import { LanguageService } from '../../../client/common/utils/localize';
+import {
+ getPylanceExtensionUri,
+ ProposeLSStateKeys,
+ ProposePylanceBanner
+} from '../../../client/languageServices/proposeLanguageServerBanner';
-// import { expect } from 'chai';
-// import * as typemoq from 'typemoq';
-// import { IApplicationShell } from '../../../client/common/application/types';
-// import { IConfigurationService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types';
-// import {
-// ProposeLanguageServerBanner,
-// ProposeLSStateKeys
-// } from '../../../client/languageServices/proposeLanguageServerBanner';
+interface IExperimentLsCombination {
+ inExperiment: boolean;
+ lsType: LanguageServerType;
+ shouldShowBanner: boolean;
+}
+const testData: IExperimentLsCombination[] = [
+ { inExperiment: true, lsType: LanguageServerType.None, shouldShowBanner: true },
+ { inExperiment: true, lsType: LanguageServerType.Microsoft, shouldShowBanner: true },
+ { inExperiment: true, lsType: LanguageServerType.Node, shouldShowBanner: false },
+ { inExperiment: true, lsType: LanguageServerType.Jedi, shouldShowBanner: false },
+ { inExperiment: false, lsType: LanguageServerType.None, shouldShowBanner: false },
+ { inExperiment: false, lsType: LanguageServerType.Microsoft, shouldShowBanner: false },
+ { inExperiment: false, lsType: LanguageServerType.Node, shouldShowBanner: false },
+ { inExperiment: false, lsType: LanguageServerType.Jedi, shouldShowBanner: false }
+];
-// suite('Propose New Language Server Banner', () => {
-// let config: typemoq.IMock;
-// let appShell: typemoq.IMock;
-// const message =
-// 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.';
-// const yes = 'Try it now';
-// const no = 'No thanks';
-// const later = 'Remind me Later';
+suite('Propose Pylance Banner', () => {
+ let config: typemoq.IMock;
+ let appShell: typemoq.IMock;
+ let appEnv: typemoq.IMock;
+ let settings: typemoq.IMock;
-// setup(() => {
-// config = typemoq.Mock.ofType();
-// appShell = typemoq.Mock.ofType();
-// });
-// test('Is debugger enabled upon creation?', () => {
-// const enabledValue: boolean = true;
-// const testBanner: ProposeLanguageServerBanner = preparePopup(enabledValue, 100, appShell.object, config.object);
-// expect(testBanner.enabled).to.be.equal(true, 'Sampling 100/100 should always enable the banner.');
-// });
-// test('Do not show banner when it is disabled', () => {
-// appShell
-// .setup((a) =>
-// a.showInformationMessage(
-// typemoq.It.isValue(message),
-// typemoq.It.isValue(yes),
-// typemoq.It.isValue(no),
-// typemoq.It.isValue(later)
-// )
-// )
-// .verifiable(typemoq.Times.never());
-// const enabled: boolean = true;
-// const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object);
-// testBanner.showBanner().ignoreErrors();
-// });
-// test('shouldShowBanner must return false when Banner is implicitly disabled by sampling', () => {
-// const enabled: boolean = true;
-// const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object);
-// expect(testBanner.enabled).to.be.equal(false, 'We implicitly disabled the banner, it should never show.');
-// });
-// test('shouldShowBanner must return false when Banner is explicitly disabled', async () => {
-// const enabled: boolean = true;
-// const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 100, appShell.object, config.object);
+ const message = LanguageService.proposePylanceMessage();
+ const yes = LanguageService.tryItNow();
+ const no = LanguageService.bannerLabelNo();
+ const later = LanguageService.remindMeLater();
-// expect(await testBanner.shouldShowBanner()).to.be.equal(
-// true,
-// '100% sample size should always make the banner enabled.'
-// );
-// await testBanner.disable();
-// expect(await testBanner.shouldShowBanner()).to.be.equal(
-// false,
-// 'Explicitly disabled banner shouldShowBanner != false.'
-// );
-// });
-// });
+ setup(() => {
+ config = typemoq.Mock.ofType();
+ settings = typemoq.Mock.ofType();
+ config.setup((x) => x.getSettings(typemoq.It.isAny())).returns(() => settings.object);
+ appShell = typemoq.Mock.ofType();
+ appEnv = typemoq.Mock.ofType();
+ appEnv.setup((x) => x.uriScheme).returns(() => 'scheme');
+ });
+ testData.forEach((t) => {
+ test(`${t.inExperiment ? 'In' : 'Not in'} experiment and "python.languageServer": "${t.lsType}" should ${
+ t.shouldShowBanner ? 'show' : 'not show'
+ } banner`, async () => {
+ settings.setup((x) => x.languageServer).returns(() => t.lsType);
+ const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.inExperiment, false);
+ const actual = await testBanner.shouldShowBanner();
+ expect(actual).to.be.equal(t.shouldShowBanner, `shouldShowBanner() returned ${actual}`);
+ });
+ });
+ testData.forEach((t) => {
+ test(`When Pylance is installed, banner should not be shown when "python.languageServer": "${t.lsType}"`, async () => {
+ settings.setup((x) => x.languageServer).returns(() => t.lsType);
+ const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.inExperiment, true);
+ const actual = await testBanner.shouldShowBanner();
+ expect(actual).to.be.equal(false, `shouldShowBanner() returned ${actual}`);
+ });
+ });
+ test('Do not show banner when it is disabled', async () => {
+ appShell
+ .setup((a) =>
+ a.showInformationMessage(
+ typemoq.It.isValue(message),
+ typemoq.It.isValue(yes),
+ typemoq.It.isValue(no),
+ typemoq.It.isValue(later)
+ )
+ )
+ .verifiable(typemoq.Times.never());
+ const testBanner = preparePopup(false, appShell.object, appEnv.object, config.object, true, false);
+ await testBanner.showBanner();
+ appShell.verifyAll();
+ });
+ test('Clicking No should disable the banner', async () => {
+ appShell
+ .setup((a) =>
+ a.showInformationMessage(
+ typemoq.It.isValue(message),
+ typemoq.It.isValue(yes),
+ typemoq.It.isValue(no),
+ typemoq.It.isValue(later)
+ )
+ )
+ .returns(async () => no)
+ .verifiable(typemoq.Times.once());
+ appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.never());
-// function preparePopup(
-// enabledValue: boolean,
-// sampleValue: number,
-// appShell: IApplicationShell,
-// config: IConfigurationService
-// ): ProposeLanguageServerBanner {
-// const myfactory: typemoq.IMock = typemoq.Mock.ofType