From 7378a0852bc5428f5770b25479bca6c3a365b8aa Mon Sep 17 00:00:00 2001 From: dlilley Date: Mon, 15 Jun 2026 15:35:08 -0400 Subject: [PATCH] Changes for v1.3.12 --- .eslintrc.json | 12 + CHANGELOG.md | 8 + README.md | 108 +- package-lock.json | 4132 +++++++++++++++-- package.json | 302 +- public/Workspace.png | Bin 0 -> 50001 bytes public/logo_outline.svg | 3 + server | 2 +- src/commandwindow/CommandWindow.ts | 121 +- src/debug/MatlabDebugger.ts | 1 + src/extension.ts | 10 +- src/notifications/Notifications.ts | 4 + src/services/projects/MatlabProjectService.ts | 26 +- src/test/tools/tester/TerminalTester.ts | 18 + src/test/tools/tester/VSCodeTester.ts | 63 +- .../tools/tester/WorkspaceBrowserTester.ts | 281 ++ src/test/ui/workspacebrowser.test.ts | 158 + src/test/unit/CommandWindow.test.ts | 332 ++ src/test/unit/mock-vscode.ts | 80 + src/test/workspacebrowser/icons.test.ts | 97 + .../provider/configuration.test.ts | 199 + .../provider/contextMenuCommands.test.ts | 152 + .../provider/dataThrottling.test.ts | 97 + src/test/workspacebrowser/provider/helpers.ts | 159 + .../provider/lifecycle.test.ts | 207 + .../provider/serverMessages.test.ts | 217 + .../provider/versionGate.test.ts | 75 + .../provider/webviewMessages.test.ts | 302 ++ src/test/workspacebrowser/runTest.ts | 111 + src/test/workspacebrowser/webview.test.ts | 893 ++++ .../WorkspaceBrowserProvider.ts | 623 +++ src/workspacebrowser/icons.ts | 46 + .../resources/icons/dark/ws3d.svg | 7 + .../resources/icons/dark/wsBrackets.svg | 5 + .../resources/icons/dark/wsCalendar.svg | 16 + .../resources/icons/dark/wsCharacter.svg | 9 + .../resources/icons/dark/wsCheck.svg | 4 + .../resources/icons/dark/wsClock.svg | 5 + .../resources/icons/dark/wsDate.svg | 5 + .../resources/icons/dark/wsDefault.svg | 4 + .../resources/icons/dark/wsDots.svg | 9 + .../resources/icons/dark/wsSparse.svg | 4 + .../resources/icons/dark/wsString.svg | 11 + .../resources/icons/dark/wsTable.svg | 7 + .../resources/icons/dark/wsTableTime.svg | 10 + .../resources/icons/dark/wsTall.svg | 6 + .../resources/icons/dark/wsTime.svg | 6 + .../resources/icons/dark/wsTree.svg | 4 + .../resources/icons/light/ws3d.svg | 7 + .../resources/icons/light/wsBrackets.svg | 5 + .../resources/icons/light/wsCalendar.svg | 16 + .../resources/icons/light/wsCharacter.svg | 9 + .../resources/icons/light/wsCheck.svg | 4 + .../resources/icons/light/wsClock.svg | 5 + .../resources/icons/light/wsDate.svg | 5 + .../resources/icons/light/wsDefault.svg | 4 + .../resources/icons/light/wsDots.svg | 9 + .../resources/icons/light/wsSparse.svg | 4 + .../resources/icons/light/wsString.svg | 11 + .../resources/icons/light/wsTable.svg | 7 + .../resources/icons/light/wsTableTime.svg | 10 + .../resources/icons/light/wsTall.svg | 6 + .../resources/icons/light/wsTime.svg | 6 + .../resources/icons/light/wsTree.svg | 4 + src/workspacebrowser/resources/webview.css | 197 + src/workspacebrowser/templates.ts | 82 + src/workspacebrowser/types.ts | 59 + src/workspacebrowser/webview-main.ts | 11 + src/workspacebrowser/webview.ts | 828 ++++ syntaxes | 2 +- webpack.config.js | 24 + 71 files changed, 9619 insertions(+), 647 deletions(-) create mode 100644 public/Workspace.png create mode 100644 public/logo_outline.svg create mode 100644 src/test/tools/tester/WorkspaceBrowserTester.ts create mode 100644 src/test/ui/workspacebrowser.test.ts create mode 100644 src/test/unit/CommandWindow.test.ts create mode 100644 src/test/unit/mock-vscode.ts create mode 100644 src/test/workspacebrowser/icons.test.ts create mode 100644 src/test/workspacebrowser/provider/configuration.test.ts create mode 100644 src/test/workspacebrowser/provider/contextMenuCommands.test.ts create mode 100644 src/test/workspacebrowser/provider/dataThrottling.test.ts create mode 100644 src/test/workspacebrowser/provider/helpers.ts create mode 100644 src/test/workspacebrowser/provider/lifecycle.test.ts create mode 100644 src/test/workspacebrowser/provider/serverMessages.test.ts create mode 100644 src/test/workspacebrowser/provider/versionGate.test.ts create mode 100644 src/test/workspacebrowser/provider/webviewMessages.test.ts create mode 100644 src/test/workspacebrowser/runTest.ts create mode 100644 src/test/workspacebrowser/webview.test.ts create mode 100644 src/workspacebrowser/WorkspaceBrowserProvider.ts create mode 100644 src/workspacebrowser/icons.ts create mode 100644 src/workspacebrowser/resources/icons/dark/ws3d.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsBrackets.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsCalendar.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsCharacter.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsCheck.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsClock.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsDate.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsDefault.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsDots.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsSparse.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsString.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsTable.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsTableTime.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsTall.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsTime.svg create mode 100644 src/workspacebrowser/resources/icons/dark/wsTree.svg create mode 100644 src/workspacebrowser/resources/icons/light/ws3d.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsBrackets.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsCalendar.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsCharacter.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsCheck.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsClock.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsDate.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsDefault.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsDots.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsSparse.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsString.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsTable.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsTableTime.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsTall.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsTime.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsTree.svg create mode 100644 src/workspacebrowser/resources/webview.css create mode 100644 src/workspacebrowser/templates.ts create mode 100644 src/workspacebrowser/types.ts create mode 100644 src/workspacebrowser/webview-main.ts create mode 100644 src/workspacebrowser/webview.ts create mode 100644 webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index b53ab36..601348a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,5 +45,17 @@ "node_modules", "**/*.d.ts", "webpack.config.js" + ], + "overrides": [ + { + "files": ["src/test/workspacebrowser/**/*.ts"], + "rules": { + // Chai assertions (expect(x).to.be.true) are property accesses that + // trigger no-unused-expressions. This is a known Chai/ESLint conflict. + "@typescript-eslint/no-unused-expressions": "off", + // Mock stubs and disposables use empty function bodies as intentional no-ops. + "@typescript-eslint/no-empty-function": "off" + } + } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb8941..a66f186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.12] - 2026-06-15 + +### Added +- Interactively inspect the MATLAB workspace in the MATLAB view with support for renaming, sorting, and deleting variables (Addresses [mathworks/MATLAB-extension-for-vscode#100](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/100)) +- Improved formatting of warnings in output +- Support for the diary command (Addresses [mathworks/MATLAB-extension-for-vscode#186](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/186)) +- Support for word-based navigation and editing keyboard shortcuts in the MATLAB Terminal, including Ctrl+Left/Right to move by word, Ctrl+Shift+Left/Right to select by word, and Ctrl+Backspace/Delete to delete words (Addresses [mathworks/MATLAB-extension-for-vscode#249](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/249)) + ## [1.3.11] - 2026-05-08 ### Added diff --git a/README.md b/README.md index ed13b1c..eb5783a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,18 @@ You can use this extension with or without MATLAB installed on your system. Howe **Note:** This extension no longer supports MATLAB R2021a. To use advanced features or run MATLAB code, you must have MATLAB R2021b or later installed. +## Table of Contents +1. [Installation](#installation) +2. [Get Started](#get-started) +3. [Run and Debug MATLAB Code](#run-and-debug-matlab-code) +4. [View MATLAB Workspace Contents](#view-matlab-workspace-content) +5. [Work with MATLAB Projects](#work-with-matlab-projects) +6. [Run MATLAB In Jupyter Notebooks](#run-matlab-in-jupyter-notebooks) +7. [Configuration](#configuration) +8. [Troubleshooting](#troubleshooting) +9. [Contact Us](#contact-us) +10. [Release Notes](#release-notes) + ## Installation You can install the extension from within Visual Studio Code or download it from [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=MathWorks.language-matlab). After installing the extension, you might need to configure it to make full use of all the features. For more information, see the [Configuration](#configuration) section. @@ -47,6 +59,22 @@ 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. +## View MATLAB Workspace Content +If you have MATLAB R2023a or later installed on your system, you can interactively inspect the MATLAB workspace by opening the MATLAB view in Visual Studio Code. In the Workspace section of the MATLAB view, you can interactively manage variables in the MATLAB workspace while you run and debug MATLAB code. To access the MATLAB view, select the MATLAB icon in the Activity Bar on the left side of the Visual Studio Code window, which opens the view in the Side Bar. + +In the Workspace section of the MATLAB view, you can: +* View the name, value, size, and class of each variable in the MATLAB workspace. +* Interactively rename, delete, and sort variables in the MATLAB workspace. + +**Tip:** To access the MATLAB Workspace while debugging, drag the MATLAB icon from the Activity bar into the Run and Debug view. This adds the Workspace section of the MATLAB view to the Run and Debug view. + +![MATLAB Workspace Screenshot](public/Workspace.png) + +### Limitations +* Interactively editing workspace variables in the Workspace section of the MATLAB view is limited to inline edits. Editing complex data types such as matrices, cells, and structs is not supported. +* Multi-row selection is not supported. +* Refreshing the workspace is not supported. + ## Work with MATLAB Projects If you have MATLAB R2021b or later installed on your system, you can work with MATLAB Projects directly in Visual Studio Code. You can create new projects, open existing projects, and close the current project. @@ -62,11 +90,16 @@ You also can use this extension along with the Jupyter Extension for Visual Stud ## Configuration To configure the extension, go to the extension settings and select from the available options. -### MATLAB Default Editor Setting +### MATLAB Settings +#### MATLAB Default Editor Setting By default, the extension uses the editor specified in the MATLAB Editor/Debugger settings to open files with the MATLAB `edit` and `open` commands. To make Visual Studio Code the default editor for these commands, set the `MATLAB.defaultEditor` setting to `true`. To revert to using the editor specified in the MATLAB Editor/Debugger settings, set `MATLAB.defaultEditor` to `false`. **Note:** Certain file types always open in MATLAB by default — for example, live scripts saved in the binary Live Code file format (.mlx) and MATLAB app files (.mlapp). -### MATLAB Install Path Setting +#### MATLAB Index Workspace Setting +By default, the extension indexes all the MATLAB code files (`.m`) in your current Visual Studio Code workspace. Indexing allows the extension to find and navigate between your MATLAB code files. +You can disable indexing to improve the performance of the extension. To disable indexing, set the `MATLAB.indexWorkspace` setting to `false`. Disabling indexing can cause features such as code navigation not to function as expected. + +#### MATLAB Install Path Setting If you have MATLAB installed on your system, the extension automatically checks the system path for the location of the MATLAB executable. If the MATLAB executable is not on the system path, you may need to manually set the `MATLAB.installPath` setting to the full path of your MATLAB installation. For example, `C:\Program Files\MATLAB\R2022b` (Windows®), `/Applications/MATLAB_R2022b.app` (macOS), or `/usr/local/MATLAB/R2022b` (Linux®). You can determine the full path of your MATLAB installation by using the `matlabroot` command in MATLAB. @@ -81,40 +114,48 @@ In the extension settings, set the `MATLAB.installPath` setting to the value ret ![MATLAB Install Path Setting](public/InstallPathSetting.png) -### MATLAB Automatically Start Debugger Setting -By default, the extension does not automatically start the Visual Studio Code debugger when MATLAB reaches a breakpoint. To enable automatically starting the Visual Studio Code debugger, set the `MATLAB.startDebuggerAutomatically` setting to `true`. When starting the Visual Studio Debugger is disabled, MATLAB still stops at breakpoints, and you can debug your code in the MATLAB terminal using the MATLAB debugging functions. - -### MATLAB Index Workspace Setting -By default, the extension indexes all the MATLAB code files (`.m`) in your current workspace. Indexing allows the extension to find and navigate between your MATLAB code files. -You can disable indexing to improve the performance of the extension. To disable indexing, set the `MATLAB.indexWorkspace` setting to `false`. Disabling indexing can cause features such as code navigation not to function as expected. - -### MATLAB Connection Timing Setting +#### MATLAB Connection Timing Setting By default, the extension starts MATLAB in the background when you open a MATLAB code file in Visual Studio Code. To control when the extension starts MATLAB, set the `MATLAB.matlabConnectionTiming` setting to one of these values: * `onStart` (default) — Start MATLAB as soon as a MATLAB code file is opened. * `onDemand` — Start MATLAB only when needed for a given action. * `never` — Never start MATLAB. Note: Some functionality is available only with MATLAB running in the background. -### MATLAB Max File Size for Analysis Setting +#### MATLAB Max File Size for Analysis Setting By default, the extension analyzes all files, regardless of their size, for features such as linting, code navigation, and symbol renaming. To limit the maximum number of characters a file can contain, set the `MATLAB.maxFileSizeForAnalysis` setting. For example, to limit the number of characters to 50,000, set the `MATLAB.maxFileSizeForAnalysis` setting to `50000`. If a file contains more than the maximum number of characters, features such as linting, code navigation, and symbol renaming are disabled for that file. To remove the limit and analyze all files regardless of their size, set the `MATLAB.maxFileSizeForAnalysis` setting to `0`. -### MATLAB Show Feature Not Available Error Setting +#### MATLAB Prewarm Graphics Setting +By default, MATLAB services are started early to improve the first-time performance of MATLAB figure rendering. To disable this behavior, set the `MATLAB.prewarmGraphics` setting to `false`. +This setting is supported with MATLAB R2025a and later. For earlier releases, this setting is ignored. + +#### MATLAB Show Feature Not Available Error Setting By default, the extension displays an error when a feature requires MATLAB and MATLAB is unable to start. To not display an error, set the `MATLAB.showFeatureNotAvailableError` setting to `false`. -### MATLAB Sign In Setting +#### MATLAB Sign In Setting By default, the extension assumes that the MATLAB installation specified in the Install Path setting is activated. To enable browser-based sign in to your MathWorks account using the Online License Manager or a Network License Manager, set the `MATLAB.signIn` setting to true. When this setting is enabled, the extension prompts you to sign in when it starts MATLAB. -### MATLAB Telemetry Setting +#### MATLAB Start Debugger Automatically Setting +By default, the extension does not automatically start the Visual Studio Code debugger when MATLAB reaches a breakpoint. To enable automatically starting the Visual Studio Code debugger, set the `MATLAB.startDebuggerAutomatically` setting to `true`. When starting the Visual Studio Debugger is disabled, MATLAB still stops at breakpoints, and you can debug your code in the MATLAB terminal using the MATLAB debugging functions. + +#### MATLAB Telemetry Setting You can help improve the extension by sending user experience information to MathWorks®. By default, the extension sends user experience information to MathWorks. To disable sending information, set the `MATLAB.telemetry` setting to `false`. For more information, see the [MathWorks Privacy Policy](https://www.mathworks.com/company/aboutus/policies_statements.html). -### MATLAB Prewarm Graphics Setting -By default, MATLAB services are started early to improve the first-time performance of MATLAB figure rendering. To disable this behavior, set the `MATLAB.prewarmGraphics` setting to `false`. -This setting is supported with MATLAB R2025a and later. For earlier releases, this setting is ignored. +### MATLAB Workspace Settings +#### MATLAB Maximum Workspace Variables Setting +By default, the extension displays up to 500 variables in the Workspace section of the MATLAB view. Limiting the number of displayed variables can improve performance, especially when working with large workspaces. If the MATLAB workspace contains more variables than the specified limit, the extension displays the first variables in the sorted list, up to the configured maximum. +To control how many variables to display, set the `MATLAB.maximumWorkspaceVariables` setting. For example, set the `MATLAB.maximumWorkspaceVariables` setting to `100` to display up to 100 variables. + +#### MATLAB Workspace Sort Method Setting +By default, variables in the Workspace section of the MATLAB view are sorted alphanumerically. +To change the sort method, set the `MATLAB.workspaceSortMethod` setting to one of these values: + +* `Natural (default)` — Compare numbers within names by numeric value. **Example**: [var1, var2, var10] +* `Lexicographic` — Compare names character by character. **Example**: [var1, var10, var2]. ## Troubleshooting If the MATLAB install path is not properly configured, you get an error when you try to use certain advanced features, such as document formatting and code navigation. @@ -126,35 +167,4 @@ We encourage all feedback. If you encounter a technical issue or have an enhance ## Release Notes -For a complete list of changes, see the [CHANGELOG](CHANGELOG.md). - -### 1.3.0 -Release date: 2024-12-18 - -Added: -* Debugging support -* Support for inserting code snippets shipped with MATLAB (requires MATLAB R2025a or later) -* Support for opening additional MATLAB file types (e.g. `.slx`, `.fig`) from the Visual Studio Code context menu (Community contribution from @Gusmano-2-OSU) - -Fixed: -* Syntax highlighting improvements (Community contribution from @apozharski) - -### 1.2.0 -Release date: 2024-03-05 - -Added: -* Code execution support - -### 1.1.0 -Release date: 2023-06-05 - -Added: -* Document symbol and outline support - -Fixed: -* Code folding no longer matches `end` when used in strings, comments, and to denote the end of a matrix - -### 1.0.0 -Release date: 2023-04-26 - -* Initial release. +For a complete list of changes, see the [Change Log](CHANGELOG.md). diff --git a/package-lock.json b/package-lock.json index 06fd46a..7b21679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "language-matlab", - "version": "1.3.11", + "version": "1.3.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.3.11", + "version": "1.3.12", "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.56.0", @@ -14,15 +14,20 @@ "vscode-languageclient": "^8.0.2" }, "devDependencies": { + "@types/chai": "^5.2.2", "@types/glob": "^8.0.0", + "@types/jsdom": "^21.1.7", "@types/mocha": "^9.1.1", + "@types/mock-require": "^3.0.0", "@types/node": "^18.19.50", "@types/node-fetch": "^2.6.2", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.67.0", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^2.19.0", + "chai": "^5.2.1", "copyfiles": "^2.4.1", "eslint": "^8.23.0", "eslint-config-standard-with-typescript": "^22.0.0", @@ -30,9 +35,16 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.0.1", "glob": "^8.0.3", + "jsdom": "^26.1.0", "mocha": "^10.0.0", + "mock-require": "^3.0.3", + "node-loader": "^2.1.0", + "sinon": "^18.0.0", + "ts-loader": "^9.5.2", "typescript": "^5.0.4", - "vscode-extension-tester": "8.14.1" + "vscode-extension-tester": "8.14.1", + "webpack": "^5.101.0", + "webpack-cli": "^6.0.1" }, "engines": { "vscode": "^1.67.0" @@ -47,6 +59,27 @@ "node": ">=0.10.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@azu/format-text": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", @@ -65,47 +98,38 @@ } }, "node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/@azure/core-auth": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", - "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } + "license": "0BSD" }, - "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, + "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-auth/node_modules/tslib": { @@ -132,18 +156,6 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-client/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -151,34 +163,22 @@ "dev": true }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz", - "integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==", - "dev": true, - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.9.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "dev": true, + "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { @@ -188,15 +188,16 @@ "dev": true }, "node_modules/@azure/core-tracing": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", - "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-tracing/node_modules/tslib": { @@ -206,28 +207,18 @@ "dev": true }, "node_modules/@azure/core-util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", - "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", - "dev": true, - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, + "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-util/node_modules/tslib": { @@ -237,28 +228,26 @@ "dev": true }, "node_modules/@azure/identity": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", - "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.9.2", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/identity/node_modules/tslib": { @@ -268,15 +257,17 @@ "dev": true }, "node_modules/@azure/logger": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", - "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, + "license": "MIT", "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/logger/node_modules/tslib": { @@ -286,47 +277,40 @@ "dev": true }, "node_modules/@azure/msal-browser": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.13.0.tgz", - "integrity": "sha512-fD906nmJei3yE7la6DZTdUtXKvpwzJURkfsiz9747Icv4pit77cegSm6prJTKLQ1fw4iiZzrrWwxnhMLrTf5gQ==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.9.0" + "@azure/msal-common": "16.6.2" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.9.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", - "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==", + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", - "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.12.0", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" + "@azure/msal-common": "16.6.2", + "jsonwebtoken": "^9.0.0" }, "engines": { - "node": ">=16" - } - }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", - "dev": true, - "engines": { - "node": ">=0.8.0" + "node": ">=20" } }, "node_modules/@babel/code-frame": { @@ -369,6 +353,133 @@ "node": ">=18" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -570,6 +681,17 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -579,6 +701,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -945,6 +1078,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -1018,6 +1192,53 @@ "@textlint/ast-node-types": "15.5.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", @@ -1040,6 +1261,18 @@ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1066,6 +1299,13 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, + "node_modules/@types/mock-require": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-3.0.0.tgz", + "integrity": "sha512-TZ/s3ufaF+kEFPx9PqXKNd7OsPJoVsgOd59EA8XQLTRjJCsOdx5sEAOGhyrZ3Os78iaDKyGJWdQWyYWRg1Iyvg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.19.75", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", @@ -1116,6 +1356,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.77.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.77.0.tgz", @@ -1329,6 +1593,28 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1578,82 +1864,304 @@ "node": ">=4" } }, - "node_modules/@vscode/vsce/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vscode/vsce/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" } }, - "node_modules/@vscode/vsce/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, - "node_modules/@vscode/vsce/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=18.12.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, - "node_modules/@vscode/vsce/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } } }, - "node_modules/@vscode/vsce/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } + "license": "BSD-3-Clause" }, - "node_modules/@vscode/vsce/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.16.0", @@ -1669,6 +2177,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -1705,6 +2226,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "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", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -1905,6 +2468,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1983,6 +2556,29 @@ ], "optional": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2083,6 +2679,41 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2146,6 +2777,13 @@ "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -2155,6 +2793,22 @@ "semver": "^7.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/c8": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", @@ -2367,6 +3021,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2383,6 +3075,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -2464,6 +3166,16 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -2560,6 +3272,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2686,6 +3405,71 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2770,6 +3554,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2785,6 +3576,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2801,6 +3602,36 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -2829,12 +3660,16 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { @@ -3020,12 +3855,29 @@ "url": "https://bevry.me/fund" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.339", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz", + "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3035,6 +3887,20 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -3047,6 +3913,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -3147,6 +4026,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3206,10 +4092,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3853,9 +4740,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -3869,6 +4756,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -4231,6 +5128,13 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", @@ -4490,6 +5394,19 @@ "node": ">=14" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4569,6 +5486,19 @@ "node": ">=16.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4622,6 +5552,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4682,6 +5632,16 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5022,6 +5982,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5311,6 +6278,37 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5330,6 +6328,83 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5381,12 +6456,13 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, + "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -5402,29 +6478,6 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.2", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -5437,6 +6490,13 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -5530,6 +6590,48 @@ "uc.micro": "^1.0.1" } }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5556,37 +6658,43 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5598,7 +6706,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.truncate": { "version": "4.4.2", @@ -5623,6 +6732,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -5922,6 +7038,40 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=4.3.0" + } + }, + "node_modules/mock-require/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/mock-require/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5953,6 +7103,36 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nise": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.5.tgz", + "integrity": "sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.1.1", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.3.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/node-abi": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.35.0.tgz", @@ -5998,6 +7178,33 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-loader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", + "integrity": "sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.3" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, "node_modules/node-sarif-builder": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", @@ -6141,6 +7348,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6263,49 +7477,24 @@ } }, "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, + "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6518,6 +7707,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -6593,12 +7792,13 @@ } }, "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, + "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -6617,6 +7817,19 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6675,6 +7888,17 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6685,6 +7909,16 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -6711,6 +7945,75 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -6804,9 +8107,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6984,6 +8287,19 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7040,6 +8356,13 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7086,6 +8409,29 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7187,6 +8533,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7285,6 +8651,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", @@ -7300,6 +8673,77 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/secretlint": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", @@ -7661,6 +9105,25 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7689,6 +9152,37 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -7751,16 +9245,6 @@ "node": ">= 0.4" } }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7968,6 +9452,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -8021,6 +9512,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -8154,6 +9659,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -8180,9 +9745,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -8330,11 +9895,32 @@ "xtend": "~4.0.1" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.14" } @@ -8357,6 +9943,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8371,6 +9970,27 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-loader": { + "version": "9.5.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", + "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -8439,6 +10059,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -8636,6 +10266,37 @@ "node-int64": "^0.4.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8664,15 +10325,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8810,9 +10462,9 @@ } }, "node_modules/vscode-extension-tester/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -9064,11 +10716,200 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -9189,6 +11030,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", @@ -9237,10 +11085,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -9257,6 +11106,32 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -9279,6 +11154,13 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9384,6 +11266,27 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, + "@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "requires": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "@azu/format-text": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", @@ -9400,42 +11303,33 @@ } }, "@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "requires": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "dependencies": { "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true } } }, "@azure/core-auth": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", - "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, "requires": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.1.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" }, "dependencies": { - "@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "requires": { - "tslib": "^2.6.2" - } - }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -9459,15 +11353,6 @@ "tslib": "^2.6.2" }, "dependencies": { - "@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "requires": { - "tslib": "^2.6.2" - } - }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -9477,30 +11362,20 @@ } }, "@azure/core-rest-pipeline": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz", - "integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "dev": true, "requires": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.9.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" }, "dependencies": { - "@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "requires": { - "tslib": "^2.6.2" - } - }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -9510,9 +11385,9 @@ } }, "@azure/core-tracing": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", - "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, "requires": { "tslib": "^2.6.2" @@ -9527,24 +11402,16 @@ } }, "@azure/core-util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", - "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "requires": { - "@azure/abort-controller": "^2.0.0", + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "dependencies": { - "@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "requires": { - "tslib": "^2.6.2" - } - }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -9554,24 +11421,21 @@ } }, "@azure/identity": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", - "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.3.0", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.9.2", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", "tslib": "^2.2.0" }, "dependencies": { @@ -9584,11 +11448,12 @@ } }, "@azure/logger": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", - "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "requires": { + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "dependencies": { @@ -9601,37 +11466,28 @@ } }, "@azure/msal-browser": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.13.0.tgz", - "integrity": "sha512-fD906nmJei3yE7la6DZTdUtXKvpwzJURkfsiz9747Icv4pit77cegSm6prJTKLQ1fw4iiZzrrWwxnhMLrTf5gQ==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", "dev": true, "requires": { - "@azure/msal-common": "14.9.0" + "@azure/msal-common": "16.6.2" } }, "@azure/msal-common": { - "version": "14.9.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", - "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==", + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", "dev": true }, "@azure/msal-node": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", - "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", "dev": true, "requires": { - "@azure/msal-common": "14.12.0", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, - "dependencies": { - "@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", - "dev": true - } + "@azure/msal-common": "16.6.2", + "jsonwebtoken": "^9.0.0" } }, "@babel/code-frame": { @@ -9663,6 +11519,50 @@ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true }, + "@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true + }, + "@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "requires": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + } + }, + "@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "peer": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "peer": true + }, + "@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -9795,12 +11695,32 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true }, + "@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -10068,6 +11988,42 @@ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, "@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -10134,6 +12090,48 @@ "@textlint/ast-node-types": "15.5.2" } }, + "@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "requires": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, "@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", @@ -10156,6 +12154,17 @@ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -10180,6 +12189,12 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, + "@types/mock-require": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-3.0.0.tgz", + "integrity": "sha512-TZ/s3ufaF+kEFPx9PqXKNd7OsPJoVsgOd59EA8XQLTRjJCsOdx5sEAOGhyrZ3Os78iaDKyGJWdQWyYWRg1Iyvg==", + "dev": true + }, "@types/node": { "version": "18.19.75", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", @@ -10227,6 +12242,27 @@ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true }, + "@types/sinon": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true + }, + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, "@types/vscode": { "version": "1.77.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.77.0.tgz", @@ -10343,6 +12379,25 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "requires": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + } + } + }, "@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -10549,28 +12604,207 @@ "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", "dev": true, - "optional": true + "optional": true + }, + "@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "dev": true, + "optional": true + }, + "@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "dev": true, + "optional": true + }, + "@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } }, - "@vscode/vsce-sign-linux-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", - "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, - "optional": true + "requires": {} }, - "@vscode/vsce-sign-win32-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", - "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, - "optional": true + "requires": {} }, - "@vscode/vsce-sign-win32-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", - "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, - "optional": true + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "acorn": { "version": "8.16.0", @@ -10579,6 +12813,13 @@ "dev": true, "peer": true }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "requires": {} + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -10604,6 +12845,35 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "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", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -10736,6 +13006,12 @@ "is-array-buffer": "^3.0.4" } }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -10785,6 +13061,18 @@ "dev": true, "optional": true }, + "baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10869,6 +13157,20 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "peer": true, + "requires": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -10914,6 +13216,12 @@ "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -10923,6 +13231,15 @@ "semver": "^7.0.0" } }, + "bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "requires": { + "run-applescript": "^7.0.0" + } + }, "c8": { "version": "10.1.3", "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", @@ -11065,6 +13382,25 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true + }, + "chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -11075,6 +13411,12 @@ "supports-color": "^7.1.0" } }, + "check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true + }, "cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -11137,6 +13479,12 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, + "chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true + }, "cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -11206,6 +13554,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -11306,6 +13660,53 @@ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true }, + "cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "requires": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + } + }, + "data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "requires": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "dependencies": { + "tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "requires": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -11354,6 +13755,12 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, + "decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -11363,6 +13770,12 @@ "mimic-response": "^3.1.0" } }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -11376,6 +13789,22 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "requires": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + } + }, + "default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true + }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -11394,9 +13823,9 @@ } }, "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true }, "define-properties": { @@ -11528,12 +13957,24 @@ "version-range": "^4.15.0" } }, + "electron-to-chromium": { + "version": "1.5.339", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz", + "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -11543,12 +13984,28 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + } + }, "entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", "dev": true }, + "envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true + }, "environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -11629,6 +14086,12 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true }, + "es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true + }, "es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -11671,9 +14134,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -12122,9 +14585,15 @@ "dev": true }, "fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, "fastq": { @@ -12402,6 +14871,12 @@ "is-glob": "^4.0.3" } }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -12555,6 +15030,15 @@ "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", "dev": true }, + "html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^3.1.1" + } + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -12615,6 +15099,15 @@ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -12644,6 +15137,16 @@ "resolve-from": "^4.0.0" } }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -12690,6 +15193,12 @@ "side-channel": "^1.1.0" } }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, "is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12891,6 +15400,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -13068,6 +15583,28 @@ "@isaacs/cliui": "^8.0.2" } }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13083,6 +15620,61 @@ "argparse": "^2.0.1" } }, + "jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "requires": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "requires": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -13127,12 +15719,12 @@ } }, "jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "requires": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -13142,29 +15734,6 @@ "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" - }, - "dependencies": { - "jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "dev": true, - "requires": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", - "dev": true, - "requires": { - "jwa": "^1.4.2", - "safe-buffer": "^5.0.1" - } - } } }, "jszip": { @@ -13179,6 +15748,12 @@ "setimmediate": "^1.0.5" } }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -13260,6 +15835,31 @@ "uc.micro": "^1.0.1" } }, + "loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "dependencies": { + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + } + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13339,6 +15939,12 @@ "is-unicode-supported": "^0.1.0" } }, + "loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, "lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -13548,6 +16154,33 @@ } } }, + "mock-require": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", + "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==", + "dev": true, + "requires": { + "get-caller-file": "^1.0.2", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13579,6 +16212,35 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nise": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.5.tgz", + "integrity": "sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^15.1.1", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.3.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + } + } + }, "node-abi": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.35.0.tgz", @@ -13610,6 +16272,21 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node-loader": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", + "integrity": "sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g==", + "dev": true, + "requires": { + "loader-utils": "^2.0.3" + } + }, + "node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true + }, "node-sarif-builder": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", @@ -13722,6 +16399,12 @@ "boolbase": "^1.0.0" } }, + "nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true + }, "object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -13802,31 +16485,15 @@ } }, "open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "dependencies": { - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - } + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" } }, "optionator": { @@ -13965,6 +16632,12 @@ "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -14023,12 +16696,20 @@ } }, "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "requires": { - "entities": "^4.4.0" + "entities": "^6.0.0" + }, + "dependencies": { + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true + } } }, "parse5-htmlparser2-tree-adapter": { @@ -14083,12 +16764,24 @@ } } }, + "path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -14107,6 +16800,54 @@ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, "pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14176,9 +16917,9 @@ "dev": true }, "qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "requires": { "side-channel": "^1.1.0" @@ -14301,6 +17042,15 @@ "picomatch": "^2.2.1" } }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, "reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -14337,6 +17087,12 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -14366,6 +17122,23 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -14433,6 +17206,18 @@ } } }, + "rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true + }, + "run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14498,6 +17283,12 @@ "is-regex": "^1.2.1" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", @@ -14513,6 +17304,57 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "secretlint": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", @@ -14735,6 +17577,20 @@ "simple-concat": "^1.0.0" } }, + "sinon": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", + "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -14752,6 +17608,30 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -14800,12 +17680,6 @@ "internal-slot": "^1.1.0" } }, - "stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -14945,6 +17819,12 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -14984,6 +17864,12 @@ } } }, + "tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true + }, "tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -15102,6 +17988,38 @@ "supports-hyperlinks": "^3.2.0" } }, + "terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + } + }, "test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -15120,9 +18038,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -15230,10 +18148,25 @@ "xtend": "~4.0.1" } }, + "tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "requires": { + "tldts-core": "^6.1.86" + } + }, + "tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, "tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true }, "to-buffer": { @@ -15251,6 +18184,15 @@ "is-number": "^7.0.0" } }, + "tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "requires": { + "tldts": "^6.1.32" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -15265,6 +18207,19 @@ "utf8-byte-length": "^1.0.1" } }, + "ts-loader": { + "version": "9.5.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", + "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + } + }, "tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -15317,6 +18272,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -15455,6 +18416,16 @@ "node-int64": "^0.4.0" } }, + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -15482,12 +18453,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -15593,9 +18558,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -15779,11 +18744,133 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, + "watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "peer": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "peer": true, + "requires": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "dependencies": { + "commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + } + }, + "webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true + }, + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -15871,6 +18958,12 @@ "has-tostringtag": "^1.0.2" } }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, "workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", @@ -15906,12 +18999,27 @@ "dev": true }, "ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "requires": {} }, + "wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "requires": { + "is-wsl": "^3.1.0" + } + }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, "xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -15928,6 +19036,12 @@ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 6cf9530..f2db70f 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.11", + "version": "1.3.12", "engines": { "vscode": "^1.67.0" }, @@ -89,6 +89,26 @@ "title": "MATLAB: Show Language Server Output" }, { + "command": "matlab.wsb.editValue", + "title": "Edit Value" + }, + { + "command": "matlab.wsb.deleteVariable", + "title": "Delete" + }, + { + "command": "matlab.wsb.renameVariable", + "title": "Rename" + }, + { + "command": "matlab.wsb.sortAscending", + "title": "Sort Ascending" + }, + { + "command": "matlab.wsb.sortDescending", + "title": "Sort Descending" + }, + { "command": "matlab.project.open", "title": "MATLAB: Open Project..." }, @@ -126,6 +146,21 @@ "command": "matlab.interrupt", "key": "Ctrl+C", "when": "((editorTextFocus && !editorHasSelection && editorLangId == matlab) || (terminalFocus && matlab.isActiveTerminal && !matlab.terminalHasSelection && !terminalTextSelectedInFocused))" + }, + { + "command": "matlab.wsb.deleteVariable", + "key": "delete", + "when": "focusedView == 'workspaceBrowserSidebarView'" + }, + { + "command": "matlab.wsb.renameVariable", + "key": "f2", + "when": "focusedView == 'workspaceBrowserSidebarView'" + }, + { + "command": "matlab.wsb.editValue", + "key": "enter", + "when": "focusedView == 'workspaceBrowserSidebarView'" } ], "breakpoints": [ @@ -161,6 +196,26 @@ { "command": "matlab.changeDirectory", "when": "false" + }, + { + "command": "matlab.wsb.editValue", + "when": "false" + }, + { + "command": "matlab.wsb.deleteVariable", + "when": "false" + }, + { + "command": "matlab.wsb.renameVariable", + "when": "false" + }, + { + "command": "matlab.wsb.sortAscending", + "when": "false" + }, + { + "command": "matlab.wsb.sortDescending", + "when": "false" } ], "editor/title/run": [ @@ -229,6 +284,33 @@ "when": "explorerResourceIsFolder" } ], + + "webview/context": [ + { + "command": "matlab.wsb.deleteVariable", + "when": "webviewId == 'workspaceBrowserSidebarView' && webviewSection == 'row'", + "group": "wsb_row@2" + }, + { + "command": "matlab.wsb.renameVariable", + "when": "webviewId == 'workspaceBrowserSidebarView' && webviewSection == 'row'", + "group": "wsb_row@3" + }, + { + "command": "matlab.wsb.editValue", + "when": "webviewId == 'workspaceBrowserSidebarView' && webviewSection == 'row'", + "group": "wsb_row@4" + }, + { + "command": "matlab.wsb.sortAscending", + "when": "webviewId == 'workspaceBrowserSidebarView' && webviewSection == 'header'" + }, + { + "command": "matlab.wsb.sortDescending", + "when": "webviewId == 'workspaceBrowserSidebarView' && webviewSection == 'header'" + } + ], + "matlab.project": [ { "command": "matlab.project.new", @@ -257,84 +339,108 @@ "label": "MATLAB: Project" } ], - "configuration": { - "title": "MATLAB", - "properties": { - "MATLAB.installPath": { - "type": "string", - "markdownDescription": "The full path to the top-level directory of the MATLAB installation you want to use with this extension. You can determine the full path to your MATLAB installation using the `matlabroot` command in MATLAB. For more information, refer to the [README](https://github.com/mathworks/MATLAB-extension-for-vscode/blob/main/README.md). This setting can be specified for both the user and workspace setting scopes using the User and Workspace tabs above.", - "scope": "machine-overridable" - }, - "MATLAB.matlabConnectionTiming": { - "type": "string", - "default": "onStart", - "description": "Choose when this extension starts MATLAB in the background. Some functionality will be unavailable without MATLAB running in the background.", - "enum": [ - "onStart", - "onDemand", - "never" - ], - "enumDescriptions": [ - "Start MATLAB as soon as a MATLAB code file is opened", - "Start MATLAB when needed for a given action", - "Never start MATLAB" - ], - "scope": "window" - }, - "MATLAB.indexWorkspace": { - "type": "boolean", - "default": true, - "description": "Automatically index all MATLAB code files (.m) in the current workspace.", - "scope": "window" - }, - "MATLAB.startDebuggerAutomatically": { - "type": "boolean", - "default": false, - "markdownDescription": "Automatically start the Visual Studio Code debugger when MATLAB reaches a breakpoint.", - "scope": "window" - }, - "MATLAB.telemetry": { - "type": "boolean", - "default": true, - "markdownDescription": "Help improve this extension by sending user experience information to MathWorks. For more information, see the [MathWorks Privacy Policy](https://www.mathworks.com/company/aboutus/policies_statements.html).", - "scope": "window", - "tags": [ - "telemetry", - "usesOnlineServices" - ] - }, - "MATLAB.signIn": { - "type": "boolean", - "default": false, - "markdownDescription": "Enable this option to present Sign In Options for unactivated MATLAB installations.", - "scope": "machine" - }, - "MATLAB.showFeatureNotAvailableError": { - "type": "boolean", - "default": true, - "description": "Display an error when a feature requires MATLAB and MATLAB is unable to start.", - "scope": "window" - }, - "MATLAB.maxFileSizeForAnalysis": { - "type": "number", - "default": 0, - "markdownDescription": "The maximum number of characters a file can contain for features such as linting, code navigation, and symbol renaming to be enabled. Use `0` for no limit.", - "scope": "window" - }, - "MATLAB.prewarmGraphics": { - "type": "boolean", - "default": true, - "description": "Prewarm graphics at MATLAB startup to improve performance of first-time graphics rendering.", - "scope": "window" - }, - "MATLAB.defaultEditor": { - "type": "boolean", - "default": true, - "markdownDescription": "Use Visual Studio Code instead of the MATLAB Editor to open and edit files using the `open` and `edit` commands. Certain file types always open in MATLAB by default — for example, live scripts saved in the binary Live Code file format (.mlx) and MATLAB app files (.mlapp).", - "scope": "window" + "configuration": [ + { + "title": "MATLAB", + "properties": { + "MATLAB.installPath": { + "type": "string", + "markdownDescription": "The full path to the top-level directory of the MATLAB installation you want to use with this extension. You can determine the full path to your MATLAB installation using the `matlabroot` command in MATLAB. For more information, refer to the [README](https://github.com/mathworks/MATLAB-extension-for-vscode/blob/main/README.md). This setting can be specified for both the user and workspace setting scopes using the User and Workspace tabs above.", + "scope": "machine-overridable" + }, + "MATLAB.matlabConnectionTiming": { + "type": "string", + "default": "onStart", + "description": "Choose when this extension starts MATLAB in the background. Some functionality will be unavailable without MATLAB running in the background.", + "enum": [ + "onStart", + "onDemand", + "never" + ], + "enumDescriptions": [ + "Start MATLAB as soon as a MATLAB code file is opened", + "Start MATLAB when needed for a given action", + "Never start MATLAB" + ], + "scope": "window" + }, + "MATLAB.indexWorkspace": { + "type": "boolean", + "default": true, + "description": "Automatically index all MATLAB code files (.m) in the current workspace.", + "scope": "window" + }, + "MATLAB.startDebuggerAutomatically": { + "type": "boolean", + "default": false, + "markdownDescription": "Automatically start the Visual Studio Code debugger when MATLAB reaches a breakpoint.", + "scope": "window" + }, + "MATLAB.telemetry": { + "type": "boolean", + "default": true, + "markdownDescription": "Help improve this extension by sending user experience information to MathWorks. For more information, see the [MathWorks Privacy Policy](https://www.mathworks.com/company/aboutus/policies_statements.html).", + "scope": "window", + "tags": [ + "telemetry", + "usesOnlineServices" + ] + }, + "MATLAB.signIn": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable this option to present Sign In Options for unactivated MATLAB installations.", + "scope": "machine" + }, + "MATLAB.showFeatureNotAvailableError": { + "type": "boolean", + "default": true, + "description": "Display an error when a feature requires MATLAB and MATLAB is unable to start.", + "scope": "window" + }, + "MATLAB.maxFileSizeForAnalysis": { + "type": "number", + "default": 0, + "markdownDescription": "The maximum number of characters a file can contain for features such as linting, code navigation, and symbol renaming to be enabled. Use `0` for no limit.", + "scope": "window" + }, + "MATLAB.prewarmGraphics": { + "type": "boolean", + "default": true, + "description": "Prewarm graphics at MATLAB startup to improve performance of first-time graphics rendering.", + "scope": "window" + }, + "MATLAB.defaultEditor": { + "type": "boolean", + "default": true, + "markdownDescription": "Use Visual Studio Code instead of the MATLAB Editor to open and edit files using the `open` and `edit` commands. Certain file types always open in MATLAB by default — for example, live scripts saved in the binary Live Code file format (.mlx) and MATLAB app files (.mlapp).", + "scope": "window" + } + } + }, + { + "title": "Workspace", + "properties": { + "MATLAB.maximumWorkspaceVariables": { + "type": "integer", + "default": 500, + "markdownDescription": "Maximum number of variables to display in the Workspace panel.\n\n_Limiting this value helps ensure panel performance._", + "scope": "window" + }, + "MATLAB.workspaceSortMethod": { + "type": "string", + "default": "natural", + "enum": ["natural", "lexicographic"], + "enumDescriptions": [ + "Compare numbers within names by numeric value", + "Compare names character by character" + ], + "markdownDescription": "Choose how variables are sorted in the Workspace panel.\n\n_Sorting is natural by default._", + "scope": "window" + } } } - }, + ], "languages": [ { "id": "matlab", @@ -383,37 +489,64 @@ "id": "matlab.terminal-profile" } ] + }, + "viewsContainers": { + "activitybar": [ + { + "id": "matlabSidebar", + "title": "MATLAB", + "icon": "./public/logo_outline.svg" + } + ] + }, + "views": { + "matlabSidebar": [ + { + "id": "workspaceBrowserSidebarView", + "name": "Workspace", + "type": "webview", + "icon": "./public/logo_outline.svg" + } + ] } }, "scripts": { "vscode:prepublish": "npm run compile && cd server && npm prune --production && cd ..", - "compile": "tsc -p ./ && cd server && npm run compile && cd ..", - "watch": "tsc -watch -p ./ && cd server && npm run watch && cd ..", + "compile": "tsc -p ./ && npm run copy-wsb-resources && webpack && cd server && npm run compile && cd ..", + "watch": "tsc -watch -p ./ && npm run copy-wsb-resources && cd server && npm run watch && webpack --watch && cd ..", "test-setup": "npm run compile && npm run lint && npm run copy-test-files && npm run copy-config-files", "copy-test-files": "cd src && copyfiles ./test/test-files/**/*.m ./../out/ && cd ..", "copy-config-files": "cd src && copyfiles ./test/tools/config/*.* ./../out/ -all && cd ..", + "copy-wsb-resources": "copyfiles \"src/workspacebrowser/resources/**/*\" out/workspacebrowser/resources/ -u 3", "lint": "eslint src --ext ts", "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-smoke:fast": "node ./out/test/smoke/runTest.js", "test-ui:fast": "node ./out/test/ui/runTest.js", + "test-wsb": "npm run test-setup && node out/test/workspacebrowser/runTest.js", + "test-wsb:fast": "node out/test/workspacebrowser/runTest.js", "test": "npm run test-setup && npm run test:fast", - "test:fast": "npm run test-smoke:fast && npm run test-ui:fast", + "test:fast": "npm run test-smoke:fast && npm run test-ui:fast && npm run test-wsb:fast", "project-install": "node ./build/projectInstall.js", "project-install-clean": "node ./build/projectInstall.js --clean", "package": "vsce package" }, "devDependencies": { + "@types/chai": "^5.2.2", "@types/glob": "^8.0.0", + "@types/jsdom": "^21.1.7", "@types/mocha": "^9.1.1", + "@types/mock-require": "^3.0.0", "@types/node": "^18.19.50", "@types/node-fetch": "^2.6.2", + "@types/sinon": "^17.0.3", "@types/vscode": "^1.67.0", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^2.19.0", + "chai": "^5.2.1", "copyfiles": "^2.4.1", "eslint": "^8.23.0", "eslint-config-standard-with-typescript": "^22.0.0", @@ -421,9 +554,16 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.0.1", "glob": "^8.0.3", + "jsdom": "^26.1.0", "mocha": "^10.0.0", + "mock-require": "^3.0.3", + "node-loader": "^2.1.0", + "sinon": "^18.0.0", + "ts-loader": "^9.5.2", "typescript": "^5.0.4", - "vscode-extension-tester": "8.14.1" + "vscode-extension-tester": "8.14.1", + "webpack": "^5.101.0", + "webpack-cli": "^6.0.1" }, "dependencies": { "@vscode/debugadapter": "^1.56.0", diff --git a/public/Workspace.png b/public/Workspace.png new file mode 100644 index 0000000000000000000000000000000000000000..b39727a7938ebae02cf8fede915cf0388b336281 GIT binary patch literal 50001 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 Doeu*v literal 0 HcmV?d00001 diff --git a/public/logo_outline.svg b/public/logo_outline.svg new file mode 100644 index 0000000..c4721d2 --- /dev/null +++ b/public/logo_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/server b/server index 38a75ce..046ae83 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 38a75cec8d87311a85c4e632b2c5908073c28dfe +Subproject commit 046ae83001dadb25c27184e24edddee4afc52eb3 diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index 1ce5bba..b9e4d07 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -58,6 +58,13 @@ const ACTION_KEYS = { TAB: '\t', SHIFT_TAB: ESC + '[Z', + CTRL_LEFT: ESC + '[1;5D', + CTRL_RIGHT: ESC + '[1;5C', + CTRL_SHIFT_LEFT: ESC + '[1;6D', + CTRL_SHIFT_RIGHT: ESC + '[1;6C', + CTRL_BACKSPACE: '\x17', + CTRL_DELETE: ESC + 'd', + INVERT_COLORS: ESC + '[7m', RESTORE_COLORS: ESC + '[27m', RED_FOREGROUND: ESC + '[31m', @@ -82,6 +89,8 @@ const RIGHT_REGEX = /^(\x1b\[C)+$/; 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)$/; +// eslint-disable-next-line no-control-regex +const WARNING_SENTINAL_REGEX = /((?:\[\x08)|(?:\]\x08))/; const PROMPTS = { IDLE_PROMPT: '>> ', @@ -465,6 +474,18 @@ export default class CommandWindow implements vscode.Pseudoterminal { return this._handleTab(Direction.FORWARDS); case ACTION_KEYS.SHIFT_TAB: return this._handleTab(Direction.BACKWARDS); + case ACTION_KEYS.CTRL_LEFT: + return this._handleWordLeftRight(CursorDirection.LEFT, AnchorPolicy.MOVE); + case ACTION_KEYS.CTRL_RIGHT: + return this._handleWordLeftRight(CursorDirection.RIGHT, AnchorPolicy.MOVE); + case ACTION_KEYS.CTRL_SHIFT_LEFT: + return this._handleWordLeftRight(CursorDirection.LEFT, AnchorPolicy.KEEP); + case ACTION_KEYS.CTRL_SHIFT_RIGHT: + return this._handleWordLeftRight(CursorDirection.RIGHT, AnchorPolicy.KEEP); + case ACTION_KEYS.CTRL_BACKSPACE: + return this._handleWordBackspace(); + case ACTION_KEYS.CTRL_DELETE: + return this._handleWordDelete(); default: { let result = false; // Handle repeated left/right arrow keys. This is what is received when using Alt+Mouse to move the cursor. @@ -627,6 +648,98 @@ export default class CommandWindow implements vscode.Pseudoterminal { return true; } + private _isWordChar (ch: string): boolean { + return /\w/.test(ch); + } + + private _isWhitespace (ch: string): boolean { + return /\s/.test(ch); + } + + private _charCategory (ch: string): number { + if (this._isWordChar(ch)) return 0; + if (this._isWhitespace(ch)) return 1; + return 2; // punctuation/symbols + } + + private _findWordBoundary (direction: CursorDirection): number { + const editableText = this._currentPromptLine.substring(this._currentPrompt.length); + let pos = this._activeCursorIndex; + + if (direction === CursorDirection.LEFT) { + if (pos === 0) return 0; + pos--; + // Skip whitespace going left + while (pos > 0 && this._isWhitespace(editableText[pos])) { + pos--; + } + // Skip same-category characters going left + const cat = this._charCategory(editableText[pos]); + while (pos > 0 && this._charCategory(editableText[pos - 1]) === cat) { + pos--; + } + } else { + const max = this._getMaxIndexOnLine(); + if (pos === max) return max; + // Skip same-category characters going right + const cat = this._charCategory(editableText[pos]); + while (pos < max && this._charCategory(editableText[pos]) === cat) { + pos++; + } + // Skip whitespace going right + while (pos < max && this._isWhitespace(editableText[pos])) { + pos++; + } + } + return pos; + } + + private _handleWordLeftRight (direction: CursorDirection, anchorPolicy: AnchorPolicy): boolean { + const isLineDirty = this._possiblyUpdateAnchorForCursorChange(anchorPolicy); + const newIndex = this._findWordBoundary(direction); + if (newIndex === this._activeCursorIndex) { + return isLineDirty; + } + this._activeCursorIndex = newIndex; + this._forcedEndOfLineCursorValue = undefined; + this._invalidateCompletionData(); + return true; + } + + private _handleWordBackspace (): boolean { + if (this._activeAnchorIndex !== undefined) { + return this._removeSelection(); + } + if (this._activeCursorIndex === 0) { + return false; + } + const targetIndex = this._findWordBoundary(CursorDirection.LEFT); + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(targetIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(this._activeCursorIndex)); + this._currentPromptLine = before + after; + this._activeCursorIndex = targetIndex; + this._forcedEndOfLineCursorValue = false; + this._markCurrentLineChanged(); + this._invalidateCompletionData(); + return true; + } + + private _handleWordDelete (): boolean { + if (this._activeAnchorIndex !== undefined) { + return this._removeSelection(); + } + if (this._activeCursorIndex === this._getMaxIndexOnLine()) { + return false; + } + const targetIndex = this._findWordBoundary(CursorDirection.RIGHT); + const before = this._currentPromptLine.substring(0, this._getAbsoluteIndexOnLine(this._activeCursorIndex)); + const after = this._currentPromptLine.substring(this._getAbsoluteIndexOnLine(targetIndex)); + this._currentPromptLine = before + after; + this._markCurrentLineChanged(); + this._invalidateCompletionData(); + return true; + } + private _replaceCurrentLineWithNewLine (updatedLine: string, cursorIndex?: number): boolean { this._currentPromptLine = updatedLine; this._activeCursorIndex = cursorIndex ?? this._getMaxIndexOnLine(); @@ -728,9 +841,10 @@ export default class CommandWindow implements vscode.Pseudoterminal { addOutput (output: TextEvent): void { if (this._initialized) { if (output.stream === 0) { - if (output.text.startsWith('Warning:')) { + if (output.text.startsWith('[\b')) { + const text = output.text.replace(WARNING_SENTINAL_REGEX, '') this._writeEmitter.fire(ACTION_KEYS.YELLOW_FOREGROUND); - this.handleText(output.text, true); + this.handleText(text, true); this._writeEmitter.fire(ACTION_KEYS.ALL_DEFAULT_COLORS); } else { this.handleText(output.text, true); @@ -900,6 +1014,9 @@ export default class CommandWindow implements vscode.Pseudoterminal { } else if (state === PromptState.PAUSE) { this._changePrompt(PROMPTS.BUSY_PROMPT); } else if (state === PromptState.INPUT) { + // Bring command window to the front + void vscode.commands.executeCommand('matlab.openCommandWindow') + this._changePrompt(this._currentInputPromptString ?? PROMPTS.FAKE_INPUT_PROMPT); } else { this._changePrompt(PROMPTS.BUSY_PROMPT); diff --git a/src/debug/MatlabDebugger.ts b/src/debug/MatlabDebugger.ts index 7c59931..6446a99 100644 --- a/src/debug/MatlabDebugger.ts +++ b/src/debug/MatlabDebugger.ts @@ -85,6 +85,7 @@ export default class MatlabDebugger extends BaseService { return; } + this._activeSession = session; void this._terminalService.openTerminalOrBringToFront(); }), diff --git a/src/extension.ts b/src/extension.ts index 9a1cdcd..598a1c2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import SectionStylingService from './services/sections/view/SectionStylingServic import TelemetryLogger, { TelemetryEvent } from './services/telemetry/TelemetryLogger' import * as LicensingUtils from './utils/LicensingUtils' import BaseService from './services/BaseService' +import WorkspaceBrowserProvider from './workspacebrowser/WorkspaceBrowserProvider' import MatlabProjectService from './services/projects/MatlabProjectService' const CONNECTION_STATUS_COMMAND = 'matlab.changeMatlabConnection' @@ -96,7 +97,7 @@ class MatlabExtension extends BaseService { const sectionStylingService = new SectionStylingService(this.sectionModel) // Initialize MATLAB Project Service - const matlabProjectService = new MatlabProjectService(this.client, this.mvm) + const matlabProjectService = new MatlabProjectService(this.client, this.mvm, this.telemetryLogger) // Add all disposable services to context subscriptions this.own( @@ -155,6 +156,13 @@ class MatlabExtension extends BaseService { // Set up event listener to react to changes in VS Code's configuration this.own(vscode.workspace.onDidChangeConfiguration(() => this.handleConfigurationChanged)) + + // Initialize workspace browser — the provider handles lifecycle internally via MVM events + const workspaceBrowserProvider = new WorkspaceBrowserProvider(context, multiclientNotifier, this.mvm, this.telemetryLogger) + this.own( + vscode.window.registerWebviewViewProvider('workspaceBrowserSidebarView', workspaceBrowserProvider), + workspaceBrowserProvider + ) } /** diff --git a/src/notifications/Notifications.ts b/src/notifications/Notifications.ts index 606a417..0b60b80 100644 --- a/src/notifications/Notifications.ts +++ b/src/notifications/Notifications.ts @@ -57,6 +57,10 @@ enum Notification { // Default Editor EditorExecutablePath = 'matlab/otherEditor', + // Workspace Browser + WSBServerMessage = 'WSBServerMessage', + WSBClientMessage = 'WSBClientMessage', + // MATLAB projects ProjectOpened = 'matlab/project/opened', ProjectClosed = 'matlab/project/closed' diff --git a/src/services/projects/MatlabProjectService.ts b/src/services/projects/MatlabProjectService.ts index 355a7f6..e30bd84 100644 --- a/src/services/projects/MatlabProjectService.ts +++ b/src/services/projects/MatlabProjectService.ts @@ -6,6 +6,7 @@ import { LanguageClient } from 'vscode-languageclient/node' import BaseService from '../BaseService' import { MatlabMVMConnectionState, MVM } from '../../commandwindow/MVM' import Notification from '../../notifications/Notifications' +import TelemetryLogger from '../telemetry/TelemetryLogger' interface ProjectInformation { name: string @@ -24,7 +25,7 @@ export default class MatlabProjectService extends BaseService { private readonly projectOpenStatusNotification: vscode.StatusBarItem private isProjectOpen = false - constructor (private readonly client: LanguageClient, private readonly mvm: MVM) { + constructor (private readonly client: LanguageClient, private readonly mvm: MVM, private readonly telemetryLogger: TelemetryLogger) { super() // Initialize isOpen context to false @@ -102,7 +103,7 @@ export default class MatlabProjectService extends BaseService { this.updateUiOnProjectOpening() // Send command to MALTAB to open the project - const result = await this.mvm.feval('matlabls.project.openProject', 0, [fileOrFolderUri.fsPath]) + const result = await this.mvm.feval('matlabls.project.openProject', 0, [fileOrFolderUri.fsPath], true) if ('error' in result) { const error = result.error as MatlabError @@ -110,6 +111,13 @@ export default class MatlabProjectService extends BaseService { this.updateUiOnProjectClose() } else { // No direct action - the UI will be updated via notification from the language server + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { + action_type: 'projectOpened', + result: '' + } + }) } } @@ -136,6 +144,13 @@ export default class MatlabProjectService extends BaseService { void vscode.window.showErrorMessage(error.msg) } else { // No direct action - the UI will be updated via notification from the language server + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { + action_type: 'projectClosed', + result: '' + } + }) } } @@ -187,6 +202,13 @@ export default class MatlabProjectService extends BaseService { void vscode.window.showErrorMessage(error.msg) } else { // No direct action - the UI will be updated via notification from the language server + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { + action_type: 'projectCreated', + result: '' + } + }) } } diff --git a/src/test/tools/tester/TerminalTester.ts b/src/test/tools/tester/TerminalTester.ts index 6304485..bbb4944 100644 --- a/src/test/tools/tester/TerminalTester.ts +++ b/src/test/tools/tester/TerminalTester.ts @@ -32,6 +32,24 @@ export class TerminalTester { return await this.vs.poll(this.getTerminalContent.bind(this), expected, `Assertion on terminal content: ${message}`) } + /** + * Extracts content from the terminal based on the provided regular expression. + * + * @param regexp A regexp used to match content within the terminal + * @returns An array of string results which match the provided RegExp, or null if no results are found + */ + public async extractContent (regexp: RegExp): Promise { + const content = await this.terminal.getText() + + if (regexp.global) { + const matchResults = content.match(regexp) + return matchResults + } else { + const matchResults = content.match(regexp) + return matchResults === null ? null : [matchResults[0]] + } + } + /** * Get the content of the MATLAB terminal */ diff --git a/src/test/tools/tester/VSCodeTester.ts b/src/test/tools/tester/VSCodeTester.ts index 30d933e..9b3e384 100644 --- a/src/test/tools/tester/VSCodeTester.ts +++ b/src/test/tools/tester/VSCodeTester.ts @@ -3,6 +3,7 @@ import * as vet from 'vscode-extension-tester' import * as path from 'path' import * as PollingUtils from '../utils/PollingUtils' import { TerminalTester } from './TerminalTester' +import { WorkspaceBrowserTester } from './WorkspaceBrowserTester' import * as assert from 'assert' import { EditorTester } from './EditorTester' @@ -16,6 +17,7 @@ export class VSCodeTester { private readonly statusbar: vet.StatusBar public readonly workbench: vet.Workbench public terminal!: TerminalTester + public workspaceBrowser!: WorkspaceBrowserTester public constructor () { this.vs = this @@ -46,6 +48,41 @@ export class VSCodeTester { return await this.assertMATLABDisconnected() } + /** + * Determines if the connected MATLAB's version is less than the provided version. + * + * @param version The MATLAB version to check against (e.g. "R2023a") + */ + public async isMatlabVersionLessThan (version: string): Promise { + const matlabVersion = await this.getMatlabVersion() + return matlabVersion != null && matlabVersion < version + } + + /** + * Determines if MATLAB version has been outputted into the terminal. + * Returns true if MATLAB version is not null. + */ + private async isMATLABVersionAvailable (): Promise { + const version = await this.terminal.extractContent(/R\d\d\d\d[ab]/) + return version != null + } + + /** + * Gets the MATLAB version of the connected MATLAB. + * + * **Note:** As a side effect of calling this, the terminal will be cleared. + * + * @returns The MATLAB version (e.g. "R2023a") of the connected MATLAB version + */ + private async getMatlabVersion (): Promise { + await this.assertMATLABConnected() + await this.terminal.executeCommand('version') + await this.poll(this.isMATLABVersionAvailable.bind(this), true, 'Expected version output in terminal', 100000) + const version = await this.terminal.extractContent(/R\d\d\d\d[ab]/) + await this.terminal.executeCommand('clc') + return version === null ? null : version[0] + } + /** * check for quickpick to contain a label */ @@ -121,12 +158,36 @@ export class VSCodeTester { */ public async openMATLABTerminal (): Promise { await this.executeCommand('matlab.openCommandWindow') - await this.pause(1000) + await this.pause(5000) const terminal = await new vet.BottomBarPanel().openTerminalView() const terminalTester = new TerminalTester(this, terminal) this.terminal = terminalTester } + /** + * Opens the Workspace Browser sidebar and creates a new WorkspaceBrowserTester + */ + public async openWorkspaceBrowser (): Promise { + const activityBar = new vet.ActivityBar() + const matlabControl = await activityBar.getViewControl('MATLAB') + if (matlabControl != null) { + await matlabControl.click() + } + await this.poll(this.isSideBarOpen.bind(this), true, 'Expected MATLAB sidebar to open') + this.workspaceBrowser = new WorkspaceBrowserTester(this) + } + + private async isSideBarOpen (): Promise { + try { + const sideBar = new vet.SideBarView() + const content = sideBar.getContent() + const sections = await content.getSections() + return sections.length > 0 + } catch { + return false + } + } + 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/tools/tester/WorkspaceBrowserTester.ts b/src/test/tools/tester/WorkspaceBrowserTester.ts new file mode 100644 index 0000000..a24c81a --- /dev/null +++ b/src/test/tools/tester/WorkspaceBrowserTester.ts @@ -0,0 +1,281 @@ +// Copyright 2026 The MathWorks, Inc. +import * as vet from 'vscode-extension-tester' +import { WebDriver, By, until, Key } from 'selenium-webdriver' +import * as assert from 'assert' +import { VSCodeTester } from './VSCodeTester' + +export class WorkspaceBrowserTester { + private readonly vs: VSCodeTester + private readonly driver: WebDriver + private inFrame: boolean = false + private handle: string | undefined + + constructor (vs: VSCodeTester) { + this.vs = vs + this.driver = vet.VSBrowser.instance.driver + } + + // ── Frame Switching ───────────────────────────────────────── + + private async switchToFrame (timeout = 10000): Promise { + if (this.inFrame) return + + this.handle = await this.driver.getWindowHandle() + await this.driver.switchTo().defaultContent() + + const outerIframe = await this.driver.wait( + until.elementLocated(By.css('iframe.webview.ready')), timeout, + 'Timed out waiting for outer webview iframe' + ) + await this.driver.switchTo().frame(outerIframe) + + const innerIframe = await this.driver.wait( + until.elementLocated(By.id('active-frame')), timeout, + 'Timed out waiting for active-frame iframe' + ) + await this.driver.switchTo().frame(innerIframe) + + await this.driver.wait( + until.elementLocated(By.css('.wsb-container')), timeout, + 'Timed out waiting for .wsb-container inside webview' + ) + + this.inFrame = true + } + + private async switchBack (): Promise { + if (!this.inFrame) return + if (this.handle == null) { + this.handle = await this.driver.getWindowHandle() + } + await this.driver.switchTo().window(this.handle) + this.inFrame = false + } + + // ── Read Operations ───────────────────────────────────────── + + private async getRowCount (): Promise { + await this.switchToFrame() + try { + const trs = await this.driver.findElements(By.css('.wsb-table tbody tr')) + return trs.length + } finally { + await this.switchBack() + } + } + + private async getVariableNames (): Promise { + await this.switchToFrame() + try { + const inputs = await this.driver.findElements(By.css('.wsb-table tbody .wsb-name-input')) + const names: string[] = [] + for (const input of inputs) { + names.push(await input.getAttribute('value')) + } + return names + } finally { + await this.switchBack() + } + } + + // ── Assertions ────────────────────────────────────────────── + + async assertVariableExists (varName: string, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.hasVariable.bind(this, varName), true, + message !== '' ? message : `Expected variable "${varName}" to exist in WSB`, timeout + ) + } + + async assertVariableNotExists (varName: string, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.hasVariable.bind(this, varName), false, + message !== '' ? message : `Expected variable "${varName}" to not exist in WSB`, timeout + ) + } + + async assertVariableValue (varName: string, expected: string, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.getVariableField.bind(this, varName, 'value'), expected, + message !== '' ? message : `Expected variable "${varName}" to have value "${expected}"`, timeout + ) + } + + async assertVariableClass (varName: string, expected: string, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.getVariableField.bind(this, varName, 'class'), expected, + message !== '' ? message : `Expected variable "${varName}" to have class "${expected}"`, timeout + ) + } + + async assertVariableSize (varName: string, expected: string, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.getVariableField.bind(this, varName, 'size'), expected, + message !== '' ? message : `Expected variable "${varName}" to have size "${expected}"`, timeout + ) + } + + async assertRowCount (expected: number, message = '', timeout = 30000): Promise { + return await this.vs.poll( + this.getRowCount.bind(this), expected, + message !== '' ? message : `Expected WSB to have ${expected} rows`, timeout + ) + } + + async assertVariableOrder (expectedNames: string[], message = '', timeout = 30000): Promise { + const check = async (): Promise => { + const names = await this.getVariableNames() + if (names.length !== expectedNames.length) return false + return names.every((name, i) => name === expectedNames[i]) + } + return await this.vs.poll( + check.bind(this), true, + message !== '' ? message : `Expected variable order: [${expectedNames.join(', ')}]`, timeout + ) + } + + // ── Edit Operations ───────────────────────────────────────── + + async editValue (varName: string, newValue: string): Promise { + await this.editCell(`tr[data-var="${varName}"] .wsb-value-input`, newValue, Key.ENTER) + } + + async renameVariable (varName: string, newName: string): Promise { + await this.editCell(`tr[data-var="${varName}"] .wsb-name-input`, newName, Key.ENTER) + } + + async editValueAndCancel (varName: string, tempValue: string): Promise { + await this.editCell(`tr[data-var="${varName}"] .wsb-value-input`, tempValue, Key.ESCAPE) + } + + // ── Context Menu Operations ──────────────────────────────── + + async rightClickRow (varName: string): Promise { + await this.contextClick(`tr[data-var="${varName}"]`) + } + + async rightClickColumnHeader (columnName: string): Promise { + await this.contextClick(`th[data-col="${columnName}"]`) + } + + async assertContextMenuVisible (message = ''): Promise { + const elements = await this.driver.findElements( + By.css('.context-view.monaco-menu-container') + ) + assert.strictEqual(elements.length > 0, true, message !== '' ? message : 'Expected context menu to be visible') + } + + async assertContextMenuContains (label: string, message = '', timeout = 10000): Promise { + const hasLabel = async (): Promise => { + const items = await this.driver.findElements( + By.css('.context-view.monaco-menu-container .action-label') + ) + for (const item of items) { + const text = await item.getText() + if (text === label) return true + } + return false + } + return await this.vs.poll( + hasLabel.bind(this), true, + message !== '' ? message : `Expected context menu to contain "${label}"`, timeout + ) + } + + async closeContextMenu (): Promise { + await this.driver.actions({ async: true }).sendKeys(Key.ESCAPE).perform() + } + + // ── Sort Operations ───────────────────────────────────────── + + async clickColumnHeader (columnName: string): Promise { + await this.switchToFrame() + try { + const header = await this.driver.findElement( + By.css(`th[data-col="${columnName}"]`) + ) + await header.click() + } finally { + await this.switchBack() + } + } + + // ── Private Helpers ───────────────────────────────────────── + + private async editCell (selector: string, text: string, confirmKey: string): Promise { + await this.switchToFrame() + try { + const input = await this.driver.findElement(By.css(selector)) + const actions = this.driver.actions({ async: true }) + await actions.doubleClick(input).perform() + await this.waitForEditMode(input) + await input.sendKeys(text) + await input.sendKeys(confirmKey) + await this.waitForReadOnly(input) + } finally { + await this.switchBack() + } + } + + private async contextClick (selector: string): Promise { + await this.switchToFrame() + try { + const element = await this.driver.findElement(By.css(selector)) + const actions = this.driver.actions({ async: true }) + await actions.contextClick(element).perform() + } finally { + await this.switchBack() + } + } + + private async waitForEditMode (input: vet.WebElement): Promise { + await this.driver.wait(async () => { + const readOnly = await input.getAttribute('readOnly') + return readOnly === null || readOnly === 'false' + }, 5000, 'Timed out waiting for input to enter edit mode') + } + + private async waitForReadOnly (input: vet.WebElement): Promise { + await this.driver.wait(async () => { + const readOnly = await input.getAttribute('readOnly') + return readOnly === 'true' + }, 5000, 'Timed out waiting for input to return to readonly') + } + + private async hasVariable (varName: string): Promise { + await this.switchToFrame() + try { + const elements = await this.driver.findElements(By.css(`tr[data-var="${varName}"]`)) + return elements.length > 0 + } catch { + return false + } finally { + await this.switchBack() + } + } + + private async getVariableField (varName: string, field: 'value' | 'class' | 'size'): Promise { + await this.switchToFrame() + try { + let selector: string + switch (field) { + case 'value': + selector = `tr[data-var="${varName}"] .wsb-value-input` + break + case 'class': + selector = `tr[data-var="${varName}"] td[data-col="Class"] .wsb-text-input` + break + case 'size': + selector = `tr[data-var="${varName}"] td[data-col="Size"] .wsb-text-input` + break + } + const elements = await this.driver.findElements(By.css(selector)) + if (elements.length === 0) return '' + return await elements[0].getAttribute('value') + } catch { + return '' + } finally { + await this.switchBack() + } + } +} diff --git a/src/test/ui/workspacebrowser.test.ts b/src/test/ui/workspacebrowser.test.ts new file mode 100644 index 0000000..8829f82 --- /dev/null +++ b/src/test/ui/workspacebrowser.test.ts @@ -0,0 +1,158 @@ +// Copyright 2026 The MathWorks, Inc. +import { VSCodeTester } from '../tools/tester/VSCodeTester' +import { before, afterEach, after } from 'mocha'; + +suite('Workspace Browser UI Tests', function () { + let vs: VSCodeTester + + before(async function () { + vs = new VSCodeTester(); + await vs.openEditor('hScript1.m') + await vs.assertMATLABConnected() + await vs.openMATLABTerminal() + + if (await vs.isMatlabVersionLessThan('R2023a')) { + // Workspace browser only supported on R2023a and later - skip tests + this.skip() + } + + await vs.openWorkspaceBrowser() + await vs.terminal.assertContains('>>', 'wait for ready prompt') + await vs.terminal.executeCommand('clc') + }); + + afterEach(async function () { + await vs.terminal.executeCommand('clear; clc') + await vs.workspaceBrowser.assertRowCount(0, 'workspace should be empty after clear') + }); + + after(async function () { + await vs.disconnectFromMATLAB() + }); + + // ── Variable Display ──────────────────────────────────────── + + test('Variable appears in WSB after creation in terminal', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x', 'x should appear in workspace') + await vs.workspaceBrowser.assertVariableValue('x', '5', 'value should be 5') + await vs.workspaceBrowser.assertVariableClass('x', 'double', 'class should be double') + await vs.workspaceBrowser.assertVariableSize('x', '1×1', 'size should be 1×1') + }) + + test('Multiple variables display correctly', async function () { + await vs.terminal.executeCommand("x = 5; y = 'hello'; z = [1 2 3];") + await vs.workspaceBrowser.assertRowCount(3, 'three variables should be shown') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.assertVariableExists('y') + await vs.workspaceBrowser.assertVariableExists('z') + await vs.workspaceBrowser.assertVariableClass('y', 'char') + await vs.workspaceBrowser.assertVariableSize('z', '1×3') + }) + + test('Variable removal reflected after clear', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.terminal.executeCommand('clear x') + await vs.workspaceBrowser.assertVariableNotExists('x', 'x should be removed after clear') + }) + + test('Variable value updates on reassignment', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableValue('x', '5') + await vs.terminal.executeCommand('x = 10;') + await vs.workspaceBrowser.assertVariableValue('x', '10', 'value should update to 10') + }) + + // ── Edit Value ────────────────────────────────────────────── + + test('Edit value via double-click and Enter', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.editValue('x', '42') + await vs.workspaceBrowser.assertVariableValue('x', '42', 'value should be updated to 42') + }) + + test('Edit value cancel via Escape reverts', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.editValueAndCancel('x', '999') + await vs.workspaceBrowser.assertVariableValue('x', '5', 'value should remain 5 after Escape') + }) + + // ── Rename Variable ───────────────────────────────────────── + + test('Rename variable via double-click on name', async function () { + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.renameVariable('x', 'myVar') + await vs.workspaceBrowser.assertVariableNotExists('x', 'old name should be gone') + await vs.workspaceBrowser.assertVariableExists('myVar', 'new name should appear') + await vs.workspaceBrowser.assertVariableValue('myVar', '5', 'value should be preserved') + }) + + test('Rename preserves value and updates display', async function () { + await vs.terminal.executeCommand('data = [1 2 3 4 5];') + await vs.workspaceBrowser.assertVariableExists('data') + await vs.workspaceBrowser.renameVariable('data', 'results') + await vs.workspaceBrowser.assertVariableExists('results') + await vs.workspaceBrowser.assertVariableSize('results', '1×5', 'size should be preserved') + await vs.workspaceBrowser.assertVariableClass('results', 'double', 'class should be preserved') + }) + + // ── Context Menu ─────────────────────────────────────────── + + // Note: Native context menus are not supported on macOS by vscode-extension-tester + // https://github.com/redhat-developer/vscode-extension-tester/blob/main/KNOWN_ISSUES.md#macos-known-limitations-of-native-objects + + test('Right-click on row shows context menu with expected items', async function () { + if (process.platform === 'darwin') this.skip() + + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.rightClickRow('x') + await vs.workspaceBrowser.assertContextMenuVisible('Context menu should appear after right-clicking a row') + await vs.workspaceBrowser.assertContextMenuContains('Delete', 'Context menu should contain "Delete"') + await vs.workspaceBrowser.assertContextMenuContains('Rename', 'Context menu should contain "Rename"') + await vs.workspaceBrowser.assertContextMenuContains('Edit Value', 'Context menu should contain "Edit Value"') + await vs.workspaceBrowser.closeContextMenu() + }) + + test('Right-click on column header shows context menu with sort options', async function () { + if (process.platform === 'darwin') this.skip() + + await vs.terminal.executeCommand('x = 5;') + await vs.workspaceBrowser.assertVariableExists('x') + await vs.workspaceBrowser.rightClickColumnHeader('Name') + await vs.workspaceBrowser.assertContextMenuVisible('Context menu should appear after right-clicking a header') + await vs.workspaceBrowser.assertContextMenuContains('Sort Ascending', 'Context menu should contain "Sort Ascending"') + await vs.workspaceBrowser.assertContextMenuContains('Sort Descending', 'Context menu should contain "Sort Descending"') + await vs.workspaceBrowser.closeContextMenu() + }) + + // ── Sort by Header Click ──────────────────────────────────── + + test('Click Name header sorts ascending and descending', async function () { + await vs.terminal.executeCommand('b = 1; a = 2; c = 3;') + await vs.workspaceBrowser.assertRowCount(3) + // Click a different column first so that clicking Name guarantees ascending + await vs.workspaceBrowser.clickColumnHeader('Class') + await vs.workspaceBrowser.clickColumnHeader('Name') + await vs.workspaceBrowser.assertVariableOrder(['a', 'b', 'c'], 'should be sorted A-Z') + // Second click on same column toggles to descending + await vs.workspaceBrowser.clickColumnHeader('Name') + await vs.workspaceBrowser.assertVariableOrder(['c', 'b', 'a'], 'should be sorted Z-A') + }) + + test('Sort persists after new variable creation', async function () { + await vs.terminal.executeCommand('b = 1; a = 2;') + await vs.workspaceBrowser.assertRowCount(2) + // Click a different column first, then Name, to guarantee Name/ascending + await vs.workspaceBrowser.clickColumnHeader('Class') + await vs.workspaceBrowser.clickColumnHeader('Name') + await vs.workspaceBrowser.assertVariableOrder(['a', 'b']) + await vs.terminal.executeCommand('aa = 3;') + await vs.workspaceBrowser.assertRowCount(3) + await vs.workspaceBrowser.assertVariableOrder(['a', 'aa', 'b'], 'sort should be maintained') + }) +}); diff --git a/src/test/unit/CommandWindow.test.ts b/src/test/unit/CommandWindow.test.ts new file mode 100644 index 0000000..0dba6f3 --- /dev/null +++ b/src/test/unit/CommandWindow.test.ts @@ -0,0 +1,332 @@ +// Copyright 2026 The MathWorks, Inc. + +/* eslint-disable import/first, @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ + +// Register vscode mock BEFORE any imports that depend on it +import { registerMockVscode } from './mock-vscode'; +registerMockVscode(); + +import * as assert from 'assert'; +import { suite, test, setup } from 'mocha'; +import CommandWindow from '../../commandwindow/CommandWindow'; +import { MVM, MatlabMVMConnectionState } from '../../commandwindow/MVM'; +import { PromptState } from '../../commandwindow/MVMInterface'; + +/** + * Creates a CommandWindow with minimal mocks, initialized to READY state. + */ +function createTestCommandWindow (): CommandWindow { + const mockNotifier = { + onNotification: () => ({ dispose: () => {} }), + sendNotification: () => {} + }; + + // Capture event listeners so we can fire promptChange + const eventListeners: Record void> = {}; + const mockMvm = Object.create(MVM.prototype); + mockMvm.on = (event: string, handler: (...args: any[]) => void) => { + eventListeners[event] = handler; + return { dispose: () => {} }; + }; + mockMvm.getMatlabState = () => MatlabMVMConnectionState.CONNECTED; + mockMvm.emit = () => {}; + + const cw = new CommandWindow(mockMvm, mockNotifier as any); + cw.open({ rows: 30, columns: 100 }); + + // Simulate MATLAB becoming ready — sets prompt to '>> ' and state to READY + if (eventListeners.promptChange !== undefined) { + eventListeners.promptChange(PromptState.READY, true); + } + + return cw; +} + +/** + * Gets the current editable text (part after prompt). + */ +function getEditableText (cw: any): string { + return cw._currentPromptLine.substring(cw._currentPrompt.length); +} + +/** + * Gets the cursor position within the editable text. + */ +function getCursorIndex (cw: any): number { + return cw._lastKnownCursorIndex; +} + +/** + * Gets the selection anchor index, if any. + */ +function getAnchorIndex (cw: any): number | undefined { + return cw._lastKnownAnchorIndex; +} + +const ESC = '\x1b'; +const KEYS = { + CTRL_LEFT: ESC + '[1;5D', + CTRL_RIGHT: ESC + '[1;5C', + CTRL_SHIFT_LEFT: ESC + '[1;6D', + CTRL_SHIFT_RIGHT: ESC + '[1;6C', + CTRL_BACKSPACE: '\x17', + CTRL_DELETE: ESC + 'd', + LEFT: ESC + '[D', + RIGHT: ESC + '[C', + HOME: ESC + '[H' +}; + +suite('CommandWindow Escape (clear line)', () => { + let cw: CommandWindow; + + setup(() => { + cw = createTestCommandWindow(); + }); + + test('Escape clears the current input line', () => { + cw.handleInput('some typed text'); + cw.handleInput('\x1b'); + assert.strictEqual(getEditableText(cw), ''); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('Escape clears line and removes selection', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_SHIFT_LEFT); // select 'world' + cw.handleInput('\x1b'); + assert.strictEqual(getEditableText(cw), ''); + assert.strictEqual(getAnchorIndex(cw), undefined); + }); +}); + +suite('CommandWindow Word Navigation', () => { + let cw: CommandWindow; + + setup(() => { + cw = createTestCommandWindow(); + }); + + suite('Ctrl+Left (word left)', () => { + test('jumps to start of previous word', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 6); + }); + + test('jumps over multiple words', () => { + cw.handleInput('one two three'); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 8); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 4); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('does nothing at start of line', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('stops at punctuation boundary', () => { + cw.handleInput('foo + bar'); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 6); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 4); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('handles single word', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('handles only whitespace', () => { + cw.handleInput(' '); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('handles only punctuation', () => { + cw.handleInput('+++'); + cw.handleInput(KEYS.CTRL_LEFT); + assert.strictEqual(getCursorIndex(cw), 0); + }); + }); + + suite('Ctrl+Right (word right)', () => { + test('jumps to start of next word', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 6); + }); + + test('jumps over multiple words', () => { + cw.handleInput('one two three'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 4); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 8); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 13); + }); + + test('does nothing at end of line', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 5); + }); + + test('jumps from mid-word to end of word then past whitespace', () => { + cw.handleInput('hello world'); + // Move cursor to index 2 (inside 'hello') + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.RIGHT); + cw.handleInput(KEYS.RIGHT); + cw.handleInput(KEYS.CTRL_RIGHT); + // Should skip remaining word chars ('llo') then non-word (' ') = index 6 + assert.strictEqual(getCursorIndex(cw), 6); + }); + + test('handles only whitespace', () => { + cw.handleInput(' '); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 3); + }); + + test('handles only punctuation', () => { + cw.handleInput('+++'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_RIGHT); + assert.strictEqual(getCursorIndex(cw), 3); + }); + }); + + suite('Ctrl+Shift+Left (word select left)', () => { + test('selects previous word', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_SHIFT_LEFT); + assert.strictEqual(getCursorIndex(cw), 6); + assert.strictEqual(getAnchorIndex(cw), 11); + }); + + test('extends selection over multiple words', () => { + cw.handleInput('one two three'); + cw.handleInput(KEYS.CTRL_SHIFT_LEFT); + cw.handleInput(KEYS.CTRL_SHIFT_LEFT); + assert.strictEqual(getCursorIndex(cw), 4); + assert.strictEqual(getAnchorIndex(cw), 13); + }); + }); + + suite('Ctrl+Shift+Right (word select right)', () => { + test('selects next word', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_SHIFT_RIGHT); + assert.strictEqual(getCursorIndex(cw), 6); + assert.strictEqual(getAnchorIndex(cw), 0); + }); + }); + + suite('Ctrl+Backspace (delete word left)', () => { + test('deletes previous word from end', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'hello '); + assert.strictEqual(getCursorIndex(cw), 6); + }); + + test('deletes word and trailing space', () => { + cw.handleInput('one two three'); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'one two '); + }); + + test('deletes from middle of line', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.LEFT); + cw.handleInput(KEYS.LEFT); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'hello ld'); + }); + + test('does nothing at start of line', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'hello'); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('deletes entire single word', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), ''); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('handles consecutive spaces', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'hello '); + }); + + test('removes selection instead of word when selection is active', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.CTRL_SHIFT_LEFT); // select 'world' + cw.handleInput(KEYS.CTRL_BACKSPACE); + assert.strictEqual(getEditableText(cw), 'hello '); + assert.strictEqual(getAnchorIndex(cw), undefined); + }); + }); + + suite('Ctrl+Delete (delete word right)', () => { + test('deletes next word from start', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_DELETE); + assert.strictEqual(getEditableText(cw), 'world'); + assert.strictEqual(getCursorIndex(cw), 0); + }); + + test('deletes word from middle', () => { + cw.handleInput('one two three'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_RIGHT); // move past 'one ' + cw.handleInput(KEYS.CTRL_DELETE); + assert.strictEqual(getEditableText(cw), 'one three'); + assert.strictEqual(getCursorIndex(cw), 4); + }); + + test('does nothing at end of line', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.CTRL_DELETE); + assert.strictEqual(getEditableText(cw), 'hello'); + }); + + test('deletes entire single word from start', () => { + cw.handleInput('hello'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_DELETE); + assert.strictEqual(getEditableText(cw), ''); + }); + + test('removes selection instead of word when selection is active', () => { + cw.handleInput('hello world'); + cw.handleInput(KEYS.HOME); + cw.handleInput(KEYS.CTRL_SHIFT_RIGHT); // select 'hello ' + cw.handleInput(KEYS.CTRL_DELETE); + assert.strictEqual(getEditableText(cw), 'world'); + assert.strictEqual(getAnchorIndex(cw), undefined); + }); + }); +}); diff --git a/src/test/unit/mock-vscode.ts b/src/test/unit/mock-vscode.ts new file mode 100644 index 0000000..1820eca --- /dev/null +++ b/src/test/unit/mock-vscode.ts @@ -0,0 +1,80 @@ +// Copyright 2026 The MathWorks, Inc. + +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ + +/** + * Minimal mock of the 'vscode' module for unit testing CommandWindow. + * Must be registered before importing CommandWindow. + */ + +class MockEventEmitter { + private _listeners: Array<(e: T) => void> = []; + + event = (listener: (e: T) => void): { dispose: () => void } => { + this._listeners.push(listener); + return { dispose: () => {} }; + }; + + fire (data: T): void { + for (const l of this._listeners) l(data); + } + + dispose (): void { + this._listeners = []; + } +} + +class MockDisposable { + dispose (): void {} +} + +const mockVscode = { + EventEmitter: MockEventEmitter, + Disposable: MockDisposable, + commands: { + executeCommand: (..._args: unknown[]) => Promise.resolve() + }, + window: { + onDidOpenTerminal: () => ({ dispose: () => {} }), + onDidCloseTerminal: () => ({ dispose: () => {} }), + onDidChangeActiveTerminal: () => ({ dispose: () => {} }), + createTerminal: () => ({}), + registerTerminalProfileProvider: () => ({ dispose: () => {} }) + }, + workspace: { + getConfiguration: () => ({ + get: () => [], + update: () => Promise.resolve() + }) + }, + env: { + clipboard: { + readText: () => Promise.resolve(''), + writeText: () => Promise.resolve() + } + } +}; + +export function registerMockVscode (): void { + // Inject 'vscode' into Node's module cache without resolving it on disk + const Module = require('module'); + const originalResolveFilename = Module._resolveFilename; + Module._resolveFilename = function (request: string, ...args: any[]) { + if (request === 'vscode') { + return 'vscode'; // Return a fake path + } + return originalResolveFilename.call(this, request, ...args); + }; + + require.cache.vscode = { + id: 'vscode', + filename: 'vscode', + loaded: true, + exports: mockVscode, + children: [], + paths: [], + path: '', + isPreloading: false, + require: require + } as any; +} diff --git a/src/test/workspacebrowser/icons.test.ts b/src/test/workspacebrowser/icons.test.ts new file mode 100644 index 0000000..0efa43c --- /dev/null +++ b/src/test/workspacebrowser/icons.test.ts @@ -0,0 +1,97 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for the icon mapping module that resolves MATLAB class names to SVG filenames. +// Verifies direct lookups, numeric type fallback, substring-based fallback, and the +// default icon for unknown types. + +import { expect } from 'chai' +import { getIconFilename } from '../../workspacebrowser/icons' + +suite('getIconFilename', () => { + // ── Direct lookup: each mapped type returns its specific icon ── + // These tests act as a regression guard against accidental changes to the + // icon mapping table. If a mapping is removed or renamed, the corresponding + // test will fail and flag the mismatch before it reaches users. + + suite('known types return their mapped icon', () => { + const knownTypes: Array<[string, string]> = [ + ['object', 'ws3d.svg'], + ['cell', 'wsBrackets.svg'], + ['calendarduration', 'wsCalendar.svg'], + ['char', 'wsCharacter.svg'], + ['logical', 'wsCheck.svg'], + ['duration', 'wsClock.svg'], + ['datetime', 'wsDate.svg'], + ['default', 'wsDefault.svg'], + ['categorical', 'wsDots.svg'], + ['ordinal', 'wsDots.svg'], + ['nominal', 'wsDots.svg'], + ['sparse', 'wsSparse.svg'], + ['string', 'wsString.svg'], + ['table', 'wsTable.svg'], + ['dataset', 'wsTable.svg'], + ['timetable', 'wsTableTime.svg'], + ['eventtable', 'wsTableTime.svg'], + ['tall', 'wsTall.svg'], + ['timeseries', 'wsTime.svg'], + ['struct', 'wsTree.svg'] + ] + + for (const [matlabClass, expectedIcon] of knownTypes) { + test(`'${matlabClass}' -> '${expectedIcon}'`, () => { + expect(getIconFilename(matlabClass)).to.equal(expectedIcon) + }) + } + }) + + // ── Numeric type fallback: all MATLAB numeric primitives share the default icon ── + // MATLAB has 10 numeric primitive types (double, single, int8..uint64) that are + // not individually listed in the icon map. They must all resolve to wsDefault.svg + // via the NUMERIC_TYPES set. If a new numeric type is added to MATLAB in the + // future and not handled, this suite will need to be updated. + + suite('numeric types fall back to wsDefault.svg', () => { + const numericTypes = [ + 'double', 'single', + 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64' + ] + + for (const numType of numericTypes) { + test(`'${numType}' -> 'wsDefault.svg'`, () => { + expect(getIconFilename(numType)).to.equal('wsDefault.svg') + }) + } + }) + + // ── Substring-based fallback: types containing 'sparse' or 'tall' ── + // MATLAB compound types like 'double sparse' or 'tall double' include the + // qualifier as a substring. These don't match any direct key in the icon map, + // so the fallback logic checks for substring containment. This is important + // because the server reports these as compound class strings, not separate + // tokens, and without this fallback they would incorrectly get the object icon. + + suite('substring fallback for compound types', () => { + test("type containing 'sparse' returns wsSparse.svg", () => { + expect(getIconFilename('double sparse')).to.equal('wsSparse.svg') + }) + + test("type containing 'tall' returns wsTall.svg", () => { + expect(getIconFilename('tall double')).to.equal('wsTall.svg') + }) + }) + + // ── Unknown types fall back to the generic object icon ── + // Users can define custom MATLAB classes that won't match any known type or + // substring pattern. These must gracefully fall back to the generic object + // icon (ws3d.svg) rather than causing a render error or showing no icon. + + suite('unknown types', () => { + test('completely unknown class returns ws3d.svg (object icon)', () => { + expect(getIconFilename('somecustomclass')).to.equal('ws3d.svg') + }) + + test('empty string returns ws3d.svg', () => { + expect(getIconFilename('')).to.equal('ws3d.svg') + }) + }) +}) diff --git a/src/test/workspacebrowser/provider/configuration.test.ts b/src/test/workspacebrowser/provider/configuration.test.ts new file mode 100644 index 0000000..fba2e87 --- /dev/null +++ b/src/test/workspacebrowser/provider/configuration.test.ts @@ -0,0 +1,199 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for configuration-driven behavior in WorkspaceBrowserProvider. +// Verifies that the max variables warning fires when the workspace exceeds +// the configured limit, and that the GetData request respects the limit. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { createProviderTestHarness } from './helpers' +import { WSB_DEFAULT_MAX_VARIABLES } from '../../../workspacebrowser/WorkspaceBrowserProvider' + +suite('WorkspaceBrowserProvider — configuration', () => { + // The provider reads MATLAB.maximumWorkspaceVariables to cap how many + // variables it requests from the server. Without this, workspaces with + // thousands of variables would cause excessive data transfer and rendering + // lag. These tests verify the GetData request respects the limit and that + // the max-variables warning fires when appropriate. + + let clock: sinon.SinonFakeTimers + + setup(() => { + clock = sinon.useFakeTimers() + }) + + teardown(() => { + clock.restore() + sinon.restore() + }) + + // GetData requests should use the configured max variables as the endRow. + // The default is 500, so with 10 rows the endRow should be min(11, 500) = 11. + test('GetData request respects max variables setting', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + + serverHandler({ type: 'Size', rowCount: 10, columnCount: 4 }) + clock.tick(300) + + const getData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(getData).to.not.be.undefined + expect(getData?.args[1]?.endRow).to.equal(11) + }) + + // When the workspace has fewer rows than the limit, endRow equals rowCount + 1 + // (1-indexed MATLAB convention: rows 1 through rowCount inclusive). + test('GetData endRow is rowCount + 1 when below limit', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + + serverHandler({ type: 'Size', rowCount: 50, columnCount: 4 }) + clock.tick(300) + + const getData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(getData?.args[1]?.endRow).to.equal(51) + }) + + // When the server returns more rows than maxVariables, the provider caps the + // data sent to the webview. The default limit is 500, so 600 rows from the + // server should result in only 500 rows posted to the webview. + test('data sent to webview is capped at max variables', () => { + const { serverHandler, postMessage } = createProviderTestHarness() + const numRows = 600 + + // Provide column metadata so handleDataMessage can map positional data + serverHandler({ + type: 'Columns', + columns: [ + { column: 'Name', label: 'Name' }, + { column: 'Value', label: 'Value' }, + { column: 'Size', label: 'Size' }, + { column: 'Class', label: 'Class' } + ] + }) + + // Send a Size message to set numRows, then send Data with 600 rows + serverHandler({ type: 'Size', rowCount: numRows, columnCount: 4 }) + clock.tick(300) + + const rows = Array.from({ length: numRows }, (_, i) => ({ + data: [`var${i}`, String(i), '1x1', 'double'] + })) + serverHandler({ type: 'Data', data: rows }) + + const setDataCall = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataCall).to.not.be.undefined + expect(setDataCall?.args[0]?.rows.length).to.equal(WSB_DEFAULT_MAX_VARIABLES) + }) + + // DataChanged events with size info should also trigger GetData through + // the throttle, maintaining the same max-variables cap. + test('DataChanged with size info triggers throttled GetData', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + + // Initial Size message + serverHandler({ type: 'Size', rowCount: 5, columnCount: 4 }) + clock.tick(300) + sendNotification.resetHistory() + + // DataChanged with updated size + serverHandler({ type: 'DataChanged', rowCount: 8, columnCount: 4 }) + clock.tick(300) + + const getData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(getData).to.not.be.undefined + expect(getData?.args[1]?.endRow).to.equal(9) + }) + + // ── Sort method setting ──────────────────────────────────────── + + // Default natural sorting compares numbers within names by numeric value, + // so "var2" sorts before "var10". This verifies the default behavior is + // preserved when no setting override is present. + test('sortRows uses natural order by default', () => { + const { serverHandler, postMessage, webviewHandler } = createProviderTestHarness({ captureWebviewHandler: true }) + + serverHandler({ + type: 'Columns', + columns: [ + { name: 'Name', label: 'Name', sortable: true, resizable: true }, + { name: 'Value', label: 'Value', sortable: false, resizable: true }, + { name: 'Size', label: 'Size', sortable: true, resizable: true }, + { name: 'Class', label: 'Class', sortable: true, resizable: true } + ] + }) + serverHandler({ + type: 'Data', + data: [ + { data: ['var10', '10', '1x1', 'double'] }, + { data: ['var2', '2', '1x1', 'double'] }, + { data: ['var1', '1', '1x1', 'double'] } + ] + }) + postMessage.resetHistory() + + // Setting sort state triggers sendDataToWebview since savedState starts undefined + webviewHandler({ type: 'stateChanged', state: { sortColumn: 'Name', sortDirection: 'asc' } }) + + const setDataCall = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataCall).to.not.be.undefined + const names = setDataCall?.args[0]?.rows.map((r: { name: string }) => r.name) + // Natural: var1, var2, var10 (numeric comparison) + expect(names).to.deep.equal(['var1', 'var2', 'var10']) + }) + + // When workspaceSortMethod is "lexicographic", sorting uses character-by-character + // comparison where "var10" < "var2" because '1' < '2'. + test('sortRows uses lexicographic order when configured', () => { + const getConfigStub = sinon.stub(vscode.workspace, 'getConfiguration') + getConfigStub.returns({ + get: (key: string, defaultValue: unknown) => { + if (key === 'workspaceSortMethod') return 'lexicographic' + return defaultValue + } + } as any) + + const { serverHandler, postMessage, webviewHandler } = createProviderTestHarness({ captureWebviewHandler: true }) + + serverHandler({ + type: 'Columns', + columns: [ + { name: 'Name', label: 'Name', sortable: true, resizable: true }, + { name: 'Value', label: 'Value', sortable: false, resizable: true }, + { name: 'Size', label: 'Size', sortable: true, resizable: true }, + { name: 'Class', label: 'Class', sortable: true, resizable: true } + ] + }) + serverHandler({ + type: 'Data', + data: [ + { data: ['var10', '10', '1x1', 'double'] }, + { data: ['var2', '2', '1x1', 'double'] }, + { data: ['var1', '1', '1x1', 'double'] } + ] + }) + postMessage.resetHistory() + + // Setting sort state triggers sendDataToWebview since savedState starts undefined + webviewHandler({ type: 'stateChanged', state: { sortColumn: 'Name', sortDirection: 'asc' } }) + + const setDataCall = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataCall).to.not.be.undefined + const names = setDataCall?.args[0]?.rows.map((r: { name: string }) => r.name) + // Lexicographic: var1, var10, var2 (character comparison: '1' < '2') + expect(names).to.deep.equal(['var1', 'var10', 'var2']) + + getConfigStub.restore() + }) +}) diff --git a/src/test/workspacebrowser/provider/contextMenuCommands.test.ts b/src/test/workspacebrowser/provider/contextMenuCommands.test.ts new file mode 100644 index 0000000..428baca --- /dev/null +++ b/src/test/workspacebrowser/provider/contextMenuCommands.test.ts @@ -0,0 +1,152 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for the webview-side focusNameInput/focusValueInput message handlers +// triggered by context menu commands (Rename, Edit Value). + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { init, handleMessage, resetState } from '../../../workspacebrowser/webview' +import { WorkspaceColumn, WorkspaceVariable, WebviewToExt } from '../../../workspacebrowser/types' + +// ── Webview-side context menu focus tests ──────────────────────── +// When the user right-clicks a row and selects "Rename" or "Edit Value", +// the provider posts focusNameInput or focusValueInput to the webview. +// These tests verify that handleMessage enters edit mode correctly: +// readOnly becomes false, originalValue is stashed, and the input is focused. + +suite('Context menu — focusNameInput / focusValueInput webview handlers', () => { + let postedMessages: WebviewToExt[] = [] + + const mockApi = { + postMessage: (msg: WebviewToExt) => { postedMessages.push(msg) } + } + + const testColumns: WorkspaceColumn[] = [ + { name: 'Name', label: 'Name', sortable: true, resizable: true }, + { name: 'Class', label: 'Class', sortable: true, resizable: true }, + { name: 'Size', label: 'Size', sortable: true, resizable: true }, + { name: 'Value', label: 'Value', sortable: false, resizable: true } + ] + + const testRows: WorkspaceVariable[] = [ + { name: 'alpha', fields: { Name: 'alpha', Class: 'double', Size: '1x1', Value: '3.14' } }, + { name: 'beta', fields: { Name: 'beta', Class: 'char', Size: '1x5', Value: "'hello'" } } + ] + + setup(() => { + postedMessages = [] + resetState() + document.body.innerHTML = ` +
+
+ + + +
+
+
+ ` + init(mockApi) + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setData', rows: testRows }) + postedMessages = [] + }) + + teardown(() => { + resetState() + document.body.innerHTML = '' + sinon.restore() + }) + + // focusNameInput is the Rename context menu entry point — it must put the + // name input into edit mode so the user can type a new name immediately. + test('focusNameInput sets name input to editable with original value stashed', () => { + handleMessage({ type: 'focusNameInput', variable: 'alpha' }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + expect(input.readOnly).to.be.false + expect(input.dataset.originalValue).to.equal('alpha') + }) + + // focusValueInput is the Edit Value context menu entry point — same behavior + // as focusNameInput but targeting the value column. + test('focusValueInput sets value input to editable with original value stashed', () => { + handleMessage({ type: 'focusValueInput', variable: 'alpha' }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + expect(input.readOnly).to.be.false + expect(input.dataset.originalValue).to.equal('3.14') + }) + + // If the variable name doesn't match any row (e.g. stale context), the + // handler must be a no-op rather than throwing. + test('focusNameInput with unknown variable does not throw', () => { + expect(() => { + handleMessage({ type: 'focusNameInput', variable: 'nonexistent' }) + }).to.not.throw() + }) + + // After focusNameInput, committing a rename via blur should still post + // the renameVariable message — verifies the full context menu rename flow. + test('focusNameInput followed by blur with new name posts renameVariable', () => { + handleMessage({ type: 'focusNameInput', variable: 'alpha' }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.value = 'delta' + input.blur() + + const renameMsg = postedMessages.find(m => m.type === 'renameVariable') + expect(renameMsg).to.not.be.undefined + if (renameMsg != null && renameMsg.type === 'renameVariable') { + expect(renameMsg.variable).to.equal('alpha') + expect(renameMsg.newName).to.equal('delta') + } + }) + + // After focusValueInput, committing an edit via blur should post editValue. + test('focusValueInput followed by blur with new value posts editValue', () => { + handleMessage({ type: 'focusValueInput', variable: 'beta' }) + + const input = document.querySelector('tr[data-var="beta"] .wsb-value-input') as HTMLInputElement + input.value = '99' + input.blur() + + const editMsg = postedMessages.find(m => m.type === 'editValue') + expect(editMsg).to.not.be.undefined + if (editMsg != null && editMsg.type === 'editValue') { + expect(editMsg.variable).to.equal('beta') + expect(editMsg.newValue).to.equal('99') + } + }) + + // Empty-string rename must be rejected client-side without posting a message. + test('focusNameInput followed by clearing to empty reverts without posting', () => { + handleMessage({ type: 'focusNameInput', variable: 'alpha' }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.value = ' ' + input.blur() + + expect(input.value).to.equal('alpha') + const renameMsg = postedMessages.find(m => m.type === 'renameVariable') + expect(renameMsg).to.be.undefined + }) + + // After a successful rename, the row's data-var attribute should reflect + // the new name optimistically so subsequent context menu actions work. + test('rename updates data-var attribute optimistically', () => { + handleMessage({ type: 'focusNameInput', variable: 'alpha' }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.value = 'delta' + input.blur() + + // The row should now be addressable by the new name + const tr = document.querySelector('tr[data-var="delta"]') as HTMLTableRowElement + expect(tr).to.not.be.null + + // The vscode-context attribute should also reflect the new name + const context = JSON.parse(tr.getAttribute('data-vscode-context') ?? '{}') + expect(context.varName).to.equal('delta') + }) +}) diff --git a/src/test/workspacebrowser/provider/dataThrottling.test.ts b/src/test/workspacebrowser/provider/dataThrottling.test.ts new file mode 100644 index 0000000..812f824 --- /dev/null +++ b/src/test/workspacebrowser/provider/dataThrottling.test.ts @@ -0,0 +1,97 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for data request throttling in WorkspaceBrowserProvider. +// Verifies that rapid DataChanged events are coalesced, that zero-row +// workspaces bypass the throttle, and that disconnect cancels pending timers. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { createProviderTestHarness } from './helpers' + +suite('WorkspaceBrowserProvider — data throttling', () => { + // The MATLAB server fires DataChanged on every workspace mutation. A loop + // creating 100 variables fires 100 events in rapid succession. Without + // throttling, each would trigger a GetData round-trip, flooding the server + // and causing visible UI lag. The 300ms debounce ensures at most one request + // per interval, while the zero-row short-circuit handles workspace clear + // operations without any delay. + + let clock: sinon.SinonFakeTimers + + setup(() => { + clock = sinon.useFakeTimers() + }) + + teardown(() => { + clock.restore() + sinon.restore() + }) + + // Multiple events within the throttle window should produce exactly one request. + // This is the primary correctness property of the throttle mechanism. + test('rapid DataChanged events are coalesced into a single GetData request', () => { + const { serverHandler, sendNotification } = createProviderTestHarness({ captureStateChanged: true }) + + // Set initial size so the provider has a non-zero row count + serverHandler({ type: 'Size', rowCount: 10, columnCount: 4 }) + clock.tick(300) + sendNotification.resetHistory() + + // Fire multiple DataChanged events rapidly + serverHandler({ type: 'DataChanged', rowCount: 10, columnCount: 4 }) + serverHandler({ type: 'DataChanged', rowCount: 10, columnCount: 4 }) + serverHandler({ type: 'DataChanged', rowCount: 10, columnCount: 4 }) + + // Before throttle expires: no GetData sent + const beforeThrottle = sendNotification.getCalls().filter( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(beforeThrottle.length).to.equal(0) + + // After throttle expires: exactly one GetData sent + clock.tick(300) + const afterThrottle = sendNotification.getCalls().filter( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(afterThrottle.length).to.equal(1) + }) + + // When the workspace is empty (e.g. after 'clear all'), the table should + // clear instantly. Waiting 300ms for the throttle would make the UI feel + // unresponsive. The zero-row check bypasses the throttle entirely. + test('zero-row workspace bypasses throttle and sends empty data immediately', () => { + const { serverHandler, postMessage } = createProviderTestHarness({ captureStateChanged: true }) + postMessage.resetHistory() + + // Size message with zero rows + serverHandler({ type: 'Size', rowCount: 0, columnCount: 4 }) + + // Should immediately post empty setData (no throttle delay) + const setDataMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataMsg).to.not.be.undefined + expect(setDataMsg?.args[0]?.rows).to.deep.equal([]) + }) + + // If MATLAB disconnects while a throttled request is pending, the timer must + // be cancelled. Otherwise the GetData fires after disconnect, the server is + // gone, and we get an unhandled error or stale data on reconnection. + test('disconnect cancels pending data request timer', () => { + const { serverHandler, stateChangedCallback, sendNotification } = createProviderTestHarness({ captureStateChanged: true }) + + // Trigger a throttled request + serverHandler({ type: 'Size', rowCount: 5, columnCount: 4 }) + sendNotification.resetHistory() + + // Disconnect before the throttle fires + stateChangedCallback('connected', 'disconnected') + clock.tick(300) + + // GetData should NOT have been sent after disconnect + const getDataCalls = sendNotification.getCalls().filter( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(getDataCalls.length).to.equal(0) + }) +}) diff --git a/src/test/workspacebrowser/provider/helpers.ts b/src/test/workspacebrowser/provider/helpers.ts new file mode 100644 index 0000000..38c6c34 --- /dev/null +++ b/src/test/workspacebrowser/provider/helpers.ts @@ -0,0 +1,159 @@ +// Copyright 2026 The MathWorks, Inc. + +// Shared test factory for WorkspaceBrowserProvider tests. +// Provides a configurable mock setup so each test file can override +// only the pieces it needs without duplicating 40+ lines of boilerplate. + +import * as sinon from 'sinon' +import WorkspaceBrowserProvider from '../../../workspacebrowser/WorkspaceBrowserProvider' +import Notification from '../../../notifications/Notifications' + +// ── Mock Shapes ───────────────────────────────────────────────── + +export interface MockNotifier { + sendNotification: sinon.SinonStub + onNotification: sinon.SinonStub +} + +export interface MockMvm { + getMatlabState: sinon.SinonStub | (() => string) + getMatlabRelease: sinon.SinonStub | (() => string) + getReadyPromise: sinon.SinonStub + feval: sinon.SinonStub + eval: sinon.SinonStub + on: sinon.SinonStub + isDebugging: sinon.SinonStub +} + +export interface MockContext { + extensionUri: { toString: () => string } + workspaceState: { get: sinon.SinonStub, update: sinon.SinonStub } + subscriptions: any[] +} + +export interface MockWebviewView { + webview: { + options: Record + html: string + asWebviewUri: (uri: any) => { toString: () => string } + onDidReceiveMessage: sinon.SinonStub + postMessage: sinon.SinonStub + } + onDidDispose: sinon.SinonStub +} + +// ── Factory Options ───────────────────────────────────────────── + +export interface ProviderTestOptions { + // When true, captures the stateChanged callback from mvm.on (default: false) + captureStateChanged?: boolean + // When true, captures the webview message handler from onDidReceiveMessage (default: false) + captureWebviewHandler?: boolean +} + +// ── Factory Result ────────────────────────────────────────────── + +export interface MockTelemetryLogger { + logEvent: sinon.SinonStub +} + +export interface ProviderTestHarness { + provider: WorkspaceBrowserProvider + serverHandler: (msg: Record) => void + sendNotification: sinon.SinonStub + postMessage: sinon.SinonStub + mvm: MockMvm + context: MockContext + webviewView: MockWebviewView + telemetryLogger: MockTelemetryLogger + // Only populated when captureStateChanged is true + stateChangedCallback: (oldState: string, newState: string) => void + // Only populated when captureWebviewHandler is true + webviewHandler: (msg: Record) => void +} + +// ── Factory Function ──────────────────────────────────────────── + +// Creates a fully wired WorkspaceBrowserProvider with stubbed dependencies, +// resolves the webview view, and returns all the handles needed to drive tests. +export function createProviderTestHarness (options: ProviderTestOptions = {}): ProviderTestHarness { + let serverHandler: ((msg: Record) => void) | undefined + let stateChangedCallback: ((oldState: string, newState: string) => void) | undefined + let webviewHandler: ((msg: Record) => void) | undefined + + // ── Notifier ──────────────────────────────────────────────── + const sendNotification = sinon.stub() + const onNotification = sinon.stub().callsFake((tag: string, cb: (msg: Record) => void) => { + if (tag === Notification.WSBServerMessage) { + serverHandler = cb + } + return { dispose: () => {} } + }) + const notifier: MockNotifier = { sendNotification, onNotification } + + // ── MVM ───────────────────────────────────────────────────── + const mvmOn = sinon.stub().callsFake((event: string, cb: (...args: any[]) => void) => { + if (options.captureStateChanged === true && event === 'stateChanged') { + stateChangedCallback = cb + } + return { dispose: () => {} } + }) + const mvm: MockMvm = { + getMatlabState: sinon.stub().returns('disconnected'), + getMatlabRelease: sinon.stub().returns('R2024a'), + getReadyPromise: sinon.stub().resolves(), + feval: sinon.stub().resolves({ result: ['preview text'] }), + eval: sinon.stub().resolves(), + on: mvmOn, + isDebugging: sinon.stub().returns(false) + } + + // ── Extension Context ─────────────────────────────────────── + const context: MockContext = { + extensionUri: { toString: () => 'test://ext' }, + workspaceState: { + get: sinon.stub().returns(undefined), + update: sinon.stub().resolves() + }, + subscriptions: [] + } + + // ── Telemetry Logger ────────────────────────────────────────── + const telemetryLogger: MockTelemetryLogger = { logEvent: sinon.stub() } + + // ── Provider Construction ─────────────────────────────────── + const provider = new WorkspaceBrowserProvider(context as any, notifier as any, mvm as any, telemetryLogger as any) + + // ── Webview View ──────────────────────────────────────────── + const postMessage = sinon.stub().resolves(true) + const onDidReceiveMessage = sinon.stub().callsFake((cb: (msg: Record) => void) => { + if (options.captureWebviewHandler === true) { + webviewHandler = cb + } + return { dispose: () => {} } + }) + const webviewView: MockWebviewView = { + webview: { + options: {}, + html: '', + asWebviewUri: (uri: any) => ({ toString: () => uri?.toString?.() ?? 'test://uri' }), + onDidReceiveMessage, + postMessage + }, + onDidDispose: sinon.stub() + } + provider.resolveWebviewView(webviewView as any, {} as any, { isCancellationRequested: false } as any) + + return { + provider, + serverHandler: serverHandler!, + sendNotification, + postMessage, + mvm, + context, + webviewView, + telemetryLogger, + stateChangedCallback: stateChangedCallback!, + webviewHandler: webviewHandler! + } +} diff --git a/src/test/workspacebrowser/provider/lifecycle.test.ts b/src/test/workspacebrowser/provider/lifecycle.test.ts new file mode 100644 index 0000000..687386d --- /dev/null +++ b/src/test/workspacebrowser/provider/lifecycle.test.ts @@ -0,0 +1,207 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for MATLAB connect/disconnect lifecycle handling in WorkspaceBrowserProvider. +// Verifies that state changes trigger the correct HTML updates, startup eval, +// cache clearing, and resolveWebviewView produces appropriate HTML for each state. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import WorkspaceBrowserProvider from '../../../workspacebrowser/WorkspaceBrowserProvider' + +// ── Helpers ────────────────────────────────────────────────────── + +function createProviderComponents (): { + provider: WorkspaceBrowserProvider + mvm: { + getMatlabState: sinon.SinonStub + getMatlabRelease: sinon.SinonStub + getReadyPromise: sinon.SinonStub + feval: sinon.SinonStub + eval: sinon.SinonStub + on: sinon.SinonStub + } + stateChangedCallback: (oldState: string, newState: string) => void + sendNotification: sinon.SinonStub + telemetryLogger: { logEvent: sinon.SinonStub } +} { + let stateChangedCallback: ((oldState: string, newState: string) => void) | undefined + + const sendNotification = sinon.stub() + const onNotification = sinon.stub().returns({ dispose: () => {} }) + const notifier = { sendNotification, onNotification } + + const mvm = { + getMatlabState: sinon.stub().returns('disconnected'), + getMatlabRelease: sinon.stub().returns('R2024a'), + getReadyPromise: sinon.stub().resolves(), + feval: sinon.stub().resolves({ result: [] }), + eval: sinon.stub().resolves(), + on: sinon.stub().callsFake((event: string, cb: (...args: any[]) => void) => { + if (event === 'stateChanged') { + stateChangedCallback = cb + } + return { dispose: () => {} } + }) + } + + const context = { + extensionUri: { toString: () => 'test://ext' }, + workspaceState: { get: sinon.stub().returns(undefined), update: sinon.stub().resolves() }, + subscriptions: [] + } + + const telemetryLogger = { logEvent: sinon.stub() } + const provider = new WorkspaceBrowserProvider(context as any, notifier as any, mvm as any, telemetryLogger as any) + + return { provider, mvm, stateChangedCallback: stateChangedCallback!, sendNotification, telemetryLogger } +} + +function createWebviewView (): { webviewView: any, postMessage: sinon.SinonStub, triggerDispose: () => void } { + let disposeCallback: (() => void) | undefined + const postMessage = sinon.stub().resolves(true) + const webviewView = { + webview: { + options: {}, + html: '', + asWebviewUri: (uri: any) => ({ toString: () => uri?.toString?.() ?? 'test://uri' }), + onDidReceiveMessage: sinon.stub().returns({ dispose: () => {} }), + postMessage + }, + onDidDispose: sinon.stub().callsFake((cb: () => void) => { disposeCallback = cb }) + } + return { webviewView, postMessage, triggerDispose: () => disposeCallback?.() } +} + +suite('WorkspaceBrowserProvider — lifecycle', () => { + teardown(() => { + sinon.restore() + }) + + // ── resolveWebviewView ─────────────────────────────────────── + // resolveWebviewView is called by VS Code when the sidebar panel becomes + // visible. The HTML it generates depends on the current MATLAB connection + // state. Getting this wrong means users see a blank panel or a confusing + // error. Each connection state has its own HTML variant. + + suite('resolveWebviewView', () => { + test('shows disconnected HTML when MATLAB is not connected', () => { + const { provider } = createProviderComponents() + const { webviewView } = createWebviewView() + + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + expect(webviewView.webview.html).to.include('Disconnected') + }) + + test('shows unsupported HTML when MATLAB version is too old', () => { + const { provider, mvm } = createProviderComponents() + mvm.getMatlabState.returns('connected') + mvm.getMatlabRelease.returns('R2022b') + const { webviewView } = createWebviewView() + + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + expect(webviewView.webview.html).to.include('R2023a') + expect(webviewView.webview.html).to.include('or later') + }) + + test('logs wsbPanelOpened telemetry event', () => { + const { provider, telemetryLogger } = createProviderComponents() + const { webviewView } = createWebviewView() + + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + expect(telemetryLogger.logEvent.calledOnce).to.be.true + expect(telemetryLogger.logEvent.firstCall.args[0]).to.deep.equal({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelOpened', result: '' } + }) + }) + + test('logs wsbPanelClosed telemetry event on dispose', () => { + const { provider, telemetryLogger } = createProviderComponents() + const { webviewView, triggerDispose } = createWebviewView() + + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + telemetryLogger.logEvent.resetHistory() + + triggerDispose() + + expect(telemetryLogger.logEvent.calledOnce).to.be.true + expect(telemetryLogger.logEvent.firstCall.args[0]).to.deep.equal({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelClosed', result: '' } + }) + }) + + test('shows full interactive HTML when MATLAB is connected and supported', () => { + const { provider, mvm } = createProviderComponents() + mvm.getMatlabState.returns('connected') + mvm.getMatlabRelease.returns('R2024a') + const { webviewView } = createWebviewView() + + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + // Full HTML includes the table structure and bundle script + expect(webviewView.webview.html).to.include('wsb-table') + expect(webviewView.webview.html).to.include('bundle.js') + }) + }) + + // ── Connect/Disconnect ─────────────────────────────────────── + // The provider listens for MVM.Events.stateChanged and handles connect/disconnect + // internally — extension.ts no longer calls onMatlabConnected/onMatlabDisconnected. + // On connect: the provider must run the startup eval (which registers the WSB + // backend in MATLAB) and request the initial workspace size. + // On disconnect: caches must be cleared and the webview replaced with + // disconnected HTML so stale data is never shown after reconnection. + + suite('MATLAB state changes', () => { + test('connect triggers startup eval and GetSize request', () => { + const { provider, mvm, stateChangedCallback, sendNotification } = createProviderComponents() + const { webviewView } = createWebviewView() + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + mvm.getMatlabState.returns('connected') + mvm.getMatlabRelease.returns('R2024a') + sendNotification.resetHistory() + + // Simulate MATLAB connecting + stateChangedCallback('disconnected', 'connected') + + // Should run the startup command + expect(mvm.eval.called).to.be.true + expect(mvm.eval.firstCall.args[0]).to.include('MobileWorkspaceBrowser.startup') + + // Should request workspace size + const getSizeCall = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetSize' + ) + expect(getSizeCall).to.not.be.undefined + }) + + test('disconnect shows disconnected HTML', () => { + const { provider, stateChangedCallback } = createProviderComponents() + const { webviewView } = createWebviewView() + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + // Simulate MATLAB disconnecting + stateChangedCallback('connected', 'disconnected') + + expect(webviewView.webview.html).to.include('Disconnected') + }) + + test('connect with unsupported version shows unsupported HTML', () => { + const { provider, mvm, stateChangedCallback } = createProviderComponents() + const { webviewView } = createWebviewView() + provider.resolveWebviewView(webviewView, {} as any, { isCancellationRequested: false } as any) + + mvm.getMatlabState.returns('connected') + mvm.getMatlabRelease.returns('R2020a') + + stateChangedCallback('disconnected', 'connected') + + expect(webviewView.webview.html).to.include('R2023a') + }) + }) +}) diff --git a/src/test/workspacebrowser/provider/serverMessages.test.ts b/src/test/workspacebrowser/provider/serverMessages.test.ts new file mode 100644 index 0000000..249475f --- /dev/null +++ b/src/test/workspacebrowser/provider/serverMessages.test.ts @@ -0,0 +1,217 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for server message handling in WorkspaceBrowserProvider. +// Verifies that each WSBServerMessage type triggers the correct +// webview postMessage and internal cache updates. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { createProviderTestHarness } from './helpers' + +suite('WorkspaceBrowserProvider — server messages', () => { + // The provider receives workspace data from the MATLAB language server via + // WSBServerMessage notifications. Each message type triggers specific actions: + // caching data, posting to the webview, or requesting more data from the server. + // These tests verify the dispatch logic by directly invoking the registered + // notification handler and asserting on the resulting sendNotification and + // postMessage calls. + + let clock: sinon.SinonFakeTimers + + setup(() => { + clock = sinon.useFakeTimers() + }) + + teardown(() => { + clock.restore() + sinon.restore() + }) + + // Size is the first message after GetSize — it tells us how many rows/columns + // exist. If the column count changed, we need to re-fetch column definitions + // because the server may have added or removed columns. + test('Size message requests GetVisibleColumns when column count changes', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + serverHandler({ type: 'Size', rowCount: 5, columnCount: 4 }) + + const getVisibleCols = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetVisibleColumns' + ) + expect(getVisibleCols).to.not.be.undefined + }) + + // Data requests are throttled to prevent flooding the server when rapid + // Size/DataChanged events arrive (e.g. during a loop that creates variables). + // The 300ms debounce ensures at most one GetData per interval. + test('Size message schedules a throttled GetData request', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + sendNotification.resetHistory() + + serverHandler({ type: 'Size', rowCount: 3, columnCount: 4 }) + + // Data request should not fire immediately (throttled) + const immediateGetData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(immediateGetData).to.be.undefined + + // After the throttle delay, GetData should fire + clock.tick(300) + const delayedGetData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(delayedGetData).to.not.be.undefined + }) + + // Column definitions tell the webview what headers to render and which columns + // are sortable/resizable. They are cached so they can be replayed on webview ready. + test('Columns message posts setColumns to webview', () => { + const { serverHandler, postMessage } = createProviderTestHarness() + serverHandler({ + type: 'Columns', + columns: [ + { name: 'Name', label: 'Name' }, + { name: 'Value', label: 'Value' } + ] + }) + + const setColumnsMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setColumns' + ) + expect(setColumnsMsg).to.not.be.undefined + }) + + // The Value column displays formatted strings that don't sort meaningfully + // on the server side. Marking it non-sortable prevents the webview from + // showing a sort indicator that would mislead users. + test('Columns message marks Value column as non-sortable', () => { + const { serverHandler, postMessage } = createProviderTestHarness() + serverHandler({ + type: 'Columns', + columns: [ + { name: 'Name', label: 'Name' }, + { name: 'Value', label: 'Value' } + ] + }) + + const setColumnsMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setColumns' + ) + const valueCol = setColumnsMsg?.args[0]?.columns?.find( + (col: any) => col.name === 'Value' + ) + expect(valueCol?.sortable).to.be.false + }) + + // Data rows arrive with capitalized field names from the server (Name, Class, + // Size, Value). The provider normalizes them to lowercase to match the + // WorkspaceVariable interface. This test verifies the field mapping. + test('Data message posts setData to webview with parsed rows', () => { + const { serverHandler, postMessage } = createProviderTestHarness() + serverHandler({ + type: 'Data', + data: [ + { Name: 'x', Class: 'double', Size: '1x1', Value: '42' } + ] + }) + + const setDataMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataMsg).to.not.be.undefined + expect(setDataMsg?.args[0]?.rows).to.have.length(1) + expect(setDataMsg?.args[0]?.rows[0]?.name).to.equal('x') + }) + + // The server sends rows as positional arrays (data: [val0, val1, ...]) matched + // against the cached column order. This is the production code path — the flat + // object fallback only runs if columns haven't arrived yet. + test('Data message with positional arrays maps fields using cached column order', () => { + const { serverHandler, postMessage } = createProviderTestHarness() + + // Populate column cache so the positional path is used + serverHandler({ + type: 'Columns', + columns: [ + { column: 'Name', label: 'Name' }, + { column: 'Class', label: 'Class' }, + { column: 'Size', label: 'Size' }, + { column: 'Value', label: 'Value' } + ] + }) + postMessage.resetHistory() + + // Send data in positional array format (production format) + serverHandler({ + type: 'Data', + data: [ + { data: ['myVar', 'double', '1x1', '42'], rowNum: 1 }, + { data: ['arr', 'cell', '3x2', '{3x2 cell}'], rowNum: 2 } + ] + }) + + const setDataMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setDataMsg).to.not.be.undefined + const rows = setDataMsg?.args[0]?.rows + expect(rows).to.have.length(2) + expect(rows[0].name).to.equal('myVar') + expect(rows[0].fields.Class).to.equal('double') + expect(rows[0].fields.Size).to.equal('1x1') + expect(rows[0].fields.Value).to.equal('42') + expect(rows[1].name).to.equal('arr') + expect(rows[1].fields.Class).to.equal('cell') + }) + + // DataChanged fires whenever the workspace mutates (variable created, deleted, + // or modified). It must trigger a fresh data fetch, but through the throttle + // to coalesce bursts from rapid MATLAB operations. + test('DataChanged message schedules a throttled data re-request', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + + // First set a non-zero row count via Size + serverHandler({ type: 'Size', rowCount: 2, columnCount: 4 }) + clock.tick(300) + sendNotification.resetHistory() + + // DataChanged should trigger another throttled request + serverHandler({ type: 'DataChanged', rowCount: 3, columnCount: 4 }) + clock.tick(300) + + const getData = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetData' + ) + expect(getData).to.not.be.undefined + }) + + // InternalError is a catch-all from the server for unexpected conditions. + // The provider must log it but never throw — a crash here would kill the + // entire notification handler and silently break all subsequent messages. + test('InternalError message does not crash the provider', () => { + const { serverHandler } = createProviderTestHarness() + // Should log but not throw + expect(() => { + serverHandler({ type: 'InternalError', message: 'test error' }) + }).to.not.throw() + }) + + // WorkspaceBrowserStarted signals the MATLAB backend has finished initializing. + // Only at this point can the server respond with the real column configuration, + // so the provider must request both size and columns here. + test('WorkspaceBrowserStarted requests GetSize and GetVisibleColumns', () => { + const { serverHandler, sendNotification } = createProviderTestHarness() + sendNotification.resetHistory() + + serverHandler({ type: 'WorkspaceBrowserStarted' }) + + const getSize = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetSize' + ) + const getVisibleCols = sendNotification.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[1]?.type === 'GetVisibleColumns' + ) + expect(getSize).to.not.be.undefined + expect(getVisibleCols).to.not.be.undefined + }) +}) diff --git a/src/test/workspacebrowser/provider/versionGate.test.ts b/src/test/workspacebrowser/provider/versionGate.test.ts new file mode 100644 index 0000000..085c2bb --- /dev/null +++ b/src/test/workspacebrowser/provider/versionGate.test.ts @@ -0,0 +1,75 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for the static version gate methods on WorkspaceBrowserProvider. +// The version gate determines whether the connected MATLAB release supports +// the workspace browser feature (requires R2023a or later). + +import { expect } from 'chai' +import WorkspaceBrowserProvider from '../../../workspacebrowser/WorkspaceBrowserProvider' +import { getUnsupportedHtml, getDisconnectedHtml } from '../../../workspacebrowser/templates' + +suite('WorkspaceBrowserProvider — version gate', () => { + // The workspace browser depends on server-side APIs introduced in R2023a. + // The version gate prevents the provider from attempting to use these APIs + // with older MATLAB releases, which would cause silent failures or crashes. + // The gate is checked both on initial webview resolve and on MATLAB connect, + // so it must handle every format the MVM might report. + + suite('isSupported', () => { + // Supported releases: year >= 2023 + test('R2024a is supported', () => { + expect(WorkspaceBrowserProvider.isSupported('R2024a')).to.be.true + }) + + test('R2023a is the minimum supported release', () => { + expect(WorkspaceBrowserProvider.isSupported('R2023a')).to.be.true + }) + + test('R2023b is supported', () => { + expect(WorkspaceBrowserProvider.isSupported('R2023b')).to.be.true + }) + + test('R2025a is supported', () => { + expect(WorkspaceBrowserProvider.isSupported('R2025a')).to.be.true + }) + + // Unsupported releases: year < 2023 + test('R2022b is not supported', () => { + expect(WorkspaceBrowserProvider.isSupported('R2022b')).to.be.false + }) + + test('R2020a is not supported', () => { + expect(WorkspaceBrowserProvider.isSupported('R2020a')).to.be.false + }) + + // Edge cases: null, empty, and malformed strings can arrive when MATLAB + // has not fully initialized or the release string is corrupted. The gate + // must reject all of these rather than throwing or misinterpreting them. + test('null release returns false', () => { + expect(WorkspaceBrowserProvider.isSupported(null)).to.be.false + }) + + test('empty string returns false', () => { + expect(WorkspaceBrowserProvider.isSupported('')).to.be.false + }) + + test('lowercase r2024a is supported (case insensitive)', () => { + expect(WorkspaceBrowserProvider.isSupported('r2024a')).to.be.true + }) + }) + + suite('getUnsupportedHtml', () => { + test('returns HTML mentioning R2023a', () => { + const html = getUnsupportedHtml() + expect(html).to.include('R2023a') + expect(html).to.include('or later') + }) + }) + + suite('getDisconnectedHtml', () => { + test('returns HTML mentioning disconnected state', () => { + const html = getDisconnectedHtml() + expect(html).to.include('Disconnected') + }) + }) +}) diff --git a/src/test/workspacebrowser/provider/webviewMessages.test.ts b/src/test/workspacebrowser/provider/webviewMessages.test.ts new file mode 100644 index 0000000..2027e1b --- /dev/null +++ b/src/test/workspacebrowser/provider/webviewMessages.test.ts @@ -0,0 +1,302 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for webview message handling in WorkspaceBrowserProvider. +// Verifies that 'ready', 'editValue', 'renameVariable', 'deleteVariable', +// and 'stateChanged' messages from the webview trigger the correct actions. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { createProviderTestHarness } from './helpers' + +suite('WorkspaceBrowserProvider — webview messages', () => { + teardown(() => { + sinon.restore() + }) + + // ── ready ──────────────────────────────────────────────────── + // The webview posts 'ready' after init() completes. The provider must replay + // any cached columns, data, and state so the table renders immediately. + // Without this, reopening the sidebar panel (which destroys and recreates + // the webview) would show an empty table until the next DataChanged event. + + suite('ready message', () => { + test('sends cached columns and data back to webview', () => { + const { serverHandler, webviewHandler, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + + // Populate cache by simulating server messages + serverHandler({ + type: 'Columns', + columns: [{ name: 'Name', label: 'Name' }] + }) + serverHandler({ + type: 'Data', + data: [{ Name: 'x', Class: 'double', Size: '1x1', Value: '1' }] + }) + postMessage.resetHistory() + + // Webview signals ready — provider should replay cached data + webviewHandler({ type: 'ready' }) + + const setColumnsMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setColumns' + ) + const setDataMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setData' + ) + expect(setColumnsMsg).to.not.be.undefined + expect(setDataMsg).to.not.be.undefined + }) + + test('sends default columns when no cached columns exist', () => { + const { webviewHandler, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + postMessage.resetHistory() + + // Ready without any prior server messages — no cached columns + webviewHandler({ type: 'ready' }) + + const setColumnsMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'setColumns' + ) + expect(setColumnsMsg).to.not.be.undefined + const cols = setColumnsMsg?.args[0]?.columns + expect(cols).to.have.length(4) + expect(cols[0].name).to.equal('Name') + expect(cols[1].name).to.equal('Value') + expect(cols[2].name).to.equal('Size') + expect(cols[3].name).to.equal('Class') + }) + }) + + // ── editValue ──────────────────────────────────────────────── + // editValue is the bridge between the webview input field and MATLAB variable + // assignment. It uses evalin with the active workspace ('base' normally, + // 'caller' during debug) so edits target the displayed scope. Errors must be + // sent back as operationError so the webview can revert the input and show + // feedback. The readiness guard ensures we don't queue operations when MATLAB + // is busy. + + suite('editValue message', () => { + test('calls mvm.feval with evalin to assign the new value', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + + webviewHandler({ type: 'editValue', variable: 'x', newValue: '42' }) + // Allow promise chain to resolve + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.calledOnce).to.be.true + expect(mvm.feval.firstCall.args[0]).to.equal('evalin') + expect(mvm.feval.firstCall.args[2]).to.deep.include('x = 42;') + }) + + test('posts operationError and shows toast when feval throws', async () => { + const { webviewHandler, mvm, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.rejects(new Error('Invalid expression')) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + webviewHandler({ type: 'editValue', variable: 'x', newValue: 'bad' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + const errorMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'operationError' + ) + expect(errorMsg).to.not.be.undefined + expect(errorMsg?.args[0]?.operation).to.equal('editValue') + expect(errorMsg?.args[0]?.variable).to.equal('x') + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Edit failed') + expect(showError.firstCall.args[0]).to.include('Invalid expression') + }) + + test('shows toast notification when feval returns error', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.resolves({ error: { id: 'MATLAB:error', msg: 'Undefined function or variable', status: 'error' } }) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + webviewHandler({ type: 'editValue', variable: 'x', newValue: '[1 2 3' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Edit failed') + expect(showError.firstCall.args[0]).to.include('Undefined function or variable') + }) + + test('returns silently when MATLAB is not ready', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.getReadyPromise.rejects(new Error('not ready')) + + // Should not throw or call feval + webviewHandler({ type: 'editValue', variable: 'x', newValue: '1' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.called).to.be.false + }) + }) + + // ── renameVariable ───────────────────────────────────────────── + // renameVariable copies the variable to the new name and clears the old one. + // It checks for duplicate names in the cached workspace state before issuing + // the feval. Name validation is handled by MATLAB which returns descriptive errors. + + suite('renameVariable message', () => { + test('calls mvm.feval with evalin to rename the variable', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'y' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.calledOnce).to.be.true + expect(mvm.feval.firstCall.args[0]).to.equal('evalin') + expect(mvm.feval.firstCall.args[2]).to.deep.include("y = x; clear('x');") + }) + + test('posts operationError and shows toast when renamed to invalid name', async () => { + const { webviewHandler, mvm, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: '123bad' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + // Rejected client-side — feval is never called + expect(mvm.feval.called).to.be.false + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Rename failed') + + const errorMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'operationError' + ) + expect(errorMsg).to.not.be.undefined + expect(errorMsg?.args[0]?.variable).to.equal('x') + }) + + test('posts operationError and shows toast when new name already exists in cached rows', async () => { + const { serverHandler, webviewHandler, mvm, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + // Populate cached rows + serverHandler({ + type: 'Columns', + columns: [{ name: 'Name', label: 'Name' }, { name: 'Value', label: 'Value' }, { name: 'Size', label: 'Size' }, { name: 'Class', label: 'Class' }] + }) + serverHandler({ + type: 'Data', + data: [{ Name: 'x', Class: 'double', Size: '1x1', Value: '1' }, { Name: 'y', Class: 'double', Size: '1x1', Value: '2' }] + }) + postMessage.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'y' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.called).to.be.false + const errorMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'operationError' + ) + expect(errorMsg).to.not.be.undefined + expect(errorMsg?.args[0]?.message).to.include('already exists') + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Rename failed') + expect(showError.firstCall.args[0]).to.include('already exists') + }) + + test('posts operationError and shows toast when feval throws', async () => { + const { webviewHandler, mvm, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.rejects(new Error('MATLAB error')) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + postMessage.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'z' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + const errorMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'operationError' + ) + expect(errorMsg).to.not.be.undefined + expect(errorMsg?.args[0]?.operation).to.equal('rename') + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Rename failed') + expect(showError.firstCall.args[0]).to.include('MATLAB error') + }) + }) + + // ── deleteVariable ────────────────────────────────────────────── + // deleteVariable shows a confirmation dialog before clearing. If the user + // cancels, no action is taken. If confirmed, evalin clears the variable + // from the active workspace ('base' or 'caller' during debug). + + suite('deleteVariable message', () => { + test('calls mvm.feval with clear when user confirms', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.calledOnce).to.be.true + expect(mvm.feval.firstCall.args[0]).to.equal('evalin') + expect(mvm.feval.firstCall.args[2]).to.deep.include("clear('x');") + }) + + test('does not call feval when user cancels', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves(undefined) + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(mvm.feval.called).to.be.false + }) + + test('posts operationError and shows toast when feval throws', async () => { + const { webviewHandler, mvm, postMessage } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + mvm.feval.rejects(new Error('MATLAB error')) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + postMessage.resetHistory() + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + const errorMsg = postMessage.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0]?.type === 'operationError' + ) + expect(errorMsg).to.not.be.undefined + expect(errorMsg?.args[0]?.operation).to.equal('delete') + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Delete failed') + expect(showError.firstCall.args[0]).to.include('MATLAB error') + }) + + test('shows toast notification when feval returns error', async () => { + const { webviewHandler, mvm } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + mvm.feval.resolves({ error: { id: 'MATLAB:err', msg: 'Cannot clear protected variable', status: 'error' } }) + const showError = sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(showError.calledOnce).to.be.true + expect(showError.firstCall.args[0]).to.include('Delete failed') + expect(showError.firstCall.args[0]).to.include('Cannot clear protected variable') + }) + }) + + // ── stateChanged ───────────────────────────────────────────── + // The webview posts stateChanged whenever sort order or column widths change. + // The provider persists this to workspaceState so it survives VS Code reload + // and webview disposal. The key 'wsb-state' must match what the constructor + // reads on startup, otherwise state would be written but never restored. + + suite('stateChanged message', () => { + test('persists state to workspaceState', () => { + const { webviewHandler, context } = createProviderTestHarness({ captureWebviewHandler: true }) + const state = { sortColumn: 'Name', sortDirection: 'asc' as const } + + webviewHandler({ type: 'stateChanged', state }) + + expect(context.workspaceState.update.calledOnce).to.be.true + expect(context.workspaceState.update.firstCall.args[0]).to.equal('wsb-state') + expect(context.workspaceState.update.firstCall.args[1]).to.deep.equal(state) + }) + }) +}) diff --git a/src/test/workspacebrowser/runTest.ts b/src/test/workspacebrowser/runTest.ts new file mode 100644 index 0000000..ee930a1 --- /dev/null +++ b/src/test/workspacebrowser/runTest.ts @@ -0,0 +1,111 @@ +// Copyright 2026 The MathWorks, Inc. + +// Test runner that bootstraps a jsdom environment for workspace browser tests. +// Mocks the vscode module, browser globals, and VS Code webview API so that +// both extension-side and webview-side code can be tested in Node.js. + +import * as path from 'path' +import { JSDOM } from 'jsdom' +import * as Mocha from 'mocha' +import * as glob from 'glob' +import mock = require('mock-require') + +// ── Mock vscode module ────────────────────────────────────────── +// Must be registered before any imports that depend on it. +// Individual tests override specific APIs via sinon stubs. +mock('vscode', { + window: { + onDidChangeActiveColorTheme: () => ({ dispose: () => {} }), + createStatusBarItem: () => ({}), + showInformationMessage: () => Promise.resolve(undefined), + showWarningMessage: () => Promise.resolve(undefined), + showErrorMessage: () => Promise.resolve(undefined), + registerWebviewViewProvider: () => ({ dispose: () => {} }) + }, + workspace: { + onDidChangeConfiguration: () => ({ dispose: () => {} }), + getConfiguration: () => ({ + get: (_key: string, defaultValue: unknown) => defaultValue + }) + }, + commands: { + registerCommand: () => ({ dispose: () => {} }), + executeCommand: () => Promise.resolve() + }, + Uri: { + joinPath: (...args: unknown[]) => ({ toString: () => args.join('/') }) + }, + Disposable: class { dispose (): void {} } +}) + +// ── Browser globals via jsdom ─────────────────────────────────── +// Provides document, window, HTMLElement, and event constructors +// needed by webview.ts DOM operations. + +interface BrowserGlobal { + window: unknown + document: Document + customElements: CustomElementRegistry + HTMLElement: typeof HTMLElement +} + +declare const global: BrowserGlobal + +const dom = new JSDOM('', { + url: 'http://localhost', + pretendToBeVisual: true +}) + +;(global as unknown as BrowserGlobal).window = dom.window +;(global as unknown as BrowserGlobal).document = dom.window.document +;(global as unknown as BrowserGlobal).customElements = dom.window.customElements +;(global as unknown as BrowserGlobal).HTMLElement = dom.window.HTMLElement +;(global as unknown as Record).MouseEvent = dom.window.MouseEvent +;(global as unknown as Record).KeyboardEvent = dom.window.KeyboardEvent +;(global as unknown as Record).Event = dom.window.Event +;(global as unknown as Record).MessageEvent = dom.window.MessageEvent + +// Mock acquireVsCodeApi — production code calls this to get the postMessage API +;(global as unknown as Record).acquireVsCodeApi = () => ({ + postMessage: () => {} +}) + +// Set iconsBaseUri on the body element — the webview reads it from document.body.dataset +document.body.setAttribute('data-icons-base-uri', 'test://icons') + +// CSS.escape is not implemented in jsdom — provide a basic polyfill +if (typeof (global as unknown as Record).CSS === 'undefined') { + (global as unknown as Record).CSS = {} +} +(global as unknown as Record string }>).CSS.escape = + (s: string) => s.replace(/([^\w-])/g, '\\$1') + +// requestAnimationFrame is not available in Node.js +;(global as unknown as Record).requestAnimationFrame = (callback: FrameRequestCallback) => { + return setTimeout(callback, 16) +} + +// ── Run Mocha ─────────────────────────────────────────────────── + +async function runTests (): Promise { + const mocha = new Mocha({ + ui: 'tdd', + reporter: 'spec' + }) + + // Discover all test files recursively under this directory + const testRoot = path.resolve(__dirname, '**', '*.test.js').split(path.sep).join('/') + const testFiles = glob.sync(testRoot) + testFiles.forEach((file: string) => mocha.addFile(file)) + + return await new Promise((resolve, reject) => { + mocha.run((failures: number) => { + failures > 0 ? reject(new Error(`${failures} tests failed`)) : resolve() + }) + }) +} + +runTests().catch((err: unknown) => { + console.error(err) + process.exit(1) +}) diff --git a/src/test/workspacebrowser/webview.test.ts b/src/test/workspacebrowser/webview.test.ts new file mode 100644 index 0000000..cb7b4b7 --- /dev/null +++ b/src/test/workspacebrowser/webview.test.ts @@ -0,0 +1,893 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for the webview module — drives the real production code against jsdom +// by injecting a mock VsCodeApi via init(). Each test sets up a DOM skeleton, +// exercises the module through handleMessage() and simulated events, then asserts +// against DOM state and captured messages. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import { init, handleMessage, resetState, getState } from '../../workspacebrowser/webview' +import { WorkspaceVariable, WorkspaceColumn, WebviewToExt } from '../../workspacebrowser/types' + +// ── Helpers ────────────────────────────────────────────────────── + +// Captured messages posted to the extension host by the webview +let postedMessages: WebviewToExt[] = [] + +// Mock VsCodeApi that records messages for assertion +const mockApi = { + postMessage: (msg: WebviewToExt) => { postedMessages.push(msg) } +} + +// Standard columns matching the four workspace browser fields +const testColumns: WorkspaceColumn[] = [ + { name: 'Name', label: 'Name', sortable: true, resizable: true }, + { name: 'Class', label: 'Class', sortable: true, resizable: true }, + { name: 'Size', label: 'Size', sortable: true, resizable: true }, + { name: 'Value', label: 'Value', sortable: false, resizable: true } +] + +// Sample workspace variables for testing +const testRows: WorkspaceVariable[] = [ + { name: 'alpha', fields: { Name: 'alpha', Class: 'double', Size: '1x1', Value: '3.14' } }, + { name: 'beta', fields: { Name: 'beta', Class: 'char', Size: '1x5', Value: "'hello'" } }, + { name: 'gamma', fields: { Name: 'gamma', Class: 'struct', Size: '1x1', Value: '1x1 struct' } } +] + +// Creates the DOM skeleton that the provider HTML would produce +function setupDOM (): void { + document.body.innerHTML = ` +
+
+ + + +
+
+
+ ` +} + +// Sends columns and data to the webview to populate the table +function populateTable (): void { + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setData', rows: testRows }) +} + +// ── Test Suites ────────────────────────────────────────────────── + +suite('webview', () => { + setup(() => { + postedMessages = [] + resetState() + setupDOM() + }) + + teardown(() => { + resetState() + document.body.innerHTML = '' + sinon.restore() + }) + + // ── init ───────────────────────────────────────────────────── + // The init function is the bootstrap entry point for the webview. It must + // post a 'ready' message so the extension host knows it can start sending + // cached data. Without this, the provider would wait indefinitely. + + suite('init', () => { + test('posts ready message to signal the extension host', () => { + init(mockApi) + const readyMsg = postedMessages.find(m => m.type === 'ready') + expect(readyMsg).to.not.be.undefined + }) + }) + + // ── renderTable ────────────────────────────────────────────── + // renderTable is the core rendering function that builds the full DOM from + // module state. These tests verify the structural correctness of the output: + // header cells, data rows, data-vscode-context attributes (required for + // native context menus), icon/label layout, value inputs, resize handles, + // and sort indicators. A regression here would break the entire UI. + + suite('renderTable', () => { + setup(() => { + init(mockApi) + postedMessages = [] + }) + + test('builds header row with correct column names', () => { + populateTable() + const headers = document.querySelectorAll('.wsb-table thead th') + expect(headers.length).to.equal(4) + expect(headers[0].getAttribute('data-col')).to.equal('Name') + expect(headers[3].getAttribute('data-col')).to.equal('Value') + }) + + test('builds data rows matching the number of variables', () => { + populateTable() + const rows = document.querySelectorAll('.wsb-table tbody tr') + expect(rows.length).to.equal(3) + }) + + test('sets data-vscode-context on header cells for context menu', () => { + populateTable() + const th = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + const context = JSON.parse(th.getAttribute('data-vscode-context') ?? '{}') + expect(context.webviewSection).to.equal('header') + expect(context.columnName).to.equal('Name') + expect(context.preventDefaultContextMenuItems).to.equal(true) + }) + + test('sets data-vscode-context on data rows for context menu', () => { + populateTable() + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + const context = JSON.parse(tr.getAttribute('data-vscode-context') ?? '{}') + expect(context.webviewSection).to.equal('row') + expect(context.varName).to.equal('alpha') + expect(context.preventDefaultContextMenuItems).to.equal(true) + }) + + test('renders header when columns arrive but no data rows exist yet', () => { + handleMessage({ type: 'setColumns', columns: testColumns }) + const headers = document.querySelectorAll('.wsb-table thead th') + expect(headers.length).to.equal(4) + const dataRows = document.querySelectorAll('.wsb-table tbody tr') + expect(dataRows.length).to.equal(0) + }) + + test('Name column contains icon image and name input', () => { + populateTable() + const nameCell = document.querySelector('tr[data-var="alpha"] td[data-col="Name"]') as HTMLTableCellElement + const img = nameCell.querySelector('.wsb-icon') as HTMLImageElement + const input = nameCell.querySelector('.wsb-name-input') as HTMLInputElement + expect(img).to.not.be.null + expect(input).to.not.be.null + expect(input.value).to.equal('alpha') + expect(input.readOnly).to.be.true + }) + + test('Value column contains a readonly input that becomes editable on double-click', () => { + populateTable() + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + expect(input).to.not.be.null + expect(input.value).to.equal('3.14') + expect(input.readOnly).to.be.true + + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + expect(input.readOnly).to.be.false + }) + + test('resizer divs are appended to resizable header cells', () => { + populateTable() + const resizers = document.querySelectorAll('.wsb-resizer') + // All 4 columns are resizable in our test data + expect(resizers.length).to.equal(4) + }) + + test('sort indicator is shown on the sorted column', () => { + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setState', state: { sortColumn: 'Name', sortDirection: 'asc' } }) + handleMessage({ type: 'setData', rows: testRows }) + + const nameTh = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + expect(nameTh.classList.contains('wsb-sorted')).to.be.true + const sortIcon = nameTh.querySelector('.wsb-sort-icon') as HTMLSpanElement + expect(sortIcon.textContent).to.include('▲') + }) + + test('applies persisted column widths from state', () => { + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setState', state: { columnWidths: { Name: 120 } } }) + handleMessage({ type: 'setData', rows: testRows }) + + const nameTh = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + expect(nameTh.style.width).to.equal('120px') + expect(nameTh.classList.contains('wsb-manually-resized')).to.be.true + }) + + test('does not render rows when no columns are set', () => { + handleMessage({ type: 'setData', rows: testRows }) + const rows = document.querySelectorAll('.wsb-table tbody tr') + expect(rows.length).to.equal(0) + }) + }) + + // ── Row selection ──────────────────────────────────────────── + // Row selection drives the context menu — the selected variable is the + // target of the "Edit Value" action. The wsb-selected class also provides + // visual feedback. These tests ensure single-selection semantics: clicking + // a new row must deselect the previous one. + + suite('row selection', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('clicking a row adds wsb-selected class', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + expect(tr.classList.contains('wsb-selected')).to.be.true + }) + + test('clicking a different row moves the selection', () => { + const tr1 = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + const tr2 = document.querySelector('tr[data-var="beta"]') as HTMLTableRowElement + tr1.click() + tr2.click() + expect(tr1.classList.contains('wsb-selected')).to.be.false + expect(tr2.classList.contains('wsb-selected')).to.be.true + }) + + test('updates selectedVarName in module state', () => { + const tr = document.querySelector('tr[data-var="gamma"]') as HTMLTableRowElement + tr.click() + expect(getState().selectedVarName).to.equal('gamma') + }) + }) + + // ── Sort interaction ───────────────────────────────────────── + // Header clicks toggle column sorting with instant feedback (no server + // round-trip). The sort state must be persisted via stateChanged so it + // survives webview disposal. Non-sortable columns (Value) must be no-ops + // because the server cannot sort by computed display values. + + suite('sort interaction via header click', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('clicking a sortable header sets sort column and posts stateChanged', () => { + const nameTh = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + nameTh.click() + expect(getState().sortColumn).to.equal('Name') + expect(getState().sortDirection).to.equal('asc') + + const stateMsg = postedMessages.find(m => m.type === 'stateChanged') + expect(stateMsg).to.not.be.undefined + }) + + test('clicking the same header again toggles direction to desc', () => { + // Re-query after first click because renderTable() rebuilds the DOM + const nameTh1 = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + nameTh1.click() + const nameTh2 = document.querySelector('th[data-col="Name"]') as HTMLTableCellElement + nameTh2.click() + expect(getState().sortDirection).to.equal('desc') + }) + + test('clicking a non-sortable header (Value) does not change sort state', () => { + const valueTh = document.querySelector('th[data-col="Value"]') as HTMLTableCellElement + valueTh.click() + expect(getState().sortColumn).to.equal('') + }) + }) + + // ── Value editing ──────────────────────────────────────────── + // Inline value editing lets users assign new values to MATLAB variables + // directly from the workspace browser. The edit flow must: + // - Require a double-click to enter edit mode (single click selects the row) + // - Only post editValue when the value actually changed (avoid no-op fevals) + // - Support Escape to revert without committing + // - Return to readonly on blur + // A bug here would cause silent data loss or unnecessary server traffic. + + suite('value editing', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('double-click enters edit mode then blur with changed value posts editValue', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + expect(input.readOnly).to.be.false + + input.value = '42' + input.blur() + + expect(input.readOnly).to.be.true + const editMsg = postedMessages.find(m => m.type === 'editValue') + expect(editMsg).to.not.be.undefined + if (editMsg != null && editMsg.type === 'editValue') { + expect(editMsg.variable).to.equal('alpha') + expect(editMsg.newValue).to.equal('42') + } + }) + + test('blur with unchanged value does not post editValue', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.blur() + + const editMsg = postedMessages.find(m => m.type === 'editValue') + expect(editMsg).to.be.undefined + }) + + test('Escape key reverts the value to original', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.value = '999' + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) + expect(input.value).to.equal('3.14') + }) + }) + + // ── Error display ──────────────────────────────────────────── + // When an feval operation fails (e.g. invalid assignment syntax), the error + // must be shown inline at the element the user was interacting with, not as + // a modal dialog. For editValue errors, the input must revert to the original + // value and show a transient red border so the user sees the failure without + // losing their place in the table. + + suite('error display', () => { + let clock: sinon.SinonFakeTimers + + setup(() => { + clock = sinon.useFakeTimers() + init(mockApi) + populateTable() + postedMessages = [] + }) + + teardown(() => { + clock.restore() + }) + + test('editValue error reverts input and applies error class', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.dataset.originalValue = '3.14' + input.value = 'bad_value' + + handleMessage({ + type: 'operationError', + operation: 'editValue', + variable: 'alpha', + message: 'Error' + }) + + expect(input.value).to.equal('3.14') + expect(input.classList.contains('wsb-value-error')).to.be.true + + // Error class is removed after 2 seconds + clock.tick(2000) + expect(input.classList.contains('wsb-value-error')).to.be.false + }) + }) + + // ── Name editing (rename) ─────────────────────────────────── + // Inline name editing lets users rename MATLAB variables directly from the + // workspace browser. Like value editing, it requires double-click to enter + // edit mode and validates against duplicate names client-side. + + suite('name editing', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('double-click enters edit mode then blur with changed name posts renameVariable', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + expect(input.readOnly).to.be.false + + input.value = 'delta' + input.blur() + + expect(input.readOnly).to.be.true + const renameMsg = postedMessages.find(m => m.type === 'renameVariable') + expect(renameMsg).to.not.be.undefined + if (renameMsg != null && renameMsg.type === 'renameVariable') { + expect(renameMsg.variable).to.equal('alpha') + expect(renameMsg.newName).to.equal('delta') + } + }) + + test('blur with unchanged name does not post renameVariable', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.blur() + + const renameMsg = postedMessages.find(m => m.type === 'renameVariable') + expect(renameMsg).to.be.undefined + }) + + test('Escape key reverts the name to original', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.value = 'newname' + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) + expect(input.value).to.equal('alpha') + }) + + test('duplicate name posts renameVariable to extension for server-side validation', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.value = 'beta' + input.blur() + + const renameMsg = postedMessages.find(m => m.type === 'renameVariable') + expect(renameMsg).to.not.be.undefined + }) + + test('rename operationError reverts name input and applies error class', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + input.dataset.originalValue = 'alpha' + input.value = 'badname' + + handleMessage({ + type: 'operationError', + operation: 'rename', + variable: 'alpha', + message: 'Error' + }) + + expect(input.value).to.equal('alpha') + expect(input.classList.contains('wsb-value-error')).to.be.true + }) + }) + + // ── Incremental patching ───────────────────────────────────── + // When the server sends updated data and the row set is structurally unchanged + // (same variable names, same count), individual cells are patched in place + // rather than rebuilding the entire table. This preserves the user's active + // edit state, tooltip hover, and selection without coordination logic. + // If the row set changes structurally (added/removed/renamed variables), + // patching must fall back to a full rebuild to avoid stale DOM nodes. + + suite('incremental DOM patching', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('patches cell values in place when row set is unchanged', () => { + // Send updated data with same variable names but different values + const updatedRows: WorkspaceVariable[] = [ + { name: 'alpha', fields: { Name: 'alpha', Class: 'double', Size: '1x1', Value: '6.28' } }, + { name: 'beta', fields: { Name: 'beta', Class: 'char', Size: '1x5', Value: "'world'" } }, + { name: 'gamma', fields: { Name: 'gamma', Class: 'struct', Size: '1x1', Value: '1x1 struct' } } + ] + handleMessage({ type: 'setData', rows: updatedRows }) + + const input = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + expect(input.value).to.equal('6.28') + }) + + test('falls back to full rebuild when row count changes', () => { + const fewerRows: WorkspaceVariable[] = [ + { name: 'alpha', fields: { Name: 'alpha', Class: 'double', Size: '1x1', Value: '3.14' } } + ] + handleMessage({ type: 'setData', rows: fewerRows }) + + const rows = document.querySelectorAll('.wsb-table tbody tr') + expect(rows.length).to.equal(1) + }) + + test('falls back to full rebuild when variable names change', () => { + const renamedRows: WorkspaceVariable[] = [ + { name: 'delta', fields: { Name: 'delta', Class: 'double', Size: '1x1', Value: '1.0' } }, + { name: 'beta', fields: { Name: 'beta', Class: 'char', Size: '1x5', Value: "'hello'" } }, + { name: 'gamma', fields: { Name: 'gamma', Class: 'struct', Size: '1x1', Value: '1x1 struct' } } + ] + handleMessage({ type: 'setData', rows: renamedRows }) + + const firstRow = document.querySelector('.wsb-table tbody tr') as HTMLTableRowElement + expect(firstRow.getAttribute('data-var')).to.not.be.null + }) + }) + + // ── State application ──────────────────────────────────────── + // UI state (column widths, sort preferences) is persisted to workspaceState + // and sent back to the webview on 'ready'. These tests ensure that the + // webview correctly applies restored state so column widths and sort order + // survive webview disposal and MATLAB reconnection. + + suite('state application', () => { + setup(() => { + init(mockApi) + }) + + test('setState message applies sort column and direction', () => { + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setState', state: { sortColumn: 'Class', sortDirection: 'desc' } }) + handleMessage({ type: 'setData', rows: testRows }) + + const state = getState() + expect(state.sortColumn).to.equal('Class') + expect(state.sortDirection).to.equal('desc') + }) + + test('setState message applies column widths', () => { + handleMessage({ type: 'setColumns', columns: testColumns }) + handleMessage({ type: 'setState', state: { columnWidths: { Size: 100 } } }) + handleMessage({ type: 'setData', rows: testRows }) + + expect(getState().columnWidths).to.deep.include({ Size: 100 }) + }) + }) + + // ── Context menu sort ──────────────────────────────────────── + // VS Code native context menus dispatch sort commands via the extension host, + // which posts sortFromContextMenu to the webview. This is a separate code + // path from header clicks and must also update sort state, re-render, and + // persist the change. Without this, context menu sort items would appear + // to do nothing. + + suite('context menu sort', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('sortFromContextMenu updates sort state and re-renders', () => { + handleMessage({ type: 'sortFromContextMenu', column: 'Size', direction: 'desc' }) + + const state = getState() + expect(state.sortColumn).to.equal('Size') + expect(state.sortDirection).to.equal('desc') + + // Should post stateChanged to persist the sort preference + const stateMsg = postedMessages.find(m => m.type === 'stateChanged') + expect(stateMsg).to.not.be.undefined + }) + }) + + // ── Delete key ────────────────────────────────────────────────── + // The Delete key should trigger variable deletion when a row is selected + // and no input is in edit mode. This mirrors the context menu delete flow + // and must not interfere with text editing in name/value inputs. + + suite('delete key', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('pressing Delete with a selected row posts deleteVariable message', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.not.be.undefined + if (deleteMsg != null && deleteMsg.type === 'deleteVariable') { + expect(deleteMsg.variable).to.equal('alpha') + } + }) + + test('pressing Delete with no selected row does nothing', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.be.undefined + }) + + test('pressing Delete while editing a name input does not post deleteVariable', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + + // Enter edit mode on the name input + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + expect(nameInput.readOnly).to.be.false + nameInput.focus() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.be.undefined + }) + + test('pressing Delete while editing a value input does not post deleteVariable', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + + // Enter edit mode on the value input + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + expect(valueInput.readOnly).to.be.false + valueInput.focus() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.be.undefined + }) + + test('pressing Delete with a readonly input focused posts deleteVariable', () => { + const tr = document.querySelector('tr[data-var="beta"]') as HTMLTableRowElement + tr.click() + + // Focus a readonly text input (Class/Size column) + const textInput = document.querySelector('tr[data-var="beta"] .wsb-text-input') as HTMLInputElement + textInput.focus() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.not.be.undefined + if (deleteMsg != null && deleteMsg.type === 'deleteVariable') { + expect(deleteMsg.variable).to.equal('beta') + } + }) + + test('pressing Backspace does not trigger delete', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.be.undefined + }) + + test('pressing other keys does not trigger delete', () => { + const tr = document.querySelector('tr[data-var="alpha"]') as HTMLTableRowElement + tr.click() + postedMessages = [] + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true })) + + const deleteMsg = postedMessages.find(m => m.type === 'deleteVariable') + expect(deleteMsg).to.be.undefined + }) + }) + + // ── Arrow key navigation ──────────────────────────────────────── + // Arrow keys provide cell-based grid navigation. Up/Down move to the same + // column in adjacent rows. Left/Right move between columns in the same row. + // Navigation is blocked when an input is in edit mode. + + suite('arrow key navigation', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('ArrowDown with no focus selects the first row, first column', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('alpha') + }) + + test('ArrowUp with no focus selects the first row', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('alpha') + }) + + test('ArrowDown moves focus to same column in next row', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('beta') + const focused = document.activeElement as HTMLInputElement + expect(focused.classList.contains('wsb-name-input')).to.be.true + expect(focused.closest('tr')?.getAttribute('data-var')).to.equal('beta') + }) + + test('ArrowUp moves focus to same column in previous row', () => { + const nameInput = document.querySelector('tr[data-var="beta"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('alpha') + const focused = document.activeElement as HTMLInputElement + expect(focused.closest('tr')?.getAttribute('data-var')).to.equal('alpha') + }) + + test('ArrowDown on the last row is a no-op', () => { + const input = document.querySelector('tr[data-var="gamma"] .wsb-name-input') as HTMLInputElement + input.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('gamma') + }) + + test('ArrowUp on the first row is a no-op', () => { + const input = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + input.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('alpha') + }) + + test('ArrowRight moves focus to next column in same row', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })) + + const focused = document.activeElement as HTMLInputElement + expect(focused.closest('td')?.getAttribute('data-col')).to.equal('Class') + }) + + test('ArrowLeft moves focus to previous column in same row', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })) + + const focused = document.activeElement as HTMLInputElement + expect(focused.closest('td')?.getAttribute('data-col')).to.equal('Size') + }) + + test('ArrowRight on the last column is a no-op', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })) + + const focused = document.activeElement as HTMLInputElement + expect(focused.closest('td')?.getAttribute('data-col')).to.equal('Value') + }) + + test('ArrowLeft on the first column is a no-op', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })) + + const focused = document.activeElement as HTMLInputElement + expect(focused.closest('td')?.getAttribute('data-col')).to.equal('Name') + }) + + test('Arrow keys while editing do not move focus', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + valueInput.focus() + expect(valueInput.readOnly).to.be.false + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + + const state = getState() + expect(state.selectedVarName).to.equal('alpha') + expect(document.activeElement).to.equal(valueInput) + }) + }) + + // ── Enter key (edit focused cell) ─────────────────────────────── + // Enter activates edit mode on whichever editable cell (Name or Value) + // is currently focused. Non-editable cells (Class, Size) are ignored. + // Blocked during active editing via the noInputEditing guard. + + suite('Enter key (edit focused cell)', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('Enter on a focused readonly Name input enters edit mode', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + + expect(nameInput.readOnly).to.be.false + }) + + test('Enter on a focused readonly Value input enters edit mode', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + + expect(valueInput.readOnly).to.be.false + }) + + test('Enter on a focused Class/Size input is a no-op', () => { + const textInput = document.querySelector('tr[data-var="alpha"] .wsb-text-input') as HTMLInputElement + textInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + + expect(textInput.readOnly).to.be.true + }) + + test('Enter with no focus is a no-op', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + + const inputs = document.querySelectorAll('.wsb-value-input, .wsb-name-input') + for (const input of inputs) { + expect(input.readOnly).to.be.true + } + }) + + test('Enter while editing does not bubble to re-enter edit mode', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + valueInput.focus() + expect(valueInput.readOnly).to.be.false + + // Simulate the input's own keydown handler dispatching Enter with stopPropagation + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + valueInput.dispatchEvent(event) + + // After the input handler blurs, the input should be readonly and NOT re-enter edit mode + expect(valueInput.readOnly).to.be.true + }) + }) + + // ── F2 key (rename) ───────────────────────────────────────────── + // F2 always enters edit mode on the Name input of the selected row, + // regardless of which cell is focused. Blocked during active editing. + + suite('F2 key (rename)', () => { + setup(() => { + init(mockApi) + populateTable() + postedMessages = [] + }) + + test('F2 with a selected row enters edit mode on the name input', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true })) + + expect(nameInput.readOnly).to.be.false + }) + + test('F2 with no selected row is a no-op', () => { + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true })) + + const inputs = document.querySelectorAll('.wsb-name-input') + for (const input of inputs) { + expect(input.readOnly).to.be.true + } + }) + + test('F2 while editing a value input does not trigger rename', () => { + const valueInput = document.querySelector('tr[data-var="alpha"] .wsb-value-input') as HTMLInputElement + valueInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + valueInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true })) + + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + expect(nameInput.readOnly).to.be.true + }) + + test('F2 while editing a name input does not re-trigger edit mode', () => { + const nameInput = document.querySelector('tr[data-var="alpha"] .wsb-name-input') as HTMLInputElement + nameInput.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })) + nameInput.focus() + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true })) + + // Guard blocks; input remains in edit mode + expect(nameInput.readOnly).to.be.false + }) + }) +}) diff --git a/src/workspacebrowser/WorkspaceBrowserProvider.ts b/src/workspacebrowser/WorkspaceBrowserProvider.ts new file mode 100644 index 0000000..6479054 --- /dev/null +++ b/src/workspacebrowser/WorkspaceBrowserProvider.ts @@ -0,0 +1,623 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as vscode from 'vscode' +import BaseService from '../services/BaseService' +import { Notifier } from '../commandwindow/MultiClientNotifier' +import { MVM, MatlabMVMConnectionState } from '../commandwindow/MVM' +import Notification from '../notifications/Notifications' +import { WorkspaceVariable, WorkspaceColumn, SavedState, ExtToWebview } from './types' +import TelemetryLogger from '../services/telemetry/TelemetryLogger' +import { WSB_MINIMUM_RELEASE, getUnsupportedHtml, getDisconnectedHtml, getWebviewHtml } from './templates' + +// Default cap on how many variables the workspace browser will request +export const WSB_DEFAULT_MAX_VARIABLES = 500 + +// Debounce interval for coalescing rapid DataChanged events from the server +const DATA_THROTTLE_MS = 300 + +const MAX_VARS_SETTING_ID = 'MATLAB.maximumWorkspaceVariables' +const SORT_METHOD_SETTING_ID = 'MATLAB.workspaceSortMethod' +const MAX_VARS_BUTTON_TEXT = 'Change Maximum Variable Count' +const MAX_VARIABLES_MESSAGE_TEMPLATE = + 'The MATLAB workspace contains {currentCount} variables, which exceeds the maximum ' + + 'number of variables to display in the Workspace panel ({maxVariables}).' + +const WSB_STATE_KEY = 'wsb-state' + +// Shown immediately on webview load before the server sends column metadata. +// Order must match the server's GetVisibleColumns response: Name, Value, Size, Class. +const DEFAULT_COLUMNS: WorkspaceColumn[] = [ + { name: 'Name', label: 'Name', sortable: true, resizable: true }, + { name: 'Value', label: 'Value', sortable: false, resizable: true }, + { name: 'Size', label: 'Size', sortable: true, resizable: true }, + { name: 'Class', label: 'Class', sortable: true, resizable: true } +] + +export default class WorkspaceBrowserProvider extends BaseService implements vscode.WebviewViewProvider { + private view: vscode.WebviewView | undefined + private cachedRows: WorkspaceVariable[] | undefined + private cachedColumns: WorkspaceColumn[] | undefined + private savedState: SavedState | undefined + private numRows: number = 0 + private numColumns: number = 0 + + // Throttle state for coalescing rapid DataChanged events + private dataRequestPending: boolean = false + private dataRequestTimer: ReturnType | undefined + + // Prevents duplicate max-variables warnings during a single connection session + private maxVarsMessageShown: boolean = false + + constructor ( + private readonly extensionContext: vscode.ExtensionContext, + private readonly notifier: Notifier, + private readonly mvm: MVM, + private readonly telemetryLogger: TelemetryLogger + ) { + super() + + // Restore persisted UI state (column widths, sort preferences) + this.savedState = extensionContext.workspaceState.get(WSB_STATE_KEY) + + // Listen for workspace data from the MATLAB language server + this.own( + notifier.onNotification(Notification.WSBServerMessage, (msg: Record) => { + this.onServerMessage(msg) + }) + ) + + // React to MATLAB connection/disconnection events + this.own( + mvm.on(MVM.Events.stateChanged, (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => { + this.onMatlabStateChanged(newState) + }) + ) + + // Re-render icons when the VS Code theme changes + this.own( + vscode.window.onDidChangeActiveColorTheme(() => { + this.postToWebview({ type: 'themeChanged' }) + }) + ) + + // Re-send sorted/capped data when the user changes the max variables setting + this.own( + vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { + if (event.affectsConfiguration(MAX_VARS_SETTING_ID)) { + this.maxVarsMessageShown = false + this.checkMaxVariablesWarning() + this.sendDataToWebview() + } + if (event.affectsConfiguration(SORT_METHOD_SETTING_ID)) { + this.sendDataToWebview() + } + }) + ) + + // Register commands for the native VS Code context menu on the webview + this.own( + vscode.commands.registerCommand('matlab.wsb.editValue', (context: { varName: string } | undefined) => { + if (context?.varName == null) return + this.postToWebview({ type: 'focusValueInput', variable: context.varName }) + }), + vscode.commands.registerCommand('matlab.wsb.renameVariable', (context: { varName: string } | undefined) => { + if (context?.varName == null) return + this.postToWebview({ type: 'focusNameInput', variable: context.varName }) + }), + vscode.commands.registerCommand('matlab.wsb.deleteVariable', (context: { varName: string } | undefined) => { + if (context?.varName == null) return + void this.handleDeleteVariable(context.varName) + }), + vscode.commands.registerCommand('matlab.wsb.sortAscending', (context: { columnName: string }) => { + this.postToWebview({ type: 'sortFromContextMenu', column: context.columnName, direction: 'asc' }) + }), + vscode.commands.registerCommand('matlab.wsb.sortDescending', (context: { columnName: string }) => { + this.postToWebview({ type: 'sortFromContextMenu', column: context.columnName, direction: 'desc' }) + }) + ) + + // If MATLAB is already connected when the provider is constructed, start immediately + if (mvm.getMatlabState() === MatlabMVMConnectionState.CONNECTED) { + this.onMatlabStateChanged(MatlabMVMConnectionState.CONNECTED) + } + } + + // ── WebviewViewProvider ─────────────────────────────────────────── + + resolveWebviewView ( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + token: vscode.CancellationToken + ): void { + this.view = webviewView + + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelOpened', result: '' } + }) + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.extensionContext.extensionUri] + } + + // Render appropriate HTML based on current connection state + if (this.mvm.getMatlabState() === MatlabMVMConnectionState.CONNECTED) { + if (WorkspaceBrowserProvider.isSupported(this.mvm.getMatlabRelease())) { + webviewView.webview.html = getWebviewHtml(webviewView.webview, this.extensionContext.extensionUri) + } else { + webviewView.webview.html = getUnsupportedHtml() + } + } else { + webviewView.webview.html = getDisconnectedHtml() + } + + // Route messages from the webview to the handler + this.own( + webviewView.webview.onDidReceiveMessage((msg: Record) => { + this.onWebviewMessage(msg) + }) + ) + + webviewView.onDidDispose(() => { + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelClosed', result: '' } + }) + }) + } + + // ── MATLAB Lifecycle ───────────────────────────────────────────── + + private onMatlabStateChanged (newState: MatlabMVMConnectionState): void { + if (newState === MatlabMVMConnectionState.CONNECTED) { + this.onMatlabConnected() + } else { + this.onMatlabDisconnected() + } + } + + private onMatlabConnected (): void { + if (!WorkspaceBrowserProvider.isSupported(this.mvm.getMatlabRelease())) { + if (this.view != null) { + this.view.webview.html = getUnsupportedHtml() + } + return + } + + // Refresh the webview HTML so the script and styles are loaded + if (this.view != null) { + this.view.webview.html = getWebviewHtml(this.view.webview, this.extensionContext.extensionUri) + } + + // Start the MATLAB workspace browser backend + void this.mvm.eval( + 'internal.matlab.desktop_workspacebrowser.MobileWorkspaceBrowser.startup;', + false + ) + + // Request initial workspace size from the language server + this.notifier.sendNotification(Notification.WSBClientMessage, { type: 'GetSize' }) + } + + private onMatlabDisconnected (): void { + // Clear all cached data so stale values are never shown after reconnection + this.cachedRows = undefined + this.cachedColumns = undefined + this.numRows = 0 + this.numColumns = 0 + this.maxVarsMessageShown = false + this.cancelPendingDataRequest() + + if (this.view != null) { + this.view.webview.html = getDisconnectedHtml() + } + } + + // ── Version Gate ───────────────────────────────────────────────── + + // Returns true if the given MATLAB release string meets the minimum version for workspace browser + static isSupported (release: string | null): boolean { + if (release == null || release === '') return false + return release >= WSB_MINIMUM_RELEASE + } + + // Begins with a letter, contains only alphanumeric/underscore, max 2048 characters + static isValidMatlabIdentifier (name: string): boolean { + return name.length > 0 && name.length <= 2048 && (/^[a-zA-Z]\w*$/).test(name) + } + + // ── Server Message Handling ────────────────────────────────────── + + private onServerMessage (msg: Record): void { + if (msg?.type == null) return + + switch (msg.type) { + case 'Size': + this.handleSizeMessage(msg) + break + case 'Columns': + this.handleColumnsMessage(msg) + break + case 'Data': + this.handleDataMessage(msg) + break + case 'DataChanged': + this.handleDataChangedMessage(msg) + break + case 'InternalError': + console.error('Workspace Browser server error:', msg.message) + break + case 'WorkspaceBrowserStarted': + this.notifier.sendNotification(Notification.WSBClientMessage, { type: 'GetSize' }) + this.notifier.sendNotification(Notification.WSBClientMessage, { type: 'GetVisibleColumns' }) + break + } + } + + private handleSizeMessage (msg: Record): void { + this.updateSizeAndColumns(msg.rowCount, msg.columnCount) + this.checkMaxVariablesWarning() + this.scheduleDataRequest() + } + + private handleColumnsMessage (msg: Record): void { + const rawColumns = msg.columns as Array> + if (rawColumns == null) return + + // Server sends 'column'; legacy format sent 'name'/'Name'. The fallback + // chain accommodates both without requiring a server version check. + this.cachedColumns = rawColumns.map((col: Record): WorkspaceColumn => { + const colName = String(col.column ?? col.name ?? col.Name ?? '') + return { + name: colName, + label: String(col.label ?? col.Label ?? colName), + sortable: colName !== 'Value', + resizable: true + } + }) + + this.postToWebview({ type: 'setColumns', columns: this.cachedColumns }) + } + + private handleDataMessage (msg: Record): void { + const rawData = msg.data as Array> + if (rawData == null) return + + // Build a column-name → index map for extracting named fields from positional data + const colIndex: Record = {} + if (this.cachedColumns != null) { + this.cachedColumns.forEach((col: WorkspaceColumn, i: number) => { colIndex[col.name] = i }) + } + + this.cachedRows = rawData.map((row: Record): WorkspaceVariable => { + const fields: Record = {} + const dataArray = row.data as unknown[] | undefined + + if (dataArray != null && this.cachedColumns != null) { + // Primary format: server sends { data: [val0, val1, ...], rowNum: N } + // where the data array is positional, matching the cached column order. + for (const col of this.cachedColumns) { + fields[col.name] = String(dataArray[colIndex[col.name]] ?? '') + } + } else { + // Fallback: flat object with named fields (when columns haven't arrived yet + // or the server uses the legacy per-field format instead of positional arrays) + for (const key of Object.keys(row)) { + if (key === 'data' || key === 'rowNum') continue + fields[key] = String(row[key] ?? '') + } + } + + return { name: fields.Name ?? '', fields } + }) + + this.sendDataToWebview() + } + + private handleDataChangedMessage (msg: Record): void { + if (msg.columnCount !== undefined) { + this.updateSizeAndColumns(msg.rowCount, msg.columnCount) + } + + this.checkMaxVariablesWarning() + this.scheduleDataRequest() + } + + // Shared logic for updating cached row/column counts from Size and DataChanged messages + private updateSizeAndColumns (rawRowCount: unknown, rawColumnCount: unknown): void { + const parsedColumnCount = Number(rawColumnCount) + const newColumnCount = Math.max(0, Math.floor(Number.isNaN(parsedColumnCount) ? 0 : parsedColumnCount)) + if (newColumnCount !== this.numColumns) { + this.notifier.sendNotification(Notification.WSBClientMessage, { type: 'GetVisibleColumns' }) + } + const parsedRowCount = Number(rawRowCount) + this.numRows = Math.max(0, Math.floor(Number.isNaN(parsedRowCount) ? 0 : parsedRowCount)) + this.numColumns = newColumnCount + } + + // ── Webview Message Handling ───────────────────────────────────── + + private onWebviewMessage (msg: Record): void { + switch (msg.type) { + case 'ready': + this.handleWebviewReady() + break + case 'editValue': + void this.handleEditValue(msg.variable as string, msg.newValue as string) + break + case 'renameVariable': + void this.handleRenameVariable(msg.variable as string, msg.newName as string) + break + case 'deleteVariable': + void this.handleDeleteVariable(msg.variable as string) + break + case 'stateChanged': + this.handleStateChanged(msg.state as SavedState) + break + case 'openMaxVariablesSetting': + void vscode.commands.executeCommand('workbench.action.openSettings', MAX_VARS_SETTING_ID) + break + } + } + + // Send cached columns, data, and state to the webview when it signals readiness + private handleWebviewReady (): void { + // Always send columns so the header is visible immediately + const columnsToSend = this.cachedColumns ?? DEFAULT_COLUMNS + this.postToWebview({ type: 'setColumns', columns: columnsToSend }) + + if (this.cachedRows != null) { + this.sendDataToWebview() + } + if (this.savedState != null) { + this.postToWebview({ type: 'setState', state: this.savedState }) + } + + // If no cached data exists, request fresh data from the server + if (this.cachedRows == null) { + this.notifier.sendNotification(Notification.WSBClientMessage, { type: 'GetSize' }) + } + } + + // Assign a new value to a MATLAB variable in the active workspace + private async handleEditValue (variable: string, newValue: string): Promise { + try { + await this.mvm.getReadyPromise() + } catch { + this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message: 'MATLAB is not ready' }) + return + } + + try { + const response = await this.evalInWorkspace(`${variable} = ${newValue};`) + if ('error' in response) { + const message = this.extractErrorMessage(response.error) + this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message }) + this.showMatlabError('editValue', message) + } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message }) + this.showMatlabError('editValue', message) + } + } + + // Rename a variable by assigning to the new name and clearing the old one in the active workspace + private async handleRenameVariable (oldName: string, newName: string): Promise { + if (!WorkspaceBrowserProvider.isValidMatlabIdentifier(newName)) { + const message = `'${newName}' is not a valid MATLAB variable name. ` + + 'Names must begin with a letter, contain only letters/digits/underscores, and not exceed 2048 characters.' + this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) + this.showMatlabError('rename', message) + return + } + + // Client-side duplicate check against cached workspace state + if (this.cachedRows?.some((row: WorkspaceVariable) => row.name === newName) === true) { + const message = `A variable named "${newName}" already exists` + this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) + this.showMatlabError('rename', message) + return + } + + try { + await this.mvm.getReadyPromise() + } catch { + this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message: 'MATLAB is not ready' }) + return + } + + try { + const response = await this.evalInWorkspace(`${newName} = ${oldName}; clear('${oldName}');`) + if ('error' in response) { + const message = this.extractErrorMessage(response.error) + this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) + this.showMatlabError('rename', message) + } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) + this.showMatlabError('rename', message) + } + } + + // Delete a variable from the active workspace after user confirms via dialog + private async handleDeleteVariable (variable: string): Promise { + const confirmation = await vscode.window.showWarningMessage( + `Delete variable "${variable}" from the workspace?`, + { modal: true }, + 'Delete' + ) + if (confirmation !== 'Delete') return + + try { + await this.mvm.getReadyPromise() + } catch { + this.postToWebview({ type: 'operationError', operation: 'delete', variable, message: 'MATLAB is not ready' }) + return + } + + try { + const response = await this.evalInWorkspace(`clear('${variable}');`) + if ('error' in response) { + const message = this.extractErrorMessage(response.error) + this.postToWebview({ type: 'operationError', operation: 'delete', variable, message }) + this.showMatlabError('delete', message) + } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + this.postToWebview({ type: 'operationError', operation: 'delete', variable, message }) + this.showMatlabError('delete', message) + } + } + + // Persist UI state (column widths, sort preferences) to workspace storage. + // Re-sends sorted/capped data when sort preferences change. + private handleStateChanged (state: SavedState): void { + const sortChanged = state.sortColumn !== this.savedState?.sortColumn || + state.sortDirection !== this.savedState?.sortDirection + this.savedState = state + void this.extensionContext.workspaceState.update(WSB_STATE_KEY, state) + + if (sortChanged) { + this.sendDataToWebview() + } + } + + // Executes a MATLAB command in the workspace the browser is currently displaying. + // During debug, uses feval('eval', ...) to execute in the paused frame's workspace + // while still receiving error responses through the feval channel. + // Outside debug, uses evalin('base', ...) to target the base workspace. + private async evalInWorkspace (command: string): Promise<{ error: unknown } | { result: unknown[] }> { + if (this.mvm.isDebugging()) { + return await this.mvm.feval('eval', 0, [command], false) + } + return await this.mvm.feval('evalin', 0, ['base', command], false) + } + + // ── Data Request Throttling ────────────────────────────────────── + + // Coalesces rapid DataChanged events to avoid flooding the server with GetData requests + private scheduleDataRequest (): void { + // Empty workspace: bypass throttle and clear immediately + if (this.numRows === 0) { + this.cancelPendingDataRequest() + this.cachedRows = [] + this.postToWebview({ type: 'setData', rows: [] }) + this.postToWebview({ type: 'setTruncationInfo', displayedCount: 0, totalCount: 0 }) + return + } + + if (this.dataRequestPending) return + this.dataRequestPending = true + this.dataRequestTimer = setTimeout(() => { + this.dataRequestPending = false + this.dataRequestTimer = undefined + this.requestData() + }, DATA_THROTTLE_MS) + } + + private cancelPendingDataRequest (): void { + if (this.dataRequestTimer != null) { + clearTimeout(this.dataRequestTimer) + this.dataRequestTimer = undefined + } + this.dataRequestPending = false + } + + private requestData (): void { + this.notifier.sendNotification(Notification.WSBClientMessage, { + type: 'GetData', + startRow: 1, + endRow: this.numRows + 1 + }) + } + + // Sorts the full cached dataset, caps to maxVariables, and sends to the webview + private sendDataToWebview (): void { + if (this.cachedRows == null) return + + const sorted = this.sortRows(this.cachedRows) + const maxVariables = this.getMaxVariableCount() + const capped = sorted.length > maxVariables ? sorted.slice(0, maxVariables) : sorted + + this.postToWebview({ type: 'setData', rows: capped }) + this.postToWebview({ type: 'setTruncationInfo', displayedCount: capped.length, totalCount: this.numRows }) + } + + private sortRows (data: WorkspaceVariable[]): WorkspaceVariable[] { + const column = this.savedState?.sortColumn ?? '' + const direction = this.savedState?.sortDirection ?? 'asc' + if (column === '' || data.length === 0) return data + + const numeric = this.getSortMethod() === 'natural' + const sorted = [...data] + sorted.sort((a: WorkspaceVariable, b: WorkspaceVariable): number => { + const aVal = a.fields[column] ?? '' + const bVal = b.fields[column] ?? '' + const cmp = aVal.localeCompare(bVal, undefined, { numeric, sensitivity: 'base' }) + return direction === 'asc' ? cmp : -cmp + }) + return sorted + } + + // ── Max Variables Warning ──────────────────────────────────────── + + private getMaxVariableCount (): number { + return vscode.workspace.getConfiguration('MATLAB') + .get('maximumWorkspaceVariables', WSB_DEFAULT_MAX_VARIABLES) + } + + private getSortMethod (): 'natural' | 'lexicographic' { + return vscode.workspace.getConfiguration('MATLAB') + .get<'natural' | 'lexicographic'>('workspaceSortMethod', 'natural') + } + + // Show an info message when the workspace exceeds the configured variable limit + private checkMaxVariablesWarning (): void { + const max = this.getMaxVariableCount() + if (this.numRows > max && !this.maxVarsMessageShown) { + this.maxVarsMessageShown = true + const message = MAX_VARIABLES_MESSAGE_TEMPLATE + .replace('{currentCount}', this.numRows.toString()) + .replace('{maxVariables}', max.toString()) + + void vscode.window.showInformationMessage(message, MAX_VARS_BUTTON_TEXT) + .then((selection: string | undefined) => { + if (selection === MAX_VARS_BUTTON_TEXT) { + void vscode.commands.executeCommand('workbench.action.openSettings', MAX_VARS_SETTING_ID) + } + }) + } + } + + // ── Webview HTML & Messaging ───────────────────────────────────── + + private postToWebview (message: ExtToWebview): void { + if (this.view != null) { + void this.view.webview.postMessage(message) + } + } + + // MATLAB feval errors arrive as { id, msg, status } objects. + // Extracts the human-readable msg field for display in toast notifications. + private extractErrorMessage (error: unknown): string { + if (typeof error === 'string') return error + if (error != null && typeof error === 'object' && 'msg' in error) { + return String((error as { msg: unknown }).msg) + } + return String(error) + } + + // Shows a persistent toast notification so the user knows why an operation failed. + // The operation prefix provides context since toasts appear globally in VS Code. + private showMatlabError (operation: string, message: string): void { + const prefix = operation === 'editValue' + ? 'Edit failed' + : operation === 'rename' + ? 'Rename failed' + : 'Delete failed' + void vscode.window.showErrorMessage(`${prefix}: ${message}`) + } +} diff --git a/src/workspacebrowser/icons.ts b/src/workspacebrowser/icons.ts new file mode 100644 index 0000000..911b6c0 --- /dev/null +++ b/src/workspacebrowser/icons.ts @@ -0,0 +1,46 @@ +// Copyright 2026 The MathWorks, Inc. + +// Maps MATLAB variable class names to their SVG icon filenames. +// Theme (dark/light) is determined at render time in the webview, not here. +const ICON_MAP: Record = { + object: 'ws3d.svg', + cell: 'wsBrackets.svg', + calendarduration: 'wsCalendar.svg', + char: 'wsCharacter.svg', + logical: 'wsCheck.svg', + duration: 'wsClock.svg', + datetime: 'wsDate.svg', + default: 'wsDefault.svg', + categorical: 'wsDots.svg', + ordinal: 'wsDots.svg', + nominal: 'wsDots.svg', + sparse: 'wsSparse.svg', + string: 'wsString.svg', + table: 'wsTable.svg', + dataset: 'wsTable.svg', + timetable: 'wsTableTime.svg', + eventtable: 'wsTableTime.svg', + tall: 'wsTall.svg', + timeseries: 'wsTime.svg', + struct: 'wsTree.svg' +} + +// All MATLAB numeric primitive types share the default numeric icon +const NUMERIC_TYPES: Set = new Set([ + 'double', 'single', + 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64' +]) + +// Returns the SVG filename for a given MATLAB class name. +// Fallback order: direct match -> numeric type -> sparse-containing -> tall-containing -> object default +export function getIconFilename (matlabClass: string): string { + const direct = ICON_MAP[matlabClass] + if (direct != null) return direct + + if (NUMERIC_TYPES.has(matlabClass)) return 'wsDefault.svg' + if (matlabClass.includes('sparse')) return 'wsSparse.svg' + if (matlabClass.includes('tall')) return 'wsTall.svg' + + // Unknown types fall back to the generic object icon + return 'ws3d.svg' +} diff --git a/src/workspacebrowser/resources/icons/dark/ws3d.svg b/src/workspacebrowser/resources/icons/dark/ws3d.svg new file mode 100644 index 0000000..789c47e --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/ws3d.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsBrackets.svg b/src/workspacebrowser/resources/icons/dark/wsBrackets.svg new file mode 100644 index 0000000..b95e4cb --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsBrackets.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsCalendar.svg b/src/workspacebrowser/resources/icons/dark/wsCalendar.svg new file mode 100644 index 0000000..63a501f --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsCalendar.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsCharacter.svg b/src/workspacebrowser/resources/icons/dark/wsCharacter.svg new file mode 100644 index 0000000..8dbdfaf --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsCharacter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsCheck.svg b/src/workspacebrowser/resources/icons/dark/wsCheck.svg new file mode 100644 index 0000000..4341913 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsCheck.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsClock.svg b/src/workspacebrowser/resources/icons/dark/wsClock.svg new file mode 100644 index 0000000..9755a8b --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsClock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsDate.svg b/src/workspacebrowser/resources/icons/dark/wsDate.svg new file mode 100644 index 0000000..ee7c4f8 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsDate.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsDefault.svg b/src/workspacebrowser/resources/icons/dark/wsDefault.svg new file mode 100644 index 0000000..2bbc3a4 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsDefault.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsDots.svg b/src/workspacebrowser/resources/icons/dark/wsDots.svg new file mode 100644 index 0000000..0636f41 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsDots.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsSparse.svg b/src/workspacebrowser/resources/icons/dark/wsSparse.svg new file mode 100644 index 0000000..f46b143 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsSparse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsString.svg b/src/workspacebrowser/resources/icons/dark/wsString.svg new file mode 100644 index 0000000..0723378 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsString.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsTable.svg b/src/workspacebrowser/resources/icons/dark/wsTable.svg new file mode 100644 index 0000000..9877aa0 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsTable.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsTableTime.svg b/src/workspacebrowser/resources/icons/dark/wsTableTime.svg new file mode 100644 index 0000000..715eb2f --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsTableTime.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsTall.svg b/src/workspacebrowser/resources/icons/dark/wsTall.svg new file mode 100644 index 0000000..dc5bd98 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsTall.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsTime.svg b/src/workspacebrowser/resources/icons/dark/wsTime.svg new file mode 100644 index 0000000..3a2497a --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsTime.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/workspacebrowser/resources/icons/dark/wsTree.svg b/src/workspacebrowser/resources/icons/dark/wsTree.svg new file mode 100644 index 0000000..20706c2 --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsTree.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/light/ws3d.svg b/src/workspacebrowser/resources/icons/light/ws3d.svg new file mode 100644 index 0000000..baf6a82 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/ws3d.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsBrackets.svg b/src/workspacebrowser/resources/icons/light/wsBrackets.svg new file mode 100644 index 0000000..ef4fad0 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsBrackets.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsCalendar.svg b/src/workspacebrowser/resources/icons/light/wsCalendar.svg new file mode 100644 index 0000000..9d3d0b8 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsCalendar.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsCharacter.svg b/src/workspacebrowser/resources/icons/light/wsCharacter.svg new file mode 100644 index 0000000..8d10092 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsCharacter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsCheck.svg b/src/workspacebrowser/resources/icons/light/wsCheck.svg new file mode 100644 index 0000000..5bd2aaa --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsCheck.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsClock.svg b/src/workspacebrowser/resources/icons/light/wsClock.svg new file mode 100644 index 0000000..98bc3f0 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsClock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsDate.svg b/src/workspacebrowser/resources/icons/light/wsDate.svg new file mode 100644 index 0000000..0f4c084 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsDate.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsDefault.svg b/src/workspacebrowser/resources/icons/light/wsDefault.svg new file mode 100644 index 0000000..aa72115 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsDefault.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsDots.svg b/src/workspacebrowser/resources/icons/light/wsDots.svg new file mode 100644 index 0000000..6810412 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsDots.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsSparse.svg b/src/workspacebrowser/resources/icons/light/wsSparse.svg new file mode 100644 index 0000000..0a8be12 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsSparse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsString.svg b/src/workspacebrowser/resources/icons/light/wsString.svg new file mode 100644 index 0000000..d5d9259 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsString.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsTable.svg b/src/workspacebrowser/resources/icons/light/wsTable.svg new file mode 100644 index 0000000..bb3c43c --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsTable.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsTableTime.svg b/src/workspacebrowser/resources/icons/light/wsTableTime.svg new file mode 100644 index 0000000..5fd43f3 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsTableTime.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsTall.svg b/src/workspacebrowser/resources/icons/light/wsTall.svg new file mode 100644 index 0000000..242116c --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsTall.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsTime.svg b/src/workspacebrowser/resources/icons/light/wsTime.svg new file mode 100644 index 0000000..74f5e75 --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsTime.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsTree.svg b/src/workspacebrowser/resources/icons/light/wsTree.svg new file mode 100644 index 0000000..0f3b1de --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsTree.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/workspacebrowser/resources/webview.css b/src/workspacebrowser/resources/webview.css new file mode 100644 index 0000000..e9b53fa --- /dev/null +++ b/src/workspacebrowser/resources/webview.css @@ -0,0 +1,197 @@ +/* Copyright 2026 The MathWorks, Inc. */ + +/* All colors reference VS Code CSS custom properties for theme integration */ + +/* ── Layout ─────────────────────────────────────────────────────── */ + +.wsb-container { + height: 100vh; + display: flex; + flex-direction: column; +} + +/* Scrollable region for both vertical and horizontal overflow */ +.wsb-table-wrap { + flex: 1; + overflow: auto; +} + +/* Fills container width via width:100%. + table-layout:fixed enforces hard min/max-width constraints on cells. + Grows beyond container when resized columns' explicit px widths exceed 100%. */ +.wsb-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +/* ── Header ─────────────────────────────────────────────────────── */ + +/* Sticky positioning keeps headers visible during vertical scroll */ +.wsb-table th { + position: sticky; + top: -1px; + z-index: 1; + background: var(--vscode-sideBarSectionHeader-background); + text-align: left; + padding: 2px; + white-space: nowrap; + cursor: default; + min-width: 55px; + max-width: 200px; + border: 1px solid var(--vscode-editorGroup-border); +} + +/* Name column needs more room for the type icon + variable name */ +th[data-col="Name"] { + min-width: 65px; + max-width: 150px; +} + +/* Remove default max-width when user has manually resized a column */ +.wsb-manually-resized { + max-width: none; +} + +/* ── Sort Indicator ─────────────────────────────────────────────── */ + +/* Hidden by default; visible on header hover or when the column is actively sorted */ +.wsb-sort-icon { + display: none; + margin-left: 5px; +} + +th:hover .wsb-sort-icon, +th.wsb-sorted .wsb-sort-icon { + display: inline; +} + +/* ── Column Resizer ─────────────────────────────────────────────── */ + +/* Drag handle at the right edge of each header cell. + 8px hit area makes targeting easier under the col-resize cursor. */ +.wsb-resizer { + position: absolute; + right: 0; + top: 0; + width: 8px; + height: 100%; + cursor: col-resize; + user-select: none; + z-index: 2; +} + +/* ── Data Cells ─────────────────────────────────────────────────── */ + +/* Data cells inherit their width from the header row via table-layout:fixed. + Inputs handle their own text clipping via overflow:hidden + text-overflow:ellipsis. */ +.wsb-table td { + padding: 2px; + border: 1px solid var(--vscode-editorGroup-border); +} + + +/* ── Cell Inputs ───────────────────────────────────────────────── */ + +/* Shared base for all cell inputs: borderless, inherits styling, truncates with ellipsis. + min-width:0 overrides the browser's intrinsic input minimum so table-layout:fixed controls sizing. + box-sizing:border-box ensures width:100% includes Chromium's default input padding. */ +.wsb-text-input, +.wsb-value-input, +.wsb-name-input { + background: inherit; + border: none; + color: inherit; + width: 100%; + min-width: 0; + box-sizing: border-box; + font-family: inherit; + font-size: inherit; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Readonly inputs show default cursor instead of text I-beam */ +.wsb-text-input, +.wsb-value-input[readonly], +.wsb-name-input[readonly] { + cursor: default; +} + +/* ── Name Column Icon + Input ──────────────────────────────────── */ + +/* Flex row for type icon + name input; min-width:0 allows truncation inside flex */ +.wsb-icon-label { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; + width: 100%; +} + +/* Fixed-size type icon; flex-shrink:0 prevents it from collapsing in narrow columns */ +.wsb-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* Name input overrides for flex layout inside .wsb-icon-label */ +.wsb-name-input { + padding: 0; + min-width: 0; + flex: 1; + white-space: nowrap; +} + +/* ── Row States ─────────────────────────────────────────────────── */ + +/* Outline instead of border avoids layout shift on selection */ +.wsb-table tbody tr { + outline: 1px solid transparent; + outline-offset: -1px; +} + +.wsb-table tbody tr:nth-child(even) { + background: var(--vscode-keybindingTable-rowsBackground); +} + +.wsb-table tbody tr:hover { + background: var(--vscode-list-hoverBackground); +} + +.wsb-table tbody tr.wsb-selected { + background: var(--vscode-list-activeSelectionBackground); + outline-color: var(--vscode-list-focusOutline); +} + +/* ── Error Flash ────────────────────────────────────────────────── */ + +/* Transient red border on value/name inputs when an operation fails (2-second timeout). + !important is needed to override the input's border: none. */ +.wsb-value-error { + border: 1px solid var(--vscode-inputValidation-errorBorder) !important; +} + +/* ── Status Bar ────────────────────────────────────────────────── */ + +/* Fixed-height bar showing truncation info; truncates its own text if the panel is narrow */ +.wsb-status-bar { + flex-shrink: 0; + padding: 4px 8px; + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.wsb-status-link { + color: var(--vscode-textLink-foreground); + text-decoration: none; + cursor: pointer; +} + +.wsb-status-link:hover { + text-decoration: underline; +} diff --git a/src/workspacebrowser/templates.ts b/src/workspacebrowser/templates.ts new file mode 100644 index 0000000..515eac1 --- /dev/null +++ b/src/workspacebrowser/templates.ts @@ -0,0 +1,82 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as vscode from 'vscode' + +export const WSB_MINIMUM_RELEASE = 'R2023a' + +export function getUnsupportedHtml (): string { + return ` + + +Workspace + + +
+
⚠️
+

MATLAB Version Not Supported

+

The Workspace panel requires MATLAB ${WSB_MINIMUM_RELEASE} or later.

+

Please upgrade your MATLAB installation to use this feature.

+
+` +} + +export function getDisconnectedHtml (): string { + return ` + + +Workspace + + +
+
⚠️
+

MATLAB Disconnected

+

The workspace browser is not available because MATLAB is not connected.

+

Connect to MATLAB to view workspace variables.

+
+` +} + +// Generates the full interactive HTML for the webview when MATLAB is connected and supported +export function getWebviewHtml (webview: vscode.Webview, extensionUri: vscode.Uri): string { + const stylesUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'out', 'workspacebrowser', 'resources', 'webview.css') + ) + const bundleUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'out', 'bundle.js') + ) + const iconsBaseUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, 'out', 'workspacebrowser', 'resources') + ) + + return ` + + + + + Workspace + + + +
+
+ + + +
+
+ +
+ + +` +} diff --git a/src/workspacebrowser/types.ts b/src/workspacebrowser/types.ts new file mode 100644 index 0000000..ba6a0ea --- /dev/null +++ b/src/workspacebrowser/types.ts @@ -0,0 +1,59 @@ +// Copyright 2026 The MathWorks, Inc. + +/** + * Represents a single MATLAB workspace variable with its display properties. + * The `name` field is always present for row identity (selection, patching, context menus). + * All column values live in `fields`, keyed by original-case column name. + */ +export interface WorkspaceVariable { + name: string + fields: Record +} + +/** + * Defines the display and behavior properties for a single table column. + * Received from the MATLAB language server via the Columns message. + */ +export interface WorkspaceColumn { + name: string + label: string + sortable: boolean + resizable: boolean +} + +/** + * Persisted UI state that survives webview disposal and MATLAB reconnection. + * Stored in vscode.ExtensionContext.workspaceState under the key 'wsb-state'. + */ +export interface SavedState { + columnWidths?: Record + sortColumn?: string + sortDirection?: 'asc' | 'desc' +} + +/** + * Messages sent from the extension host to the webview. + * Discriminated union on the 'type' field for exhaustive switch handling. + */ +export type ExtToWebview = + | { type: 'setData', rows: WorkspaceVariable[] } + | { type: 'setColumns', columns: WorkspaceColumn[] } + | { type: 'setState', state: SavedState } + | { type: 'setTruncationInfo', displayedCount: number, totalCount: number } + | { type: 'themeChanged' } + | { type: 'operationError', operation: string, variable: string, message: string } + | { type: 'sortFromContextMenu', column: string, direction: 'asc' | 'desc' } + | { type: 'focusValueInput', variable: string } + | { type: 'focusNameInput', variable: string } + +/** + * Messages sent from the webview to the extension host. + * Discriminated union on the 'type' field for exhaustive switch handling. + */ +export type WebviewToExt = + | { type: 'ready' } + | { type: 'editValue', variable: string, newValue: string } + | { type: 'renameVariable', variable: string, newName: string } + | { type: 'deleteVariable', variable: string } + | { type: 'stateChanged', state: SavedState } + | { type: 'openMaxVariablesSetting' } diff --git a/src/workspacebrowser/webview-main.ts b/src/workspacebrowser/webview-main.ts new file mode 100644 index 0000000..41adef5 --- /dev/null +++ b/src/workspacebrowser/webview-main.ts @@ -0,0 +1,11 @@ +// Copyright 2026 The MathWorks, Inc. + +// Thin webpack entry point that separates the production bootstrap from the testable module. +// Tests import webview.ts directly and inject a mock API via init(). + +import { init } from './webview' +import { WebviewToExt } from './types' + +declare function acquireVsCodeApi (): { postMessage: (msg: WebviewToExt) => void } + +init(acquireVsCodeApi()) diff --git a/src/workspacebrowser/webview.ts b/src/workspacebrowser/webview.ts new file mode 100644 index 0000000..07d171b --- /dev/null +++ b/src/workspacebrowser/webview.ts @@ -0,0 +1,828 @@ +// Copyright 2026 The MathWorks, Inc. + +// All webview UI logic in one module with exported functions for testability. +// No classes or custom elements — plain functions that operate on the DOM. + +import { WorkspaceVariable, WorkspaceColumn, SavedState, ExtToWebview, WebviewToExt } from './types' +import { getIconFilename } from './icons' + +// VsCodeApi is injected via init() — tests pass a mock, production uses acquireVsCodeApi() +interface VsCodeApi { postMessage: (msg: WebviewToExt) => void } + +// Reads the icons base URI from the data attribute set by the provider on the body element +function getIconsBaseUri (): string { + return document.body.dataset.iconsBaseUri ?? '' +} + +// ── Module-Level State ─────────────────────────────────────────── + +let vscodeApi: VsCodeApi +let columns: WorkspaceColumn[] = [] +let rows: WorkspaceVariable[] = [] +let sortColumn: string = '' +let sortDirection: 'asc' | 'desc' = 'asc' +let columnWidths: Record = {} +let selectedVarName: string | null = null +let resizing: boolean = false + +// ── Keyboard Bindings ─────────────────────────────────────────── + +type KeyAction = () => void +type KeyGuard = () => boolean + +interface KeyBinding { + key: string + action: KeyAction + guard?: KeyGuard +} + +function noInputEditing (): boolean { + const active = document.activeElement as HTMLInputElement | null + return !(active != null && active.tagName === 'INPUT' && !active.readOnly) +} + +function deleteSelectedVariable (): void { + if (selectedVarName == null) return + vscodeApi.postMessage({ type: 'deleteVariable', variable: selectedVarName }) +} + +function getFocusedCellInfo (): { tr: HTMLTableRowElement, colName: string } | null { + const active = document.activeElement as HTMLElement | null + if (active == null) return null + const td = active.closest('td[data-col]') + const tr = active.closest('tr[data-var]') + if (td == null || tr == null) return null + return { tr, colName: td.getAttribute('data-col')! } +} + +function focusCell (tr: Element, colName: string): void { + const td = tr.querySelector(`td[data-col="${CSS.escape(colName)}"]`) + const input = td?.querySelector('input') as HTMLInputElement | null + if (input != null) { + selectRow(tr.getAttribute('data-var')!) + input.focus() + tr.scrollIntoView?.({ block: 'nearest' }) + } +} + +function moveDown (): void { + const info = getFocusedCellInfo() + if (info != null) { + const next = info.tr.nextElementSibling + if (next != null) focusCell(next, info.colName) + } else { + const first = document.querySelector('.wsb-table tbody tr') + if (first != null) focusCell(first, columns[0]?.name ?? 'Name') + } +} + +function moveUp (): void { + const info = getFocusedCellInfo() + if (info != null) { + const prev = info.tr.previousElementSibling + if (prev != null) focusCell(prev, info.colName) + } else { + const first = document.querySelector('.wsb-table tbody tr') + if (first != null) focusCell(first, columns[0]?.name ?? 'Name') + } +} + +function moveRight (): void { + const info = getFocusedCellInfo() + if (info == null) return + const td = info.tr.querySelector(`td[data-col="${CSS.escape(info.colName)}"]`) + const nextTd = td?.nextElementSibling as HTMLTableCellElement | null + const nextCol = nextTd?.getAttribute('data-col') + if (nextCol != null) focusCell(info.tr, nextCol) +} + +function moveLeft (): void { + const info = getFocusedCellInfo() + if (info == null) return + const td = info.tr.querySelector(`td[data-col="${CSS.escape(info.colName)}"]`) + const prevTd = td?.previousElementSibling as HTMLTableCellElement | null + const prevCol = prevTd?.getAttribute('data-col') + if (prevCol != null) focusCell(info.tr, prevCol) +} + +function editFocusedCell (): void { + const active = document.activeElement as HTMLInputElement | null + if (active == null) return + if (active.classList.contains('wsb-name-input') || active.classList.contains('wsb-value-input')) { + if (active.readOnly) enterEditMode(active) + } +} + +function renameSelectedVariable (): void { + if (selectedVarName == null) return + const input = findNameInput(selectedVarName) + if (input != null) enterEditMode(input) +} + +const KEY_BINDINGS: KeyBinding[] = [ + { key: 'Delete', action: deleteSelectedVariable, guard: noInputEditing }, + { key: 'ArrowDown', action: moveDown, guard: noInputEditing }, + { key: 'ArrowUp', action: moveUp, guard: noInputEditing }, + { key: 'ArrowLeft', action: moveLeft, guard: noInputEditing }, + { key: 'ArrowRight', action: moveRight, guard: noInputEditing }, + { key: 'Enter', action: editFocusedCell, guard: noInputEditing }, + { key: 'F2', action: renameSelectedVariable, guard: noInputEditing } +] + +// ── Exported Functions ─────────────────────────────────────────── + +// Called once by webview-main.ts to bind event listeners and signal readiness +export function init (api: VsCodeApi): void { + vscodeApi = api + + // Bind event delegation on the pre-existing table element created by the provider HTML + const table = document.querySelector('.wsb-table') as HTMLTableElement + if (table != null) { + table.addEventListener('click', handleTableClick) + table.addEventListener('focusin', handleFocusIn) + table.addEventListener('focusout', handleFocusOut) + } + + // Handle keyboard shortcuts via the dispatch table + document.addEventListener('keydown', handleDocumentKeydown) + + // Listen for messages from the extension host + window.addEventListener('message', (e: MessageEvent) => { + handleMessage(e.data as ExtToWebview) + }) + + // Signal readiness so the provider sends cached data + vscodeApi.postMessage({ type: 'ready' }) +} + +// Dispatches inbound messages from the extension host +export function handleMessage (msg: ExtToWebview): void { + switch (msg.type) { + case 'setData': onSetData(msg.rows); break + case 'setColumns': onSetColumns(msg.columns); break + case 'setState': onSetState(msg.state); break + case 'setTruncationInfo': onSetTruncationInfo(msg.displayedCount, msg.totalCount); break + case 'themeChanged': onThemeChanged(); break + case 'operationError': onOperationError(msg); break + case 'sortFromContextMenu': onSortFromContextMenu(msg.column, msg.direction); break + case 'focusValueInput': onFocusValueInput(msg.variable); break + case 'focusNameInput': onFocusNameInput(msg.variable); break + } +} + +// Full table rebuild from current module-level state +export function renderTable (): void { + const table = document.querySelector('.wsb-table') as HTMLTableElement + if (table == null) return + + const thead = table.querySelector('thead') as HTMLTableSectionElement + const tbody = table.querySelector('tbody') as HTMLTableSectionElement + + // Capture scroll position before clearing so it can be restored after rebuild + const tableWrap = table.closest('.wsb-table-wrap') + const scrollTop = tableWrap?.scrollTop ?? 0 + + thead.innerHTML = '' + tbody.innerHTML = '' + + if (columns.length === 0) return + + // ── Build Header Row ───────────────────────────────────────── + const headerRow = document.createElement('tr') + for (const col of columns) { + headerRow.appendChild(createHeaderCell(col)) + } + thead.appendChild(headerRow) + + // ── Build Data Rows ────────────────────────────────────────── + for (const row of rows) { + const tr = createDataRow(row) + tbody.appendChild(tr) + } + + // Restore scroll position after rebuild (clamped to the new scroll range) + if (tableWrap != null) { + tableWrap.scrollTop = Math.min(scrollTop, tableWrap.scrollHeight - tableWrap.clientHeight) + } +} + +// Clears all module-level state — called in test teardown to prevent leakage +export function resetState (): void { + columns = [] + rows = [] + sortColumn = '' + sortDirection = 'asc' + columnWidths = {} + selectedVarName = null + resizing = false +} + +// Returns a snapshot of current state for test assertions +export function getState (): { + columns: WorkspaceColumn[] + rows: WorkspaceVariable[] + sortColumn: string + sortDirection: 'asc' | 'desc' + columnWidths: Record + selectedVarName: string | null +} { + return { columns, rows, sortColumn, sortDirection, columnWidths, selectedVarName } +} + +// ── Message Handlers ───────────────────────────────────────────── + +function onSetData (newRows: WorkspaceVariable[]): void { + const oldRows = rows + rows = newRows + + if (canPatchInPlace(oldRows, newRows)) { + patchRows(newRows) + } else { + renderTable() + } +} + +function onSetColumns (newColumns: WorkspaceColumn[]): void { + columns = newColumns + renderTable() +} + +// Merges incoming persisted state into module state and re-renders +function onSetState (state: SavedState): void { + if (state.columnWidths != null) columnWidths = state.columnWidths + if (state.sortColumn != null) sortColumn = state.sortColumn + if (state.sortDirection != null) sortDirection = state.sortDirection + renderTable() +} + +// Shows or hides the truncation status bar below the table +function onSetTruncationInfo (displayedCount: number, totalCount: number): void { + const statusBar = document.querySelector('.wsb-status-bar') as HTMLElement + if (statusBar == null) return + + if (displayedCount < totalCount) { + statusBar.innerHTML = '' + const text = document.createElement('span') + text.textContent = `Showing ${displayedCount} of ${totalCount} variables. ` + const link = document.createElement('a') + link.textContent = 'Change limit' + link.href = '#' + link.className = 'wsb-status-link' + link.addEventListener('click', (e: MouseEvent) => { + e.preventDefault() + vscodeApi.postMessage({ type: 'openMaxVariablesSetting' }) + }) + statusBar.appendChild(text) + statusBar.appendChild(link) + statusBar.style.display = 'block' + } else { + statusBar.style.display = 'none' + } +} + +// Handles errors from failed extension-side operations +function onOperationError (msg: { operation: string, variable: string, message: string }): void { + if (msg.operation === 'editValue') { + revertWithErrorFlash(findValueInput(msg.variable)) + } else if (msg.operation === 'rename') { + revertRenameWithErrorFlash(msg.variable) + } +} + +// Reverts a failed rename: finds the input by its stashed original name, +// undoes the optimistic data-var update, and shows the error flash. +function revertRenameWithErrorFlash (originalName: string): void { + const input = findNameInput(originalName) ?? + document.querySelector(`.wsb-name-input[data-original-value="${CSS.escape(originalName)}"]`) + if (input == null) return + const tr = input.closest('tr') + if (tr != null) { + tr.dataset.var = originalName + const context = JSON.parse(tr.getAttribute('data-vscode-context') ?? '{}') + context.varName = originalName + tr.setAttribute('data-vscode-context', JSON.stringify(context)) + } + revertWithErrorFlash(input) +} + +// Reverts an input to its stashed original value and shows a transient red border +function revertWithErrorFlash (input: HTMLInputElement | null): void { + if (input == null) return + input.value = input.dataset.originalValue ?? '' + input.classList.add('wsb-value-error') + setTimeout(() => input.classList.remove('wsb-value-error'), 2000) +} + +// Updates icon paths when the VS Code color theme changes +function onThemeChanged (): void { + const isDark = document.body.classList.contains('vscode-dark') + const icons = document.querySelectorAll('.wsb-icon') + icons.forEach((img) => { + const iconFile = getIconFilename(img.alt) + img.src = `${getIconsBaseUri()}/icons/${isDark ? 'dark' : 'light'}/${iconFile}` + }) +} + +// Applies a sort triggered from the VS Code native context menu +function onSortFromContextMenu (column: string, direction: 'asc' | 'desc'): void { + sortColumn = column + sortDirection = direction + updateSortIndicators() + notifyStateChanged() +} + +// Transitions an input from readonly display to active edit mode. +// Stashes the current value for revert-on-Escape and selects all text. +function enterEditMode (input: HTMLInputElement): void { + input.readOnly = false + input.dataset.originalValue = input.value + input.focus() + input.select() +} + +// Focuses the Value input for a variable, entering edit mode +function onFocusValueInput (variable: string): void { + const input = findValueInput(variable) + if (input != null) { + enterEditMode(input) + } +} + +// Focuses the Name input for a variable, entering edit mode +function onFocusNameInput (variable: string): void { + const input = findNameInput(variable) + if (input != null) { + enterEditMode(input) + } +} + +// ── Header Sort Indicator Update ───────────────────────────────── + +function updateSortIndicators (): void { + const headers = document.querySelectorAll('.wsb-table thead th') + for (const th of headers) { + const colName = th.getAttribute('data-col') + const icon = th.querySelector('.wsb-sort-icon') + if (icon == null) continue + + if (colName === sortColumn) { + th.classList.add('wsb-sorted') + icon.textContent = sortDirection === 'asc' ? ' ▲' : ' ▼' + } else { + th.classList.remove('wsb-sorted') + icon.textContent = ' ▲' + } + } +} + +// ── Header Cell Creation ───────────────────────────────────────── + +// Builds a single header cell: label, sort indicator, resizer handle, and persisted width +function createHeaderCell (col: WorkspaceColumn): HTMLTableCellElement { + const th = document.createElement('th') + th.setAttribute('data-col', col.name) + + // VS Code reads this attribute for context menu when clauses + th.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: col.sortable ? 'header' : 'header-nosort', + columnName: col.name, + preventDefaultContextMenuItems: true + })) + + const labelSpan = document.createElement('span') + labelSpan.textContent = col.label + th.appendChild(labelSpan) + + if (col.sortable) { + const sortIcon = document.createElement('span') + sortIcon.className = 'wsb-sort-icon' + if (sortColumn === col.name) { + th.classList.add('wsb-sorted') + sortIcon.textContent = sortDirection === 'asc' ? ' ▲' : ' ▼' + } else { + sortIcon.textContent = ' ▲' + } + th.appendChild(sortIcon) + } + + if (col.resizable) { + const resizerDiv = document.createElement('div') + resizerDiv.className = 'wsb-resizer' + resizerDiv.addEventListener('mousedown', (e: MouseEvent) => { + startColumnResize(e, th, col.name) + }) + th.appendChild(resizerDiv) + } + + // Restore persisted width with triple-constraint to prevent table-layout rebalancing + if (columnWidths[col.name] != null) { + const w = `${columnWidths[col.name]}px` + th.style.width = w + th.style.minWidth = w + th.style.maxWidth = w + th.classList.add('wsb-manually-resized') + } + + return th +} + +// ── Data Row Creation ──────────────────────────────────────────── + +function createDataRow (row: WorkspaceVariable): HTMLTableRowElement { + const tr = document.createElement('tr') + tr.setAttribute('data-var', row.name) + + // VS Code reads this attribute for context menu when clauses + tr.setAttribute('data-vscode-context', JSON.stringify({ + webviewSection: 'row', + varName: row.name, + preventDefaultContextMenuItems: true + })) + + if (row.name === selectedVarName) { + tr.classList.add('wsb-selected') + } + + for (const col of columns) { + const td = document.createElement('td') + td.setAttribute('data-col', col.name) + + if (col.name === 'Name') { + createNameCell(td, row) + } else if (col.name === 'Value') { + createValueCell(td, row) + } else { + createTextCell(td, row, col.name) + } + + // Restore persisted width with triple-constraint to match header cells + if (columnWidths[col.name] != null) { + const w = `${columnWidths[col.name]}px` + td.style.width = w + td.style.minWidth = w + td.style.maxWidth = w + } + + tr.appendChild(td) + } + + return tr +} + +// Name column: type icon + editable input (readonly until double-click or context menu) +function createNameCell (td: HTMLTableCellElement, row: WorkspaceVariable): void { + const iconLabel = document.createElement('div') + iconLabel.className = 'wsb-icon-label' + + const img = document.createElement('img') + img.className = 'wsb-icon' + const iconFile = getIconFilename(row.fields.Class ?? '') + const isDark = document.body.classList.contains('vscode-dark') + img.src = `${getIconsBaseUri()}/icons/${isDark ? 'dark' : 'light'}/${iconFile}` + img.alt = row.fields.Class ?? '' + + const input = document.createElement('input') + input.type = 'text' + input.size = 1 + input.className = 'wsb-name-input' + input.value = row.name + input.readOnly = true + input.dataset.varName = row.name + input.dataset.originalValue = row.name + + // Double-click enters edit mode + input.addEventListener('dblclick', () => { + enterEditMode(input) + }) + + // Commit rename on blur if the name changed, then return to readonly + input.addEventListener('blur', () => { + const newName = input.value.trim() + const originalName = input.dataset.originalValue ?? '' + input.classList.remove('wsb-value-error') + input.readOnly = true + + // Reject empty names — revert without a server round-trip + if (newName === '') { + input.value = originalName + return + } + + if (newName !== originalName) { + // Optimistically update the row's data attributes so context menu + // actions target the new name before the server sends refreshed data + const tr = input.closest('tr') + if (tr != null) { + tr.dataset.var = newName + const context = JSON.parse(tr.getAttribute('data-vscode-context') ?? '{}') + context.varName = newName + tr.setAttribute('data-vscode-context', JSON.stringify(context)) + } + input.dataset.varName = newName + + vscodeApi.postMessage({ type: 'renameVariable', variable: originalName, newName }) + } + }) + + // Only handle Enter/Escape when actively editing — let readonly state bubble to the document handler + input.addEventListener('keydown', (e: KeyboardEvent) => { + if (input.readOnly) return + if (e.key === 'Enter') { + e.stopPropagation() + input.blur() + } else if (e.key === 'Escape') { + e.stopPropagation() + input.value = input.dataset.originalValue ?? '' + input.blur() + } + }) + + iconLabel.appendChild(img) + iconLabel.appendChild(input) + td.appendChild(iconLabel) +} + +// Value column: editable input (readonly until double-click or context menu) +function createValueCell (td: HTMLTableCellElement, row: WorkspaceVariable): void { + const input = document.createElement('input') + input.type = 'text' + input.size = 1 + input.className = 'wsb-value-input' + input.value = row.fields.Value ?? '' + input.readOnly = true + input.dataset.varName = row.name + input.dataset.originalValue = row.fields.Value ?? '' + + // Double-click enters edit mode + input.addEventListener('dblclick', () => { + enterEditMode(input) + }) + + // Commit edit on blur if the value changed, then return to readonly + input.addEventListener('blur', () => { + const newValue = input.value + const originalValue = input.dataset.originalValue ?? '' + input.readOnly = true + if (newValue !== originalValue) { + vscodeApi.postMessage({ type: 'editValue', variable: row.name, newValue }) + } + }) + + // Only handle Enter/Escape when actively editing — let readonly state bubble to the document handler + input.addEventListener('keydown', (e: KeyboardEvent) => { + if (input.readOnly) return + if (e.key === 'Enter') { + e.stopPropagation() + input.blur() + } else if (e.key === 'Escape') { + e.stopPropagation() + input.value = input.dataset.originalValue ?? '' + input.blur() + } + }) + + td.appendChild(input) +} + +// Non-interactive columns (Class, Size): readonly input with size=1 for consistent table layout +function createTextCell (td: HTMLTableCellElement, row: WorkspaceVariable, colName: string): void { + const input = document.createElement('input') + input.type = 'text' + input.size = 1 + input.className = 'wsb-text-input' + input.readOnly = true + input.tabIndex = -1 + input.value = row.fields[colName] ?? '' + td.appendChild(input) +} + +// ── Incremental DOM Patching ───────────────────────────────────── + +// Checks whether the incoming data can be patched in-place without a full rebuild. +// Data arrives pre-sorted from the extension host, so we compare row identity directly. +function canPatchInPlace (oldRows: WorkspaceVariable[], newRows: WorkspaceVariable[]): boolean { + if (columns.length === 0) return false + if (oldRows.length !== newRows.length) return false + + const tbody = document.querySelector('.wsb-table tbody') + if (tbody == null || tbody.children.length !== oldRows.length) return false + + for (let i = 0; i < oldRows.length; i++) { + if (oldRows[i].name !== newRows[i].name) return false + } + + return true +} + +// Patches individual cells in-place rather than rebuilding the entire table. +// Preserves active edit state by skipping focused value inputs. +function patchRows (sortedRows: WorkspaceVariable[]): void { + const tbody = document.querySelector('.wsb-table tbody') as HTMLTableSectionElement + if (tbody == null) return + + for (let i = 0; i < sortedRows.length; i++) { + const row = sortedRows[i] + const tr = tbody.children[i] as HTMLTableRowElement + if (tr == null) continue + + for (const col of columns) { + const td = tr.querySelector(`td[data-col="${CSS.escape(col.name)}"]`) as HTMLTableCellElement + if (td == null) continue + + if (col.name === 'Name') { + // Patch icon if the variable's class changed + const img = td.querySelector('.wsb-icon') + if (img != null) { + const iconFile = getIconFilename(row.fields.Class ?? '') + const isDark = document.body.classList.contains('vscode-dark') + const newSrc = `${getIconsBaseUri()}/icons/${isDark ? 'dark' : 'light'}/${iconFile}` + if (img.src !== newSrc) { + img.src = newSrc + img.alt = row.fields.Class ?? '' + } + } + // Skip patching name input if the user is actively editing + const nameInput = td.querySelector('.wsb-name-input') + if (nameInput != null && document.activeElement !== nameInput) { + if (nameInput.value !== row.name) { + nameInput.value = row.name + nameInput.dataset.originalValue = row.name + nameInput.dataset.varName = row.name + } + } + } else if (col.name === 'Value') { + // Skip patching if the user is actively editing this input + const input = td.querySelector('.wsb-value-input') + if (input != null && document.activeElement !== input) { + const newValue = row.fields.Value ?? '' + if (input.value !== newValue) { + input.value = newValue + input.dataset.originalValue = newValue + } + } + } else { + const newText = row.fields[col.name] ?? '' + const input = td.querySelector('.wsb-text-input') + if (input != null && input.value !== newText) { + input.value = newText + } + } + } + } +} + +// ── Event Handlers ─────────────────────────────────────────────── + +// Delegated click handler for the table element +function handleTableClick (e: MouseEvent): void { + const target = e.target as HTMLElement + + // Header click: toggle sort (skip if a resize is in progress — see startColumnResize) + const th = target.closest('th') + if (th != null) { + if (resizing) return + const colName = th.getAttribute('data-col') + if (colName == null) return + const col = columns.find((c: WorkspaceColumn) => c.name === colName) + if (col == null || !col.sortable) return + + // Toggle direction if same column, otherwise start ascending + if (sortColumn === colName) { + sortDirection = sortDirection === 'asc' ? 'desc' : 'asc' + } else { + sortColumn = colName + sortDirection = 'asc' + } + updateSortIndicators() + notifyStateChanged() + return + } + + // Row click: select the row + const tr = target.closest('tr[data-var]') + if (tr != null) { + const varName = tr.getAttribute('data-var') + if (varName != null) { + selectRow(varName) + } + } +} + +// Manages focus-related CSS classes and syncs row selection when a cell receives focus +function handleFocusIn (e: FocusEvent): void { + const target = e.target as HTMLElement + const tr = target.closest('tr[data-var]') + if (tr != null) { + tr.classList.add('wsb-row-focused') + tr.classList.remove('wsb-row-selected-unfocused') + const varName = tr.getAttribute('data-var') + if (varName != null) selectRow(varName) + } +} + +function handleFocusOut (e: FocusEvent): void { + const target = e.target as HTMLElement + const tr = target.closest('tr[data-var]') + if (tr != null) { + tr.classList.remove('wsb-row-focused') + if (tr.classList.contains('wsb-selected')) { + tr.classList.add('wsb-row-selected-unfocused') + } + } +} + +// Dispatches keyboard shortcuts via the KEY_BINDINGS table +function handleDocumentKeydown (e: KeyboardEvent): void { + const binding = KEY_BINDINGS.find( + b => b.key === e.key && (b.guard == null || b.guard()) + ) + if (binding != null) { + e.preventDefault() + binding.action() + } +} + +// ── Row Selection ──────────────────────────────────────────────── + +function selectRow (varName: string): void { + // Remove selection from the previously selected row + const prev = document.querySelector('.wsb-table tbody tr.wsb-selected') + if (prev != null) { + prev.classList.remove('wsb-selected') + } + + // Apply selection to the clicked row + selectedVarName = varName + const tr = document.querySelector(`.wsb-table tbody tr[data-var="${CSS.escape(varName)}"]`) + if (tr != null) { + tr.classList.add('wsb-selected') + } +} + +// ── Column Resizing ────────────────────────────────────────────── + +function startColumnResize (e: MouseEvent, th: HTMLTableCellElement, colName: string): void { + e.preventDefault() + e.stopPropagation() + resizing = true + + const startX = e.clientX + const startW = th.offsetWidth + // Use the stylesheet-defined minimum + const minWidth = colName === 'Name' ? 65 : 55 + + const onMouseMove = (moveEvent: MouseEvent): void => { + const delta = moveEvent.clientX - startX + const newWidth = Math.max(minWidth, startW + delta) + + // Pin all three width properties so table-layout:fixed cannot redistribute space + th.style.width = `${newWidth}px` + th.style.minWidth = `${newWidth}px` + th.style.maxWidth = `${newWidth}px` + th.classList.add('wsb-manually-resized') + + // Mirror the triple-constraint on all data cells in this column + const cells = document.querySelectorAll(`.wsb-table td[data-col="${CSS.escape(colName)}"]`) + cells.forEach((cell: Element) => { + const cellEl = cell as HTMLElement + cellEl.style.width = `${newWidth}px` + cellEl.style.minWidth = `${newWidth}px` + cellEl.style.maxWidth = `${newWidth}px` + }) + } + + const onMouseUp = (): void => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + + // Brief delay before clearing the resizing flag prevents + // the sort click handler from firing during a resize + setTimeout(() => { resizing = false }, 50) + + columnWidths[colName] = th.offsetWidth + notifyStateChanged() + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) +} + +// ── Helpers ────────────────────────────────────────────────────── + +// Finds the value input element for a given variable name +function findValueInput (varName: string): HTMLInputElement | null { + return document.querySelector(`.wsb-table tbody tr[data-var="${CSS.escape(varName)}"] .wsb-value-input`) +} + +// Finds the name input element for a given variable name +function findNameInput (varName: string): HTMLInputElement | null { + return document.querySelector(`.wsb-table tbody tr[data-var="${CSS.escape(varName)}"] .wsb-name-input`) +} + +// Posts the current UI state to the extension for persistence +function notifyStateChanged (): void { + vscodeApi.postMessage({ + type: 'stateChanged', + state: { columnWidths, sortColumn, sortDirection } + }) +} diff --git a/syntaxes b/syntaxes index 264823e..fd473eb 160000 --- a/syntaxes +++ b/syntaxes @@ -1 +1 @@ -Subproject commit 264823ebcfa67017621ac3fcf7f7493a56e83d44 +Subproject commit fd473ebfa18cb31a5da7aa1c5d20deee4e122ba9 diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..8922f26 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,24 @@ +'use strict'; +const path = require('path'); + +module.exports = { + target: 'web', + mode: 'development', // production or 'development' for non-minified output + entry: './src/workspacebrowser/webview-main.ts', // Workspace browser webview entry point + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'out'), + }, + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, +};