diff --git a/.eslintrc.json b/.eslintrc.json index 9456fa6..74e2d74 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,7 @@ "@typescript-eslint/semi": "off", "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/promise-function-async": "off", + "@typescript-eslint/consistent-type-assertions": "off", "@typescript-eslint/indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1, diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d83f08..e469a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] - 2024-12-18 + +### Notice +- This extension no longer supports MATLAB R2021a. To make use of the advanced features of the extension or run and debug MATLAB code, you must have MATLAB R2021b or later installed. + ### Added -- Open non-code MATLAB files (e.g. `.slx`, `.fig`) via the context menu +- Debugging support +- Support for inserting code snippets shipped with MATLAB (requires MATLAB R2025a or later) +- Support for opening additional MATLAB file types (e.g. `.slx`, `.fig`) from the Visual Studio Code context menu (Community contribution from @Gusmano-2-OSU) + +### Fixed +- Syntax highlighting improvements (Community contribution from @apozharski) +- Patches CVE-2024-21538, CVE-2024-52798, and CVE-2024-55565 ## [1.2.7] - 2024-11-07 diff --git a/README.md b/README.md index b35efb8..cf216ee 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # MATLAB extension for Visual Studio Code -This extension provides support for editing and running MATLAB® code in Visual Studio® Code and includes features such as syntax highlighting, code analysis, navigation support, and more. +This extension provides support for editing, running, and debugging MATLAB® code in Visual Studio® Code and includes features such as syntax highlighting, code analysis, navigation support, and more. -You can use this extension with or without MATLAB installed on your system. However, to make use of the advanced features of the extension or run MATLAB code, you must have MATLAB R2021a or later installed. For more information, see the [Get Started](#get-started) section. +You can use this extension with or without MATLAB installed on your system. However, to make use of the advanced features of the extension or run MATLAB code, you must have MATLAB R2021b or later installed. For more information, see the [Get Started](#get-started) section. -**Note:** This extension will no longer support MATLAB R2021a in a future release. To use advanced features or run MATLAB code, you will need to have MATLAB R2021b or later installed. - -For an overview of some of the major features of this extension, you can watch the [Introducing the New MATLAB Extension for Visual Studio Code](https://www.youtube.com/watch?v=kYTBAr9LlGg) video. +**Note:** This extension no longer supports MATLAB R2021a. To use advanced features or run MATLAB code, you must have MATLAB R2021b or later installed. ## Installation You can install the extension from within Visual Studio Code or download it from [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=MathWorks.language-matlab). After installing the extension, you might need to configure it to make full use of all the features. For more information, see the [Configuration](#configuration) section. @@ -24,7 +22,7 @@ The extension provides several basic features, regardless of whether you have MA ### Advanced Features (requires MATLAB installed on your system) If you have MATLAB R2021a or later installed on your system, you have access to an additional set of advanced features. These features include: -* Code execution +* Code execution and debugging * Automatic code completion * Source code formatting (document formatting) * Code navigation @@ -34,18 +32,21 @@ If you have MATLAB R2021a or later installed on your system, you have access to ![MATLAB Extension Demo](public/AdvancedFeatures.gif) -## Run MATLAB Code -You can run a MATLAB code file or a selection within a MATLAB code file in Visual Studio Code using the Run button at the top of the file or the `Run File` or `Run Current Selection` commands. When you run the file, output displays in the "Terminal" pane of Visual Studio Code. You also can enter MATLAB code directly in the MATLAB terminal. To stop execution of MATLAB code, press **Ctrl+C**. +## Run and Debug MATLAB Code +To run a MATLAB code file in Visual Studio Code, click the Run button at the top of the file. You also can use the `Run File` or `Run Current Selection` commands. When you run the file, output displays in the "Terminal" pane of Visual Studio Code. You also can enter MATLAB code directly in the MATLAB terminal. To stop execution of MATLAB code, press **Ctrl+C**. + +To debug a MATLAB code file, add breakpoints to the file by clicking the area to the left of an executable line. Then run the file. Visual Studio Code stops at the first breakpoint. +When Visual Studio Code is paused, you can use the **Debug toolbar** to peforms debugging actions such as **Continue**, **Step into**, and **Stop**. You also can enter commands in the MATLAB terminal to perform debug actions or change variable values. Use the **Run and Debug** view to see your workspace variables, watch points, and call stack. -![MATLAB Execution Demo](public/CodeExecution.gif) +![MATLAB Execution Demo](public/RunDebugCode.gif) ### Limitations -There are some limitations to running MATLAB code in Visual Studio Code: -* Debugging is not supported. -* The **pause** and **input** functions are not supported. +There are some limitations to running and debugging MATLAB code in Visual Studio Code: * Output from timers, callbacks, and DataQueue objects is not shown in the Command Window. * Creating a custom run configuration for a file is not supported. - +* When using the **dbstop** and **dbclear** functions to set and clear breakpoints, the breakpoints are added to file but are not shown in Visual Studio Code. +* Variable values changed in the MATLAB terminal when Visual Studio Code is paused do not update in the **Run and Debug** view until the next time Visual Studio Code pauses. +* Changing which stack frame is selected in Visual Studio Code will not automatically change the workspace used by the MATLAB terminal when debugging ## Configuration To configure the extension, go to the extension settings and select from the available options. @@ -65,6 +66,9 @@ In the extension settings, set the `matlab.installPath` setting to the value ret ![MATLAB Install Path Setting](public/InstallPathSetting.png) +### MATLAB Automatically Start Debugger Setting +By default, the extension automatically starts the Visual Studio Code debugger when MATLAB reaches a breakpoint. To disable automatically starting the Visual Studio Code debugger, set the `matlab.automaticallyStartDebugger` setting to `false`. When starting the Visual Studio Debugger is disabled, MATLAB still stops at breakpoints, and you can debug your code in the MATLAB terminal using the MATLAB debugging functions. + ### MATLAB Index Workspace Setting By default, the extension indexes all the MATLAB code files (`.m`) in your current workspace. Indexing allows the extension to find and navigate between your MATLAB code files. You can disable indexing to improve the performance of the extension. To disable indexing, set the `matlab.indexWorkspace` setting to `false`. Disabling indexing can cause features such as code navigation not to function as expected. @@ -76,22 +80,24 @@ By default, the extension starts MATLAB in the background when you open a MATLAB * `never` — Never start MATLAB. Note: Some functionality is available only with MATLAB running in the background. -### MATLAB Telemetry Setting -You can help improve the extension by sending user experience information to MathWorks®. By default, the extension sends user experience information to MathWorks. To disable sending information, set the `matlab.telemetry` setting to `false`. - -For more information, see the [MathWorks Privacy Policy](https://www.mathworks.com/company/aboutus/policies_statements.html). +### MATLAB Max File Size for Analysis Setting +By default, the extension analyzes all files, regardless of their size, for features such as linting, code navigation, and symbol renaming. To limit the maximum number of characters a file can contain, set the `matlab.maxFileSizeForAnalysis` setting. For example, to limit the number of characters to 50,000, set the `matlab.maxFileSizeForAnalysis` setting to `50000`. If a file contains more than the maximum number of characters, features such as linting, code navigation, and symbol renaming are disabled for that file. To remove the limit and analyze all files regardless of their size, set the `matlab.maxFileSizeForAnalysis` setting to `0`. ### MATLAB Show Feature Not Available Error Setting By default, the extension displays an error when a feature requires MATLAB and MATLAB is unable to start. To not display an error, set the `matlab.showFeatureNotAvailableError` setting to `false`. -### MATLAB Max File Size for Analysis Setting -By default, the extension analyzes all files, regardless of their size, for features such as linting, code navigation, and symbol renaming. To limit the maximum number of characters a file can contain, set the `matlab.maxFileSizeForAnalysis` setting. For example, to limit the number of characters to 50,000, set the `matlab.maxFileSizeForAnalysis` setting to `50000`. If a file contains more than the maximum number of characters, features such as linting, code navigation, and symbol renaming are disabled for that file. To remove the limit and analyze all files regardless of their size, set the `matlab.maxFileSizeForAnalysis` setting to `0`. - ### MATLAB Sign In Setting By default, the extension assumes that the MATLAB installation specified in the Install Path setting is activated. To enable browser-based sign in to your MathWorks account using the Online License Manager or a Network License Manager, set the `matlab.signIn` setting to true. When this setting is enabled, the extension prompts you to sign in when it starts MATLAB. +### MATLAB Telemetry Setting +You can help improve the extension by sending user experience information to MathWorks®. By default, the extension sends user experience information to MathWorks. To disable sending information, set the `matlab.telemetry` setting to `false`. + +For more information, see the [MathWorks Privacy Policy](https://www.mathworks.com/company/aboutus/policies_statements.html). + + + ## Troubleshooting If the MATLAB install path is not properly configured, you get an error when you try to use certain advanced features, such as document formatting and code navigation. @@ -102,6 +108,16 @@ We encourage all feedback. If you encounter a technical issue or have an enhance ## Release Notes +### 1.3.0 +Release date: 2024-12-18 +Added: +* Debugging support +* Support for inserting code snippets shipped with MATLAB (requires MATLAB R2025a or later) +* Support for opening additional MATLAB file types (e.g. `.slx`, `.fig`) from the Visual Studio Code context menu (Community contribution from @Gusmano-2-OSU) + +Fixed: +* Syntax highlighting improvements (Community contribution from @apozharski) + ### 1.2.0 Release date: 2024-03-05 diff --git a/package-lock.json b/package-lock.json index f92cc58..5b041f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "language-matlab", - "version": "1.2.7", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.2.7", + "version": "1.3.0", "hasInstallScript": true, "license": "MIT", "dependencies": { + "@vscode/debugadapter": "^1.56.0", "node-fetch": "^2.6.6", "vscode-languageclient": "^8.0.2" }, @@ -627,9 +628,9 @@ } }, "node_modules/@redhat-developer/locators": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@redhat-developer/locators/-/locators-1.3.0.tgz", - "integrity": "sha512-dY8vNdAxFCX0Bl1/9rcSgQKeglI2H5XyCXPMPjyo517Fi2+lPLibonRdZS7imrwVCRkkXShf0A4qK1sGi7PI6g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/locators/-/locators-1.4.1.tgz", + "integrity": "sha512-cKmUlYpPRMp4rPGUXm1HYX/6UWV5LFqfBkAcQmZdlmvvzOY7y8yIc/JnWmH1+z4dWjEjtk1NTi24QCY7khH+Lg==", "dev": true, "peerDependencies": { "@redhat-developer/page-objects": ">=1.0.0", @@ -637,9 +638,9 @@ } }, "node_modules/@redhat-developer/page-objects": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@redhat-developer/page-objects/-/page-objects-1.3.0.tgz", - "integrity": "sha512-sTpiGyEnNRvJtZpJWgbz/K3qVWdqv67/KJhIQdRCOUbPuqYfk3ouF/Lxu+qVvHLhlPbgUPd6hz2Jtw1XyqSSZA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/page-objects/-/page-objects-1.4.1.tgz", + "integrity": "sha512-F5FxAmw+0lEkxIyAMXewCa0PCZymvqZXgN8r3/dQE7nfEeWXHcjwotl9Z0LW0eXF4f5Vi6XjSwjgtGD5e4qwYw==", "dev": true, "dependencies": { "clipboardy": "^4.0.0", @@ -748,11 +749,12 @@ } }, "node_modules/@types/selenium-webdriver": { - "version": "4.1.22", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.22.tgz", - "integrity": "sha512-MCL4l7q8dwxejr2Q2NXLyNwHWMPdlWE0Kpn6fFwJtvkJF7PTkG5jkvbH/X1IAAQxgt/L1dA8u2GtDeekvSKvOA==", + "version": "4.1.26", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.26.tgz", + "integrity": "sha512-PUgqsyNffal0eAU0bzGlh37MJo558aporAPZoKqBeB/pF7zhKl1S3zqza0GpwFqgoigNxWhEIJzru75eeYco/w==", "dev": true, "dependencies": { + "@types/node": "*", "@types/ws": "*" } }, @@ -769,9 +771,9 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -965,6 +967,22 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vscode/debugadapter": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.67.0.tgz", + "integrity": "sha512-AzCiKbRdOheHFaTJ2BVdfGuREIOhNEtMUVDv4lJqIPCfZ/60tXELvVSOFGG+6hNMiMaSrAcXP9sPSlZLZEsmLA==", + "dependencies": { + "@vscode/debugprotocol": "1.67.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vscode/debugprotocol": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.67.0.tgz", + "integrity": "sha512-vTn5JwZ+LQy2QqT/wUD8Rlrb+7eLo5fsiKIxD5i0BZIuvdRbxTTfGU7+47PsorMrfBzozngIrocKCKS3OVnYyw==" + }, "node_modules/@vscode/test-electron": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", @@ -1351,9 +1369,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -2093,9 +2111,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -2135,12 +2153,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2265,9 +2283,9 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -4803,32 +4821,31 @@ "optional": true }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -4836,48 +4853,21 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -4886,21 +4876,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -4917,9 +4892,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/mute-stream": { @@ -4928,18 +4903,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -5793,15 +5756,15 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.23.0.tgz", - "integrity": "sha512-DdvtInpnMt95Td8VApvmAw7oSydBD9twIRXqoMyRoGMvL1dAnMFxdrwnW6L0d/pF/uoNTjbVUarwGZ9wIGNStA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.24.0.tgz", + "integrity": "sha512-qrqoHhHPZuKLiz5VAQUxrn3ILs7/cYqn2/x96r35g5JjkLUjOS1lX+F+tEJKhRMlQ/MGJ+N1016JF5g2xZUFzw==", "dev": true, "dependencies": { "@bazel/runfiles": "^5.8.1", "jszip": "^3.10.1", "tmp": "^0.2.3", - "ws": "^8.17.1" + "ws": "^8.18.0" }, "engines": { "node": ">= 14.21.0" @@ -5822,9 +5785,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -6617,9 +6580,9 @@ "dev": true }, "node_modules/utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true }, "node_modules/util-deprecate": { @@ -6975,9 +6938,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -7093,9 +7056,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" @@ -7619,16 +7582,16 @@ "optional": true }, "@redhat-developer/locators": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@redhat-developer/locators/-/locators-1.3.0.tgz", - "integrity": "sha512-dY8vNdAxFCX0Bl1/9rcSgQKeglI2H5XyCXPMPjyo517Fi2+lPLibonRdZS7imrwVCRkkXShf0A4qK1sGi7PI6g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/locators/-/locators-1.4.1.tgz", + "integrity": "sha512-cKmUlYpPRMp4rPGUXm1HYX/6UWV5LFqfBkAcQmZdlmvvzOY7y8yIc/JnWmH1+z4dWjEjtk1NTi24QCY7khH+Lg==", "dev": true, "requires": {} }, "@redhat-developer/page-objects": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@redhat-developer/page-objects/-/page-objects-1.3.0.tgz", - "integrity": "sha512-sTpiGyEnNRvJtZpJWgbz/K3qVWdqv67/KJhIQdRCOUbPuqYfk3ouF/Lxu+qVvHLhlPbgUPd6hz2Jtw1XyqSSZA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/page-objects/-/page-objects-1.4.1.tgz", + "integrity": "sha512-F5FxAmw+0lEkxIyAMXewCa0PCZymvqZXgN8r3/dQE7nfEeWXHcjwotl9Z0LW0eXF4f5Vi6XjSwjgtGD5e4qwYw==", "dev": true, "requires": { "clipboardy": "^4.0.0", @@ -7721,11 +7684,12 @@ } }, "@types/selenium-webdriver": { - "version": "4.1.22", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.22.tgz", - "integrity": "sha512-MCL4l7q8dwxejr2Q2NXLyNwHWMPdlWE0Kpn6fFwJtvkJF7PTkG5jkvbH/X1IAAQxgt/L1dA8u2GtDeekvSKvOA==", + "version": "4.1.26", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.26.tgz", + "integrity": "sha512-PUgqsyNffal0eAU0bzGlh37MJo558aporAPZoKqBeB/pF7zhKl1S3zqza0GpwFqgoigNxWhEIJzru75eeYco/w==", "dev": true, "requires": { + "@types/node": "*", "@types/ws": "*" } }, @@ -7742,9 +7706,9 @@ "dev": true }, "@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "requires": { "@types/node": "*" @@ -7849,6 +7813,19 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@vscode/debugadapter": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.67.0.tgz", + "integrity": "sha512-AzCiKbRdOheHFaTJ2BVdfGuREIOhNEtMUVDv4lJqIPCfZ/60tXELvVSOFGG+6hNMiMaSrAcXP9sPSlZLZEsmLA==", + "requires": { + "@vscode/debugprotocol": "1.67.0" + } + }, + "@vscode/debugprotocol": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.67.0.tgz", + "integrity": "sha512-vTn5JwZ+LQy2QqT/wUD8Rlrb+7eLo5fsiKIxD5i0BZIuvdRbxTTfGU7+47PsorMrfBzozngIrocKCKS3OVnYyw==" + }, "@vscode/test-electron": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.8.tgz", @@ -8122,9 +8099,9 @@ } }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-regex": { @@ -8673,9 +8650,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -8703,12 +8680,12 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -8786,9 +8763,9 @@ "optional": true }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, "dir-glob": { @@ -10638,85 +10615,51 @@ "optional": true }, "mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "dependencies": { - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - } } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -10729,9 +10672,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "mute-stream": { @@ -10740,12 +10683,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -11363,15 +11300,15 @@ "dev": true }, "selenium-webdriver": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.23.0.tgz", - "integrity": "sha512-DdvtInpnMt95Td8VApvmAw7oSydBD9twIRXqoMyRoGMvL1dAnMFxdrwnW6L0d/pF/uoNTjbVUarwGZ9wIGNStA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.24.0.tgz", + "integrity": "sha512-qrqoHhHPZuKLiz5VAQUxrn3ILs7/cYqn2/x96r35g5JjkLUjOS1lX+F+tEJKhRMlQ/MGJ+N1016JF5g2xZUFzw==", "dev": true, "requires": { "@bazel/runfiles": "^5.8.1", "jszip": "^3.10.1", "tmp": "^0.2.3", - "ws": "^8.17.1" + "ws": "^8.18.0" } }, "semver": { @@ -11383,9 +11320,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -11979,9 +11916,9 @@ "dev": true }, "utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true }, "util-deprecate": { @@ -12245,9 +12182,9 @@ } }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "wrap-ansi": { @@ -12324,9 +12261,9 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, "yargs-unparser": { diff --git a/package.json b/package.json index 532e8f9..94e6c53 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Edit MATLAB code with syntax highlighting, linting, navigation support, and more", "icon": "public/L-Membrane_RGB_128x128.png", "license": "MIT", - "version": "1.2.7", + "version": "1.3.0", "engines": { "vscode": "^1.67.0" }, @@ -17,9 +17,12 @@ }, "categories": [ "Programming Languages", - "Snippets" + "Snippets", + "Debuggers" ], "activationEvents": [ + "onDebugResolve:matlab", + "onDebugDynamicConfigurations:matlab", "onLanguage:matlab", "onTerminalProfile:matlab.terminal-profile" ], @@ -33,12 +36,14 @@ { "command": "matlab.runFile", "title": "MATLAB: Run File", - "icon": "$(play)" + "icon": "$(play)", + "when": "!matlab.isDebugging" }, { "command": "matlab.runSelection", "title": "MATLAB: Run Current Selection", - "icon": "$(play)" + "icon": "$(play)", + "when": "!matlab.isDebugging" }, { "command": "matlab.openCommandWindow", @@ -78,18 +83,32 @@ { "command": "matlab.runFile", "key": "f5", - "when": "editorTextFocus && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed && resourceScheme != 'untitled'" + "when": "editorTextFocus && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed && resourceScheme != 'untitled' && !matlab.isDebugging" }, { "command": "matlab.runSelection", "key": "shift+enter", - "when": "editorTextFocus && editorHasSelection && !editorHasMultipleSelections && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed" + "when": "editorTextFocus && editorHasSelection && !editorHasMultipleSelections && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed && !matlab.isDebugging" }, { "command": "matlab.interrupt", "key": "Ctrl+C", "mac": "Cmd+C", - "when": "((editorTextFocus && !editorHasSelection && editorLangId == matlab) || (terminalFocus && matlab.isActiveTerminal && !matlab.terminalHasSelection && !terminalTextSelectedInFocused)) " + "when": "((editorTextFocus && !editorHasSelection && editorLangId == matlab) || (terminalFocus && matlab.isActiveTerminal && !matlab.terminalHasSelection && !terminalTextSelectedInFocused))" + } + ], + "breakpoints": [ + { + "language": "matlab" + } + ], + "debuggers": [ + { + "type": "matlab", + "languages": [ + "matlab" + ], + "label": "MATLAB Label" } ], "menus": { @@ -110,24 +129,24 @@ "editor/title/run": [ { "command": "matlab.runFile", - "when": "editorLangId == matlab && resourceScheme != 'untitled'", + "when": "editorLangId == matlab && resourceScheme != 'untitled' && !matlab.isDebugging", "group": "1_run" }, { "command": "matlab.runSelection", - "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections", + "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections && !matlab.isDebugging", "group": "2_run" } ], "editor/context": [ { "command": "matlab.runFile", - "when": "editorLangId == matlab && resourceScheme != 'untitled'", + "when": "editorLangId == matlab && resourceScheme != 'untitled' && !matlab.isDebugging", "group": "1_run" }, { "command": "matlab.runSelection", - "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections", + "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections && !matlab.isDebugging", "group": "1_run" } ], @@ -193,6 +212,12 @@ "description": "Automatically index all MATLAB code files (.m) in the current workspace.", "scope": "window" }, + "MATLAB.startDebuggerAutomatically": { + "type": "boolean", + "default": true, + "markdownDescription": "Automatically start the Visual Studio Code debugger when MATLAB reaches a breakpoint.", + "scope": "window" + }, "MATLAB.telemetry": { "type": "boolean", "default": true, @@ -301,6 +326,7 @@ }, "dependencies": { "node-fetch": "^2.6.6", - "vscode-languageclient": "^8.0.2" + "vscode-languageclient": "^8.0.2", + "@vscode/debugadapter": "^1.56.0" } } diff --git a/public/RunDebugCode.gif b/public/RunDebugCode.gif new file mode 100644 index 0000000..ae4110c Binary files /dev/null and b/public/RunDebugCode.gif differ diff --git a/server b/server index bf34646..a9b0cf5 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit bf34646e9afbb21a45db1977c23eaa7a768ecd76 +Subproject commit a9b0cf578f12070fb01ab3950fd31233b8bae407 diff --git a/snippets/matlab.json b/snippets/matlab.json index b953053..dfaa9c4 100644 --- a/snippets/matlab.json +++ b/snippets/matlab.json @@ -2,29 +2,29 @@ "For Loop": { "prefix": "for", "body": [ - "for ${1:index} = ${2:values}", + "for ${1:index} = ${2:initVal}:${3:endVal}", "\t$0", "end" ], - "description": "for loop to repeat specified number of times" + "description": "For loop to repeat specified number of times" }, "Parallel For Loop": { - "prefix": "parfor", + "prefix": ["parfor", "for"], "body": [ - "parfor ${1:index} = ${2:values}", + "parfor ${1:index} = ${2:initVal}:${3:endVal}", "\t$0", "end" ], "description": "Parallel for loop (requires Parallel Computing Toolbox)" }, - "Execute Parallel Code": { + "SPMD block": { "prefix": "spmd", "body": [ "spmd", "\t$0", "end" ], - "description": "Execute code in parallel on workers of parallel pool (requires Parallel Computing Toolbox)" + "description": "Execute code block in parallel on MATLAB workers (requires Parallel Computing Toolbox)" }, "If Statement": { "prefix": "if", @@ -53,7 +53,7 @@ "\t$0", "end" ], - "description": "while loop to repeat when condition is true" + "description": "While loop to repeat when condition is true" }, "Try-Catch Statement": { "prefix": "try", @@ -97,6 +97,8 @@ "", "\tmethods", "\t\tfunction obj = $1(${3:inputArg1})", + "\t\t\tobj.$2 = $3^2;", + "\t\t\tdisp(obj.$2);", "\t\t\t$0", "\t\tend", "\tend", diff --git a/src/Notifications.ts b/src/Notifications.ts index 6ecaef6..99dbba3 100644 --- a/src/Notifications.ts +++ b/src/Notifications.ts @@ -17,17 +17,28 @@ enum Notification { MatlabRequestInstance = 'matlab/request', MVMEvalRequest = 'evalRequest', - MVMEvalComplete = 'evalRequest', + MVMEvalComplete = 'evalResponse', MVMFevalRequest = 'fevalRequest', - MVMFevalComplete = 'fevalRequest', + MVMFevalComplete = 'fevalResponse', + MVMSetBreakpointRequest = 'setBreakpointRequest', + MVMSetBreakpointComplete = 'setBreakpointResponse', + MVMClearBreakpointRequest = 'clearBreakpointRequest', + MVMClearBreakpointComplete = 'clearBreakpointResponse', MVMText = 'text', MVMClc = 'clc', + MVMPromptChange = 'mvmPromptChange', MVMInterruptRequest = 'interruptRequest', + MVMUnpauseRequest = 'unpauseRequest', MVMStateChange = 'mvmStateChange', + DebuggingStateChange = 'DebuggingStateChange', + DebugAdaptorRequest = 'DebugAdaptorRequest', + DebugAdaptorResponse = 'DebugAdaptorResponse', + DebugAdaptorEvent = 'DebugAdaptorEvent', + // Telemetry LogTelemetryData = 'telemetry/logdata', diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index a74789c..9e63b89 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -1,8 +1,8 @@ // Copyright 2024 The MathWorks, Inc. import * as vscode from 'vscode' -import MVM, { MatlabState } from './MVM' -import { TextEvent } from './MVMInterface' +import { MVM, MatlabState } from './MVM' +import { TextEvent, PromptState } from './MVMInterface' /** * Direction of cursor movement @@ -68,7 +68,10 @@ const ACTION_KEYS = { }; const PROMPTS = { - IDLE_PROMPT: '>> ' + IDLE_PROMPT: '>> ', + DEBUG_PROMPT: 'K>> ', + FAKE_INPUT_PROMPT: '? ', + BUSY_PROMPT: '' }; /** @@ -79,30 +82,29 @@ export default class CommandWindow implements vscode.Pseudoterminal { private readonly _writeEmitter: vscode.EventEmitter; private _initialized: boolean = false; - private _isBusy: boolean = false; - private readonly _currentPrompt = PROMPTS.IDLE_PROMPT; + private _currentPrompt = PROMPTS.IDLE_PROMPT; + private _currentState: PromptState = PromptState.INITIALIZING; - private _currentLine: string = this._currentPrompt; + private _currentPromptLine: string = this._currentPrompt; private _cursorIndex: number = 0; private _anchorIndex?: number = undefined; + private _lastOutputLine: string = ''; + private readonly _commandHistory: string[] = []; private _historyIndex: number = 0; private _lastKnownCurrentLine: string = ''; - private _isLineDirty: boolean = false; - private _terminalDimensions: vscode.TerminalDimensions; private _lastSentTerminalDimensions: vscode.TerminalDimensions | null = null; - private readonly _inputQueue: string[] = []; - - private _justTypedLastInRow: boolean = false; + private _justTypedLastInColumn: boolean = false; constructor (mvm: MVM) { this._mvm = mvm; - this._mvm.onOutput = this.addOutput.bind(this); - this._mvm.onClc = this.clear.bind(this); + this._mvm.on(MVM.Events.output, this.addOutput.bind(this)); + this._mvm.on(MVM.Events.clc, this.clear.bind(this)); + this._mvm.on(MVM.Events.promptChange, this._handlePromptChange.bind(this)); this._initialized = false; @@ -110,7 +112,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { this.onDidWrite = this._writeEmitter.event; this._terminalDimensions = { rows: 30, columns: 100 }; - this._mvm.addStateChangeListener(this._handleMatlabStateChange.bind(this)); + this._mvm.on(MVM.Events.stateChanged, this._handleMatlabStateChange.bind(this)); this._updateHasSelectionContext(); } @@ -130,16 +132,14 @@ export default class CommandWindow implements vscode.Pseudoterminal { const currentMatlabState = this._mvm.getMatlabState(); if (currentMatlabState === MatlabState.READY) { - this._isBusy = false; this._initialized = true; - this._writeCurrentLine(); + this._writeCurrentPromptLine(); } else if (currentMatlabState === MatlabState.DISCONNECTED) { this._clearState(); - this._isBusy = false; this._initialized = false; + this._currentState = PromptState.INITIALIZING; } else if (currentMatlabState === MatlabState.BUSY) { this._clearState(); - this._isBusy = true; this._initialized = true; } } @@ -164,16 +164,13 @@ export default class CommandWindow implements vscode.Pseudoterminal { if (newState === MatlabState.READY) { this._clearState(); - this._isBusy = false; this._initialized = true; - this._writeCurrentLine(); + this._writeCurrentPromptLine(); } else if (newState === MatlabState.DISCONNECTED) { this._clearState(); - this._isBusy = false; this._initialized = false; } else if (newState === MatlabState.BUSY) { this._clearState(); - this._isBusy = true; this._initialized = true; } } @@ -182,11 +179,12 @@ export default class CommandWindow implements vscode.Pseudoterminal { * Clear current line and selection */ private _setToEmptyPrompt (): void { - this._currentLine = this._currentPrompt; - this._lastKnownCurrentLine = this._currentLine; + this._currentPromptLine = this._currentPrompt; + this._lastKnownCurrentLine = ''; this._cursorIndex = 0; this._anchorIndex = undefined; this._updateHasSelectionContext(); + this._updateWhetherJustTypedInLastColumn(); } /** @@ -194,11 +192,11 @@ export default class CommandWindow implements vscode.Pseudoterminal { * @param command */ insertCommandForEval (command: string): void { - if (this._currentLine !== this._currentPrompt) { + if (this._currentPromptLine !== this._currentPrompt) { this._setToEmptyPrompt(); - this._writeCurrentLine(); + this._writeCurrentPromptLine(); } - // TODO: handle partially enter commands when run is hit + this.handleInput(command + ACTION_KEYS.NEWLINE); } @@ -208,26 +206,16 @@ export default class CommandWindow implements vscode.Pseudoterminal { * @returns */ handleInput (data: string): void { - if (!this._initialized) { + if (!this._initialized || this._currentState === PromptState.INITIALIZING) { return; } - this._inputQueue.push(data); - this._processQueueUntilBusy(); - } - - private _processQueueItem (): void { - const nextItem = this._inputQueue.shift(); - if (nextItem === undefined) { + if (this._currentState === PromptState.PAUSE) { + this._mvm.unpause(); return; } - this.handleText(nextItem, false); - } - private _processQueueUntilBusy (): void { - while (this._inputQueue.length > 0 && !this._isBusy) { - this._processQueueItem(); - } + this.handleText(data, false); } /** @@ -237,130 +225,144 @@ export default class CommandWindow implements vscode.Pseudoterminal { * @returns */ handleText (data: string, isOutput: boolean): void { - if (data.startsWith(ESC)) { - /* eslint-disable-next-line no-control-regex */ - const match = data.match(/^\x1b\[(?[0-9]+);(?[0-9]+)R$/) - if (match !== null && 'groups' in match && (match.groups != null) && 'row' in match.groups && 'col' in match.groups) { + if (this._isSpecialKey(data)) { + // For now, disallow output from containing control characters. + if (isOutput) { return; } - switch (data) { - case ACTION_KEYS.LEFT: - this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.MOVE); - break; - case ACTION_KEYS.RIGHT: - this._handleLeftRight(CursorDirection.RIGHT, AnchorPolicy.MOVE); - break; - case ACTION_KEYS.SHIFT_LEFT: - this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.KEEP); - break; - case ACTION_KEYS.SHIFT_RIGHT: - this._handleLeftRight(CursorDirection.RIGHT, AnchorPolicy.KEEP); - break; - case ACTION_KEYS.END: - this._handleEnd(AnchorPolicy.MOVE); - break; - case ACTION_KEYS.SHIFT_END: - this._handleEnd(AnchorPolicy.KEEP); - break; - case ACTION_KEYS.HOME: - this._handleHome(AnchorPolicy.MOVE); - break; - case ACTION_KEYS.SHIFT_HOME: - this._handleHome(AnchorPolicy.KEEP); - break; - case ACTION_KEYS.DELETE: - this._handleDelete(); - break; - case ACTION_KEYS.UP: - this._handleNavigateHistory(HistoryDirection.BACKWARDS); - break; - case ACTION_KEYS.DOWN: - this._handleNavigateHistory(HistoryDirection.FORWARDS); - break; - case ACTION_KEYS.ESCAPE: - this._handleEscape(); - } - - if (this._isLineDirty) { - this._writeCurrentLine(); + if (this._handleActionKeys(data)) { + this._writeCurrentPromptLine(); } return; } - switch (data) { - case ACTION_KEYS.BACKSPACE: - case ACTION_KEYS.BACKSPACE_ALTERNATIVE: - this._handleBackspace(); - return; - case ACTION_KEYS.SELECT_ALL: - this._handleSelectAll(); - return; - case ACTION_KEYS.COPY: - this._handleCopy(); - return; - case ACTION_KEYS.PASTE: - this._handlePaste(); - return; - } - if (data.length === 1 && data.charCodeAt(0) < ' '.charCodeAt(0) && data !== '\r' && data !== '\n') { return; } const lines = this._preprocessInputLines(data); - - // Case 1: Normal typing - if (lines.length === 1) { - this._handleLine(lines[0]); - - // Case 2: Normal typing followed by an enter. - } else if (lines.length === 2 && lines[1].length === 0) { - this._handleLine(lines[0]); - if (isOutput) { - this._handleOutputEnter(); - } else { - this._handleEnter(); - } - // Case 3: Multi-line input (ie, from pasting, etc) - } else { + if (isOutput) { for (let i = 0; i < lines.length; i++) { - this._handleLine(lines[i] + (i === lines.length - 1 ? '' : ACTION_KEYS.NEWLINE)); + this._handleOutputLine(lines[i], i !== lines.length - 1); } - if (isOutput) { - this._handleOutputEnter(); + } else { + // Case 1: Normal typing + if (lines.length === 1) { + this._handleLine(lines[0]); + + // Case 2: Normal typing followed by an enter. + } else if (lines.length === 2 && lines[1].length === 0) { + this._handleLine(lines[0]); + this._handleEnter(); + // Case 3: Multi-line input (ie, from pasting, etc) } else { + for (let i = 0; i < lines.length; i++) { + this._handleLine(lines[i] + ((i === lines.length - 1) ? '' : ACTION_KEYS.NEWLINE)); + } this._handleEnter(); } } } + private _handleOutputLine (line: string, implicitNewlineAtEnd: boolean): void { + const numberOfLinesBehind = Math.floor(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); + if (numberOfLinesBehind !== 0) { + this._writeEmitter.fire(ACTION_KEYS.UP.repeat(numberOfLinesBehind)); + } + + if (this._lastOutputLine.length !== 0) { + this._writeEmitter.fire(ACTION_KEYS.UP); + } + + this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING); + + this._lastOutputLine += line; + this._writeEmitter.fire(this._lastOutputLine); + if (implicitNewlineAtEnd) { + this._handleOutputNewline(); + } + + if (this._lastOutputLine.length !== 0) { + this._writeEmitter.fire(ACTION_KEYS.NEWLINE) + } + + this._writeCurrentPromptLine(false); + } + + private _handleOutputNewline (): void { + this._writeEmitter.fire(ACTION_KEYS.NEWLINE); + this._lastOutputLine = ''; + } + + private _isSpecialKey (data: string): boolean { + return data.startsWith(ESC) || Object.values(ACTION_KEYS).includes(data) + } + + private _handleActionKeys (keyCode: string): boolean { + switch (keyCode) { + case ACTION_KEYS.LEFT: + return this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.MOVE); + case ACTION_KEYS.RIGHT: + return this._handleLeftRight(CursorDirection.RIGHT, AnchorPolicy.MOVE); + case ACTION_KEYS.SHIFT_LEFT: + return this._handleLeftRight(CursorDirection.LEFT, AnchorPolicy.KEEP); + case ACTION_KEYS.SHIFT_RIGHT: + return this._handleLeftRight(CursorDirection.RIGHT, AnchorPolicy.KEEP); + case ACTION_KEYS.END: + return this._handleEnd(AnchorPolicy.MOVE); + case ACTION_KEYS.SHIFT_END: + return this._handleEnd(AnchorPolicy.KEEP); + case ACTION_KEYS.HOME: + return this._handleHome(AnchorPolicy.MOVE); + case ACTION_KEYS.SHIFT_HOME: + return this._handleHome(AnchorPolicy.KEEP); + case ACTION_KEYS.DELETE: + return this._handleDelete(); + case ACTION_KEYS.UP: + return this._handleNavigateHistory(HistoryDirection.BACKWARDS); + case ACTION_KEYS.DOWN: + return this._handleNavigateHistory(HistoryDirection.FORWARDS); + case ACTION_KEYS.ESCAPE: + return this._handleEscape(); + case ACTION_KEYS.BACKSPACE: + case ACTION_KEYS.BACKSPACE_ALTERNATIVE: + return this._handleBackspace(); + case ACTION_KEYS.SELECT_ALL: + return this._handleSelectAll(); + case ACTION_KEYS.COPY: + return this._handleCopy(); + case ACTION_KEYS.PASTE: + return this._handlePaste(); + default: + return false; + } + } + private _preprocessInputLines (data: string): string[] { data = data.replace(/\r\n?/g, '\n'); const lines = data.split('\n'); return lines; } - private _handleNavigateHistory (direction: HistoryDirection): void { + private _handleNavigateHistory (direction: HistoryDirection): boolean { const isCurrentlyAtEndOfHistory = this._historyIndex === this._commandHistory.length; const isCurrentlyAtBeginningOfHistory = this._historyIndex === 0; if (direction === HistoryDirection.BACKWARDS && isCurrentlyAtBeginningOfHistory) { - return; + return false; } if (direction === HistoryDirection.FORWARDS && isCurrentlyAtEndOfHistory) { - return; + return false; } if (isCurrentlyAtEndOfHistory) { - this._lastKnownCurrentLine = this._currentLine; + this._lastKnownCurrentLine = this._stripCurrentPrompt(this._currentPromptLine); } this._historyIndex += direction === HistoryDirection.BACKWARDS ? -1 : 1; - this._replaceCurrentLineWithNewLine(this._getHistoryItem(this._historyIndex)) - - this._justTypedLastInRow = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; + return this._replaceCurrentLineWithNewLine(this._currentPrompt + this._getHistoryItem(this._historyIndex)); } private _markCurrentLineChanged (): void { @@ -368,38 +370,43 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._lastKnownCurrentLine = ''; } - private _possiblyUpdateAnchor (policy: AnchorPolicy): void { + private _possiblyUpdateAnchorForCursorChange (policy: AnchorPolicy): boolean { + let isLineDirty = false; if (policy === AnchorPolicy.MOVE && this._anchorIndex !== undefined) { this._anchorIndex = undefined; - this._isLineDirty = true; + isLineDirty = true; } else if (policy === AnchorPolicy.KEEP) { if (this._anchorIndex === undefined) { this._anchorIndex = this._cursorIndex; } - this._isLineDirty = true; + isLineDirty = true; } this._updateHasSelectionContext(); + return isLineDirty; } - private _handleEnd (anchorPolicy: AnchorPolicy): void { + private _handleEnd (anchorPolicy: AnchorPolicy): boolean { const currentCursorLine = Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); - this._possiblyUpdateAnchor(anchorPolicy); + const isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); this._cursorIndex = this._getMaxIndexOnLine(); this._moveCursorToCurrent(currentCursorLine); + return isLineDirty; } - private _handleHome (anchorPolicy: AnchorPolicy): void { + private _handleHome (anchorPolicy: AnchorPolicy): boolean { const currentCursorLine = Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); - this._possiblyUpdateAnchor(anchorPolicy); + const isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); this._cursorIndex = 0; this._moveCursorToCurrent(currentCursorLine); + return isLineDirty; } - private _handleLeftRight (direction: CursorDirection, anchorPolicy: AnchorPolicy): void { + private _handleLeftRight (direction: CursorDirection, anchorPolicy: AnchorPolicy): boolean { + let isLineDirty = false; if (direction === CursorDirection.LEFT && this._cursorIndex !== 0) { - if (this._justTypedLastInRow) { + if (this._justTypedLastInColumn) { // Don't actually move the cursor, but do move the index we think the cursor is at. - this._justTypedLastInRow = false; + this._justTypedLastInColumn = false; } else { if (this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0) { this._writeEmitter.fire(ACTION_KEYS.UP + ACTION_KEYS.MOVE_TO_POSITION_IN_LINE(this._terminalDimensions.columns)); @@ -408,12 +415,12 @@ export default class CommandWindow implements vscode.Pseudoterminal { } } - this._possiblyUpdateAnchor(anchorPolicy); + isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); this._cursorIndex--; } if (direction === CursorDirection.RIGHT && this._cursorIndex !== this._getMaxIndexOnLine()) { - if (this._justTypedLastInRow) { + if (this._justTypedLastInColumn) { // Not possible } else { if (this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === (this._terminalDimensions.columns - 1)) { @@ -423,165 +430,158 @@ export default class CommandWindow implements vscode.Pseudoterminal { } } - this._possiblyUpdateAnchor(anchorPolicy); + isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); this._cursorIndex++; } + + return isLineDirty; } private _getMaxIndexOnLine (): number { - return this._currentLine.length - this._currentPrompt.length; + return this._currentPromptLine.length - this._currentPrompt.length; } private _getAbsoluteIndexOnLine (index: number): number { return index + this._currentPrompt.length; } - private _handleBackspace (): void { + private _handleBackspace (): boolean { if (this._anchorIndex !== undefined) { - this._removeSelection(); - if (this._isLineDirty) { - this._markCurrentLineChanged(); - this._writeCurrentLine(); - } - return; + return this._removeSelection(); } if (this._cursorIndex === 0) { - return; + return false; } - const before = this._currentLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex) - 1); - const after = this._currentLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); - this._currentLine = before + after; + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex) - 1); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); + this._currentPromptLine = before + after; this._cursorIndex--; this._markCurrentLineChanged(); - this._writeCurrentLine(); + return true; } - private _handleSelectAll (): void { + private _handleSelectAll (): boolean { this._cursorIndex = this._getMaxIndexOnLine(); this._anchorIndex = 0; this._updateHasSelectionContext(); - this._writeCurrentLine(); + return true; } - private _handleDelete (): void { + private _handleDelete (): boolean { if (this._anchorIndex !== undefined) { - this._removeSelection(); - if (this._isLineDirty) { - this._markCurrentLineChanged(); - this._writeCurrentLine(); - } - return; + return this._removeSelection(); } if (this._cursorIndex === this._getMaxIndexOnLine()) { - return; + return false; } - const before = this._currentLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); - const after = this._currentLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex) + 1); - this._currentLine = before + after; + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex) + 1); + this._currentPromptLine = before + after; this._markCurrentLineChanged(); - this._writeCurrentLine(); + return true; } - private _writeCurrentLine (): void { - const numberOfLinesBehind = Math.floor(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); - if (numberOfLinesBehind !== 0) { - this._writeEmitter.fire(ACTION_KEYS.UP.repeat(numberOfLinesBehind)) + private _writeCurrentPromptLine (eraseExisting: boolean = true): void { + if (eraseExisting) { + this._eraseExistingPromptLine(); } - this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING) if (this._anchorIndex === undefined) { - this._writeEmitter.fire(this._currentLine) + this._writeEmitter.fire(this._currentPromptLine) } else { const selectionStart = this._currentPrompt.length + Math.min(this._cursorIndex, this._anchorIndex); const selectionEnd = this._currentPrompt.length + Math.max(this._cursorIndex, this._anchorIndex); - const preSelection = this._currentLine.slice(0, selectionStart); - const selection = this._currentLine.slice(selectionStart, selectionEnd); - const postSelection = this._currentLine.slice(selectionEnd); + const preSelection = this._currentPromptLine.slice(0, selectionStart); + const selection = this._currentPromptLine.slice(selectionStart, selectionEnd); + const postSelection = this._currentPromptLine.slice(selectionEnd); this._writeEmitter.fire(preSelection); this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); this._writeEmitter.fire(selection); this._writeEmitter.fire(ACTION_KEYS.RESTORE_COLORS); this._writeEmitter.fire(postSelection); } - const currentCursorLine = Math.ceil(this._currentLine.length / this._terminalDimensions.columns); + const currentCursorLine = Math.ceil(this._currentPromptLine.length / this._terminalDimensions.columns); this._moveCursorToCurrent(currentCursorLine); - this._isLineDirty = false; } - private _replaceCurrentLineWithNewLine (updatedLine: string): void { - this._currentLine = updatedLine; + private _eraseExistingPromptLine (): void { + const numberOfLinesBehind = Math.floor(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); + if (numberOfLinesBehind !== 0) { + this._writeEmitter.fire(ACTION_KEYS.UP.repeat(numberOfLinesBehind)) + } + this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING) + } + + private _replaceCurrentLineWithNewLine (updatedLine: string): boolean { + this._eraseExistingPromptLine(); + this._currentPromptLine = updatedLine; this._cursorIndex = this._getMaxIndexOnLine(); this._anchorIndex = undefined; - this._writeCurrentLine(); + this._updateWhetherJustTypedInLastColumn(); + this._writeCurrentPromptLine(false); + return false; } - private _removeSelection (): void { + private _removeSelection (): boolean { if (this._anchorIndex === undefined || this._cursorIndex === this._anchorIndex) { this._anchorIndex = undefined; this._updateHasSelectionContext(); - return; + return false; } const selectionStart = this._getAbsoluteIndexOnLine(Math.min(this._cursorIndex, this._anchorIndex)); const selectionEnd = this._getAbsoluteIndexOnLine(Math.max(this._cursorIndex, this._anchorIndex)); - const preSelection = this._currentLine.slice(0, selectionStart); - const postSelection = this._currentLine.slice(selectionEnd); - this._currentLine = preSelection + postSelection; + const preSelection = this._currentPromptLine.slice(0, selectionStart); + const postSelection = this._currentPromptLine.slice(selectionEnd); + this._currentPromptLine = preSelection + postSelection; this._cursorIndex = selectionStart - this._currentPrompt.length; this._anchorIndex = undefined; - this._isLineDirty = true; this._updateHasSelectionContext(); + return true; } private _handleLine (line: string): void { - this._removeSelection(); - if (this._isLineDirty) { - this._writeCurrentLine(); + if (this._removeSelection()) { + this._writeCurrentPromptLine(); } if (this._cursorIndex === this._getMaxIndexOnLine()) { - this._currentLine += line; + this._currentPromptLine += line; this._cursorIndex += line.length; this._writeEmitter.fire(line); } else { - const before = this._currentLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); - const after = this._currentLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); - this._currentLine = before + line + after; + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); + this._currentPromptLine = before + line + after; this._cursorIndex += line.length; - this._isLineDirty = true; - this._writeCurrentLine(); + this._writeCurrentPromptLine(); } this._markCurrentLineChanged(); - this._justTypedLastInRow = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; - } - - private _handleOutputEnter (): void { - this._handleEnd(AnchorPolicy.MOVE); - this._writeEmitter.fire(ACTION_KEYS.NEWLINE); + this._justTypedLastInColumn = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; } private _handleEnter (): void { - const stringToEvaluate = this._currentLine.substring(this._getAbsoluteIndexOnLine(0), this._getAbsoluteIndexOnLine(this._getMaxIndexOnLine())).trim(); - this._addToHistory(this._currentLine); + const stringToEvaluate = this._stripCurrentPrompt(this._currentPromptLine).trim(); + this._addToHistory(stringToEvaluate); this._handleEnd(AnchorPolicy.MOVE); this._writeEmitter.fire(ACTION_KEYS.NEWLINE); - this._setToEmptyPrompt(); - this._isBusy = true; - this._evaluateCommand(stringToEvaluate).then(() => { - this._setToEmptyPrompt(); - this._writeCurrentLine(); - this._justTypedLastInRow = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; - this._isBusy = false; - this._processQueueUntilBusy(); - }, () => { - // Ignored - }) + + this._lastOutputLine = ''; + this._currentPromptLine = this._currentPrompt; + this._justTypedLastInColumn = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; + this._cursorIndex = 0; + this._anchorIndex = undefined; + this._updateHasSelectionContext(); + this._lastKnownCurrentLine = this._stripCurrentPrompt(this._currentPromptLine); + this._writeCurrentPromptLine(); + + void this._evaluateCommand(stringToEvaluate); } private _addToHistory (command: string): void { - const isEmpty = command === this._currentPrompt; + const isEmpty = command === ''; const isLastInHistory = this._commandHistory.length !== 0 && command === this._commandHistory[this._commandHistory.length - 1]; if (!isEmpty && !isLastInHistory) { this._commandHistory.push(command); @@ -598,10 +598,11 @@ export default class CommandWindow implements vscode.Pseudoterminal { } private _moveCursorToCurrent (lineOfInputCursorIsCurrentlyOn?: number): void { - const lineNumberCursorShouldBeOn = Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); + const lineNumberCursorShouldBeOn = Math.max(1, Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns)); if (lineOfInputCursorIsCurrentlyOn === undefined) { lineOfInputCursorIsCurrentlyOn = lineNumberCursorShouldBeOn; } + lineOfInputCursorIsCurrentlyOn = Math.max(1, lineOfInputCursorIsCurrentlyOn); if (lineNumberCursorShouldBeOn > lineOfInputCursorIsCurrentlyOn) { this._writeEmitter.fire(ACTION_KEYS.DOWN.repeat(lineNumberCursorShouldBeOn - lineOfInputCursorIsCurrentlyOn)); } else if (lineNumberCursorShouldBeOn < lineOfInputCursorIsCurrentlyOn) { @@ -648,34 +649,70 @@ export default class CommandWindow implements vscode.Pseudoterminal { clear (): void { this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY) void vscode.commands.executeCommand('workbench.action.terminal.clear'); + this._setToEmptyPrompt(); } private _updateHasSelectionContext (): void { void vscode.commands.executeCommand('setContext', 'matlab.terminalHasSelection', this._anchorIndex !== undefined); } - private _handleCopy (): void { + private _handleCopy (): boolean { if (this._anchorIndex === undefined) { - return; + return false; } const selectionStart = this._currentPrompt.length + Math.min(this._cursorIndex, this._anchorIndex); const selectionEnd = this._currentPrompt.length + Math.max(this._cursorIndex, this._anchorIndex); - const selection = this._currentLine.slice(selectionStart, selectionEnd); + const selection = this._currentPromptLine.slice(selectionStart, selectionEnd); void vscode.env.clipboard.writeText(selection); + return false; } - private _handlePaste (): void { + private _handlePaste (): boolean { vscode.env.clipboard.readText().then((text: string) => { this.handleInput(text); }, () => { // Ignored }); + return false; } - private _handleEscape (): void { + private _handleEscape (): boolean { this._setToEmptyPrompt(); - this._isLineDirty = true; + return true; + } + + private _handlePromptChange (state: PromptState, isIdle: boolean): void { + this._currentState = state; + if (state === PromptState.READY) { + this._changePrompt(PROMPTS.IDLE_PROMPT); + } else if (state === PromptState.DEBUG) { + this._changePrompt(PROMPTS.DEBUG_PROMPT); + } else if (state === PromptState.PAUSE) { + this._changePrompt(PROMPTS.BUSY_PROMPT); + } else if (state === PromptState.INPUT) { + this._changePrompt(PROMPTS.FAKE_INPUT_PROMPT); + } else { + this._changePrompt(PROMPTS.BUSY_PROMPT); + } + } + + private _changePrompt (prompt: string): void { + if (this._currentPrompt !== PROMPTS.BUSY_PROMPT) { + this._currentPromptLine = this._stripCurrentPrompt(this._currentPromptLine); + } + this._currentPrompt = prompt; + this._currentPromptLine = this._currentPrompt + this._currentPromptLine; + this._updateWhetherJustTypedInLastColumn(); + this._writeCurrentPromptLine(); + } + + private _stripCurrentPrompt (line: string): string { + return this._currentPromptLine.slice(this._currentPrompt.length); + } + + private _updateWhetherJustTypedInLastColumn (): void { + this._justTypedLastInColumn = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; } onDidWrite: vscode.Event; diff --git a/src/commandwindow/ExecutionCommandProvider.ts b/src/commandwindow/ExecutionCommandProvider.ts index b5e5ebd..35d8738 100644 --- a/src/commandwindow/ExecutionCommandProvider.ts +++ b/src/commandwindow/ExecutionCommandProvider.ts @@ -1,7 +1,7 @@ // Copyright 2024 The MathWorks, Inc. import * as vscode from 'vscode' -import MVM from './MVM' +import { MVM } from './MVM' import TerminalService from './TerminalService' import TelemetryLogger from '../telemetry/TelemetryLogger' import * as path from 'path' @@ -120,7 +120,8 @@ export default class ExecutionCommandProvider { } catch (e) { return; } - if (mdbfileonpathResult.error !== undefined) { + + if ('error' in mdbfileonpathResult) { return; } diff --git a/src/commandwindow/MVM.ts b/src/commandwindow/MVM.ts index 76d5de7..031f0cd 100644 --- a/src/commandwindow/MVM.ts +++ b/src/commandwindow/MVM.ts @@ -1,8 +1,9 @@ // Copyright 2024 The MathWorks, Inc. -import IMVM, { TextEvent, FEvalResponse, EvalResponse, FEvalError } from './MVMInterface' +import { TextEvent, FEvalResponse, EvalResponse, MVMError, BreakpointResponse, Capability } from './MVMInterface' import { createResolvablePromise, ResolvablePromise, Notifier } from './Utilities' import Notification from '../Notifications' +import EventEmitter = require('events') /** * The current state of MATLAB @@ -13,10 +14,28 @@ export enum MatlabState { BUSY = 'busy' } +interface MatlabStateUpdate { + state: string + release: string | null +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MatlabData = any; + +enum Events { + clc = 'clc', + output = 'output', + promptChange = 'promptChange', + stateChanged = 'stateChanged', + debuggingStateChanged = 'debuggingStateChanged' +} + /** * A clientside implementation of MATLAB */ -export default class MVMImpl implements IMVM { +export class MVM extends EventEmitter { + static Events = Events; + private _requestMap: {[requestId: string]: {promise: ResolvablePromise, isUserEval: boolean}} = {} private _pendingUserEvals: number; @@ -24,19 +43,35 @@ export default class MVMImpl implements IMVM { private readonly _stateObservers: Array<(oldState: MatlabState, newState: MatlabState) => void> = []; private _currentState: MatlabState = MatlabState.DISCONNECTED; + private _currentRelease: string | null = null; private _currentReadyPromise: ResolvablePromise; + private _isCurrentlyDebugging = false; + constructor (notifier: Notifier) { + super(); + this._notifier = notifier; this._notifier.onNotification(Notification.MVMEvalComplete, this._handleEvalResponse.bind(this)); this._notifier.onNotification(Notification.MVMFevalComplete, this._handleFevalResponse.bind(this)); + this._notifier.onNotification(Notification.MVMSetBreakpointComplete, this._handleBreakpointResponse.bind(this)); + this._notifier.onNotification(Notification.MVMClearBreakpointComplete, this._handleBreakpointResponse.bind(this)); this._notifier.onNotification(Notification.MVMStateChange, this._handleMatlabStateChange.bind(this)); this._notifier.onNotification(Notification.MVMText, (data: TextEvent) => { - this.onOutput(data) + this.emit(Events.output, data); + }); + this._notifier.onNotification(Notification.MVMClc, () => { + this.emit(Events.clc); + }); + this._notifier.onNotification(Notification.MVMPromptChange, (data) => { + this.emit(Events.promptChange, data.state, data.isIdle); + }); + this._notifier.onNotification(Notification.DebuggingStateChange, (isDebugging) => { + this._isCurrentlyDebugging = isDebugging; + this.emit(Events.debuggingStateChanged, isDebugging); }); - this._notifier.onNotification(Notification.MVMClc, () => { this.onClc() }); this._currentReadyPromise = createResolvablePromise(); @@ -62,17 +97,32 @@ export default class MVMImpl implements IMVM { return this._pendingUserEvals > 0 ? MatlabState.BUSY : MatlabState.READY; } - private _handleMatlabStateChange (newState: string): void { + /** + * + * @returns The current release of MATLAB + */ + getMatlabRelease (): string | null { + return this._currentRelease; + } + + /** + * + * @returns The current release of MATLAB + */ + isDebugging (): boolean { + return this.getMatlabState() !== MatlabState.DISCONNECTED && this._isCurrentlyDebugging; + } + + private _handleMatlabStateChange (newState: MatlabStateUpdate): void { const oldState = this._currentState; - this._currentState = MatlabState[newState.toUpperCase() as keyof typeof MatlabState]; + this._currentState = MatlabState[newState.state.toUpperCase() as keyof typeof MatlabState]; + this._currentRelease = newState.release; if (this._currentState === MatlabState.DISCONNECTED) { this._handleDisconnection(); } - this._stateObservers.forEach((observer) => { - observer(oldState, this._currentState); - }, this); + this.emit(MVM.Events.stateChanged, oldState, this._currentState); if (this._currentState !== MatlabState.DISCONNECTED) { this._pendingUserEvals = 0; @@ -95,22 +145,13 @@ export default class MVMImpl implements IMVM { this._pendingUserEvals = 0; } - /** - * Allow listening to MATLAB state changes - * @param observer - */ - addStateChangeListener (observer: (oldState: MatlabState, newState: MatlabState) => void): void { - this._stateObservers.push(observer); - } - /** * Evaluate the given command. * @param command the command to run * @param isUserEval Only user evals contribute to the current busy state * @returns a promise that is resolved when the eval completes */ - eval (command: string): ResolvablePromise; - eval (command: string, isUserEval: boolean = true): ResolvablePromise { + eval (command: string, isUserEval: boolean = true, capabilitiesToRemove?: Capability[]): ResolvablePromise { const requestId = this._getNewRequestId(); const promise = createResolvablePromise(); this._requestMap[requestId] = { @@ -125,7 +166,9 @@ export default class MVMImpl implements IMVM { this._currentReadyPromise.then(() => { this._notifier.sendNotification(Notification.MVMEvalRequest, { requestId, - command + command, + isUserEval, + capabilitiesToRemove }); }, () => { // Ignored @@ -141,9 +184,9 @@ export default class MVMImpl implements IMVM { * @param args The arguments of the function * @returns A promise resolved when the feval completes */ - feval (functionName: string, nargout: number, args: unknown[]): ResolvablePromise { + feval (functionName: string, nargout: number, args: unknown[], capabilitiesToRemove?: Capability[]): ResolvablePromise { const requestId = this._getNewRequestId(); - const promise = createResolvablePromise(); + const promise = createResolvablePromise(); this._requestMap[requestId] = { promise, isUserEval: false @@ -154,7 +197,8 @@ export default class MVMImpl implements IMVM { requestId, functionName, nargout, - args + args, + capabilitiesToRemove }); }, () => { // Ignored @@ -170,21 +214,6 @@ export default class MVMImpl implements IMVM { this._notifier.sendNotification(Notification.MVMInterruptRequest); } - /** - * Called with output from any requests - * @param data - */ - onOutput (data: TextEvent): void { - throw new Error('Method not overridden.'); - } - - /** - * Called when a clc is run - */ - onClc (): void { - throw new Error('Method not overridden.'); - } - private _handleEvalResponse (message: EvalResponse): void { const obj = this._requestMap[message.requestId]; if (obj === undefined) { @@ -214,4 +243,67 @@ export default class MVMImpl implements IMVM { private _getNewRequestId (): string { return Math.random().toString(36).substr(2, 9); } + + setBreakpoint (fileName: string, lineNumber: number, condition?: string, anonymousIndex?: number): ResolvablePromise { + const requestId = this._getNewRequestId(); + const promise = createResolvablePromise(); + this._requestMap[requestId] = { + promise, + isUserEval: false + }; + + this._currentReadyPromise.then(() => { + this._notifier.sendNotification(Notification.MVMSetBreakpointRequest, { + requestId, + fileName, + lineNumber, + condition, + anonymousIndex + }); + }, () => { + // Ignored + }); + + return promise; + } + + clearBreakpoint (fileName: string, lineNumber: number, condition?: string, anonymousIndex?: number): ResolvablePromise { + const requestId = this._getNewRequestId(); + const promise = createResolvablePromise(); + this._requestMap[requestId] = { + promise, + isUserEval: false + }; + + this._currentReadyPromise.then(() => { + this._notifier.sendNotification(Notification.MVMClearBreakpointRequest, { + requestId, + fileName, + lineNumber, + condition, + anonymousIndex + }); + }, () => { + // Ignored + }); + + return promise; + } + + private _handleBreakpointResponse (message: BreakpointResponse): void { + const obj = this._requestMap[message.requestId]; + if (obj === undefined) { + return; + } + const promise = obj.promise; + promise.resolve(); + } + + unpause (): void { + this._currentReadyPromise.then(() => { + this._notifier.sendNotification(Notification.MVMUnpauseRequest, {}); + }, () => { + // Ignored + }); + } } diff --git a/src/commandwindow/MVMInterface.ts b/src/commandwindow/MVMInterface.ts index 3bed1bf..4835004 100644 --- a/src/commandwindow/MVMInterface.ts +++ b/src/commandwindow/MVMInterface.ts @@ -1,6 +1,14 @@ // Copyright 2024 The MathWorks, Inc. -import { ResolvablePromise } from './Utilities'; +export enum Capability { + InteractiveCommandLine = 'InteractiveCommandLine', + Swing = 'Swing', + ComplexSwing = 'ComplexSwing', + LocalClient = 'LocalClient', + WebWindow = 'WebWindow', + ModalDialogs = 'ModalDialogs', + Debugging = 'Debugging' +} /** * Represents text coming from MATLAB @@ -16,6 +24,8 @@ export interface TextEvent { export interface EvalRequest { requestId: string | number command: string + isUserEval: boolean + capabilitiesToRemove?: Capability[] } /** @@ -33,10 +43,11 @@ export interface FEvalRequest { functionName: string nargout: number args: unknown[] + capabilitiesToRemove?: Capability[] } /** - * Represents a feval response to MATLAB + * Represents a feval response from MATLAB */ export interface FEvalResponse { requestId: string | number @@ -44,19 +55,38 @@ export interface FEvalResponse { } /** - * MATLAB Error result + * Represents a breakpoint request to MATLAB */ -export interface FEvalError { - error: unknown +export interface BreakpointRequest { + requestId: string | number + fileName: string + lineNumber: number + condition?: string + anonymousIndex?: number +} + +/** +* Represents a breakpoint response from MATLAB +*/ +export interface BreakpointResponse { + requestId: string | number + error?: MVMError } /** - * The base functionality for any MVM instance to support + * MATLAB Error result */ -export default interface IMVM { - eval: (command: string) => ResolvablePromise - feval: (functionName: string, nargout: number, args: unknown[]) => ResolvablePromise - interrupt: () => void - onOutput: (data: TextEvent) => void - onClc: () => void +export interface MVMError { + error: unknown +} + +export enum PromptState { + INITIALIZING = 'INITIALIZING', + READY = 'READY', + BUSY = 'BUSY', + DEBUG = 'DEBUG', + INPUT = 'INPUT', + PAUSE = 'PAUSE', + MORE = 'MORE', + COMPLETING_BLOCK = 'COMPLETING_BLOCK' } diff --git a/src/commandwindow/TerminalService.ts b/src/commandwindow/TerminalService.ts index eb5cd8d..8abc634 100644 --- a/src/commandwindow/TerminalService.ts +++ b/src/commandwindow/TerminalService.ts @@ -1,7 +1,7 @@ // Copyright 2024 The MathWorks, Inc. import * as vscode from 'vscode' -import MVM from './MVM' +import { MVM } from './MVM' import { Notifier, ResolvablePromise, createResolvablePromise } from './Utilities' import CommandWindow from './CommandWindow' import Notification from '../Notifications' diff --git a/src/commandwindow/Utilities.ts b/src/commandwindow/Utilities.ts index e006772..d0ed20c 100644 --- a/src/commandwindow/Utilities.ts +++ b/src/commandwindow/Utilities.ts @@ -29,8 +29,40 @@ export function createResolvablePromise (): ResolvablePromise { * Represents an object that can send and recieve data on specific channels. */ export interface Notifier { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any sendNotification: (tag: string, data?: any) => void - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any onNotification: (tag: string, callback: (data: any) => void) => void } + +export class MultiClientNotifier implements Notifier { + readonly _notifier: Notifier; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _callbacks: { [tag: string]: Array<(data: any) => void> } = {}; + + constructor (notifier: Notifier) { + this._notifier = notifier; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendNotification (tag: string, data?: any): void { + this._notifier.sendNotification(tag, data); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onNotification (tag: string, callback: (data: any) => void): void { + if (!(tag in this._callbacks)) { + this._callbacks[tag] = []; + this._notifier.onNotification(tag, this._handler.bind(this, tag)); + } + this._callbacks[tag].push(callback); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _handler (tag: string, data: any): void { + this._callbacks[tag].forEach((callback) => { + callback(data); + }, this); + } +} diff --git a/src/debug/MatlabDebugAdaptor.ts b/src/debug/MatlabDebugAdaptor.ts new file mode 100644 index 0000000..798f402 --- /dev/null +++ b/src/debug/MatlabDebugAdaptor.ts @@ -0,0 +1,153 @@ +// Copyright 2024 The MathWorks, Inc. + +import * as vscode from 'vscode' +import * as debug from '@vscode/debugadapter' +import { DebugProtocol } from '@vscode/debugprotocol'; +import { Notifier } from '../commandwindow/Utilities' +import { MVM, MatlabState } from '../commandwindow/MVM' +import Notification from '../Notifications' + +class PackagedRequest { + debugRequest: DebugProtocol.Request; + tag: number; + + constructor (request: DebugProtocol.Request, debugAdaptorId: number) { + this.debugRequest = request; + this.tag = debugAdaptorId; + } +} + +interface PackagedResponse { + debugResponse: DebugProtocol.Response + tag: number +} + +interface PackagedEvent { + debugEvent: DebugProtocol.Event +} + +export default class MatlabDebugAdaptor extends debug.DebugSession { + private readonly _mvm: MVM; + private readonly _notifier: Notifier; + private readonly _baseSessionGetter: (dontAutoStart: boolean) => Promise; + + private readonly _isBase: boolean; + + static _nextId = 1; + private readonly _debugAdaptorId: number; + + private _isMatlabConnected: boolean = false; + private _isStarted: boolean = false; + + constructor (mvm: MVM, notifier: Notifier, baseSessionGetter: (dontAutoStart: boolean) => Promise, isBase: boolean) { + super() + + this._mvm = mvm; + this._notifier = notifier; + this._baseSessionGetter = baseSessionGetter; + + this._debugAdaptorId = MatlabDebugAdaptor._nextId; + MatlabDebugAdaptor._nextId += 1; + + this._isBase = isBase; + + this._notifier.onNotification(Notification.DebugAdaptorResponse, this._handleResponseNotification.bind(this)); + this._notifier.onNotification(Notification.DebugAdaptorEvent, this._handleEventNotification.bind(this)); + + if (this._isBase) { + this._setupDocumentListeners(); + } + } + + private _isLifecycleEvent (event: DebugProtocol.Event): boolean { + return event.event === 'initialized' || event.event === 'exited' || event.event === 'terminate'; + } + + private _handleEventNotification (packagedEvent: PackagedEvent): void { + const event = packagedEvent.debugEvent; + if (this._isBase) { + if (this._isLifecycleEvent(event)) { + this.sendEvent(event); + } + } else { + if (this._isStarted) { + this.sendEvent(event); + } + } + + if (event.event === 'terminate') { + this._isStarted = false; + } + } + + private _handleResponseNotification (packagedResonse: PackagedResponse): void { + const response = this._unpackageResponse(packagedResonse); + if (response !== null) { + this.sendResponse(response); + } + } + + private _packageRequest (request: DebugProtocol.Request): PackagedRequest { + return new PackagedRequest(request, this._debugAdaptorId); + } + + private _unpackageResponse (response: PackagedResponse): DebugProtocol.Response | null { + if (response.tag === this._debugAdaptorId) { + return response.debugResponse; + } else { + return null; + } + } + + private _setupDocumentListeners (): void { + this._mvm.on(MVM.Events.stateChanged, (oldState: MatlabState, newState: MatlabState) => { + if (oldState === newState) { + return; + } + + if (newState === MatlabState.DISCONNECTED) { + this._isMatlabConnected = false; + } else { + if (!this._isMatlabConnected) { + vscode.workspace.textDocuments.forEach(this._sendCacheFilePathRequest.bind(this)); + } + this._isMatlabConnected = true; + } + }); + vscode.workspace.onDidOpenTextDocument(this._sendCacheFilePathRequest.bind(this)); + } + + private _sendCacheFilePathRequest (document: vscode.TextDocument): void { + if (!this._isMatlabConnected) { + return; + } + + if (document.fileName.endsWith('.m')) { + const cacheRequest: DebugProtocol.Request = { + seq: -1, + type: 'request', + command: 'cacheFilePath', + arguments: { fileName: document.fileName } + }; + this.dispatchRequest(cacheRequest); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendErrorResponse (response: DebugProtocol.Response, codeOrMessage: number | DebugProtocol.Message, format?: string, variables?: any, dest?: debug.ErrorDestination): void { + super.sendErrorResponse(response, codeOrMessage, format, variables, dest); + } + + protected dispatchRequest (request: DebugProtocol.Request): void { + if (request.command === 'initialize') { + this._isStarted = true; + } + + this._notifier.sendNotification(Notification.DebugAdaptorRequest, this._packageRequest(request)); + } + + handleDisconnect (): void { + this.sendEvent(new debug.ExitedEvent(0)); + this.sendEvent(new debug.TerminatedEvent(false)); + } +} diff --git a/src/debug/MatlabDebugger.ts b/src/debug/MatlabDebugger.ts new file mode 100644 index 0000000..810a139 --- /dev/null +++ b/src/debug/MatlabDebugger.ts @@ -0,0 +1,228 @@ +// Copyright 2024 The MathWorks, Inc. + +import * as vscode from 'vscode' +import { Notifier } from '../commandwindow/Utilities'; +import { MVM, MatlabState } from '../commandwindow/MVM' +import MatlabDebugAdaptor from './MatlabDebugAdaptor'; +import Notification from '../Notifications'; +import TelemetryLogger from '../telemetry/TelemetryLogger'; + +export default class MatlabDebugger { + private readonly _baseDebugAdaptor: MatlabDebugAdaptor; + private readonly _nestedDebugAdaptor: MatlabDebugAdaptor; + private readonly _mvm: MVM; + private readonly _notifier: Notifier; + private readonly _telemetryLogger: TelemetryLogger; + + private _isDebugAdaptorStarted: boolean = false; + + private _baseDebugSession: vscode.DebugSession | null = null; + private readonly _activeSessions: Set = new Set(); + + private _hasSentNotification: boolean = false; + + constructor (mvm: MVM, notifier: Notifier, telemetryLogger: TelemetryLogger) { + this._mvm = mvm; + this._notifier = notifier; + this._telemetryLogger = telemetryLogger; + this._baseDebugAdaptor = new MatlabDebugAdaptor(mvm, notifier, this._getBaseDebugSession.bind(this), true) + this._nestedDebugAdaptor = new MatlabDebugAdaptor(mvm, notifier, this._getBaseDebugSession.bind(this), false) + this._initialize(); + + vscode.workspace.onDidChangeConfiguration(async (event) => { + if (event.affectsConfiguration('MATLAB.startDebuggerAutomatically')) { + const shouldAutoStart = await vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? true; + if (shouldAutoStart && this._mvm.getMatlabState() !== MatlabState.DISCONNECTED) { + void this._getBaseDebugSession(false); + } else { + if (!this._mvm.isDebugging()) { + this._baseDebugAdaptor.handleDisconnect(); + if (this._baseDebugSession != null) { + void vscode.debug.stopDebugging(this._baseDebugSession) + this._baseDebugSession = null; + this._isDebugAdaptorStarted = false; + } + } + } + } + }); + } + + private async _getBaseDebugSession (dontAutoStart: boolean): Promise { + if (dontAutoStart) { + return this._baseDebugSession; + } + + if (this._mvm.getMatlabState() === MatlabState.DISCONNECTED) { + throw new Error('No base debugging session exists'); + } + + if (this._baseDebugSession != null) { + return this._baseDebugSession; + } + + await this._startBaseSession(); + + if (this._baseDebugSession === null) { + throw new Error('No base debugging session exists'); + } + + return this._baseDebugSession; + } + + private async _startBaseSession (): Promise { + await vscode.debug.startDebugging(undefined, { + name: 'base matlab', + type: 'matlab', + request: 'launch' + }, { + debugUI: { simple: true } + } as vscode.DebugSessionOptions); + } + + private _initialize (): void { + vscode.debug.registerDebugConfigurationProvider('matlab', { + resolveDebugConfiguration (folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { + config.name = 'matlab'; + config.type = 'matlab'; + config.request = 'launch'; + return config; + } + } as vscode.DebugConfigurationProvider) + + const baseDebugAdaptor = this._baseDebugAdaptor; + const nestedDebugAdaptor = this._nestedDebugAdaptor; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const matlabDebugger = this; + vscode.debug.registerDebugAdapterDescriptorFactory('matlab', { + createDebugAdapterDescriptor (_session: vscode.DebugSession): vscode.ProviderResult { + if (matlabDebugger._baseDebugSession == null) { + return new vscode.DebugAdapterInlineImplementation(baseDebugAdaptor); + } else { + return new vscode.DebugAdapterInlineImplementation(nestedDebugAdaptor); + } + } + } as vscode.DebugAdapterDescriptorFactory); + + vscode.debug.onDidStartDebugSession(async (session: vscode.DebugSession) => { + if (session.type !== 'matlab') { + return; + } + + this._activeSessions.add(session); + session.name = 'MATLAB'; + + const isInvalidToStartSession = (await vscode.workspace.getConfiguration('MATLAB').get('matlabConnectionTiming')) === 'never' + if (isInvalidToStartSession && this._mvm.getMatlabState() === MatlabState.DISCONNECTED) { + void vscode.debug.stopDebugging(session); + return; + } + + this._notifier.sendNotification(Notification.MatlabRequestInstance); + if (this._baseDebugSession == null) { + this._baseDebugSession = session; + } else { + if (!this._mvm.isDebugging()) { + void vscode.debug.stopDebugging(session); + } + } + }); + + vscode.debug.onDidTerminateDebugSession(async (session: vscode.DebugSession) => { + this._activeSessions.delete(session); + if (session === this._baseDebugSession) { + this._baseDebugSession = null; + } else { + this._isDebugAdaptorStarted = false; + } + if (this._mvm.getMatlabState() !== MatlabState.DISCONNECTED) { + const shouldAutoStart = await vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? true; + if (shouldAutoStart) { + void this._getBaseDebugSession(false); + } + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this._mvm.on(MVM.Events.stateChanged, this._handleMvmStateChange.bind(this)); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this._mvm.on(MVM.Events.debuggingStateChanged, this._handleMatlabDebuggingStateChange.bind(this)); + } + + private async _handleMvmStateChange (oldState: MatlabState, newState: MatlabState): Promise { + if (newState === MatlabState.READY && oldState === MatlabState.DISCONNECTED) { + void vscode.commands.executeCommand('setContext', 'matlab.isDebugging', false); + const shouldAutoStart = await vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? true; + if (shouldAutoStart) { + void this._getBaseDebugSession(false); + } + } else if (newState === MatlabState.DISCONNECTED) { + void vscode.commands.executeCommand('setContext', 'matlab.isDebugging', false); + this._baseDebugAdaptor.handleDisconnect(); + this._activeSessions.forEach((session) => { + void vscode.debug.stopDebugging(session); + }) + this._isDebugAdaptorStarted = false; + } + } + + private async _handleMatlabDebuggingStateChange (isDebugging: boolean): Promise { + void vscode.commands.executeCommand('setContext', 'matlab.isDebugging', isDebugging); + + if (!isDebugging) { + return; + } + + const shouldReact = await this._shouldReactToDebugEvent(); + const isStillDebugging = this._mvm.isDebugging(); + + if (shouldReact && isStillDebugging) { + void this._maybeStartDebugger(); + } + } + + private async _maybeStartDebugger (): Promise { + if (this._isDebugAdaptorStarted) { + return; + } + + if (!this._hasSentNotification) { + this._hasSentNotification = true; + this._telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { + action_type: 'debuggerStarted', + result: '' + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const baseSession = this._baseDebugSession ?? undefined; + + const wasDebuggerStartSuccessful = await vscode.debug.startDebugging(undefined, { + name: 'matlab', + type: 'matlab', + request: 'launch' + }, { + parentSession: baseSession, + compact: true, + suppressDebugStatusbar: false, + suppressDebugToolbar: false, + suppressDebugView: false + }); + + this._isDebugAdaptorStarted = wasDebuggerStartSuccessful; + } + + private async _shouldReactToDebugEvent (): Promise { + const shouldAutoStart = await vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? true; + if (!shouldAutoStart) { + const baseSession = await this._getBaseDebugSession(true); + if (baseSession === null) { + return false; + } + } + return true; + } +} diff --git a/src/extension.ts b/src/extension.ts index 0e5670e..5b8179f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,14 +7,16 @@ import { } from 'vscode-languageclient/node' import NotificationConstants from './NotificationConstants' import TelemetryLogger, { TelemetryEvent } from './telemetry/TelemetryLogger' -import MVM from './commandwindow/MVM' -import { Notifier } from './commandwindow/Utilities' +import { MVM } from './commandwindow/MVM' +import { Notifier, MultiClientNotifier } from './commandwindow/Utilities' import TerminalService from './commandwindow/TerminalService' import Notification from './Notifications' import ExecutionCommandProvider from './commandwindow/ExecutionCommandProvider' import * as LicensingUtils from './utils/LicensingUtils' import DeprecationPopupService from './DeprecationPopupService' import SectionStylingService from './styling/SectionStylingService' +import MatlabDebugger from './debug/MatlabDebugger' + let client: LanguageClient const OPEN_SETTINGS_ACTION = 'workbench.action.openSettings' const MATLAB_INSTALL_PATH_SETTING = 'matlab.installPath' @@ -37,6 +39,8 @@ let deprecationPopupService: DeprecationPopupService let sectionStylingService: SectionStylingService; let mvm: MVM; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let matlabDebugger: MatlabDebugger; let terminalService: TerminalService; let executionCommandProvider: ExecutionCommandProvider; @@ -118,9 +122,12 @@ export async function activate (context: vscode.ExtensionContext): Promise client.onNotification(Notification.MatlabFeatureUnavailable, () => handleFeatureUnavailable()) client.onNotification(Notification.MatlabFeatureUnavailableNoMatlab, () => handleFeatureUnavailableWithNoMatlab()) client.onNotification(Notification.LogTelemetryData, (data: TelemetryEvent) => handleTelemetryReceived(data)) - mvm = new MVM(client as Notifier); - terminalService = new TerminalService(client as Notifier, mvm); + + const multiclientNotifier = new MultiClientNotifier(client as Notifier); + mvm = new MVM(multiclientNotifier); + terminalService = new TerminalService(multiclientNotifier, mvm); executionCommandProvider = new ExecutionCommandProvider(mvm, terminalService, telemetryLogger); + matlabDebugger = new MatlabDebugger(mvm, multiclientNotifier, telemetryLogger); context.subscriptions.push(vscode.commands.registerCommand('matlab.runFile', async () => await executionCommandProvider.handleRunFile())) context.subscriptions.push(vscode.commands.registerCommand('matlab.runSelection', async () => await executionCommandProvider.handleRunSelection())) @@ -388,7 +395,8 @@ function getServerArgs (context: vscode.ExtensionContext): string[] { const configuration = vscode.workspace.getConfiguration('MATLAB') const args = [ `--matlabInstallPath=${configuration.get('installPath') ?? ''}`, - `--matlabConnectionTiming=${configuration.get('launchMatlab') ?? 'onStart'}` + `--matlabConnectionTiming=${configuration.get('launchMatlab') ?? 'onStart'}`, + '--snippetIgnoreList=\'For Loop;If Statement;If-Else Statement;While Loop;Try-Catch Statement;Switch Statement;Function Definition;Class Definition;Parallel For Loop;SPMD block\'' ] if (configuration.get('indexWorkspace') ?? false) { diff --git a/syntaxes b/syntaxes index 326bf67..d7fa696 160000 --- a/syntaxes +++ b/syntaxes @@ -1 +1 @@ -Subproject commit 326bf67462f0e9ecf62ba307819c72a331520549 +Subproject commit d7fa696a8a3f1e981ed3271b49f63fc7529c7883