diff --git a/.github/ISSUE_TEMPLATE/5_question.md b/.github/ISSUE_TEMPLATE/5_question.md deleted file mode 100644 index 966c6ab477e8..000000000000 --- a/.github/ISSUE_TEMPLATE/5_question.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Question -about: Please search existing issues or Stack Overflow (https://stackoverflow.com/questions/tagged/visual-studio-code+python) to avoid creating duplicates -labels: classify ---- - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..658e6a5d037b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/visual-studio-code+python + about: Please ask questions here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6830e6673f58..31bdd96e7ae6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ TypeScript errors and warnings will be displayed in the `Problems` window of Vis ### Validate your changes To test the changes you launch a development version of VS Code on the workspace vscode, which you are currently editing. -Use the `Launch Extension` launch option. +Use the `Extension` launch option. ### Debugging Unit Tests @@ -147,7 +147,7 @@ To run only the functional tests: ### Standard Debugging -Clone the repo into any directory, open that directory in VSCode, and use the `Launch Extension` launch option within VSCode. +Clone the repo into any directory, open that directory in VSCode, and use the `Extension` launch option within VSCode. ### Debugging the Python Extension Debugger diff --git a/build/.mocha.unittests.ts.opts b/build/.mocha.unittests.ts.opts index f6672aed1db6..9d97e8ef972a 100644 --- a/build/.mocha.unittests.ts.opts +++ b/build/.mocha.unittests.ts.opts @@ -1,8 +1,8 @@ --require ts-node/register ---require out/test/unittests.js +--require src/test/unittests.ts --reporter mocha-multi-reporters --reporter-options configFile=build/.mocha-multi-reporters.config --ui tdd --recursive --colors -./src/test/**/*.unit.test.ts \ No newline at end of file +./src/test/**/*.unit.test.ts diff --git a/build/ci/postInstall.js b/build/ci/postInstall.js index 42f85b47fc46..3198674c4668 100644 --- a/build/ci/postInstall.js +++ b/build/ci/postInstall.js @@ -14,7 +14,7 @@ var constants_1 = require("../constants"); * The solution is to modify the type definition file after `npm install`. */ function fixJupyterLabDTSFiles() { - var relativePath = path.join('node_modules', '@jupyterlab', 'coreutils', 'lib', 'settingregistry.d.ts'); + var relativePath = path.join('node_modules', '@jupyterlab', 'services', 'node_modules', '@jupyterlab', 'coreutils', 'lib', 'settingregistry.d.ts'); var filePath = path.join(constants_1.ExtensionRootDir, relativePath); if (!fs.existsSync(filePath)) { throw new Error("Type Definition file from JupyterLab not found '" + filePath + "' (pvsc post install script)"); diff --git a/build/ci/templates/globals.yml b/build/ci/templates/globals.yml index b12fbcc4909b..f3316f04d335 100644 --- a/build/ci/templates/globals.yml +++ b/build/ci/templates/globals.yml @@ -1,5 +1,5 @@ variables: - PythonVersion: '3.7.4' # Always use latest version. + PythonVersion: '3.7' # Always use latest version. NodeVersion: '10.11.0' # Check version of node used in VS Code. NpmVersion: '6.10.3' MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. diff --git a/build/ci/templates/jobs/uitest.yml b/build/ci/templates/jobs/uitest.yml index 1d03bddcfafb..e13ee26736eb 100644 --- a/build/ci/templates/jobs/uitest.yml +++ b/build/ci/templates/jobs/uitest.yml @@ -95,7 +95,7 @@ parameters: # All scenarios tagged with `@noNeedToTestInAllPython`, will run in the latest version of Python. # When using other versions of Python, ignore `@noNeedToTestInAllPython`. { - "version": "3.7.4", + "version": "3.7", "displayName": "37", "excludeTags": "not @python3.6 and not @python3.5 and not @python2" }, diff --git a/experiments.json b/experiments.json index c6327eeba69d..565efa3b4df6 100644 --- a/experiments.json +++ b/experiments.json @@ -30,16 +30,16 @@ "max": 20 }, { - "name": "DebugAdapterFactory - control", + "name": "DebugAdapterFactoryInsiders - control", "salt": "DebugAdapterFactory", "min": 0, - "max": 0 + "max": 50 }, { - "name": "DebugAdapterFactory - experiment", + "name": "DebugAdapterFactoryInsiders - experiment", "salt": "DebugAdapterFactory", - "min": 0, - "max": 0 + "min": 50, + "max": 100 }, { "name": "PtvsdWheels37 - control", diff --git a/gulpfile.js b/gulpfile.js index b57071c8b46f..9fcfe4e92ece 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -266,7 +266,7 @@ gulp.task('installPtvsdWheels', async () => { }); if (!success) { console.info("Failed to install new PTVSD wheels using 'python3', attempting to install using 'python'"); - await spawnAsync('python', args.concat(requirement)).catch(ex => console.error("Failed to install PTVSD 5.0 wheels using 'python'", ex)); + await spawnAsync('python', args).catch(ex => console.error("Failed to install PTVSD 5.0 wheels using 'python'", ex)); } }); @@ -283,7 +283,7 @@ gulp.task('installOldPtvsd', async () => { }); if (!success) { console.info("Failed to install PTVSD using 'python3', attempting to install using 'python'"); - await spawnAsync('python', args.concat(requirement)).catch(ex => console.error("Failed to install PTVSD using 'python'", ex)); + await spawnAsync('python', args).catch(ex => console.error("Failed to install PTVSD using 'python'", ex)); } }); diff --git a/news/1 Enhancements/8078.md b/news/1 Enhancements/8078.md new file mode 100644 index 000000000000..46d527d74a79 --- /dev/null +++ b/news/1 Enhancements/8078.md @@ -0,0 +1 @@ +Prompt to open exported `Notebook` in the `Notebook Editor`. diff --git a/news/1 Enhancements/8103.md b/news/1 Enhancements/8103.md new file mode 100644 index 000000000000..10e9a46f01ce --- /dev/null +++ b/news/1 Enhancements/8103.md @@ -0,0 +1,2 @@ +Enhance "select a workspace" message when selecting interpreter +(thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) diff --git a/news/1 Enhancements/8106.md b/news/1 Enhancements/8106.md new file mode 100644 index 000000000000..620185d45a9b --- /dev/null +++ b/news/1 Enhancements/8106.md @@ -0,0 +1 @@ +Add logging support for python debug adapter. diff --git a/news/1 Enhancements/8289.md b/news/1 Enhancements/8289.md new file mode 100644 index 000000000000..51c0b0483332 --- /dev/null +++ b/news/1 Enhancements/8289.md @@ -0,0 +1 @@ +Style adjustments to line numbers (color and width) in the `Native Editor`, to line up with VS Code styles. diff --git a/news/1 Enhancements/8320.md b/news/1 Enhancements/8320.md new file mode 100644 index 000000000000..f2da7310e1d5 --- /dev/null +++ b/news/1 Enhancements/8320.md @@ -0,0 +1,2 @@ +Added command translations for Turkish. +(thanks to [alioguzhan](https://github.com/alioguzhan/)) diff --git a/news/2 Fixes/7567.md b/news/2 Fixes/7567.md new file mode 100644 index 000000000000..4e44c44ae022 --- /dev/null +++ b/news/2 Fixes/7567.md @@ -0,0 +1 @@ +When exporting a notebook editor to python script don't use the temp file location for generating the export. \ No newline at end of file diff --git a/news/2 Fixes/7853.md b/news/2 Fixes/7853.md new file mode 100644 index 000000000000..41bec365dc14 --- /dev/null +++ b/news/2 Fixes/7853.md @@ -0,0 +1 @@ +'Clear All Output' now deletes execution count for all cells. diff --git a/news/2 Fixes/7873.md b/news/2 Fixes/7873.md new file mode 100644 index 000000000000..fea20e6091bc --- /dev/null +++ b/news/2 Fixes/7873.md @@ -0,0 +1 @@ +Fix strings of commas appearing in text/html output in the notebook editor. \ No newline at end of file diff --git a/news/2 Fixes/7980.md b/news/2 Fixes/7980.md new file mode 100644 index 000000000000..d7c342489d5f --- /dev/null +++ b/news/2 Fixes/7980.md @@ -0,0 +1 @@ +When creating a new blank notebook, it has existing text in it already. diff --git a/news/2 Fixes/7992.md b/news/2 Fixes/7992.md new file mode 100644 index 000000000000..b5acc8d9dd92 --- /dev/null +++ b/news/2 Fixes/7992.md @@ -0,0 +1 @@ +Can now include a LaTeX-style equation without surrounding the equation with '$' in a markdown cell. diff --git a/news/2 Fixes/8003.md b/news/2 Fixes/8003.md new file mode 100644 index 000000000000..898b7701b358 --- /dev/null +++ b/news/2 Fixes/8003.md @@ -0,0 +1 @@ +Make a spinner appear during executing a cell. diff --git a/news/2 Fixes/8006.md b/news/2 Fixes/8006.md new file mode 100644 index 000000000000..8c880cc2450e --- /dev/null +++ b/news/2 Fixes/8006.md @@ -0,0 +1 @@ +Signature help is overflowing out of the signature help widget on the Notebook Editor. diff --git a/news/2 Fixes/8019.md b/news/2 Fixes/8019.md new file mode 100644 index 000000000000..66b2505089f1 --- /dev/null +++ b/news/2 Fixes/8019.md @@ -0,0 +1 @@ +Correctly restart jupyter sessions when the active interpreter is changed. \ No newline at end of file diff --git a/news/2 Fixes/8021.md b/news/2 Fixes/8021.md new file mode 100644 index 000000000000..479b3442d628 --- /dev/null +++ b/news/2 Fixes/8021.md @@ -0,0 +1 @@ +Clear up wording around jupyterServerURI and remove the quick pick from the flow of setting that. \ No newline at end of file diff --git a/news/2 Fixes/8039.md b/news/2 Fixes/8039.md new file mode 100644 index 000000000000..2c974d201eaf --- /dev/null +++ b/news/2 Fixes/8039.md @@ -0,0 +1 @@ +Minimize the GPU impact of the interactive window and the notebook editor. diff --git a/news/2 Fixes/8084.md b/news/2 Fixes/8084.md new file mode 100644 index 000000000000..b148695f9c5d --- /dev/null +++ b/news/2 Fixes/8084.md @@ -0,0 +1 @@ +When checking the version of `pandas`, use the same interpreter used to start `Jupyter`. diff --git a/news/2 Fixes/8132.md b/news/2 Fixes/8132.md new file mode 100644 index 000000000000..0cf1677adae0 --- /dev/null +++ b/news/2 Fixes/8132.md @@ -0,0 +1 @@ +Cannot create more than one blank notebook. diff --git a/news/2 Fixes/8151.md b/news/2 Fixes/8151.md new file mode 100644 index 000000000000..cc56d3b5bb86 --- /dev/null +++ b/news/2 Fixes/8151.md @@ -0,0 +1 @@ +Support `⌘+s` keyboard shortcut for saving `Notebooks`. diff --git a/news/2 Fixes/8205.md b/news/2 Fixes/8205.md new file mode 100644 index 000000000000..9f1702ce3473 --- /dev/null +++ b/news/2 Fixes/8205.md @@ -0,0 +1 @@ +Scroll the notebook editor when giving focus or changing line of a code cell. diff --git a/news/2 Fixes/8215.md b/news/2 Fixes/8215.md new file mode 100644 index 000000000000..7cd48a27f9d4 --- /dev/null +++ b/news/2 Fixes/8215.md @@ -0,0 +1 @@ +Prevent code from changing in the Notebook Editor while running a cell. diff --git a/news/2 Fixes/8263.md b/news/2 Fixes/8263.md new file mode 100644 index 000000000000..84e5069390b1 --- /dev/null +++ b/news/2 Fixes/8263.md @@ -0,0 +1 @@ +When updating the Python extension, unsaved changes to notebooks are lost. diff --git a/news/2 Fixes/8296.md b/news/2 Fixes/8296.md new file mode 100644 index 000000000000..3a3efed45c74 --- /dev/null +++ b/news/2 Fixes/8296.md @@ -0,0 +1 @@ +Fix CI to use Python 3.7.5. diff --git a/news/3 Code Health/6065.md b/news/3 Code Health/6065.md new file mode 100644 index 000000000000..adc8071b9e50 --- /dev/null +++ b/news/3 Code Health/6065.md @@ -0,0 +1 @@ +Add unit tests for src/client/common/process/pythonProcess.ts. diff --git a/news/3 Code Health/7809.md b/news/3 Code Health/7809.md new file mode 100644 index 000000000000..9aed9516303a --- /dev/null +++ b/news/3 Code Health/7809.md @@ -0,0 +1 @@ +Update Test Explorer icons to match new VS Code icons \ No newline at end of file diff --git a/news/3 Code Health/8255.md b/news/3 Code Health/8255.md new file mode 100644 index 000000000000..fcfe36913369 --- /dev/null +++ b/news/3 Code Health/8255.md @@ -0,0 +1 @@ +Timeout with new waitForMessage in native editor tests. diff --git a/news/3 Code Health/8280.md b/news/3 Code Health/8280.md new file mode 100644 index 000000000000..3a6b6e771061 --- /dev/null +++ b/news/3 Code Health/8280.md @@ -0,0 +1 @@ +Remove code used to track perf of creation classes. diff --git a/package-lock.json b/package-lock.json index 5abaca6979da..39679948ba9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1197,202 +1197,22 @@ "integrity": "sha512-EzRFg92bRSD1W/zeuNkeGwph0nkWf+pP2l/lYW4/5hav7RjKKBN5kV1Ix7Tvi0CMu3pC4Wi/U7rNisiJMR3ORg==", "dev": true }, - "@jupyterlab/apputils": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-0.19.1.tgz", - "integrity": "sha512-//vajDyVyKwXU7qycRBJC37dljjWeya+YpJV3z5sSXgVBefEBZ0kcrJ92ugDSht4I4SRv6x+kw6K9mBXKaYu9Q==", - "requires": { - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/services": "^3.2.1", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/commands": "^1.6.1", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/domutils": "^1.1.2", - "@phosphor/messaging": "^1.2.2", - "@phosphor/properties": "^1.1.2", - "@phosphor/signaling": "^1.2.2", - "@phosphor/virtualdom": "^1.1.2", - "@phosphor/widgets": "^1.6.0", - "@types/react": "~16.4.13", - "react": "~16.4.2", - "react-dom": "~16.4.2", - "sanitize-html": "~1.18.2" - }, - "dependencies": { - "@types/react": { - "version": "16.4.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.18.tgz", - "integrity": "sha512-eFzJKEg6pdeaukVLVZ8Xb79CTl/ysX+ExmOfAAqcFlCCK5TgFDD9kWR0S18sglQ3EmM8U+80enjUqbfnUyqpdA==", - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" - } - }, - "react": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz", - "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==", - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - } - }, - "react-dom": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.2.tgz", - "integrity": "sha512-Usl73nQqzvmJN+89r97zmeUpQDKDlh58eX6Hbs/ERdDHzeBzWy+ENk7fsGQ+5KxArV1iOFPT46/VneklK9zoWw==", - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - } - } - } - }, - "@jupyterlab/attachments": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/attachments/-/attachments-0.19.1.tgz", - "integrity": "sha512-JujH1ExOao6ytb0J305iqOtj+GxISq/9zgt9kugwUCYZpvGdHau7WAYQs5YDPIQSZjSdYzjBREssFoirOOSVmA==", - "requires": { - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@jupyterlab/rendermime": "^0.19.1", - "@jupyterlab/rendermime-interfaces": "^1.2.1", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2" - } - }, - "@jupyterlab/cells": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/cells/-/cells-0.19.1.tgz", - "integrity": "sha512-7vVriCTVC6sXPDvh1/L2WzdAga2Xkg0cFgEcXuIJLwt25ppmqs9DYgMhhUBr0G734mZwmQDXCJ/BqOUw1qgHKg==", - "requires": { - "@jupyterlab/apputils": "^0.19.1", - "@jupyterlab/attachments": "^0.19.1", - "@jupyterlab/codeeditor": "^0.19.1", - "@jupyterlab/codemirror": "^0.19.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@jupyterlab/outputarea": "^0.19.1", - "@jupyterlab/rendermime": "^0.19.1", - "@jupyterlab/services": "^3.2.1", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/messaging": "^1.2.2", - "@phosphor/signaling": "^1.2.2", - "@phosphor/widgets": "^1.6.0", - "react": "~16.4.2" - }, - "dependencies": { - "react": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz", - "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==", - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - } - } - } - }, - "@jupyterlab/codeeditor": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-0.19.1.tgz", - "integrity": "sha512-mCL4YiCCX5JOAIs21mleSmlkejAw6FVLwmvKGxBCwoNj+cSYUKzGBYuA2xvxAZi/5HaiQU8R+ITbAwk2QoMc6w==", - "requires": { - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/messaging": "^1.2.2", - "@phosphor/signaling": "^1.2.2", - "@phosphor/widgets": "^1.6.0", - "react": "~16.4.2", - "react-dom": "~16.4.2" - }, - "dependencies": { - "react": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz", - "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==", - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - } - }, - "react-dom": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.2.tgz", - "integrity": "sha512-Usl73nQqzvmJN+89r97zmeUpQDKDlh58eX6Hbs/ERdDHzeBzWy+ENk7fsGQ+5KxArV1iOFPT46/VneklK9zoWw==", - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.0" - } - } - } - }, - "@jupyterlab/codemirror": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-0.19.1.tgz", - "integrity": "sha512-JbZRm9vW1lN4Y4VghBQEys7vWoaZM54E0OdTlWjJq1kF8WhKWzp8Do/tnQ5fKVUGw40BMB0xBnfAaHHCSs5vHw==", - "requires": { - "@jupyterlab/apputils": "^0.19.1", - "@jupyterlab/codeeditor": "^0.19.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2", - "codemirror": "~5.39.0" - } - }, "@jupyterlab/coreutils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.tgz", - "integrity": "sha512-XkGMBXqEAnENC4fK/L3uEqqxyNUtf4TI/1XNDln7d5VOPHQJSBTbYlBAZ0AQotn2qbs4WqmV6icxje2ITVedqQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.1.0.tgz", + "integrity": "sha512-ZqgzDUyanyvc86gtCrIbc1M6iniKHYmWNWHvWOcnq3KIP3wk3grchsTYPTfQDxcUS6F04baPGp/KohEU2ml40Q==", "requires": { - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2", - "ajv": "~5.1.6", - "comment-json": "^1.1.3", + "@phosphor/commands": "^1.6.3", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.2.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.2.3", + "ajv": "^6.5.5", + "json5": "^2.1.0", "minimist": "~1.2.0", - "moment": "~2.21.0", + "moment": "^2.24.0", "path-posix": "~1.0.0", "url-parse": "~1.4.3" - }, - "dependencies": { - "ajv": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.1.6.tgz", - "integrity": "sha1-Sy8aGd7Ok9V6whYDfj6XkcfdFWQ=", - "requires": { - "co": "^4.6.0", - "json-schema-traverse": "^0.3.0", - "json-stable-stringify": "^1.0.1" - } - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "moment": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", - "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" - } } }, "@jupyterlab/observables": { @@ -1407,54 +1227,6 @@ "@phosphor/signaling": "^1.2.3" } }, - "@jupyterlab/outputarea": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@jupyterlab/outputarea/-/outputarea-0.19.2.tgz", - "integrity": "sha512-uYL/RfwY//83UWsmENg4b4s6wXr9OWE4zbKtMY4zoJoT5tsnWyIiSnvy26m/Pslo1Y1wlLjVPqCoJFcEARKb+A==", - "requires": { - "@jupyterlab/apputils": "^0.19.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@jupyterlab/rendermime": "^0.19.1", - "@jupyterlab/rendermime-interfaces": "^1.2.1", - "@jupyterlab/services": "^3.2.1", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/messaging": "^1.2.2", - "@phosphor/signaling": "^1.2.2", - "@phosphor/widgets": "^1.6.0" - } - }, - "@jupyterlab/rendermime": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-0.19.1.tgz", - "integrity": "sha512-MD+/lMwFa/b9QtJyFghTXFqkuRha74+vWTaPcfzREdHRiKYHYPHWOD/gb8cLIr4c7J3VaEVZWEpI8udYgyANvA==", - "requires": { - "@jupyterlab/apputils": "^0.19.1", - "@jupyterlab/codemirror": "^0.19.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@jupyterlab/rendermime-interfaces": "^1.2.1", - "@jupyterlab/services": "^3.2.1", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/messaging": "^1.2.2", - "@phosphor/signaling": "^1.2.2", - "@phosphor/widgets": "^1.6.0", - "ansi_up": "^3.0.0", - "marked": "~0.4.0" - } - }, - "@jupyterlab/rendermime-interfaces": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-1.4.0.tgz", - "integrity": "sha512-6GtbudtK7Yl37ldnQUQ6hTUDzjVgemiDXokEdsSTLPOPoPwKw3gaEBfxrOz/LBITfqIfbpuBzYU8lQ4rHq0TfQ==", - "requires": { - "@phosphor/coreutils": "^1.3.1", - "@phosphor/widgets": "^1.8.0" - } - }, "@jupyterlab/services": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-3.2.1.tgz", @@ -1517,15 +1289,9 @@ } }, "@msrvida/python-program-analysis": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@msrvida/python-program-analysis/-/python-program-analysis-0.2.6.tgz", - "integrity": "sha512-DlclIdsuwMktplZlxhTCky0fLIAy925MCEWKqdU/6PLV6L+FiG7jZ7/V2qZohjmBdJEuJoifERWzkUrP+PaH0A==", - "requires": { - "@jupyterlab/cells": "^0.19.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/rendermime": "^0.19.1", - "js-yaml": "^3.13.1" - } + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@msrvida/python-program-analysis/-/python-program-analysis-0.4.1.tgz", + "integrity": "sha512-8jCtPTTxXyvN3udvz71inScvFvQil7Wh61newdrq79CjuV0W634GCDGuyGpI5G+kgX9PbBZPQJTc1+BMNMB5sQ==" }, "@nteract/markdown": { "version": "3.0.1", @@ -1688,16 +1454,16 @@ } }, "@phosphor/commands": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@phosphor/commands/-/commands-1.7.1.tgz", - "integrity": "sha512-KELPYLrNLVkMA5XntDogQkKXWbhLhpjxLBD75faywoe4GCyVsm//CA7Wn50+eVo0pI87z27Qbtzo0TR6NH4Jvw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@phosphor/commands/-/commands-1.7.2.tgz", + "integrity": "sha512-iSyBIWMHsus323BVEARBhuVZNnVel8USo+FIPaAxGcq+icTSSe6+NtSxVQSmZblGN6Qm4iw6I6VtiSx0e6YDgQ==", "requires": { "@phosphor/algorithm": "^1.2.0", "@phosphor/coreutils": "^1.3.1", - "@phosphor/disposable": "^1.3.0", + "@phosphor/disposable": "^1.3.1", "@phosphor/domutils": "^1.1.4", "@phosphor/keyboard": "^1.1.3", - "@phosphor/signaling": "^1.3.0" + "@phosphor/signaling": "^1.3.1" }, "dependencies": { "@phosphor/algorithm": { @@ -1706,18 +1472,18 @@ "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" }, "@phosphor/disposable": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.0.tgz", - "integrity": "sha512-wHQov7HoS20mU6yuEz5ZMPhfxHdcxGovjPoid0QwccUEOm33UBkWlxaJGm9ONycezIX8je7ZuPOf/gf7JI6Dlg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", "requires": { "@phosphor/algorithm": "^1.2.0", - "@phosphor/signaling": "^1.3.0" + "@phosphor/signaling": "^1.3.1" } }, "@phosphor/signaling": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.0.tgz", - "integrity": "sha512-ZbG2Mof4LGSkaEuDicqA2o2TKu3i5zanjr2GkevI/82aKBD7cI1NGLGT55HZwtE87/gOF4FIM3d3DeyrFDMjMQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", "requires": { "@phosphor/algorithm": "^1.2.0" } @@ -1743,39 +1509,6 @@ "resolved": "https://registry.npmjs.org/@phosphor/domutils/-/domutils-1.1.4.tgz", "integrity": "sha512-ivwq5TWjQpKcHKXO8PrMl+/cKqbgxPClPiCKc1gwbMd+6hnW5VLwNG0WBzJTxCzXK43HxX18oH+tOZ3E04wc3w==" }, - "@phosphor/dragdrop": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@phosphor/dragdrop/-/dragdrop-1.4.0.tgz", - "integrity": "sha512-JqmDAKczviUe7NEkiDf/A6H2glgVmHAREip8dGBli4lvV+CQqPFyl4Xm7XCnR9qiEqNrP+0SfwPpywNa0me3nQ==", - "requires": { - "@phosphor/coreutils": "^1.3.1", - "@phosphor/disposable": "^1.3.0" - }, - "dependencies": { - "@phosphor/algorithm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", - "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" - }, - "@phosphor/disposable": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.0.tgz", - "integrity": "sha512-wHQov7HoS20mU6yuEz5ZMPhfxHdcxGovjPoid0QwccUEOm33UBkWlxaJGm9ONycezIX8je7ZuPOf/gf7JI6Dlg==", - "requires": { - "@phosphor/algorithm": "^1.2.0", - "@phosphor/signaling": "^1.3.0" - } - }, - "@phosphor/signaling": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.0.tgz", - "integrity": "sha512-ZbG2Mof4LGSkaEuDicqA2o2TKu3i5zanjr2GkevI/82aKBD7cI1NGLGT55HZwtE87/gOF4FIM3d3DeyrFDMjMQ==", - "requires": { - "@phosphor/algorithm": "^1.2.0" - } - } - } - }, "@phosphor/keyboard": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@phosphor/keyboard/-/keyboard-1.1.3.tgz", @@ -1803,80 +1536,6 @@ "@phosphor/algorithm": "^1.1.3" } }, - "@phosphor/virtualdom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz", - "integrity": "sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w==", - "requires": { - "@phosphor/algorithm": "^1.2.0" - }, - "dependencies": { - "@phosphor/algorithm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", - "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" - } - } - }, - "@phosphor/widgets": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@phosphor/widgets/-/widgets-1.9.2.tgz", - "integrity": "sha512-93BOdp3lGsEdkpa+bv+XhIKqRyNKStu71IeDtvqiRyRyMjDnMjwfQCjNDdYbsWYyWWKk4TH7buC0cIlmbIPAHQ==", - "requires": { - "@phosphor/algorithm": "^1.2.0", - "@phosphor/commands": "^1.7.1", - "@phosphor/coreutils": "^1.3.1", - "@phosphor/disposable": "^1.3.0", - "@phosphor/domutils": "^1.1.4", - "@phosphor/dragdrop": "^1.4.0", - "@phosphor/keyboard": "^1.1.3", - "@phosphor/messaging": "^1.3.0", - "@phosphor/properties": "^1.1.3", - "@phosphor/signaling": "^1.3.0", - "@phosphor/virtualdom": "^1.2.0" - }, - "dependencies": { - "@phosphor/algorithm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", - "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" - }, - "@phosphor/collections": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@phosphor/collections/-/collections-1.2.0.tgz", - "integrity": "sha512-T9/0EjSuY6+ga2LIFRZ0xupciOR3Qnyy8Q95lhGTC0FXZUFwC8fl9e8On6IcwasCszS+1n8dtZUWSIynfgdpzw==", - "requires": { - "@phosphor/algorithm": "^1.2.0" - } - }, - "@phosphor/disposable": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.0.tgz", - "integrity": "sha512-wHQov7HoS20mU6yuEz5ZMPhfxHdcxGovjPoid0QwccUEOm33UBkWlxaJGm9ONycezIX8je7ZuPOf/gf7JI6Dlg==", - "requires": { - "@phosphor/algorithm": "^1.2.0", - "@phosphor/signaling": "^1.3.0" - } - }, - "@phosphor/messaging": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.3.0.tgz", - "integrity": "sha512-k0JE+BTMKlkM335S2AmmJxoYYNRwOdW5jKBqLgjJdGRvUQkM0+2i60ahM45+J23atGJDv9esKUUBINiKHFhLew==", - "requires": { - "@phosphor/algorithm": "^1.2.0", - "@phosphor/collections": "^1.2.0" - } - }, - "@phosphor/signaling": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.0.tgz", - "integrity": "sha512-ZbG2Mof4LGSkaEuDicqA2o2TKu3i5zanjr2GkevI/82aKBD7cI1NGLGT55HZwtE87/gOF4FIM3d3DeyrFDMjMQ==", - "requires": { - "@phosphor/algorithm": "^1.2.0" - } - } - } - }, "@sindresorhus/is": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", @@ -2248,7 +1907,8 @@ "@types/prop-types": { "version": "15.7.1", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", - "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==", + "dev": true }, "@types/react": { "version": "16.8.23", @@ -2866,11 +2526,6 @@ "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", "dev": true }, - "ansi_up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-3.0.0.tgz", - "integrity": "sha1-J/Rdj0V9nO/1nk6gPI5vE8GjA+g=" - }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -3285,7 +2940,8 @@ "array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true }, "array-unique": { "version": "0.3.2", @@ -3323,7 +2979,8 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true }, "asn1": { "version": "0.2.4", @@ -5626,11 +5283,6 @@ "urlgrey": "^0.4.4" } }, - "codemirror": { - "version": "5.39.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.39.2.tgz", - "integrity": "sha512-mchBy0kQ1Wggi+e58SmoLgKO4nG7s/BqNg6/6TRbhsnXI/KRG+fKAvRQ1LLhZZ6ZtUoDQ0dl5aMhE+IkSRh60Q==" - }, "collapse-white-space": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.5.tgz", @@ -6057,7 +5709,8 @@ "core-js": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true }, "core-js-compat": { "version": "3.1.4", @@ -6567,7 +6220,8 @@ "csstype": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", - "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==" + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", + "dev": true }, "cucumber-html-reporter": { "version": "4.0.5", @@ -7441,6 +7095,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, "requires": { "domelementtype": "^1.3.0", "entities": "^1.1.1" @@ -7466,7 +7121,8 @@ "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true }, "domexception": { "version": "1.0.1", @@ -7481,6 +7137,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, "requires": { "domelementtype": "1" } @@ -7489,6 +7146,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -7783,7 +7441,8 @@ "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true }, "env-paths": { "version": "1.0.0", @@ -8633,6 +8292,7 @@ "version": "0.8.17", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "dev": true, "requires": { "core-js": "^1.0.0", "isomorphic-fetch": "^2.1.1", @@ -10762,6 +10422,7 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, "requires": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", @@ -11588,6 +11249,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, "requires": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" @@ -11946,7 +11608,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, "requires": { "minimist": "^1.2.0" } @@ -12313,11 +11974,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", @@ -12342,11 +11998,6 @@ "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", "dev": true }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" - }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", @@ -12380,17 +12031,8 @@ "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true }, "lodash.some": { "version": "4.6.0", @@ -12620,11 +12262,6 @@ "uc.micro": "^1.0.5" } }, - "marked": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.4.0.tgz", - "integrity": "sha512-tMsdNBgOsrUophCAFQl0XPe6Zqk/uy9gnue+jIIKhykO51hxyu6uNx7zBPy0+y/WKYVZZMspV9YeXLNdKk+iYw==" - }, "martinez-polygon-clipping": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz", @@ -13325,13 +12962,12 @@ "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", - "dev": true + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "monaco-editor": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.16.2.tgz", - "integrity": "sha512-NtGrFzf54jADe7qsWh3lazhS7Kj0XHkJUGBq9fA/Jbwc+sgVcyfsYF6z2AQ7hPqDC+JmdOt/OwFjBnRwqXtx6w==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz", + "integrity": "sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw==", "dev": true }, "monaco-editor-textmate": { @@ -13933,7 +13569,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "numeral": { "version": "2.0.6", @@ -15063,16 +14700,6 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, "postcss-modules-extract-imports": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", @@ -15307,6 +14934,7 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, "requires": { "asap": "~2.0.3" } @@ -16643,23 +16271,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sanitize-html": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.18.5.tgz", - "integrity": "sha512-z0MV+AqOnDZVSQQHr/vwimRykKVyPuGZnjWDzIiV1mdgQEG9HMx9qrEapcOQeUmSsPvHZ04BXTuXQkB/vvbU9A==", - "requires": { - "chalk": "^2.3.0", - "htmlparser2": "^3.9.0", - "lodash.clonedeep": "^4.5.0", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.mergewith": "^4.6.0", - "postcss": "^6.0.14", - "srcset": "^1.0.0", - "xtend": "^4.0.0" - } - }, "sass-loader": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", @@ -16922,7 +16533,8 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true }, "setprototypeof": { "version": "1.1.1", @@ -17379,15 +16991,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "srcset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz", - "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=", - "requires": { - "array-uniq": "^1.0.2", - "number-is-nan": "^1.0.0" - } - }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -18918,7 +18521,8 @@ "ua-parser-js": { "version": "0.7.20", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", - "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" + "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==", + "dev": true }, "uc.micro": { "version": "1.0.6", @@ -20082,7 +19686,8 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", + "dev": true }, "whatwg-mimetype": { "version": "2.3.0", diff --git a/package.json b/package.json index f7b874406d61..38900e84d328 100644 --- a/package.json +++ b/package.json @@ -1526,7 +1526,7 @@ "python.dataScience.jupyterServerURI": { "type": "string", "default": "local", - "description": "Select the Jupyter server URI to connect to. Select 'local' to launch a new Jupyter server on the local machine.", + "description": "When a Notebook Editor or Interactive Window session is started, create the kernel on the specified Jupyter server. Select 'local' to create a new Jupyter server on this local machine.", "scope": "resource" }, "python.dataScience.notebookFileRoot": { @@ -1693,9 +1693,15 @@ "scope": "resource" }, "python.dataScience.enableGather": { + "type": "boolean", + "default": true, + "description": "Enable code gather for executed cells. For a gathered cell, that cell and only the code it depends on will be exported to a new notebook.", + "scope": "resource" + }, + "python.dataScience.gatherToScript": { "type": "boolean", "default": false, - "description": "Enable code gathering for single cells in the interactive window (experimental).", + "description": "Gather code to a python script rather than a notebook.", "scope": "resource" }, "python.dataScience.codeLenses": { @@ -2679,7 +2685,8 @@ "dependencies": { "@blueprintjs/select": "3.10.0", "@jupyterlab/services": "^3.2.1", - "@msrvida/python-program-analysis": "^0.2.6", + "@jupyterlab/coreutils": "^3.1.0", + "@msrvida/python-program-analysis": "^0.4.1", "ansi-regex": "^4.1.0", "arch": "^2.1.0", "azure-storage": "^2.10.3", @@ -2856,7 +2863,7 @@ "mocha": "^6.1.4", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", - "monaco-editor": "0.16.2", + "monaco-editor": "0.18.1", "monaco-editor-webpack-plugin": "^1.7.0", "nock": "^10.0.6", "node-has-native-dependencies": "^1.0.2", diff --git a/package.nls.json b/package.nls.json index 214b992680d4..6bc1eea59bcd 100644 --- a/package.nls.json +++ b/package.nls.json @@ -50,7 +50,7 @@ "python.command.python.datascience.execSelectionInteractive.title": "Run Selection/Line in Python Interactive Window", "python.command.python.datascience.runcell.title": "Run Cell", "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", - "python.command.python.datascience.selectjupyteruri.title": "Specify Jupyter Server URI", + "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", "python.command.python.datascience.importnotebook.title": "Import Jupyter Notebook", "python.command.python.datascience.opennotebook.title": "Open in Notebook Editor", "python.command.python.datascience.importnotebookfile.title": "Convert to Python Script", @@ -184,10 +184,9 @@ "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", "DataScience.libraryNotInstalled": "Data Science library {0} is not installed. Install?", "DataScience.jupyterInstall": "Install", - "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", - "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", - "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", + "DataScience.jupyterSelectURIPrompt": "Enter the URI of the running Jupyter server or specify 'local' to create one locally when needed", "DataScience.jupyterSelectURIInvalidURI": "Invalid URI specified", + "DataScience.jupyterSelectWatermarkFormat": "{0} OR {1}", "DataScience.jupyterSelectPasswordPrompt": "Enter your notebook password", "DataScience.jupyterNotebookFailure": "Jupyter notebook failed to launch. \r\n{0}", "DataScience.jupyterNotebookConnectFailed": "Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}", @@ -273,7 +272,7 @@ "Common.openOutputPanel": "Show output", "LanguageService.downloadFailedOutputMessage": "Language server download failed", "LanguageService.extractionFailedOutputMessage": "Language server extraction failed", - "LanguageService.extractionCompletedOutputMessage": "Language server dowload complete", + "LanguageService.extractionCompletedOutputMessage": "Language server download complete", "LanguageService.extractionDoneOutputMessage": "done", "LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly", "DataScience.variableExplorerNameColumn": "Name", @@ -378,5 +377,7 @@ "DataScience.notebookNotFound" : "python -m jupyter notebook --version is not running", "DataScience.findJupyterCommandProgress" : "Active interpreter does not support {0}. Searching for the best available interpreter.", "DataScience.findJupyterCommandProgressCheckInterpreter": "Checking {0}.", - "DataScience.findJupyterCommandProgressSearchCurrentPath": "Searching current path." + "DataScience.findJupyterCommandProgressSearchCurrentPath": "Searching current path.", + "DataScience.gatheredScriptDescription": "# This file contains only the code required to produce the results of the gathered cell.\n", + "DataScience.gatheredNotebookDescriptionInMarkdown": "# Gathered Notebook\nGenerated from ```{0}```\n\nThis notebook contains only the code and cells required to produce the same results as the gathered cell.\n\nPlease 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." } diff --git a/package.nls.nl.json b/package.nls.nl.json index 4197ddaa9e75..fbd609feb622 100644 --- a/package.nls.nl.json +++ b/package.nls.nl.json @@ -98,10 +98,6 @@ "DataScience.pythonRestartHeader": "Kernel herstart:", "Linter.InstalledButNotEnabled": "Linter {0} is geinstalleerd maar niet ingeschakeld.", "Linter.replaceWithSelectedLinter": "Meerdere linters zijn ingeschakeld in de instellingen. Vervangen met '{0}'?", - "DataScience.jupyterSelectURILaunchLocal": "Een lokale Jupyter-server starten wanneer nodig", - "DataScience.jupyterSelectURISpecifyURI": "Voer de URI in om te verbinden met een draaiende Jupiter-server", - "DataScience.jupyterSelectURIPrompt": "Voer de URI in van een Jupiter-server", - "DataScience.jupyterSelectURIInvalidURI": "Ongeldige URI gespecificeerd", "DataScience.jupyterNotebookFailure": "Jupyter-notebook kon niet starten. \r\n{0}", "DataScience.jupyterNotebookConnectFailed": "Verbinden met Jupiter-notebook is niet gelukt. \r\n{0}\r\n{1}", "DataScience.notebookVersionFormat": "Jupyter-notebook versie: {0}", diff --git a/package.nls.tr.json b/package.nls.tr.json new file mode 100644 index 000000000000..6164d873ed3e --- /dev/null +++ b/package.nls.tr.json @@ -0,0 +1,33 @@ +{ + "python.command.python.sortImports.title": "Import İfadelerini Sırala", + "python.command.python.startREPL.title": "REPL Başlat", + "python.command.python.createTerminal.title": "Terminal Oluştur", + "python.command.python.buildWorkspaceSymbols.title": "Çalışma Alanındaki Sembolleri Derle", + "python.command.python.runtests.title": "Testleri Çalıştır", + "python.command.python.debugtests.title": "Testleri Debug Et", + "python.command.python.execInTerminal.title": "Terminalde Çalıştır", + "python.command.python.setInterpreter.title": "Bir Interpreter Seçin", + "python.command.python.refactorExtractVariable.title": "Değişken Çıkar", + "python.command.python.refactorExtractMethod.title": "Metot Çıkar", + "python.command.python.viewTestOutput.title": "Test Çıktısını Görüntüle", + "python.command.python.selectAndRunTestMethod.title": "Test Metodu Çalıştır", + "python.command.python.selectAndDebugTestMethod.title": "Test Metodu Debug Et", + "python.command.python.selectAndRunTestFile.title": "Bir Test Dosyası Seç ve Çalıştır", + "python.command.python.runCurrentTestFile.title": "Aktif Test Dosyasını Çalıştır", + "python.command.python.runFailedTests.title": "Başarısız Testleri Çalıştır", + "python.command.python.discoverTests.title": "Testleri Keşfet", + "python.command.python.discoveringTests.title": "Testler Keşfediliyor...", + "python.command.python.execSelectionInTerminal.title": "Seçimi/Satırı Terminalde Çalıştır", + "python.command.python.execSelectionInDjangoShell.title": "Seçimi/Satırı Django Shell'inde Çalıştır", + "python.command.python.goToPythonObject.title": "Python Nesnesine Git", + "python.command.python.setLinter.title": "Bir Linter Seç", + "python.command.python.enableLinting.title": "Linting'i Aktifleştir", + "python.command.python.runLinting.title": "Linter Çalıştır", + "python.snippet.launch.standard.label": "Python: Geçerli Dosya", + "python.snippet.launch.module.label": "Python: Modül", + "python.snippet.launch.module.default": "modül-adını-yazın", + "python.snippet.launch.attach.label": "Python: Remote Attach", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid Uygulaması" + } diff --git a/resources/dark/debug.svg b/resources/dark/debug.svg index ac144d8cdbee..ff7828487e9a 100644 --- a/resources/dark/debug.svg +++ b/resources/dark/debug.svg @@ -1,7 +1,3 @@ - - - - - - + + diff --git a/resources/dark/refresh.svg b/resources/dark/refresh.svg index d79fdaa4e8e4..0442b2af7322 100644 --- a/resources/dark/refresh.svg +++ b/resources/dark/refresh.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/resources/dark/repl.svg b/resources/dark/repl.svg index a9f836e0845a..1e2d3b4ee13d 100644 --- a/resources/dark/repl.svg +++ b/resources/dark/repl.svg @@ -1 +1,3 @@ -repl \ No newline at end of file + + + diff --git a/resources/dark/run-failed-tests.svg b/resources/dark/run-failed-tests.svg index b5b32569a69d..27b4d3c7fc77 100644 --- a/resources/dark/run-failed-tests.svg +++ b/resources/dark/run-failed-tests.svg @@ -1 +1,11 @@ -RunFailedTest_16x \ No newline at end of file + + + + + + + + + + + diff --git a/resources/dark/run-tests.svg b/resources/dark/run-tests.svg index eda7bc169f93..9ccf37eb6031 100644 --- a/resources/dark/run-tests.svg +++ b/resources/dark/run-tests.svg @@ -1 +1,3 @@ -StartTestGroupWithDebug_16x \ No newline at end of file + + + diff --git a/resources/dark/start.svg b/resources/dark/start.svg index 85cb1025609c..53478e4ec0d4 100644 --- a/resources/dark/start.svg +++ b/resources/dark/start.svg @@ -1 +1,3 @@ -continue \ No newline at end of file + + + diff --git a/resources/dark/status-error.svg b/resources/dark/status-error.svg index e4bc6b3f186d..8caeaf036dd3 100644 --- a/resources/dark/status-error.svg +++ b/resources/dark/status-error.svg @@ -1 +1,3 @@ -StatusCriticalError_16x \ No newline at end of file + + + diff --git a/resources/dark/status-ok.svg b/resources/dark/status-ok.svg index 190ea318b635..4e281aac381b 100644 --- a/resources/dark/status-ok.svg +++ b/resources/dark/status-ok.svg @@ -1 +1,4 @@ - + + + + diff --git a/resources/dark/status-unknown.svg b/resources/dark/status-unknown.svg index fa03a7b7a55a..960fb0aa27fe 100644 --- a/resources/dark/status-unknown.svg +++ b/resources/dark/status-unknown.svg @@ -1 +1,3 @@ -StatusHelp_grey_16x \ No newline at end of file + + + diff --git a/resources/dark/stop.svg b/resources/dark/stop.svg index ee59f1cb24f9..b30b6e346d6c 100644 --- a/resources/dark/stop.svg +++ b/resources/dark/stop.svg @@ -1 +1,4 @@ -StatusStop_16x \ No newline at end of file + + + + diff --git a/resources/light/debug.svg b/resources/light/debug.svg index 6755c15bf695..8510a05d742b 100644 --- a/resources/light/debug.svg +++ b/resources/light/debug.svg @@ -1,7 +1,3 @@ - - - - - - + + diff --git a/resources/light/refresh.svg b/resources/light/refresh.svg index e0345748192e..8ade09dfae59 100644 --- a/resources/light/refresh.svg +++ b/resources/light/refresh.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/resources/light/repl.svg b/resources/light/repl.svg index 7087bd011e14..429cb22b71f5 100644 --- a/resources/light/repl.svg +++ b/resources/light/repl.svg @@ -1 +1,3 @@ -repl \ No newline at end of file + + + diff --git a/resources/light/run-failed-tests.svg b/resources/light/run-failed-tests.svg index fc67ad813674..f522730115b6 100644 --- a/resources/light/run-failed-tests.svg +++ b/resources/light/run-failed-tests.svg @@ -1 +1,11 @@ -RunFailedTest_16x \ No newline at end of file + + + + + + + + + + + diff --git a/resources/light/run-tests.svg b/resources/light/run-tests.svg index 3b5497ccaf62..317fb9bd7ed3 100644 --- a/resources/light/run-tests.svg +++ b/resources/light/run-tests.svg @@ -1 +1,3 @@ -StartTestGroupWithDebug_16x \ No newline at end of file + + + diff --git a/resources/light/start.svg b/resources/light/start.svg index 85cb1025609c..f41a5c8fa2d8 100644 --- a/resources/light/start.svg +++ b/resources/light/start.svg @@ -1 +1,3 @@ -continue \ No newline at end of file + + + diff --git a/resources/light/status-error.svg b/resources/light/status-error.svg index 04cded9a4230..bfca80a2d234 100644 --- a/resources/light/status-error.svg +++ b/resources/light/status-error.svg @@ -1 +1,3 @@ -StatusCriticalError_16x \ No newline at end of file + + + diff --git a/resources/light/status-ok.svg b/resources/light/status-ok.svg index 34a24a1a2f66..9487635d69bb 100644 --- a/resources/light/status-ok.svg +++ b/resources/light/status-ok.svg @@ -1 +1,4 @@ - + + + + diff --git a/resources/light/status-unknown.svg b/resources/light/status-unknown.svg index 27240be1aa6b..25d74d5023cf 100644 --- a/resources/light/status-unknown.svg +++ b/resources/light/status-unknown.svg @@ -1 +1,3 @@ -StatusHelp_grey_16x \ No newline at end of file + + + diff --git a/resources/light/stop.svg b/resources/light/stop.svg index 4f76333c131c..41a99170575a 100644 --- a/resources/light/stop.svg +++ b/resources/light/stop.svg @@ -1 +1,4 @@ -StatusStop_16x \ No newline at end of file + + + + diff --git a/src/client/common/experimentGroups.ts b/src/client/common/experimentGroups.ts index 9ca508801ec9..0957892f564e 100644 --- a/src/client/common/experimentGroups.ts +++ b/src/client/common/experimentGroups.ts @@ -15,8 +15,8 @@ export enum ShowExtensionSurveyPrompt { // Experiment to check whether the extension should use the new VS Code debug adapter API. export enum DebugAdapterDescriptorFactory { - control = 'DebugAdapterFactory - control', - experiment = 'DebugAdapterFactory - experiment' + control = 'DebugAdapterFactoryInsiders - control', + experiment = 'DebugAdapterFactoryInsiders - experiment' } // Experiment to check whether the ptvsd launcher should use pre-installed ptvsd wheels for debugging. diff --git a/src/client/common/types.ts b/src/client/common/types.ts index f7c391103f6d..46649d0dff99 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -324,6 +324,7 @@ export interface IDataScienceSettings { collapseCellInputCodeByDefault: boolean; maxOutputSize: number; enableGather?: boolean; + gatherToScript?: boolean; gatherRules?: IGatherRule[]; sendSelectionToInteractiveWindow: boolean; markdownRegularExpression: string; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index da6e41fb2c5e..875de896c688 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -45,7 +45,7 @@ export namespace LanguageService { export const lsFailedToExtract = localize('LanguageService.lsFailedToExtract', 'We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); export const downloadFailedOutputMessage = localize('LanguageService.downloadFailedOutputMessage', 'Language server download failed.'); export const extractionFailedOutputMessage = localize('LanguageService.extractionFailedOutputMessage', 'Language server extraction failed.'); - export const extractionCompletedOutputMessage = localize('LanguageService.extractionCompletedOutputMessage', 'Language server dowload complete.'); + export const extractionCompletedOutputMessage = localize('LanguageService.extractionCompletedOutputMessage', 'Language server download complete.'); export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); export const reloadVSCodeIfSeachPathHasChanged = localize('LanguageService.reloadVSCodeIfSeachPathHasChanged', 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); @@ -177,10 +177,9 @@ export namespace DataScience { export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:'); export const pythonVersionHeaderNoPyKernel = localize('DataScience.pythonVersionHeaderNoPyKernel', 'Python Version may not match, no ipykernel found:'); - export const jupyterSelectURILaunchLocal = localize('DataScience.jupyterSelectURILaunchLocal', 'Launch local Jupyter server'); - export const jupyterSelectURISpecifyURI = localize('DataScience.jupyterSelectURISpecifyURI', 'Type in the URI for the Jupyter server'); - export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of a Jupyter server'); + export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of the running Jupyter server or specify "local" to create one locally when needed'); export const jupyterSelectURIInvalidURI = localize('DataScience.jupyterSelectURIInvalidURI', 'Invalid URI specified'); + export const jupyterSelectWatermarkFormat = localize('DataScience.jupyterSelectWatermarkFormat', '{0} OR {1}'); export const jupyterSelectPasswordPrompt = localize('DataScience.jupyterSelectPasswordPrompt', 'Enter your notebook password'); export const jupyterNotebookFailure = localize('DataScience.jupyterNotebookFailure', 'Jupyter notebook failed to launch. \r\n{0}'); export const jupyterNotebookConnectFailed = localize('DataScience.jupyterNotebookConnectFailed', 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'); @@ -294,6 +293,8 @@ export namespace DataScience { export const findJupyterCommandProgress = localize('DataScience.findJupyterCommandProgress', 'Active interpreter does not support {0}. Searching for the best available interpreter.'); export const findJupyterCommandProgressCheckInterpreter = localize('DataScience.findJupyterCommandProgressCheckInterpreter', 'Checking {0}.'); export const findJupyterCommandProgressSearchCurrentPath = localize('DataScience.findJupyterCommandProgressSearchCurrentPath', 'Searching current path.'); + export const gatheredScriptDescription = localize('DataScience.gatheredScriptDescription', '# This file contains the minimal amount of code required to produce the code cell gathered.\n'); + export const gatheredNotebookDescriptionInMarkdown = localize('DataScience.gatheredNotebookDescriptionInMarkdown', '## This notebook was generated for a cell gathered from {0}.'); } export namespace DebugConfigStrings { diff --git a/src/client/datascience/cellFactory.ts b/src/client/datascience/cellFactory.ts index 6fcc20387ffb..cbc881f625b4 100644 --- a/src/client/datascience/cellFactory.ts +++ b/src/client/datascience/cellFactory.ts @@ -107,10 +107,56 @@ export function hasCells(document: TextDocument, settings?: IDataScienceSettings return false; } -export function generateCellRanges(document: TextDocument, settings?: IDataScienceSettings): { range: Range; title: string; cell_type: string }[] { +// CellRange is used as the basis for creating new ICells. We only use it in this file. +interface ICellRange { + range: Range; + title: string; + cell_type: string; +} + +export function generateCellsFromString(source: string, settings?: IDataScienceSettings): ICell[] { + const lines: string[] = source.splitLines({ trim: false, removeEmptyEntries: false }); + + // Find all the lines that start a cell + const matcher = new CellMatcher(settings); + const starts: { startLine: number; title: string; code: string; cell_type: string }[] = []; + let currentCode: string | undefined; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (matcher.isCell(line)) { + if (starts.length > 0 && currentCode) { + const previousCell = starts[starts.length - 1]; + previousCell.code = currentCode; + } + const results = matcher.exec(line); + if (results !== undefined) { + starts.push({ + startLine: index + 1, + title: results, + cell_type: matcher.getCellType(line), + code: '' + }); + } + currentCode = undefined; + } + currentCode = currentCode ? `${currentCode}\n${line}` : line; + } + + if (starts.length >= 1 && currentCode) { + const previousCell = starts[starts.length - 1]; + previousCell.code = currentCode; + } + + // For each one, get its text and turn it into a cell + return Array.prototype.concat(...starts.map(s => { + return generateCells(settings, s.code, '', s.startLine, false, uuid()); + })); +} + +export function generateCellRangesFromDocument(document: TextDocument, settings?: IDataScienceSettings): ICellRange[] { // Implmentation of getCells here based on Don's Jupyter extension work const matcher = new CellMatcher(settings); - const cells : { range: Range; title: string; cell_type: string }[] = []; + const cells: ICellRange[] = []; for (let index = 0; index < document.lineCount; index += 1) { const line = document.lineAt(index); if (matcher.isCell(line.text)) { @@ -140,12 +186,11 @@ export function generateCellRanges(document: TextDocument, settings?: IDataScien } export function generateCellsFromDocument(document: TextDocument, settings?: IDataScienceSettings): ICell[] { - // Get our ranges. They'll determine our cells - const ranges = generateCellRanges(document, settings); + const ranges = generateCellRangesFromDocument(document, settings); // For each one, get its text and turn it into a cell - return Array.prototype.concat(...ranges.map(r => { - const code = document.getText(r.range); - return generateCells(settings, code, document.fileName, r.range.start.line, false, uuid()); + return Array.prototype.concat(...ranges.map(cr => { + const code = document.getText(cr.range); + return generateCells(settings, code, '', cr.range.start.line, false, uuid()); })); } diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index c8638639dbb2..067ecf504d9f 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -202,7 +202,12 @@ export enum Telemetry { NotebookRunCount = 'DATASCIENCE.NATIVE.NOTEBOOK_RUN_COUNT', NotebookOpenCount = 'DATASCIENCE.NATIVE.NOTEBOOK_OPEN_COUNT', NotebookOpenTime = 'DS_INTERNAL.NATIVE.NOTEBOOK_OPEN_TIME', - SessionIdleTimeout = 'DATASCIENCE.JUPYTER_IDLE_TIMEOUT' + SessionIdleTimeout = 'DATASCIENCE.JUPYTER_IDLE_TIMEOUT', + NotebookExecutionActivated = 'DATASCIENCE.NOTEBOOK.EXECUTION.ACTIVATED', + JupyterNotInstalledErrorShown = 'DATASCIENCE.JUPYTER_NOT_INSTALLED_ERROR_SHOWN', + JupyterCommandSearch = 'DATASCIENCE.JUPYTER_COMMAND_SEARCH', + UserInstalledJupyter = 'DATASCIENCE.USER_INSTALLED_JUPYTER', + UserDidNotInstallJupyter = 'DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER' } export enum NativeKeyboardCommandTelemetry { diff --git a/src/client/datascience/data-viewing/dataViewerProvider.ts b/src/client/datascience/data-viewing/dataViewerProvider.ts index ff220598781a..4e17bedd9bc2 100644 --- a/src/client/datascience/data-viewing/dataViewerProvider.ts +++ b/src/client/datascience/data-viewing/dataViewerProvider.ts @@ -9,9 +9,8 @@ import { IPythonExecutionFactory } from '../../common/process/types'; import { IAsyncDisposable, IAsyncDisposableRegistry } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { IDataViewer, IDataViewerProvider, IJupyterVariables, INotebook } from '../types'; +import { IDataViewer, IDataViewerProvider, IJupyterExecution, IJupyterVariables, INotebook } from '../types'; @injectable() export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable { @@ -21,7 +20,7 @@ export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, @inject(IJupyterVariables) private variables: IJupyterVariables, @inject(IPythonExecutionFactory) private pythonFactory: IPythonExecutionFactory, - @inject(IInterpreterService) private interpreterService: IInterpreterService + @inject(IJupyterExecution) private readonly jupyterExecution: IJupyterExecution ) { asyncRegistry.push(this); } @@ -45,7 +44,7 @@ export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable } public async getPandasVersion(): Promise<{ major: number; minor: number; build: number } | undefined> { - const interpreter = await this.interpreterService.getActiveInterpreter(); + const interpreter = await this.jupyterExecution.getUsableJupyterPython(); const launcher = await this.pythonFactory.createActivatedEnvironment({ resource: undefined, interpreter, allowEnvironmentFetchExceptions: true }); try { const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], { throwOnStdErr: true }); diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts index dc32a43b9b76..efdaaacce034 100644 --- a/src/client/datascience/datascience.ts +++ b/src/client/datascience/datascience.ts @@ -218,18 +218,15 @@ export class DataScience implements IDataScience { @captureTelemetry(Telemetry.SelectJupyterURI) public async selectJupyterURI(): Promise { - const quickPickOptions = [localize.DataScience.jupyterSelectURILaunchLocal(), localize.DataScience.jupyterSelectURISpecifyURI()]; - const selection = await this.appShell.showQuickPick(quickPickOptions, { ignoreFocusOut: true }); - switch (selection) { - case localize.DataScience.jupyterSelectURILaunchLocal(): - return this.setJupyterURIToLocal(); - break; - case localize.DataScience.jupyterSelectURISpecifyURI(): - return this.selectJupyterLaunchURI(); - break; - default: - // If user cancels quick pick we will get undefined as the selection and fall through here - break; + const userURI = await this.appShell.showInputBox({ + prompt: localize.DataScience.jupyterSelectURIPrompt(), + placeHolder: localize.DataScience.jupyterSelectWatermarkFormat().format('local', 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'), validateInput: this.validateSelectJupyterURI, ignoreFocusOut: true + }); + + if (userURI && userURI.toUpperCase() === 'LOCAL') { + await this.setJupyterURIToLocal(); + } else if (userURI) { + await this.setJupyterURIToRemote(userURI); } } @@ -274,6 +271,22 @@ export class DataScience implements IDataScience { this.commandManager.executeCommand('workbench.action.debug.continue'); } } + private validateSelectJupyterURI = (inputText: string): string | undefined | null => { + // First check if the input is 'local' specifically checking against local so culture less comparison is fine + if (inputText.toUpperCase() === 'LOCAL') { + return null; + } + + try { + // tslint:disable-next-line:no-unused-expression + new URL(inputText); + } catch { + return localize.DataScience.jupyterSelectURIInvalidURI(); + } + + // Return null tells the dialog that our string is valid + return null; + } @captureTelemetry(Telemetry.SetJupyterURIToLocal) private async setJupyterURIToLocal(): Promise { @@ -281,16 +294,8 @@ export class DataScience implements IDataScience { } @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) - private async selectJupyterLaunchURI(): Promise { - // First get the proposed URI from the user - const userURI = await this.appShell.showInputBox({ - prompt: localize.DataScience.jupyterSelectURIPrompt(), - placeHolder: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe', validateInput: this.validateURI, ignoreFocusOut: true - }); - - if (userURI) { - await this.configuration.updateSetting('dataScience.jupyterServerURI', userURI, undefined, vscode.ConfigurationTarget.Workspace); - } + private async setJupyterURIToRemote(userURI: string): Promise { + await this.configuration.updateSetting('dataScience.jupyterServerURI', userURI, undefined, vscode.ConfigurationTarget.Workspace); } @captureTelemetry(Telemetry.AddCellBelow) @@ -370,18 +375,6 @@ export class DataScience implements IDataScience { } } - private validateURI = (testURI: string): string | undefined | null => { - try { - // tslint:disable-next-line:no-unused-expression - new URL(testURI); - } catch { - return localize.DataScience.jupyterSelectURIInvalidURI(); - } - - // Return null tells the dialog that our string is valid - return null; - } - private onSettingsChanged = () => { const settings = this.configuration.getSettings(); const enabled = settings.datascience.enabled; @@ -512,9 +505,9 @@ export class DataScience implements IDataScience { } } @swallowExceptions('Error in sending DS Startup telemetry') - private sendStartupTelemetry(){ + private sendStartupTelemetry() { const filesConfig = this.workspace.getConfiguration('files'); - sendTelemetryEvent(Telemetry.AutoSaveEnabled, undefined, {enabled: filesConfig.get('autoSave', 'off') !== 'off'}); + sendTelemetryEvent(Telemetry.AutoSaveEnabled, undefined, { enabled: filesConfig.get('autoSave', 'off') !== 'off' }); } private async createNewNotebook(): Promise { diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts index 180711da5893..58750962bbba 100644 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ b/src/client/datascience/editor-integration/codeLensFactory.ts @@ -9,7 +9,7 @@ import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { generateCellRanges } from '../cellFactory'; +import { generateCellRangesFromDocument } from '../cellFactory'; import { CodeLensCommands, Commands } from '../constants'; import { InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; import { ICell, ICellHashProvider, ICodeLensFactory, IFileHashes, IInteractiveWindowListener } from '../types'; @@ -63,7 +63,7 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList } public createCodeLenses(document: TextDocument): CodeLens[] { - const ranges = generateCellRanges(document, this.configService.getSettings().datascience); + const ranges = generateCellRangesFromDocument(document, this.configService.getSettings().datascience); const commands = this.enumerateCommands(); const hashes = this.configService.getSettings().datascience.addGotoCodeLenses ? this.hashProvider.getHashes() : []; const codeLenses: CodeLens[] = []; diff --git a/src/client/datascience/editor-integration/decorator.ts b/src/client/datascience/editor-integration/decorator.ts index e8e62b5c1bd2..42c5cd3b7097 100644 --- a/src/client/datascience/editor-integration/decorator.ts +++ b/src/client/datascience/editor-integration/decorator.ts @@ -8,7 +8,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { IDocumentManager } from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../common/types'; -import { generateCellRanges } from '../cellFactory'; +import { generateCellRangesFromDocument } from '../cellFactory'; @injectable() export class Decorator implements IExtensionSingleActivationService, IDisposable { @@ -100,7 +100,7 @@ export class Decorator implements IExtensionSingleActivationService, IDisposable const settings = this.configuration.getSettings().datascience; if (settings.decorateCells && settings.enabled) { // Find all of the cells - const cells = generateCellRanges(editor.document, this.configuration.getSettings().datascience); + const cells = generateCellRangesFromDocument(editor.document, this.configuration.getSettings().datascience); // Find the range for our active cell. const currentRange = cells.map(c => c.range).filter(r => r.contains(editor.selection.anchor)); diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts index 049d981ea6f4..a4a4b1730934 100644 --- a/src/client/datascience/errorHandler/errorHandler.ts +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -7,6 +7,8 @@ import { IInstallationChannelManager } from '../../common/installer/types'; import { ILogger, Product } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; import { IDataScienceErrorHandler } from '../types'; @@ -20,6 +22,7 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { public async handleError(err: Error): Promise { if (err instanceof JupyterInstallError) { + sendTelemetryEvent(Telemetry.JupyterNotInstalledErrorShown); const response = await this.applicationShell.showInformationMessage( err.message, localize.DataScience.jupyterInstall(), @@ -33,6 +36,7 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { const product = ProductNames.get(Product.jupyter); if (installer && product) { + sendTelemetryEvent(Telemetry.UserInstalledJupyter); installer.installModule(product) .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); } else if (installers[0] && product) { @@ -40,6 +44,8 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); } } + } else if (response === localize.DataScience.notebookCheckForImportNo()) { + sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); } else if (response === err.actionTitle) { // This is a special error that shows a link to open for more help this.applicationShell.openUrl(err.action); diff --git a/src/client/datascience/gather/gather.ts b/src/client/datascience/gather/gather.ts index 8a578989a719..c28000ea5e39 100644 --- a/src/client/datascience/gather/gather.ts +++ b/src/client/datascience/gather/gather.ts @@ -1,29 +1,22 @@ -import { DataflowAnalyzer } from '@msrvida/python-program-analysis'; -import { Cell as ICell, LogCell } from '@msrvida/python-program-analysis/dist/es5/cell'; -import { CellSlice } from '@msrvida/python-program-analysis/dist/es5/cellslice'; -import { ExecutionLogSlicer } from '@msrvida/python-program-analysis/dist/es5/log-slicer'; +import { CellSlice, DataflowAnalyzer, ExecutionLogSlicer } from '@msrvida/python-program-analysis'; +import { Cell as IGatherCell } from '@msrvida/python-program-analysis/dist/es5/cell'; import { inject, injectable } from 'inversify'; -// tslint:disable-next-line: no-require-imports -import cloneDeep = require('lodash/cloneDeep'); import { IApplicationShell, ICommandManager } from '../../common/application/types'; import { traceInfo } from '../../common/logger'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import * as localize from '../../common/utils/localize'; // tslint:disable-next-line: no-duplicate-imports import { Common } from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { CellMatcher } from '../cellMatcher'; -import { concatMultilineStringInput } from '../common'; import { Identifiers } from '../constants'; -import { CellState, ICell as IVscCell, IGatherExecution, INotebookExecutionLogger } from '../types'; +import { CellState, ICell as IVscCell, IGatherExecution } from '../types'; /** * An adapter class to wrap the code gathering functionality from [microsoft/python-program-analysis](https://www.npmjs.com/package/@msrvida/python-program-analysis). */ @injectable() -export class GatherExecution implements IGatherExecution, INotebookExecutionLogger { - private _executionSlicer: ExecutionLogSlicer; +export class GatherExecution implements IGatherExecution { + private _executionSlicer: ExecutionLogSlicer; private dataflowAnalyzer: DataflowAnalyzer; private _enabled: boolean; @@ -44,40 +37,24 @@ export class GatherExecution implements IGatherExecution, INotebookExecutionLogg traceInfo('Gathering tools have been activated'); } + public logExecution(vscCell: IVscCell): void { + const gatherCell = convertVscToGatherCell(vscCell); - public async preExecute(_vscCell: IVscCell, _silent: boolean): Promise { - // This function is just implemented here for compliance with the INotebookExecutionLogger interface - noop(); + if (gatherCell) { + this._executionSlicer.logExecution(gatherCell); + } } - public async postExecute(vscCell: IVscCell, _silent: boolean): Promise { - if (this._enabled) { - // Don't log if vscCell.data.source is an empty string or if it was - // silently executed. Original Jupyter extension also does this. - if (vscCell.data.source !== '' && !_silent) { - // First make a copy of this cell, as we are going to modify it - const cloneCell: IVscCell = cloneDeep(vscCell); - - // Strip first line marker. We can't do this at JupyterServer.executeCodeObservable because it messes up hashing - const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); - cloneCell.data.source = cellMatcher.stripFirstMarker(concatMultilineStringInput(vscCell.data.source)); - - // Convert IVscCell to IGatherCell - const cell = convertVscToGatherCell(cloneCell) as LogCell; - - // Call internal logging method - this._executionSlicer.logExecution(cell); - } - } + public async resetLog(): Promise { + this._executionSlicer.reset(); } /** * For a given code cell, returns a string representing a program containing all the code it depends on. */ public gatherCode(vscCell: IVscCell): string { - // sliceAllExecutions does a lookup based on executionEventId - const cell = convertVscToGatherCell(vscCell); - if (cell === undefined) { + const gatherCell = convertVscToGatherCell(vscCell); + if (!gatherCell) { return ''; } @@ -85,11 +62,11 @@ export class GatherExecution implements IGatherExecution, INotebookExecutionLogg const defaultCellMarker = this.configService.getSettings().datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker; // Call internal slice method - const slices = this._executionSlicer.sliceAllExecutions(cell); + const slices = this._executionSlicer.sliceAllExecutions(gatherCell.persistentId); const program = slices.length > 0 ? slices[0].cellSlices.reduce(concat, '').replace(/#%%/g, defaultCellMarker) : ''; // Add a comment at the top of the file explaining what gather does - const descriptor = '# This file contains the minimal amount of code required to produce the code cell you gathered.\n'; + const descriptor = localize.DataScience.gatheredScriptDescription(); return descriptor.concat(program); } @@ -122,7 +99,7 @@ export class GatherExecution implements IGatherExecution, INotebookExecutionLogg /** * Accumulator to concatenate cell slices for a sliced program, preserving cell structures. */ -function concat(existingText: string, newText: CellSlice) { +function concat(existingText: string, newText: CellSlice): string { // Include our cell marker so that cell slices are preserved return `${existingText}#%%\n${newText.textSliceLines}\n\n`; } @@ -131,14 +108,11 @@ function concat(existingText: string, newText: CellSlice) { * This is called to convert VS Code ICells to Gather ICells for logging. * @param cell A cell object conforming to the VS Code cell interface */ -function convertVscToGatherCell(cell: IVscCell): ICell | undefined { +function convertVscToGatherCell(cell: IVscCell): IGatherCell | undefined { // This should always be true since we only want to log code cells. Putting this here so types match for outputs property if (cell.data.cell_type === 'code') { - const result: ICell = { + const result: IGatherCell = { // tslint:disable-next-line no-unnecessary-local-variable - id: cell.id, - gathered: false, - dirty: false, text: cell.data.source, // This may need to change for native notebook support since in the original Gather code this refers to the number of times that this same cell was executed @@ -147,9 +121,7 @@ function convertVscToGatherCell(cell: IVscCell): ICell | undefined { // This may need to change for native notebook support, since this is intended to persist in the metadata for a notebook that is saved and then re-loaded persistentId: cell.id, - outputs: cell.data.outputs, - hasError: cell.state === CellState.error, - is_cell: true + hasError: cell.state === CellState.error // tslint:disable-next-line: no-any } as any; return result; diff --git a/src/client/datascience/gather/gatherListener.ts b/src/client/datascience/gather/gatherListener.ts index c5639afe5b18..acd356c41118 100644 --- a/src/client/datascience/gather/gatherListener.ts +++ b/src/client/datascience/gather/gatherListener.ts @@ -1,23 +1,38 @@ import { inject, injectable } from 'inversify'; -import { Event, EventEmitter, Position, ViewColumn } from 'vscode'; +import * as uuid from 'uuid/v4'; +import { Event, EventEmitter, Position, Uri, ViewColumn } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../common/application/types'; import { PYTHON_LANGUAGE } from '../../common/constants'; import { IFileSystem } from '../../common/platform/types'; +import { IConfigurationService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; -import { ICell, IGatherExecution, IInteractiveWindowListener } from '../types'; +import { generateCellsFromString } from '../cellFactory'; +import { Identifiers } from '../constants'; +import { IInteractiveWindowMapping, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; +import { ICell, IGatherExecution, IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution, INotebook, INotebookEditorProvider, INotebookExporter } from '../types'; +import { GatherLogger } from './gatherLogger'; @injectable() export class GatherListener implements IInteractiveWindowListener { // tslint:disable-next-line: no-any private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ message: string; payload: any }>(); + private gatherLogger: GatherLogger; + private notebookUri: Uri = Uri.parse(''); constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IGatherExecution) private gather: IGatherExecution, @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IGatherExecution) private gatherExecution: IGatherExecution, + @inject(INotebookExporter) private jupyterExporter: INotebookExporter, + @inject(INotebookEditorProvider) private ipynbProvider: INotebookEditorProvider, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IFileSystem) private fileSystem: IFileSystem - ) { } + ) { + this.gatherLogger = new GatherLogger(this.gather, this.configService); + } public dispose() { noop(); @@ -31,11 +46,16 @@ export class GatherListener implements IInteractiveWindowListener { // tslint:disable-next-line: no-any public onMessage(message: string, payload?: any): void { switch (message) { - case InteractiveWindowMessages.GatherCode: - if (payload) { - const cell = payload as ICell; - this.gatherCode(cell); - } + case InteractiveWindowMessages.NotebookExecutionActivated: + this.handleMessage(message, payload, this.doSetLogger); + break; + + case InteractiveWindowMessages.GatherCodeRequest: + this.handleMessage(message, payload, this.doGather); + break; + + case InteractiveWindowMessages.RestartKernel: + this.gather.resetLog(); break; default: @@ -43,18 +63,77 @@ export class GatherListener implements IInteractiveWindowListener { } } - public gatherCode(payload: ICell) { + // tslint:disable:no-any + private handleMessage(_message: T, payload: any, handler: (args: M[T]) => void) { + const args = payload as M[T]; + handler.bind(this)(args); + } + + private doSetLogger(payload: Uri): void { + this.setLogger(payload).ignoreErrors(); + } + + private async setLogger(notebookUri: Uri) { + this.notebookUri = notebookUri; + + // First get the active server + const activeServer = await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions()); + + let nb: INotebook | undefined; + // If that works, see if there's a matching notebook running + if (activeServer) { + nb = await activeServer.getNotebook(notebookUri); + + // If we have an executing notebook, add the gather logger. + if (nb) { + nb.addLogger(this.gatherLogger); + } + } + } + + private doGather(payload: ICell): void { this.gatherCodeInternal(payload).catch(err => { this.applicationShell.showErrorMessage(err); }); } private gatherCodeInternal = async (cell: ICell) => { - const slicedProgram = this.gatherExecution.gatherCode(cell); + const slicedProgram = this.gather.gatherCode(cell); + + if (this.configService.getSettings().datascience.gatherToScript) { + await this.showFile(slicedProgram, cell.file); + } else { + await this.showNotebook(slicedProgram, cell); + } + } + + private async showNotebook(slicedProgram: string, cell: ICell) { + if (slicedProgram) { + let cells: ICell[] = [{ + id: uuid(), + file: '', + line: 0, + state: 0, + data: { + cell_type: 'markdown', + source: localize.DataScience.gatheredNotebookDescriptionInMarkdown().format(cell.file === Identifiers.EmptyFileName ? this.notebookUri.fsPath : cell.file), + metadata: {} + } + }]; + + // Create new notebook with the returned program and open it. + cells = cells.concat(generateCellsFromString(slicedProgram)); + + const notebook = await this.jupyterExporter.translateToNotebook(cells); + const contents = JSON.stringify(notebook); + await this.ipynbProvider.createNew(contents); + } + } + private async showFile(slicedProgram: string, filename: string) { // Don't want to open the gathered code on top of the interactive window let viewColumn: ViewColumn | undefined; - const fileNameMatch = this.documentManager.visibleTextEditors.filter(textEditor => this.fileSystem.arePathsSame(textEditor.document.fileName, cell.file)); + const fileNameMatch = this.documentManager.visibleTextEditors.filter(textEditor => this.fileSystem.arePathsSame(textEditor.document.fileName, filename)); const definedVisibleEditors = this.documentManager.visibleTextEditors.filter(textEditor => textEditor.viewColumn !== undefined); if (this.documentManager.visibleTextEditors.length > 0 && fileNameMatch.length > 0) { // Original file is visible diff --git a/src/client/datascience/gather/gatherLogger.ts b/src/client/datascience/gather/gatherLogger.ts new file mode 100644 index 000000000000..efae3af72ce0 --- /dev/null +++ b/src/client/datascience/gather/gatherLogger.ts @@ -0,0 +1,41 @@ +import { inject, injectable } from 'inversify'; +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { IConfigurationService } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { CellMatcher } from '../cellMatcher'; +import { concatMultilineStringInput } from '../common'; +import { ICell as IVscCell, IGatherExecution, INotebookExecutionLogger } from '../types'; +import { GatherExecution } from './gather'; + +@injectable() +export class GatherLogger implements INotebookExecutionLogger { + + constructor( + @inject(GatherExecution) private gather: IGatherExecution, + @inject(IConfigurationService) private configService: IConfigurationService + ) { + } + + public async preExecute(_vscCell: IVscCell, _silent: boolean): Promise { + // This function is just implemented here for compliance with the INotebookExecutionLogger interface + noop(); + } + + public async postExecute(vscCell: IVscCell, _silent: boolean): Promise { + if (this.gather.enabled) { + // Don't log if vscCell.data.source is an empty string or if it was + // silently executed. Original Jupyter extension also does this. + if (vscCell.data.source !== '' && !_silent) { + // First make a copy of this cell, as we are going to modify it + const cloneCell: IVscCell = cloneDeep(vscCell); + + // Strip first line marker. We can't do this at JupyterServer.executeCodeObservable because it messes up hashing + const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); + cloneCell.data.source = cellMatcher.stripFirstMarker(concatMultilineStringInput(vscCell.data.source)); + + this.gather.logExecution(cloneCell); + } + } + } +} diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index 126fe652feac..bc569fc93d14 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -29,7 +29,7 @@ import * as localize from '../../common/utils/localize'; import { StopWatch } from '../../common/utils/stopWatch'; import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { generateCellRanges } from '../cellFactory'; +import { generateCellRangesFromDocument } from '../cellFactory'; import { CellMatcher } from '../cellMatcher'; import { Identifiers, Telemetry } from '../constants'; import { ColumnWarningSize } from '../data-viewing/types'; @@ -346,7 +346,7 @@ export abstract class InteractiveBase extends WebViewHost { if (this.notebook && !this.restartingKernel) { - const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus(), undefined, undefined, this); + const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus(), true, undefined, undefined, this); const settings = this.configuration.getSettings(); const interruptTimeout = settings.datascience.jupyterInterruptTimeout; @@ -463,7 +463,7 @@ export abstract class InteractiveBase extends WebViewHost { - const result = this.statusProvider.set(message, undefined, undefined, this); + protected setStatus = (message: string, showInWebView: boolean): Disposable => { + const result = this.statusProvider.set(message, showInWebView, undefined, undefined, this); this.potentiallyUnfinishedStatus.push(result); return result; } @@ -733,7 +733,7 @@ export abstract class InteractiveBase extends WebViewHost { // Status depends upon if we're about to connect to existing server or not. const status = (await this.jupyterExecution.getServer(await this.getNotebookOptions())) ? - this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); + this.setStatus(localize.DataScience.connectingToJupyter(), true) : this.setStatus(localize.DataScience.startingJupyter(), true); // Check to see if we support ipykernel or not try { @@ -899,7 +899,7 @@ export abstract class InteractiveBase extends WebViewHost { // Update our load promise. We need to restart the jupyter server - this.loadPromise = this.reloadWithNew(); + if (this.loadPromise) { + this.loadPromise = this.reloadWithNew(); + } } private async stopServer(): Promise { @@ -951,7 +953,7 @@ export abstract class InteractiveBase extends WebViewHost { - const status = this.setStatus(localize.DataScience.startingJupyter()); + const status = this.setStatus(localize.DataScience.startingJupyter(), true); try { // Not the same as reload, we need to actually wait for the server. await this.stopServer(); @@ -1013,7 +1015,7 @@ export abstract class InteractiveBase extends WebViewHost 0; const line = editor.selection.start.line; const revealLine = line + 1; @@ -1088,6 +1090,11 @@ export abstract class InteractiveBase extends WebViewHost{ + private async updateVersionInfoInNotebook(): Promise { // Use the active interpreter const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); - if (usableInterpreter && usableInterpreter.version && this.notebookJson.metadata && this.notebookJson.metadata.language_info){ + if (usableInterpreter && usableInterpreter.version && this.notebookJson.metadata && this.notebookJson.metadata.language_info) { this.notebookJson.metadata.language_info.version = `${usableInterpreter.version.major}.${usableInterpreter.version.minor}.${usableInterpreter.version.patch}`; } } @@ -580,15 +581,30 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { * @memberof NativeEditor */ private async getStoredContents(): Promise { - const data = this.globalStorage.get<{contents?: string; lastModifiedTimeMs?: number}>(this.getStorageKey()); + const key = this.getStorageKey(); + const data = this.globalStorage.get<{ contents?: string; lastModifiedTimeMs?: number }>(key); // Check whether the file has been modified since the last time the contents were saved. - if (data && data.lastModifiedTimeMs && !this.isUntitled && this.file.scheme === 'file'){ + if (data && data.lastModifiedTimeMs && !this.isUntitled && this.file.scheme === 'file') { const stat = await this.fileSystem.stat(this.file.fsPath); - if (stat.mtime > data.lastModifiedTimeMs){ + if (stat.mtime > data.lastModifiedTimeMs) { return; } } - return data ? data.contents : undefined; + if (data && !this.isUntitled && data.contents) { + return data.contents; + } + + const workspaceData = this.localStorage.get(key); + if (workspaceData && !this.isUntitled) { + // Make sure to clear so we don't use this again. + this.localStorage.update(key, undefined); + + // Transfer this to global storage so we use that next time instead + const stat = await this.fileSystem.stat(this.file.fsPath); + this.globalStorage.update(key, { contents: workspaceData, lastModifiedTimeMs: stat ? stat.mtime : undefined }); + + return workspaceData; + } } /** @@ -605,7 +621,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { const key = this.getStorageKey(); // Keep track of the time when this data was saved. // This way when we retrieve the data we can compare it against last modified date of the file. - await this.globalStorage.update(key, {contents, lastModifiedTimeMs: Date.now()}); + await this.globalStorage.update(key, contents ? { contents, lastModifiedTimeMs: Date.now() } : undefined); } private async close(): Promise { @@ -757,7 +773,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { @captureTelemetry(Telemetry.ConvertToPythonFile, undefined, false) private async export(cells: ICell[]): Promise { - const status = this.setStatus(localize.DataScience.convertingToPythonFile()); + const status = this.setStatus(localize.DataScience.convertingToPythonFile(), false); // First generate a temporary notebook with these cells. let tempFile: TemporaryFile | undefined; try { @@ -767,7 +783,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { await this.fileSystem.writeFile(tempFile.filePath, this.generateNotebookContent(cells), { encoding: 'utf-8' }); // Import this file and show it - const contents = await this.importer.importFromFile(tempFile.filePath); + const contents = await this.importer.importFromFile(tempFile.filePath, this.file.fsPath); if (contents) { await this.viewDocument(contents); } diff --git a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts index 7031c91578c0..6d089c94eec6 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts @@ -17,10 +17,12 @@ import { IDataScienceErrorHandler, INotebookEditor, INotebookEditorProvider, INo @injectable() export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisposable { + private activeEditors: Map = new Map(); private executedEditors: Set = new Set(); private notebookCount: number = 0; private openedNotebookCount: number = 0; + private nextNumber: number = 1; constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, @@ -71,7 +73,6 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp sendTelemetryEvent(Telemetry.NotebookRunCount, this.executedEditors.size); sendTelemetryEvent(Telemetry.NotebookWorkspaceCount, this.notebookCount); } - public get activeEditor(): INotebookEditor | undefined { const active = [...this.activeEditors.entries()].find(e => e[1].active); if (active) { @@ -105,11 +106,15 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp } @captureTelemetry(Telemetry.CreateNewNotebook, undefined, false) - public async createNew(): Promise { + public async createNew(contents?: string): Promise { // Create a new URI for the dummy file using our root workspace path const uri = await this.getNextNewNotebookUri(); this.notebookCount += 1; - return this.open(uri, ''); + if (contents) { + return this.open(uri, contents); + } else { + return this.open(uri, ''); + } } public async getNotebookOptions(): Promise { @@ -129,15 +134,16 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp purpose: Identifiers.HistoryPurpose // Share the same one as the interactive window. Just need a new session }; } + /** * Open ipynb files when user opens an ipynb file. * * @private * @memberof NativeEditorProvider */ - private onDidChangeActiveTextEditorHandler(editor?: TextEditor){ + private onDidChangeActiveTextEditorHandler(editor?: TextEditor) { // I we're a source control diff view, then ignore this editor. - if (!editor || this.isEditorPartOfDiffView(editor)){ + if (!editor || this.isEditorPartOfDiffView(editor)) { return; } this.openNotebookAndCloseEditor(editor.document, true).ignoreErrors(); @@ -162,15 +168,24 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp private onOpenedEditor(e: INotebookEditor) { this.activeEditors.set(e.file.fsPath, e); + this.disposables.push(e.saved(this.onSavedEditor.bind(this, e.file.fsPath))); this.openedNotebookCount += 1; } + private onSavedEditor(oldPath: string, e: INotebookEditor) { + // Switch our key for this editor + if (this.activeEditors.has(oldPath)) { + this.activeEditors.delete(oldPath); + } + this.activeEditors.set(e.file.fsPath, e); + } + private async getNextNewNotebookUri(): Promise { // Start in the root and look for files starting with untitled let number = 1; const dir = this.workspace.rootPath; if (dir) { - const existing = await this.fileSystem.search(`${dir}/${localize.DataScience.untitledNotebookFileName()}-*.ipynb`); + const existing = await this.fileSystem.search(path.join(dir, `${localize.DataScience.untitledNotebookFileName()}-*.ipynb`)); // Sort by number existing.sort(); @@ -181,11 +196,13 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp if (match && match.length > 1) { number = parseInt(match[2], 10); } + return Uri.file(path.join(dir, `${localize.DataScience.untitledNotebookFileName()}-${number + 1}`)); } - return Uri.file(path.join(dir, `${localize.DataScience.untitledNotebookFileName()}-${number}`)); } - return Uri.file(`${localize.DataScience.untitledNotebookFileName()}-${number}`); + const result = Uri.file(`${localize.DataScience.untitledNotebookFileName()}-${this.nextNumber}`); + this.nextNumber += 1; + return result; } private openNotebookAndCloseEditor = async (document: TextDocument, closeDocumentBeforeOpeningNotebook: boolean) => { @@ -193,12 +210,12 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp if (this.isNotebook(document) && this.configuration.getSettings().datascience.useNotebookEditor && !this.activeEditors.has(document.uri.fsPath)) { try { - const contents = document. getText(); + const contents = document.getText(); const uri = document.uri; const closeActiveEditorCommand = 'workbench.action.closeActiveEditor'; if (closeDocumentBeforeOpeningNotebook) { - if (!this.documentManager.activeTextEditor || this.documentManager.activeTextEditor.document !== document){ + if (!this.documentManager.activeTextEditor || this.documentManager.activeTextEditor.document !== document) { await this.documentManager.showTextDocument(document); } await this.cmdManager.executeCommand(closeActiveEditorCommand); @@ -207,7 +224,7 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp // Open our own editor. await this.open(uri, contents); - if (!closeDocumentBeforeOpeningNotebook){ + if (!closeDocumentBeforeOpeningNotebook) { // Then switch back to the ipynb and close it. // If we don't do it in this order, the close will switch to the wrong item await this.documentManager.showTextDocument(document); @@ -226,14 +243,14 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp * @param {TextEditor} editor * @memberof NativeEditorProvider */ - private isEditorPartOfDiffView(editor?: TextEditor){ - if (!editor){ + private isEditorPartOfDiffView(editor?: TextEditor) { + if (!editor) { return false; } // There's no easy way to determine if the user is openeing a diff view. // One simple way is to check if there are 2 editor opened, and if both editors point to the same file // One file with the `file` scheme and the other with the `git` scheme. - if (this.documentManager.visibleTextEditors.length <= 1){ + if (this.documentManager.visibleTextEditors.length <= 1) { return false; } @@ -242,18 +259,18 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp // Possible we have a git diff view (with two editors git and file scheme), and we open the file view // on the side (different view column). const gitSchemeEditor = this.documentManager.visibleTextEditors.find(editorUri => - editorUri.document.uri.scheme === 'git' && - this.fileSystem.arePathsSame(editorUri.document.uri.fsPath, editor.document.uri.fsPath)); + editorUri.document.uri.scheme === 'git' && + this.fileSystem.arePathsSame(editorUri.document.uri.fsPath, editor.document.uri.fsPath)); - if (!gitSchemeEditor){ + if (!gitSchemeEditor) { return false; } const fileSchemeEditor = this.documentManager.visibleTextEditors.find(editorUri => - editorUri.document.uri.scheme === 'file' && - this.fileSystem.arePathsSame(editorUri.document.uri.fsPath, editor.document.uri.fsPath) && - editorUri.viewColumn === gitSchemeEditor.viewColumn); - if (!fileSchemeEditor){ + editorUri.document.uri.scheme === 'file' && + this.fileSystem.arePathsSame(editorUri.document.uri.fsPath, editor.document.uri.fsPath) && + editorUri.viewColumn === gitSchemeEditor.viewColumn); + if (!fileSchemeEditor) { return false; } diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index 653c61f4eeb3..98c860b378d4 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -17,7 +17,7 @@ import { } from '../../common/application/types'; import { ContextKey } from '../../common/contextKey'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { IInterpreterService } from '../../interpreter/contracts'; @@ -48,6 +48,7 @@ import { export class InteractiveWindow extends InteractiveBase implements IInteractiveWindow { private closedEvent: EventEmitter = new EventEmitter(); private waitingForExportCells: boolean = false; + private trackedJupyterStart: boolean = false; constructor( @multiInject(IInteractiveWindowListener) listeners: IInteractiveWindowListener[], @@ -71,7 +72,8 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi @inject(IJupyterVariables) jupyterVariables: IJupyterVariables, @inject(IJupyterDebugger) jupyterDebugger: IJupyterDebugger, @inject(INotebookEditorProvider) editorProvider: INotebookEditorProvider, - @inject(IDataScienceErrorHandler) errorHandler: IDataScienceErrorHandler + @inject(IDataScienceErrorHandler) errorHandler: IDataScienceErrorHandler, + @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory ) { super( listeners, @@ -251,7 +253,16 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi protected async closeBecauseOfFailure(_exc: Error): Promise { this.dispose(); } - + protected startServer(): Promise { + // Keep track of users who have used interactive window in a worksapce folder. + // To be used if/when changing workflows related to startup of jupyter. + if (!this.trackedJupyterStart){ + this.trackedJupyterStart = true; + const store = this.stateFactory.createGlobalPersistentState('INTERACTIVE_WINDOW_USED', false); + store.updateValue(true).ignoreErrors(); + } + return super.startServer(); + } @captureTelemetry(Telemetry.ExportNotebook, undefined, false) // tslint:disable-next-line: no-any no-empty private export(cells: ICell[]) { diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts index 4933f6dde117..19b0c8dbefe3 100644 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -16,7 +16,7 @@ import { IConfigurationService, IDisposableRegistry, ILogger } from '../../commo import * as localize from '../../common/utils/localize'; import { captureTelemetry } from '../../telemetry'; import { CommandSource } from '../../testing/common/constants'; -import { generateCellRanges, generateCellsFromDocument } from '../cellFactory'; +import { generateCellRangesFromDocument, generateCellsFromDocument } from '../cellFactory'; import { Commands, Identifiers, Telemetry } from '../constants'; import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { @@ -25,6 +25,7 @@ import { IInteractiveBase, IInteractiveWindowProvider, IJupyterExecution, + INotebookEditorProvider, INotebookExporter, INotebookImporter, INotebookServer, @@ -45,7 +46,8 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList @inject(IConfigurationService) private configuration: IConfigurationService, @inject(IStatusProvider) private statusProvider: IStatusProvider, @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider ) { } @@ -159,7 +161,6 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList saveLabel: localize.DataScience.exportDialogTitle(), filters: filtersObject }); - await this.waitForStatus(async () => { if (uri) { let directoryChange; @@ -172,16 +173,19 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList await this.fileSystem.writeFile(uri.fsPath, JSON.stringify(notebook)); } }, localize.DataScience.exportingFormat(), file); - // When all done, show a notice that it completed. - const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; if (uri && uri.fsPath) { - this.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => { - if (str === openQuestion) { - // If the user wants to, open the notebook they just generated. - this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors(); - } - }); + const openQuestion1 = localize.DataScience.exportOpenQuestion1(); + const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; + const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])]; + const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), ...questions); + if (selection === openQuestion1) { + await this.ipynbProvider.open(uri, await this.fileSystem.readFile(uri.fsPath)); + } + if (selection === openQuestion2) { + // If the user wants to, open the notebook they just generated. + this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors(); + } } } } @@ -194,7 +198,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList // If the current file is the active editor, then generate cells from the document. const activeEditor = this.documentManager.activeTextEditor; if (activeEditor && activeEditor.document && this.fileSystem.arePathsSame(activeEditor.document.fileName, file)) { - const ranges = generateCellRanges(activeEditor.document); + const ranges = generateCellRangesFromDocument(activeEditor.document); if (ranges.length > 0) { // Ask user for path const output = await this.showExportDialog(); @@ -220,13 +224,17 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList }); // When all done, show a notice that it completed. - const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; - this.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => { - if (str === openQuestion && output) { - // If the user wants to, open the notebook they just generated. - this.jupyterExecution.spawnNotebook(output).ignoreErrors(); - } - }); + const openQuestion1 = localize.DataScience.exportOpenQuestion1(); + const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; + const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])]; + const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), ...questions); + if (selection === openQuestion1) { + await this.ipynbProvider.open(Uri.file(output), await this.fileSystem.readFile(output)); + } + if (selection === openQuestion2) { + // If the user wants to, open the notebook they just generated. + this.jupyterExecution.spawnNotebook(output).ignoreErrors(); + } return Uri.file(output); } @@ -351,7 +359,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, interactiveWindow?: IInteractiveBase): Promise { const message = file ? format.format(file) : format; - return this.statusProvider.waitWithStatus(promise, message, undefined, canceled, interactiveWindow); + return this.statusProvider.waitWithStatus(promise, message, true, undefined, canceled, interactiveWindow); } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) diff --git a/src/client/datascience/jupyter/jupyterCommandFinder.ts b/src/client/datascience/jupyter/jupyterCommandFinder.ts index 0a3b10da7412..2dc9398b52ea 100644 --- a/src/client/datascience/jupyter/jupyterCommandFinder.ts +++ b/src/client/datascience/jupyter/jupyterCommandFinder.ts @@ -51,7 +51,7 @@ type ProgressNotification = Progress<{ message?: string | undefined; increment?: export class JupyterCommandFinder { private readonly processServicePromise: Promise; private jupyterPath?: string; - private readonly commands = new Map(); + private readonly commands = new Map>(); constructor( private readonly interpreterService: IInterpreterService, private readonly executionFactory: IPythonExecutionFactory, @@ -100,13 +100,29 @@ export class JupyterCommandFinder { // Only log telemetry if not already found (meaning the first time) const timer = new StopWatch(); - try { - const result = await this.findBestCommandImpl(command, cancelToken); - this.commands.set(command, result); - return result; - } finally { - sendTelemetryEvent(Telemetry.FindJupyterCommand, timer.elapsedTime, { command }); + const promise = this.findBestCommandImpl(command, cancelToken) + .finally(() => sendTelemetryEvent(Telemetry.FindJupyterCommand, timer.elapsedTime, { command })); + + if (cancelToken) { + let promiseCompleted = false; + promise.finally(() => promiseCompleted = true).ignoreErrors(); + + // If the promise is not pending, then remove the item from cache. + // As the promise would not complete correctly, as its been cancelled. + if (cancelToken.isCancellationRequested && !promiseCompleted) { + this.commands.delete(command); + } + cancelToken.onCancellationRequested(() => { + // If the promise is not pending, then remove the item from cache. + // As the promise would not complete correctly, as its been cancelled. + if (!promiseCompleted) { + this.commands.delete(command); + } + }); } + + this.commands.set(command, promise); + return promise; } /** @@ -193,7 +209,7 @@ export class JupyterCommandFinder { // First we look in the current interpreter const current = await this.interpreterService.getActiveInterpreter(); - + const stopWatch = new StopWatch(); if (isCommandFinderCancelled(command, cancelToken)) { return cancelledResult; } @@ -204,6 +220,8 @@ export class JupyterCommandFinder { // Save our error information. This should propagate out as the error information for the command firstError = found.error; + } else { + this.sendSearchTelemetry(command, 'activeInterpreter', stopWatch.elapsedTime, cancelToken); } // Display a progress message when searching, as this could take a while. @@ -228,6 +246,11 @@ export class JupyterCommandFinder { if (found.status === ModuleExistsStatus.NotFound) { progress.report({ message: localize.DataScience.findJupyterCommandProgressSearchCurrentPath() }); found = await this.findPathCommand(command, cancelToken); + if (found.status !== ModuleExistsStatus.NotFound){ + this.sendSearchTelemetry(command, 'path', stopWatch.elapsedTime, cancelToken); + } + } else { + this.sendSearchTelemetry(command, 'otherInterpreter', stopWatch.elapsedTime, cancelToken); } return found; @@ -240,9 +263,18 @@ export class JupyterCommandFinder { found.error = firstError; } + if (found.status === ModuleExistsStatus.NotFound){ + this.sendSearchTelemetry(command, 'nowhere', stopWatch.elapsedTime, cancelToken); + } + return found; } - + private sendSearchTelemetry(command: JupyterCommands, where: 'activeInterpreter' | 'otherInterpreter' | 'path' | 'nowhere', elapsedTime: number, cancelToken?: CancellationToken){ + if (Cancellation.isCanceled(cancelToken)){ + return; + } + sendTelemetryEvent(Telemetry.JupyterCommandSearch, elapsedTime, {where, command}); + } private async searchOtherInterpretersForCommand( command: JupyterCommands, progress: ProgressNotification, diff --git a/src/client/datascience/jupyter/jupyterDebugger.ts b/src/client/datascience/jupyter/jupyterDebugger.ts index 56afb727606e..40eb56172b06 100644 --- a/src/client/datascience/jupyter/jupyterDebugger.ts +++ b/src/client/datascience/jupyter/jupyterDebugger.ts @@ -3,15 +3,17 @@ 'use strict'; import { nbformat } from '@jupyterlab/coreutils'; import { inject, injectable } from 'inversify'; +import * as path from 'path'; import * as uuid from 'uuid/v4'; import { DebugConfiguration } from 'vscode'; import * as vsls from 'vsls/vscode'; - import { IApplicationShell, ICommandManager, IDebugService, IWorkspaceService } from '../../common/application/types'; +import { DebugAdapterDescriptorFactory, DebugAdapterNewPtvsd } from '../../common/experimentGroups'; import { traceError, traceInfo, traceWarning } from '../../common/logger'; import { IPlatformService } from '../../common/platform/types'; -import { IConfigurationService } from '../../common/types'; +import { IConfigurationService, IExperimentsManager, Version } from '../../common/types'; import * as localize from '../../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../../constants'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { concatMultilineStringOutput } from '../common'; import { Identifiers, Telemetry } from '../constants'; @@ -31,15 +33,9 @@ import { ILiveShareHasRole } from './liveshare/types'; const pythonShellCommand = `_sysexec = sys.executable\r\n_quoted_sysexec = '"' + _sysexec + '"'\r\n!{_quoted_sysexec}`; -interface IPtvsdVersion { - major: number; - minor: number; - revision: string; -} - @injectable() export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { - private requiredPtvsdVersion: IPtvsdVersion = { major: 4, minor: 3, revision: '' }; + private requiredPtvsdVersion: Version = { major: 4, minor: 3, patch: 0, build: [], prerelease: [], raw: '' }; private configs: Map = new Map(); constructor( @inject(IApplicationShell) private appShell: IApplicationShell, @@ -47,7 +43,8 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDebugService) private debugService: IDebugService, @inject(IPlatformService) private platform: IPlatformService, - @inject(IWorkspaceService) private workspace: IWorkspaceService + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager ) { } @@ -175,7 +172,32 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { return result; } - private calculatePtvsdPathList(_notebook: INotebook): string | undefined { + /** + * Gets the path to PTVSD. + * Temporary hack to check if python >= 3.7 and if experiments is enabled, then use new debugger, else old. + * (temporary to hardcode and use these in here). + * The old debugger will soon go away into oblivion... + * @private + * @param {INotebook} notebook + * @returns {Promise} + * @memberof JupyterDebugger + */ + private async getPtvsdPath(notebook: INotebook): Promise { + const oldPtvsd = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'old_ptvsd'); + if (!this.experimentsManager.inExperiment(DebugAdapterDescriptorFactory.experiment) || + !this.experimentsManager.inExperiment(DebugAdapterNewPtvsd.experiment)){ + return oldPtvsd; + } + const pythonVersion = await this.getKernelPythonVersion(notebook); + // The new debug adapter is only supported in 3.7 + // Code can be found here (src/client/debugger/extension/adapter/factory.ts). + if (!pythonVersion || pythonVersion.major < 3 || pythonVersion.minor < 7){ + return oldPtvsd; + } + + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); + } + private async calculatePtvsdPathList(notebook: INotebook): Promise { const extraPaths: string[] = []; // Add the settings path first as it takes precedence over the ptvsd extension path @@ -184,7 +206,7 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { // Escape windows path chars so they end up in the source escaped if (settingsPath) { if (this.platform.isWindows) { - settingsPath = settingsPath.replace('\\', '\\\\'); + settingsPath = settingsPath.replace(/\\/g, '\\\\'); } extraPaths.push(settingsPath); @@ -194,14 +216,14 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { // installed locally by the extension // Actually until this is resolved: https://github.com/microsoft/vscode-python/issues/7615, skip adding // this path. - // const connectionInfo = notebook.server.getConnectionInfo(); - // if (connectionInfo && connectionInfo.localLaunch) { - // let localPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); - // if (this.platform.isWindows) { - // localPath = localPath.replace('\\', '\\\\'); - // } - // extraPaths.push(localPath); - // } + const connectionInfo = notebook.server.getConnectionInfo(); + if (connectionInfo && connectionInfo.localLaunch) { + let localPath = await this.getPtvsdPath(notebook); + if (this.platform.isWindows) { + localPath = localPath.replace(/\\/g, '\\\\'); + } + extraPaths.push(localPath); + } if (extraPaths && extraPaths.length > 0) { return extraPaths.reduce((totalPath, currentPath) => { @@ -220,7 +242,7 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { // Append our local ptvsd path and ptvsd settings path to sys.path private async appendPtvsdPaths(notebook: INotebook): Promise { - const ptvsdPathList = this.calculatePtvsdPathList(notebook); + const ptvsdPathList = await this.calculatePtvsdPathList(notebook); if (ptvsdPathList && ptvsdPathList.length > 0) { const result = await this.executeSilently(notebook, `import sys\r\nsys.path.extend([${ptvsdPathList}])\r\nsys.path`); @@ -247,11 +269,16 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { return notebook.execute(code, Identifiers.EmptyFileName, 0, uuid(), undefined, true); } - private async ptvsdCheck(notebook: INotebook): Promise { + private async getKernelPythonVersion(notebook: INotebook): Promise { + const execResults = await this.executeSilently(notebook, 'import sys;print(sys.version)'); + return this.parseVersionInfo(execResults, 'pythonVersionInfo'); + } + + private async ptvsdCheck(notebook: INotebook): Promise { // We don't want to actually import ptvsd to check version so run !python instead. If we import an old version it's hard to get rid of on // an upgrade needed scenario // tslint:disable-next-line:no-multiline-string - const ptvsdPathList = this.calculatePtvsdPathList(notebook); + const ptvsdPathList = await this.calculatePtvsdPathList(notebook); let code; if (ptvsdPathList) { @@ -261,12 +288,12 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { } const ptvsdVersionResults = await this.executeSilently(notebook, code); - return this.parsePtvsdVersionInfo(ptvsdVersionResults); + return this.parseVersionInfo(ptvsdVersionResults, 'parsePtvsdVersionInfo'); } - private parsePtvsdVersionInfo(cells: ICell[]): IPtvsdVersion | undefined { + private parseVersionInfo(cells: ICell[], purpose: 'parsePtvsdVersionInfo' | 'pythonVersionInfo'): Version | undefined { if (cells.length < 1 || cells[0].state !== CellState.finished) { - this.traceCellResults('parsePtvsdVersionInfo', cells); + this.traceCellResults(purpose, cells); return undefined; } @@ -280,19 +307,22 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { const packageVersionMatch = packageVersionRegex.exec(outputString); if (packageVersionMatch) { + const major = parseInt(packageVersionMatch[1], 10); + const minor = parseInt(packageVersionMatch[2], 10); + const patch = parseInt(packageVersionMatch[3], 10); return { - major: parseInt(packageVersionMatch[1], 10), minor: parseInt(packageVersionMatch[2], 10), revision: packageVersionMatch[3] + major, minor, patch, build: [], prerelease: [], raw: `${major}.${minor}.${patch}` }; } } - this.traceCellResults('parsingPtvsdVersionInfo', cells); + this.traceCellResults(purpose, cells); return undefined; } // Check to see if the we have the required version of ptvsd to support debugging - private ptvsdMeetsRequirement(version: IPtvsdVersion): boolean { + private ptvsdMeetsRequirement(version: Version): boolean { if (version.major > this.requiredPtvsdVersion.major) { return true; } else if (version.major === this.requiredPtvsdVersion.major && version.minor >= this.requiredPtvsdVersion.minor) { @@ -303,7 +333,7 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { } @captureTelemetry(Telemetry.PtvsdPromptToInstall) - private async promptToInstallPtvsd(notebook: INotebook, oldVersion: IPtvsdVersion | undefined): Promise { + private async promptToInstallPtvsd(notebook: INotebook, oldVersion: Version | undefined): Promise { const promptMessage = oldVersion ? localize.DataScience.jupyterDebuggerInstallPtvsdUpdate() : localize.DataScience.jupyterDebuggerInstallPtvsdNew(); const result = await this.appShell.showInformationMessage(promptMessage, localize.DataScience.jupyterDebuggerInstallPtvsdYes(), localize.DataScience.jupyterDebuggerInstallPtvsdNo()); diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 12f04e68e8e0..2b85079f5f60 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -1,45 +1,38 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { Kernel } from '@jupyterlab/services'; -import { execSync } from 'child_process'; -import * as os from 'os'; -import * as path from 'path'; import { URL } from 'url'; import * as uuid from 'uuid/v4'; import { CancellationToken, Event, EventEmitter } from 'vscode'; import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; -import { Cancellation, CancellationError } from '../../common/cancellation'; +import { Cancellation } from '../../common/cancellation'; import { traceInfo } from '../../common/logger'; -import { IFileSystem, TemporaryDirectory } from '../../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory, SpawnOptions } from '../../common/process/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../../common/process/types'; import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { EXTENSION_ROOT_DIR } from '../../constants'; import { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { JupyterCommands, RegExpValues, Telemetry } from '../constants'; +import { JupyterCommands, Telemetry } from '../constants'; import { IConnection, IJupyterCommandFactory, IJupyterExecution, IJupyterKernelSpec, - IJupyterSessionManager, IJupyterSessionManagerFactory, INotebookServer, INotebookServerLaunchInfo, INotebookServerOptions } from '../types'; import { IFindCommandResult, JupyterCommandFinder } from './jupyterCommandFinder'; -import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; import { JupyterInstallError } from './jupyterInstallError'; -import { JupyterKernelSpec } from './jupyterKernelSpec'; import { JupyterSelfCertsError } from './jupyterSelfCertsError'; import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; +import { KernelService } from './kernelService'; +import { NotebookStarter } from './notebookStarter'; export class JupyterExecutionBase implements IJupyterExecution { @@ -47,17 +40,19 @@ export class JupyterExecutionBase implements IJupyterExecution { private eventEmitter: EventEmitter = new EventEmitter(); private disposed: boolean = false; private readonly commandFinder: JupyterCommandFinder; + private readonly kernelService: KernelService; + private readonly notebookStarter: NotebookStarter; constructor( _liveShare: ILiveShareApi, - private readonly executionFactory: IPythonExecutionFactory, + executionFactory: IPythonExecutionFactory, private readonly interpreterService: IInterpreterService, - private readonly processServiceFactory: IProcessServiceFactory, + processServiceFactory: IProcessServiceFactory, knownSearchPaths: IKnownSearchPathsForInterpreters, private readonly logger: ILogger, private readonly disposableRegistry: IDisposableRegistry, - private readonly asyncRegistry: IAsyncDisposableRegistry, - private readonly fileSystem: IFileSystem, + asyncRegistry: IAsyncDisposableRegistry, + fileSystem: IFileSystem, private readonly sessionManagerFactory: IJupyterSessionManagerFactory, workspace: IWorkspaceService, private readonly configuration: IConfigurationService, @@ -69,6 +64,10 @@ export class JupyterExecutionBase implements IJupyterExecution { fileSystem, logger, processServiceFactory, commandFactory, workspace, serviceContainer.get(IApplicationShell)); + this.kernelService = new KernelService(this, this.commandFinder, asyncRegistry, + processServiceFactory, interpreterService, fileSystem); + this.notebookStarter = new NotebookStarter(executionFactory, this, this.commandFinder, + this.kernelService, fileSystem, serviceContainer); this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(() => this.onSettingsChanged())); this.disposableRegistry.push(this); @@ -240,45 +239,6 @@ export class JupyterExecutionBase implements IJupyterExecution { return Promise.resolve(undefined); } - @captureTelemetry(Telemetry.FindJupyterKernelSpec) - protected async getMatchingKernelSpec(sessionManager: IJupyterSessionManager | undefined, cancelToken?: CancellationToken): Promise { - try { - // If not using an active connection, check on disk - if (!sessionManager) { - traceInfo('Searching for best interpreter'); - - // Get our best interpreter. We want its python path - const bestInterpreter = await this.getUsableJupyterPython(cancelToken); - - traceInfo(`Best interpreter is ${bestInterpreter ? bestInterpreter.path : 'notfound'}`); - - // Enumerate our kernel specs that jupyter will know about and see if - // one of them already matches based on path - if (bestInterpreter && !await this.hasSpecPathMatch(bestInterpreter, cancelToken)) { - - // Nobody matches on path, so generate a new kernel spec - if (await this.isKernelCreateSupported(cancelToken)) { - await this.addMatchingSpec(bestInterpreter, cancelToken); - } - } - } - - // Now enumerate them again - const enumerator = sessionManager ? () => sessionManager.getActiveKernelSpecs() : () => this.enumerateSpecs(cancelToken); - - // Then find our match - return this.findSpecMatch(enumerator); - } catch (e) { - // ECONNREFUSED seems to happen here. Log the error, but don't let it bubble out. We don't really need a kernel spec - this.logger.logWarning(e); - - // Double check our jupyter server is still running. - if (sessionManager && sessionManager.getConnInfo().localProcExitCode) { - throw new Error(localize.DataScience.jupyterServerCrashed().format(sessionManager!.getConnInfo().localProcExitCode!.toString())); - } - } - } - protected async findBestCommand(command: JupyterCommands, cancelToken?: CancellationToken): Promise { return this.commandFinder.findBestCommand(command, cancelToken); } @@ -318,7 +278,7 @@ export class JupyterExecutionBase implements IJupyterExecution { if (!kernelSpec && connection.localLaunch) { traceInfo(`Getting kernel specs for ${options ? options.purpose : 'unknown type of'} server`); const sessionManager = await this.sessionManagerFactory.create(connection); - kernelSpec = await this.getMatchingKernelSpec(sessionManager, cancelToken); + kernelSpec = await this.kernelService.getMatchingKernelSpec(sessionManager, cancelToken); await sessionManager.dispose(); } @@ -360,107 +320,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommand(JupyterCommands.NotebookCommand, cancelToken); this.checkNotebookCommand(notebookCommand); - - // Now actually launch it - let exitCode: number | null = 0; - try { - // Generate a temp dir with a unique GUID, both to match up our started server and to easily clean up after - const tempDir = await this.generateTempDir(); - this.disposableRegistry.push(tempDir); - - // In the temp dir, create an empty config python file. This is the same - // as starting jupyter with all of the defaults. - const configFile = useDefaultConfig ? path.join(tempDir.path, 'jupyter_notebook_config.py') : undefined; - if (configFile) { - await this.fileSystem.writeFile(configFile, ''); - this.logger.logInformation(`Generating custom default config at ${configFile}`); - } - - // Create extra args based on if we have a config or not - const extraArgs: string[] = []; - if (useDefaultConfig) { - extraArgs.push(`--config=${configFile}`); - } - // Check for the debug environment variable being set. Setting this - // causes Jupyter to output a lot more information about what it's doing - // under the covers and can be used to investigate problems with Jupyter. - if (process.env && process.env.VSCODE_PYTHON_DEBUG_JUPYTER) { - extraArgs.push('--debug'); - } - - // Modify the data rate limit if starting locally. The default prevents large dataframes from being returned. - extraArgs.push('--NotebookApp.iopub_data_rate_limit=10000000000.0'); - - // Check for a docker situation. - try { - if (await this.fileSystem.fileExists('/proc/self/cgroup')) { - const cgroup = await this.fileSystem.readFile('/proc/self/cgroup'); - if (cgroup.includes('docker')) { - // We definitely need an ip address. - extraArgs.push('--ip'); - extraArgs.push('127.0.0.1'); - - // Now see if we need --allow-root. - const idResults = execSync('id', { encoding: 'utf-8' }); - if (idResults.includes('(root)')) { - extraArgs.push('--allow-root'); - } - } - } - } catch { - noop(); - } - - // Use this temp file and config file to generate a list of args for our command - const args: string[] = [...['--no-browser', `--notebook-dir=${tempDir.path}`], ...extraArgs]; - - // Before starting the notebook process, make sure we generate a kernel spec - const kernelSpec = await this.getMatchingKernelSpec(undefined, cancelToken); - - // Make sure we haven't canceled already. - if (cancelToken && cancelToken.isCancellationRequested) { - throw new CancellationError(); - } - - // Then use this to launch our notebook process. - const stopWatch = new StopWatch(); - const launchResult = await notebookCommand.command!.execObservable(args, { throwOnStdErr: false, encoding: 'utf8', token: cancelToken }); - - // Watch for premature exits - if (launchResult.proc) { - launchResult.proc.on('exit', (c) => exitCode = c); - } - - // Make sure this process gets cleaned up. We might be canceled before the connection finishes. - if (launchResult && cancelToken) { - cancelToken.onCancellationRequested(() => { - launchResult.dispose(); - }); - } - - // Wait for the connection information on this result - const connection = await JupyterConnection.waitForConnection( - tempDir.path, this.getJupyterServerInfo, launchResult, this.serviceContainer, cancelToken); - - // Fire off telemetry for the process being talkable - sendTelemetryEvent(Telemetry.StartJupyterProcess, stopWatch.elapsedTime); - - return { - connection: connection, - kernelSpec: kernelSpec - }; - } catch (err) { - if (err instanceof CancellationError) { - throw err; - } - - // Something else went wrong. See if the local proc died or not. - if (exitCode !== 0) { - throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString())); - } else { - throw new Error(localize.DataScience.jupyterNotebookFailure().format(err)); - } - } + return this.notebookStarter.start(useDefaultConfig, cancelToken); } private getUsableJupyterPythonImpl = async (cancelToken?: CancellationToken): Promise => { @@ -473,114 +333,11 @@ export class JupyterExecutionBase implements IJupyterExecution { return undefined; } - private getJupyterServerInfo = async (cancelToken?: CancellationToken): Promise => { - // We have a small python file here that we will execute to get the server info from all running Jupyter instances - const bestInterpreter = await this.getUsableJupyterPython(cancelToken); - if (bestInterpreter) { - const newOptions: SpawnOptions = { mergeStdOutErr: true, token: cancelToken }; - const launcher = await this.executionFactory.createActivatedEnvironment( - { resource: undefined, interpreter: bestInterpreter, allowEnvironmentFetchExceptions: true }); - const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - const serverInfoString = await launcher.exec([file], newOptions); - - let serverInfos: JupyterServerInfo[]; - try { - // Parse out our results, return undefined if we can't suss it out - serverInfos = JSON.parse(serverInfoString.stdout.trim()) as JupyterServerInfo[]; - } catch (err) { - return undefined; - } - return serverInfos; - } - - return undefined; - } - private onSettingsChanged() { // Clear our usableJupyterInterpreter so that we recompute our values this.usablePythonInterpreter = undefined; } - private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise { - const displayName = localize.DataScience.historyTitle(); - const ipykernelCommand = await this.findBestCommand(JupyterCommands.KernelCreateCommand, cancelToken); - - // If this fails, then we just skip this spec - try { - // Run the ipykernel install command. This will generate a new kernel spec. However - // it will be pointing to the python that ran it. We'll fix that up afterwards - const name = uuid(); - if (ipykernelCommand && ipykernelCommand.command) { - const result = await ipykernelCommand.command.exec(['install', '--user', '--name', name, '--display-name', `'${displayName}'`], { throwOnStdErr: true, encoding: 'utf8', token: cancelToken }); - - // Result should have our file name. - const match = RegExpValues.PyKernelOutputRegEx.exec(result.stdout); - const diskPath = match && match !== null && match.length > 1 ? path.join(match[1], 'kernel.json') : await this.findSpecPath(name); - - // Make sure we delete this file at some point. When we close VS code is probably good. It will also be destroy when - // the kernel spec goes away - this.asyncRegistry.push({ - dispose: async () => { - if (!diskPath) { - return; - } - try { - await this.fileSystem.deleteDirectory(path.dirname(diskPath)); - } catch { - noop(); - } - } - }); - - // If that works, rewrite our active interpreter into the argv - if (diskPath && bestInterpreter) { - if (await this.fileSystem.fileExists(diskPath)) { - const specModel: Kernel.ISpecModel = JSON.parse(await this.fileSystem.readFile(diskPath)); - specModel.argv[0] = bestInterpreter.path; - await this.fileSystem.writeFile(diskPath, JSON.stringify(specModel), { flag: 'w', encoding: 'utf8' }); - } - } - } - } catch (err) { - this.logger.logError(err); - } - } - - private findSpecPath = async (specName: string, cancelToken?: CancellationToken): Promise => { - // Enumerate all specs and get path for the match - const specs = await this.enumerateSpecs(cancelToken); - const match = specs! - .filter(s => s !== undefined) - .find(s => { - const js = s as JupyterKernelSpec; - return js && js.name === specName; - }) as JupyterKernelSpec; - return match ? match.specFile : undefined; - } - - private async generateTempDir(): Promise { - const resultDir = path.join(os.tmpdir(), uuid()); - await this.fileSystem.createDirectory(resultDir); - - return { - path: resultDir, - dispose: async () => { - // Try ten times. Process may still be up and running. - // We don't want to do async as async dispose means it may never finish and then we don't - // delete - let count = 0; - while (count < 10) { - try { - await this.fileSystem.deleteDirectory(resultDir); - count = 10; - } catch { - count += 1; - } - } - } - }; - } - private isCommandSupported = async (command: JupyterCommands, cancelToken?: CancellationToken): Promise => { // See if we can find the command try { @@ -591,194 +348,4 @@ export class JupyterExecutionBase implements IJupyterExecution { return false; } } - - private hasSpecPathMatch = async (info: PythonInterpreter | undefined, cancelToken?: CancellationToken): Promise => { - if (info) { - // Enumerate our specs - const specs = await this.enumerateSpecs(cancelToken); - - // See if any of their paths match - return specs.findIndex(s => { - if (info && s && s.path) { - return this.fileSystem.arePathsSame(s.path, info.path); - } - return false; - }) >= 0; - } - - // If no active interpreter, just act like everything is okay as we can't find a new spec anyway - return true; - } - - private async getInterpreterDetailsFromProcess(baseProcessName: string): Promise { - if (path.basename(baseProcessName) !== baseProcessName) { - // This function should only be called with a non qualified path. We're using this - // function to figure out the qualified path - return undefined; - } - - // Make sure it's python based - if (!baseProcessName.toLocaleLowerCase().includes('python')) { - return undefined; - } - - try { - // Create a new process service to use to execute this process - const processService = await this.processServiceFactory.create(); - - // Ask python for what path it's running at. - const output = await processService.exec(baseProcessName, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true }); - const fullPath = output.stdout.trim(); - - // Use this path to get the interpreter details. - return this.interpreterService.getInterpreterDetails(fullPath); - } catch { - // Any failure, just assume this path is invalid. - return undefined; - } - } - - //tslint:disable-next-line:cyclomatic-complexity - private findSpecMatch = async (enumerator: () => Promise<(IJupyterKernelSpec | undefined)[]>): Promise => { - traceInfo('Searching for a kernelspec match'); - // Extract our current python information that the user has picked. - // We'll match against this. - const info = await this.interpreterService.getActiveInterpreter(); - let bestScore = 0; - let bestSpec: IJupyterKernelSpec | undefined; - - // Then enumerate our specs - const specs = await enumerator(); - - // For each get its details as we will likely need them - const specDetails = await Promise.all(specs.map(async s => { - if (s && s.path && s.path.length > 0 && await this.fileSystem.fileExists(s.path)) { - return this.interpreterService.getInterpreterDetails(s.path); - } - if (s && s.path && s.path.length > 0 && path.basename(s.path) === s.path) { - // This means the s.path isn't fully qualified. Try figuring it out. - return this.getInterpreterDetailsFromProcess(s.path); - } - })); - - for (let i = 0; specs && i < specs.length; i += 1) { - const spec = specs[i]; - let score = 0; - - // First match on language. No point if not python. - if (spec && spec.language && spec.language.toLocaleLowerCase() === 'python') { - // Language match - score += 1; - - // See if the path matches. Don't bother if the language doesn't. - if (spec && spec.path && spec.path.length > 0 && info && spec.path === info.path) { - // Path match - score += 10; - } - - // See if the version is the same - if (info && info.version && specDetails[i]) { - const details = specDetails[i]; - if (details && details.version) { - if (details.version.major === info.version.major) { - // Major version match - score += 4; - - if (details.version.minor === info.version.minor) { - // Minor version match - score += 2; - - if (details.version.patch === info.version.patch) { - // Minor version match - score += 1; - } - } - } - } - } else if (info && info.version && spec && spec.path && spec.path.toLocaleLowerCase() === 'python' && spec.name) { - // This should be our current python. - - // Search for a digit on the end of the name. It should match our major version - const match = /\D+(\d+)/.exec(spec.name); - if (match && match !== null && match.length > 0) { - // See if the version number matches - const nameVersion = parseInt(match[0], 10); - if (nameVersion && nameVersion === info.version.major) { - score += 4; - } - } - } - } - - // Update high score - if (score > bestScore) { - bestScore = score; - bestSpec = spec; - } - } - - // If still not set, at least pick the first one - if (!bestSpec && specs && specs.length > 0) { - bestSpec = specs[0]; - } - - traceInfo(`Found kernelspec match ${bestSpec ? `${bestSpec.name}' '${bestSpec.path}` : 'undefined'}`); - return bestSpec; - } - - private async readSpec(kernelSpecOutputLine: string): Promise { - const match = RegExpValues.KernelSpecOutputRegEx.exec(kernelSpecOutputLine); - if (match && match !== null && match.length > 2) { - // Second match should be our path to the kernel spec - const file = path.join(match[2], 'kernel.json'); - try { - if (await this.fileSystem.fileExists(file)) { - // Turn this into a IJupyterKernelSpec - const model = JSON.parse(await this.fileSystem.readFile(file)); - model.name = match[1]; - return new JupyterKernelSpec(model, file); - } - } catch { - // Just return nothing if we can't parse. - } - } - - return undefined; - } - - private enumerateSpecs = async (_cancelToken?: CancellationToken): Promise<(JupyterKernelSpec | undefined)[]> => { - if (await this.isKernelSpecSupported()) { - const kernelSpecCommand = await this.findBestCommand(JupyterCommands.KernelSpecCommand); - - if (kernelSpecCommand.command) { - try { - traceInfo('Asking for kernelspecs from jupyter'); - - // Ask for our current list. - const list = await kernelSpecCommand.command.exec(['list'], { throwOnStdErr: true, encoding: 'utf8' }); - - traceInfo('Parsing kernelspecs from jupyter'); - - // This should give us back a key value pair we can parse - const lines = list.stdout.splitLines({ trim: false, removeEmptyEntries: true }); - - // Generate all of the promises at once - const promises = lines.map(l => this.readSpec(l)); - - traceInfo('Awaiting the read of kernelspecs from jupyter'); - - // Then let them run concurrently (they are file io) - const specs = await Promise.all(promises); - - traceInfo('Returning kernelspecs from jupyter'); - return specs!.filter(s => s); - } catch { - // This is failing for some folks. In that case return nothing - return []; - } - } - } - - return []; - } } diff --git a/src/client/datascience/jupyter/jupyterImporter.ts b/src/client/datascience/jupyter/jupyterImporter.ts index 2fcf89670d2f..c7db56d274bb 100644 --- a/src/client/datascience/jupyter/jupyterImporter.ts +++ b/src/client/datascience/jupyter/jupyterImporter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { nbformat } from '@jupyterlab/coreutils'; import * as fs from 'fs-extra'; import { inject, injectable } from 'inversify'; import * as os from 'os'; @@ -47,19 +47,20 @@ export class JupyterImporter implements INotebookImporter { this.templatePromise = this.createTemplateFile(); } - public async importFromFile(file: string): Promise { + public async importFromFile(contentsFile: string, originalFile?: string): Promise { const template = await this.templatePromise; // If the user has requested it, add a cd command to the imported file so that relative paths still work const settings = this.configuration.getSettings(); let directoryChange: string | undefined; if (settings.datascience.changeDirOnImportExport) { - directoryChange = await this.calculateDirectoryChange(file); + // If an original file is passed in, then use that for calculating the directory change as contents might be an invalid location + directoryChange = await this.calculateDirectoryChange(originalFile ? originalFile : contentsFile); } // Use the jupyter nbconvert functionality to turn the notebook into a python file if (await this.jupyterExecution.isImportSupported()) { - let fileOutput: string = await this.jupyterExecution.importNotebook(file, template); + let fileOutput: string = await this.jupyterExecution.importNotebook(contentsFile, template); if (fileOutput.includes('get_ipython()')) { fileOutput = this.addIPythonImport(fileOutput); } diff --git a/src/client/datascience/jupyter/jupyterKernelSpec.ts b/src/client/datascience/jupyter/jupyterKernelSpec.ts index 9881db8671c7..25b171e0464c 100644 --- a/src/client/datascience/jupyter/jupyterKernelSpec.ts +++ b/src/client/datascience/jupyter/jupyterKernelSpec.ts @@ -15,7 +15,7 @@ export class JupyterKernelSpec implements IJupyterKernelSpec { public language: string; public path: string; public specFile: string | undefined; - constructor(specModel : Kernel.ISpecModel, file?: string) { + constructor(specModel: Kernel.ISpecModel, file?: string) { this.name = specModel.name; this.language = specModel.language; this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : ''; diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index 94bcf6186119..cf95a9f3befd 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -255,6 +255,10 @@ export class JupyterNotebookBase implements INotebook { return this.updateWorkingDirectory(file); } + public addLogger(logger: INotebookExecutionLogger) { + this.loggers.push(logger); + } + public executeObservable(code: string, file: string, line: number, id: string, silent: boolean = false): Observable { // Create an observable and wrap the result so we can time it. const stopWatch = new StopWatch(); diff --git a/src/client/datascience/jupyter/kernelService.ts b/src/client/datascience/jupyter/kernelService.ts new file mode 100644 index 000000000000..46130666232a --- /dev/null +++ b/src/client/datascience/jupyter/kernelService.ts @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Kernel } from '@jupyterlab/services'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken } from 'vscode'; +import '../../common/extensions'; +import { traceError, traceInfo, traceWarning } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { IAsyncDisposableRegistry } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; +import { captureTelemetry } from '../../telemetry'; +import { JupyterCommands, RegExpValues, Telemetry } from '../constants'; +import { IJupyterExecution, IJupyterKernelSpec, IJupyterSessionManager } from '../types'; +import { JupyterCommandFinder } from './jupyterCommandFinder'; +import { JupyterKernelSpec } from './jupyterKernelSpec'; + +/** + * Responsible for kernel management and the like. + * + * @export + * @class KernelService + */ +export class KernelService { + constructor( + private readonly jupyterExecution: IJupyterExecution, + private readonly commandFinder: JupyterCommandFinder, + private readonly asyncRegistry: IAsyncDisposableRegistry, + private readonly processServiceFactory: IProcessServiceFactory, + private readonly interpreterService: IInterpreterService, + private readonly fileSystem: IFileSystem + ) {} + @captureTelemetry(Telemetry.FindJupyterKernelSpec) + public async getMatchingKernelSpec(sessionManager: IJupyterSessionManager | undefined, cancelToken?: CancellationToken): Promise { + try { + // If not using an active connection, check on disk + if (!sessionManager) { + traceInfo('Searching for best interpreter'); + + // Get our best interpreter. We want its python path + const bestInterpreter = await this.jupyterExecution.getUsableJupyterPython(cancelToken); + + traceInfo(`Best interpreter is ${bestInterpreter ? bestInterpreter.path : 'notfound'}`); + + // Enumerate our kernel specs that jupyter will know about and see if + // one of them already matches based on path + if (bestInterpreter && !(await this.hasSpecPathMatch(bestInterpreter, cancelToken))) { + // Nobody matches on path, so generate a new kernel spec + if (await this.jupyterExecution.isKernelCreateSupported(cancelToken)) { + await this.addMatchingSpec(bestInterpreter, cancelToken); + } + } + } + + // Now enumerate them again + const enumerator = sessionManager ? () => sessionManager.getActiveKernelSpecs() : () => this.enumerateSpecs(cancelToken); + + // Then find our match + return this.findSpecMatch(enumerator); + } catch (e) { + // ECONNREFUSED seems to happen here. Log the error, but don't let it bubble out. We don't really need a kernel spec + traceWarning(e); + + // Double check our jupyter server is still running. + if (sessionManager && sessionManager.getConnInfo().localProcExitCode) { + throw new Error(localize.DataScience.jupyterServerCrashed().format(sessionManager!.getConnInfo().localProcExitCode!.toString())); + } + } + } + private hasSpecPathMatch = async (info: PythonInterpreter | undefined, cancelToken?: CancellationToken): Promise => { + if (info) { + // Enumerate our specs + const specs = await this.enumerateSpecs(cancelToken); + + // See if any of their paths match + return ( + specs.findIndex(s => { + if (info && s && s.path) { + return this.fileSystem.arePathsSame(s.path, info.path); + } + return false; + }) >= 0 + ); + } + + // If no active interpreter, just act like everything is okay as we can't find a new spec anyway + return true; + } + + private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise { + const displayName = localize.DataScience.historyTitle(); + const ipykernelCommand = await this.commandFinder.findBestCommand(JupyterCommands.KernelCreateCommand, cancelToken); + + // If this fails, then we just skip this spec + try { + // Run the ipykernel install command. This will generate a new kernel spec. However + // it will be pointing to the python that ran it. We'll fix that up afterwards + const name = uuid(); + if (ipykernelCommand && ipykernelCommand.command) { + const result = await ipykernelCommand.command.exec(['install', '--user', '--name', name, '--display-name', `'${displayName}'`], { + throwOnStdErr: true, + encoding: 'utf8', + token: cancelToken + }); + + // Result should have our file name. + const match = RegExpValues.PyKernelOutputRegEx.exec(result.stdout); + const diskPath = match && match !== null && match.length > 1 ? path.join(match[1], 'kernel.json') : await this.findSpecPath(name); + + // Make sure we delete this file at some point. When we close VS code is probably good. It will also be destroy when + // the kernel spec goes away + this.asyncRegistry.push({ + dispose: async () => { + if (!diskPath) { + return; + } + try { + await this.fileSystem.deleteDirectory(path.dirname(diskPath)); + } catch { + noop(); + } + } + }); + + // If that works, rewrite our active interpreter into the argv + if (diskPath && bestInterpreter) { + if (await this.fileSystem.fileExists(diskPath)) { + const specModel: Kernel.ISpecModel = JSON.parse(await this.fileSystem.readFile(diskPath)); + specModel.argv[0] = bestInterpreter.path; + await this.fileSystem.writeFile(diskPath, JSON.stringify(specModel), { flag: 'w', encoding: 'utf8' }); + } + } + } + } catch (err) { + traceError(err); + } + } + + private findSpecPath = async (specName: string, cancelToken?: CancellationToken): Promise => { + // Enumerate all specs and get path for the match + const specs = await this.enumerateSpecs(cancelToken); + const match = specs! + .filter(s => s !== undefined) + .find(s => { + const js = s as JupyterKernelSpec; + return js && js.name === specName; + }) as JupyterKernelSpec; + return match ? match.specFile : undefined; + } + + //tslint:disable-next-line:cyclomatic-complexity + private findSpecMatch = async (enumerator: () => Promise<(IJupyterKernelSpec | undefined)[]>): Promise => { + traceInfo('Searching for a kernelspec match'); + // Extract our current python information that the user has picked. + // We'll match against this. + const info = await this.interpreterService.getActiveInterpreter(); + let bestScore = 0; + let bestSpec: IJupyterKernelSpec | undefined; + + // Then enumerate our specs + const specs = await enumerator(); + + // For each get its details as we will likely need them + const specDetails = await Promise.all( + specs.map(async s => { + if (s && s.path && s.path.length > 0 && (await this.fileSystem.fileExists(s.path))) { + return this.interpreterService.getInterpreterDetails(s.path); + } + if (s && s.path && s.path.length > 0 && path.basename(s.path) === s.path) { + // This means the s.path isn't fully qualified. Try figuring it out. + return this.getInterpreterDetailsFromProcess(s.path); + } + }) + ); + + for (let i = 0; specs && i < specs.length; i += 1) { + const spec = specs[i]; + let score = 0; + + // First match on language. No point if not python. + if (spec && spec.language && spec.language.toLocaleLowerCase() === 'python') { + // Language match + score += 1; + + // See if the path matches. Don't bother if the language doesn't. + if (spec && spec.path && spec.path.length > 0 && info && spec.path === info.path) { + // Path match + score += 10; + } + + // See if the version is the same + if (info && info.version && specDetails[i]) { + const details = specDetails[i]; + if (details && details.version) { + if (details.version.major === info.version.major) { + // Major version match + score += 4; + + if (details.version.minor === info.version.minor) { + // Minor version match + score += 2; + + if (details.version.patch === info.version.patch) { + // Minor version match + score += 1; + } + } + } + } + } else if (info && info.version && spec && spec.path && spec.path.toLocaleLowerCase() === 'python' && spec.name) { + // This should be our current python. + + // Search for a digit on the end of the name. It should match our major version + const match = /\D+(\d+)/.exec(spec.name); + if (match && match !== null && match.length > 0) { + // See if the version number matches + const nameVersion = parseInt(match[0], 10); + if (nameVersion && nameVersion === info.version.major) { + score += 4; + } + } + } + } + + // Update high score + if (score > bestScore) { + bestScore = score; + bestSpec = spec; + } + } + + // If still not set, at least pick the first one + if (!bestSpec && specs && specs.length > 0) { + bestSpec = specs[0]; + } + + traceInfo(`Found kernelspec match ${bestSpec ? `${bestSpec.name}' '${bestSpec.path}` : 'undefined'}`); + return bestSpec; + } + + private async readSpec(kernelSpecOutputLine: string): Promise { + const match = RegExpValues.KernelSpecOutputRegEx.exec(kernelSpecOutputLine); + if (match && match !== null && match.length > 2) { + // Second match should be our path to the kernel spec + const file = path.join(match[2], 'kernel.json'); + try { + if (await this.fileSystem.fileExists(file)) { + // Turn this into a IJupyterKernelSpec + const model = JSON.parse(await this.fileSystem.readFile(file)); + model.name = match[1]; + return new JupyterKernelSpec(model, file); + } + } catch { + // Just return nothing if we can't parse. + } + } + + return undefined; + } + + private enumerateSpecs = async (_cancelToken?: CancellationToken): Promise<(JupyterKernelSpec | undefined)[]> => { + if (await this.jupyterExecution.isKernelSpecSupported()) { + const kernelSpecCommand = await this.commandFinder.findBestCommand(JupyterCommands.KernelSpecCommand); + + if (kernelSpecCommand.command) { + try { + traceInfo('Asking for kernelspecs from jupyter'); + + // Ask for our current list. + const list = await kernelSpecCommand.command.exec(['list'], { throwOnStdErr: true, encoding: 'utf8' }); + + traceInfo('Parsing kernelspecs from jupyter'); + + // This should give us back a key value pair we can parse + const lines = list.stdout.splitLines({ trim: false, removeEmptyEntries: true }); + + // Generate all of the promises at once + const promises = lines.map(l => this.readSpec(l)); + + traceInfo('Awaiting the read of kernelspecs from jupyter'); + + // Then let them run concurrently (they are file io) + const specs = await Promise.all(promises); + + traceInfo('Returning kernelspecs from jupyter'); + return specs!.filter(s => s); + } catch { + // This is failing for some folks. In that case return nothing + return []; + } + } + } + + return []; + } + private async getInterpreterDetailsFromProcess(baseProcessName: string): Promise { + if (path.basename(baseProcessName) !== baseProcessName) { + // This function should only be called with a non qualified path. We're using this + // function to figure out the qualified path + return undefined; + } + + // Make sure it's python based + if (!baseProcessName.toLocaleLowerCase().includes('python')) { + return undefined; + } + + try { + // Create a new process service to use to execute this process + const processService = await this.processServiceFactory.create(); + + // Ask python for what path it's running at. + const output = await processService.exec(baseProcessName, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true }); + const fullPath = output.stdout.trim(); + + // Use this path to get the interpreter details. + return this.interpreterService.getInterpreterDetails(fullPath); + } catch { + // Any failure, just assume this path is invalid. + return undefined; + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts index f9427d34192d..0a1a7fddd96f 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts @@ -63,7 +63,7 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec commandFactory, serviceContainer); asyncRegistry.push(this); - this.serverCache = new ServerCache(configuration, workspace, fileSystem); + this.serverCache = new ServerCache(configuration, workspace, fileSystem, interpreterService); } public async dispose(): Promise { diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts index d2c8a572f6f2..f55a9c486323 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts @@ -14,7 +14,7 @@ import { createDeferred } from '../../../common/utils/async'; import * as localize from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; import { LiveShare, LiveShareCommands } from '../../constants'; -import { ICell, INotebook, INotebookCompletion, INotebookServer, InterruptResult } from '../../types'; +import { ICell, INotebook, INotebookCompletion, INotebookExecutionLogger, INotebookServer, InterruptResult } from '../../types'; import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; import { ResponseQueue } from './responseQueue'; import { IExecuteObservableResponse, ILiveShareParticipant, IServerResponse } from './types'; @@ -92,6 +92,10 @@ export class GuestJupyterNotebook return Promise.resolve(); } + public addLogger(_logger: INotebookExecutionLogger): void { + noop(); + } + public async setMatplotLibStyle(_useDark: boolean): Promise { // Guest can't change the style. Maybe output a warning here? } diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts index ce66abee96d6..f4dd6d50f17a 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts @@ -64,7 +64,7 @@ export class HostJupyterExecution configService, commandFactory, serviceContainer); - this.serverCache = new ServerCache(configService, workspace, fileSys); + this.serverCache = new ServerCache(configService, workspace, fileSys, interpreterService); } public async dispose(): Promise { diff --git a/src/client/datascience/jupyter/liveshare/serverCache.ts b/src/client/datascience/jupyter/liveshare/serverCache.ts index 714b02287fe7..99597935224f 100644 --- a/src/client/datascience/jupyter/liveshare/serverCache.ts +++ b/src/client/datascience/jupyter/liveshare/serverCache.ts @@ -9,6 +9,7 @@ import * as uuid from 'uuid/v4'; import { IWorkspaceService } from '../../../common/application/types'; import { IFileSystem } from '../../../common/platform/types'; import { IAsyncDisposable, IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { INotebookServer, INotebookServerOptions } from '../../types'; export class ServerCache implements IAsyncDisposable { @@ -18,7 +19,8 @@ export class ServerCache implements IAsyncDisposable { constructor( private configService: IConfigurationService, private workspace: IWorkspaceService, - private fileSystem: IFileSystem + private fileSystem: IFileSystem, + private interpreterService: IInterpreterService ) { } public async get(options?: INotebookServerOptions): Promise { @@ -60,13 +62,16 @@ export class ServerCache implements IAsyncDisposable { } public async generateDefaultOptions(options?: INotebookServerOptions): Promise { + const activeInterpreter = await this.interpreterService.getActiveInterpreter(); + const activeInterpreterPath = activeInterpreter ? activeInterpreter.path : undefined; return { enableDebugging: options ? options.enableDebugging : false, uri: options ? options.uri : undefined, useDefaultConfig: options ? options.useDefaultConfig : true, // Default for this is true. usingDarkTheme: options ? options.usingDarkTheme : undefined, purpose: options ? options.purpose : uuid(), - workingDir: options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory() + workingDir: options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory(), + interpreterPath: options && options.interpreterPath ? options.interpreterPath : activeInterpreterPath }; } @@ -76,11 +81,12 @@ export class ServerCache implements IAsyncDisposable { } else { // combine all the values together to make a unique key const uri = options.uri ? options.uri : ''; + const interpreter = options.interpreterPath ? options.interpreterPath : ''; const useFlag = options.useDefaultConfig ? 'true' : 'false'; const debug = options.enableDebugging ? 'true' : 'false'; // tslint:disable-next-line:no-suspicious-comment // TODO: Should there be some separator in the key? - return `${options.purpose}${uri}${useFlag}${options.workingDir}${debug}`; + return `${options.purpose}${uri}${useFlag}${options.workingDir}${debug}${interpreter}`; } } diff --git a/src/client/datascience/jupyter/notebookStarter.ts b/src/client/datascience/jupyter/notebookStarter.ts new file mode 100644 index 000000000000..8133f1ef6f0e --- /dev/null +++ b/src/client/datascience/jupyter/notebookStarter.ts @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as cp from 'child_process'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken, Disposable } from 'vscode'; +import { CancellationError } from '../../common/cancellation'; +import { traceInfo } from '../../common/logger'; +import { IFileSystem, TemporaryDirectory } from '../../common/platform/types'; +import { IPythonExecutionFactory, SpawnOptions } from '../../common/process/types'; +import { IDisposable } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IServiceContainer } from '../../ioc/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { JupyterCommands, Telemetry } from '../constants'; +import { IConnection, IJupyterExecution, IJupyterKernelSpec } from '../types'; +import { JupyterCommandFinder } from './jupyterCommandFinder'; +import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; +import { KernelService } from './kernelService'; + +/** + * Responsible for starting a notebook. + * Separate class as theres quite a lot of work involved in starting a notebook. + * + * @export + * @class NotebookStarter + * @implements {Disposable} + */ +export class NotebookStarter implements Disposable { + private readonly disposables: IDisposable[] = []; + constructor( + private readonly executionFactory: IPythonExecutionFactory, + private readonly jupyterExecution: IJupyterExecution, + private readonly commandFinder: JupyterCommandFinder, + private readonly kernelService: KernelService, + private readonly fileSystem: IFileSystem, + private readonly serviceContainer: IServiceContainer + ) {} + public dispose() { + while (this.disposables.length > 0) { + const disposable = this.disposables.shift(); + try { + if (disposable) { + disposable.dispose(); + } + } catch { + // Nohting + } + } + } + // tslint:disable-next-line: max-func-body-length + public async start(useDefaultConfig: boolean, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { + const notebookCommand = await this.commandFinder.findBestCommand(JupyterCommands.NotebookCommand); + // Now actually launch it + let exitCode: number | null = 0; + try { + // Generate a temp dir with a unique GUID, both to match up our started server and to easily clean up after + const tempDirPromise = this.generateTempDir(); + tempDirPromise.then(dir => this.disposables.push(dir)).ignoreErrors(); + + // Before starting the notebook process, make sure we generate a kernel spec + const [args, kernelSpec] = await Promise.all([this.generateArguments(useDefaultConfig, tempDirPromise), this.kernelService.getMatchingKernelSpec(undefined, cancelToken)]); + + // Make sure we haven't canceled already. + if (cancelToken && cancelToken.isCancellationRequested) { + throw new CancellationError(); + } + + // Then use this to launch our notebook process. + const stopWatch = new StopWatch(); + const launchResult = await notebookCommand.command!.execObservable(args, { throwOnStdErr: false, encoding: 'utf8', token: cancelToken }); + + // Watch for premature exits + if (launchResult.proc) { + launchResult.proc.on('exit', c => (exitCode = c)); + } + + // Make sure this process gets cleaned up. We might be canceled before the connection finishes. + if (launchResult && cancelToken) { + cancelToken.onCancellationRequested(() => { + launchResult.dispose(); + }); + } + + // Wait for the connection information on this result + const tempDir = await tempDirPromise; + const connection = await JupyterConnection.waitForConnection(tempDir.path, this.getJupyterServerInfo, launchResult, this.serviceContainer, cancelToken); + + // Fire off telemetry for the process being talkable + sendTelemetryEvent(Telemetry.StartJupyterProcess, stopWatch.elapsedTime); + + return { + connection: connection, + kernelSpec: kernelSpec + }; + } catch (err) { + if (err instanceof CancellationError) { + throw err; + } + + // Something else went wrong. See if the local proc died or not. + if (exitCode !== 0) { + throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString())); + } else { + throw new Error(localize.DataScience.jupyterNotebookFailure().format(err)); + } + } + } + + private async generateArguments(useDefaultConfig: boolean, tempDirPromise: Promise): Promise{ + // Parallelize as much as possible. + const promisedArgs: Promise[] = []; + promisedArgs.push(Promise.resolve('--no-browser')); + promisedArgs.push(this.getNotebookDirArgument(tempDirPromise)); + if (useDefaultConfig) { + promisedArgs.push(this.getConfigArgument(tempDirPromise)); + } + // Modify the data rate limit if starting locally. The default prevents large dataframes from being returned. + promisedArgs.push(Promise.resolve('--NotebookApp.iopub_data_rate_limit=10000000000.0')); + + const [args, dockerArgs] = await Promise.all([Promise.all(promisedArgs), this.getDockerArguments()]); + + // Check for the debug environment variable being set. Setting this + // causes Jupyter to output a lot more information about what it's doing + // under the covers and can be used to investigate problems with Jupyter. + const debugArgs = (process.env && process.env.VSCODE_PYTHON_DEBUG_JUPYTER) ? ['--debug'] : []; + + // Use this temp file and config file to generate a list of args for our command + return [...args, ...dockerArgs, ...debugArgs]; + } + + /** + * Gets the `--notebook-dir` argument. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private async getNotebookDirArgument(tempDirectory: Promise): Promise { + const tempDir = await tempDirectory; + return `--notebook-dir=${tempDir.path}`; + } + + /** + * Gets the `--config` argument. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private async getConfigArgument(tempDirectory: Promise): Promise { + const tempDir = await tempDirectory; + // In the temp dir, create an empty config python file. This is the same + // as starting jupyter with all of the defaults. + const configFile = path.join(tempDir.path, 'jupyter_notebook_config.py'); + await this.fileSystem.writeFile(configFile, ''); + traceInfo(`Generating custom default config at ${configFile}`); + + // Create extra args based on if we have a config or not + return `--config=${configFile}`; + } + + /** + * Adds the `--ip` and `--allow-root` arguments when in docker. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private async getDockerArguments(): Promise { + const args: string[] = []; + // Check for a docker situation. + try { + const cgroup = await this.fileSystem.readFile('/proc/self/cgroup').catch(() => ''); + if (!cgroup.includes('docker')) { + return args; + } + // We definitely need an ip address. + args.push('--ip'); + args.push('127.0.0.1'); + + // Now see if we need --allow-root. + return new Promise(resolve => { + cp.exec('id', { encoding: 'utf-8' }, (_, stdout: string | Buffer) => { + if (stdout && stdout.toString().includes('(root)')) { + args.push('--allow-root'); + } + resolve([]); + }); + }); + } catch { + return args; + } + } + private async generateTempDir(): Promise { + const resultDir = path.join(os.tmpdir(), uuid()); + await this.fileSystem.createDirectory(resultDir); + + return { + path: resultDir, + dispose: async () => { + // Try ten times. Process may still be up and running. + // We don't want to do async as async dispose means it may never finish and then we don't + // delete + let count = 0; + while (count < 10) { + try { + await this.fileSystem.deleteDirectory(resultDir); + count = 10; + } catch { + count += 1; + } + } + } + }; + } + private getJupyterServerInfo = async (cancelToken?: CancellationToken): Promise => { + // We have a small python file here that we will execute to get the server info from all running Jupyter instances + const bestInterpreter = await this.jupyterExecution.getUsableJupyterPython(cancelToken); + if (bestInterpreter) { + const newOptions: SpawnOptions = { mergeStdOutErr: true, token: cancelToken }; + const launcher = await this.executionFactory.createActivatedEnvironment({ resource: undefined, interpreter: bestInterpreter, allowEnvironmentFetchExceptions: true }); + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); + const serverInfoString = await launcher.exec([file], newOptions); + + let serverInfos: JupyterServerInfo[]; + try { + // Parse out our results, return undefined if we can't suss it out + serverInfos = JSON.parse(serverInfoString.stdout.trim()) as JupyterServerInfo[]; + } catch (err) { + return undefined; + } + return serverInfos; + } + + return undefined; + } +} diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index 435b6285d99b..7a811e612c8d 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -2,12 +2,8 @@ // Licensed under the MIT License. 'use strict'; import { IExtensionSingleActivationService } from '../activation/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { ClassType, IServiceManager } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; +import { IServiceManager } from '../ioc/types'; import { CodeCssGenerator } from './codeCssGenerator'; -import { Telemetry } from './constants'; import { DataViewer } from './data-viewing/dataViewer'; import { DataViewerProvider } from './data-viewing/dataViewerProvider'; import { DataScience } from './datascience'; @@ -80,65 +76,47 @@ import { IThemeFinder } from './types'; -// tslint:disable:no-any -function wrapType(ctor: ClassType): ClassType { - return class extends ctor { - constructor(...args: any[]) { - const stopWatch = new StopWatch(); - super(...args); - try { - // ctor name is minified. compute from the class definition - const className = ctor.toString().match(/\w+/g)![1]; - sendTelemetryEvent(Telemetry.ClassConstructionTime, stopWatch.elapsedTime, { class: className }); - } catch { - noop(); - } - } - }; -} - export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IDataScienceCodeLensProvider, wrapType(DataScienceCodeLensProvider)); - serviceManager.addSingleton(IDataScience, wrapType(DataScience)); - serviceManager.addSingleton(IJupyterExecution, wrapType(JupyterExecutionFactory)); - serviceManager.addSingleton(IDataScienceCommandListener, wrapType(InteractiveWindowCommandListener)); - serviceManager.addSingleton(IInteractiveWindowProvider, wrapType(InteractiveWindowProvider)); - serviceManager.add(IInteractiveWindow, wrapType(InteractiveWindow)); - serviceManager.add(INotebookExporter, wrapType(JupyterExporter)); - serviceManager.add(INotebookImporter, wrapType(JupyterImporter)); - serviceManager.add(INotebookServer, wrapType(JupyterServerFactory)); - serviceManager.addSingleton(ICodeCssGenerator, wrapType(CodeCssGenerator)); - serviceManager.addSingleton(IJupyterPasswordConnect, wrapType(JupyterPasswordConnect)); - serviceManager.addSingleton(IStatusProvider, wrapType(StatusProvider)); - serviceManager.addSingleton(IJupyterSessionManagerFactory, wrapType(JupyterSessionManagerFactory)); - serviceManager.addSingleton(IJupyterVariables, wrapType(JupyterVariables)); - serviceManager.add(ICodeWatcher, wrapType(CodeWatcher)); - serviceManager.add(IJupyterCommandFactory, wrapType(JupyterCommandFactory)); - serviceManager.addSingleton(IThemeFinder, wrapType(ThemeFinder)); - serviceManager.addSingleton(IDataViewerProvider, wrapType(DataViewerProvider)); - serviceManager.add(IDataViewer, wrapType(DataViewer)); - serviceManager.addSingleton(IExtensionSingleActivationService, wrapType(Decorator)); - serviceManager.add(IInteractiveWindowListener, wrapType(DotNetIntellisenseProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(JediIntellisenseProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(LinkProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(ShowPlotListener)); - serviceManager.add(IInteractiveWindowListener, wrapType(DebugListener)); - serviceManager.add(IInteractiveWindowListener, wrapType(GatherListener)); - serviceManager.add(IInteractiveWindowListener, wrapType(AutoSaveService)); - serviceManager.addSingleton(IPlotViewerProvider, wrapType(PlotViewerProvider)); - serviceManager.add(IPlotViewer, wrapType(PlotViewer)); - serviceManager.addSingleton(IJupyterDebugger, wrapType(JupyterDebugger)); - serviceManager.add(IDataScienceErrorHandler, wrapType(DataScienceErrorHandler)); - serviceManager.addSingleton(ICodeLensFactory, wrapType(CodeLensFactory)); - serviceManager.addSingleton(ICellHashProvider, wrapType(CellHashProvider)); - serviceManager.addSingleton(IGatherExecution, wrapType(GatherExecution)); + serviceManager.addSingleton(IDataScienceCodeLensProvider, DataScienceCodeLensProvider); + serviceManager.addSingleton(IDataScience, DataScience); + serviceManager.addSingleton(IJupyterExecution, JupyterExecutionFactory); + serviceManager.addSingleton(IDataScienceCommandListener, InteractiveWindowCommandListener); + serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); + serviceManager.add(IInteractiveWindow, InteractiveWindow); + serviceManager.add(INotebookExporter, JupyterExporter); + serviceManager.add(INotebookImporter, JupyterImporter); + serviceManager.add(INotebookServer, JupyterServerFactory); + serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); + serviceManager.addSingleton(IJupyterPasswordConnect, JupyterPasswordConnect); + serviceManager.addSingleton(IStatusProvider, StatusProvider); + serviceManager.addSingleton(IJupyterSessionManagerFactory, JupyterSessionManagerFactory); + serviceManager.addSingleton(IJupyterVariables, JupyterVariables); + serviceManager.add(ICodeWatcher, CodeWatcher); + serviceManager.add(IJupyterCommandFactory, JupyterCommandFactory); + serviceManager.addSingleton(IThemeFinder, ThemeFinder); + serviceManager.addSingleton(IDataViewerProvider, DataViewerProvider); + serviceManager.add(IDataViewer, DataViewer); + serviceManager.addSingleton(IExtensionSingleActivationService, Decorator); + serviceManager.add(IInteractiveWindowListener, DotNetIntellisenseProvider); + serviceManager.add(IInteractiveWindowListener, JediIntellisenseProvider); + serviceManager.add(IInteractiveWindowListener, LinkProvider); + serviceManager.add(IInteractiveWindowListener, ShowPlotListener); + serviceManager.add(IInteractiveWindowListener, DebugListener); + serviceManager.add(IInteractiveWindowListener, GatherListener); + serviceManager.add(IInteractiveWindowListener, AutoSaveService); + serviceManager.addSingleton(IPlotViewerProvider, PlotViewerProvider); + serviceManager.add(IPlotViewer, PlotViewer); + serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger); + serviceManager.add(IDataScienceErrorHandler, DataScienceErrorHandler); + serviceManager.addSingleton(ICodeLensFactory, CodeLensFactory); + serviceManager.addSingleton(ICellHashProvider, CellHashProvider); + serviceManager.add(IGatherExecution, GatherExecution); serviceManager.addBinding(ICellHashProvider, IInteractiveWindowListener); serviceManager.addBinding(ICellHashProvider, INotebookExecutionLogger); serviceManager.addBinding(IJupyterDebugger, ICellHashListener); - serviceManager.addSingleton(INotebookEditorProvider, wrapType(NativeEditorProvider)); - serviceManager.add(INotebookEditor, wrapType(NativeEditor)); - serviceManager.addSingleton(IDataScienceCommandListener, wrapType(NativeEditorCommandListener)); - serviceManager.addBinding(IGatherExecution, INotebookExecutionLogger); + serviceManager.addSingleton(INotebookEditorProvider, NativeEditorProvider); + serviceManager.add(INotebookEditor, NativeEditor); + serviceManager.addSingleton(IDataScienceCommandListener, NativeEditorCommandListener); serviceManager.addBinding(ICodeLensFactory, IInteractiveWindowListener); - serviceManager.addSingleton(IDebugLocationTracker, wrapType(DebugLocationTrackerFactory)); + serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory); } diff --git a/src/client/datascience/statusProvider.ts b/src/client/datascience/statusProvider.ts index eb011870de97..5aa1c2718a1a 100644 --- a/src/client/datascience/statusProvider.ts +++ b/src/client/datascience/statusProvider.ts @@ -55,9 +55,9 @@ export class StatusProvider implements IStatusProvider { constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell) { } - public set(message: string, timeout?: number, cancel?: () => void, panel?: IInteractiveBase): Disposable { + public set(message: string, showInWebView: boolean, timeout?: number, cancel?: () => void, panel?: IInteractiveBase): Disposable { // Start our progress - this.incrementCount(panel); + this.incrementCount(showInWebView, panel); // Create a StatusItem that will return our promise const statusItem = new StatusItem(message, () => this.decrementCount(panel), timeout); @@ -82,9 +82,9 @@ export class StatusProvider implements IStatusProvider { return statusItem; } - public async waitWithStatus(promise: () => Promise, message: string, timeout?: number, cancel?: () => void, panel?: IInteractiveBase): Promise { + public async waitWithStatus(promise: () => Promise, message: string, showInWebView: boolean, timeout?: number, cancel?: () => void, panel?: IInteractiveBase): Promise { // Create a status item and wait for our promise to either finish or reject - const status = this.set(message, timeout, cancel, panel); + const status = this.set(message, showInWebView, timeout, cancel, panel); let result: T; try { result = await promise(); @@ -94,9 +94,9 @@ export class StatusProvider implements IStatusProvider { return result; } - private incrementCount = (panel?: IInteractiveBase) => { + private incrementCount = (showInWebView: boolean, panel?: IInteractiveBase) => { if (this.statusCount === 0) { - if (panel) { + if (panel && showInWebView) { panel.startProgress(); } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 187452885686..5741d71f7a13 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -98,6 +98,7 @@ export interface INotebook extends IAsyncDisposable { setLaunchingFile(file: string): Promise; getSysInfo(): Promise; setMatplotLibStyle(useDark: boolean): Promise; + addLogger(logger: INotebookExecutionLogger): void; } export interface INotebookServerOptions { @@ -106,6 +107,7 @@ export interface INotebookServerOptions { usingDarkTheme?: boolean; useDefaultConfig?: boolean; workingDir?: string; + interpreterPath?: string; purpose: string; } @@ -118,7 +120,9 @@ export interface INotebookExecutionLogger { export const IGatherExecution = Symbol('IGatherExecution'); export interface IGatherExecution { enabled: boolean; + logExecution(vscCell: ICell): void; gatherCode(vscCell: ICell): string; + resetLog(): void; } export const IJupyterExecution = Symbol('IJupyterExecution'); @@ -186,7 +190,7 @@ export interface IJupyterKernelSpec extends IAsyncDisposable { export const INotebookImporter = Symbol('INotebookImporter'); export interface INotebookImporter extends Disposable { - importFromFile(file: string): Promise; + importFromFile(contentsFile: string, originalFile?: string): Promise; // originalFile is the base file if file is a temp file / location importCellsFromFile(file: string): Promise; importCells(json: string): Promise; } @@ -241,7 +245,7 @@ export interface INotebookEditorProvider { readonly editors: INotebookEditor[]; open(file: Uri, contents: string): Promise; show(file: Uri): Promise; - createNew(): Promise; + createNew(contents?: string): Promise; getNotebookOptions(): Promise; } @@ -383,10 +387,10 @@ export const IStatusProvider = Symbol('IStatusProvider'); export interface IStatusProvider { // call this function to set the new status on the active // interactive window. Dispose of the returned object when done. - set(message: string, timeout?: number, canceled?: () => void, interactivePanel?: IInteractiveBase): Disposable; + set(message: string, showInWebView: boolean, timeout?: number, canceled?: () => void, interactivePanel?: IInteractiveBase): Disposable; // call this function to wait for a promise while displaying status - waitWithStatus(promise: () => Promise, message: string, timeout?: number, canceled?: () => void, interactivePanel?: IInteractiveBase): Promise; + waitWithStatus(promise: () => Promise, message: string, showInWebView: boolean, timeout?: number, canceled?: () => void, interactivePanel?: IInteractiveBase): Promise; } export interface IJupyterCommand { diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts index 2416d3bb2892..60dc77b7f796 100644 --- a/src/client/debugger/extension/adapter/activator.ts +++ b/src/client/debugger/extension/adapter/activator.ts @@ -9,19 +9,21 @@ import { IDebugService } from '../../../common/application/types'; import { DebugAdapterDescriptorFactory } from '../../../common/experimentGroups'; import { IDisposableRegistry, IExperimentsManager } from '../../../common/types'; import { DebuggerTypeName } from '../../constants'; -import { IDebugAdapterDescriptorFactory } from '../types'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory } from '../types'; @injectable() export class DebugAdapterActivator implements IExtensionSingleActivationService { constructor( @inject(IDebugService) private readonly debugService: IDebugService, - @inject(IDebugAdapterDescriptorFactory) private factory: IDebugAdapterDescriptorFactory, + @inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory, + @inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory, @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager - ) {} + ) { } public async activate(): Promise { if (this.experimentsManager.inExperiment(DebugAdapterDescriptorFactory.experiment)) { - this.disposables.push(this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.factory)); + this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory)); + this.disposables.push(this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory)); } else { this.experimentsManager.sendTelemetryIfInExperiment(DebugAdapterDescriptorFactory.control); } diff --git a/src/client/debugger/extension/adapter/logging.ts b/src/client/debugger/extension/adapter/logging.ts new file mode 100644 index 000000000000..9c21831f71cf --- /dev/null +++ b/src/client/debugger/extension/adapter/logging.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as fs from 'fs'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugConfiguration, DebugSession, ProviderResult } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IFileSystem } from '../../../common/platform/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; + +class DebugSessionLoggingTracker implements DebugAdapterTracker { + private readonly enabled: boolean = false; + private stream: fs.WriteStream | undefined; + private timer = new StopWatch(); + + constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { + this.enabled = this.session.configuration.logToFile as boolean; + if (this.enabled) { + const fileName = `debugger.vscode_${this.session.id}.log`; + this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); + } + } + + public onWillStartSession() { + this.timer.reset(); + this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); + } + + public onWillReceiveMessage(message: DebugProtocol.Message) { + this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); + } + + public onDidSendMessage(message: DebugProtocol.Message) { + this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); + } + + public onWillStopSession() { + this.log('Stopping Session\n'); + } + + public onError(error: Error) { + this.log(`Error:\n${this.stringify(error)}\n`); + } + + public onExit(code: number | undefined, signal: string | undefined) { + this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); + } + + private log(message: string) { + if (this.enabled) { + this.stream!.write(`${this.timer.elapsedTime} ${message}`); + } + } + + private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { + return JSON.stringify(data, null, 4); + } +} + +@injectable() +export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { + constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) { } + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + return new DebugSessionLoggingTracker(session, this.fileSystem); + } +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index b3facb549d05..418e4bb405a6 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -8,6 +8,7 @@ import { IServiceManager } from '../../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../types'; import { DebugAdapterActivator } from './adapter/activator'; import { DebugAdapterDescriptorFactory } from './adapter/factory'; +import { DebugSessionLoggingFactory } from './adapter/logging'; import { DebuggerBanner } from './banner'; import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; @@ -26,7 +27,7 @@ import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from './hooks/childProcessAttachService'; import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; -import { DebugConfigurationType, IDebugAdapterDescriptorFactory, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types'; +import { DebugConfigurationType, IDebugAdapterDescriptorFactory, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner, IDebugSessionLoggingFactory } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IExtensionSingleActivationService, LaunchJsonCompletionProvider); @@ -47,4 +48,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper); serviceManager.addSingleton(IExtensionSingleActivationService, DebugAdapterActivator); serviceManager.addSingleton(IDebugAdapterDescriptorFactory, DebugAdapterDescriptorFactory); + serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 1accd67965a6..f2f239d70ad1 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,7 +3,7 @@ 'use strict'; -import { CancellationToken, DebugAdapterDescriptorFactory, DebugConfigurationProvider, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider, WorkspaceFolder } from 'vscode'; import { InputStep, MultiStepInput } from '../../common/utils/multiStepInput'; import { RemoteDebugOptions } from '../debugAdapter/types'; import { DebugConfigurationArguments } from '../types'; @@ -44,3 +44,7 @@ export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFa } export type DebugAdapterPtvsdPathInfo = { extensionVersion: string; ptvsdPath: string }; + +export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); + +export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory { } diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 8afaffd63554..a1f964f2d31f 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -108,7 +108,7 @@ export class InterpreterSelector implements IInterpreterSelector { } // Ok we have multiple workspaces, get the user to pick a folder. - const workspaceFolder = await this.applicationShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + const workspaceFolder = await this.applicationShell.showWorkspaceFolderPick({ placeHolder: 'Select the workspace to set the interpreter' }); return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 702740ff9a1c..2a8409154759 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -13,7 +13,7 @@ import { AppinsightsKey, EXTENSION_ROOT_DIR, isTestExecution, PVSC_EXTENSION_ID import { traceInfo } from '../common/logger'; import { TerminalShellType } from '../common/terminal/types'; import { StopWatch } from '../common/utils/stopWatch'; -import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry, Telemetry } from '../datascience/constants'; +import { JupyterCommands, NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry, Telemetry } from '../datascience/constants'; import { DebugConfigurationType } from '../debugger/extension/types'; import { ConsoleType, TriggerType } from '../debugger/types'; import { AutoSelectionRule } from '../interpreter/autoSelection/types'; @@ -1362,6 +1362,7 @@ export interface IEventNamePropertyMapping { [Telemetry.CodeLensAverageAcquisitionTime]: never | undefined; [Telemetry.CollapseAll]: never | undefined; [Telemetry.ConnectFailedJupyter]: never | undefined; + [Telemetry.NotebookExecutionActivated]: never | undefined; [Telemetry.ConnectLocalJupyter]: never | undefined; [Telemetry.ConnectRemoteJupyter]: never | undefined; [Telemetry.ConnectRemoteFailedJupyter]: never | undefined; @@ -1425,13 +1426,17 @@ export interface IEventNamePropertyMapping { [Telemetry.RunFileInteractive]: never | undefined; [Telemetry.RunFromLine]: never | undefined; [Telemetry.ScrolledToCell]: never | undefined; - [Telemetry.CellCount]: { count: number} ; + [Telemetry.CellCount]: { count: number }; [Telemetry.Save]: never | undefined; - [Telemetry.AutoSaveEnabled]: {enabled: boolean}; + [Telemetry.AutoSaveEnabled]: { enabled: boolean }; [Telemetry.SelfCertsMessageClose]: never | undefined; [Telemetry.SelfCertsMessageEnabled]: never | undefined; [Telemetry.SelectJupyterURI]: never | undefined; [Telemetry.SessionIdleTimeout]: never | undefined; + [Telemetry.JupyterNotInstalledErrorShown]: never | undefined; + [Telemetry.JupyterCommandSearch]: { where: 'activeInterpreter' | 'otherInterpreter' | 'path' | 'nowhere'; command: JupyterCommands }; + [Telemetry.UserInstalledJupyter]: never | undefined; + [Telemetry.UserDidNotInstallJupyter]: never | undefined; [Telemetry.SetJupyterURIToLocal]: never | undefined; [Telemetry.SetJupyterURIToUserSpecified]: never | undefined; [Telemetry.ShiftEnterBannerShown]: never | undefined; diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 490673ed40f3..ee707df767a3 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -8,8 +8,7 @@ import { IApplicationShell, IDocumentManager } from '../../common/application/ty import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; import '../../common/extensions'; import { traceError } from '../../common/logger'; -import { IProcessServiceFactory } from '../../common/process/types'; -import { IConfigurationService } from '../../common/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; import { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; @@ -17,13 +16,11 @@ import { ICodeExecutionHelper } from '../types'; export class CodeExecutionHelper implements ICodeExecutionHelper { private readonly documentManager: IDocumentManager; private readonly applicationShell: IApplicationShell; - private readonly processServiceFactory: IProcessServiceFactory; - private readonly configurationService: IConfigurationService; + private readonly pythonServiceFactory: IPythonExecutionFactory; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.documentManager = serviceContainer.get(IDocumentManager); this.applicationShell = serviceContainer.get(IApplicationShell); - this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); - this.configurationService = serviceContainer.get(IConfigurationService); + this.pythonServiceFactory = serviceContainer.get(IPythonExecutionFactory); } public async normalizeLines(code: string, resource?: Uri): Promise { try { @@ -33,10 +30,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // On windows cr is not handled well by python when passing in/out via stdin/stdout. // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); - const pythonPath = this.configurationService.getSettings(resource).pythonPath; const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'normalizeForInterpreter.py'), code]; - const processService = await this.processServiceFactory.create(resource); - const proc = await processService.exec(pythonPath, args, { throwOnStdErr: true }); + const processService = await this.pythonServiceFactory.create({ resource }); + const proc = await processService.exec(args, { throwOnStdErr: true }); return proc.stdout; } catch (ex) { diff --git a/src/client/testing/navigation/symbolProvider.ts b/src/client/testing/navigation/symbolProvider.ts index f854cc75548c..66fdf00e0ef0 100644 --- a/src/client/testing/navigation/symbolProvider.ts +++ b/src/client/testing/navigation/symbolProvider.ts @@ -7,8 +7,7 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, DocumentSymbolProvider, Location, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode'; import { traceError } from '../../common/logger'; -import { IProcessServiceFactory } from '../../common/process/types'; -import { IConfigurationService } from '../../common/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; import { EXTENSION_ROOT_DIR } from '../../constants'; type RawSymbol = { namespace: string; name: string; range: Range }; @@ -20,10 +19,7 @@ type Symbols = { @injectable() export class TestFileSymbolProvider implements DocumentSymbolProvider { - constructor( - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory - ) {} + constructor(@inject(IPythonExecutionFactory) private readonly pythonServiceFactory: IPythonExecutionFactory) {} public async provideDocumentSymbols(document: TextDocument, token: CancellationToken): Promise { const rawSymbols = await this.getSymbols(document, token); if (!rawSymbols) { @@ -53,10 +49,9 @@ export class TestFileSymbolProvider implements DocumentSymbolProvider { if (document.isDirty) { scriptArgs.push(document.getText()); } - const pythonPath = this.configurationService.getSettings(document.uri).pythonPath; const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), ...scriptArgs]; - const processService = await this.processServiceFactory.create(document.uri); - const proc = await processService.exec(pythonPath, args, { throwOnStdErr: true, token }); + const pythonService = await this.pythonServiceFactory.create({ resource: document.uri }); + const proc = await pythonService.exec(args, { throwOnStdErr: true, token }); return JSON.parse(proc.stdout); } catch (ex) { diff --git a/src/datascience-ui/history-react/index.html b/src/datascience-ui/history-react/index.html index 9e9b10e5a0f5..e157945c4a3a 100644 --- a/src/datascience-ui/history-react/index.html +++ b/src/datascience-ui/history-react/index.html @@ -254,6 +254,7 @@ --vscode-titleBar-activeForeground: #333333; --vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); --vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); +--code-comment-color: green; --vscode-widget-shadow: #a8a8a8; } body { diff --git a/src/datascience-ui/history-react/interactiveCell.tsx b/src/datascience-ui/history-react/interactiveCell.tsx index a437b975db2d..dcc6f68b9d9e 100644 --- a/src/datascience-ui/history-react/interactiveCell.tsx +++ b/src/datascience-ui/history-react/interactiveCell.tsx @@ -16,7 +16,7 @@ import { CollapseButton } from '../interactive-common/collapseButton'; import { ExecutionCount } from '../interactive-common/executionCount'; import { InformationMessages } from '../interactive-common/informationMessages'; import { InputHistory } from '../interactive-common/inputHistory'; -import { ICellViewModel, IFont } from '../interactive-common/mainState'; +import { CursorPos, ICellViewModel, IFont } from '../interactive-common/mainState'; import { IKeyboardEvent } from '../react-common/event'; import { getLocString } from '../react-common/locReactSide'; import { getSettings } from '../react-common/settingsReactSide'; @@ -100,7 +100,7 @@ export class InteractiveCell extends React.Component { // This depends upon what type of cell we are. if (this.props.cellVM.cell.data.cell_type === 'code') { if (this.codeRef.current) { - this.codeRef.current.giveFocus(); + this.codeRef.current.giveFocus(CursorPos.Current); } } } diff --git a/src/datascience-ui/history-react/interactivePanel.tsx b/src/datascience-ui/history-react/interactivePanel.tsx index b64187253155..575180eaa77c 100644 --- a/src/datascience-ui/history-react/interactivePanel.tsx +++ b/src/datascience-ui/history-react/interactivePanel.tsx @@ -363,11 +363,12 @@ export class InteractivePanel extends React.Component this.stateController.gatherCell(cell); const hasNoSource = !cell || !cell.cell.file || cell.cell.file === Identifiers.EmptyFileName; + const gatherHidden = !cell || !this.state.enableGather || cell.cell.data.cell_type !== 'code'; return ( [
-
{this.renderAddDivider(true)}
@@ -197,7 +197,7 @@ export class NativeCell extends React.Component { private onMouseDoubleClick = (ev: React.MouseEvent) => { // When we receive double click, propagate upwards. Might change our state ev.stopPropagation(); - this.props.focusCell(this.cellId, true); + this.props.focusCell(this.cellId, true, CursorPos.Current); } private shouldRenderCodeEditor = () : boolean => { @@ -251,7 +251,7 @@ export class NativeCell extends React.Component { } break; case 's': - if (e.ctrlKey) { + if ((e.ctrlKey && getOSType() !== OSType.OSX) || (e.metaKey && getOSType() === OSType.OSX)) { // This is save, save our cells this.props.stateController.save(); } @@ -377,7 +377,7 @@ export class NativeCell extends React.Component { if (this.wrapperRef && this.wrapperRef.current && this.isFocused()) { e.stopPropagation(); this.isCodeCell() ? this.onCodeUnfocused() : this.onMarkdownUnfocused(); - this.props.focusCell(this.cellId, false); + this.props.focusCell(this.cellId, false, CursorPos.Current); this.props.stateController.sendCommand(NativeCommandType.Unfocus, 'keyboard'); } } @@ -386,7 +386,7 @@ export class NativeCell extends React.Component { const prevCellId = this.getPrevCellId(); if (prevCellId) { e.stopPropagation(); - this.moveSelection(prevCellId, this.isFocused()); + this.moveSelection(prevCellId, this.isFocused(), CursorPos.Bottom); } this.props.stateController.sendCommand(NativeCommandType.ArrowUp, 'keyboard'); @@ -397,7 +397,7 @@ export class NativeCell extends React.Component { if (nextCellId) { e.stopPropagation(); - this.moveSelection(nextCellId, this.isFocused()); + this.moveSelection(nextCellId, this.isFocused(), CursorPos.Top); } this.props.stateController.sendCommand(NativeCommandType.ArrowDown, 'keyboard'); @@ -408,7 +408,7 @@ export class NativeCell extends React.Component { if (!this.isFocused() && !e.editorInfo && this.wrapperRef && this.wrapperRef && this.isSelected()) { e.stopPropagation(); e.preventDefault(); - this.props.focusCell(this.cellId, true); + this.props.focusCell(this.cellId, true, CursorPos.Current); } } @@ -475,8 +475,8 @@ export class NativeCell extends React.Component { this.props.stateController.sendCommand(NativeCommandType.Run, 'keyboard'); } - private moveSelection(cellId: string, focusCode: boolean) { - this.props.selectCell(cellId, focusCode); + private moveSelection(cellId: string, focusCode: boolean, cursorPos: CursorPos = CursorPos.Current) { + this.props.selectCell(cellId, focusCode, cursorPos); } private submitCell = (possibleContents?: string) => { @@ -501,7 +501,7 @@ export class NativeCell extends React.Component { this.props.stateController.sendCommand(NativeCommandType.AddToEnd, 'mouse'); if (newCell) { // Make async because the click changes focus. - setTimeout(() => this.props.focusCell(newCell, true), 0); + setTimeout(() => this.props.focusCell(newCell, true, CursorPos.Top), 0); } } @@ -558,6 +558,9 @@ export class NativeCell extends React.Component { private renderMiddleToolbar = () => { const cellId = this.props.cellVM.cell.id; + const gatherCell = () => { + this.props.stateController.gatherCell(this.props.cellVM); + }; const deleteCell = () => { this.props.stateController.possiblyDeleteCell(cellId); this.props.stateController.sendCommand(NativeCommandType.DeleteCell, 'mouse'); @@ -570,6 +573,11 @@ export class NativeCell extends React.Component { this.props.stateController.runBelow(cellId); this.props.stateController.sendCommand(NativeCommandType.RunBelow, 'mouse'); }; + const gatherDisabled = this.props.cellVM.cell.data.execution_count === null || + this.props.cellVM.hasBeenRun === null || + this.props.cellVM.hasBeenRun === false || + this.isMarkdownCell() || + getSettings().enableGather === false; const canRunAbove = this.props.stateController.canRunAbove(cellId); const canRunBelow = this.props.cellVM.cell.state === CellState.finished || this.props.cellVM.cell.state === CellState.error; const switchTooltip = this.props.cellVM.cell.data.cell_type === 'code' ? getLocString('DataScience.switchToMarkdown', 'Change to markdown') : @@ -596,6 +604,9 @@ export class NativeCell extends React.Component { +
); } @@ -628,26 +639,29 @@ export class NativeCell extends React.Component { private renderInput = () => { if (this.shouldRenderInput()) { return ( - +
+ + {this.renderMiddleToolbar()} +
); } return null; diff --git a/src/datascience-ui/native-editor/nativeEditor.less b/src/datascience-ui/native-editor/nativeEditor.less index 3c5c3e128ff9..af4cd3899bd3 100644 --- a/src/datascience-ui/native-editor/nativeEditor.less +++ b/src/datascience-ui/native-editor/nativeEditor.less @@ -25,24 +25,17 @@ } .cell-wrapper { - margin: 1px 2px; - border-color: transparent; - border-style: solid; - border-width: 2px 2px 0px 20px; + margin: 2px 2px 0px 0px; position: relative; min-height: 55px; } .cell-wrapper-focused { - border-color: transparent; - border-style: solid; - border-width: 2px 2px 0px 20px; + margin: 2px 2px 0px 0px; } .cell-wrapper-selected { - border-color: transparent; - border-style: solid; - border-width: 2px 2px 0px 20px; + margin: 2px 2px 0px 0px; } .cell-menu-bar-outer { @@ -140,8 +133,8 @@ .controls-div { min-width: 34px; - max-width: 34px; - padding-left: 10px; + padding-left: 4px; + padding-right: 4px; display: block; grid-column: 2; grid-row: 1; diff --git a/src/datascience-ui/native-editor/nativeEditor.tsx b/src/datascience-ui/native-editor/nativeEditor.tsx index c5b502f25d3a..d11c5917d52c 100644 --- a/src/datascience-ui/native-editor/nativeEditor.tsx +++ b/src/datascience-ui/native-editor/nativeEditor.tsx @@ -6,10 +6,12 @@ import './nativeEditor.less'; import * as React from 'react'; import { noop } from '../../client/common/utils/misc'; +import { OSType } from '../../client/common/utils/platform'; import { NativeCommandType } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel'; -import { ICellViewModel, IMainState } from '../interactive-common/mainState'; +import { CursorPos, ICellViewModel, IMainState } from '../interactive-common/mainState'; import { IVariablePanelProps, VariablePanel } from '../interactive-common/variablePanel'; +import { getOSType } from '../react-common/constants'; import { ErrorBoundary } from '../react-common/errorBoundary'; import { Image, ImageName } from '../react-common/image'; import { ImageButton } from '../react-common/imageButton'; @@ -57,7 +59,7 @@ export class NativeEditor extends React.Component this.focusCell(newCell, true), 0); + setTimeout(() => this.focusCell(newCell, true, CursorPos.Top), 0); } }; const addCellLine = this.state.cellVMs.length === 0 ? null : @@ -145,15 +147,15 @@ export class NativeEditor extends React.Component { + private moveSelectionToExisting = (cellId: string, focusCode: boolean, cursorPos: CursorPos) => { // Cell should already exist in the UI if (this.contentPanelRef) { this.stateController.selectCell(cellId, focusCode ? cellId : undefined); - this.focusCell(cellId, focusCode ? true : false); + this.focusCell(cellId, focusCode ? true : false, cursorPos); } } - private selectCell = (id: string, focusCode: boolean) => { + private selectCell = (id: string, focusCode: boolean, cursorPos: CursorPos) => { // Check to see that this cell already exists in our window (it's part of the rendered state) const cells = this.state.cellVMs.map(c => c.cell).filter(c => c.data.cell_type !== 'messages'); if (cells.find(c => c.id === id)) { @@ -164,7 +166,7 @@ export class NativeEditor extends React.Component this.moveSelectionToExisting(id, focusCode), 1); + setTimeout(() => this.moveSelectionToExisting(id, focusCode, cursorPos), 1); } // tslint:disable: react-this-binding-issue @@ -174,7 +176,7 @@ export class NativeEditor extends React.Component this.focusCell(newCell.cell.id, true), 0); + setTimeout(() => this.focusCell(newCell.cell.id, true, CursorPos.Top), 0); } }; const runAll = () => { @@ -321,14 +323,14 @@ export class NativeEditor extends React.Component this.focusCell(newCell, true), 0); + setTimeout(() => this.focusCell(newCell, true, CursorPos.Top), 0); } }; const lastLine = index === this.state.cellVMs.length - 1 ? @@ -416,11 +418,11 @@ export class NativeEditor extends React.Component); } - private focusCell = (cellId: string, focusCode: boolean): void => { + private focusCell = (cellId: string, focusCode: boolean, cursorPos: CursorPos): void => { this.stateController.selectCell(cellId, focusCode ? cellId : undefined); const ref = this.cellRefs.get(cellId); if (ref && ref.current) { - ref.current.giveFocus(focusCode); + ref.current.giveFocus(focusCode, cursorPos); } } @@ -430,7 +432,7 @@ export class NativeEditor extends React.Component { - this.focusCell(newCell, true); + this.focusCell(newCell, true, CursorPos.Current); }, 0); } } diff --git a/src/datascience-ui/native-editor/nativeEditorStateController.ts b/src/datascience-ui/native-editor/nativeEditorStateController.ts index 012a9c916eee..a588637fd79e 100644 --- a/src/datascience-ui/native-editor/nativeEditorStateController.ts +++ b/src/datascience-ui/native-editor/nativeEditorStateController.ts @@ -306,4 +306,17 @@ export class NativeEditorStateController extends MainStateController { } } } + + /** + * Custom editor settings for Native editor. + * + * @protected + * @returns + * @memberof NativeEditorStateController + */ + protected computeEditorOptions() { + const options = super.computeEditorOptions(); + options.lineDecorationsWidth = 5; + return options; + } } diff --git a/src/datascience-ui/react-common/constants.ts b/src/datascience-ui/react-common/constants.ts index 8c70577dda8c..e621dbee8ed8 100644 --- a/src/datascience-ui/react-common/constants.ts +++ b/src/datascience-ui/react-common/constants.ts @@ -1,3 +1,5 @@ +import { OSType } from '../../client/common/utils/platform'; + // Javascript keyCodes export const KeyCodes = { LeftArrow: 37, @@ -9,3 +11,15 @@ export const KeyCodes = { End: 35, Home: 36 }; + +export function getOSType() { + if (window.navigator.platform.startsWith('Mac')){ + return OSType.OSX; + } else if (window.navigator.platform.startsWith('Win')){ + return OSType.Windows; + } else if (window.navigator.userAgent.indexOf('Linux') > 0){ + return OSType.Linux; + } else { + return OSType.Unknown; + } +} diff --git a/src/datascience-ui/react-common/monacoEditor.css b/src/datascience-ui/react-common/monacoEditor.css index 3803e4fd9a0d..4e41c4ab8579 100644 --- a/src/datascience-ui/react-common/monacoEditor.css +++ b/src/datascience-ui/react-common/monacoEditor.css @@ -107,22 +107,10 @@ padding-right: 4px; } -.monaco-editor .current-line ~ .line-numbers { - color: var(--override-foreground, var(--vscode-editor-foreground)) !important; -} -.monaco-editor .line-numbers { - color: var(--override-foreground, var(--vscode-editor-foreground)) !important; -} - .monaco-editor .margin { background-color: transparent !important; } -.monaco-editor .margin-view-overlays-border-on { - margin-right: 1px; - padding-right: 1px; - background-color: transparent !important; - border-right-color: black; - border-right-style: solid; - border-right-width: 1px; +.monaco-editor .parameter-hints-widget > .wrapper { + overflow: hidden; } diff --git a/src/datascience-ui/react-common/monacoEditor.tsx b/src/datascience-ui/react-common/monacoEditor.tsx index 3ee223fd140a..5e3440018556 100644 --- a/src/datascience-ui/react-common/monacoEditor.tsx +++ b/src/datascience-ui/react-common/monacoEditor.tsx @@ -40,7 +40,6 @@ export interface IMonacoEditorProps { measureWidthClassName?: string; editorMounted(editor: monacoEditor.editor.IStandaloneCodeEditor): void; openLink(uri: monacoEditor.Uri): void; - lineCountChanged(newCount: number): void; } export interface IMonacoEditorState { @@ -68,7 +67,10 @@ export class MonacoEditor extends React.Component { this.windowResized(); + // Also recompute our visible line heights + this.debouncedComputeLineTops(); })); // Setup our context menu to show up outside. Autocomplete doesn't have this problem so it just works @@ -190,6 +194,13 @@ export class MonacoEditor extends React.Component { + this.throttledUpdateWidgetPosition(); + this.throttledScrollOntoScreen(editor); })); // Update our margin to include the correct line number style @@ -301,6 +312,58 @@ export class MonacoEditor extends React.Component { + const match = l.style.top ? /(.+)px/.exec(l.style.top) : null; + return match ? parseInt(match[0], 10) : Infinity; + }); + return this.lineTops; + } + + private scrollOntoScreen(_editor: monacoEditor.editor.IStandaloneCodeEditor) { + // Scroll to the visible line that has our current line + const visibleLineDivs = this.getVisibleLines(); + const current = this.getCurrentVisibleLine(); + if (current !== undefined && current >= 0) { + window.console.log(`Scrolling to line ${current}`); + visibleLineDivs[current].scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); + } + } + private watchStyles = (mutations: MutationRecord[], _observer: MutationObserver): void => { try { if (mutations && mutations.length > 0 && this.styleObserver) { @@ -419,9 +482,6 @@ export class MonacoEditor extends React.Component { + let processService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let executionService: PythonExecutionService; + const pythonPath = 'path/to/python'; + + setup(() => { + processService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + const serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + + serviceContainer.setup(s => s.get(IFileSystem)).returns(() => fileSystem.object); + + executionService = new PythonExecutionService(serviceContainer.object, processService.object, pythonPath); + }); + + test('getInterpreterInformation should return an object if the python path is valid', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true + }; + + processService.setup(p => p.exec(pythonPath, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + + const result = await executionService.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x64, + path: pythonPath, + version: new SemVer('3.7.5-candidate'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal(expectedResult, 'Incorrect value returned by getInterpreterInformation().'); + }); + + test('getInterpreterInformation should return an object if the version info contains less than 4 items', async () => { + const json = { + versionInfo: [3, 7, 5], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true + }; + + processService.setup(p => p.exec(pythonPath, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + + const result = await executionService.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x64, + path: pythonPath, + version: new SemVer('3.7.5'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal(expectedResult, 'Incorrect value returned by getInterpreterInformation() with truncated versionInfo.'); + }); + + test('getInterpreterInformation should return an object with the architecture value set to x86 if json.is64bit is not 64bit', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: false + }; + + processService.setup(p => p.exec(pythonPath, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + + const result = await executionService.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x86, + path: pythonPath, + version: new SemVer('3.7.5-candidate'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal(expectedResult, 'Incorrect value returned by getInterpreterInformation() for x86b architecture.'); + }); + + test('getInterpreterInformation should error out if interpreterInfo.py times out', async () => { + // tslint:disable-next-line: no-any + processService.setup(p => p.exec(pythonPath, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined as any)); + + const result = await executionService.getInterpreterInformation(); + + expect(result).to.equal(undefined, 'getInterpreterInfo() should return undefined because interpreterInfo timed out.'); + }); + + test('getInterpreterInformation should return undefined if the json value returned by interpreterInfo.py is not valid', async () => { + processService.setup(p => p.exec(pythonPath, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'bad json' })); + + const result = await executionService.getInterpreterInformation(); + + expect(result).to.equal(undefined, 'getInterpreterInfo() should return undefined because of bad json.'); + }); + + test('getExecutablePath should return pythonPath if pythonPath is a file', async () => { + fileSystem.setup(f => f.fileExists(pythonPath)).returns(() => Promise.resolve(true)); + + const result = await executionService.getExecutablePath(); + + expect(result).to.equal(pythonPath, 'getExecutablePath() sbould return pythonPath if it\'s a file'); + }); + + test('getExecutablePath should not return pythonPath if pythonPath is not a file', async () => { + const executablePath = 'path/to/dummy/executable'; + fileSystem.setup(f => f.fileExists(pythonPath)).returns(() => Promise.resolve(false)); + processService + .setup(p => p.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: executablePath })); + + const result = await executionService.getExecutablePath(); + + expect(result).to.equal(executablePath, 'getExecutablePath() sbould not return pythonPath if it\'s not a file'); + }); + + test('getExecutablePath should throw if the result of exec() writes to stderr', async () => { + const stderr = 'bar'; + fileSystem.setup(f => f.fileExists(pythonPath)).returns(() => Promise.resolve(false)); + processService.setup(p => p.exec(pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true })).returns(() => Promise.reject(new StdErrError(stderr))); + + const result = executionService.getExecutablePath(); + + expect(result).to.eventually.be.rejectedWith(stderr); + }); + + test('isModuleInstalled should call processService.exec()', async () => { + const moduleName = 'foo'; + processService.setup(p => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })).returns(() => Promise.resolve({ stdout: '' })); + + await executionService.isModuleInstalled(moduleName); + + processService.verify(async p => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true }), TypeMoq.Times.once()); + }); + + test('isModuleInstalled should return true when processService.exec() succeeds', async () => { + const moduleName = 'foo'; + processService.setup(p => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })).returns(() => Promise.resolve({ stdout: '' })); + + const result = await executionService.isModuleInstalled(moduleName); + + expect(result).to.equal(true, 'isModuleInstalled() should return true if the module exists'); + }); + + test('isModuleInstalled should return false when processService.exec() throws', async () => { + const moduleName = 'foo'; + processService.setup(p => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })).returns(() => Promise.reject(new StdErrError('bar'))); + + const result = await executionService.isModuleInstalled(moduleName); + + expect(result).to.equal(false, 'isModuleInstalled() should return false if the module does not exist'); + }); + + test('execObservable should call processService.execObservable', () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const observable = { + proc: undefined, + // tslint:disable-next-line: no-any + out: {} as any, + dispose: () => { + noop(); + } + }; + processService.setup(p => p.execObservable(pythonPath, args, options)).returns(() => observable); + + const result = executionService.execObservable(args, options); + + processService.verify(p => p.execObservable(pythonPath, args, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execObservable should return an observable'); + }); + + test('execModuleObservable should call processService.execObservable with the -m argument', () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-m', moduleName, ...args]; + const options = {}; + const observable = { + proc: undefined, + // tslint:disable-next-line: no-any + out: {} as any, + dispose: () => { + noop(); + } + }; + processService.setup(p => p.execObservable(pythonPath, expectedArgs, options)).returns(() => observable); + + const result = executionService.execModuleObservable(moduleName, args, options); + + processService.verify(p => p.execObservable(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execModuleObservable should return an observable'); + }); + + test('exec should call processService.exec', async () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const stdout = 'foo'; + processService.setup(p => p.exec(pythonPath, args, options)).returns(() => Promise.resolve({ stdout })); + + const result = await executionService.exec(args, options); + + processService.verify(p => p.exec(pythonPath, args, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should call processService.exec with the -m argument', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-m', moduleName, ...args]; + const options = {}; + const stdout = 'bar'; + processService.setup(p => p.exec(pythonPath, expectedArgs, options)).returns(() => Promise.resolve({ stdout })); + + const result = await executionService.execModule(moduleName, args, options); + + processService.verify(p => p.exec(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should throw an error if the module is not installed', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-m', moduleName, ...args]; + const options = {}; + processService.setup(p => p.exec(pythonPath, expectedArgs, options)).returns(() => Promise.resolve({ stdout: 'bar', stderr: `Error: No module named ${moduleName}` })); + processService.setup(p => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })).returns(() => Promise.reject(new StdErrError('not installed'))); + + const result = executionService.execModule(moduleName, args, options); + + expect(result).to.eventually.be.rejectedWith(`Module '${moduleName}' not installed`); + }); +}); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 111b27a4a10f..a36f39b99d3b 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -41,6 +41,7 @@ import { WorkspaceService } from '../../client/common/application/workspace'; import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { ExperimentsManager } from '../../client/common/experiments'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { IInstallationChannelManager } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; @@ -83,6 +84,7 @@ import { IAsyncDisposableRegistry, IConfigurationService, ICurrentProcess, + IExperimentsManager, IExtensions, ILogger, IPathUtils, @@ -404,8 +406,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(IInteractiveWindowListener, GatherListener); this.serviceManager.addBinding(ICellHashProvider, INotebookExecutionLogger); this.serviceManager.addBinding(IJupyterDebugger, ICellHashListener); - this.serviceManager.addSingleton(IGatherExecution, GatherExecution); - this.serviceManager.addBinding(IGatherExecution, INotebookExecutionLogger); + this.serviceManager.add(IGatherExecution, GatherExecution); this.serviceManager.addSingleton(ICodeLensFactory, CodeLensFactory); this.serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); this.serviceManager.addSingleton(InterpeterHashProviderFactory, InterpeterHashProviderFactory); @@ -413,6 +414,12 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); this.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); + // Disable experiments. + const experimentManager = mock(ExperimentsManager); + when(experimentManager.inExperiment(anything())).thenReturn(false); + when(experimentManager.activate()).thenResolve(); + this.serviceManager.addSingletonInstance(IExperimentsManager, instance(experimentManager)); + // Setup our command list this.commandManager.registerCommand('setContext', (name: string, value: boolean) => { this.setContexts[name] = value; @@ -592,9 +599,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); - if (this.mockJupyter) { - this.mockJupyter.addInterpreter(this.workingPython, SupportedCommands.all); - } + this.addInterpreter(this.workingPython, SupportedCommands.all); } // tslint:disable:any @@ -711,6 +716,12 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.pythonSettings.jediEnabled = enabled; } + public addInterpreter(newInterpreter: PythonInterpreter, commands: SupportedCommands) { + if (this.mockJupyter) { + this.mockJupyter.addInterpreter(newInterpreter, commands); + } + } + private findPythonPath(): string { try { const output = child_process.execFileSync('python', ['-c', 'import sys;print(sys.executable)'], { encoding: 'utf8' }); diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 09ef0f3b8499..bf9c26228fe5 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -179,7 +179,7 @@ suite('DataScience Code Watcher Unit Tests', () => { const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%%`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); @@ -200,7 +200,7 @@ suite('DataScience Code Watcher Unit Tests', () => { const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `dummy`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); @@ -228,7 +228,7 @@ third line #%% fourth line`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); @@ -265,7 +265,7 @@ fourth line pythonSettings.datascience.codeRegularExpression = '(#\\s*\\|#\\s*\\)'; pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\|#\\s*\\)'; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); @@ -303,7 +303,7 @@ fourth line pythonSettings.datascience.codeRegularExpression = '# * code cell)'; pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\|#\\s*\\)'; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); @@ -356,8 +356,10 @@ fourth line `#%% testing1 #%% -testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); +testing2`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + + document.setup(doc => doc.getText()).returns(() => inputText).verifiable(TypeMoq.Times.exactly(1)); codeWatcher.setDocument(document.object); @@ -384,28 +386,20 @@ testing2`; // Command tests override getText, so just need the ranges here `#%% testing1 #%% -testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - // Specify our range and text here - const testRange1 = new Range(0, 0, 1, 8); - const testString1 = 'testing1'; - document.setup(doc => doc.getText(testRange1)).returns(() => testString1).verifiable(TypeMoq.Times.once()); - const testRange2 = new Range(2, 0, 3, 8); - const testString2 = 'testing2'; - document.setup(doc => doc.getText(testRange2)).returns(() => testString2).verifiable(TypeMoq.Times.once()); +testing2`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting1'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isAny() )).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting2'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), TypeMoq.It.isAny(), @@ -427,13 +421,12 @@ testing2`; // Command tests override getText, so just need the ranges here testing1 #%% testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - document.setup(d => d.getText(new Range(2, 0, 3, 8))).returns(() => 'testing2').verifiable(TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting2'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), TypeMoq.It.is((ed: TextEditor) => { @@ -646,7 +639,7 @@ testing3`; testing1 #%% testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); helper.setup(h => h.getSelectedTextToExecute(TypeMoq.It.is((ed: TextEditor) => { @@ -683,16 +676,13 @@ testing2`; `#%% testing1 #%% -testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - const testRange = new Range(0, 0, 1, 8); - const testString = 'testing1'; - document.setup(d => d.getText(testRange)).returns(() => testString).verifiable(TypeMoq.Times.atLeastOnce()); +testing2`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting1'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.is((ed: TextEditor) => { @@ -736,13 +726,14 @@ testing2`; // Command tests override getText, so just need the ranges here const version = 1; const inputText = '#%% foobar'; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + document.setup(doc => doc.getText()).returns(() => inputText); documentManager.setup(d => d.textDocuments).returns(() => [document.object]); const codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, debugLocationTracker.object, documentManager.object, configService.object, commandManager.object, disposables, debugService.object, fileSystem.object); let result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); expect(result, 'result not okay').to.be.ok; let codeLens = result as CodeLens[]; - expect(codeLens.length).to.equal(3, 'Code lens wrong length'); + expect(codeLens.length).to.equal(3, 'Code lens wrong length - initial'); expect(contexts.get(EditorContexts.HasCodeCells)).to.be.equal(true, 'Code cells context not set'); @@ -760,7 +751,8 @@ testing2`; // Command tests override getText, so just need the ranges here result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); expect(result, 'result not okay').to.be.ok; codeLens = result as CodeLens[]; - expect(codeLens.length).to.equal(3, 'Code lens wrong length'); + expect(codeLens.length).to.equal(3, 'Code lens wrong length - final'); + }); test('Test the RunAllCellsAbove command with an error', async () => { @@ -815,27 +807,19 @@ testing2`; testing1 #%% testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - // Specify our range and text here - const testRange1 = new Range(0, 0, 1, 8); - const testString1 = 'testing1'; - document.setup(doc => doc.getText(testRange1)).returns(() => testString1).verifiable(TypeMoq.Times.once()); - const testRange2 = new Range(2, 0, 3, 8); - const testString2 = 'testing2'; - document.setup(doc => doc.getText(testRange2)).returns(() => testString2).verifiable(TypeMoq.Times.never()); + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); codeWatcher.setDocument(document.object); // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting1'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isAny() )).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), + activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('#%%\ntesting2'), TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), TypeMoq.It.isAny(), diff --git a/src/test/datascience/editor-integration/helpers.ts b/src/test/datascience/editor-integration/helpers.ts index bd363fb16ba2..e9b3fcebf546 100644 --- a/src/test/datascience/editor-integration/helpers.ts +++ b/src/test/datascience/editor-integration/helpers.ts @@ -22,7 +22,7 @@ export function createDocument(inputText: string, fileName: string, fileVersion: document.setup(d => d.version).returns(() => fileVersion).verifiable(times); // Next add the lines in - document.setup(d => d.lineCount).returns(() => inputLines.length).verifiable(times); + document.setup(d => d.lineCount).returns(() => inputLines.length); const textLines = inputLines.map((line, index) => { const textLine = TypeMoq.Mock.ofType(); diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index 65e7fe4497c7..89c199044bef 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -44,6 +44,7 @@ import { IJupyterKernelSpec, INotebook, INotebookCompletion, + INotebookExecutionLogger, INotebookServer, INotebookServerLaunchInfo, InterruptResult @@ -103,6 +104,10 @@ class MockJupyterNotebook implements INotebook { noop(); } + public addLogger(_logger: INotebookExecutionLogger): void { + noop(); + } + public getSysInfo(): Promise { return Promise.resolve(undefined); } @@ -455,7 +460,7 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list'], [], []); - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); } @@ -467,7 +472,7 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list'], [], []); - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); } function setupMissingNotebookPythonService(service: TypeMoq.IMock) { @@ -495,7 +500,7 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupProcessServiceExec(service, workingPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], [], []); - setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); } function setupMissingKernelProcessService(service: TypeMoq.IMock, notebookStdErr?: string[]) { @@ -503,7 +508,7 @@ suite('Jupyter Execution', async () => { const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); setupProcessServiceExec(service, missingKernelPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], [], []); - setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); } function setupPathProcessService(jupyterPath: string, service: TypeMoq.IMock, notebookStdErr?: string[]) { @@ -512,7 +517,7 @@ suite('Jupyter Execution', async () => { setupProcessServiceExec(service, jupyterPath, ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); setupProcessServiceExec(service, jupyterPath, ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); setupProcessServiceExec(service, jupyterPath, ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupProcessServiceExecObservable(service, jupyterPath, ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); + setupProcessServiceExecObservable(service, jupyterPath, ['notebook', '--no-browser', /--notebook-dir=.*/, /--config=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); // WE also check for existence with just the key jupyter setupProcessServiceExec(service, 'jupyter', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); @@ -573,7 +578,7 @@ suite('Jupyter Execution', async () => { when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({report: noop}, new CancellationTokenSource().token).then(resolve).catch(reject); + cb({ report: noop }, new CancellationTokenSource().token).then(resolve).catch(reject); }); }); @@ -751,7 +756,7 @@ suite('Jupyter Execution', async () => { reset(application); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({report: noop}, progressCancellation.token).then(resolve).catch(reject); + cb({ report: noop }, progressCancellation.token).then(resolve).catch(reject); }); }); @@ -770,7 +775,7 @@ suite('Jupyter Execution', async () => { reset(application); when(application.withProgress(anything(), anything())).thenCall((_, cb: (_: any, token: any) => Promise) => { return new Promise((resolve, reject) => { - cb({report: noop}, progressCancellation.token).then(resolve).catch(reject); + cb({ report: noop }, progressCancellation.token).then(resolve).catch(reject); }); }); diff --git a/src/test/datascience/gather/gather.unit.test.ts b/src/test/datascience/gather/gather.unit.test.ts index a2a395a95594..ff4e5eba5484 100644 --- a/src/test/datascience/gather/gather.unit.test.ts +++ b/src/test/datascience/gather/gather.unit.test.ts @@ -7,6 +7,7 @@ import * as TypeMoq from 'typemoq'; import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; import { IConfigurationService, IDataScienceSettings, IDisposableRegistry, IPythonSettings } from '../../../client/common/types'; import { GatherExecution } from '../../../client/datascience/gather/gather'; +import { GatherLogger } from '../../../client/datascience/gather/gatherLogger'; import { ICell as IVscCell } from '../../../client/datascience/types'; // tslint:disable-next-line: max-func-body-length @@ -125,13 +126,14 @@ suite('DataScience code gathering unit tests', () => { configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); const gatherExecution = new GatherExecution(configurationService.object, appShell.object, disposableRegistry.object, commandManager.object); + const gatherLogger = new GatherLogger(gatherExecution, configurationService.object); test('Logs a cell execution', async () => { let count = 0; for (const c of codeCells) { - await gatherExecution.postExecute(c, false); + await gatherLogger.postExecute(c, false); count += 1; - assert.equal(gatherExecution.executionSlicer._executionLog.length, count); + assert.equal(gatherExecution.executionSlicer.executionLog.length, count); } }); @@ -139,7 +141,7 @@ suite('DataScience code gathering unit tests', () => { const defaultCellMarker = '# %%'; const cell: IVscCell = codeCells[codeCells.length - 1]; const program = gatherExecution.gatherCode(cell); - const expectedProgram = `# This file contains the minimal amount of code required to produce the code cell you gathered.\n${defaultCellMarker}\nfrom bokeh.plotting import show, figure, output_notebook\n\n${defaultCellMarker}\nx = [1,2,3,4,5]\ny = [21,9,15,17,4]\n\n${defaultCellMarker}\np=figure(title='demo',x_axis_label='x',y_axis_label='y')\n\n${defaultCellMarker}\np.line(x,y,line_width=2)\n\n${defaultCellMarker}\nshow(p)\n`; + const expectedProgram = `# This file contains only the code required to produce the results of the gathered cell.\n${defaultCellMarker}\nfrom bokeh.plotting import show, figure, output_notebook\n\n${defaultCellMarker}\nx = [1,2,3,4,5]\ny = [21,9,15,17,4]\n\n${defaultCellMarker}\np=figure(title='demo',x_axis_label='x',y_axis_label='y')\n\n${defaultCellMarker}\np.line(x,y,line_width=2)\n\n${defaultCellMarker}\nshow(p)\n`; assert.equal(program.trim(), expectedProgram.trim()); }); }); diff --git a/src/test/datascience/interactive-ipynb/nativeEditor.unit.test.ts b/src/test/datascience/interactive-ipynb/nativeEditor.unit.test.ts index f95127b1ca6f..b875acbb65b3 100644 --- a/src/test/datascience/interactive-ipynb/nativeEditor.unit.test.ts +++ b/src/test/datascience/interactive-ipynb/nativeEditor.unit.test.ts @@ -37,7 +37,6 @@ import { JupyterExecutionFactory } from '../../../client/datascience/jupyter/jup import { JupyterExporter } from '../../../client/datascience/jupyter/jupyterExporter'; import { JupyterImporter } from '../../../client/datascience/jupyter/jupyterImporter'; import { JupyterVariables } from '../../../client/datascience/jupyter/jupyterVariables'; -import { StatusProvider } from '../../../client/datascience/statusProvider'; import { ThemeFinder } from '../../../client/datascience/themeFinder'; import { ICodeCssGenerator, @@ -49,7 +48,6 @@ import { INotebookEditorProvider, INotebookExporter, INotebookImporter, - IStatusProvider, IThemeFinder } from '../../../client/datascience/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; @@ -58,6 +56,7 @@ import { createEmptyCell } from '../../../datascience-ui/interactive-common/main import { waitForCondition } from '../../common'; import { noop } from '../../core'; import { MockMemento } from '../../mocks/mementos'; +import { MockStatusProvider } from '../mockStatusProvider'; // tslint:disable: no-any chai-vague-errors no-unused-expression @@ -66,7 +65,7 @@ suite('Data Science - Native Editor', () => { let workspace: IWorkspaceService; let configService: IConfigurationService; let fileSystem: IFileSystem; - let doctManager: IDocumentManager; + let docManager: IDocumentManager; let dsErrorHandler: IDataScienceErrorHandler; let cmdManager: ICommandManager; let liveShare: ILiveShareApi; @@ -76,7 +75,7 @@ suite('Data Science - Native Editor', () => { const disposables: Disposable[] = []; let cssGenerator: ICodeCssGenerator; let themeFinder: IThemeFinder; - let statusProvider: IStatusProvider; + let statusProvider: MockStatusProvider; let executionProvider: IJupyterExecution; let exportProvider: INotebookExporter; let editorProvider: INotebookEditorProvider; @@ -85,6 +84,7 @@ suite('Data Science - Native Editor', () => { let jupyterDebugger: IJupyterDebugger; let importer: INotebookImporter; let storage: MockMemento; + let localStorage: MockMemento; let storageUpdateSpy: sinon.SinonSpy<[string, any], Thenable>; const baseFile = `{ "cells": [ @@ -185,10 +185,11 @@ suite('Data Science - Native Editor', () => { setup(() => { storage = new MockMemento(); + localStorage = new MockMemento(); storageUpdateSpy = sinon.spy(storage, 'update'); configService = mock(ConfigurationService); fileSystem = mock(FileSystem); - doctManager = mock(DocumentManager); + docManager = mock(DocumentManager); dsErrorHandler = mock(DataScienceErrorHandler); cmdManager = mock(CommandManager); workspace = mock(WorkspaceService); @@ -198,7 +199,7 @@ suite('Data Science - Native Editor', () => { webPanelProvider = mock(WebPanelProvider); cssGenerator = mock(CodeCssGenerator); themeFinder = mock(ThemeFinder); - statusProvider = mock(StatusProvider); + statusProvider = new MockStatusProvider(); executionProvider = mock(JupyterExecutionFactory); exportProvider = mock(JupyterExporter); editorProvider = mock(NativeEditorProvider); @@ -218,10 +219,11 @@ suite('Data Science - Native Editor', () => { when(interpreterService.onDidChangeInterpreter).thenReturn(interprerterChangeEvent.event); const editorChangeEvent = new EventEmitter(); - when(doctManager.onDidChangeActiveTextEditor).thenReturn(editorChangeEvent.event); + when(docManager.onDidChangeActiveTextEditor).thenReturn(editorChangeEvent.event); const sessionChangedEvent = new EventEmitter(); when(executionProvider.sessionChanged).thenReturn(sessionChangedEvent.event); + }); teardown(() => { @@ -234,13 +236,13 @@ suite('Data Science - Native Editor', () => { [], instance(liveShare), instance(applicationShell), - instance(doctManager), + instance(docManager), instance(interpreterService), instance(webPanelProvider), disposables, instance(cssGenerator), instance(themeFinder), - instance(statusProvider), + statusProvider, instance(executionProvider), instance(fileSystem), instance(configService), @@ -253,13 +255,14 @@ suite('Data Science - Native Editor', () => { instance(jupyterDebugger), instance(importer), instance(dsErrorHandler), - storage + storage, + localStorage ); } test('Create new editor and add some cells', async () => { const editor = createEditor(); - await editor.load(baseFile, Uri.parse('file://foo.ipynb')); + await editor.load(baseFile, Uri.parse('file:///foo.ipynb')); expect(editor.contents).to.be.equal(baseFile); editor.onMessage(InteractiveWindowMessages.InsertCell, { index: 0, cell: createEmptyCell('1', 1) }); expect(editor.cells).to.be.lengthOf(4); @@ -269,7 +272,7 @@ suite('Data Science - Native Editor', () => { test('Move cells around', async () => { const editor = createEditor(); - await editor.load(baseFile, Uri.parse('file://foo.ipynb')); + await editor.load(baseFile, Uri.parse('file:///foo.ipynb')); expect(editor.contents).to.be.equal(baseFile); editor.onMessage(InteractiveWindowMessages.SwapCells, { firstCellId: 'NotebookImport#0', secondCellId: 'NotebookImport#1' }); expect(editor.cells).to.be.lengthOf(3); @@ -279,7 +282,7 @@ suite('Data Science - Native Editor', () => { test('Edit/delete cells', async () => { const editor = createEditor(); - await editor.load(baseFile, Uri.parse('file://foo.ipynb')); + await editor.load(baseFile, Uri.parse('file:///foo.ipynb')); expect(editor.contents).to.be.equal(baseFile); expect(editor.isDirty).to.be.equal(false, 'Editor should not be dirty'); editor.onMessage(InteractiveWindowMessages.EditCell, { @@ -327,7 +330,7 @@ suite('Data Science - Native Editor', () => { return editor; } test('Editing a notebook will save uncommitted changes into memento', async () => { - const file = Uri.parse('file://foo.ipynb'); + const file = Uri.parse('file:///foo.ipynb'); // Initially nothing in memento expect(storage.get(`notebook-storage-${file.toString()}`)).to.be.undefined; @@ -336,7 +339,7 @@ suite('Data Science - Native Editor', () => { }); test('Opening a notebook will restore uncommitted changes', async () => { - const file = Uri.parse('file://foo.ipynb'); + const file = Uri.parse('file:///foo.ipynb'); when(fileSystem.stat(anything())).thenResolve({ mtime: 1 } as any); // Initially nothing in memento @@ -363,7 +366,7 @@ suite('Data Science - Native Editor', () => { }); test('Opening a notebook will restore uncommitted changes (ignoring contents of file)', async () => { - const file = Uri.parse('file://foo.ipynb'); + const file = Uri.parse('file:///foo.ipynb'); when(fileSystem.stat(anything())).thenResolve({ mtime: 1 } as any); // Initially nothing in memento @@ -391,7 +394,7 @@ suite('Data Science - Native Editor', () => { }); test('Opening a notebook will NOT restore uncommitted changes if file has been modified since', async () => { - const file = Uri.parse('file://foo.ipynb'); + const file = Uri.parse('file:///foo.ipynb'); when(fileSystem.stat(anything())).thenResolve({ mtime: 1 } as any); // Initially nothing in memento @@ -419,7 +422,7 @@ suite('Data Science - Native Editor', () => { }); test('Pyton version info will be updated in notebook when a cell has been executed', async () => { - const file = Uri.parse('file://foo.ipynb'); + const file = Uri.parse('file:///foo.ipynb'); const editor = createEditor(); await editor.load(baseFile, file); @@ -429,11 +432,11 @@ suite('Data Science - Native Editor', () => { expect(contents.metadata!.language_info!.version).to.not.equal('10.11.12'); // When a cell is executed, then ensure we store the python version info in the notebook data. - const version: Version = {build: [], major: 10, minor: 11, patch: 12, prerelease: [], raw: '10.11.12'}; - when(executionProvider.getUsableJupyterPython()).thenResolve(({version} as any)); + const version: Version = { build: [], major: 10, minor: 11, patch: 12, prerelease: [], raw: '10.11.12' }; + when(executionProvider.getUsableJupyterPython()).thenResolve(({ version } as any)); try { - editor.onMessage(InteractiveWindowMessages.SubmitNewCell, {code: 'hello', id: '1'}); + editor.onMessage(InteractiveWindowMessages.SubmitNewCell, { code: 'hello', id: '1' }); } catch { // Ignore errors related to running cells, assume that works. noop(); @@ -453,4 +456,62 @@ suite('Data Science - Native Editor', () => { contents = JSON.parse(editor.contents) as nbformat.INotebookContent; expect(contents.metadata!.language_info!.version).to.equal('10.11.12'); }); + + test('Opening file with local storage but no global will still open with old contents', async () => { + // This test is really for making sure when a user upgrades to a new extension, we still have their old storage + const file = Uri.parse('file:///foo.ipynb'); + + // Initially nothing in memento + expect(storage.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + expect(localStorage.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + + // Put the regular file into the local storage + localStorage.update(`notebook-storage-${file.toString()}`, baseFile); + const editor = createEditor(); + await editor.load('', file); + + // It should load with that value + expect(editor.contents).to.be.equal(baseFile); + expect(editor.cells).to.be.lengthOf(3); + }); + + test('Export to Python script file from notebook.', async () => { + // Temp file location needed for export + const tempFile = { + dispose: () => { + return undefined; + }, + filePath: '/foo/bar.ipynb' + }; + when(fileSystem.createTemporaryFile('.ipynb')).thenResolve(tempFile); + + // Set up our importer to return file contents, check that we have the correct temp file location and + // original file location + const file = Uri.parse('file:///foo.ipynb'); + when(importer.importFromFile('/foo/bar.ipynb', file.fsPath)).thenResolve('# File Contents'); + + // Just return empty objects here, we don't care about open or show function, just that they were called + when(docManager.openTextDocument({ language: 'python', content: '# File Contents' })).thenResolve({} as any); + when(docManager.showTextDocument(anything(), anything())).thenResolve({} as any); + + const editor = createEditor(); + await editor.load(baseFile, file); + expect(editor.contents).to.be.equal(baseFile); + + // Make our call to actually export + editor.onMessage(InteractiveWindowMessages.Export, editor.cells); + + await waitForCondition(async () => { + try { + // Wait until showTextDocument has been called, that's the signal that export is done + verify(docManager.showTextDocument(anything(), anything())).atLeast(1); + return true; + } catch { + return false; + } + }, 1_000, 'Timeout'); + + // Verify that we also opened our text document not exact match as verify doesn't seem to match that + verify(docManager.openTextDocument(anything())).once(); + }); }); diff --git a/src/test/datascience/interactive-ipynb/nativeEditorProvider.unit.test.ts b/src/test/datascience/interactive-ipynb/nativeEditorProvider.unit.test.ts index 96ce959e9f2d..b8eef37d04e1 100644 --- a/src/test/datascience/interactive-ipynb/nativeEditorProvider.unit.test.ts +++ b/src/test/datascience/interactive-ipynb/nativeEditorProvider.unit.test.ts @@ -30,22 +30,52 @@ suite('Data Science - Native Editor Provider', () => { let workspace: IWorkspaceService; let configService: IConfigurationService; let fileSystem: IFileSystem; - let doctManager: IDocumentManager; + let docManager: IDocumentManager; let dsErrorHandler: IDataScienceErrorHandler; let cmdManager: ICommandManager; let svcContainer: IServiceContainer; + let eventEmitter: EventEmitter; + let editor: typemoq.IMock; + let file: Uri; setup(() => { svcContainer = mock(ServiceContainer); configService = mock(ConfigurationService); fileSystem = mock(FileSystem); - doctManager = mock(DocumentManager); + docManager = mock(DocumentManager); dsErrorHandler = mock(DataScienceErrorHandler); cmdManager = mock(CommandManager); workspace = mock(WorkspaceService); + eventEmitter = new EventEmitter(); }); - function createNotebookProvider() { + function createNotebookProvider(shouldOpenNotebookEditor: boolean) { + editor = typemoq.Mock.ofType(); + when(configService.getSettings()).thenReturn({ datascience: { useNotebookEditor: true } } as any); + when(docManager.onDidChangeActiveTextEditor).thenReturn(eventEmitter.event); + when(docManager.visibleTextEditors).thenReturn([]); + editor.setup(e => e.closed).returns(() => new EventEmitter().event); + editor.setup(e => e.executed).returns(() => new EventEmitter().event); + editor.setup(e => (e as any).then).returns(() => undefined); + when(svcContainer.get(INotebookEditor)).thenReturn(editor.object); + + // Ensure the editor is created and the load and show methods are invoked. + const invocationCount = shouldOpenNotebookEditor ? 1 : 0; + editor + .setup(e => e.load(typemoq.It.isAny(), typemoq.It.isAny())) + .returns((_a1: string, f: Uri) => { + file = f; + return Promise.resolve(); + }) + .verifiable(typemoq.Times.exactly(invocationCount)); + editor + .setup(e => e.show()) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.exactly(invocationCount)); + editor + .setup(e => e.file) + .returns(() => file); + return new NativeEditorProvider( instance(svcContainer), instance(mock(AsyncDisposableRegistry)), @@ -53,7 +83,7 @@ suite('Data Science - Native Editor Provider', () => { instance(workspace), instance(configService), instance(fileSystem), - instance(doctManager), + instance(docManager), instance(cmdManager), instance(dsErrorHandler) ); @@ -71,28 +101,7 @@ suite('Data Science - Native Editor Provider', () => { return textEditor.object; } async function testAutomaticallyOpeningNotebookEditorWhenOpeningFiles(uri: Uri, shouldOpenNotebookEditor: boolean) { - const eventEmitter = new EventEmitter(); - const editor = typemoq.Mock.ofType(); - when(configService.getSettings()).thenReturn({ datascience: { useNotebookEditor: true } } as any); - when(doctManager.onDidChangeActiveTextEditor).thenReturn(eventEmitter.event); - when(doctManager.visibleTextEditors).thenReturn([]); - editor.setup(e => e.closed).returns(() => new EventEmitter().event); - editor.setup(e => e.executed).returns(() => new EventEmitter().event); - editor.setup(e => (e as any).then).returns(() => undefined); - when(svcContainer.get(INotebookEditor)).thenReturn(editor.object); - - // Ensure the editor is created and the load and show methods are invoked. - const invocationCount = shouldOpenNotebookEditor ? 1 : 0; - editor - .setup(e => e.load(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.exactly(invocationCount)); - editor - .setup(e => e.show()) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.exactly(invocationCount)); - - const notebookEditor = createNotebookProvider(); + const notebookEditor = createNotebookProvider(shouldOpenNotebookEditor); // Open a text document. const textDoc = createTextDocument(uri, 'hello'); @@ -122,4 +131,11 @@ suite('Data Science - Native Editor Provider', () => { test('Do not open the notebook editor when an ipynb file is opened with a git scheme (comparing staged/modified files)', async () => { await testAutomaticallyOpeningNotebookEditorWhenOpeningFiles(Uri.parse('git://some//text file.txt'), false); }); + test('Multiple new notebooks have new names', async () => { + const provider = createNotebookProvider(false); + const n1 = await provider.createNew(); + expect(n1.file.fsPath).to.be.include('Untitled-1'); + const n2 = await provider.createNew(); + expect(n2.file.fsPath).to.be.include('Untitled-2'); + }); }); diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx index e8e375a45de6..83186959a72f 100644 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -577,6 +577,7 @@ for _ in range(50): runMountedTest('Gather code run from text editor', async (wrapper) => { ioc.getSettings().datascience.enableGather = true; + ioc.getSettings().datascience.gatherToScript = true; // Enter some code. const code = `${defaultCellMarker}\na=1\na`; await addCode(ioc, wrapper, code); @@ -590,12 +591,13 @@ for _ in range(50): const docManager = ioc.get(IDocumentManager) as MockDocumentManager; assert.notEqual(docManager.activeTextEditor, undefined); if (docManager.activeTextEditor) { - assert.equal(docManager.activeTextEditor.document.getText(), `# This file contains the minimal amount of code required to produce the code cell you gathered.\n${defaultCellMarker}\na=1\na\n\n`); + assert.equal(docManager.activeTextEditor.document.getText(), `# This file contains only the code required to produce the results of the gathered cell.\n${defaultCellMarker}\na=1\na\n\n`); } }, () => { return ioc; }); runMountedTest('Gather code run from input box', async (wrapper) => { ioc.getSettings().datascience.enableGather = true; + ioc.getSettings().datascience.gatherToScript = true; // Create an interactive window so that it listens to the results. const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); @@ -612,7 +614,7 @@ for _ in range(50): const docManager = ioc.get(IDocumentManager) as MockDocumentManager; assert.notEqual(docManager.activeTextEditor, undefined); if (docManager.activeTextEditor) { - assert.equal(docManager.activeTextEditor.document.getText(), `# This file contains the minimal amount of code required to produce the code cell you gathered.\n${defaultCellMarker}\na=1\na\n\n`); + assert.equal(docManager.activeTextEditor.document.getText(), `# This file contains only the code required to produce the results of the gathered cell.\n${defaultCellMarker}\na=1\na\n\n`); } }, () => { return ioc; }); diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts index 648708b8f7e1..743dc943aa80 100644 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -3,22 +3,25 @@ 'use strict'; import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; import { assert } from 'chai'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; import * as TypeMoq from 'typemoq'; import * as uuid from 'uuid/v4'; -import { Disposable, EventEmitter, Uri } from 'vscode'; +import { EventEmitter, Uri } from 'vscode'; import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; import { ConfigurationService } from '../../client/common/configuration/service'; import { Logger } from '../../client/common/logger'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposable, ILogger } from '../../client/common/types'; +import * as localize from '../../client/common/utils/localize'; import { generateCells } from '../../client/datascience/cellFactory'; import { Commands } from '../../client/datascience/constants'; import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; +import { NativeEditorProvider } from '../../client/datascience/interactive-ipynb/nativeEditorProvider'; import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; @@ -27,20 +30,19 @@ import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyte import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; import { - IInteractiveBase, IInteractiveWindow, + IJupyterExecution, INotebook, - INotebookServer, - IStatusProvider + INotebookEditorProvider, + INotebookServer } from '../../client/datascience/types'; import { InterpreterService } from '../../client/interpreter/interpreterService'; import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; import { ServiceContainer } from '../../client/ioc/container'; -import { noop } from '../core'; import { MockAutoSelectionService } from '../mocks/autoSelector'; -import * as vscodeMocks from '../vscode-mock'; import { MockCommandManager } from './mockCommandManager'; import { MockDocumentManager } from './mockDocumentManager'; +import { MockStatusProvider } from './mockStatusProvider'; // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length @@ -53,19 +55,6 @@ function createTypeMoq(tag: string): TypeMoq.IMock { return result; } -class MockStatusProvider implements IStatusProvider { - public set(_message: string, _timeout?: number, _cancel?: () => void, _panel?: IInteractiveBase): Disposable { - return { - dispose: noop - }; - } - - public waitWithStatus(promise: () => Promise, _message: string, _timeout?: number, _canceled?: () => void, _panel?: IInteractiveBase): Promise { - return promise(); - } - -} - // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length suite('Interactive window command listener', async () => { const interpreterService = mock(InterpreterService); @@ -81,26 +70,16 @@ suite('Interactive window command listener', async () => { const dataScienceErrorHandler = mock(DataScienceErrorHandler); const notebookImporter = mock(JupyterImporter); const notebookExporter = mock(JupyterExporter); - const applicationShell = mock(ApplicationShell); - const jupyterExecution = mock(JupyterExecutionFactory); + let applicationShell: IApplicationShell; + let jupyterExecution: IJupyterExecution; const interactiveWindow = createTypeMoq('Interactive Window'); const documentManager = new MockDocumentManager(); const statusProvider = new MockStatusProvider(); const commandManager = new MockCommandManager(); + let notebookEditorProvider: INotebookEditorProvider; const server = createTypeMoq('jupyter server'); let lastFileContents: any; - suiteSetup(() => { - vscodeMocks.initialize(); - }); - suiteTeardown(() => { - noop(); - }); - - setup(() => { - noop(); - }); - teardown(() => { documentManager.activeTextEditor = undefined; lastFileContents = undefined; @@ -125,6 +104,10 @@ suite('Interactive window command listener', async () => { } function createCommandListener(): InteractiveWindowCommandListener { + notebookEditorProvider = mock(NativeEditorProvider); + jupyterExecution = mock(JupyterExecutionFactory); + applicationShell = mock(ApplicationShell); + // Setup defaults when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter'); @@ -204,9 +187,7 @@ suite('Interactive window command listener', async () => { } ); - if (jupyterExecution.isNotebookSupported) { - when(jupyterExecution.isNotebookSupported()).thenResolve(true); - } + when(jupyterExecution.isNotebookSupported()).thenResolve(true); documentManager.addDocument('#%%\r\nprint("code")', 'bar.ipynb'); @@ -225,7 +206,8 @@ suite('Interactive window command listener', async () => { instance(configService), statusProvider, instance(notebookImporter), - instance(dataScienceErrorHandler)); + instance(dataScienceErrorHandler), + instance(notebookEditorProvider)); result.register(commandManager); return result; @@ -247,9 +229,14 @@ suite('Interactive window command listener', async () => { const doc = await documentManager.openTextDocument('bar.ipynb'); await documentManager.showTextDocument(doc); when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); + when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(Promise.resolve('moo')); + when(jupyterExecution.isSpawnSupported()).thenResolve(true); await commandManager.executeCommand(Commands.ExportFileAsNotebook, Uri.file('bar.ipynb'), undefined); + assert.ok(lastFileContents, 'Export file was not written to'); + verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once(); }); test('Export File and output', async () => { createCommandListener(); @@ -264,9 +251,13 @@ suite('Interactive window command listener', async () => { when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(Promise.resolve('moo')); + when(jupyterExecution.isSpawnSupported()).thenResolve(true); await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb')); + assert.ok(lastFileContents, 'Export file was not written to'); + verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once(); }); test('Export skipped on no file', async () => { createCommandListener(); @@ -282,5 +273,4 @@ suite('Interactive window command listener', async () => { await commandManager.executeCommand(Commands.ExportFileAsNotebook, undefined, undefined); assert.ok(lastFileContents, 'Export file was not written to'); }); - }); diff --git a/src/test/datascience/latexManipulation.unit.test.ts b/src/test/datascience/latexManipulation.unit.test.ts new file mode 100644 index 000000000000..f20e044b8ec3 --- /dev/null +++ b/src/test/datascience/latexManipulation.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import { fixLatexEquations } from '../../datascience-ui/interactive-common/latexManipulation'; + +// tslint:disable: max-func-body-length +suite('Data Science - LaTeX Manipulation', () => { + const markdown1 = `\\begin{align} +\\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ +\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ +\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 +\\end{align} +sample text`; + + const output1 = ` +$$ +\\begin{align} +\\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ +\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ +\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 +\\end{align} +$$ + +sample text`; + + const markdown2 = `$\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}$ +sample text +$\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}$ +sample text`; + + const markdown3 = `\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text + +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}`; + + const output3 = ` +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ +`; + + test('Latex - Equations don\'t have $$', () => { + const result = fixLatexEquations(markdown1); + expect(result).to.be.equal(output1, 'Result is incorrect'); + }); + + test('Latex - Equations have $', () => { + const result = fixLatexEquations(markdown2); + expect(result).to.be.equal(markdown2, 'Result is incorrect'); + }); + + test('Latex - Multiple equations don\'t have $$', () => { + const result = fixLatexEquations(markdown3); + expect(result).to.be.equal(output3, 'Result is incorrect'); + }); +}); diff --git a/src/test/datascience/mockStatusProvider.ts b/src/test/datascience/mockStatusProvider.ts new file mode 100644 index 000000000000..7dd9414486f4 --- /dev/null +++ b/src/test/datascience/mockStatusProvider.ts @@ -0,0 +1,18 @@ +import { Disposable } from 'vscode'; +import { + IInteractiveBase, + IStatusProvider +} from '../../client/datascience/types'; +import { noop } from '../core'; +export class MockStatusProvider implements IStatusProvider { + public set(_message: string, _inweb: boolean, _timeout?: number, _cancel?: () => void, _panel?: IInteractiveBase): Disposable { + return { + dispose: noop + }; + } + + public waitWithStatus(promise: () => Promise, _message: string, _inweb: boolean, _timeout?: number, _canceled?: () => void, _panel?: IInteractiveBase): Promise { + return promise(); + } + +} diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index dcddf66545c2..c81140967889 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -14,7 +14,7 @@ import { Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; -import { createDeferred, waitForPromise } from '../../client/common/utils/async'; +import { createDeferred } from '../../client/common/utils/async'; import { createTemporaryFile } from '../../client/common/utils/fs'; import { noop } from '../../client/common/utils/misc'; import { Identifiers } from '../../client/datascience/constants'; @@ -202,7 +202,7 @@ for _ in range(50): // find the buttons on the cell itself let cell = getLastOutputCell(wrapper, 'NativeCell'); let ImageButtons = cell.find(ImageButton); - assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + assert.equal(ImageButtons.length, 8, 'Cell buttons not found'); let deleteButton = ImageButtons.at(6); // Make sure delete works @@ -216,7 +216,7 @@ for _ in range(50): // least one cell in the file. cell = getLastOutputCell(wrapper, 'NativeCell'); ImageButtons = cell.find(ImageButton); - assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + assert.equal(ImageButtons.length, 8, 'Cell buttons not found'); deleteButton = ImageButtons.at(6); afterDelete = await getNativeCellResults(wrapper, 1, async () => { @@ -344,8 +344,9 @@ for _ in range(50): const cell = getOutputCell(wrapper, 'NativeCell', 1); assert.ok(cell, 'Cannot find the first cell'); const imageButtons = cell!.find(ImageButton); - assert.equal(imageButtons.length, 7, 'Cell buttons not found'); - const runButton = imageButtons.at(2); + assert.equal(imageButtons.length, 8, 'Cell buttons not found'); + const runButton = imageButtons.findWhere(w => w.props().tooltip === 'Run cell'); + assert.equal(runButton.length, 1, 'No run button found'); const update = waitForMessage(ioc, InteractiveWindowMessages.RenderComplete); runButton.simulate('click'); await update; @@ -566,12 +567,20 @@ for _ in range(50): }); suite('Keyboard Shortcuts', () => { + const originalPlatform = window.navigator.platform; + Object.defineProperty(window.navigator, 'platform', ((value: string) => { + return { + get: () => value, + set: (v: string) => value = v + }; + })(originalPlatform)); setup(async function() { + (window.navigator as any).platform = originalPlatform; initIoc(); // tslint:disable-next-line: no-invalid-this await setupFunction.call(this); }); - + teardown(() => (window.navigator as any).platform = originalPlatform); test('Traverse cells by using ArrowUp and ArrowDown, k and j', async () => { const keyCodesAndPositions = [ // When we press arrow down in the first cell, then second cell gets selected. @@ -960,7 +969,27 @@ for _ in range(50): } }); - test('Test save using the key \'s\'', async () => { + test('Test save using the key \'ctrl+s\' on Windows', async () => { + (window.navigator as any).platform = 'Win'; + clickCell(0); + + await addCell(wrapper, ioc, 'a=1\na', true); + + const notebookProvider = ioc.get(INotebookEditorProvider); + const editor = notebookProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + simulateKeyPressOnCell(1, { code: 's', ctrlKey: true }); + + await waitForCondition(() => savedPromise.promise.then(() => true).catch(() => false), 1_000, 'Timedout'); + + assert.ok(!editor!.isDirty, 'Editor should not be dirty after saving'); + }); + + test('Test save using the key \'ctrl+s\' on Mac', async () => { + (window.navigator as any).platform = 'Mac'; clickCell(0); await addCell(wrapper, ioc, 'a=1\na', true); @@ -973,10 +1002,48 @@ for _ in range(50): simulateKeyPressOnCell(1, { code: 's', ctrlKey: true }); - await waitForPromise(savedPromise.promise, 1_000); + await expect(waitForCondition(() => savedPromise.promise.then(() => true).catch(() => false), 1_000, 'Timedout')).to.eventually.be.rejected; + assert.ok(editor!.isDirty, 'Editor be dirty as nothing got saved'); + }); + + test('Test save using the key \'cmd+s\' on a Mac', async () => { + (window.navigator as any).platform = 'Mac'; + + clickCell(0); + + await addCell(wrapper, ioc, 'a=1\na', true); + + const notebookProvider = ioc.get(INotebookEditorProvider); + const editor = notebookProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + simulateKeyPressOnCell(1, { code: 's', metaKey: true }); + + await waitForCondition(() => savedPromise.promise.then(() => true).catch(() => false), 1_000, 'Timedout'); assert.ok(!editor!.isDirty, 'Editor should not be dirty after saving'); }); + test('Test save using the key \'cmd+s\' on a Windows', async () => { + (window.navigator as any).platform = 'Win'; + + clickCell(0); + + await addCell(wrapper, ioc, 'a=1\na', true); + + const notebookProvider = ioc.get(INotebookEditorProvider); + const editor = notebookProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + // CMD+s won't work on Windows. + simulateKeyPressOnCell(1, { code: 's', metaKey: true }); + + await expect(waitForCondition(() => savedPromise.promise.then(() => true).catch(() => false), 1_000, 'Timedout')).to.eventually.be.rejected; + assert.ok(editor!.isDirty, 'Editor be dirty as nothing got saved'); + }); }); suite('Auto Save', () => { diff --git a/src/test/datascience/nativeEditorTestHelpers.tsx b/src/test/datascience/nativeEditorTestHelpers.tsx index 4ddd9df9f19c..82090ff94998 100644 --- a/src/test/datascience/nativeEditorTestHelpers.tsx +++ b/src/test/datascience/nativeEditorTestHelpers.tsx @@ -8,6 +8,7 @@ import { Uri } from 'vscode'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { IJupyterExecution, INotebookEditor, INotebookEditorProvider } from '../../client/datascience/types'; +import { CursorPos } from '../../datascience-ui/interactive-common/mainState'; import { NativeCell } from '../../datascience-ui/native-editor/nativeCell'; import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; import { ImageButton } from '../../datascience-ui/react-common/imageButton'; @@ -85,7 +86,7 @@ export function focusCell(ioc: DataScienceIocContainer, wrapper: ReactWrapper { } }); + runTest('Change Interpreter', async () => { + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + + // Real Jupyter doesn't help this test at all and is tricky to set up for it, so just skip it + if (!isRollingBuild) { + const server = await createNotebook(true); + + // Create again, we should get the same server from the cache + const server2 = await createNotebook(true); + assert.equal(server, server2, 'With no settings changed we should return the cached server'); + + // Create a new mock interpreter with a different path + const newPython: PythonInterpreter = { + path: '/foo/bar/baz/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + type: InterpreterType.Unknown, + architecture: Architecture.x64, + }; + + // Add interpreter into mock jupyter service and set it as active + ioc.addInterpreter(newPython, SupportedCommands.all); + + // Create a new notebook, we should not be the same anymore + const server3 = await createNotebook(true); + assert.notEqual(server, server3, 'With interpreter changed we should return a new server'); + } else { + console.log(`Skipping Change Interpreter test in non-mocked Jupyter case`); + } + }); + runTest('Restart kernel', async () => { addMockData(`a=1${os.EOL}a`, 1); addMockData(`a+=1${os.EOL}a`, 2); diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx index dc2071a395cd..44099f6f9675 100644 --- a/src/test/datascience/testHelpers.tsx +++ b/src/test/datascience/testHelpers.tsx @@ -36,7 +36,7 @@ export enum CellPosition { Last = 'last' } -export function waitForMessage(ioc: DataScienceIocContainer, message: string): Promise { +export function waitForMessage(ioc: DataScienceIocContainer, message: string, timeoutMs: number = 65000): Promise { // Wait for the mounted web panel to send a message back to the data explorer const promise = createDeferred(); let handler: (m: string, p: any) => void; @@ -44,7 +44,7 @@ export function waitForMessage(ioc: DataScienceIocContainer, message: string): P if (!promise.resolved) { promise.reject(new Error(`Waiting for ${message} timed out`)); } - }, 3000); // Max 3 seconds for a message. Should be almost instant but this will make tests fail faster than the max timeout. + }, timeoutMs); handler = (m: string, _p: any) => { if (m === message) { ioc.removeMessageListener(handler); diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx index 385df9b391e5..cefe39f2b8e1 100644 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -14,9 +14,8 @@ import { InteractivePanel } from '../../datascience-ui/history-react/interactive import { VariableExplorer } from '../../datascience-ui/interactive-common/variableExplorer'; import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { addCode, runMountedTest as interactiveRunMountedTest } from './interactiveWindowTestHelpers'; -import { addCell, createNewEditor, runMountedTest as nativeRunMountedTest } from './nativeEditorTestHelpers'; -import { waitForUpdate } from './reactHelpers'; +import { addCode } from './interactiveWindowTestHelpers'; +import { addCell, createNewEditor } from './nativeEditorTestHelpers'; import { runDoubleTest, waitForMessage } from './testHelpers'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string @@ -65,34 +64,6 @@ suite('DataScience Interactive Window variable explorer tests', () => { return waitForMessage(ioc, InteractiveWindowMessages.VariablesComplete); } - async function checkVariableLoading(wrapper: ReactWrapper, React.Component>, targetRenderCount: number) { - const basicCode: string = `value = 'hello world'`; - - openVariableExplorer(wrapper); - - await addCodeImpartial(wrapper, 'a=1\na'); - await addCodeImpartial(wrapper, basicCode, false); - - // Target a render count before loading is finished - await waitForUpdate(wrapper, VariableExplorer, targetRenderCount); - - let targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'value', value: 'Loading...', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - - // Now wait for one more update and then check the variables, we should have loaded the value var - await waitForUpdate(wrapper, VariableExplorer, 1); - - targetVariables = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value', value: "'hello world'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - } - async function addCodeImpartial(wrapper: ReactWrapper, React.Component>, code: string, waitForVariables: boolean = true, expectedRenderCount: number = 4, expectError: boolean = false): Promise, React.Component>> { const variablesUpdated = waitForVariables ? waitForVariablesUpdated() : Promise.resolve(); const nodes = wrapper.find('InteractivePanel'); @@ -181,16 +152,6 @@ value = 'hello world'`; verifyVariables(wrapper, targetVariables); }, () => { return ioc; }); - // For the loading tests we check before the explorer is fully loaded, so split tests here to check - // with different target render counts - nativeRunMountedTest('Variable Explorer - Native Loading', async (wrapper) => { - await checkVariableLoading(wrapper, 3); - }, () => { return ioc; }); - - interactiveRunMountedTest('Variable Explorer - Interactive Loading', async (wrapper) => { - await checkVariableLoading(wrapper, 2); - }, () => { return ioc; }); - // Test our display of basic types. We render 8 rows by default so only 8 values per test runDoubleTest('Variable explorer - Types A', async (wrapper) => { const basicCode: string = `myList = [1, 2, 3] diff --git a/src/test/debugger/extension/adapter/activator.unit.test.ts b/src/test/debugger/extension/adapter/activator.unit.test.ts index 1f83e54ddc80..07d4336ffaab 100644 --- a/src/test/debugger/extension/adapter/activator.unit.test.ts +++ b/src/test/debugger/extension/adapter/activator.unit.test.ts @@ -22,19 +22,21 @@ import { FileSystem } from '../../../../client/common/platform/fileSystem'; import { IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator'; import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; -import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory } from '../../../../client/debugger/extension/types'; import { clearTelemetryReporter } from '../../../../client/telemetry'; import { EventName } from '../../../../client/telemetry/constants'; import { noop } from '../../../core'; import { MockOutputChannel } from '../../../mockClasses'; // tslint:disable-next-line: max-func-body-length -suite('Debugging - Adapter Factory Registration', () => { +suite('Debugging - Adapter Factory and logger Registration', () => { const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; let activator: IExtensionSingleActivationService; let debugService: IDebugService; - let factory: IDebugAdapterDescriptorFactory; + let descriptorFactory: IDebugAdapterDescriptorFactory; + let loggingFactory: IDebugSessionLoggingFactory; let disposableRegistry: IDisposableRegistry; let experimentsManager: ExperimentsManager; let spiedInstance: ExperimentsManager; @@ -80,9 +82,10 @@ suite('Debugging - Adapter Factory Registration', () => { spiedInstance = spy(experimentsManager); debugService = mock(DebugService); - factory = mock(DebugAdapterDescriptorFactory); + descriptorFactory = mock(DebugAdapterDescriptorFactory); + loggingFactory = mock(DebugSessionLoggingFactory); disposableRegistry = []; - activator = new DebugAdapterActivator(instance(debugService), instance(factory), disposableRegistry, experimentsManager); + activator = new DebugAdapterActivator(instance(debugService), instance(descriptorFactory), instance(loggingFactory), disposableRegistry, experimentsManager); }); teardown(() => { @@ -100,17 +103,19 @@ suite('Debugging - Adapter Factory Registration', () => { await activator.activate(); - verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(factory))).once(); + verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once(); + verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once(); }); test('Register a disposable item if inside the DA experiment', async () => { when(spiedInstance.inExperiment(DebugAdapterExperiment.experiment)).thenReturn(true); const disposable = { dispose: noop }; + when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable); when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable); await activator.activate(); - assert.deepEqual(disposableRegistry, [disposable]); + assert.deepEqual(disposableRegistry, [disposable, disposable]); }); test('Send experiment group telemetry if inside the DA experiment', async () => { @@ -127,7 +132,8 @@ suite('Debugging - Adapter Factory Registration', () => { await activator.activate(); - verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(factory))).never(); + verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).never(); + verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).never(); }); test('Don\'t register a disposable item if not inside the DA experiment', async () => { diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts new file mode 100644 index 000000000000..cae4d7b19328 --- /dev/null +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; + +// tslint:disable-next-line: max-func-body-length +suite('Debugging - Session Logging', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let loggerFactory: DebugSessionLoggingFactory; + let fsService: FileSystem; + let writeStream: fs.WriteStream; + + setup(() => { + fsService = mock(FileSystem); + writeStream = mock(fs.WriteStream); + + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + loggerFactory = new DebugSessionLoggingFactory(instance(fsService)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); + + function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python' + }, + id: id, + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve() + }; + } + + function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession { + const session = createSession(id, workspaceFolder); + session.configuration.logToFile = logToFile; + return session; + } + + class TestMessage implements DebugProtocol.ProtocolMessage { + public seq: number; + public type: string; + public id: number; + public format: string; + public variables?: { [key: string]: string }; + public sendTelemetry?: boolean; + public showUser?: boolean; + public url?: string; + public urlLabel?: string; + constructor(id: number, seq: number, type: string) { + this.id = id; + this.format = 'json'; + this.seq = seq; + this.type = type; + } + } + + test('Create logger using session without logToFile', async () => { + const session = createSession('test1'); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + await loggerFactory.createDebugAdapterTracker(session); + + verify(fsService.createWriteStream(filePath)).never(); + }); + + test('Create logger using session with logToFile set to false', async () => { + const session = createSessionWithLogging('test2', false); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenReturn(true); + const logger = await loggerFactory.createDebugAdapterTracker(session); + if (logger) { + logger.onWillStartSession!(); + } + + verify(fsService.createWriteStream(filePath)).never(); + verify(writeStream.write(anything())).never(); + }); + + test('Create logger using session with logToFile set to true', async () => { + const session = createSessionWithLogging('test3', true); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + const logs: string[] = []; + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenCall((msg) => logs.push(msg)); + + const message = new TestMessage(1, 1, 'test-message'); + const logger = await loggerFactory.createDebugAdapterTracker(session); + + if (logger) { + logger.onWillStartSession!(); + assert.ok(logs.pop()!.includes('Starting Session')); + + logger.onDidSendMessage!(message); + const sentLog = logs.pop(); + assert.ok(sentLog!.includes('Client <-- Adapter')); + assert.ok(sentLog!.includes('test-message')); + + logger.onWillReceiveMessage!(message); + const receivedLog = logs.pop(); + assert.ok(receivedLog!.includes('Client --> Adapter')); + assert.ok(receivedLog!.includes('test-message')); + + logger.onWillStopSession!(); + assert.ok(logs.pop()!.includes('Stopping Session')); + + logger.onError!(new Error('test error message')); + assert.ok(logs.pop()!.includes('Error')); + + logger.onExit!(111, '222'); + const exitLog1 = logs.pop(); + assert.ok(exitLog1!.includes('Exit-Code: 111')); + assert.ok(exitLog1!.includes('Signal: 222')); + + logger.onExit!(undefined, undefined); + const exitLog2 = logs.pop(); + assert.ok(exitLog2!.includes('Exit-Code: 0')); + assert.ok(exitLog2!.includes('Signal: none')); + } + + verify(fsService.createWriteStream(filePath)).once(); + verify(writeStream.write(anything())).times(7); + assert.deepEqual(logs, []); + }); +}); diff --git a/src/test/mocks/vsc/telemetryReporter.ts b/src/test/mocks/vsc/telemetryReporter.ts index 9b5ef94178cf..d0250eb1cf5e 100644 --- a/src/test/mocks/vsc/telemetryReporter.ts +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -4,8 +4,7 @@ 'use strict'; // tslint:disable:all -import * as telemetry from 'vscode-extension-telemetry'; -export class vscMockTelemetryReporter implements telemetry.default { +export class vscMockTelemetryReporter { constructor() { // } diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index be8185f2b238..bfe0633f9647 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -14,8 +14,7 @@ import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/cons import '../../../client/common/extensions'; import { BufferDecoder } from '../../../client/common/process/decoder'; import { ProcessService } from '../../../client/common/process/proc'; -import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; -import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; import { OSType } from '../../../client/common/utils/platform'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -32,28 +31,22 @@ suite('Terminal - Code Execution Helper', () => { let helper: ICodeExecutionHelper; let document: TypeMoq.IMock; let editor: TypeMoq.IMock; - let processService: TypeMoq.IMock; - let configService: TypeMoq.IMock; + let pythonService: TypeMoq.IMock; setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); const envVariablesProvider = TypeMoq.Mock.ofType(); - processService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); + pythonService = TypeMoq.Mock.ofType(); // tslint:disable-next-line:no-any - processService.setup((x: any) => x.then).returns(() => undefined); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonService.setup((x: any) => x.then).returns(() => undefined); envVariablesProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - const processServiceFactory = TypeMoq.Mock.ofType(); - processServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => processServiceFactory.object); + const pythonExecFactory = TypeMoq.Mock.ofType(); + pythonExecFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonService.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())).returns(() => pythonExecFactory.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => documentManager.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => applicationShell.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())).returns(() => envVariablesProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); helper = new CodeExecutionHelper(serviceContainer.object); document = TypeMoq.Mock.ofType(); @@ -63,19 +56,20 @@ suite('Terminal - Code Execution Helper', () => { async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { const actualProcessService = new ProcessService(new BufferDecoder()); - processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((file, args, options) => { - return actualProcessService.exec.apply(actualProcessService, [file, args, options]); + pythonService + .setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((args, options) => { + return actualProcessService.exec.apply(actualProcessService, [PYTHON_PATH, args, options]); }); const normalizedZCode = await helper.normalizeLines(source); // In case file has been saved with different line endings. expectedSource = expectedSource.splitLines({ removeEmptyEntries: false, trim: false }).join(EOL); expect(normalizedZCode).to.be.equal(expectedSource); } - test('Ensure blank lines are NOT removed when code is not indented (simple)', async function () { + test('Ensure blank lines are NOT removed when code is not indented (simple)', async function() { // This test has not been working for many months in Python 2.7 under // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { // tslint:disable-next-line:no-invalid-this return this.skip(); } @@ -87,19 +81,20 @@ suite('Terminal - Code Execution Helper', () => { test('Ensure there are no multiple-CR elements in the normalized code.', async () => { const code = ['import sys', '', '', '', 'print(sys.executable)', '', 'print("1234")', '', '', 'print(1)', 'print(2)']; const actualProcessService = new ProcessService(new BufferDecoder()); - processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((file, args, options) => { - return actualProcessService.exec.apply(actualProcessService, [file, args, options]); + pythonService + .setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((args, options) => { + return actualProcessService.exec.apply(actualProcessService, [PYTHON_PATH, args, options]); }); const normalizedCode = await helper.normalizeLines(code.join(EOL)); const doubleCrIndex = normalizedCode.indexOf('\r\r'); expect(doubleCrIndex).to.be.equal(-1, 'Double CR (CRCRLF) line endings detected in normalized code snippet.'); }); ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach(fileNameSuffix => { - test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async function () { + test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async function() { // This test has not been working for many months in Python 2.7 under // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { // tslint:disable-next-line:no-invalid-this return this.skip(); } @@ -108,10 +103,10 @@ suite('Terminal - Code Execution Helper', () => { const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); await ensureBlankLinesAreRemoved(code, expectedCode); }); - test(`Ensure last two blank lines are preserved (Sample${fileNameSuffix})`, async function () { + test(`Ensure last two blank lines are preserved (Sample${fileNameSuffix})`, async function() { // This test has not been working for many months in Python 2.7 under // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { // tslint:disable-next-line:no-invalid-this return this.skip(); } @@ -120,10 +115,10 @@ suite('Terminal - Code Execution Helper', () => { const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); await ensureBlankLinesAreRemoved(code + EOL, expectedCode + EOL); }); - test(`Ensure last two blank lines are preserved even if we have more than 2 trailing blank lines (Sample${fileNameSuffix})`, async function () { + test(`Ensure last two blank lines are preserved even if we have more than 2 trailing blank lines (Sample${fileNameSuffix})`, async function() { // This test has not been working for many months in Python 2.7 under // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { // tslint:disable-next-line:no-invalid-this return this.skip(); } @@ -133,7 +128,7 @@ suite('Terminal - Code Execution Helper', () => { await ensureBlankLinesAreRemoved(code + EOL + EOL + EOL + EOL, expectedCode + EOL); }); }); - test('Display message if there\s no active file', async () => { + test('Display message if there\'s no active file', async () => { documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); const uri = await helper.getFileToExecute(); @@ -233,14 +228,20 @@ suite('Terminal - Code Execution Helper', () => { }); test('saveFileIfDirty will not fail if file is not opened', async () => { - documentManager.setup(d => d.textDocuments).returns(() => []).verifiable(TypeMoq.Times.once()); + documentManager + .setup(d => d.textDocuments) + .returns(() => []) + .verifiable(TypeMoq.Times.once()); await helper.saveFileIfDirty(Uri.file(`${__filename}.py`)); documentManager.verifyAll(); }); test('File will be saved if file is dirty', async () => { - documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + documentManager + .setup(d => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); document.setup(doc => doc.isUntitled).returns(() => false); document.setup(doc => doc.isDirty).returns(() => true); document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); @@ -253,7 +254,10 @@ suite('Terminal - Code Execution Helper', () => { }); test('File will be not saved if file is not dirty', async () => { - documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + documentManager + .setup(d => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); document.setup(doc => doc.isUntitled).returns(() => false); document.setup(doc => doc.isDirty).returns(() => false); document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); diff --git a/src/test/testing/navigation/symbolNavigator.unit.test.ts b/src/test/testing/navigation/symbolNavigator.unit.test.ts index 8b4e2cc2f656..0cfef1c989c7 100644 --- a/src/test/testing/navigation/symbolNavigator.unit.test.ts +++ b/src/test/testing/navigation/symbolNavigator.unit.test.ts @@ -5,45 +5,49 @@ import { expect } from 'chai'; import * as path from 'path'; -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { CancellationToken, CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { ProcessService } from '../../../client/common/process/proc'; -import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; -import { ExecutionResult, IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; -import { IConfigurationService, IDocumentSymbolProvider } from '../../../client/common/types'; +import { ExecutionResult, IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { IDocumentSymbolProvider } from '../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../client/constants'; import { TestFileSymbolProvider } from '../../../client/testing/navigation/symbolProvider'; // tslint:disable:max-func-body-length no-any suite('Unit Tests - Navigation Command Handler', () => { let symbolProvider: IDocumentSymbolProvider; - let configService: IConfigurationService; - let processFactory: IProcessServiceFactory; - let processService: IProcessService; + let pythonExecFactory: typemoq.IMock; + let pythonService: typemoq.IMock; let doc: typemoq.IMock; let token: CancellationToken; setup(() => { - configService = mock(ConfigurationService); - processFactory = mock(ProcessServiceFactory); - processService = mock(ProcessService); + pythonService = typemoq.Mock.ofType(); + pythonExecFactory = typemoq.Mock.ofType(); + + // Both typemoq and ts-mockito fail to resolve promises on dynamically created mocks + // A solution is to mock the `then` on the mock that the `Promise` resolves to. + // typemoq: https://github.com/florinn/typemoq/issues/66#issuecomment-315681245 + // ts-mockito: https://github.com/NagRock/ts-mockito/issues/163#issuecomment-536210863 + // In this case, the factory below returns a promise that is a mock of python service + // so we need to mock the `then` on the service. + pythonService.setup((x: any) => x.then).returns(() => undefined); + + pythonExecFactory.setup(factory => factory.create(typemoq.It.isAny())).returns(async () => pythonService.object); + doc = typemoq.Mock.ofType(); token = new CancellationTokenSource().token; - symbolProvider = new TestFileSymbolProvider(instance(configService), instance(processFactory)); }); test('Ensure no symbols are returned when file has not been saved', async () => { doc.setup(d => d.isUntitled) .returns(() => true) .verifiable(typemoq.Times.once()); + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); expect(symbols).to.be.lengthOf(0); doc.verifyAll(); }); test('Ensure no symbols are returned when there are errors in running the code', async () => { - when(configService.getSettings(anything())).thenThrow(new Error('Kaboom')); doc.setup(d => d.isUntitled) .returns(() => false) .verifiable(typemoq.Times.once()); @@ -54,14 +58,19 @@ suite('Unit Tests - Navigation Command Handler', () => { .returns(() => Uri.file(__filename)) .verifiable(typemoq.Times.atLeastOnce()); + pythonService + .setup(service => service.exec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(async () => { + return { stdout: '' }; + }); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); - verify(configService.getSettings(anything())).once(); expect(symbols).to.be.lengthOf(0); doc.verifyAll(); }); test('Ensure no symbols are returned when there are no symbols to be returned', async () => { - const pythonPath = 'Hello There'; const docUri = Uri.file(__filename); const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), docUri.fsPath]; const proc: ExecutionResult = { @@ -76,22 +85,20 @@ suite('Unit Tests - Navigation Command Handler', () => { doc.setup(d => d.uri) .returns(() => docUri) .verifiable(typemoq.Times.atLeastOnce()); - when(configService.getSettings(anything())).thenReturn({ pythonPath } as any); - when(processFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(pythonPath, anything(), anything())).thenResolve(proc); - doc.setup(d => d.isDirty).returns(() => false); - doc.setup(d => d.uri).returns(() => docUri); + pythonService + .setup(service => service.exec(typemoq.It.isValue(args), typemoq.It.isAny())) + .returns(async () => proc) + .verifiable(typemoq.Times.once()); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); - verify(configService.getSettings(anything())).once(); - verify(processFactory.create(anything())).once(); - verify(processService.exec(pythonPath, deepEqual(args), deepEqual({ throwOnStdErr: true, token }))).once(); expect(symbols).to.be.lengthOf(0); doc.verifyAll(); + pythonService.verifyAll(); }); test('Ensure symbols are returned', async () => { - const pythonPath = 'Hello There'; const docUri = Uri.file(__filename); const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), docUri.fsPath]; const proc: ExecutionResult = { @@ -131,19 +138,18 @@ suite('Unit Tests - Navigation Command Handler', () => { doc.setup(d => d.uri) .returns(() => docUri) .verifiable(typemoq.Times.atLeastOnce()); - when(configService.getSettings(anything())).thenReturn({ pythonPath } as any); - when(processFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(pythonPath, anything(), anything())).thenResolve(proc); - doc.setup(d => d.isDirty).returns(() => false); - doc.setup(d => d.uri).returns(() => docUri); + pythonService + .setup(service => service.exec(typemoq.It.isValue(args), typemoq.It.isAny())) + .returns(async () => proc) + .verifiable(typemoq.Times.once()); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); const symbols = (await symbolProvider.provideDocumentSymbols(doc.object, token)) as SymbolInformation[]; - verify(configService.getSettings(anything())).once(); - verify(processFactory.create(anything())).once(); - verify(processService.exec(pythonPath, deepEqual(args), deepEqual({ throwOnStdErr: true, token }))).once(); expect(symbols).to.be.lengthOf(3); doc.verifyAll(); + pythonService.verifyAll(); expect(symbols[0].kind).to.be.equal(SymbolKind.Class); expect(symbols[0].name).to.be.equal('one'); expect(symbols[0].location.range).to.be.deep.equal(new Range(1, 2, 3, 4)); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index fb8b870c919f..93798c7697a5 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -38,7 +38,7 @@ export function initialize() { return mockedVSCode; } if (request === 'vscode-extension-telemetry') { - return { default: vscMockTelemetryReporter }; + return { default: vscMockTelemetryReporter as any }; } // less files need to be in import statements to be converted to css // But we don't want to try to load them in the mock vscode