diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a6dca..4bf2138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.9] - 2026-03-09 + +### Added +- Support for displaying wide CJK (Chinese, Japanese, and Korean) characters in the MATLAB terminal. (Addresses [mathworks/MATLAB-extension-for-vscode#266](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/266) and [mathworks/MATLAB-extension-for-vscode#276](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/276)) + +### Fixed +- Resolves an issue starting MATLAB when the `HOME` environment variable is set to an invalid folder (Addresses [mathworks/MATLAB-extension-for-vscode#164](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/164)) +- Resolves an issue with extraneous debug sessions remaining after debugger is exited (Addresses [mathworks/MATLAB-extension-for-vscode#258](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/258)) +- Applied patches for CVE-2025-13465, CVE-2025-69873, CVE-2026-2327, CVE-2026-2391, CVE-2026-24001, CVE-2026-25547, CVE-2026-26996, CVE-2026-27601, CVE-2026-27903, and CVE-2026-27904 + ## [1.3.8] - 2026-01-09 ### Added diff --git a/package-lock.json b/package-lock.json index 911c7e9..170587e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "language-matlab", - "version": "1.3.8", + "version": "1.3.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.3.8", + "version": "1.3.9", "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.56.0", @@ -454,27 +454,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -726,10 +705,11 @@ } }, "node_modules/@secretlint/config-loader/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1498,6 +1478,29 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1576,15 +1579,16 @@ } }, "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1656,10 +1660,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2729,10 +2734,11 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4008,10 +4014,11 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5170,10 +5177,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -5422,10 +5430,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5516,10 +5525,11 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6362,9 +6372,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7390,10 +7400,11 @@ } }, "node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7557,14 +7568,27 @@ "node": ">=18" } }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/test-exclude/node_modules/glob": { @@ -7609,12 +7633,13 @@ "dev": true }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7848,10 +7873,11 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" }, "node_modules/undici-types": { "version": "5.26.5", @@ -8061,6 +8087,29 @@ "node": ">=4" } }, + "node_modules/vscode-extension-tester/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/vscode-extension-tester/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/vscode-extension-tester/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -8165,15 +8214,16 @@ } }, "node_modules/vscode-extension-tester/node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8213,10 +8263,11 @@ } }, "node_modules/vscode-extension-tester/node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -8335,9 +8386,10 @@ } }, "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8959,21 +9011,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true - }, - "@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "requires": { - "@isaacs/balanced-match": "^4.0.1" - } - }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -9159,9 +9196,9 @@ }, "dependencies": { "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -9651,6 +9688,21 @@ "color-convert": "^1.9.0" } }, + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, + "brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -9711,12 +9763,12 @@ }, "dependencies": { "minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "requires": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" } } } @@ -9849,9 +9901,9 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -10616,9 +10668,9 @@ "optional": true }, "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, "dir-glob": { @@ -11543,9 +11595,9 @@ } }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -12381,9 +12433,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true }, "lodash.includes": { @@ -12569,9 +12621,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -12640,9 +12692,9 @@ } }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -13262,9 +13314,9 @@ "dev": true }, "qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "requires": { "side-channel": "^1.1.0" @@ -13960,9 +14012,9 @@ }, "dependencies": { "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -14108,13 +14160,19 @@ "minimatch": "^9.0.4" }, "dependencies": { + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "requires": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" } }, "glob": { @@ -14148,12 +14206,12 @@ "dev": true }, "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" } }, "path-scurry": { @@ -14331,9 +14389,9 @@ } }, "underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true }, "undici-types": { @@ -14507,6 +14565,21 @@ "color-convert": "^1.9.0" } }, + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, + "brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -14584,12 +14657,12 @@ }, "dependencies": { "minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "requires": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" } } } @@ -14619,9 +14692,9 @@ } }, "markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -14709,9 +14782,9 @@ } }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "requires": { "brace-expansion": "^2.0.1" } diff --git a/package.json b/package.json index b23fd7a..76a623b 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.8", + "version": "1.3.9", "engines": { "vscode": "^1.67.0" }, diff --git a/server b/server index 96c428b..40f8239 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 96c428b0be6ba6f109a03036163d4a37b7d2bc7d +Subproject commit 40f8239ce683edca98af5c74485c46616c46a1c5 diff --git a/src/DefaultEditorService.ts b/src/DefaultEditorService.ts index a9d4136..65ed89b 100644 --- a/src/DefaultEditorService.ts +++ b/src/DefaultEditorService.ts @@ -1,4 +1,4 @@ -// Copyright 2025 The MathWorks, Inc. +// Copyright 2025-2026 The MathWorks, Inc. import * as fs from 'fs'; import * as os from 'os'; @@ -6,7 +6,7 @@ import { exec } from 'child_process'; import * as path from 'path' import * as vscode from 'vscode' import { LanguageClient } from 'vscode-languageclient/node'; -import { MatlabState, MVM } from './commandwindow/MVM' +import { MatlabMVMConnectionState, MVM } from './commandwindow/MVM' import Notification from './Notifications' export default class DefaultEditorService { @@ -19,12 +19,12 @@ export default class DefaultEditorService { }) ) - mvm.on(MVM.Events.stateChanged, (oldState: MatlabState, newState: MatlabState) => { + mvm.on(MVM.Events.stateChanged, (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => { if (oldState === newState) { return; } - if (newState === MatlabState.READY || newState === MatlabState.BUSY) { + if (newState === MatlabMVMConnectionState.CONNECTED) { if (!this.initialized) { this.initialized = true void this.handleConfigChanged() diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index aaeda9d..471c8f6 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -1,7 +1,7 @@ -// Copyright 2024-2025 The MathWorks, Inc. +// Copyright 2024-2026 The MathWorks, Inc. import * as vscode from 'vscode' -import { MVM, MatlabState } from './MVM' +import { MVM, MatlabMVMConnectionState } from './MVM' import { TextEvent, PromptState } from './MVMInterface' import { createResolvablePromise, Notifier, ResolvablePromise } from './Utilities'; import Notification from '../Notifications'; @@ -75,6 +75,10 @@ const ACTION_KEYS = { const LEFT_REGEX = /^(\x1b\[D)+$/; // eslint-disable-next-line no-control-regex const RIGHT_REGEX = /^(\x1b\[C)+$/; +// eslint-disable-next-line no-control-regex +const WIDE_CHAR_REGEX = /[\u3001-\u3015\u301C\u3040-\u30FF\u3131-\u314E\u3400-\u4DBF\u4e00-\u9FFF\uAC00-\uD7A3\uFF01-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B\uFF3C\uFF3D\uFF3F\uFF5B\uFF5D]/; + +const RELEASE_REGEX = /^R20([0-9]{2})(a|b)$/; const PROMPTS = { IDLE_PROMPT: '>> ', @@ -100,25 +104,30 @@ export default class CommandWindow implements vscode.Pseudoterminal { private readonly _writeEmitter: vscode.EventEmitter; private _initialized: boolean = false; - private _currentPrompt: string = PROMPTS.IDLE_PROMPT; + private _currentPrompt: string = PROMPTS.BUSY_PROMPT; private _currentState: PromptState = PromptState.INITIALIZING; private _currentPromptLine: string = this._currentPrompt; - private _cursorIndex: number = 0; - private _anchorIndex?: number = undefined; + + private _lastKnownCursorIndex: number = 0; + private _lastKnownAnchorIndex?: number = undefined; + private _lastKnownCursorLine: number = 0; + private _lastKnownDidTypeAtEndOfLine: boolean = false; + + private _activeCursorIndex: number = 0; + private _activeAnchorIndex?: number = undefined; + private _forcedEndOfLineCursorValue?: boolean = undefined; private _lastOutputLine: string = ''; private readonly _rawCommandHistory: string[] = []; private _historyIndex: number = 0; - private _lastKnownCurrentLine: string = ''; + private _lastKnownCurrentLineForHistory: string = ''; private _filteredCommandHistory: string[] = []; private _terminalDimensions: vscode.TerminalDimensions; private _lastSentTerminalDimensions: vscode.TerminalDimensions | null = null; - private _justTypedLastInColumn: boolean = false; - private readonly _notifier: Notifier; private _latestTabCompletionData?: CompletionList; @@ -158,62 +167,136 @@ export default class CommandWindow implements vscode.Pseudoterminal { } this._writeEmitter.fire(ACTION_KEYS.SET_CURSOR_STYLE_TO_BAR); + this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY); + this._resetLastKnown(); const currentMatlabState = this._mvm.getMatlabState(); - if (currentMatlabState === MatlabState.READY) { + if (currentMatlabState === MatlabMVMConnectionState.CONNECTED) { this._initialized = true; - this._writeCurrentPromptLine(); - } else if (currentMatlabState === MatlabState.DISCONNECTED) { - this._clearState(); - this._initialized = false; + this._doUpdate(); + } else if (currentMatlabState === MatlabMVMConnectionState.DISCONNECTED) { this._currentState = PromptState.INITIALIZING; - } else if (currentMatlabState === MatlabState.BUSY) { - this._clearState(); - this._initialized = true; + this._currentPrompt = PROMPTS.BUSY_PROMPT; + this._clearAndResetToCurrentPrompt(); + this._initialized = false; } } close (): void { - // Unimplemented + this._resetLastKnown(); } - /** - * Resets the terminal state - */ - private _clearState (): void { - this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY) - this._setToEmptyPrompt(); - this._lastSentTerminalDimensions = null; + private _doEdit (callback: () => void): void { + this._activeAnchorIndex = this._lastKnownAnchorIndex; + this._activeCursorIndex = this._lastKnownCursorIndex; + this._forcedEndOfLineCursorValue = undefined; + + callback(); + + this._doUpdate(); } - private _handleMatlabStateChange (oldState: MatlabState, newState: MatlabState): void { + private _doUpdate (): void { + this._eraseExistingPromptLine(); + this._writeCurrentPromptLine(); + + const actualCursorPositionAfterJustRetyping = this._indexToLineColumnForCurrentPromptLine(this._getMaxIndexOnLine(), true); + + if (this._forcedEndOfLineCursorValue === undefined) { + this._lastKnownDidTypeAtEndOfLine = this._activeCursorIndex === this._getMaxIndexOnLine() && this._indexToLineColumnForCurrentPromptLine(this._getMaxIndexOnLine(), false).column === 0; + } else { + this._lastKnownDidTypeAtEndOfLine = this._forcedEndOfLineCursorValue; + } + + this._moveCursorToCurrent(actualCursorPositionAfterJustRetyping.line); + + this._lastKnownCursorIndex = this._activeCursorIndex; + this._lastKnownAnchorIndex = this._activeAnchorIndex; + const location = this._indexToLineColumnForCurrentPromptLine(this._lastKnownCursorIndex, this._lastKnownDidTypeAtEndOfLine); + this._lastKnownCursorLine = location.line; + + this._updateHasSelectionContext(); + } + + private _writeCurrentPromptLine (): void { + if (this._activeAnchorIndex === undefined) { + this._writeEmitter.fire(this._currentPromptLine) + } else { + const selectionStart = this._currentPrompt.length + Math.min(this._activeCursorIndex, this._activeAnchorIndex); + const selectionEnd = this._currentPrompt.length + Math.max(this._activeCursorIndex, this._activeAnchorIndex); + 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); + } + } + + private _moveCursorToCurrent (currentCursorLine: number): void { + const lineColumnCursorShouldBeOn = this._indexToLineColumnForCurrentPromptLine(this._activeCursorIndex, this._lastKnownDidTypeAtEndOfLine); + const lineOfInputCursorIsCurrentlyOn = currentCursorLine; + + const key = (lineColumnCursorShouldBeOn.line > lineOfInputCursorIsCurrentlyOn ? ACTION_KEYS.DOWN : ACTION_KEYS.UP); + const count = Math.abs(lineColumnCursorShouldBeOn.line - lineOfInputCursorIsCurrentlyOn); + this._writeEmitter.fire(key.repeat(count)); + this._writeEmitter.fire(ACTION_KEYS.MOVE_TO_POSITION_IN_LINE(lineColumnCursorShouldBeOn.column + 1)); + } + + private _eraseExistingPromptLine (): void { + const numberOfLinesBehind = this._lastKnownCursorLine; + if (numberOfLinesBehind !== 0) { + for (let i = 0; i < numberOfLinesBehind; i++) { + this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING + ACTION_KEYS.UP); + } + } + this._writeEmitter.fire(ACTION_KEYS.CLEAR_AND_MOVE_TO_BEGINNING) + } + + private _resetLastKnown (): void { + this._lastKnownCursorIndex = 0; + this._lastKnownAnchorIndex = undefined; + this._lastKnownCursorLine = 0; + } + + private _handleMatlabStateChange (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState): void { if (oldState === newState) { return; } - if (newState === MatlabState.READY) { - this._clearState(); + if (newState === MatlabMVMConnectionState.CONNECTED) { this._initialized = true; - this._writeCurrentPromptLine(); - } else if (newState === MatlabState.DISCONNECTED) { - this._clearState(); + } + + this._clearAndResetToCurrentPrompt(); + this._doUpdate(); + + if (newState === MatlabMVMConnectionState.DISCONNECTED) { this._initialized = false; - } else if (newState === MatlabState.BUSY) { - this._clearState(); - this._initialized = true; } } + /** + * Resets the terminal state + */ + private _clearAndResetToCurrentPrompt (): void { + this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY); + this._setToEmptyPrompt(); + this._resetLastKnown(); + this._lastSentTerminalDimensions = null; + } + /** * Clear current line and selection */ private _setToEmptyPrompt (): void { this._currentPromptLine = this._currentPrompt; - this._lastKnownCurrentLine = ''; - this._cursorIndex = 0; - this._anchorIndex = undefined; + this._lastKnownCurrentLineForHistory = ''; + this._activeAnchorIndex = undefined; + this._activeCursorIndex = 0; this._updateHasSelectionContext(); - this._updateWhetherJustTypedInLastColumn(); } /** @@ -221,10 +304,11 @@ export default class CommandWindow implements vscode.Pseudoterminal { * @param command */ insertCommandForEval (command: string): void { - if (this._currentPromptLine !== this._currentPrompt) { - this._setToEmptyPrompt(); - this._writeCurrentPromptLine(); - } + this._doEdit(() => { + if (this._currentPromptLine !== this._currentPrompt) { + this._setToEmptyPrompt(); + } + }); this.handleInput(command + ACTION_KEYS.NEWLINE); } @@ -260,9 +344,9 @@ export default class CommandWindow implements vscode.Pseudoterminal { return; } - if (this._handleActionKeys(data)) { - this._writeCurrentPromptLine(); - } + this._doEdit(() => { + this._handleActionKeys(data); + }); return; } @@ -272,31 +356,42 @@ export default class CommandWindow implements vscode.Pseudoterminal { const lines = this._preprocessInputLines(data); if (isOutput) { - for (let i = 0; i < lines.length; i++) { - this._handleOutputLine(lines[i], i !== lines.length - 1); - } + this._doEdit(() => { + for (let i = 0; i < lines.length; i++) { + this._handleOutputLine(lines[i], i !== lines.length - 1); + } + }); } else { this._invalidateCompletionData(); // Case 1: Normal typing if (lines.length === 1) { - this._handleLine(lines[0]); - + this._doEdit(() => { + 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(); + this._doEdit(() => { + this._handleLine(lines[0]); + }); + this._doEdit(() => { + 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(); + this._doEdit(() => { + for (let i = 0; i < lines.length; i++) { + this._handleLine(lines[i] + ((i === lines.length - 1) ? '' : ACTION_KEYS.NEWLINE)); + } + }); + this._doEdit(() => { + this._handleEnter(); + }); } } } private _handleOutputLine (line: string, implicitNewlineAtEnd: boolean): void { - const numberOfLinesBehind = Math.floor(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); + const numberOfLinesBehind = this._indexToLineColumnForCurrentPromptLine(this._lastKnownCursorIndex, this._lastKnownDidTypeAtEndOfLine).line; if (numberOfLinesBehind !== 0) { this._writeEmitter.fire(ACTION_KEYS.UP.repeat(numberOfLinesBehind)); } @@ -316,8 +411,6 @@ export default class CommandWindow implements vscode.Pseudoterminal { if (this._lastOutputLine.length !== 0) { this._writeEmitter.fire(ACTION_KEYS.NEWLINE) } - - this._writeCurrentPromptLine(false); } private _handleOutputNewline (): void { @@ -403,7 +496,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { } if (isAtEnd) { - this._lastKnownCurrentLine = this._stripCurrentPrompt(this._currentPromptLine); + this._lastKnownCurrentLineForHistory = this._stripCurrentPrompt(this._currentPromptLine); } this._historyIndex += direction === Direction.BACKWARDS ? -1 : 1; @@ -421,71 +514,56 @@ export default class CommandWindow implements vscode.Pseudoterminal { } this._historyIndex = this._filteredCommandHistory.length; - this._lastKnownCurrentLine = ''; + this._lastKnownCurrentLineForHistory = ''; } private _possiblyUpdateAnchorForCursorChange (policy: AnchorPolicy): boolean { let isLineDirty = false; - if (policy === AnchorPolicy.MOVE && this._anchorIndex !== undefined) { - this._anchorIndex = undefined; + if (policy === AnchorPolicy.MOVE && this._activeAnchorIndex !== undefined) { + this._activeAnchorIndex = undefined; isLineDirty = true; } else if (policy === AnchorPolicy.KEEP) { - if (this._anchorIndex === undefined) { - this._anchorIndex = this._cursorIndex; + if (this._activeAnchorIndex === undefined) { + this._activeAnchorIndex = this._activeCursorIndex; } isLineDirty = true; } - this._updateHasSelectionContext(); return isLineDirty; } private _handleEnd (anchorPolicy: AnchorPolicy): boolean { - const currentCursorLine = Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); const isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); - this._cursorIndex = this._getMaxIndexOnLine(); - this._moveCursorToCurrent(currentCursorLine); + const oldCursorIndex = this._activeCursorIndex; + this._activeCursorIndex = this._getMaxIndexOnLine(); + if (this._activeCursorIndex !== oldCursorIndex) { + this._forcedEndOfLineCursorValue = false; + } return isLineDirty; } private _handleHome (anchorPolicy: AnchorPolicy): boolean { - const currentCursorLine = Math.ceil(this._getAbsoluteIndexOnLine(this._cursorIndex) / this._terminalDimensions.columns); const isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); - this._cursorIndex = 0; - this._moveCursorToCurrent(currentCursorLine); + this._activeCursorIndex = 0; + this._forcedEndOfLineCursorValue = false; return isLineDirty; } private _handleLeftRight (direction: CursorDirection, anchorPolicy: AnchorPolicy): boolean { - let isLineDirty = false; - if (direction === CursorDirection.LEFT && this._cursorIndex !== 0) { - if (this._justTypedLastInColumn) { + let isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); + + isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); + + if (direction === CursorDirection.LEFT && this._activeCursorIndex !== 0) { + if (this._lastKnownDidTypeAtEndOfLine) { // Don't actually move the cursor, but do move the index we think the cursor is at. - 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)); - } else { - this._writeEmitter.fire(ACTION_KEYS.LEFT); - } + this._forcedEndOfLineCursorValue = false; } - isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); - this._cursorIndex--; + this._activeCursorIndex--; } - if (direction === CursorDirection.RIGHT && this._cursorIndex !== this._getMaxIndexOnLine()) { - if (this._justTypedLastInColumn) { - // Not possible - } else { - if (this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === (this._terminalDimensions.columns - 1)) { - this._writeEmitter.fire(ACTION_KEYS.DOWN + ACTION_KEYS.MOVE_TO_POSITION_IN_LINE(0)); - } else { - this._writeEmitter.fire(ACTION_KEYS.RIGHT); - } - } - - isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); - this._cursorIndex++; + if (direction === CursorDirection.RIGHT && this._activeCursorIndex !== this._getMaxIndexOnLine()) { + this._activeCursorIndex++; } this._invalidateCompletionData(); @@ -501,123 +579,85 @@ export default class CommandWindow implements vscode.Pseudoterminal { } private _handleBackspace (): boolean { - if (this._anchorIndex !== undefined) { + if (this._activeAnchorIndex !== undefined) { return this._removeSelection(); } - if (this._cursorIndex === 0) { + if (this._activeCursorIndex === 0) { return false; } - const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex) - 1); - const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._activeCursorIndex) - 1); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._activeCursorIndex)); this._currentPromptLine = before + after; - this._cursorIndex--; + this._activeCursorIndex--; + this._forcedEndOfLineCursorValue = false; this._markCurrentLineChanged(); this._invalidateCompletionData(); return true; } private _handleSelectAll (): boolean { - this._cursorIndex = this._getMaxIndexOnLine(); - this._anchorIndex = 0; + this._forcedEndOfLineCursorValue = false; + this._activeCursorIndex = this._getMaxIndexOnLine(); + this._activeAnchorIndex = 0; this._updateHasSelectionContext(); this._invalidateCompletionData(); return true; } private _handleDelete (): boolean { - if (this._anchorIndex !== undefined) { + if (this._activeAnchorIndex !== undefined) { return this._removeSelection(); } - if (this._cursorIndex === this._getMaxIndexOnLine()) { + if (this._activeCursorIndex === this._getMaxIndexOnLine()) { return false; } - const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); - const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex) + 1); + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._activeCursorIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._activeCursorIndex) + 1); this._currentPromptLine = before + after; this._markCurrentLineChanged(); this._invalidateCompletionData(); return true; } - private _writeCurrentPromptLine (eraseExisting: boolean = true): void { - if (eraseExisting) { - this._eraseExistingPromptLine(); - } - if (this._anchorIndex === undefined) { - 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._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._currentPromptLine.length / this._terminalDimensions.columns); - this._moveCursorToCurrent(currentCursorLine); - } - - 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, cursorIndex?: number): boolean { - this._eraseExistingPromptLine(); this._currentPromptLine = updatedLine; - this._cursorIndex = cursorIndex ?? this._getMaxIndexOnLine(); - this._anchorIndex = undefined; - this._updateWhetherJustTypedInLastColumn(); - this._writeCurrentPromptLine(false); - return false; + this._activeCursorIndex = cursorIndex ?? this._getMaxIndexOnLine(); + this._activeAnchorIndex = undefined; + return true; } private _removeSelection (): boolean { - if (this._anchorIndex === undefined || this._cursorIndex === this._anchorIndex) { - this._anchorIndex = undefined; - this._updateHasSelectionContext(); + if (this._activeAnchorIndex === undefined || this._activeCursorIndex === this._activeAnchorIndex) { + this._activeAnchorIndex = undefined; return false; } - const selectionStart = this._getAbsoluteIndexOnLine(Math.min(this._cursorIndex, this._anchorIndex)); - const selectionEnd = this._getAbsoluteIndexOnLine(Math.max(this._cursorIndex, this._anchorIndex)); + const selectionStart = this._getAbsoluteIndexOnLine(Math.min(this._activeCursorIndex, this._activeAnchorIndex)); + const selectionEnd = this._getAbsoluteIndexOnLine(Math.max(this._activeCursorIndex, this._activeAnchorIndex)); 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._updateHasSelectionContext(); + this._activeCursorIndex = selectionStart - this._currentPrompt.length; + this._activeAnchorIndex = undefined; return true; } private _handleLine (line: string): void { - if (this._removeSelection()) { - this._writeCurrentPromptLine(); - } + this._removeSelection() - if (this._cursorIndex === this._getMaxIndexOnLine()) { + if (this._activeCursorIndex === this._getMaxIndexOnLine()) { this._currentPromptLine += line; - this._cursorIndex += line.length; - this._writeEmitter.fire(line); + this._activeCursorIndex += line.length; } else { - const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._cursorIndex)); - const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._cursorIndex)); + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._activeCursorIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._activeCursorIndex)); this._currentPromptLine = before + line + after; - this._cursorIndex += line.length; - this._writeCurrentPromptLine(); + this._activeCursorIndex += line.length; } this._markCurrentLineChanged(); - this._justTypedLastInColumn = this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns === 0; } private _handleEnter (): void { @@ -628,22 +668,18 @@ export default class CommandWindow implements vscode.Pseudoterminal { 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(); + this._activeCursorIndex = 0; + this._activeAnchorIndex = undefined; + this._lastKnownCurrentLineForHistory = this._stripCurrentPrompt(this._currentPromptLine); this._invalidateCompletionData(); void this._evaluateCommand(stringToEvaluate); } private _addToHistory (command: string): void { const isEmpty = command === ''; - const isLastInHistory = - this._rawCommandHistory.length !== 0 && - command === this._rawCommandHistory[this._rawCommandHistory.length - 1]; - if (!isEmpty && !isLastInHistory) { + const isLastInHistory = this._rawCommandHistory.length !== 0 && command === this._rawCommandHistory[this._rawCommandHistory.length - 1]; + const isMultiLine = command.includes('\n'); + if (!isEmpty && !isLastInHistory && !isMultiLine) { this._rawCommandHistory.push(command); } this._historyIndex = this._rawCommandHistory.length; @@ -653,21 +689,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { private _getHistoryItem (n: number): string { return (this._historyIndex < this._filteredCommandHistory.length) ? this._filteredCommandHistory[n] - : this._lastKnownCurrentLine; - } - - private _moveCursorToCurrent (lineOfInputCursorIsCurrentlyOn?: number): void { - 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) { - this._writeEmitter.fire(ACTION_KEYS.UP.repeat(lineOfInputCursorIsCurrentlyOn - lineNumberCursorShouldBeOn)); - } - this._writeEmitter.fire(ACTION_KEYS.MOVE_TO_POSITION_IN_LINE((this._getAbsoluteIndexOnLine(this._cursorIndex) % this._terminalDimensions.columns) + 1)); + : this._lastKnownCurrentLineForHistory; } setDimensions (dimensions: vscode.TerminalDimensions): void { @@ -676,7 +698,16 @@ export default class CommandWindow implements vscode.Pseudoterminal { private _sendTerminalDimensionsIfNeeded (): void { if ((this._lastSentTerminalDimensions == null) || this._lastSentTerminalDimensions.columns !== this._terminalDimensions.columns || this._lastSentTerminalDimensions.rows !== this._terminalDimensions.rows) { - void this._mvm.eval(`try; if usejava('jvm'); com.mathworks.mde.cmdwin.CmdWinMLIF.setCWSize(${this._terminalDimensions.rows}, ${this._terminalDimensions.columns}); end; end;`); + const release = this._mvm.getMatlabRelease(); + if (release === null) { + return; + } + const match = release.match(RELEASE_REGEX); + if (match?.[1] !== undefined && Number.parseInt(match[1]) < 25) { + void this._mvm.eval(`try; if usejava('jvm'); com.mathworks.mde.cmdwin.CmdWinMLIF.setCWSize(${this._terminalDimensions.rows}, ${this._terminalDimensions.columns}); end; end;`); + } else { + void this._mvm.eval(`settings_vscode__ = settings; settings_vscode__.matlab.commandwindow.WindowSize.TemporaryValue = [${this._terminalDimensions.rows}, ${this._terminalDimensions.columns}]; clear settings_vscode__;`); + } this._lastSentTerminalDimensions = this._terminalDimensions; } } @@ -706,21 +737,23 @@ export default class CommandWindow implements vscode.Pseudoterminal { * Clears the command window, and also wipes out the terminal's scroll history as well. */ clear (): void { - this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY) - this._setToEmptyPrompt(); + this._doEdit(() => { + this._setToEmptyPrompt(); + }); + this._writeEmitter.fire(ACTION_KEYS.CLEAR_COMPLETELY); } private _updateHasSelectionContext (): void { - void vscode.commands.executeCommand('setContext', 'matlab.terminalHasSelection', this._anchorIndex !== undefined); + void vscode.commands.executeCommand('setContext', 'matlab.terminalHasSelection', this._activeAnchorIndex !== undefined); } private _handleCopy (): boolean { - if (this._anchorIndex === undefined) { + if (this._activeAnchorIndex === undefined) { 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 selectionStart = this._currentPrompt.length + Math.min(this._activeCursorIndex, this._activeAnchorIndex); + const selectionEnd = this._currentPrompt.length + Math.max(this._activeCursorIndex, this._activeAnchorIndex); const selection = this._currentPromptLine.slice(selectionStart, selectionEnd); void vscode.env.clipboard.writeText(selection); return false; @@ -792,7 +825,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { let i; for (i = 0; i < cumulativeLengths.length; i++) { - if (this._cursorIndex <= cumulativeLengths[i]) { + if (this._activeCursorIndex <= cumulativeLengths[i]) { break; } } @@ -812,8 +845,8 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._replaceCurrentLineWithNewLine(this._currentPrompt + newLine, codeBefore.length + currentCompletion.length); } else { // Otherwise we want to just insert the new completion directly at the cursor. - const codeBefore = currentLine.substring(0, this._cursorIndex); - const codeAfter = currentLine.substring(this._cursorIndex); + const codeBefore = currentLine.substring(0, this._activeCursorIndex); + const codeAfter = currentLine.substring(this._activeCursorIndex); // And construct the new line with the replacement made const newLine = codeBefore + currentCompletion + codeAfter; @@ -830,7 +863,7 @@ export default class CommandWindow implements vscode.Pseudoterminal { } else { // Otherwise, request new completion data and do a completion when the data has come in. const code = this._stripCurrentPrompt(this._currentPromptLine); - const offset = this._cursorIndex; + const offset = this._activeCursorIndex; if (code.trim() === '') { return false; } @@ -838,7 +871,11 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._requestCompletionData(code, offset).then((completions: CompletionList) => { this._latestTabCompletionData = completions; this._currentCompletionIndex = 0; - this._doCompletion(); + setTimeout(() => { + this._doEdit(() => { + this._doCompletion(); + }); + }, 250); }, () => { /* intentionally empty */ }); } return true; @@ -860,56 +897,74 @@ export default class CommandWindow implements vscode.Pseudoterminal { } 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(); + this._doEdit(() => { + if (this._currentPrompt !== PROMPTS.BUSY_PROMPT) { + this._currentPromptLine = this._stripCurrentPrompt(this._currentPromptLine); + } + this._currentPrompt = prompt; + this._currentPromptLine = this._currentPrompt + this._currentPromptLine; + }); } 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; - } + private _indexToLineColumnForCurrentPromptLine (index: number, didTypeAtEndOfLine: boolean): { line: number, column: number} { + let line = 0; + let column = 0; + const absoluteIndex = this._getAbsoluteIndexOnLine(index); + for (let i = 0; i < absoluteIndex; i++) { + const isWideChar = this._isDoubleWidthCharacter(this._currentPromptLine.charAt(i)); - onDidWrite: vscode.Event; - onDidOverrideDimensions?: vscode.Event | undefined; - onDidClose?: vscode.Event | undefined; - onDidChangeName?: vscode.Event | undefined; + const shouldHandleLastCharacterInColumn = didTypeAtEndOfLine && i === absoluteIndex - 1; - /** - * - * @param data Helper used to log input is a readible manner - */ - private _logInput (data: string): void { - let shouldPrint = false; - let s = '['; - const prefix = '' - for (let i = 0; i < data.length; i++) { - let ch = data[i]; - if (data.charCodeAt(i) === 0x1b) { - ch = 'ESC' - shouldPrint = true; + if (!isWideChar) { + if (column === this._terminalDimensions.columns - 1) { + if (!shouldHandleLastCharacterInColumn) { + column = 0; + line++; + } else { + column++; + } + } else { + column++; + } } else { - if (ch.match(/[a-z0-9,./;'[\]\\`~!@#$%^&*()_+\-=|:'{}<>?]/i) === null) { - let hex = data.charCodeAt(i).toString(16); - if (hex.length === 1) { - hex = '0' + hex; + if (column === this._terminalDimensions.columns - 2) { + if (!shouldHandleLastCharacterInColumn) { + column = 0; + line++; } - ch = '\\x' + hex; - shouldPrint = true; + } else if (column === this._terminalDimensions.columns - 3) { + const isNextCharWide = i + 1 < this._currentPromptLine.length && this._isDoubleWidthCharacter(this._currentPromptLine.charAt(i + 1)); + if (isNextCharWide) { + column = 0; + line++; + } else { + column += 2; + } + } else if (column === this._terminalDimensions.columns - 1) { + column = 2; + line++; + } else { + column += 2; } } - s += prefix + ch; - } - s += ']' - if (shouldPrint) { - console.log(s); } + + return { + line: line, + column: column + }; } + + private _isDoubleWidthCharacter (char: string): boolean { + return WIDE_CHAR_REGEX.test(char); + } + + onDidWrite: vscode.Event; + onDidOverrideDimensions?: vscode.Event | undefined; + onDidClose?: vscode.Event | undefined; + onDidChangeName?: vscode.Event | undefined; } diff --git a/src/commandwindow/MVM.ts b/src/commandwindow/MVM.ts index 3b4c0c0..b8f7d7c 100644 --- a/src/commandwindow/MVM.ts +++ b/src/commandwindow/MVM.ts @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The MathWorks, Inc. +// Copyright 2024-2026 The MathWorks, Inc. import { TextEvent, FEvalResponse, EvalResponse, MVMError, BreakpointResponse, Capability } from './MVMInterface' import { createResolvablePromise, ResolvablePromise, Notifier } from './Utilities' @@ -8,10 +8,9 @@ import EventEmitter = require('events') /** * The current state of MATLAB */ -export enum MatlabState { +export enum MatlabMVMConnectionState { DISCONNECTED = 'disconnected', - READY = 'ready', - BUSY = 'busy' + CONNECTED = 'connected' } interface MatlabStateUpdate { @@ -41,8 +40,8 @@ export class MVM extends EventEmitter { private readonly _notifier: Notifier; - private readonly _stateObservers: Array<(oldState: MatlabState, newState: MatlabState) => void> = []; - private _currentState: MatlabState = MatlabState.DISCONNECTED; + private readonly _stateObservers: Array<(oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => void> = []; + private _currentState: MatlabMVMConnectionState = MatlabMVMConnectionState.DISCONNECTED; private _currentRelease: string | null = null; private _currentReadyPromise: ResolvablePromise; @@ -90,11 +89,8 @@ export class MVM extends EventEmitter { * * @returns The current state of MATLAB */ - getMatlabState (): MatlabState { - if (this._currentState === MatlabState.DISCONNECTED) { - return this._currentState; - } - return this._pendingUserEvals > 0 ? MatlabState.BUSY : MatlabState.READY; + getMatlabState (): MatlabMVMConnectionState { + return this._currentState; } /** @@ -110,21 +106,21 @@ export class MVM extends EventEmitter { * @returns The current release of MATLAB */ isDebugging (): boolean { - return this.getMatlabState() !== MatlabState.DISCONNECTED && this._isCurrentlyDebugging; + return this.getMatlabState() !== MatlabMVMConnectionState.DISCONNECTED && this._isCurrentlyDebugging; } private _handleMatlabStateChange (newState: MatlabStateUpdate): void { const oldState = this._currentState; - this._currentState = MatlabState[newState.state.toUpperCase() as keyof typeof MatlabState]; + this._currentState = MatlabMVMConnectionState[newState.state.toUpperCase() as keyof typeof MatlabMVMConnectionState]; this._currentRelease = newState.release; - if (this._currentState === MatlabState.DISCONNECTED) { + if (this._currentState === MatlabMVMConnectionState.DISCONNECTED) { this._handleDisconnection(); } this.emit(MVM.Events.stateChanged, oldState, this._currentState); - if (this._currentState !== MatlabState.DISCONNECTED) { + if (this._currentState !== MatlabMVMConnectionState.DISCONNECTED) { this._pendingUserEvals = 0; this._currentReadyPromise.resolve(); } diff --git a/src/debug/BreakpointSynchronizer.ts b/src/debug/BreakpointSynchronizer.ts new file mode 100644 index 0000000..9f90c21 --- /dev/null +++ b/src/debug/BreakpointSynchronizer.ts @@ -0,0 +1,163 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as vscode from 'vscode' +import { DebugProtocol } from '@vscode/debugprotocol'; +import { Disposable } from 'vscode'; +import { MatlabMVMConnectionState, MVM } from '../commandwindow/MVM'; + +export default class BreakpointSynchronizer { + private readonly _mvm: MVM; + private _tracking: boolean = false; + private _listeners: Disposable[] = []; + private _timer: NodeJS.Timeout | null = null; + private readonly _requestDispatcher: (request: DebugProtocol.Request) => void; + + private _forcedOff: boolean = true; + + private readonly _dirtyFiles: Set = new Set(); + + constructor (mvm: MVM, requestDispatcher: (request: DebugProtocol.Request) => void) { + this._mvm = mvm; + this._requestDispatcher = requestDispatcher; + + this._mvm.on(MVM.Events.stateChanged, (oldState, newState) => { + if (this._forcedOff) { + return; + } + + if (newState !== MatlabMVMConnectionState.DISCONNECTED) { + this._startTracking(); + } else { + this._stopTracking(); + } + }); + } + + disable (): void { + this._forcedOff = true; + this._stopTracking(); + } + + enable (): void { + this._forcedOff = false; + this._maybeStartTracking(); + } + + _maybeStartTracking (): void { + if (this._mvm.getMatlabState() !== MatlabMVMConnectionState.DISCONNECTED) { + this._startTracking(); + } + } + + _startTracking (): void { + if (this._tracking) { + return; + } + + this._tracking = true; + + this._listeners = []; + + this._listeners.push(vscode.debug.onDidChangeBreakpoints((event) => { + event.added.filter((b) => b instanceof vscode.SourceBreakpoint).forEach((breakpoint) => { + this._dirtyFiles.add(breakpoint.location.uri.fsPath); + }); + event.changed.filter((b) => b instanceof vscode.SourceBreakpoint).forEach((breakpoint) => { + this._dirtyFiles.add(breakpoint.location.uri.fsPath); + }); + event.removed.filter((b) => b instanceof vscode.SourceBreakpoint).forEach((breakpoint) => { + this._dirtyFiles.add(breakpoint.location.uri.fsPath); + }); + this._triggerUpdate(); + })); + + this._listeners.push(vscode.workspace.onDidSaveTextDocument((event) => { + if (this._dirtyFiles.has(event.uri.fsPath)) { + this._triggerUpdate(); + } + })); + + this._handleBreakpointsChanged(true); + } + + _triggerUpdate (): void { + if (this._timer == null) { + this._timer = setTimeout(this._handleBreakpointsChanged.bind(this, false), 1000); + } + } + + /** + * On breakpoint change, send relevant breakpoint update events to the server as though they were DAP events. + * @param refreshAll + */ + _handleBreakpointsChanged (refreshAll: boolean = true): void { + this._timer = null; + const breakpointRequests = {} as { [path: string]: DebugProtocol.SetBreakpointsRequest }; + + const createSetBreakpointRequest = (path: string): DebugProtocol.SetBreakpointsRequest => ({ + arguments: { + source: { + path: path + }, + breakpoints: [] + }, + type: 'request', + command: 'setBreakpoints', + seq: 0 + }); + + // Untitled and dirty files should not be included in the updates. + const ignoredFiles = new Set(); + vscode.workspace.textDocuments.forEach((document) => { + if (document.isDirty || document.isUntitled) { + ignoredFiles.add(document.uri.fsPath); + } + }); + + // Create empty breakpoint request for each file that we have had breakpoint change events for (and that are not ignored). + const relevantDirtyFilePaths = Array.from(this._dirtyFiles.values()).filter((path) => !ignoredFiles.has(path)); + relevantDirtyFilePaths.forEach((path) => { + breakpointRequests[path] = createSetBreakpointRequest(path); + }); + + // For each breakpoint in VS Code, optionally filter out based on whether we are doing a full refresh, or only sending changed breakpoints. + vscode.debug.breakpoints.filter((b) => b instanceof vscode.SourceBreakpoint).filter((breakpoint) => { + const path = breakpoint.location.uri.fsPath; + if (refreshAll) { + return true; + } else { + return this._dirtyFiles.has(path) && !ignoredFiles.has(path); + } + }) + .forEach((breakpoint) => { + // For each breakpoint, update the request structure with the current data. + const path = breakpoint.location.uri.fsPath; + if (breakpointRequests[path] === undefined) { + breakpointRequests[path] = createSetBreakpointRequest(path); + } + breakpointRequests[path].arguments.breakpoints?.push({ + line: breakpoint.location.range.start.line + 1, + condition: breakpoint.condition, + hitCondition: breakpoint.hitCondition, + logMessage: breakpoint.logMessage + }); + }); + + // Send each request object to the MATLAB Debug Adaptor + Object.values(breakpointRequests).forEach((request) => { + this._requestDispatcher(request); + }); + + // The remoev any handled files from the list of dirty files. + relevantDirtyFilePaths.forEach((path) => { + this._dirtyFiles.delete(path); + }); + } + + _stopTracking (): void { + this._tracking = false; + this._dirtyFiles.clear(); + this._listeners.forEach((listener) => listener.dispose()); + this._listeners = []; + } +} diff --git a/src/debug/MatlabDebugAdaptor.ts b/src/debug/MatlabDebugAdaptor.ts index 798f402..39e88cb 100644 --- a/src/debug/MatlabDebugAdaptor.ts +++ b/src/debug/MatlabDebugAdaptor.ts @@ -1,10 +1,10 @@ -// Copyright 2024 The MathWorks, Inc. +// Copyright 2024-2026 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 { MVM, MatlabMVMConnectionState } from '../commandwindow/MVM' import Notification from '../Notifications' class PackagedRequest { @@ -29,9 +29,6 @@ interface PackagedEvent { 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; @@ -39,24 +36,19 @@ export default class MatlabDebugAdaptor extends debug.DebugSession { private _isMatlabConnected: boolean = false; private _isStarted: boolean = false; - constructor (mvm: MVM, notifier: Notifier, baseSessionGetter: (dontAutoStart: boolean) => Promise, isBase: boolean) { + constructor (mvm: MVM, notifier: Notifier) { 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(); - } + this._setupDocumentListeners(); } private _isLifecycleEvent (event: DebugProtocol.Event): boolean { @@ -65,14 +57,8 @@ export default class MatlabDebugAdaptor extends debug.DebugSession { 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 (this._isStarted) { + this.sendEvent(event); } if (event.event === 'terminate') { @@ -100,12 +86,12 @@ export default class MatlabDebugAdaptor extends debug.DebugSession { } private _setupDocumentListeners (): void { - this._mvm.on(MVM.Events.stateChanged, (oldState: MatlabState, newState: MatlabState) => { + this._mvm.on(MVM.Events.stateChanged, (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => { if (oldState === newState) { return; } - if (newState === MatlabState.DISCONNECTED) { + if (newState === MatlabMVMConnectionState.DISCONNECTED) { this._isMatlabConnected = false; } else { if (!this._isMatlabConnected) { @@ -138,7 +124,7 @@ export default class MatlabDebugAdaptor extends debug.DebugSession { super.sendErrorResponse(response, codeOrMessage, format, variables, dest); } - protected dispatchRequest (request: DebugProtocol.Request): void { + dispatchRequest (request: DebugProtocol.Request): void { if (request.command === 'initialize') { this._isStarted = true; } diff --git a/src/debug/MatlabDebugger.ts b/src/debug/MatlabDebugger.ts index c8f3c6f..87cd77b 100644 --- a/src/debug/MatlabDebugger.ts +++ b/src/debug/MatlabDebugger.ts @@ -1,85 +1,58 @@ -// Copyright 2024-2025 The MathWorks, Inc. +// Copyright 2024-2026 The MathWorks, Inc. import * as vscode from 'vscode' import { Notifier } from '../commandwindow/Utilities'; -import { MVM, MatlabState } from '../commandwindow/MVM' +import { MVM, MatlabMVMConnectionState } from '../commandwindow/MVM' import MatlabDebugAdaptor from './MatlabDebugAdaptor'; -import Notification from '../Notifications'; import TelemetryLogger from '../telemetry/TelemetryLogger'; +import BreakpointSynchronizer from './BreakpointSynchronizer'; +import TerminalService from '../commandwindow/TerminalService'; export default class MatlabDebugger { - private readonly _baseDebugAdaptor: MatlabDebugAdaptor; - private readonly _nestedDebugAdaptor: MatlabDebugAdaptor; + private readonly _adaptor: MatlabDebugAdaptor; private readonly _mvm: MVM; private readonly _notifier: Notifier; private readonly _telemetryLogger: TelemetryLogger; + private readonly _terminalService: TerminalService; - private _isDebugAdaptorStarted: boolean = false; + private readonly _breakpointSynchronizer: BreakpointSynchronizer; - private _baseDebugSession: vscode.DebugSession | null = null; - private readonly _activeSessions: Set = new Set(); + private _activeSession?: vscode.DebugSession; + + private _shouldAutoStart: boolean = false; private _hasSentNotification: boolean = false; - constructor (mvm: MVM, notifier: Notifier, telemetryLogger: TelemetryLogger) { + constructor (mvm: MVM, notifier: Notifier, telemetryLogger: TelemetryLogger, terminalService: TerminalService) { 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._terminalService = terminalService; + + this._adaptor = new MatlabDebugAdaptor(mvm, notifier); + this._breakpointSynchronizer = new BreakpointSynchronizer(mvm, this._adaptor.dispatchRequest.bind(this._adaptor)); this._initialize(); + this._shouldAutoStart = vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? false; + if (this._shouldAutoStart) { + this._breakpointSynchronizer.enable(); + } else { + this._breakpointSynchronizer.disable(); + } + 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); + this._shouldAutoStart = vscode.workspace.getConfiguration('MATLAB')?.get('startDebuggerAutomatically') ?? false; + if (this._shouldAutoStart) { + this._breakpointSynchronizer.enable(); + void this._maybeStartDebugger(); } else { - if (!this._mvm.isDebugging()) { - this._baseDebugAdaptor.handleDisconnect(); - if (this._baseDebugSession != null) { - void vscode.debug.stopDebugging(this._baseDebugSession) - this._baseDebugSession = null; - this._isDebugAdaptorStarted = false; - } - } + this._breakpointSynchronizer.disable(); } } }); } - 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 { @@ -90,17 +63,11 @@ export default class MatlabDebugger { } } as vscode.DebugConfigurationProvider) - const baseDebugAdaptor = this._baseDebugAdaptor; - const nestedDebugAdaptor = this._nestedDebugAdaptor; - // eslint-disable-next-line @typescript-eslint/no-this-alias - const matlabDebugger = this; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const outerThis = 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); - } + return new vscode.DebugAdapterInlineImplementation(outerThis._adaptor); } } as vscode.DebugAdapterDescriptorFactory); @@ -109,37 +76,24 @@ export default class MatlabDebugger { return; } - this._activeSessions.add(session); + if (this._activeSession != null) { + void vscode.debug.stopDebugging(session); + } + session.name = 'MATLAB'; - const isInvalidToStartSession = (await vscode.workspace.getConfiguration('MATLAB').get('matlabConnectionTiming')) === 'never' - if (isInvalidToStartSession && this._mvm.getMatlabState() === MatlabState.DISCONNECTED) { + const isInvalidToStartSession = vscode.workspace.getConfiguration('MATLAB').get('matlabConnectionTiming') === 'never' + if (isInvalidToStartSession && this._mvm.getMatlabState() === MatlabMVMConnectionState.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); - } - } + void this._terminalService.openTerminalOrBringToFront(); }); 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); - } + if (this._activeSession === session) { + this._activeSession = undefined; } }); @@ -148,11 +102,7 @@ export default class MatlabDebugger { if ((vscode.debug as any).onDidChangeActiveStackItem !== undefined) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (vscode.debug as any).onDidChangeActiveStackItem((frame: any) => { - if (this._baseDebugSession === null) { - return; - } - - if (!this._activeSessions.has(frame.session) || frame.session === this._baseDebugSession) { + if (this._activeSession == null || this._activeSession !== frame.session || frame.frameId === undefined) { return; } @@ -169,20 +119,13 @@ export default class MatlabDebugger { 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) { + private async _handleMvmStateChange (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState): Promise { + if (newState === MatlabMVMConnectionState.CONNECTED && oldState === MatlabMVMConnectionState.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) { + } else if (newState === MatlabMVMConnectionState.DISCONNECTED) { void vscode.commands.executeCommand('setContext', 'matlab.isDebugging', false); - this._baseDebugAdaptor.handleDisconnect(); - this._activeSessions.forEach((session) => { - void vscode.debug.stopDebugging(session); - }) - this._isDebugAdaptorStarted = false; + this._adaptor.handleDisconnect(); + void vscode.debug.stopDebugging(this._activeSession); } } @@ -193,16 +136,15 @@ export default class MatlabDebugger { return; } - const shouldReact = await this._shouldReactToDebugEvent(); - const isStillDebugging = this._mvm.isDebugging(); - - if (shouldReact && isStillDebugging) { - void this._maybeStartDebugger(); - } + void this._maybeStartDebugger(); } private async _maybeStartDebugger (): Promise { - if (this._isDebugAdaptorStarted) { + if (!this._shouldAutoStart || !this._mvm.isDebugging()) { + return; + } + + if (this._activeSession !== undefined) { return; } @@ -217,32 +159,18 @@ export default class MatlabDebugger { }); } - // 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, + const debuggerConfiguration = { + parentSession: undefined, compact: true, suppressDebugStatusbar: false, suppressDebugToolbar: false, suppressDebugView: false - }); - - this._isDebugAdaptorStarted = wasDebuggerStartSuccessful; - } + } as vscode.DebugSessionOptions; - 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; + await vscode.debug.startDebugging(undefined, { + name: 'matlab', + type: 'matlab', + request: 'launch' + }, debuggerConfiguration); } } diff --git a/src/extension.ts b/src/extension.ts index e145c3b..07de5c0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -132,7 +132,7 @@ export async function activate (context: vscode.ExtensionContext): Promise mvm = new MVM(multiclientNotifier); terminalService = new TerminalService(multiclientNotifier, mvm); executionCommandProvider = new ExecutionCommandProvider(mvm, terminalService, telemetryLogger); - matlabDebugger = new MatlabDebugger(mvm, multiclientNotifier, telemetryLogger); + matlabDebugger = new MatlabDebugger(mvm, multiclientNotifier, telemetryLogger, terminalService); // 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. diff --git a/src/test/test-files/hScript3.m b/src/test/test-files/hScript3.m new file mode 100644 index 0000000..9532533 --- /dev/null +++ b/src/test/test-files/hScript3.m @@ -0,0 +1,6 @@ +a=1; +keyboard +for i = 1:10 + disp(i) + pause(0.5) +end diff --git a/src/test/tools/tester/DebuggerTester.ts b/src/test/tools/tester/DebuggerTester.ts index 0a843bd..a13483f 100644 --- a/src/test/tools/tester/DebuggerTester.ts +++ b/src/test/tools/tester/DebuggerTester.ts @@ -23,6 +23,7 @@ export class DebuggerTester { public async setBreakpointOnLine (lineNumber: number): Promise { await this.editor.toggleBreakpoint(lineNumber); + await this.vs.pause(1000) return await this.vs.poll(() => this.isBreakpointOnLine(lineNumber), true, `Expected breakpoint to exist on line ${lineNumber}`); } diff --git a/src/test/tools/tester/TestSuite.ts b/src/test/tools/tester/TestSuite.ts index 0cd0ba9..02600f0 100644 --- a/src/test/tools/tester/TestSuite.ts +++ b/src/test/tools/tester/TestSuite.ts @@ -60,12 +60,14 @@ export class TestSuite { */ public async enqueueTests (tests: string[]): Promise { let failed = false; + let firstRun = true; const exTester = new ExTester(this.storageFolder, this.releaseQuality, undefined) await exTester.downloadCode(this.vscodeVersion) await exTester.downloadChromeDriver(this.vscodeVersion) await exTester.installVsix({ vsixFile: this.vsixPath }) console.log(`Queueing tests:\n${tests.join('\n')}\n`) for (const test of tests) { + if (!firstRun) await PollingUtils.pause(30000); // wait for state to be reset before running next test const testPath = path.join(this.testsRoot, test) console.log(`Running test: ${test}`) try { @@ -84,7 +86,7 @@ export class TestSuite { failed = true; console.error('\x1b[31m%s\x1b[0m', err) } - await PollingUtils.pause(30000); // wait for state to be reset before running next test + firstRun = false; } if (failed) { console.error('\x1b[31m%s\x1b[0m', 'One or more tests failed.'); diff --git a/src/test/tools/tester/VSCodeTester.ts b/src/test/tools/tester/VSCodeTester.ts index 207449b..6791565 100644 --- a/src/test/tools/tester/VSCodeTester.ts +++ b/src/test/tools/tester/VSCodeTester.ts @@ -119,20 +119,12 @@ export class VSCodeTester { /** * Opens the MATLAB terminal and creates a new terminal tester */ - public async openMATLABTerminal (): Promise { - const prompt = await this.workbench.openCommandPrompt() as vet.InputBox - await prompt.setText('>matlab.openCommandWindow') - await this.selectQuickPick(prompt, 'MATLAB: Open Command Window'); + public async openMATLABTerminal (): Promise { + await this.executeWorkbenchCommand('matlab.openCommandWindow') + await this.pause(1000) const terminal = await new vet.BottomBarPanel().openTerminalView() const terminalTester = new TerminalTester(this, terminal) this.terminal = terminalTester - return terminalTester - } - - public async runCurrentFile (): Promise { - const prompt = await this.workbench.openCommandPrompt() - await prompt.setText('>matlab.runFile') - return await prompt.confirm() } public async setSetting (id: string, value: string): Promise { diff --git a/src/test/ui/debugging.test.ts b/src/test/ui/debugging.test.ts index af794a5..0442197 100644 --- a/src/test/ui/debugging.test.ts +++ b/src/test/ui/debugging.test.ts @@ -10,10 +10,10 @@ suite('Debugging UI Tests', () => { vs = new VSCodeTester(); await vs.openEditor('hScript1.m') await vs.assertMATLABConnected() - await vs.closeActiveEditor() await vs.openMATLABTerminal() - await vs.terminal.executeCommand(`addpath('${vs.getTestFilesDirectory()}')`) - await vs.terminal.executeCommand('clc') + await vs.terminal.assertContains('>>', 'wait for ready prompt') + await vs.terminal.executeCommand(`addpath('${vs.getTestFilesDirectory()}'); clc`) + await vs.closeActiveEditor() }); afterEach(async () => { @@ -29,7 +29,7 @@ suite('Debugging UI Tests', () => { const editor = await vs.openEditor('hScript2.m') await editor.debugger.setBreakpointOnLine(1) await editor.debugger.setBreakpointOnLine(3) - await vs.runCurrentFile() + await editor.type(Key.F5, 'F5 to run file'); await editor.debugger.assertStoppedAtLine(1) await editor.type(Key.F5, 'F5 to continue'); await editor.debugger.assertStoppedAtLine(3) @@ -45,7 +45,7 @@ suite('Debugging UI Tests', () => { const editor = await vs.openEditor('hScript2.m') await vs.terminal.executeCommand('dbstop in hScript2 at 1') await vs.terminal.executeCommand('dbstop in hScript2 at 3') - await vs.runCurrentFile() + await editor.type(Key.F5, 'F5 to run file'); await editor.debugger.assertStoppedAtLine(1) await vs.terminal.assertContains('K>>', 'terminal should have K prompt') await vs.terminal.executeCommand('dbcont') @@ -57,14 +57,25 @@ suite('Debugging UI Tests', () => { }) test('Executing commands while debugging', async () => { - const editor = await vs.openEditor('hScript2.m') - await editor.debugger.setBreakpointOnLine(1) - await vs.runCurrentFile() - await editor.debugger.assertStoppedAtLine(1) + const editor = await vs.openEditor('hScript3.m') + await editor.type(Key.F5, 'F5 to run file'); + await editor.debugger.assertStoppedAtLine(2) // hScript3.m has keyboard on line 2 await vs.terminal.executeCommand('12+17') await vs.terminal.assertContains('29', 'output should appear in terminal') - await vs.terminal.executeCommand('dbquit') + await editor.type(Key.chord(Key.SHIFT, Key.F5), 'Shift+F5 to stop'); + await editor.debugger.assertNotDebugging() + }) + + test('Test pause and resume while debugging', async () => { + const editor = await vs.openEditor('hScript3.m') + await editor.type(Key.F5, 'F5 to run file'); + await editor.debugger.assertStoppedAtLine(2) // Ensure we are stopped in a debug session + await editor.type(Key.F5, 'F5 to resume') + await editor.debugger.assertNotDebugging() + await vs.pause(2000) // Allow execution for a few seconds + await editor.type(Key.F6, 'F6 to pause') + await editor.debugger.assertDebugging() + await editor.type(Key.F5, 'F5 to resume') await editor.debugger.assertNotDebugging() - await editor.debugger.clearBreakpointOnLine(1) }) }); diff --git a/src/test/ui/execution.test.ts b/src/test/ui/execution.test.ts index a973b40..b6fe39d 100644 --- a/src/test/ui/execution.test.ts +++ b/src/test/ui/execution.test.ts @@ -10,9 +10,10 @@ suite('Execution UI Tests', () => { vs = new VSCodeTester(); await vs.openEditor('hScript1.m') await vs.assertMATLABConnected() - await vs.closeActiveEditor() await vs.openMATLABTerminal() - await vs.terminal.executeCommand(`addpath('${vs.getTestFilesDirectory()}')`) + await vs.terminal.assertContains('>>', 'wait for ready prompt') + await vs.terminal.executeCommand(`addpath('${vs.getTestFilesDirectory()}'); clc`) + await vs.closeActiveEditor() }); after(async () => { @@ -27,14 +28,4 @@ suite('Execution UI Tests', () => { await editor.type(Key.chord(Key.CONTROL, Key.ENTER), 'Ctrl+Enter to run section'); await vs.terminal.assertContains('0', 'Value of a should not be updated') }) - - test('Test pause and resume', async () => { - const editor = await vs.openEditor('hPauseScript.m') - await vs.runCurrentFile() - await vs.pause(2500) // Allow execution for a few seconds - await editor.type(Key.F6, 'F6 to pause') - await editor.debugger.assertDebugging() - await editor.type(Key.F5, 'F5 to resume') - await editor.debugger.assertNotDebugging() - }) }); diff --git a/src/test/ui/terminal.test.ts b/src/test/ui/terminal.test.ts index b87fb60..0233110 100644 --- a/src/test/ui/terminal.test.ts +++ b/src/test/ui/terminal.test.ts @@ -10,8 +10,8 @@ suite('Terminal UI Tests', () => { vs = new VSCodeTester(); await vs.openEditor('hScript1.m') await vs.assertMATLABConnected() - await vs.closeActiveEditor() await vs.openMATLABTerminal() + await vs.closeActiveEditor() }); afterEach(async () => { diff --git a/syntaxes b/syntaxes index d7fa696..a3e2cf3 160000 --- a/syntaxes +++ b/syntaxes @@ -1 +1 @@ -Subproject commit d7fa696a8a3f1e981ed3271b49f63fc7529c7883 +Subproject commit a3e2cf3f2031cc3b16aff0d1f68f9b43f8778a77