diff --git a/CHANGELOG.md b/CHANGELOG.md index 9357e6c..3d9f0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.4] - 2025-07-31 + +### Added +- Support for running sections in MATLAB code +- Support for formatting a selection +- Display the language server output panel using the `matlab.showLanguageServerOutput` command +- Breadcrumbs and Outline view now include methods, properties, and enumerations for improved navigation + +### Fixed +- Applied patches for CVE-2023-44270, CVE-2024-11831, CVE-2025-27789, CVE-2025-30359, CVE-2025-30360, CVE-2025-32996, CVE-2025-48387, and CVE-2025-5889 +- Resolves issue where extension stops working after calling `restoredefaultpath` + ## [1.3.3] - 2025-05-15 ### Added diff --git a/README.md b/README.md index a601a85..ca93e77 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ There are some limitations to running and debugging MATLAB code in Visual Studio * 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. +## Run MATLAB in Jupyter Notebooks +You also can use this extension along with the Jupyter Extension for Visual Studio Code to run MATLAB in Jupyter notebooks using Visual Studio Code. For instructions, see [Run MATLAB in Jupyter Notebooks Using VS Code](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/install_guides/vscode/README.md). + ## Configuration To configure the extension, go to the extension settings and select from the available options. diff --git a/package-lock.json b/package-lock.json index e3f9753..b69eefb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "language-matlab", - "version": "1.3.3", + "version": "1.3.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.3.3", + "version": "1.3.4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1522,10 +1522,11 @@ } }, "node_modules/@vscode/vsce/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1989,10 +1990,11 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3972,10 +3974,11 @@ } }, "node_modules/glob/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5454,10 +5457,11 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7130,10 +7134,11 @@ "dev": true }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -7216,10 +7221,11 @@ } }, "node_modules/targz/node_modules/tar-fs": { - "version": "1.16.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.4.tgz", - "integrity": "sha512-u3XczWoYAIVXe5GOKK6+VeWaHjtc47W7hyuTo3+4cNakcCcuDmlkYiiHEsECwTkcI3h1VUgtwBQ54+RvY6cM4w==", + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.5.tgz", + "integrity": "sha512-1ergVCCysmwHQNrOS+Pjm4DQ4nrGp43+Xnu4MRGjCnQu/m3hEgLNS78d5z+B8OJ1hN5EejJdCSFZE1oM6AQXAQ==", "dev": true, + "license": "MIT", "dependencies": { "chownr": "^1.0.1", "mkdirp": "^0.5.1", @@ -7276,10 +7282,11 @@ } }, "node_modules/test-exclude/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7778,10 +7785,11 @@ } }, "node_modules/vscode-extension-tester/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -8050,9 +8058,10 @@ } }, "node_modules/vscode-languageclient/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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -9390,9 +9399,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -9811,9 +9820,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -11251,9 +11260,9 @@ }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -12337,9 +12346,9 @@ }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -13544,9 +13553,9 @@ } }, "tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "optional": true, "requires": { @@ -13623,9 +13632,9 @@ } }, "tar-fs": { - "version": "1.16.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.4.tgz", - "integrity": "sha512-u3XczWoYAIVXe5GOKK6+VeWaHjtc47W7hyuTo3+4cNakcCcuDmlkYiiHEsECwTkcI3h1VUgtwBQ54+RvY6cM4w==", + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.5.tgz", + "integrity": "sha512-1ergVCCysmwHQNrOS+Pjm4DQ4nrGp43+Xnu4MRGjCnQu/m3hEgLNS78d5z+B8OJ1hN5EejJdCSFZE1oM6AQXAQ==", "dev": true, "requires": { "chownr": "^1.0.1", @@ -13673,9 +13682,9 @@ }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -14071,9 +14080,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -14272,9 +14281,9 @@ }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } diff --git a/package.json b/package.json index ddde168..780effe 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.3.3", + "version": "1.3.4", "engines": { "vscode": "^1.67.0" }, @@ -39,6 +39,12 @@ "icon": "$(play)", "when": "!matlab.isDebugging" }, + { + "command": "matlab.runSection", + "title": "MATLAB: Run Current Section", + "icon": "$(play)", + "when": "!matlab.isDebugging" + }, { "command": "matlab.runSelection", "title": "MATLAB: Run Current Selection", @@ -77,6 +83,10 @@ { "command": "matlab.resetDeprecationPopups", "title": "MATLAB: Reset Deprecation Warning Popups" + }, + { + "command": "matlab.showLanguageServerOutput", + "title": "MATLAB: Show Language Server Output" } ], "keybindings": [ @@ -85,6 +95,11 @@ "key": "f5", "when": "editorTextFocus && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed && resourceScheme != 'untitled' && !matlab.isDebugging" }, + { + "command": "matlab.runSection", + "key": "Ctrl+enter", + "when": "editorTextFocus && editorLangId == matlab && !findInputFocussed && !replaceInputFocussed && !matlab.isDebugging" + }, { "command": "matlab.runSelection", "key": "shift+enter", @@ -93,12 +108,12 @@ { "command": "matlab.interrupt", "key": "Ctrl+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" + "language": "matlab" } ], "debuggers": [ @@ -131,10 +146,15 @@ "when": "editorLangId == matlab && resourceScheme != 'untitled' && !matlab.isDebugging", "group": "1_run" }, + { + "command": "matlab.runSection", + "when": "editorLangId == matlab && !matlab.isDebugging", + "group": "2_run" + }, { "command": "matlab.runSelection", "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections && !matlab.isDebugging", - "group": "2_run" + "group": "3_run" } ], "editor/context": [ @@ -143,10 +163,15 @@ "when": "editorLangId == matlab && resourceScheme != 'untitled' && !matlab.isDebugging", "group": "1_run" }, + { + "command": "matlab.runSection", + "when": "editorLangId == matlab && !matlab.isDebugging", + "group": "2_run" + }, { "command": "matlab.runSelection", "when": "editorLangId == matlab && editorHasSelection && !editorHasMultipleSelections && !matlab.isDebugging", - "group": "1_run" + "group": "3_run" } ], "explorer/context": [ @@ -247,7 +272,6 @@ } } }, - "languages": [ { "id": "matlab", @@ -298,7 +322,10 @@ "lint:fix": "eslint src --ext ts --fix", "test-smoke": "npm run test-setup && node ./out/test/smoke/runTest.js", "test-ui": "npm run test-setup && node ./out/test/ui/runTest.js", - "test": "npm run test-smoke && npm run test-ui", + "test-smoke:fast": "node ./out/test/smoke/runTest.js", + "test-ui:fast": "node ./out/test/ui/runTest.js", + "test": "npm run test-setup && npm run test:fast", + "test:fast": "npm run test-smoke:fast && npm run test-ui:fast", "postinstall": "cd server && npm install && cd ..", "package": "vsce package" }, @@ -324,8 +351,8 @@ "vscode-extension-tester": "8.14.1" }, "dependencies": { + "@vscode/debugadapter": "^1.56.0", "node-fetch": "^2.6.6", - "vscode-languageclient": "^8.0.2", - "@vscode/debugadapter": "^1.56.0" + "vscode-languageclient": "^8.0.2" } } diff --git a/server b/server index afcef80..318372b 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit afcef8062cbcf0205e75fef665bdad83cdba8d74 +Subproject commit 318372b80cd3ae0bd7c4fc51a728362dd8ee58ea diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index d5b38ab..f701d15 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -90,6 +90,8 @@ const PROMPTS = { // eslint-disable-next-line no-useless-escape const WORD_REGEX = /(-?\d*\.\d\w*)|(\"[^\"]*\"?)|(\'[^\']*\'?)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)|(\"|\')/ +type MatlabData = any; // eslint-disable-line @typescript-eslint/no-explicit-any + /** * Represents command window. Is a pseudoterminal to be used as the input/output processor in a VS Code terminal. */ @@ -746,15 +748,15 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._pendingTabCompletionRequestNumber = this._pendingTabCompletionRequestNumber + 1; this._notifier.sendNotification(Notification.TerminalCompletionRequest, { requestId: this._pendingTabCompletionRequestNumber, - code: code, - offset: offset + code, + offset }); this._pendingTabCompletionPromise = createResolvablePromise(); return this._pendingTabCompletionPromise; } - private _handleCompletionDataResponse (data: any): void { + private _handleCompletionDataResponse (data: MatlabData): void { if (data.requestId === this._pendingTabCompletionRequestNumber && (this._pendingTabCompletionPromise != null)) { this._pendingTabCompletionPromise.resolve(data.result); } diff --git a/src/commandwindow/ExecutionCommandProvider.ts b/src/commandwindow/ExecutionCommandProvider.ts index 35d8738..9b65403 100644 --- a/src/commandwindow/ExecutionCommandProvider.ts +++ b/src/commandwindow/ExecutionCommandProvider.ts @@ -1,10 +1,12 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vscode from 'vscode' import { MVM } from './MVM' import TerminalService from './TerminalService' import TelemetryLogger from '../telemetry/TelemetryLogger' import * as path from 'path' +import { SectionModel } from '../model/SectionModel' +import { Capability } from './MVMInterface' // These values must match the results returned by mdbfileonpath.m in FilePathState.m enum FILE_PATH_STATE { @@ -181,6 +183,79 @@ export default class ExecutionCommandProvider { this._terminalService.getCommandWindow().insertCommandForEval(commandToRun); } + async handleRunSection (sectionModel: SectionModel): Promise { + this._telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { + action_type: 'runSection', + result: '' + } + }); + + const editor = vscode.window.activeTextEditor + if (editor === undefined || editor.document.languageId !== 'matlab') { + return; + } + + const sectionData = sectionModel.getSectionsForFile(editor.document.uri); + + if (sectionData === undefined) { + return; + } + + sectionData.isDirty = sectionData.isDirty ?? true; + + if (sectionData.isDirty || sectionData.sectionsTree === undefined) { + return; + } + + const fileName = path.basename(editor.document.fileName); + const filePath = editor.document.isUntitled ? fileName : path.basename(editor.document.fileName); + const text = editor.document.getText(); + const lineRange = sectionData.sectionsTree.find(editor.selection.active.line); + const sectionLineRanges = sectionData.sectionRanges.map((range) => [range.start.line + 1, range.end.line + 1]); + + if (lineRange === undefined) { + return; + } + + await this._terminalService.openTerminalOrBringToFront(); + try { + await this._mvm.getReadyPromise(); + } catch (e) { + return; + } + + const args: any[] = [ + fileName, + filePath, + text, + lineRange.start.line + 1, + lineRange.end.line + 1, + -1, + 'vscode', + '', + false, + false + ]; + + switch (this._mvm.getMatlabRelease()) { + case undefined: + case 'R2021b': + case 'R2022a': + case 'R2022b': + case 'R2023a': + break; + default: + args.push(sectionLineRanges); + break; + } + + void this._mvm.feval('matlab.internal.editor.evaluateCode', 0, args, true, [ + Capability.Debugging + ]); + } + /** * Implements the run selection action * @returns diff --git a/src/commandwindow/MVM.ts b/src/commandwindow/MVM.ts index 031f0cd..3b4c0c0 100644 --- a/src/commandwindow/MVM.ts +++ b/src/commandwindow/MVM.ts @@ -1,4 +1,4 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import { TextEvent, FEvalResponse, EvalResponse, MVMError, BreakpointResponse, Capability } from './MVMInterface' import { createResolvablePromise, ResolvablePromise, Notifier } from './Utilities' @@ -184,7 +184,7 @@ export class MVM extends EventEmitter { * @param args The arguments of the function * @returns A promise resolved when the feval completes */ - feval (functionName: string, nargout: number, args: unknown[], capabilitiesToRemove?: Capability[]): ResolvablePromise { + feval (functionName: string, nargout: number, args: unknown[], isUserEval: boolean = false, capabilitiesToRemove?: Capability[]): ResolvablePromise { const requestId = this._getNewRequestId(); const promise = createResolvablePromise(); this._requestMap[requestId] = { @@ -198,6 +198,7 @@ export class MVM extends EventEmitter { functionName, nargout, args, + isUserEval, capabilitiesToRemove }); }, () => { diff --git a/src/commandwindow/MVMInterface.ts b/src/commandwindow/MVMInterface.ts index 4835004..7f8f76c 100644 --- a/src/commandwindow/MVMInterface.ts +++ b/src/commandwindow/MVMInterface.ts @@ -1,4 +1,4 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. export enum Capability { InteractiveCommandLine = 'InteractiveCommandLine', @@ -43,6 +43,7 @@ export interface FEvalRequest { functionName: string nargout: number args: unknown[] + isUserEval: boolean capabilitiesToRemove?: Capability[] } diff --git a/src/commandwindow/Utilities.ts b/src/commandwindow/Utilities.ts index d0ed20c..7d408d3 100644 --- a/src/commandwindow/Utilities.ts +++ b/src/commandwindow/Utilities.ts @@ -1,4 +1,6 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. + +import * as vscode from 'vscode' /** * A promise with resolve and reject methods. Allows easier storing of the promise to be resolved elsewhere. @@ -32,7 +34,7 @@ export interface Notifier { // eslint-disable-next-line @typescript-eslint/no-explicit-any sendNotification: (tag: string, data?: any) => void // eslint-disable-next-line @typescript-eslint/no-explicit-any - onNotification: (tag: string, callback: (data: any) => void) => void + onNotification: (tag: string, callback: (data: any) => void) => vscode.Disposable } export class MultiClientNotifier implements Notifier { @@ -51,12 +53,19 @@ export class MultiClientNotifier implements Notifier { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - onNotification (tag: string, callback: (data: any) => void): void { + onNotification (tag: string, callback: (data: any) => void): vscode.Disposable { if (!(tag in this._callbacks)) { this._callbacks[tag] = []; this._notifier.onNotification(tag, this._handler.bind(this, tag)); } - this._callbacks[tag].push(callback); + const callbacks = this._callbacks[tag]; + callbacks.push(callback); + return new vscode.Disposable(() => { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); // Removes 1 element at the index + } + }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -66,3 +75,18 @@ export class MultiClientNotifier implements Notifier { }, this); } } + +export class Disposer extends vscode.Disposable { + private _owned: vscode.Disposable[] = []; + + constructor () { + super(() => { + this._owned.forEach((obj) => obj.dispose()); + this._owned = []; + }); + } + + own (disposable: vscode.Disposable): void { + this._owned.push(disposable); + } +} diff --git a/src/extension.ts b/src/extension.ts index 5b8179f..92477b2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,4 @@ -// Copyright 2022 - 2024 The MathWorks, Inc. +// Copyright 2022 - 2025 The MathWorks, Inc. import * as path from 'path' import * as vscode from 'vscode' @@ -14,6 +14,7 @@ import Notification from './Notifications' import ExecutionCommandProvider from './commandwindow/ExecutionCommandProvider' import * as LicensingUtils from './utils/LicensingUtils' import DeprecationPopupService from './DeprecationPopupService' +import { SectionModel } from './model/SectionModel' import SectionStylingService from './styling/SectionStylingService' import MatlabDebugger from './debug/MatlabDebugger' @@ -36,6 +37,7 @@ let telemetryLogger: TelemetryLogger let deprecationPopupService: DeprecationPopupService +let sectionModel: SectionModel; let sectionStylingService: SectionStylingService; let mvm: MVM; @@ -96,7 +98,7 @@ export async function activate (context: vscode.ExtensionContext): Promise options: { // --inspect=6009: runs the server in Node's Inspector mode so // Visual Studio® Code can attach to the server for debugging - execArgv: ['--nolazy', '--inspect=6009'] + execArgv: ['--nolazy', '--inspect=6009', '--trace-warnings'] }, args } @@ -129,15 +131,6 @@ export async function activate (context: vscode.ExtensionContext): Promise 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())) - context.subscriptions.push(vscode.commands.registerCommand('matlab.interrupt', () => executionCommandProvider.handleInterrupt())) - context.subscriptions.push(vscode.commands.registerCommand('matlab.openCommandWindow', async () => await terminalService.openTerminalOrBringToFront())) - context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderToPath(uri))) - context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderAndSubfoldersToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderAndSubfoldersToPath(uri))) - context.subscriptions.push(vscode.commands.registerCommand('matlab.changeDirectory', async (uri: vscode.Uri) => await executionCommandProvider.handleChangeDirectory(uri))) - context.subscriptions.push(vscode.commands.registerCommand('matlab.openFile', async (uri: vscode.Uri) => await executionCommandProvider.handleOpenFile(uri))) - // Register a custom command which allows the user enable / disable Sign In options. // Using this custom command would be an alternative approach to going to enabling the setting. context.subscriptions.push(vscode.commands.registerCommand(MATLAB_ENABLE_SIGN_IN_COMMAND, async () => await handleEnableSignIn())) @@ -151,8 +144,24 @@ export async function activate (context: vscode.ExtensionContext): Promise deprecationPopupService = new DeprecationPopupService(context) deprecationPopupService.initialize(client) - sectionStylingService = new SectionStylingService(context) - sectionStylingService.initialize(client); + sectionModel = new SectionModel() + sectionModel.initialize(client as Notifier); + context.subscriptions.push(sectionModel) + + sectionStylingService = new SectionStylingService(sectionModel) + sectionStylingService.initialize(); + context.subscriptions.push(sectionStylingService) + + context.subscriptions.push(vscode.commands.registerCommand('matlab.runFile', async () => await executionCommandProvider.handleRunFile())) + context.subscriptions.push(vscode.commands.registerCommand('matlab.runSection', async () => await executionCommandProvider.handleRunSection(sectionModel))) + context.subscriptions.push(vscode.commands.registerCommand('matlab.runSelection', async () => await executionCommandProvider.handleRunSelection())) + context.subscriptions.push(vscode.commands.registerCommand('matlab.interrupt', () => executionCommandProvider.handleInterrupt())) + context.subscriptions.push(vscode.commands.registerCommand('matlab.openCommandWindow', async () => await terminalService.openTerminalOrBringToFront())) + context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderToPath(uri))) + context.subscriptions.push(vscode.commands.registerCommand('matlab.addFolderAndSubfoldersToPath', async (uri: vscode.Uri) => await executionCommandProvider.handleAddFolderAndSubfoldersToPath(uri))) + context.subscriptions.push(vscode.commands.registerCommand('matlab.changeDirectory', async (uri: vscode.Uri) => await executionCommandProvider.handleChangeDirectory(uri))) + context.subscriptions.push(vscode.commands.registerCommand('matlab.openFile', async (uri: vscode.Uri) => await executionCommandProvider.handleOpenFile(uri))) + context.subscriptions.push(vscode.commands.registerCommand('matlab.showLanguageServerOutput', () => client.outputChannel.show())) await client.start() } diff --git a/src/model/LineRangeTree.ts b/src/model/LineRangeTree.ts new file mode 100644 index 0000000..f2d338a --- /dev/null +++ b/src/model/LineRangeTree.ts @@ -0,0 +1,116 @@ +// Copyright 2024-2025 The MathWorks, Inc. +import * as vscode from 'vscode'; + +/** + * A node used in the LineRangeTree. + * @class TreeNode + */ +class TreeNode { + range: vscode.Range | undefined; + children: TreeNode[]; + parent: TreeNode | undefined; + constructor (range: vscode.Range | undefined) { + this.range = range; + this.children = []; + this.parent = undefined; + } + + add (treeNode: TreeNode): void { + this.children.push(treeNode); + treeNode.parent = this; + } + + getStartLine (): number { + if (this.range !== undefined) { + return this.range.start.line; + } + return 0; + } + + getEndLine (): number { + if (this.range !== undefined) { + return this.range.end.line; + } + + return Infinity; + } +} + +export default class LineRangeTree { + private _root: TreeNode | undefined; + + constructor (sectonRanges: vscode.Range[]) { + this._set(sectonRanges); + } + + /** + * Creates a tree from the given section ranges array based on the start and end lines. + */ + _set (sectonRanges: vscode.Range[]): void { + this._root = new TreeNode(undefined); + const objectLength = sectonRanges.length; + let currentNode: TreeNode | undefined; + currentNode = this._root; + + for (let i = 0; i < objectLength; i++) { + const sectionRange = new TreeNode(sectonRanges[i]); + + while (currentNode != null) { + if (sectionRange.getStartLine() >= currentNode.getStartLine() && + sectionRange.getEndLine() <= currentNode.getEndLine()) { + currentNode.add(sectionRange); + currentNode = sectionRange; + break; + } else { + currentNode = currentNode.parent; + } + } + } + } + + /** + * Finds the object with smallest range (dfs) containing the given line number + * @param line number + * @returns Section Range + */ + find (line: number): vscode.Range | undefined { + let currentNode: TreeNode | undefined; + + currentNode = this._root; + let lastNode = currentNode; + + while (currentNode != null) { + currentNode = this._searchByLine(line, currentNode); + lastNode = currentNode ?? lastNode; + } + return (lastNode != null) ? lastNode.range : undefined; + } + + private _searchByLine (line: number, parentNode: TreeNode): TreeNode | undefined { + const length = parentNode.children.length; + if (length === 0) { + return undefined; + } + + let result: TreeNode | undefined; + let start = 0; + let end = length - 1; + + while (start <= end) { + const mid = Math.floor((start + end) / 2); + const midNode = parentNode.children[mid]; + const midNodeStartLine = midNode.getStartLine() ?? 0; + if (line >= midNodeStartLine && + line <= midNode.getEndLine()) { + result = midNode; + break; + } else if (line < midNodeStartLine) { + end = mid - 1; + } else { + start = mid + 1; + } + } + + return result; + } +} diff --git a/src/model/SectionModel.ts b/src/model/SectionModel.ts new file mode 100644 index 0000000..92db991 --- /dev/null +++ b/src/model/SectionModel.ts @@ -0,0 +1,71 @@ +// Copyright 2025 The MathWorks, Inc. + +import * as vscode from 'vscode'; +import Notification from '../Notifications' +import LineRangeTree from './LineRangeTree'; +import { Notifier, Disposer } from '../commandwindow/Utilities'; +import { EventEmitter } from 'events'; + +export interface SectionsData { + uri: string + sectionRanges: vscode.Range[] + sectionsTree?: LineRangeTree + isDirty?: boolean +} + +export class SectionModel extends Disposer { + private readonly _sectionsCacheByPath = new Map(); + eventEmitter = new EventEmitter() + + initialize (client: Notifier): void { + this.own(client.onNotification(Notification.MatlabSections, (data) => this._onNewSectionsGenerated(data))) + + this.own(vscode.workspace.onDidChangeTextDocument((event) => this._onDocumentChange(event))); + this.own(vscode.workspace.onDidCloseTextDocument((document: vscode.TextDocument) => this._onDocumentClose(document))); + } + + getSectionsForFile (uri: vscode.Uri): SectionsData | undefined { + return this._sectionsCacheByPath.get(this._getDecodedURI(uri)) + } + + private _onDocumentChange (event: vscode.TextDocumentChangeEvent): void { + const filePath = decodeURIComponent(event.document.uri.toString()); + const sectionCache = this._sectionsCacheByPath.get(filePath); + if (sectionCache === undefined || event.contentChanges.length === 0) { + return + } + // As content changes enable isDirty flag, so cursor changes will not renders sections with cache. + // This flag will reset once the sections are regenerated by matlab + sectionCache.isDirty = true; + } + + private _onNewSectionsGenerated (sectionsData: SectionsData): void { + const decodedUri = decodeURIComponent(sectionsData.uri); + const editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find((editor) => { + const textEditorURI = this._getDecodedURI(editor.document.uri) + return textEditorURI === decodedUri; + }); + + if (editor === undefined) { + return + } + + sectionsData.sectionRanges.sort((a: vscode.Range, b: vscode.Range) => a.start.line - b.start.line); + sectionsData.sectionsTree = new LineRangeTree(sectionsData.sectionRanges); + sectionsData.isDirty = false; + + this._sectionsCacheByPath.set(decodedUri, sectionsData); + + this.eventEmitter.emit('onSectionsUpdated', { sectionsData, editor }); + } + + private _onDocumentClose (document: vscode.TextDocument): void { + // Remove sections from cache when document is closed + const decodedURI = decodeURIComponent(document.uri.toString()); + this._sectionsCacheByPath.delete(decodedURI); + } + + private _getDecodedURI (uri: vscode.Uri): string { + return decodeURIComponent(uri.toString()) + } +} diff --git a/src/styling/SectionStylingService.ts b/src/styling/SectionStylingService.ts index 31c2bd5..9c88421 100644 --- a/src/styling/SectionStylingService.ts +++ b/src/styling/SectionStylingService.ts @@ -1,94 +1,41 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vscode from 'vscode'; -import { LanguageClient } from 'vscode-languageclient/node' -import Notification from '../Notifications' - -import LineRangeTree from './LineRangeTree'; import { blueBorderTopDecoration, blueBorderBottomDecoration, greyBorderTopDecoration, greyBorderBottomDecoration, fontWeightBoldDecoration } from './Decorations'; -import { StartAndEndLines, SectionsData, TopAndBottomRanges, StylingRanges } from './StylingInterfaces'; -const sectionsCacheByPath = new Map(); +import { SectionModel, SectionsData } from '../model/SectionModel' +import { Disposer } from '../commandwindow/Utilities'; +import { StartAndEndLines, TopAndBottomRanges, StylingRanges } from './StylingInterfaces'; let previousFocusedEditor: vscode.TextEditor | undefined; -class SectionStylingService { - constructor (private readonly context: vscode.ExtensionContext) {} - - initialize (client: LanguageClient): void { - this.context.subscriptions.push(client.onNotification(Notification.MatlabSections, (data) => this._onNewSectionsGenerated(data))) - // Listen to cursor change to highlight the section - this.context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection((event) => this._handleTextEditorSelectionChange(event))); - - // Clear the active blue borders for focus out - this.context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor((editor) => this._handleEditorFocusChange(editor))) - this.context.subscriptions.push(vscode.window.onDidChangeWindowState((windowFocusState) => this._handleWindowLostFocus(windowFocusState))); +export class SectionStylingService extends Disposer { + private readonly _sectionModel: SectionModel; - this.context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((event) => this._onDocumentChange(event))); - this.context.subscriptions.push(vscode.workspace.onDidCloseTextDocument((document: vscode.TextDocument) => this._onDocumentClose(document))); + constructor (sectionModel: SectionModel) { + super(); + this._sectionModel = sectionModel; } - private _onDocumentChange (event: vscode.TextDocumentChangeEvent): void { - const filePath = decodeURIComponent(event.document.uri.toString()); - const sectionCache = sectionsCacheByPath.get(filePath); - if (sectionCache === undefined || event.contentChanges.length === 0) { - return - } - // As content changes enable isSectionCreationInProgress flag, so cursor changes will not renders sections with cache. - // This flag will reset once the sections are regenerated by matlab - sectionCache.isSectionCreationInProgress = true; - } + initialize (): void { + this._sectionModel.eventEmitter.on('onSectionsUpdated', this._postProcessSectionsData.bind(this)); - private _onNewSectionsGenerated (sectionsData: SectionsData): void { - const decodedUri = decodeURIComponent(sectionsData.uri); - const editorInTheWindow: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find((editor) => { - const textEditorURI = this._getDecodedURI(editor) - return (textEditorURI === decodedUri); - }); - if (editorInTheWindow === undefined) { - return - } - this._preProcessAndSaveSectionByEditor(sectionsData, decodedUri, editorInTheWindow); - this._postProcessSectionsData(sectionsData, editorInTheWindow); - } - - private _preProcessAndSaveSectionByEditor (sectionsData: SectionsData, decodedUri: string, editorInTheWindow: vscode.TextEditor): void { - const { sectionRanges } = sectionsData; - sectionRanges.sort((a: vscode.Range, b: vscode.Range) => a.start.line - b.start.line); - const isSectionAddedAtStart = this._addFirstSectionIfNotExists(sectionRanges); - // Creates a tree of sections and saves it in the sectionsData cache - // Used for retrieving the active section to highlight - sectionsData.sectionsTree = new LineRangeTree(sectionRanges); - sectionsData.isSectionCreationInProgress = false; - sectionsData.implictSectionAtStart = isSectionAddedAtStart; - sectionsCacheByPath.set(decodedUri, sectionsData); - } + // Listen to cursor change to highlight the section + this.own(vscode.window.onDidChangeTextEditorSelection((event) => this._handleTextEditorSelectionChange(event))); - // Add first section if not exists otherwise the cursor will not highlight the section - private _addFirstSectionIfNotExists (sectionRanges: vscode.Range[]): boolean { - if (sectionRanges.length === 0) { - return false; - } - const firstSection = sectionRanges[0]; - const startLine = firstSection?.start?.line; - if (startLine === 0) { - return false; - } - const newStartLine = 0; - const newEndLine = startLine - 1; - const range = new vscode.Range(newStartLine, 0, newEndLine, 0); - sectionRanges.unshift(range); - return true; + // Clear the active blue borders for focus out + this.own(vscode.window.onDidChangeActiveTextEditor((editor) => this._handleEditorFocusChange(editor))) + this.own(vscode.window.onDidChangeWindowState((windowFocusState) => this._handleWindowLostFocus(windowFocusState))); } - private _postProcessSectionsData (sectionsData: SectionsData, editorInTheWindow: vscode.TextEditor): void { + private _postProcessSectionsData ({ sectionsData, editor }: { sectionsData: SectionsData, editor: vscode.TextEditor }): void { const { sectionRanges } = sectionsData; if (sectionRanges !== undefined && sectionRanges.length === 0) { // If there are no sections, clear the decorations - this._clearDecorations(editorInTheWindow); + this._clearDecorations(editor); return } const activeEditor = this._getActiveEditor(); - if (activeEditor !== undefined && activeEditor === editorInTheWindow) { - const cursorPosition = editorInTheWindow.selection.active; + if (activeEditor !== undefined && activeEditor === editor) { + const cursorPosition = editor.selection.active; if (cursorPosition !== undefined) { // Highlight active sections to blue and inactive sections to grey this._highlightSections(activeEditor, sectionsData, cursorPosition); @@ -96,15 +43,11 @@ class SectionStylingService { } } // Highlight all sections to grey - this._highlightSections(editorInTheWindow, sectionsData, null); + this._highlightSections(editor, sectionsData, null); } - private _clearDecorations (editorInTheWindow: vscode.TextEditor): void { - this._setDecorations(editorInTheWindow, { blue: { top: [], bottom: [] }, grey: { top: [], bottom: [] } }, []); - } - - private _getDecodedURI (editor: vscode.TextEditor): string { - return decodeURIComponent(editor.document.uri.toString()) + private _clearDecorations (editor: vscode.TextEditor): void { + this._setDecorations(editor, { blue: { top: [], bottom: [] }, grey: { top: [], bottom: [] } }, []); } private _handleWindowLostFocus (windowState: vscode.WindowState): void { @@ -128,29 +71,25 @@ class SectionStylingService { private _handleTextEditorSelectionChange (event: vscode.TextEditorSelectionChangeEvent): void { const editor = event.textEditor; const cursorPosition = editor.selection.active; - const editorSections = sectionsCacheByPath.get(this._getDecodedURI(editor)); - if (editorSections?.isSectionCreationInProgress === true) { - // Don't highlight sections if section creation is in progress - // This will create sections with cache data which will be wrong + const editorSections = this._sectionModel.getSectionsForFile(editor.document.uri); + + if (editorSections === undefined) { return; } - if (editorSections != null) { - // Don't highlight sections if there is an error - this._highlightSections(editor, editorSections, cursorPosition); + if (editorSections.isDirty === true) { + // Don't highlight sections if section creation is in progress + // This will create sections with cache data which will be wrong + return; } - } - private _onDocumentClose (document: vscode.TextDocument): void { - // Remove sections from cache when document is closed - const decodedURI = decodeURIComponent(document.uri.toString()); - sectionsCacheByPath.delete(decodedURI); + // Don't highlight sections if there is an error + this._highlightSections(editor, editorSections, cursorPosition); } private _clearBlueDecorations (previousEditor: vscode.TextEditor): void { - const decodedUri = this._getDecodedURI(previousEditor); - const sections = sectionsCacheByPath.get(decodedUri); - if (sections !== undefined && sections.isSectionCreationInProgress === false) { + const sections = this._sectionModel.getSectionsForFile(previousEditor.document.uri); + if (sections !== undefined && sections.isDirty === false) { this._highlightSections(previousEditor, sections, null); } } @@ -186,10 +125,6 @@ class SectionStylingService { } this._filterFirstAndLastSection(stylingRanges, lastLineinSection, editor.document); - if (sections.implictSectionAtStart === true) { - // Remove as the first section is implicit - allStartLinesRange.shift(); - } this._setDecorations(editor, stylingRanges, allStartLinesRange); } diff --git a/src/styling/StylingInterfaces.ts b/src/styling/StylingInterfaces.ts index 9b18357..6342554 100644 --- a/src/styling/StylingInterfaces.ts +++ b/src/styling/StylingInterfaces.ts @@ -1,22 +1,13 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2025 The MathWorks, Inc. import * as vscode from 'vscode'; -import LineRangeTree from './LineRangeTree'; interface StartAndEndLines { startLines: number[] endLines: number[] } -interface SectionsData { - uri: string - sectionRanges: vscode.Range[] - sectionsTree: LineRangeTree | undefined - isSectionCreationInProgress: boolean | undefined - implictSectionAtStart: boolean | undefined -} - interface TopAndBottomRanges { top: vscode.Range[], bottom: vscode.Range[] } interface StylingRanges {blue: TopAndBottomRanges, grey: TopAndBottomRanges} -export { StartAndEndLines, SectionsData, TopAndBottomRanges, StylingRanges }; +export { StartAndEndLines, TopAndBottomRanges, StylingRanges }; diff --git a/src/test/test-files/hSectionsScript.m b/src/test/test-files/hSectionsScript.m new file mode 100644 index 0000000..ea564c4 --- /dev/null +++ b/src/test/test-files/hSectionsScript.m @@ -0,0 +1,7 @@ +%% Section 1 +a = 1; +disp('This is first section') + +%% Section 2 +a +disp('This is second section') \ No newline at end of file diff --git a/src/test/tools/tester/VSCodeTester.ts b/src/test/tools/tester/VSCodeTester.ts index 1f1bc65..ce7d668 100644 --- a/src/test/tools/tester/VSCodeTester.ts +++ b/src/test/tools/tester/VSCodeTester.ts @@ -134,6 +134,12 @@ export class VSCodeTester { return await prompt.confirm() } + public async runCurrentSection (): Promise { + const prompt = await this.workbench.openCommandPrompt() + await prompt.setText('>matlab.runSection') + return await prompt.confirm() + } + public async setSetting (id: string, value: string): Promise { const editor = await this.workbench.openSettings(); const setting = await editor.findSettingByID(id) as vet.ComboSetting | vet.TextSetting; diff --git a/src/test/ui/execution.test.ts b/src/test/ui/execution.test.ts new file mode 100644 index 0000000..721e8a4 --- /dev/null +++ b/src/test/ui/execution.test.ts @@ -0,0 +1,29 @@ +// Copyright 2025 The MathWorks, Inc. +import { VSCodeTester } from '../tools/tester/VSCodeTester' +import { before, after } from 'mocha'; +import { Key } from 'selenium-webdriver'; + +suite('Execution Smoke Tests', () => { + let vs: VSCodeTester + + before(async () => { + vs = new VSCodeTester(); + await vs.openEditor('hScript1.m') + await vs.assertMATLABConnected() + await vs.closeActiveEditor() + await vs.openMATLABTerminal() + }); + + after(async () => { + await vs.disconnectFromMATLAB() + }); + + test('Test run section', async () => { + // Script file has a = 1 in first section and prints a in second section + const editor = await vs.openEditor('hSectionsScript.m') + await vs.terminal.executeCommand('a = 0; clc') + await editor.setCursor(5, 1) // Setting cursor on second section + await vs.runCurrentSection() + await vs.terminal.assertContains('0', 'Value of a should not be updated') + }) +}); diff --git a/src/test/ui/terminal.test.ts b/src/test/ui/terminal.test.ts index 6e9047f..d0f5ef0 100644 --- a/src/test/ui/terminal.test.ts +++ b/src/test/ui/terminal.test.ts @@ -64,4 +64,30 @@ suite('Terminal Smoke Tests', () => { await vs.terminal.assertContains('xVar + 3', 'terminal should contain xVar + 3') await vs.terminal.type(Key.ESCAPE) }) + + test('Test command history up/down', async () => { + await vs.terminal.executeCommand('a = 123;') + await vs.terminal.executeCommand('b = 456;') + await vs.terminal.executeCommand('clc') + await vs.terminal.type(Key.ARROW_UP) + await vs.terminal.type(Key.ARROW_UP) + await vs.terminal.assertContains('b = 456;', 'Up arrow should recall previous command') + await vs.terminal.type(Key.ARROW_UP) + await vs.terminal.assertContains('a = 123;', 'Second up arrow should recall earlier command') + await vs.terminal.type(Key.ARROW_DOWN) + await vs.terminal.assertContains('b = 456;', 'Down arrow should go forward in history') + await vs.terminal.type(Key.ESCAPE) + }) + + test('Test command history prefix filtering', async () => { + // Enter command that will be in history + await vs.terminal.executeCommand('a = 123;') + await vs.terminal.executeCommand('clc') + + // Type 'a' and then up arrow should recall 'a = 123;' + await vs.terminal.type('a') + await vs.terminal.type(Key.ARROW_UP) + await vs.terminal.assertContains('a = 123;', 'Up arrow after typing "a" should recall matching command') + await vs.terminal.type(Key.ESCAPE) + }); });