From 2b59d6038bb35944e388ff125c6d556a472d6789 Mon Sep 17 00:00:00 2001 From: dlilley Date: Tue, 28 Jul 2026 14:08:35 -0400 Subject: [PATCH] Changes for v1.3.13 --- .eslintrc.json | 29 + CHANGELOG.md | 11 + README.md | 12 + package-lock.json | 1018 ++++++++++------- package.json | 70 +- public/RunTests.png | Bin 0 -> 151806 bytes server | 2 +- src/commandwindow/CommandWindow.ts | 244 +++- src/commandwindow/TerminalService.ts | 8 +- src/extension.ts | 55 +- src/notifications/Notifications.ts | 8 +- src/services/telemetry/TelemetryLogger.ts | 7 +- src/services/testing/MatlabTestDiscovery.ts | 381 ++++++ src/services/testing/MatlabTestInterfaces.ts | 56 + src/services/testing/MatlabTestRunner.ts | 227 ++++ src/services/testing/MatlabTestService.ts | 48 + src/test/test-files/SampleTestClass.m | 15 + .../test-files/project-with-deps/matlab.toml | 4 + .../repoFolder/myPkg1/resources/mpackage.json | 28 + src/test/tools/tester/TerminalTester.ts | 119 ++ src/test/tools/tester/TestExplorerTester.ts | 66 ++ src/test/tools/tester/TestSuite.ts | 3 + src/test/tools/tester/VSCodeTester.ts | 26 + src/test/ui/project.test.ts | 79 +- src/test/ui/terminal.test.ts | 42 + src/test/ui/testExplorer.test.ts | 42 + src/test/unit/.mocharc.js | 5 + src/test/unit/CommandWindow.test.ts | 2 +- src/test/unit/MatlabTestDiscovery.test.ts | 228 ++++ src/test/unit/MatlabTestRunner.test.ts | 447 ++++++++ src/test/unit/TelemetryLogger.test.ts | 215 ++++ src/test/unit/mock-vscode.ts | 59 +- src/test/unit/mocks/vscode.ts | 235 ++++ src/test/unit/runTest.ts | 4 +- src/test/workspacebrowser/icons.test.ts | 1 + .../provider/lifecycle.test.ts | 121 +- .../provider/telemetry.test.ts | 312 +++++ .../provider/versionGate.test.ts | 30 + .../WorkspaceBrowserProvider.ts | 77 +- src/workspacebrowser/icons.ts | 1 + .../resources/icons/dark/wsDataDictionary.svg | 14 + .../icons/light/wsDataDictionary.svg | 14 + 42 files changed, 3795 insertions(+), 570 deletions(-) create mode 100644 public/RunTests.png create mode 100644 src/services/testing/MatlabTestDiscovery.ts create mode 100644 src/services/testing/MatlabTestInterfaces.ts create mode 100644 src/services/testing/MatlabTestRunner.ts create mode 100644 src/services/testing/MatlabTestService.ts create mode 100644 src/test/test-files/SampleTestClass.m create mode 100644 src/test/test-files/project-with-deps/matlab.toml create mode 100644 src/test/test-files/project-with-deps/repoFolder/myPkg1/resources/mpackage.json create mode 100644 src/test/tools/tester/TestExplorerTester.ts create mode 100644 src/test/ui/testExplorer.test.ts create mode 100644 src/test/unit/.mocharc.js create mode 100644 src/test/unit/MatlabTestDiscovery.test.ts create mode 100644 src/test/unit/MatlabTestRunner.test.ts create mode 100644 src/test/unit/TelemetryLogger.test.ts create mode 100644 src/test/unit/mocks/vscode.ts create mode 100644 src/test/workspacebrowser/provider/telemetry.test.ts create mode 100644 src/workspacebrowser/resources/icons/dark/wsDataDictionary.svg create mode 100644 src/workspacebrowser/resources/icons/light/wsDataDictionary.svg diff --git a/.eslintrc.json b/.eslintrc.json index 601348a..52f6d9e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,6 +56,35 @@ // Mock stubs and disposables use empty function bodies as intentional no-ops. "@typescript-eslint/no-empty-function": "off" } + }, + { + "files": ["src/test/unit/**/*.ts"], + "rules": { + // Mock objects require any types for flexible test doubles. + "@typescript-eslint/no-explicit-any": "off", + // Mock stubs and disposables use empty function bodies as intentional no-ops. + "@typescript-eslint/no-empty-function": "off", + // Test mocks use Function type for generic callback parameters. + "@typescript-eslint/ban-types": "off", + // Mock properties don't need readonly annotations. + "@typescript-eslint/prefer-readonly": "off", + // Dynamic delete is used in mock client teardown. + "@typescript-eslint/no-dynamic-delete": "off", + // Tests use new for side effects (instantiating classes under test). + "no-new": "off", + // Padded blocks are acceptable in test organization. + "padded-blocks": "off", + // Truthy object checks are used in mock conditionals. + "@typescript-eslint/strict-boolean-expressions": "off", + // Mock registration requires require() before imports. + "@typescript-eslint/no-var-requires": "off", + // Cache setup objects are more readable on one line. + "object-property-newline": "off", + // Unused vars are common in test setup (instantiation triggers side effects). + "@typescript-eslint/no-unused-vars": "off", + // Type assertions used in mock wiring. + "@typescript-eslint/no-unnecessary-type-assertion": "off" + } } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index a66f186..189cb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.13] - 2026-07-28 + +### Added +- Support for discovering and running MATLAB unit tests (Addresses [mathworks/MATLAB-extension-for-vscode#138](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/138)) +- Support for syntax highlighting in the MATLAB terminal (Addresses [mathworks/MATLAB-extension-for-vscode#295](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/295)) + +### Fixed +- Resolves an issue that prevents MATLAB workspace data from updating when a `clear` command is followed by a long-running operation, such as `figure`. (Addresses [mathworks/MATLAB-extension-for-vscode#332](https://github.com/mathworks/MATLAB-extension-for-vscode/issues/332)) +- Prevents starting the MATLAB language server in untrusted workspaces. +- Resolves an issue with telemetry not being reported when extension settings are changed. + ## [1.3.12] - 2026-06-15 ### Added diff --git a/README.md b/README.md index eb5783a..4c8a297 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ If you have MATLAB R2021b or later installed on your system, you have access to * Code analysis, such as continuous code checking and automatic fixes * Code outline * Symbol renaming +* Test discovery and execution ![MATLAB Extension Demo](public/AdvancedFeatures.gif) @@ -84,6 +85,17 @@ When a project is open, Visual Studio Code shows the project name in the status ![MATLAB Projects Screenshot](public/Projects.png) +## Run MATLAB Tests +If you have MATLAB R2021b or later installed on your system, you can run MATLAB unit tests using the Test Explorer in Visual Studio Code. To add tests to the Test Explorer, open the Testing view by selecting the Testing icon in the Activity Bar on the left side of the Visual Studio Code window, and then click **Add Test Folder** or **Add Test File** in the Test Explorer. You also can add tests using the `MATLAB: Add Test Folder` and `MATLAB: Add Test File` commands in the Command Palette. + +When you add a test folder, the extension adds all tests in that folder and its subfolders. Tests appear in a hierarchical tree organized by file, test procedure, and parameterization. The test tree updates automatically when test files are modified or when files are added to or removed from your test folders. The extension supports class-based tests (including parameterized tests), function-based tests, and script-based tests. + +To run tests, click **Run Test** to the right of a test or test file in the Test Explorer, or click **Run Tests** in the toolbar. Results appear in real time as each test completes, with icons indicating whether the test passed, failed, or remained incomplete. Additionally, as the tests run, test output appears in real time in the Test Results panel. + +If a test fails, click the failed test in the Test Explorer to navigate directly to the failing line in your test code and view test diagnostics. Alternatively, click the failed test in the Test Results panel to view diagnostic messages. + +![Test Explorer showing test results with passed and failed tests, and diagnostic output in the Test Results panel](public/RunTests.png) + ## Run MATLAB in Jupyter Notebooks You also can use this extension along with the Jupyter Extension for Visual Studio Code to run MATLAB in Jupyter notebooks using Visual Studio Code. For instructions, see [Run MATLAB in Jupyter Notebooks Using VS Code](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/install_guides/vscode/README.md). diff --git a/package-lock.json b/package-lock.json index 7b21679..518491d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { "name": "language-matlab", - "version": "1.3.12", + "version": "1.3.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "language-matlab", - "version": "1.3.12", + "version": "1.3.13", "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.56.0", "node-fetch": "^2.6.6", - "vscode-languageclient": "^8.0.2" + "vscode-languageclient": "^8.0.2", + "vscode-oniguruma": "^2.0.1", + "vscode-textmate": "^9.3.2" }, "devDependencies": { "@types/chai": "^5.2.2", @@ -673,10 +675,11 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -768,6 +771,7 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -1210,32 +1214,10 @@ "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1655,16 +1637,20 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", - "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", "dev": true, "license": "MIT", "dependencies": { - "azure-devops-node-api": "^11.0.1", + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -1674,7 +1660,7 @@ "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", - "semver": "^5.1.0", + "semver": "^7.5.2", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", @@ -1686,7 +1672,7 @@ "vsce": "vsce" }, "engines": { - "node": ">= 14" + "node": ">= 16" }, "optionalDependencies": { "keytar": "^7.7.0" @@ -1839,17 +1825,6 @@ "node": ">=4" } }, - "node_modules/@vscode/vsce/node_modules/azure-devops-node-api": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", - "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1888,6 +1863,23 @@ "node": ">=0.8.0" } }, + "node_modules/@vscode/vsce/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@vscode/vsce/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1919,16 +1911,6 @@ "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", @@ -2245,9 +2227,9 @@ } }, "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==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -2273,6 +2255,7 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2322,6 +2305,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2330,6 +2314,16 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2557,9 +2551,9 @@ "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==", + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2580,12 +2574,16 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/binaryextensions": { @@ -2651,9 +2649,9 @@ "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2814,6 +2812,7 @@ "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", "dev": true, + "license": "ISC", "dependencies": { "@bcoe/v8-coverage": "^1.0.1", "@istanbuljs/schema": "^0.1.3", @@ -2847,6 +2846,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -2857,10 +2857,11 @@ } }, "node_modules/c8/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2879,6 +2880,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -3022,9 +3024,9 @@ } }, "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==", + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", "dev": true, "funding": [ { @@ -3153,6 +3155,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3160,6 +3163,16 @@ "node": ">= 6" } }, + "node_modules/chokidar/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -3856,9 +3869,9 @@ } }, "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==", + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", "dev": true, "license": "ISC" }, @@ -3888,24 +3901,25 @@ } }, "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==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -4027,9 +4041,9 @@ } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -4655,6 +4669,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } @@ -4740,9 +4755,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -4888,15 +4903,16 @@ } }, "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" }, "engines": { @@ -4939,11 +4955,12 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5136,9 +5153,9 @@ "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", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -5353,10 +5370,11 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -5701,6 +5719,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6317,10 +6336,21 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -6591,9 +6621,9 @@ } }, "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==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, "license": "MIT", "engines": { @@ -6938,10 +6968,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -6970,6 +7001,7 @@ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", @@ -7001,9 +7033,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -7059,19 +7091,6 @@ "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", @@ -7124,9 +7143,9 @@ } }, "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==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7199,11 +7218,14 @@ } }, "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==", + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/node-sarif-builder": { "version": "3.4.0", @@ -7289,10 +7311,14 @@ "license": "ISC" }, "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "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" } @@ -7349,9 +7375,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT" }, @@ -7864,16 +7890,17 @@ "dev": true }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8280,6 +8307,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -8707,9 +8735,9 @@ } }, "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==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "peer": true, @@ -9513,9 +9541,9 @@ "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==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -9660,9 +9688,9 @@ } }, "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9679,9 +9707,9 @@ } }, "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==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9701,12 +9729,39 @@ "webpack": "^5.1.0" }, "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, "@swc/core": { "optional": true }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, "esbuild": { "optional": true }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, "uglify-js": { "optional": true } @@ -9745,9 +9800,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -9761,7 +9816,9 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -9785,9 +9842,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -9815,6 +9872,7 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -9829,16 +9887,17 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9852,6 +9911,7 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -9971,9 +10031,9 @@ } }, "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==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.0.tgz", + "integrity": "sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9987,8 +10047,14 @@ "node": ">=12.0.0" }, "peerDependencies": { + "loader-utils": "*", "typescript": "*", - "webpack": "^5.0.0" + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } } }, "node_modules/tsconfig-paths": { @@ -10396,9 +10462,9 @@ } }, "node_modules/vscode-extension-tester/node_modules/@vscode/vsce": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.1.tgz", - "integrity": "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", "dev": true, "license": "MIT", "dependencies": { @@ -10414,13 +10480,13 @@ "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", - "glob": "^11.0.0", + "glob": "^13.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", - "minimatch": "^3.0.3", + "minimatch": "^10.2.2", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", @@ -10429,7 +10495,7 @@ "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", - "yauzl": "^2.3.1", + "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "bin": { @@ -10451,6 +10517,24 @@ "node": ">=18" } }, + "node_modules/vscode-extension-tester/node_modules/@vscode/vsce/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/vscode-extension-tester/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -10462,9 +10546,9 @@ } }, "node_modules/vscode-extension-tester/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -10501,16 +10585,17 @@ } }, "node_modules/vscode-extension-tester/node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -10539,27 +10624,22 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/vscode-extension-tester/node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/vscode-extension-tester/node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } @@ -10580,15 +10660,25 @@ } }, "node_modules/vscode-extension-tester/node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -10603,6 +10693,22 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, + "node_modules/vscode-extension-tester/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/vscode-extension-tester/node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -10646,7 +10752,21 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-extension-tester/node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } }, "node_modules/vscode-extension-tester/node_modules/yocto-queue": { "version": "1.1.1", @@ -10682,9 +10802,9 @@ } }, "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -10716,6 +10836,18 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "node_modules/vscode-oniguruma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", + "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==", + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -10749,14 +10881,13 @@ "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==", + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "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", @@ -10766,20 +10897,20 @@ "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", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.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", + "loader-runner": "^4.3.2", "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", + "terser-webpack-plugin": "^5.5.0", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "webpack-sources": "^3.5.0" }, "bin": { "webpack": "bin/webpack.js" @@ -10867,9 +10998,9 @@ } }, "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==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "dev": true, "license": "MIT", "engines": { @@ -11041,7 +11172,8 @@ "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -11690,9 +11822,9 @@ } }, "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true }, "@jridgewell/gen-mapping": { @@ -12106,30 +12238,10 @@ "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true }, "@types/glob": { @@ -12431,15 +12543,19 @@ } }, "@vscode/vsce": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.19.0.tgz", - "integrity": "sha512-dAlILxC5ggOutcvJY24jxz913wimGiUrHaPkk16Gm9/PGFbz1YezWtrXsTKUtJws4fIlpX2UIlVlVESWq8lkfQ==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", "dev": true, "requires": { - "azure-devops-node-api": "^11.0.1", + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", "glob": "^7.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", @@ -12450,7 +12566,7 @@ "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", - "semver": "^5.1.0", + "semver": "^7.5.2", "tmp": "^0.2.1", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", @@ -12468,16 +12584,6 @@ "color-convert": "^1.9.0" } }, - "azure-devops-node-api": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", - "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", - "dev": true, - "requires": { - "tunnel": "0.0.6", - "typed-rest-client": "^1.8.4" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -12510,6 +12616,19 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12530,12 +12649,6 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -12855,9 +12968,9 @@ }, "dependencies": { "ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -12912,6 +13025,14 @@ "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } } }, "argparse": { @@ -13062,9 +13183,9 @@ "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==", + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", "dev": true }, "big.js": { @@ -13074,9 +13195,9 @@ "dev": true }, "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true }, "binaryextensions": { @@ -13133,9 +13254,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -13271,9 +13392,9 @@ } }, "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, "requires": { "cliui": "^8.0.1", @@ -13383,9 +13504,9 @@ "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==", + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", "dev": true }, "chai": { @@ -13470,6 +13591,12 @@ "requires": { "is-glob": "^4.0.1" } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true } } }, @@ -13958,9 +14085,9 @@ } }, "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==", + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", "dev": true }, "emoji-regex": { @@ -13985,19 +14112,19 @@ } }, "enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "dev": true, "requires": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" } }, "entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true }, "envinfo": { @@ -14087,9 +14214,9 @@ "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true }, "es-object-atoms": { @@ -14585,9 +14712,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true }, "fastest-levenshtein": { @@ -14685,15 +14812,15 @@ } }, "form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, @@ -14727,9 +14854,9 @@ "dev": true }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -14843,9 +14970,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -15001,9 +15128,9 @@ } }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "requires": { "function-bind": "^1.1.2" @@ -15612,9 +15739,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -15836,9 +15963,9 @@ } }, "loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true }, "loader-utils": { @@ -16079,9 +16206,9 @@ "dev": true }, "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true }, "mkdirp": { @@ -16126,9 +16253,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -16169,15 +16296,6 @@ "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" - } } } }, @@ -16231,9 +16349,9 @@ }, "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==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1" @@ -16282,9 +16400,9 @@ } }, "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==", + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true }, "node-sarif-builder": { @@ -16362,10 +16480,13 @@ } }, "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "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" + } }, "normalize-url": { "version": "8.0.1", @@ -16400,9 +16521,9 @@ } }, "nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true }, "object-inspect": { @@ -16747,9 +16868,9 @@ "dev": true }, "path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "requires": { "lru-cache": "^11.0.0", @@ -17326,9 +17447,9 @@ }, "dependencies": { "ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "peer": true, "requires": { @@ -17865,9 +17986,9 @@ } }, "tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true }, "tar-fs": { @@ -17989,9 +18110,9 @@ } }, "terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", @@ -18009,9 +18130,9 @@ } }, "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==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.25", @@ -18038,9 +18159,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -18067,9 +18188,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -18103,12 +18224,12 @@ "dev": true }, "minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "requires": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" } }, "path-scurry": { @@ -18208,9 +18329,9 @@ } }, "ts-loader": { - "version": "9.5.7", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", - "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.0.tgz", + "integrity": "sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -18506,9 +18627,9 @@ }, "dependencies": { "@vscode/vsce": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.1.tgz", - "integrity": "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", "dev": true, "requires": { "@azure/identity": "^4.1.0", @@ -18523,14 +18644,14 @@ "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", - "glob": "^11.0.0", + "glob": "^13.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "keytar": "^7.7.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", - "minimatch": "^3.0.3", + "minimatch": "^10.2.2", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", @@ -18539,7 +18660,7 @@ "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", - "yauzl": "^2.3.1", + "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "dependencies": { @@ -18548,6 +18669,17 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true + }, + "glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "requires": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + } } } }, @@ -18558,9 +18690,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -18584,16 +18716,16 @@ } }, "form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" } }, "glob": { @@ -18608,23 +18740,12 @@ "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" - }, - "dependencies": { - "minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "requires": { - "brace-expansion": "^5.0.2" - } - } } }, "linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "dev": true, "requires": { "uc.micro": "^2.0.0" @@ -18640,14 +18761,14 @@ } }, "markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", "dev": true, "requires": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -18659,6 +18780,15 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, + "minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "requires": { + "brace-expansion": "^5.0.5" + } + }, "p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -18689,6 +18819,15 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, + "yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "yocto-queue": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", @@ -18713,9 +18852,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "requires": { "balanced-match": "^1.0.0" } @@ -18744,6 +18883,16 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, + "vscode-oniguruma": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz", + "integrity": "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==" + }, + "vscode-textmate": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==" + }, "w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -18769,13 +18918,12 @@ "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==", + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", "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", @@ -18785,20 +18933,20 @@ "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", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.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", + "loader-runner": "^4.3.2", "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", + "terser-webpack-plugin": "^5.5.0", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" + "webpack-sources": "^3.5.0" }, "dependencies": { "mime-db": { @@ -18851,9 +18999,9 @@ } }, "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==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "dev": true }, "whatwg-encoding": { diff --git a/package.json b/package.json index efaf98e..58e7f0c 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.12", + "version": "1.3.13", "engines": { "vscode": "^1.67.0" }, @@ -27,6 +27,13 @@ "onTerminalProfile:matlab.terminal-profile" ], "main": "./out/extension.js", + "capabilities": { + "untrustedWorkspaces": { + "supported": "limited", + "description": "MATLAB language server cannot run in an untrusted workspace.", + "restrictedConfigurations": ["MATLAB.installPath"] + } + }, "contributes": { "commands": [ { @@ -124,6 +131,31 @@ { "command": "matlab.project.actions", "title": "MATLAB: Project Actions..." + }, + { + "command": "matlab.testing.runAllTests", + "title": "MATLAB: Run All Tests" + }, + { + "command": "matlab.testing.discoverTests", + "title": "MATLAB: Discover Tests" + }, + { + "command": "matlab.testing.addTestFolder", + "title": "Add Test Folder", + "category": "MATLAB", + "icon": "$(new-folder)" + }, + { + "command": "matlab.testing.addTestFile", + "title": "Add Test File", + "category": "MATLAB", + "icon": "$(new-file)" + }, + { + "command": "matlab.testing.removeTestItem", + "title": "Remove Test", + "icon": "$(trash)" } ], "keybindings": [ @@ -183,6 +215,13 @@ ] } ], + "viewsWelcome": [ + { + "view": "testing", + "contents": "[Add Test Folder](command:matlab.testing.addTestFolder)\n[Add Test File](command:matlab.testing.addTestFile)", + "when": "matlab.testing.isActive" + } + ], "menus": { "commandPalette": [ { @@ -197,6 +236,10 @@ "command": "matlab.changeDirectory", "when": "false" }, + { + "command": "matlab.testing.removeTestItem", + "when": "false" + }, { "command": "matlab.wsb.editValue", "when": "false" @@ -218,6 +261,25 @@ "when": "false" } ], + "testing/item/context": [ + { + "command": "matlab.testing.removeTestItem", + "group": "matlab@1", + "when": "controllerId == matlab-tests" + } + ], + "view/title": [ + { + "command": "matlab.testing.addTestFolder", + "when": "view == workbench.view.testing && matlab.testing.isActive", + "group": "navigation@1" + }, + { + "command": "matlab.testing.addTestFile", + "when": "view == workbench.view.testing && matlab.testing.isActive", + "group": "navigation@2" + } + ], "editor/title/run": [ { "command": "matlab.runFile", @@ -515,7 +577,7 @@ "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-test-files": "cd src && copyfiles ./test/test-files/**/*.m ./../out/ && copyfiles ./test/test-files/**/*.toml ./../out/ && copyfiles ./test/test-files/**/*.json ./../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", @@ -570,6 +632,8 @@ "dependencies": { "@vscode/debugadapter": "^1.56.0", "node-fetch": "^2.6.6", - "vscode-languageclient": "^8.0.2" + "vscode-languageclient": "^8.0.2", + "vscode-textmate": "^9.3.2", + "vscode-oniguruma": "^2.0.1" } } diff --git a/public/RunTests.png b/public/RunTests.png new file mode 100644 index 0000000000000000000000000000000000000000..1f01bd6339c615ff452c6eecd4c234d73762c3dc GIT binary patch literal 151806 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�JT`<21xZ=?tC)@WH7sm0w(*g@q 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%0DrMnqgQb43zKtei)?ozs&0i-*J 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ⅈ5!p74~o8P2mmLY_O4Q`)eZ_3W|*M6dRlp z^?p?)5grDQr>Fo%=xR?W;Ub8N&939c=)Sx5YnQq9i;mF183OV>&>C;M`~le?N9 zy1ZpC6*f~=R7AFu%?{EcKOeq|pdbpHb^d!|7I1a3HB*(isvx1B@`{#`@fk?)=0w-i z=0QnIpZx&Q5i%+&J|*SQ&SX)EPi=gjqr9A4#5|NPgCUVxkqJtTwEkg#0M74u#$Z{K z0@5t5`*WrXqTe@%R9^7|77wI%01em=F3ec$8=sz@X3&?Bl7j1$fpCfoZ=6!r+1=bc zfApjDY~IA@pYuOKtw19XKXdpg+dXQ@#=1o+!kfg5DF%q-u*gW$hxP$*bh zGDb#5f&=e(upkhPz5$99iaVV*Jmu2`MLFg36W7zWg;3lf5Z)10p%e9?CkujHKMp6X(e%Bj`N>E)v&a2upRpMOccsn z_aU1TYgdpjcW|7P;bJ6!tPahjWle9FqmKG>ptK5t3tS3{{qKJkW@g?4wrbEAp!YQG z<*L?7!HN|w}=V` z{axVeFE1~FvN|}gU`n-TXZXV{AbK`|2`?l;CJ`wzbd4qzFvQ1nFM^7Qw~;{le2+^P zjiRei-k$Iiu~RV7;m;0hjp@_d&j-MQZpCNJpg!xnW028@pzRi;Ms1>*LJW1u$jX>YGW`bY{)=MqvV7w&J z7B)7tVq%#9jOX{OeflH`gmf32(9+50~ zC-Ba^AQwOjZh1&o+&mN5JM>&zTLas25Zv`*@hwArhu8Ps8&I>-#>O;{g=dcmIj3Qf z9&Vse|GTsls%=8AP2q^T9PlI=N$rc1f={87l;!WJM zBr$M;)IK;nbAuduCCNKsUG%3zf*b2zY1 zlJLPy0HNerP(0wr(qfsMX-Y=^hnEdg4EQK?K+w;tYiMkqKjD0I@`5cT`b1-BC#8=H!7R{aIkJUF2VFF-r%W!Q8NGg z_YW}V-zTGPUILc~d|ycSKA5s{5DEZPKR|szh)p5L%^)tIS^EyeXC@}UEgsCE5*os}Nhx`^+Ck&Frlv0!H7pSknxX+J#aw2?88)}`T41jN zz5?u(MQa_fJ;3yJYKQXH*x*?|#vh4MWCMR}WafVUPe!P-Md8{1c^CAI&gp3)klSC4h_Dt zx*7<|AL2DB*Y*{X(v1U}nvTxTp;xJ)>*AT9tmPDx-Qc)z0PABB_kqhC~|v(6a%dF?}`3CvZBDnuhq^_p620 z2MN8O?_}`$M0nmC(u#V#O=_Yhrv40*t=?}>cdTwXdM)Lr^4_W5nI^HJmug!;=9J{( z9dC@f*ID@Y^WXU=7fK(K`PfalM^syi6bCfsY?uDkR=f1FpY8IG=!aA`d$8K=ZYJ>{ ziPbI3L#)sEXiUGW;v%<8fkkN1*T0-6=#pYSOa(W#;Ps;bo0pQ365N0;A&<#*cqjTM zi*RpV*SpR9(2_88caC2@^NFvIh=?8zy zXpvI$4N`SWjkCMTd0m#ua2E-x?0BF-c2s*9OD(QZh`5J%KdY)@fu<(NKY#^b4*~+5 zoo836 zKxzy$&}i_;n$)ki*52NEFtCqov?d}wv8F`A*Rlnd8N^3LDFTmud~rd5Xqpuigk>B@40ISYvNHaAmTh}X)hYP3}(`g~^5 zzi^L3)3%1bsJElNk8$}H$v)eQ)UpBLikZ=}{E3;awsyekY&t7h*h|2H!FakS&r(a~ zVb!_O0jqY-p?9HT_Klkm$}YIQi`}y2$h5Sx(Hk>>O|{Cvj`3sM&hBkQ)s{RTTJ}E{ z=T8X!J~(v02Mcx=mL8+|-?Cj$XGDnTD>f$`pk2h-|uAzb`J$>Gr8g zh`l{>cWzz6>{qx=V6rHvpb)DNxVgS}2|p-PR6iHqvHmFb2in+bHL#JW z<0CE2A9oA4XmP%Fmw?bzm#OqL2VYN6txWXE8hIWN6kT69L5lae#@^q*99*+=v(8G} zifGHGXtb?29)8koAE~fw%wX_yw%M>Pre)BQifmKxd?hgyGILG5IiE@ZNmcVT^ua>Q zFS+Q&oiz7&7W397!x&!GBDPl}H#{kWi~RZ5OQZUyz>`f(e$Zfhv|k!8!Zq~*&Xaj2 z&aANpu0xNv?HbkwDS$a4)!!K53g23wkuAGLmq&4un&2hacw7jLP{utL3dh`FoMiEZVv8pGVTW zGmc~^Wy?KnY2&KET#HH&lFSLLd$!Sy#K-MV)qs`@g=d zv&P!Y{%RrIZAz;CoiryjA!*5}?9j0{bM(RT=QoTO9kOc_Y-ash`F?6a&e0QVY2l94-%dnwujA^|2FcELIUBrq{WV)i zWg`{GRPRex5y_K=cPI3`&9MLM4vAl-OBOjLshKo#{U}UIE=peSeGe18k2F!+xmAw- zEebbJ!+(9+#);8|#%3?|{s8-F!j1%0U9nqK#_@VT$f;vQl)O%KYI;sp{P!vZ44uuSCL3EVf2F1ZdW z^Zigfjr@){=KW>$e^7}Ax4%{`S2*2Jt0}8u?HX+RggSwx-c4K5+!v@yp;Y6_j}XTW z8fT*;vu3)FdoLvp^gpS^Cq~>-R5m6}*01Xk&b%}q{e?FE@v<@!-PH*b&ic*@{NNs= z`m4fEMcQZV%$A-)AsO}QfWhuEWe;5iEU}JZm_r-^%NIz^mCH*bqL!Y z9!x5rEL7{j`l*|WnkLTYU=pO}0eW$)lCPQC1{e|ON}U241X*h$RszNo z3F@NC7nfF_rX<#wF_<#1X+FIPP}L-6$(vq^w~DL%E30V(`OFm${~1oXYi+5RrJeZU z*E)P?UQ+9`NP#Lk-+@3(8^h0gOu=zi_M`-iA|Is#V-n{u%y{nw;&6JI3E6Hb^Jzf2SeNWtt6@{rN>mC ze)pdwh9?%;*P6b(jIzxU%#t2o>NTIf<|rIK(fu40DXtZ+M@aV727~x!!pHQLqV+Y4 zl-mU%p{>xNe-=*rEaIo#j4EQ`tq;TEw+NVc|K1uFpLPVZ;0?VbpUe^Z!m=``)< zwYRsOOPbpV2fRD`ecLO{J&P49MNK~?e69jjOet=*o0?NH@*jo``&2VYd9?wM(>I>x z?g+^6l+n~I#6> z&Ho~iwn-#~>1kq5c^y7~CY>fJ;e5XGC0}m*@TN5?m|@lb`I8b6h*tqsn>-;|4m*aO zkh>T-H7}bU0B_}X514`t7Ft5$Olq%t5Y~i40M>l2LmzLB%!Y7f<=Gtg7 z#f%vVEh^LH;(UX>+TofCv#%dnt6E(h&JRwjn*mA$cIo=vu;`17_273q>`YW%G=UCE z05oG#Qztyu{0F>D`aF-c2({xUE>?uCecP_k%EWh$go~#FyWq$$ef01)$NoOtLlpa% zXHbwh*U1h~b(&rB#~Zwa1o7{Kzc;aeq8lerZL4Ga#6plru01`Yfu3QS%{(87e{)eP zc{*mCo6Re!GzE!$CrLc9QEwJwcm%DzOC1Z=KJn9nUN{h~&!yZ9U937C;CUWolJ+(y zcs19@LsO(r&~i}KB^PeE-444AJ5THc)@(`o?Su*S4iF^VAi|?mXQ73r8PLLpG5eO1 zs_ok6Z>HxNxN(S-_OeQ%S9kpxFdDRlnc}|Ilz{WpCsTm@?LxDm99@mK z*9v@Q$f$pT0LhDgz{p0>@9wjU@RGbx9YV~(aq*o(K*hjzdhS2rnDt%`Q*~aUDH1X1 z{T_1-&DYwn91k3H50ouo!N}%li{t4?anoMj?pM4J%OiqJ?j?IH1xQzVSAvbdKcyWE zu_jTusZEcqb@xm%RQr-lNOl)P$+2)`N`(Dw(-pfG+l!Is8STT4V#|q3WI@6qXV>mO z#kNS|Dl2`TbQ=AxuU`{%Va1w~)u246apAp7$d2xB8#A9r`41WdKW(|C;rmE3cTHSe zZe6@-7{g^5x}oKI@kf)8M%Ot-&0$Pr2kUE+vK`w$_9# zt#DW(_lOpijWpB7k-gxQnA!$^@?V5DtqK@L1}sV%C0;OpviR08|Fsm8%b~rr=7}L| zhT`jUmfiP?60px*tZ8`{6 z+vqG??6{I=5%NsKcO^tK9n^h#Mb7yWB7B}SU+Cmv?C0T}iw-VowPyWfP5RdBcy-5! zleb8OZ9vt%35)Nx+Fz0MHSF>35lJ(1duidiAsK*(Zf3M$U9?m(jjEg-gocxg*$gQb zxwf;5s)d6X&UA6+*;zrHVLZEb0LK81POtj z zF_)@kP7{*1iZ*NFbR|Po%;!M1+JB$h2Kw~t;oT1$%Kn#0bkFpgQu8n7W_q&^%hTP4 zuPUrDpmQ1H5IpR2Dpau0*iaXp3vDr*=~3u?PNLJ_4_)P&`$`XA=y_r*$23|b^r*LnbWrU|H^Rmz-S(5WT z@0sfD`+1vMP!g*0?AbFCF(2XD1Z=OtnM=Qp_mmSAju`r>Ovm|^hp0pwZ6^$2)Of&nCk2v zi-lTXH>W$0^pFxawa+qM%|1+^2?|W=27KYKj*l<2uM=+hll8=gf$^VqWfiI-J zBicYwiLRw6O~K3S>xnxXL#?rg@ohw^c>o-TxJXB?b$XVa;F zDK{_g4@N*%p(ipGv|}$WKrZwa+*czP>^s>6+>dY%TOV3_rIP^IEmhy4^bN|+Vh#FF z5Cg!=6WfS&3ca`Q83+PbBnjT)t{PJx?b?c))5W31B#B0@ab#V3ymJeQaxx&a-}A0& zlsdxvyqZL~D{&>Jbd*gU6`ei+9nkE%wMmXqYvrAVP;)%Du?sw2f?qjOnD{e|i zBkPIDxl1X5nAC8ixU$wKx_RAa9M7cQa$9k=cC;9Zu5KJ7^F2SNFIqA$XI0(zhJYs} z&ociRMN?MZ)!ts0+X|C+{e;KHc7=7by}hd5`d1KDksYm|pkUee;ez&~s%kyRl-I$h zW`E@>n9f7@Z|@F$snjF<{tuPq^Z%p=Ta?fE%W+)pMS8Zs`p4+{Dl@@+(n!XC%WqWK%jDa`^HuMozb;5)Z*5Y z_Z)rQLHT-Ewdu=3ePF&E<16Oi`YgyM{D44=5A22;gN64hNCm0~RZd-)W|jdKV+Xym zg$-h<6Fw&d$nWeUZ!XoMl@E@J;|DT1ymSeqeD5PGS~<@g+JBkN@2(2Vsi@#R+$||p zI;=_~B(OAQZf1BTOiWC?`)lz3lhHv;1hw6cL-&Df4=$8?jZ|4HSwX1ISwb3JiD>=u zt|{_H-@;2%RJ~iQ_!`BQ6b>>}br00>GP!5>soeuoWxh)6)4HZ;l{z1tZ5mX<1E`Zi zh=Wczl(h{>Y_QXsgKM4dGaGFN$x(FD7q_1^*`2F@<6PejpE9Azkh$unp9Q0NM}H9d5qrAfEg_@&<7)jeg-?!S{S);M7Z zZ~dBfHf!A}?Od-xeEd#XZ)rx!hfDLKhcsu}eLO}(^CZY76=^;Jl}ppDwOz*jB~d>C z@`|Ojf+PcSe#uIRjQ;1p_!3m>e*(JBpVEnA;SPUE;XlQR^*J0C+icy7mn@-UVv1}DslHuGJlG7Srs|&b;8YY zzd+UBFpdiSQJ<%^Lv-|=!G@x?&yOc!e6v~?&QCi6o2f^?^}RRz%D{H+&YhttJFpG1)5!vjfuP+R5xaEAjf z)O0ND`ma-d9H*`b=08UBcyclA@KU5NC2Lzli?^$BwnvqRAq!)Sh0Uca8&7?j$$IHx zDU7#OV5^_?5yO@~Y;dd&`P_28d9hP8Grp_~yD`x1?w3hHSM29CxE-46$hZ(wIy_oj^#sjn&w}Sr)LG|__b}CRVdz-#q@jKPXEwguvCuS|7lR4nMnm2-atzS zsQNr+DAFifp%>E{1QmB+h{o%EXy{Kliiu)UQu;uiu)oqYSaQi@b6EIljZKn)C$hpBUy52Elk+;gqUr||Fl=DX`O%Fvp-$P{TiUw6_j-moqwy`CcWYo?OM==?|1iw za<`wfZcBT$yUXifim?U#B*p!okiIu~a_Ok+y#4_VX3 zf(CDpI(r+}IEbzDSiO%7HxKSs;WCm&N@N&5T+~SZoTJ%z%hBWL8p%UIuV}K^ka@gQ ziO?nU$d33~j*3;iXu5P2Ird0C^RuZ)4Ev)}8ugsLpcM%2MuiM_(-q$|Azw=5I zsJPH^gwNpid8znB;It%YaP{ITs}4jYh;CpT(Y~J5of%(f$xVcHpIj59fW;s;7Q3lt zdaoe&`Arhm+~5SVghKt`EX(1G#F%uRjO65Uv88`WRR(j_y0Z`e7)x*9W|jNsrv?x; zd6P9;(5Hw9ZoCASv=3e0jOIMN2JPtJlDw9yyl>!oJkiT#383ktSA3(U*eC8I%r#^p zfz<80IM5Oyp-!EpWczs$BK9iY`1Zc}VWmx($Q=E0kc!nor2Te1gB`{_3j*QCeBC}J z+Q`StC%ulX#BF1XvbQw5#Bzwe`qwBd=dL%x-~MLa9AA~n$v-%V6p8YcMf|U&2n^h$ z=@(*EZ8|(mD}}rFAu081-P^4&_$2z!3^OY`?3){$N}Df*$cHv(XtWl+YH0jXVvv!$ z=4AfTc&(hVZr-5@0@2q@hpAi~fMLw5{4bW08$ zLk>I#Kfmwuy#Kz}g@}01%zf^2&OUpuz4qGa2VL7ve#gOIANt)KUGcIfl+CKYq3uDo zrh#1=6uPXGhC*C=RObyQ9ZWuYFP+SM;jBdMc5`IY^IkP>rhIWd-Ti$`?dP#e|IToq zApMx71a3F+NoV=B!L#v!%X7|^#d+Uf;X%1{ru==NZmq@e`mk}%sj8}KXnZ^bY&t`q zgZqX)Tv4qP51CA9CDJf24g4vkl^y;|u-&+Eelg;}mhtmRq^573(3Lsh2}{>;+WV_q zbQm9$brSWYI|b5`GBAA5%^Bozv?-~h^Y(wv0Uebm79N>MV#QYz%=<52+q{3ajwMGL zaj=5=Q7qQ$U-4^SKKFCzUTxfVbcfCJ#&)>d5z6ix$-uV?LT;0wj3u^YH{zo#Izxl= zOCjeQvMQS%{;A1V!v&{FT6vQOlORiI>@YQmh1XgqAb(lN?Z&Zbtpb}j@vAfCk*IX; z->BdNtn|*a>J!i$*LnFJ;n6f>ZCL#} zo>WN_oa%n3wB~y|qgm9FvH+jiq6Ks+z#LIb5tKAG&UCETfdEN?aSu<=qxuC@#Npwg z_Q|8AEZ#%#SN+SFFnjiE;}S%{+Y~sQ_HxqiBxf1 z?*A4*ksYWv%r7pMDRx&{en29Dz&)kwvnP>=Q%0F8`d+&$^4__}CGLzHZSo{hp0sO@ zV-0_Js++*@??LeY2YG0c`dJO>|NR^Cyh_8(LxUb8Jj^4BeVYa3;$5@f=f8cI*zMjV z5;iho4>&e#fF2Z1EkOmC1L7AO*~YXnk#CE>ZYK&c*2pRMDtXi_6U5nkUxB0?6gJpD zB0h>gxcoigA;=JxbB^H|Z$LY%=3r2_L0Lxa_5F;>@$%)hG(|T?o@gIITsgxMc9Z`O zF@^F-zh_)dZkB80?>W6Ij5y|HkKS^do-0hM8y&3=9#uEl zX8-VN-A_070v}qP1>ZdXyqSoW{k7>qO-*nJizF}c zO$!_M<=jqs)Zx%wUV+)W#I~X_uo5lmxKMJ6o`l+e33A=&{{X^1_rpikw5$^4mNH*LK;9O`Y|XV$_1o#WYMFrm5D z?)JYB+Q^P2D z7!xA?KMw%kQI~CR<8ELODK)ma*F9%f7iPbT5tkgSRqz;FOELi;O4HES^>>(~`eU9W z1GCOMI2{y3kXc_cSd{ac||es@rBj$a~L7=)pt#S0AbAUuiI_d;B&ivHR|OHwHeFWzo$tvEdoXx7kGkBa`G+C{*J|loi*0F2Iffv<$1UpA&KNA^j-e0_ zb6~r{9-v~rrFeoPO#a_-jQiVY0U;8VaoDo*a)5!#tRgi;03$(8IH}a2kl@9mO;f%i z?PldGtT<*Ime-GA-eQxfc}y46(!;BtEe>S6?(enoQM}I@8hyHSMF*0eRx8WT#Z-=5 z{nAPk9^blo;H$;BLF~tu-7$$ts52|5v@k_1xRtQp2*#$p)%x$TtR^MSTJIZVo)tff z!4r;m;U)OE&q!^-%ztgLfq#=n@kPIS=Et zCZEkyOpGf3mu-7K7ZQXu;lNyd?;)c__@!w8ZV@I1$K_W;z{U8tJ+Zl|{x7#jaz~FR z+WUq*M462F8exXSar;<~Mtl>>`J9G%mFenv#}9PepZ-OpQhEC~T&1ST??OR z0v|qO`Vk$N`z(Wkd(PuoVsH=c^QW-xn0L7KACdixqrA6N8j4tt%7(-qma|l zFv}$98qjC*o3ff;6`v;V0M!OPFqWA~q1TtC+u15?_J>py5*OBsgKWcO3yo=u-%%YV zlbe*zdsYk!JKwEFI82%PPKA$JQeL=S3(G@xIv!IwFC+xkCCD9z^u=?kkY8}y~i5BJWrb=P|y&6(l zRh3&<7=CdPAu~&tC(dE3_3wROQCChSt>i57tuwi~N$nk`xh8Y2tG&#}A!lGF{`a|A zGH3|Ngoa~bgm+zvTYVp_iFhf5fm&j8IF9?@H%HuEUsw0@?AY*f{?)S!TDF}{o&Uz<@u_Fe1`w zTFpo+`oFisNZV%n?@s>v#lwT<=>PkG|0bTc9bKu_>E9ao-#0u_%)XHS2L9iRMEB|c z|9O*CvsF0(p+>Ptm0dnD{a#;oOI2}J!@8+a(|_wL)=y0T)2Gk}c+bWLFc)a~6B0>O z^>t-BPoDfN{(ICmP?pA_bbBFQ#vN%rlFO?H^6a)_);aM_E3-&bF)j5=i=tJh<$A)D ztA;$e5zB!j;snvD}~$^!dXSB z9=BgklFz5da?a6v*$MS9J=iyiu&iFVk>p}8d8FA@|7sBOz-Zty$h+nlxP|3w8e>nq zq^0F%>pLcnRTYbNzLc?iu3IJn5mgO;cU~L3X0XpqT~)R-#?yEXE^k>H*zRZ%EHI3Z zKJUSe=<)wd#S9KfRTZg=-1r*ZsY~&O(}&)qp$8HxUHUigdOD)T^dmNc06Vo+juS

|aY9O7N7Y8Ct$Uz~Y^cHaNyX0m+UB5z>kY3_ZEgW1AI zFavIrVd`zy2v0YKy9Sb@f@glB3)@rEkdu(kLZkPuc$wT7q8co1!%Uc9`rZBUamF94 zat$t*Jh-Wg!*{p1ah(4C5_0xKbQ{76Z{IH>jjZ`c^qQD{+^tnuf2TE%&UTTw-$~ba z)j#m{>f7_{{Q**>VdEPGeG@G^E1Ul9r8X^5SlA7C9kJ%2Cklc6d=HK^S_MmI*LuEO zn_^qub1lzCTw};%XR`xayebiuD5jgXAs+NOvTJPQ^DG_pNpD62#LjS#VP|LEt1eul zQsCE)Vt&5F3vXm^8b@~9hYw~1kGUY)f{kVaSncioyX7Kl-WMos>wBXgb{;YJ& zJ$bD5g_gKM4*eC0CX@jyz2?~1c&Py5`6Z{}iUbu^7Z_d1j(z7(I?(zU?4#lP6qrco z&kZKq%7IRw`cV|*r3pq&Mie}eDjEKF9jQr;_7wF8D0gFy8~JIZTw+2)2~_hg-YYvq zv+Mq>ph93#YaMG~rE_%kReq*p8n`^hT^~w#DjekW2`|2XC-cwo%*EHI(WpNOL|PrK zd3{qCU)7c3>A@C1wtN!jUQV*`<4k1oXH;UxP2XAR_yHe~j=K;Jy6SzN1r_^xK-n{s z2XmLsNgeFy2m!i`@3pmCK*2JwH5>%gO}JQCa`0k9B|AGirT9^HKuad!G2;ai74%|a z*?E1Br7FTGgCH4KvX134-|XXpj*PZnwmRMU^j(uC``L9+>s}*`$fnpyoyp<~HlF|M z9ju}X=UP7`b9I;#3DxyCVGYAQ2U9zf(QCA^@mUpJ2(K@;(eV|Z&fr8!bxv*4mO0m# zQWZq0!@hDc-_G!S+6l=s+9h`nFMfUrAYV!+e4(TRq?*hKsKkHZNr*OMarVFWeve0P z*7dh%UQJD{)a%6Uy{bQ2g|}ukFDLI_CFFO0Z9&zw#dO)LBQAyB8YKx-o=!6Dimp{i zmJrj+SM9v1CInCXLX(q&V3!u+ZmY7i=oq+*aLCL6yijR$;`b(Ej_6|g4?X=AE)$ck zM)7<^ai(JyZZX`n6?UklZ1Hc4r0J%%`bF6x>$Av~J! z{ZhH|BK2g`Npvp7GC*A(S@7Q}u|b2`ms|qQCV*B6Q`SRWOG`@7GC)<^c$^^>CMH%0M1Dc>`k*<-{*V!C8pUpFyynq)(eL(*i`G`N z$E2iR^DS4Fgh4U=sq614_#%98Fdhg^oj}z9Mkt{iP@R=+d=q%TiC$E+p@R!l7uzh@ zP2$l=ec3hTtMQ~aa44rJ`A~vR1MS<>r84GRA>`FXB$?sQy&sw-%gdO;K@BeGw(lTn zJ%XX(-WI&>vWJyxs#e`fbH$(c(LdKLKE8fbedzb=@AA7w{nKB0HIHT$@g44574oO_e9FY_Zo*!U^Mc_&Y=GVzu&x6QQje;X^9MF6m zF7RpKPAVLuYY>`|vBveytT!LxwMo&-uTy`cJ**^bJz611`$I18Iv@V~-ZEIn1y2(Y zI+4&W=C#5zd(oiW{FRt>&AI0$qpj&GF{tc|S@02C&dI`Vy(w8jjNvDR#xdUCDGI@1 zDCP3WQHgjcIh;}?PU6~;T`rETWwPcZzO$_@NMQa9T`s=SYUoPoyFB40GISRQ383g zzBqQ1mOw)?UM2h0_4PgsYMadp*IzlQ?+vpx4fP*2liOe2k&swWOuUgS^}UqQS+EBs zFJK`bZ57CkTcFO$`6Zg|+Qez9Yi0a7^Z5B$HN+Or13{0fnF5G*!EdDd~n98s? ztz)9V{6~6SdR>x8%0lUTC~~KcX~s9oOyM=A`OTBA9Rv<_YnL~80u^QvhgV04%D^pf7%?-~`nWt7BkrE$VtMrQ$)Ky><#bxe%#}FbQA!e zty5{wsW8zonQ_L?OHF1%^pozXQylXZ_kR3BZ4SC zfB0b4nWK86R{YfTrh9F}0YbSEpu~TrcXXcaS3G}{ET&tV;B}9jNmDe4%>B&zf~zZL zM7I;%mdS5u#Wo>uU4*4top9C%)7OC-<=MprK6!mG(4j<7A~rXBfE;jmNC*QE*Mh*r zy88(Y>s$0VSeYRExn5$lJXj`l9wHurWC@odlZS5m_rkxEQEB>Us^$es{NWO>=DUaCc?#yWO>(#Tqa(pgv`MK+^6IYwso0&;Qy4axV@YEhYXu!IWQy$!&JyT z>HeWsH{!>DE>~(5bC5JS7oyKtm$osrni`+S;C8?$+c| zJYy3A>uF*#p4+9STe!#D+dgGwhb5Jj&uEP3hbGgnI=?%OjBr>C5L2R*7q5Y1q!-GD|4NRZlfR3j9)lmx9TWWcD83- z$Y1ZGzkEICOXf!<0*L^N??=bS^o(m0*d#_RUDP+V<;INof2z{78&Pt+DE3XsV(6^JQa!O{t{=4IgnbkOp)gZ?>!~r z$1*oJ=jMh*#kjm&PiR8z*w@t7a@kJvLQSY#%waxCT3Rd-5x(mfG{Zcg$nR5PqIQ^x zS$_g77?a@byLYtgJ24lo+S;U+mU(sg?`d^39c(a?BP|V`vD5HkoKJbfLK^2^S=NuJy?z~g;m$zg>g;T^JIi84P9!0rlp0{AIW)lu%~;Hh z7={2z`IWPCPi|`sd~YNL1DMaMDh;2DO^K7wr?R?at9+`{9fVI~xgCu97RY-;@hH!Y zGU|b1A8A8c+OO?hKMN=3g7;;~Vg|aZkdqUz87s{_`{A5uJ87%7Dn1Nh_2cd@s4c!j zyMAk=7Z>2wa)9VX_8^vR4dSI)U|44+k|2qc=AssSXx(KjJi!iT>C5i0r*UnL7)-i< z3F&(HO=oXmPPdXMp_+*q!=aMwQ(`IC>bQHdnA@NI2RD9&v@Z-F|5Z5H5xKf7U6T}g zY4vO?lVMjD3J zd$-|@FD)$r7Fq${210wjf-1+|TbFmIOJ7yB4J24k{1!8cZ>kT;fZ9*m-@lw+zJFf@ za-D+0!ude!XeNp*21%con7E~7st067;FlA}P-~s?L$QoKs6}xu>NpvekGN#x;(Gkj z8U>c<&`fC4=4rfBaeMnKP*hl0S%fTNp=nMBrF!o*G>Y8AQ&TCk--rRtiwLrCYGD@= z0RaJ^p~_@jyRk|^x@mTQRnx-nI1k@)W=1kqOg;ro zzH+Oosb-~<$FIhqmm?(es!O+DVpz+MDRjKZ5m5r0^KXDytg$WYRl z;MKmU@jPiIX3=L!#M_0cV(MpER{HNc_0Q+clGtU`(;u{oj-NESW`BXmt^F~HZQM|> zL@H`ZxeZWl_d?|ki??fgG()5h)kcavL%PXV1R6wZvqTF7g$frD=Xtvi=YBbC!nBpWF!zU z;tnnsifNv2Y0@)L8g+(m>eV{EhRXQ(hyszMhQr>zm(m4MXJ;Ou826pLJUzFQUwMgv zs?U|MkFW1!m2I-{I*99Z^ztGH1#N5!S(v*u*B|2Gj3?32tzZM!)9W(V+|2A&S|@U_ z#;ln3rPCqt79Rte#u}`z&jhp+&=DJ*c^_0?F9LMtw-AQ!-{0yHgH>mGx;y8!P@k*g z%{hJROp#KaIhf|$$alC>iQVoXe-AUpPqZoPWat!sWhB40J{c0kJM z#AIZ>z|#sC8?zUwU=$G%xr-ho=*=Jf=lPr@)~EV<^0hS-C`yIb85sCsIUn7boZ`FM zTh?s)_LHAI>-4~n#GbWVP6GaSQi(RHoHD!V^-Jrz^1bTZq#4sOwU`&(bD@4jK_G&* zU-_l3Yq+!_nDFh}bD$gP5w;SeKT+>S2@-dTs8LGE+9xm~!dZG2mdX>Rik1NMeBi*C zY^fOnFZ`X-AouUsFC6#p3sg?mF>micNCh2UhRzHy$6O!iLki$Wsy{GIpLTBDiKfg9 z?o5LA%q*|O4qrKl)@m;}NE4dgy!C@?U+C)OquNeXE~3&=&r1hHCd=PPmUy4A&gL_B zbvh<^?fucsVMj7f$Osu#7Pcm&e-qBLZvbG*LB>`}$yKH?HW%cX1WE+9uKvLc(Q;;4 zj)_ANppZA4acTKr&7?CNuzbF*b`TSPuoKVFmpmUooKc$?H#UX)@HCBNQyAG=F=ouQ z=|zX-ga{idO##714eH}xztDmFvaz|qJ&Q>$8+-e~-MRX4G#+5s@WdyjOihm#gkSLS ztuD3%>4pRb6`ROiOIxXrZorZZ*7t zG>^@~^^dyTTxTFr8~|oNFpvtaJppVA@PJIGD?0u6>uPGi=6<81@&k?nN<PIK}=}Th&@xT0nfw^W?nOdwp#!_tbT#|5QrbUwNnv)TsYnkdck_ zASbs5XIqiqr|k2Y$1~ObV?{xd)>c+;Wo6Zxnj$DHC-ngq@C%k#*fT%hsDM#-eYL)% zrareel-b4}LC*V|)~D7{`ckvke!d}~*3-YRurPtg9CvLE{cOLjX&=OgO&6-&DI!ZtVjzl?l78d!3;+}s$jp}6 zTGg{Y!#I8f9~A))xssAO1YJqua$9hG+}-o~yz3!|BP~3{{7>sWw^nS&kE8m*+i6u= z-HD4^V~^MiZ+lh$L2RfBov~ag9gPh4U@V9EQU>N1dy67~IBt(<{SZ5;)86e`l~qr} z%U?k3m|$@P2sh?4n{SUfKqY;84f_Exf=-p@BHX&?|L#9?@=JCpoChlZ;DOTum4BtEgbkt z#^uA{A=#fY4o7~(6LXA6n(hU1&FX_-Z1Q)*tL48;^>T{&%!h4yxIGU)+AS1Wokkqa z>c$dk%o_)yjKaT%k2S7>$na_+26JpYyUO6a^jAuC5YxV)DkCpKSG>jVMM$iBH*~zb zIJXf_!NI}d;T@Tsl~pzcLn0>Zl~sltc7YWC;h_sPRo;}{eX_fYw9+;+W6#@w$Ztj} z^3I*>?b%vslrV6p$eti{33#`u@NjTCy3F;;>yoPKf%RzaGN%A`nx27yKUqctgZPgq^gkM^6oEl^hRF+y*g! zFCo*!yoGrqTYIf&)YYq5IP5H3hIP<5U&ZgI4J;dde*qDVRubroa2lSS>*eOy)@hqu z7r-Yubw>lyWY;+*D=A=V?WCOoJY4XBIBb7k6990J(DffoK87lh3gGGwI_!!BpC|O>aCg62V2Jy5v9L!n#yFSa(WA9j0l*P(?a_qv$ zkh8QvqoA8l@?mG1vQYQ*+2hBf;K>6MYYgJUt1B}1>D?*k7)*i&fV=BL)3b8dH~`Ps z-^!w}pt@$HE+;GN+jv2BwdJWBfKB`_M(s2pIvKF+S!Z}*hK9FwHUFump(*}jE`{|w zvAs$B_SXXP3grG2fAVYly!U)_#c^?L_2bw);{i7EcR+w#dZ@5a^cmg6A4Aq1lcWbI z5Pjya#8O@YZ?vPO1vDM7+(Hx~P?kD-07KV9_>R~Zn*2lUrcEY@|a;xN@02=XuUZSVzMt}O6p9teh zilLmlg+Y)47-vf!Wz)U4Tj`qV&~VJ{xq`LYmePC$(Tp*b4s;x=40Z^B7M`OVQ=LOF|Yx~f64oMqQ-r~ zneX5Ry<@j{*qjX&s|<7++BK%y*?HFwMF0$tM9c%9rS^V|I^yP}b9otYb|H+ww&xQP z7Oo^wLu3c%f~Wx;|LVosT3UC1i}d5DtElvxAf{1MvDeG8$vCUi30&DTywzc0SVTl| zKwG+kY|HBNibAEULJ~JmQ`f`a!bfuDx&NS@!%+bt5Q* zp858_9_*eSG@Fv-__Y1&v|G`uxPz;@c>O05={!*6U%%U<(G{a7n$M@G+R`(KgZo=I z2?JX1PASK#{s0{xdh4f;3Md=jboXe<@s(#CEgyn{H(a?!k#ji@QnZtl@w3SuI z^BV;w2qukJSA~HxK6@|NOa6>E>+#UTUzc~e%V8%Lh#fK7>lA!Gz1TqH7Zp%RUq-PS zLosuMw*_)Fl?5Scb^c`=^FSGyug6#C@I+(9v0KZB=xpJ#T0k6c>B7 z1Et*PeDkK^1za^Xk3m)tu_LCq*AlIu8&FeIvoU)z7c(;Qai+@FYn_9m5kTIi1`q8KXWV*oBH&@cpa)`H^8SXRyH2Dr1m^R^o{{933d%N;3*N{0=Q=EEQTU!ig zpcHj-vDi`tPVG#9Tld9tM#s5*1B-#+$&*&l!hxRmHqnxEg_3!%>a1ydl&OpxSW&r? z+>-oXHpT6ogv&6usrz}|!tCOCEOO+@udwe<#m2)aPP=O4k8dOmZ9m;U+GCE?(ej4Z zv3di7y%NZr{kd4D5~mJxS>{NEgO*Bf=O(ICeHF}feb}n8jEKPjd483NC0PtBc$U8l zin}Jezi3{);U1p~801(#)K}@NKZ+F}lSSa^1KP3>@V&>c2fpRj$Hz)@=q|6Xhe4sx z3s*_WMwm1^PNEz&!$-SFu-$BTU(dxf@&y>%8US@I5E`y;=B0x`Knh_rxceuw+~qG6 zZEWG=KGLoc*EgndTdcC)K-JNaD_OvP4N}c-fns4}`&CjBC4t&(O1eKQTkMqMUVD3N z0Qn`I`cXVe;r>wS>yU5XWM}8Z>U_k&DNCgX+*>ZbojfE=qd!&TzJLIR-7GT`yTU1K zd)}tw+xg_Q4p5@gOm#ZTeK8AHDm+6p24Fka^xd!_wE9Gb>u$8&C@)(ouPp0b!$i(+ z6T!wcikN-uzXA0sJnnQa&SCQ-_hqDyjGN=rH6$Ho(m4qMRL5t;*Ic#pzT(sQ#SpHv zE8{G@+)nnU`?;N+w?A`+AN&RI2frx^61}f}uMxyrga9E)Sm1perl$Iu8hj$62#}13z-Jg6#TKTcqXQWZ$`k?nG%{sGk{oqT zUPZOL$wCJxkx(p<5ava`oS?|-TewCF05Ud^uNr^UuUU8oZMk!S+@byvA1?zGcHjt7 zV&c&9@^bi{pLNV)HDLCVPo4;<$yi(9NiT#|DdUb}Kit=Bn#fr4{@C$tVd2ssvEwI0 z@$X0$>TU}p>BI@`tw8xq6{>RT)JN&o#cVoPU~3D-TL+rddeEe>;$lwlFAOrB+R4eu z+qQ6MX`YNyzVaZmy@AWB>DOJPFEf_5+M5n!nSKIm^(99soJl?IXH5w`GnZJ zHSR}G@*3;fJvFJRNF{z6?_N6`v{>>edsfFW$S59yRIaGd2a@Q}>4T*yIiH`dvAy-* znYwh3d6q1dGz~~Z?q1$OoiEFZ7yGAX^5rtfVujtu^2@Ila2bvA1h7aufXhWiQ@~EQ zva*`2wx?Fj<4@yAk8wc*Q6Hckj<>xaARyNnV;1WJ#zecx4>S+7)OX&DPD^V6VaeBU z&>i9j*zpw=sye=9;F5O=C=Cb$h%8O};t;;ue+dX+#`pMMfA}E1xVOR+0dzj07)5vK znfzP88KzxjLkbdFKp4Dq?!+Y&>V3)$x?okh$^m6E&=^m1rVN|p#mfSSoCcf$G&Gu_ zs|3^v4~L3McQ<=ei#_*YF?fSS&52}|bVLfT`7A|z8izEP%bGos`}gjRx!XKqx-z@B zBvfPE1bef7V%-gtD!Ia;-X&Y7u@^KT(X*{~{?t&zhVQ}Zo%2vSuLO!nzx5W zEOkufzN>cPf(dp!uyqdsjiY6HI;pITdkY~#;)o7ND)Wu0t3=t1hhI#dnn&iEi1$zr zwsfiy8fM=kPuUhdNLQ0x{`&kXH@u@^(YoJxnno2TVHHb>hLo>cw6!QVcC z;S=x~#Hq4t}3`8~R(|41hpx#nC~(F|f)(rx(O#*}vxuzI_oNn4Ba5 zkbM8Z0GEJ3S9|-f(Y@habnzp(=lPE1zk~&1%N59D@W@)uRDyo9^>n$lzv{%T?rQk+ zrSQo7?I6phQIFIyWfI2wN3>MfZ{ECl+2-G_ATJLjH{(khumIlY2+$aV8?w_7VW9a2 zj$@#qmYXN{_`kXSViuEL4kX_KI@6noU!i`2yo9^Qb=&&U%v8BgF!h~X{)D^g?!`_g zve9=8sKUDl$xm5o3=~&3G$on`o;-I|U<6~BH4p#(eRHCi8psw}8O@W1e*dyo2!Yi4 zUh)GaIx&6OunQ^B*{%pQ6;w5t7r;~e=cR(zHHC+Vzu*SRM7#+_`%sSJLnt(6_DzGA;iydq)wzom=DB;>j;N5xi% zaueRSR1(}QCb!~Fuz*iHX6f~>iB+mI)%2VI$PS3D)PYty(uIY63(FNKkUQ^8^U2Eo zGEKYPFv0j)rz)+E0~rnFmG$OAjIPQUaS?aID@1!kzIjiu%W!v$KiGQy8v#wu!+ruz zC!Oj1e_Q?)^{;bkd(6K2i2VF~jlsJ4Kb&Khc6ReQO)%!*8oXBvG_3705^r=4B%?!@ z7ogw#>sTe^Hh%Pi`+InRw(;kap6X@mVdizP`1UtbU-nAW&H*N7M}9`|^ZruDimu*@ zxM>|ZJ!;$>t@{7~s1B!4S<+O7ak%*@o)I#1|EjZ}sE?r#5 zk=xq6p--Ued~gWo*wnEm>1%;UbCTPKJ(ItQdj)G@@$G+i*_IWu-luh-ch9f3R4L4P z+GlERa86eH+(jS9TJr1(p@3PFZ~nM&78-t}Ur7@@+k5VYCwk z+7DXu!?r>h_RFG*!air+g;PgjKDHR-mFAJh3DZTVNVmj3Qe zPx0N3AxnV*Gp#A1BTU9&Q_G-_E@&O(r9QmT6MoZ27gB@QLGe*tmS2Sng5wkYJH|%C z^7@@eIqFm)ZLkv`Pb1W$;>i~S)^mT)XY>(KQG)Qajh;4l`=iyAM>QWK z2N88hU7W??t`s4IM+VMC<&86l|8AFJ2cdoK`s}-mTkg-FIo4w=KP94x7{N$hDIKMu z2<}sexqkO0)8XLQJmeXc!pMl3z-86kzZj*ss2%;7TKFDf} zALmn8j*@l8G}L}_`4)2}cRqykM9lp09zCq3oHzF5N3ru{M&iGr*8zGLRL`FrEAWFk z*JR((tX>RFw~!0Y4rgd7u!K2AE61)?gOYsfgJ_%(goL5^nGqJqrRj6~ zgGivt_L3Eg=&DR^?e?^J#Tg@-{#|KM6R_iEpOOn{KjO!w<11=>*ZoT}>claca=o|| z(sP6pTi<#*Pn%*8ydW~Gx_&2#Yw)p!erJzsQose#ll1EZn+K22Tq-x|srE0RP{eaFn8x?Lkmz9?gr#O`W`y9KYFKXOlD!x}=J9S9ZKJ+*r9ILzDaN&ILUUALX)cjID zE%jpugh03A?zzG+M$MO}^P@5XiM0)uVzxJ?XVyy1#!%Ms0 zRXhJ5YfGYmm6dg>#*uDwlT=J2K0TciDE9+x+%(`IA8S$G&X@(0h^Af!?NvBta}c-U zHPjies9%4n&x*_)!fLC?>+XPY;Vrr8$epyK==Z%`dvG~Z+njo6?c`%lWx;s3BBtEK ziAu3rf|wKq%R?%?j2c2wisOz3_O~1~{z4uKI_-^>UOL4Y_a=o>?5e!=rfBW@m@{m5 z8u6Tjt%(fMOuj5FLiPV(a%#TLj6k`u@~FH7^Xkf7`yn-mtaX`9$E$z}B2|{1g zSPmYl;A%8~Ct54*U?;RNAx}{7mr$5I#QYA?+O0IIJ?oHk?a$_@`t+0_-k!#^HsYPE ztKPpu$>Y;ot$8d`Qt~B!TWr2nb-lBz7|Fp+dy|0Cdw5Qfh8ajjocN!OZT3qppB2bn zYbROA{Y+FCn1Kl@%HZ}As2kYZ6ZGRlD0l$AMwu7J{D3r-N;^3+|TKcllJQ;~QU z=ea2nf(-~AC2#c>;iU1z3h7{#G;j9d^x$CswMWr2--h_=7uudDb|NvKANney9Y)q& zHw|075$#LYn8u5+!-f&y;y;3vNM{~ibRrZT#gQ>E?9%0H9tl>+8`s5vvJl*~*{#f+mm ztWTG6VWBfH^oAX~z_@C*QAN_&3-|t}kQ!m^$|7RApw$V`D&!RuSeQ3NB&gh~L5ni+ zt9?$8&*bqrb4c^}z~6MC9s_F@1^IDW>Cei#x-;8cm|fu0X47LFkb*1_iogW#GjCZ& z1-lK2)P|hZmQ=Q;5^T|~CGtnLI+-7(NpzjEEH5LGUeUl0=uXYKgl9_=R~cI}jy)x( zxl}uXi;WsCbc}qBvwgTeSbnw1juTLuN+&Dx^K-LKmvKsX=KyZEuuL-SbgafunmnGX z9gTod+Yu4$cH_b^zeu!;1MP`+t^PV}CTq;%QR$#%?tF@SpPg?taGop7gUJy$O)aX9 zHgF<`;a`=47ba$(H?6h1i2TLr%EftuMi*{L`Lm6M))F$|guQD2*WmGh6oET)lN+Tq z9!5Wg?v`!G6C(aF(3#*zOicS&{%A>cH62KPOc#p)B<~ZNziY#q2^uiSKS8Vy5h5TU zm}*4PrJjm7uX(q&%3?n9N{|l4t)9FF8Sh)V@dx{bn(r)3OnDuY8@I0-8d8Q3arWM# zHlUShZcU9=0T4K_hvWf8Xpj^986TflZeBamH{04OwP&4K;R?9=d$(<6w^a$26sOUKY%BiJuVojZQJ z-{{L!(-<7iB_Bn{9)vx-`Kv8j^LF`LRp{wJoBwxL#K@M{w8I{UmeSr4qe>r>iKAn8mb*^Amo(cOBamjPfyMb$zWNXS*Z^WH$$xST2Y z=_mw`CbG9qawR=sOU^n+v%Rb>^D|F*3hqFV?CGGt%&l^_Ml%H$`C$D=;zPx_4Eq);-3Kn1uDg#(b0J#ASYfyuB!$hq5SGi=iToC zCxCh)GG;EyvWW(4vkQ{#^ytWz-{Ab^OCAc%t*}VLyr;n+HFt1zG+2NU%kMiNJUW)I zn*XP60ASHcA**O0cXqP-9Pi;ilb~QiVWC_&86RlLLjY>gr>gA%dw}=|(L**Qb>I4U(Yvt>KB*B+ZU#y|DS80K7q~vS2^Dg=HVtIdk=oS8-JHgRZc@UFd{bj$ zv~}V+cdf`|1uCq*zMFg*Y__*QGFpgpZU{+Tj09IajV`QMv)&d+yY-}#wx#R^H+Vjb z-0K$3a1Oo5-4NvOV54RDfX?!GI+CyWY20oVt&Jt7vAtIL@d@1DCp=B3g+^ZJS{=)T zAg*MB^M?@KC)y(UP(`ck+tu@MnCnU0>Em1?lw;x*ejF~`2L)eG&`zu-iK(`2rvYrxe$O$Sl!pwsd?TYgNjnLOir0YvDl#$8yCM7y=nE-AM`CjE z91Q=E#eK8PbC3xD9cV|BB*esmK$Zb?;p+(}9X;7K*pmmT-Fx@NW^IT)-;dnc-rfcb z7zQ?*xQprWv9S`+WCb!|1JV|NpS85SJhJvlf~J`o56@jOT^#cC?O*~IC1`@oH*41m zV02tu+=dG)=|Oq@%&MwzR@`r$6HDP%eB^;L)~qFvJpbCcmo5Wv3U1hO}$E z$;SvGlUmo&@$&0CsoI=>0Fhbn=1Kp)v)MM+!7CwXF|Z`?Ob5rMN$=pnXYl`oloM9< zu~q#Oah_AhxU`?I?U(x`=?7SsretKb2l3;oc#*^0AE@&KVjp<&Nt~!m)sQvHoQIgE5}1e40Q_@sO{zr06ynRU|QZE>)Rz%edpK z)w)pgQL5$n`4wgsmQ}!}80b8|xLDlYE_~41yK9*P!Tgk+tr~XP2MDPEp0TY2?MfoD zvkA!gZJGxMiGeu)B&gYKPg!zed;2|&i7`?ipH5>-OG&e}(*df!?rx)t<4ur|S8Y24 z3}aZzFqN*JPb!zmHYs;IJflQ!Q`OGQ>&nT=?z(^;DWHLN=W1WP>*v)Z z)WFX6c7ljI2k3naAbo3VYavf=b<{-{wZBnoFy=rJxLs~=vfJ2Vpo6}XZua^eKn$v0 zM?dl(+OsheGq*Z;Jr{#DxPj33i%$)cBMD}p?!vY(?R?#zYJvIjAW{I=-L3Z4MaJJ@ zH1_0FOH!5JNv+2w-I0f7b{d8@mFy9sWv4v~^wPoZRbam~;j?)N{16@67NA2lTKN4l zponen?05h}C)_1%qDu%M!b+JMrvYVI3o4LqC@#h`HpXeH`a{}q3R;iU+ZC^ZWWm+o zjmprb#3M7F2OF@XCH)WUnZyJ^5lEP3>lnE0rJ*l@p{p#0!~bYdGFU!k(Ha<;}% zb=?G{Zb<~}C_wj^aB=~~^?>Yux4tZXyh`KKVAY+d+S;p-OTY23ZNk3Sq^y#qYofa1 z(nr{STTeEM&%u&&srYU2zskR34zQc0s{QKTomY}$ds;A~Zx`0+>lKN+885Kdq11Zk zX2uD9{nhsrga@pe$(d73TW#2P-0>A5qSpz2K_D3LK@ZfRQ8exY;^#-XFfQz zRoBy--CcL<^09RV=Qx;h41nF`c z6)5}ai)lurw4HWrhWpH>hc{6SdtK#?6%wV}k1WJIHeRXKf8SF{AW>EDAZP9y;K=dc zUjkhf057Qmk=E-A+E_`xew_o;U=;9VDbT)xD{pll-elKb4nFGy?)SEZu}B#3cWqw|6JG{gK25# z#Ej@Y>Qp>z08-7?)sp-}hhXNjqxT=q75VxGEbkRB11YK9(JqoJ(c2i%Fd-tg&xnJ; zcftyb$QO`0<=?$y0d3RU1_w2vRKR^>kTbYL=9WG)skO4ggKJ^Y$L)J@Otg-JmE197 zZ{OU}@fy0eyxbbb$pJdt{R9qMbaFE2So-Xisn}Jl+6B4>as%-qploqWwd=Xoe+pVM zg12y~+$|~5KD*$*G4};&O$}5X$m2Vl?HdCQERY2n)seBVUt)7r*##2m23Y0i}UVW#tX=e~Js%`;jK1 zhg&P*VM((7L_a@mbV27Au^h5d>92xST%y>8Tcqz(!;BfH5!Mcotul0^9v|AO4o z%ycAZ$^IqMQ070b0iY+8Sx?wKd$v|7SPg~xmK$t?mY2v# zko9g9$SsWn{90(HulHPyWAx&pk%4a|SAxA#L3B%yt>;q6x(IJ52k3ICW?fKJWalVQ z|No8}0J&dPUcUTveOwvI%*tx-t1NQw-Wj>imk*jC?2}a9W_#c*r1HbC^&GnJ)Bo^^ zt2<(hD>n{x;5ADfiEl4|k^D0?MREMfwE{GRV&l#oWcK{_R*Te`bjK)Sm_x>35j8|m(jOLs}bC8QhP2S2~>yZ$T}ORsR@ zIcLtyoS8j)XEGFsdgn}VIqv-g2%F~09zY`h>l>{1{r?@J%z$Ws{ri~y?Bq0FKhTaV69Ac@OoANs;4!v+*V)jh@Rem&H={Dqh7mDG8u{k5H&&1cl>XD zCfBr%h5q(iTM~Mr>cS9jlDKhNre%105(*L7z*Z8_d^moa6D4U0N65; z1pPT$@qn1FFQAlJL(SNG?C4-&nL`)I1J@A-bmvW~?UX)@D)#{e@W2H~OrJ)1(+_1a zzpnrvfE-_7vs+)kzalW#leBad=ouHmb?kY>7t5_=n0EDYP2R>IAL!|83p>HXl=8NK zL}n+5qt-R7As5HCcYmZ-vb^W4aC*J0aab{$7CYeI7TJJW+<#vns8N+@Fnae7!Ew2% zj+wIuqJ@4)_#^tXr9oTGpY_}OZ-n~yXme@7+{fU`n6~cN(+7djyKK39p zEE{c!$!vm~lTib=T7@RUh7R$u`Mq1Wt(hc2`7SRWdj@?fm#6vL4Uc~ttL3Us@YmcD zyhybV?2i5G{b5mhCPNPH*^>MpmtAFcVmgSkB`7dq%CHXwMQ73GV$$~(zM#h2UN_UI2ON%TNVS1#;oj_|6MLSH>lL)J03|-eD>C%cXU3e1x z^o~9dPzDpoYd>wg{%2P>TOF|f#@ciA_g=;cVVe<}G?6u&_4tIGqW3Bk>Qr%U z36p=yHc{bKDB}$*r)%^MtnO=I{wXk>5anE~uNVn#pbcQH)FEg7PD!=G9yOqIpc(VU z_Hv=0j5JLZHA+7zU1O#RvLotwT3bIRr)u|^>abC|wT4ZKt3Ro(YEg{vTYShs&@rD^ z`{y4w78@Q|=W<&F=^dK#efS|Of7>ecdZ-{2jYrds`TA+YG7@%RgW#pIj~Vk%OHEs11JUx(+a z%OiIm?K{$Dm_F+y?)^WfcZ$!4o|v?&tYIai>m9SI`?e9m7jIbaPiL`p-%#7dHd*3o zXn?Zoc(vz@4BB%o95KE>g7y&fCR@V^{5RD=>_2d}4qxzcN!zjy9aWcG^BvW#c6O-f z;I4ep-A>lo^cyP2zx^R+>{0#=Ck~1}Ox}mBW7yu|9{=i%5GEhf$FQsUKFd9>Z-*b4 zc^n6uUuY4%WX4Lyou_ud#;!CaaXb*BW!=oax|!30$@KT_R!mNK3PJnhzW6aMS)Io` z>(ACj8Dh)N9v*yfhu`^5!l*R!fgJ$$B$Tp+gTAqD&^?rkQpNsSht^$m&Eu9Iv^WHt z)tv~%v$)AN^d7)w0n_lY_Lo>R0&AaMU7Y2Ur~#e;pRJE$Pv1L@YUvL{%CHY)LNa1fqzEBLQgszwL$!r(DqXG+8eO`= z_PQI;Qt7M>cxTzkV6NB~SWM5HIxC7Q@E+_q)>ibU_BvKGSWKsF8^1WM2>BO+20g*i zM6VTDqGyy}Ohw`%(34`5o37TeG4pLgH?J|)KxfwAxA@l=Pyz1!uj~3FA*`}hd-Iqj zN&##u!Jn|PTW}>RJSMBj=L2PAP2cQQ7T(wa+7nU4b-s==xQ9SD$|O7;B5?h{E=Mp- zr5?WM^pZONmbmrU{nwBE>1xIDNYVVUUGhRzDiJa@M5Up}*qZ*wym5Ry@-%E6lKWo! z&!)PcSg!@C{|c%uDJ$;phDV~WkjIwFuKs#>v>{2WAB&t0rSIP#a$hAD?r7`5EpU?8 z@_q2Ssr=`@wz}v04elih@4(0-mbUwjH)Ur`zMS#Dhy-P5RzOn`y4te(+pJxY9FXE) zocwT!Uwm&Scp1tpHePKNpi&Y9F`%F@0{h+(IEqY>r^>L>o$O6cNy*<5NPj+A9jVk> z-{3oJHs?x@Q%g;jMIMA>bCh7hL{}6P{0f7bBC43VK@93*d56NXc>C;e?=ie=S#_;z zS=BxTdN~EPI0P_Onc!n~o$(fL>Px10xjs`yczUu-m7(8Qul(M+RY4sC=%50%ro(bT zbU_!n2pbU!40T^Ce`-UlbFc5rG3`Uld{~L|HKZw9KeTE3V_NStxYXUWD=mvtA6R0V zHlVmX&B`vzK}7a6WvzjTBU@$DReu~pj?h{%$P#t0eE`__+5GQ??o6Roi7xv&5&qN zWaXx}9prX6Jy*_jrB!$@Y?mtd9;a@md?QE#aS*$~S#vCLp=XravS@>BxW{zKSdM-Q zWY01Nc?@VWwZ!RWqvhSZVxj7pu;T zoozk5IeOdzTRwJO$!;Hr(F!!|$?~(Y_PX_mp&n#EmDvJwj#jC9|CqPTC}x>!+X6jr zyQfM7bgc~~>6rD(%G#rjY(2tF~XDi@fj@QRLgU(9amNl4%ThRv{;DBSe@EA0YF^Jb`R*oc%Vg5ZM8IF5-uSr-2rec zvXTkP|1HYVbqYWB&)kzQ9ZVjupfH0FbO|10TfGYckjnuVvu@KU7m8sJI~!6)h9h6oeAVQr$bx8Yj@WrV~}16 zNrl~*%;8-f>5*);_KTP;aG2FP89h_YA5XIfFP8Q+=o}67ei! zXJTVlL|DXtnK7hf;iD8)POUOFFml+a#{m&19#+-tNit@TKEBLoUbbJHIWKiNJiYq# z#jQDw!W(gX*%yBH#Rug+wt|khQ``hbvt6^GH|190CrtsObc6PX^w?dy2vc0~~hA>=W=~v|Vwzo>k?(XW-^QWRSsB^Q< z%0)%7`TpMY_KgUG_SG8myi#<14!Xn!>k9CfK)l2G@eIp_FL(tfM@$?nM#g5YtJd`Q zmsLqaEsiK@4b~cL)gP?K$2C8?29@(9ymxa;ihiOPisQp=)JXUV)0NCXnEV>b@d_O5 ziiOe%M62N~3u%l|9#@y`Id#)_e-DemLt`o}j?s8*=|6vVlaCoj4v$8X*_`f7yv^qfaX)sS^dq?XoF-gm(277xN!lLs;NBjCSMbBDzKJ= z1Q2W2N57wc;(SpwE#yTETlP0!P&ov9r~-Y7BJEJ>tUNArRH9lhNlSESy4jKeVp4%RegV`lGx z^NYF-M$1&YF#aadEj*a7Fyy^FzI2;)#ogyB39|5{Ro||Lc!C#}$vDiMH8y|1eSkjG znWiNI++pLSU!>2bHkNXY%4ro;Q4LgbUNRh7mbDHt zXV-;NqSgmsZut!oc6SSQc4w(lLp}&R-e}ab?dxRP+$^m}YO7FVJW)u{B%jH&4qu(> z4$;tY0NR(douK+HrKb~Xy9NvO3e1Ra+(3afK52uBM;psh^&n!SzhCu^)qD=XOfu;E zG-E2jACrk80uzJnJG*N(v2X11fb|B3WTm_=jzbY=e@cuYeWARe`^UtFqrg~0;CK2C z%5B9QlXGfrChWsJ#giSy2V!hjD{37P_px5rXa_9;IUD)&6>adYHuI2$ar3p6TiS5R?qx~>ix?YH_Q zJ+2O-Lqd4oF8jccGIuZ{u7t*-#P~fkbBOaX|LlIFt!)7f%ieCq*?mT`8?L&#sg(4Y z+0oHTHPAgsA0#FQRqA@;u$uW%Ht@KPIF)P7s?h*jH(OT~t)GEZ*}w!;9TRJe#32=w zlzIX_pg0|w#{hG1DM0OdrnX1`!2DKB;SFdIumO}EkH^Z0#QgEVaEv6Cu+Ag*?(W(i zfd9_6*4{m7bvu85y3q-`-FSuiHXFPP1eob&o1=$i@8bQPqO2Px^Xb}DOAT$tn;(#m zkkyt8ow&?~X7$dH?>)Ymn-BjY>ogmgT_FFI{b4ZX)xx3k-(tf$=cSX_>QY^QhCrzK zZXQHkp*~VCMISk@{ptY~D-8%+e1a8np4F!pXw4!tE#voMph*9*uBO2i!c<<1pe80W zL5xvgcdC;3=UvVcECRly%$NbdfL?-X@%pr{PQ%D(dN_^w-Nc01^F}5Zg;+{&-8$-k z+v`pNpi2e5Xl{QZlTX>G?6=vJS9*FnFtzX)85k%+sMuTwaPPYzL^Q~x-$hcgUpFfk zikOR>k_f}Z{`xf>OF;~gV0i2MG#J61Yx{UJ9~gLLe1Gvu!ong=rt$&mY7%uc1VtUb zL*VrZ0S+$g?(sHTz3P)%rH)!bR*T1~rsfj8=0z5_z1QDvG)Tb?_Tmb)(hK?p)2ZD1 zreL0(&`$I>+O3Jd`g9I%>mXtKU zao5t~4hjn5&NT>0L<6}u%Z2Iq3UIl8ije)ta;*MPej9vLYFy7zs31XHlJ zZws-zdwYI+mcwOxvV6_GRrauoOi1vqAQ`-~(+5~EE2*Q236^$)h~vIoieycOfUn?; zIPi)oXu!zUR{pEsmuTD0XF>5rSqTYXRu@2EJcv}k)rAjunyW+fau%xFvyFvj_9O8A>AGf-p0J_0IRMm; z+S><8#DH~&RaE@8x;pp;AVZS?onFANdsOO5o5yqh3zBcn=FQ&-uwR4dt9~g>PYnD_ zJd(|}RbM42H-qGDN|K+yrX$baKdj^M&z<)pe0K zYLE3jH_g(J;5Pb~FYScs36f`-zN**fl&H1`v?r^<^cyr2rjq zz>(=~Yv8~4eI)|wk5YzP#sNl2jQY$&`Hb}Rti@#=H_MI-3i$=bleMN00WjAKRtH>m z-+ZmCt(!e>E5$fFo$2HqCw@SN;IYE|Td@Bvo*y9Rs=6viLc$OfNC~dKc4*&Wpy@Zh zI{Fr&{pbQbeTeJ|yUc~l9O{VRLa&!nqg=U^1mw5u2*Qktl^PDRMoA-egxP(N{^Vjd zy%sAmU^);^2b)8{lB{;DxiL!4`Wyfo-FCC=YcT>my7Likgz$;vdTmkXfI|Uo4Q_m{ zJMc7m-p*JC24oU4Z8Al2l^PnXYZa=5N0BdyQo!MRydnI4yf`%mWE6h0vtWQSAYy9^ z?c&15@%o6(KfsgOXh8aS#Z`f<^gsQzx1;wtj^SjvhT~KAUO4bCXOSF*p9*RN(@$P_ z)5VR()E@rGnDEf8EH)Q=Wb3?V+XJtyb{VmcD9PSiXeb!)v>5Oi50ADx_MCKCA4)H7 ztjAJzP6jG|J0O2wxB1-!G`?vdZI260W{dw^9Ut0OpBI-9a9^p! z$;t8FXmFmaXkP*;ezH^M^1P@ujnSBeD4(2n8oo6>{W6}$$eTm9URmy3X>q?SoVtfO zn96HaTuIoY1fo-7Vq$*5q0LQ;DTv?Y>>G2lzZ01*MGoDwF;^6~LQ zbMvL_6DbpSgaV2lz41yV`VV$y3yJ;h?QqTx-}DDQ`$$=8 zAz;u5GCg zIx5Nx&o>&3rnLR!alSFb&i#rG~sHS`817r6MD-u=zdX>*fQ=Ahhz1=!`F zBCy66tS)eYWMexfhKi1^b)?+2zUUs9s%JeZsolM9cE6oT<8B-9>{6Gc!cBGj9(? zzN?!qH!_=gTCiK9v6N!Y;|B zZYT8u@9bV*yl+l;hOz~p!Gyv>LZq!OCx3wKX0p%j+OfV| ztk3%vv)|T!!}m3AiaqJG_Aqe2OyNAorIho6QLu4&STe=YOf{+uCgx^NonUSzf&n78=@gUAP6=WP|m# zo23hQZ=&83mrdR3``GxH+wx41c}K?Kx241n@X!UfbSi^gEwcC+)J;lmA)QqDzkcek zj-f7OPGqdPUC(KL|NdQ#j*NQ%ib-FX|qC!%L$u7`ubUt-cCY88_pkVgVU4}*%pU{cz6&*xAffK&Hp7YC`qlEfR9HmB&5|P0 z`-RSqS=gyUH+K#SAmfH+ayOC(GJ^SDl|)-_0^97XD~m#3FPXEuJo+au2JfpIZFW8N zkE!?x5Wj%x07z@H&$|qYF}n6VWnYq`pfW&B@dwR5+_f-8Ja+n@Uek`PZ+u!`KIYU* zglB^2WC8m7@l?%ARYsJ^Hu7SB&9k-dp#ji?0uEah14!}+GiPg)&NLziW8P@3y#pWB zi+r$w>~L-)9kLDIcmy#;$dNyS=nG=siARUZ;wx{NN>9^Z$hi!$&KYb%@x1&@rxkOC zvoCn8MPQB8S&08~z1C!H15f;+q(VW@g(GPkoAE+W?45>|;dOTUdq(mB7V3>tR9Sx$~y*cqJuUM&dt2q@Qw*=xOya^~%67VTN>Huhi>DR#u*a8a_Ecn^K zsM=B;em%xhUGWTORDTM+zlgyUyzRJjM@gEDAY`qGAOJnMgi}Zn4dUu*Lot<7GuPFq zO=VK9hOf@d>^vk@W^IK@W6GSYC5^UQ*C?%|ZZMkYUz{QGxFz%ld|(=H2#5I%+Wd$x z^~)RA(vB%4g-2fSRbt+?bd6If4h7u{H8C1*SO@>)w_#Vtfg&?5N|s;7u7&EH%!KEIb!E_zhwd~(Ekb$!wbDT6QXb7-={2AyKA8_(j{r5f595TvimBS= z0AITqePLy?bPQdY`{}k3lQUP$!rsoffW6D2&Vv*2Kpuhcj)Ndm0;Y^I!tO)QdMb0d zMP>ENr^uTFSPXl*l4dgVW-nG|Vo$5P!e3wH!2Rj_u+Dz*X3z8&&{iIWDTZixzfwdZ zuLJzIk0(9Dl3(PQ(o$(kLw+>wHGTYjODRmb{v+T7zg)LoVSQnl)xP4>Bcgmh_upL! zUl@%8LMzKrjaR|G7Cq97KHOEY8{={xLO;1v5&LlKC2!m^Mky21k5N2R&L8R8y~LHW zpP{91aQzv%KN4~&ktsz-U6pwEib=dJp5iBHZ=D2acK&pW?1r+!_&ISo)D4!%_sV^f zlwl=k0ryo1OQv?A$)Qu(e)f)KFmvWFh;iC5I3zc-{8-mwL>#yY%g>G}*$0Oo zy)E<|Y{j?L&q-K!jAZ{3SiIPAgiIqxZ~W#A5~1cvQl@I-zZqjdU69U~{bLc5wLX@rZHqsJJmH38iR$ghp*e6$QH?9SOMaYFYkbv@`73 zHOJ7Y!5}q%8HUXL;;6A9d5c@a$O6d=qmuPQ8~>Q#uWfOI7HoF|iut_VuqsS(pu^gj zjrzPevR_6~sFcg2u(52+35#EG2HcazmNLldS%BgHw2>7kVmi12_-_i**K}-~bwA<- zHfgTYo_AqneN!d0#*hD7mC|U&d{zmrbj;K$oky!R#wJD(FNXd-S8p+|7A@ z@)L599kxP#O?9v8iDt5u)gE!fVmTA8gW@6EekZihkB-{09{RJ8C-cn}_w{XSS)wxk zf`k)ABsdn`;L6VX6cX}r2cnN|SM4G%BxeYWT_1}BL^@>;8@izL z6fgcFB^`mg2~S!B9SSl0u<5S~A7B`?B*#dvdoxs<#x#R$Fb*;nnC|<0rlIBZcLd7I1-Yv)-ScL6YtieQpZ}slkI1v z)dbUeY4y#nGbpceFPkK8;ss1mN8ULJ{w6?RY-0m+JL$rsMkv|cSn|prF7rjm+)7ui zxjlu3YL*-8=0+Mnv|u`4%B$;(pVW+Tb+szej9n@qbpKz;8olKJg6naP5fz^7rjqvl_>NiHWE^YvwiLR<=o89nm4Pa3w+3L`CX z%vRtpv3VU0+0-W2j3G;VL<5S%mISMiDb6%MvQAskjCami8)|v_OscD5$`Y9}dmEfW zkE+lnudQh6DmZ5oNvDVw!df)Sc}+Yejn1~JYV?OcxtYFqL(I-b3C3c4n8zl9j9=PP zO(-j8z`}l)-b12GsBS-Q4c4$w(K4a`98N;JW*6CFrWx0FN{**pum-2vujwu~m&m?s zG!+B-+vE#+C&M-$iAU&}u^f$}R9opd%upR(1+SwyJ(IY3MX%)T{g<|R#7}r3rPSy? zHwmD0WMj4WIy%1!DPBYzzn9X+bf#F5TESISX$9QKG`)5*LnmVmB3)@;}% z#wTVSky?7JTUz~nm_I-`7xJ9U>i*k4%>l94S>Fg3lbfRA37!q%(9M4`f(J~_{b~(1 zsxK+{hO#4-hx7u=tBP~R82+}fIa*b4Q%rm4&4cCk-%M?E(O-RpBIVYaG#^NZ;d@}-T5KiJ zKhZhB_)67-wmV~5F$bGGgA(tb9-^jbi20^bw8NY1nw07?SV|ZhpiS=-cq~1}Epp?! z*_i^T9-@HUNVtOjG$1678fKMOW`k7@ciR)7ETy8i;wR3e7NUs;EyM=}wGZY#7e-Pg z@WGJQVJkAzg-zYFBynl6?Uj2X3MhqD?g!^`AHRlET`%+8O>jJEK{QknIor6XC3$}3 znJz_gdY5*^;{4Xv`Lel+d9p4{h19M1s@J=?%=!y>NQ))=3q|9zf&9OxEttXAfU^i? z%ohRQpmSK#08h?$V|m0cJ^qz&An(z5_&r)Ivs*`s)B*V%0?NsL=20pSOtATNyPxco zq#rRVSPcTmSPSYDm&~qGO42AQd^+53%i0RbF7_O%bl$-wy(%HZ>Z@!dZg!&QgPncoFTHkWsO+Iw~9>*=1YERsqe9Bq*0 zseNhQ&yRUKk;|`fj#cN=tGzfRTW5&>-Vpyhw)AD)oL%thkU-H@tMeW`8> zPH)3RDfjbQv(q_7nRKLVG96g;T=Y67|C18Uq#B{AtV(zZ_j$(n6j#Th!Pi}TG}<6% zbY^cayOLrYUzA4LAPq(i`KJhYC%*QwoQGy+XD?h4;}$ybl|8h>qS!9iIiG=j`cbOO zQQ7ea?L^>lvf*PcyiHxvRI{~`e*MC@sxs;Hi#6P;DQNsWa$V|iuejv=Ura`%9W z>IiXkN8_89jYcu^?xyHhCu|!Muq(;vxpfkp<-Skw&Cr{KIKdylmgX~dek{W=fA&}6 zxCj*BR^COz)dlSMT8fzA2Iux$Z#xKe>>;E2C9)ZJ zvKE)CZ=<+b)H6=X8@Q%MbZ&6&VWY|ISHDd!pl30~Om;8ZH+WeHWD3P1o?f=(c0@D7 zTo!oo^9$K==9itVu^BBMpn|Ttu5m@a;c2w#}D*VkmGulBcqIB09H6PB)T0vV)8 zoPJe$OPUE3P!rq!MH5TNs9Z3SE}GwFPL@XJG!vZA-rU!c^QOpN283;?%i$*HgT}2> zUJoFYZqv6QW8Qs#TsqRIbh5A=CKOeT!quud9v$oIG{rIc;jJEzudHT*RQ4spxm0hgedftM76;ecmcZ^oiF!-A|UAuBC&ALZlZ27KAE>ei2)PB z$!>$A>&dQ@S13zxibaeW3nNdqevuao1bayZ8RkXm*R;j^xSQV?N6`imoA!BV= z9Ji=jI#5Y%kiu@*2*T0&+lJ=uhP=74Pni+>-fLClNz1&)sDXCNXJv6s$bP>sX?pEg z?Ou>!Ey>yG`e|C_f3PNmNfQ56BPS&YbSIq7cCEbIP~>HTjJ5_y>+n>FnvgyCdsKe{ z3Hn(Xe??5FB+2{Y)(@3Zunu0FMs+GufSLc;pY&n*~u@%f##;hYKjEq_ePDL21883zw+iPgW* zp6Jc$pUifQCrW||VuW(K#>U33(8r`C=?j!5%c)IvYn0QkQH&+`XnNwre(Os!fjS(M zZstO>TGZ&5g))^m+vAA>dPUpgj>9EIDfH$?V|2{#(N;Sx6g3xUA)h=`mtC0m!z0}z zR;E`fHN@#}8f)hSQrzN1?^^~_v~(auX(RC3aTpW)eC^*Yy3eniwUZ1KML@o=SPgk+2X!Q%fftOf0HL>Al*&_aJ*G4W9w#JAN!c_wbp|X=dZu1n@r%_ zsHxlDWbO5odE+kHBHDd5*Hp3#wsk7E{5`A%Wplme_nSr`h6;$h}xjR0DL6W^q&~mQ;tHm0=0t5 zy?Yr+4Dzv@rzl*bGTvuu!D2{w1?q)b`}sIWP$jk~}HF-h9Q^kz<#QtussP1*`i?r~Z zCpqq1Y_P;`b+O*=>?tjcANqk1MD6uHc^rol>36|_f5(~>ck?~Gl?UEAC5Mo8mN;cr zUTW)68O3iiTX=X+B2w6q|6u|2#X@3oVq*O1JdvIUFe&Q`2pW4z;!c}-^*5P`doQD+ z46b1Ht(-<6503J`{;Ika$_>A?bo9~{koi49`I31Ttc%_AKNQe z>TRfH32i{w(IH-dv1v_q6m5W7nYTeyCE_5Fi+n1F;I$^qF81rAefV4F?01EDgV(3; z?Zq~(=)8UDblKNQ{R8E96d~wOKtz9nW#2&Jw`XpO{=PI$j>2epj(H^?2}a7&R|&nw zl9lABTg#Dp^>y^A6fTe50&*;BQKvj>CkHB8zJPWLO)CZms#4`S%RGL|CJHh`cezXcoJKI{=A4RgUzSG^N3?<&wV`EHyK~3oEsV40WisYeCGNP{L+rHnIgT4 z^>;u}gVOG+&XxVexzST}Dl|Bha366Q?qvT1`LAlV&KH<1bog!qSOb{c#PDavziV*D z8A;cVS?yWP(${^oA>ws?vAZ%nP01fWw6mL@nGWxdI*<1B{&k*QxpI6(NS9DsrjaS5 z<>wPaMp6N)ZNi(Azf|XWn(jCy-KJd4sa7S7L59!A=OXLtN$|+tZjYDDVk>5f<+9{( zJTMFdA^L!RGp>%MatXmL`E`0oXBP^> zba%Xo2y;{wwH#}86zuDx)pLSK>Y`)R);*esbiAIFuLJe~vs^j$K90nDa$H?nip`%Q zlI|gP{}PfDXH>zeSQ|Nf|2KU4gzDNqtP~Cag;MS+fz)j zm2#z>50NYvO65mjv#)pRW2)8%_iI~7T|(o*S=*M}Nw^}%$&uKS=^AZ0zxj+nhf9wM zmziziaW$D8ZYU*-V7N=M<#Oml@%XgwY8~HnCh7v>$|kmSJX2M=EUjcawPg-kQ3vus z;rtYSNJRowuF8H&OuqEYs4OfjGkR$C&Vb6ZG=F4!Ux`EY@5#8Fu`zoy1mN79i}*u> z{dJsq$D)`UJU_i07A42qj7HeZ!|c{Nu|x3XJvz24n5-SyaB7Ym?GRKucXb|c3zUx5 zf|!OE)6)F6XuqsR)gwJlWRfnERsU(rl#QCf$#}Z&VhE5ltv|_iNG^EMtbYfN-5Ie} zy3ODdbrSB=#boEV0-H-a9m}&2roB-0lSG7{tUr-Tl~*R!O}xDH+XP zpsM;i?%go0VEYa&qzjRWYM7FUyd-~Q@2|3ah3kkPgO0K=!D1lA|>pSJ78j2q6q9N`b=LRcxLcjPm zWsN#qoCZInsvE&u;dTrc`4oSR1ccAOn$%mm_ZM(F|KgEYAeC%#zNY{6w-6q~)@b*I zLFK`^yy&)Up|V^sf^4O7GF|Srq}MD~H@!j-tHqeXzTL}`2Aw<1vL%X?NXIR*aO=d~ zr*t%N5EqMJXS~F9{s~Z@eINSUc0V8vZYnbG=ges9uaC}A=gV39RY?25_txxWSAOuc zaN=M;3~RHmaw4<2@ku=BQ+Iyu?Iizu)b+BcAk@Pc5*4X2{%(RihRni_SYkaw(c1k) z0H@srF^iC3usdMeDTdLn8VY5z)AazM4>)Syh}SC}(=3pyPx?Nt6Iq!yO%)^S zi4Z>Fw3m?6JjlM}X9m{4Zg%pm@17LLaN$5J^i_>+?0C!$iX6gKSn4nN?MLG#SYxH{ zc8Z&_9By*1veu)yd-mw@^KeH0KoHp5>=|JPiFnJzM>_{NbU3f}d(-WA-u1y`PW#l@ zPiFFg_E_8%-qG3hBr7Ve(n;H|)g*tcV2j6Fi}LkI-`?%^NLUEI?AF@VTF}cXnOa-? z-j|qH=&A1Q74`xx`4WsmX||D%zsP3*$u+P%-RTN=pRml=mLwCapme=;ZHgha^|;hW z_IrGOjrD|Pgb%=SOg^(8c;~euYWuLT~caZ5dfr zD%)jr$fY;Gf{=T`Rd(IB{`$Vm`?i+cf>|1w774pl2b_w-Khj98 zfsGi6s7ph6)zE6yZHnOsh1*4bL#WkuZ`Ct`UF4c3&9`M}2OAMW7){KKhuDo*#OM&0 z-y*8{tCN!n)_hen+PbIAe%3^|sodG?i02sXu9TZVxn2L-R-DXwo7wM)zrLos_#J*0 z-gY^jmVC; zZ5`13I1=ycEJ?x#&nQRb#&a5urrD!f-%l?!%k1=(^rz<1 zXNwsk+Y=2#9UcwL^J9dlFye-Gt0fGMrrP9ekJT%8f5FsF`vVL=9aS|B%?wYEC-Se} z^TSr`E?e%fS;;U}^&`Vm{%geIec3E4sIInWhlhtd96X%qWRqGcG(0queRYqS>IOyS zM)IQN$;W!LV~UFI8e1Bf|Iim9ZV9^0t*EDhHM-JNT>kp#}uDGS2PQw22zTq%pr9$_+~ZYccXaR z>=UE!rb%5L_~Q6BPwGT6kiU$3aV`aIzQz)i3c#>v)LXvqAg|p%cb^q7ZKhPgUAD;4 zHFHq&^2iwG$0B%bVgac?vsX_9E-zM3EnLYWj-2lOzTXm6Tc`fiDx_-m=WD#Le4+>U zjAR>1AdF~&E;1!tpFbARC6RG5V{>#_843SYQ{!CcRtC)N;oVL$4b$5W*SR(rO{V?Q zXkBzPAEc>#a$7Ejc-ge!O6rK%UH)--l|d-iXy2`|sAcb>8wtAy^m2QIWxZboXI1Kg zIkkl&{U`rMnoNJyeKhD#rt0 zu{j@{dh#fmNn@&+1#l8kU7q2T4+PMiZkTdiWQ#HjN4v_+`qh(>4=Zwc>mZ0 z;O{*}jnf~pGV+P#b4Nc3w248pDc1ehjrN~h{=47V>5ZtbwDclHMkDz-9lFZpkK4OD zzPMp^RhwhlIv26FMz*w*jw>zBu zd?ntk;F`TpKApKZy5wvBr0(GmkKe$uDG?7t1xV|2V z*!v3g@Q{rMPA=nt6wrXNTKXNt=lQY*g$i_Sc~>RowR%Y$X`V=ME_pW?5As$g>;59X z^6gm~;??ks`<~*}#I(#LVrPPT)U|EdT|WttIqW;52< z*0)}m zFHS6We~4dglM=Gvhp>r>%=Sg0qNe%5Ab5pROUk~N4!bAD#KO_*C?Y=+G1ByVh85%_ zEpEB`;1TEZ1dno>=;#d6^pGbGd)y=tF8yzD($ zfdE?3{tC9G=ttKD=L19RRUsPd83tsYe+)HcG38y;>3z1s6Qrm+-7^EktG>nakvw>QfAgp#g8yZJATpg}(zf=N#YnV1>BA(mm)nPi4eI44ReI=S^%|J^#b<|P( zlpuuH#a9Tg!vb?a5Uv(Ly<}Yc{OJ>k!|V_6hC}>J_fjpIC{`qc10+_Syw2A7e4fs! zfGbci0Hd`w-{>&^Qgjp$6eJoEt(>DU3H%2ZVcvfLiX?AiV~w4iztLrujv=JLAq>_c zSI41KrUuj+ZI}Qh0>kDi{OH6tNIzAy9=lq18VTyN-8rGSFFt;hf*!oxJ+=6F2s6C` zl%o-+8`sRh@6U~BwzC?I?X`r@8#pn#`^Zln_ofaL1@9H~9FlS)wK?5d4d=SW$~6LE z`j?hcOtZYPJUw|J!o$OzqsxLWSWGAE4kiu8LBL@04lv%#$-}b^jD-ul-ZMHlI=(A7 z;CDIJ=y<--mQDG@TRyZu^xxU*=^d)<5rSC9>IM)-FxlL{+TIV*GE(LJsL(iKNVF6; ziN_cjd7K)U2!^-L(Gm68QjE?;8xvQxyO?zEqmd6&6~|}d*zR6)kWINw7Pq^y2l6*@ zw%rFI_oF`%+$R_tLxU8>bSKBl>NJTl#IHC20`faR#|!Mhdqze^2q|f4&3X&W{fU0? zY?gp8KMBb*s{nXmrs4Wvc34)qv8_#KFmGkn<8mKVWD0@^(75vMoZDWkHAU$Si2-Kc zaRKz83KoM-K6;}Mv1-)G)!~LlvqOPko1mbeo4dRDGW(w9>7PH{+P3WlVZuCEM)dj} zh<;?^(M-l881(e?h=#?L>*Hz7sV{ThNr{O#S4D(ia8+kv^ExUjs)&e)3Vt5@8_n~# z#~WK*9+&cNdqzgvH<=*c7uRu4BKI9sUOv8eL*wJdD-C8Djy|u?uEEHJ;Q;ed=x&d2 zNa%Ld;9Qw{8V^*Q8Irxv?V9U2;eYFn@@}f2V71*f=GDQ~_HFRU?BoFMkBJKWK75e> z>V?Dl=tN;%T^;oI#I&J+6?Mcw2e$h#@vwS$(`#)I(zE(Pf zocHH)%S)Q7bny$N)(8l1O#1*Cvu2asFEJ~ChNJKYgr$+uIKW52)^iPfz60yOT72Rc z7ZCxtt8HacF4_v$`@+zh?YD!Y53DWtln2ju;^Zw1Q@F21Nl8gHT3wI;B7wP^8n62= zIyyS6M)(3R%!M*3Jx}+S&V5JX#NP1$*iMy>x-94#;Qv_ny8JvnGjq7clRPvuWU+mF zabfs$2Mf?U69O-@oy%uD1^>Ys1G0F(!CNg`5+;v>FKvtY&(qsbv%HP;UK;Q+Qr*536QPnxHDAF>!*QL zC3){Yx`nCgGPBRcHBJ!r@4)u-f@0j=pOTXnRt*4L<;T4B)fqrwMsS7Ac5*&)a-fB* zL-iDUTdXKrt*(SRZ%55YeA1{AxJ(>NRf3iNM)W}xZ~v(J-})8TXwV1bqc z4W^*ZuowY(Q06;ev;$Y#$Hyls_adG~8Q^o78)g-7WynMkcsjEH(S81G+#7tI>NtD; z0U8>2cCF4lPHttT(ROfZ>W5;UnA(cCvi;r|Zh1L98}0#c;_Mt8wjQoaH$s6FN?FY% zX?Avm`((MkcesR%*taeAWTmrzWF$68{{`jv*vfgN?h)X~p`@e~b9|b}NL}ds#7Z7j z(;ccHVA9-j^o06W#Q66|izDjV_e>0Xw8kmdvlI2|%g3>`1HyS?eetG*_%5!R*?EPZ zsiOg{xAyP{xynAfgBwjx-t-NpgytJl4O%&?Jp*Vwu2iQxSn~(!dU|>jK?t1y61&!E zE~OVZ-K%ZxGoMy?`1mrw3sr!p%O#`LZ?Y3CT93l#pk`z&Ql{d_a8L%P_`h%h7Dw~> zb54GKymn)$PaYx06)dn90K<<)5ibu90`Sq|RMLjr+&tGg2Usi)poj(Vk5!`9%8L5> z%$)<-|tS&(;h@auGVNe=q& zuD5$WJzXlwrU6(_u~Fc>*txjOx5Egx_cUsA*Vm_%&ad(^E8X58map{ zMQx1%vnI`h;~O)jR(Z;{j8;0w54>?iim&5 z4|q7CGVu&!n|X*dZ}t|AXws~_K?GcOa@dVYrci0WOwxGukX)heYcg!OOy=0rU(cbh z3#b{}DK*1$nHvMs@3YRmo8t7P2Esf0Y*=@-%jd#?k>PZ;(u2Lt*+_- zGGi<)&fo!H5VSkZb^?1!$80K8S9+UyW-bAjfkE|Dn7_9lI!K7b|6j@Z4M8b{kXM=O zDh#`KjWPP!4lc7R@W@G|RZj+|;H4XNqg$9YB>m}uxtugH4~_$jJs>PGFihu!dmz1F z@D3w?C%NrM5s)KyGsg5ymz8z+%VA0Ac}n(e2021?{;7+W+gHjh1|T9K%WEwr7D}vT^Rt zZtI$XV;%>Y!p_@RwJ0SN^?wm_tUNpYoKY>R$;3jGy>^fFqd8QR$4*cz7q@q$?Jjh3 zY%B^fAhyRmf<{1@A<=tJsvc_74J4cZxm6YbZ=LYh8$j=d#m3f;p?<@8=j$uFy}cbn zLsM{eoC~O+wzjqxzvG{Z+vJ&a0(#1=j~|eYBRMed^2PT6f7W>J%9Ef3N=bbl^0~&{ z$?;gVPw)hat7r09L@8$j@Eja-p!qw&f>AaoSmH&w&M$Hs=Ok= z!^Qn+&}bj1PKk$eT)EIWH9VXQ5V(G6>!*kU#AZXr2W1pViiOMC{fv{%F4t6O6m!5d z^nVe@A!+aI;3`IIk3MrtVLuAlV(Sk3;+t=-!_h3h8ws40+iere=qOritkF`d147No zcLDD*@0tuwh`P@G9W7EsIg!{SH42tWlV&ilyGgo$2dSc6)YW&ia}XtMpp;jebnH?B zv z$Mp`b^#2kDN?uEAl5{Rk_s8|Jy?XWCKB{z$D;M4Ul?0qIj)A=LFQ@Ob6AK`Zevi1Q zzZK8tt8v_I_TxTNpG}6GX~Eej{2+SK0N_^ztQ#LCPH$1-uRntMqA*dmJx&lm+S|)f zuwtpcmHfg1__#8c-fbd1RuztEJD^J%;D_}6f&Y2f!Tnc%0a^@>pHlAta8RoS>ox+c zVhhL=zL-!swSx7<4o`L5>|*oyv8K?Q_4VzL%x0nM;S(p;GgNZMA% zLoByZ4$|s2;(`)YN8mmH?!WP1`VSiX-zOe1ba`Fle|_<RH zZk!CTnAl%}#w4G_*#GtOhCNYD?IX&1oqzjv)UwQ4fwsfDq-3kOBX6fF5Xr_X9IWDY(%n#G|I5kYt#74^-x4JeW|Q7(@s>gPM(;#4HZg-0%$fx+3<84SCw{<79F zdDmE1*E7{0-atZvkS*wnPGMm1Raz?Jc2}a-q8rs46>VyCF1=!!Y>7!J($3csxs|Jy^H2-ay~OFx}-Wgs*Rx(sCdua;q#Fe@y{P3 zfDo~xi2k*#Yz&zIZnXRHe8*>vqMqrAH{Vs`q+aI!H@N@5@AOn&l08MFx#bx?zrX0) zWQ-lVk+P~1YuaZ%*9`ZOj8$LOau0PX&krYymOI?A0&$gs82`xV!xSdSdQe62O`cixD?l=9B1*9&o`6Ne>(jr zCw4{FEF^RfYlSp;TP$zQEA~ooGzk%}l1m zGK3C@BWfum8L~S*9vmF`&Lxp|-zM7C7r%QJNth+l=4}qyQ~0DwWbxyN?adY;{QBWq z$bS!?6bx=AqZvPw4!>EuUbE_E%u`j@l$oyk93m_8=AG^C=H`5@IhwXMAOLkwA&`zK z^lgN64ntb-otwR-h1TuvP;d0If{MT|P`{{PlRRsCNx|9F9Gl^LoP;6FK#B#%DQLK5mOM07k*P=82@JILvaWr?1a* z5i4*5kRkQ0C(<9TI2a|4I6sL8>mR>`l9d9KPp>;T&wot57>-cRm1*3v3yCw*-1Sd8 zQruxCUGaZo(Q&|IVKT;;&V3LHbvr2&=jKBR1M`Re%%Lb!dQ6grU^95Ma;jmys9GWR ziOkw7Hqf5GocqE23%bb5FaK7}r6Sz&d&l)jlHZ`{wt-RXEB{lfel)k3N`9cT$ydoz zy74-|O+TU6Klheg^*b+$U&I)Vr#U*b&6`yM*)38J6C^_(c5BM=aG?#UJ5tA0X%Ft-O`h`2|2Y-dM~fVWI&6#1d1JU)H|MF3&-Af0O$* z=3Is0=jnOx@Gu_#F65!1A?vB1IPqc^*eHz-Hpmxi%DG#O$&3`gC4kCuCn;$Mp4~u- zumTz*V_bjF>2bQrmymNbVq#rD+5>H|x3sE-^wUJfM46pN5Nv(2JimZHxW&<*%yWOe zyL?&Y;b80y)tU`BkCdDRs*nnCw%@S?wfiJHo=`gV$IZ0l>02b|$c9Ss3_qcRf_i7R z0{o+3PgieZV(rT!3dduml~89e8&Uf6(tl$C=;ukJp5qDx(Uy*Uk2Rj$qsn}%5!d>o zXlOHc@BLADN6202BO86%pSDEbFg~sF;n}WBPNC_KV5aGuVmy`3d|5m%5?}D?AKhbU zL#z!`@61q57=b|gQQ1CgXr|~3Yn91CYXXhKkn;sN(NbMS4rjqq^0(Jf31d;p@k=U)zaRk$BC>dRQ>P)#%~66{=%wI+o>dK6kweZr-2r@>$@y4|a99sY@Yp zw_l6Xf?YjHH8t}Csux2~5^x>v*J0ckXZLtVYH4Yylq;UIT2K(X*vS8xnc27LBjRKh z|33OG(6I%oOc{c9FI-lAlQSLKg0fmgNRAgbwl>u(#eu}`;XBY{PDrg4#?e%GW&oUb zM_*fXXJ=>R{vz+`6mUiF@fzfE4e%gC^{hrlU)8qpnOfS*^;73KVV(W?GmOFO;&PI{ z0@mKd;ZbIAVr(pFmVS~w-x4_Sj6e3}|3Qw)#h}2cJx#lWA|*vsS}~lVnfraf4<;el zUYaQMoyj24P%QYo+s8y<@@cv+?29df%@|uDsJ$9{6*{utYMjY_H)fn>>w8nfL-Fa@ z&NmUKX#BJ<-=8fg+&-?f$PJ+M-Gkod>&IujfHO+( z2LmQVIfcN%O{_{be{4y*abewe@|K*1Uuhn z7kbB2bq6aujK@AU46-}r4g2|ErPX+uM6yMBqX>rGhlT1a)hj0_Ra;dTCJe~!MGCM@16 z<+Jd1F3cCB;gvC6M6z+EbTN@A%64wX=mGwlmfr8@7nPL_7uk79 zv1CZ_2Z;ZztvS6RR6#)aD;L`}*&TkiSgK{pnNn01-FvPuXBCKQPO=;w)LjvNTLzgp zJ+SwMeP(UpSW=!XySwbGg2|cgi8XGLKC|aEU=|J;+9B@I=rfza)}t5 zo6PK6j)<4se-I4S2v~T{x$8BAj+SsD)9wJJN~z0!t7OiJ*P;T|Ga`RVSZwPtj&8U-S;_VH)e6;tq1jHRZ}<`@33Wic8IA0o1Wx4dl@A z#W91taWOom>p`d&v;EZf8Y(Z4|IUgKqywkHW!L(rt>54JGC}_#^WFJ?3J6k%vj<3{ zRUl{a+gG}QB?)5asD>7dRt!b3Udt;W)xyvK!pDhpVF6>zCA~U!X*;2e55BAavS?UOwY*O%te}97Wl$r&EL6+u?eIC zI8xJBqK_)jdLd+1dDlC4r&p`*R|mRdgE8&wDf-jU zxazFOls|fV z39otyj@##RnvoWm8%8=_*NkGAe9&Qqmwtjpg(i}DIayfZG%T}sm}rGegB26m4f@k| zetA02sZT2zvi0%JW8!fz{!DRTb*WytJBVNY&eYaxHc~q(SkH&DK#2HDSKw9wM_Gt@ zLTC-5r^`z)pKBJKmT|U2AgbqyY$JYA$VJyc1G9Um{-ndpAOJ!B9ZFVRg5Dpg75?n> zb&Q?0DKGUJh-iVi#;zqsB{S5MUG%Q6i&+CLp|GB_u6bK8KsbB+{A<~}>i4NG-hElu z&kQsY*mE>^ZK_}kzvdq-c^U4tBa#v&be;Std(9^b1UXs_A^S0{@^q+HPL}O$59fYK zV?p#0I(FT{K%O&oUx`dJTV`5x1}>5@I~o_^=~4D|1&%aUR;YM3`r$3?CXwFrdH<=| zP^B6YUL}gaHKIuqXlQ5>NS$p{yh*DOv({K11toY5HO?eL(g#!fXC>38`m519|>*kuUXvRqLY z7ksnR$E|RHmH~&d*Uim0Fq6Y4k~2KbDcsvHsRG5D#NCh`M`j}IO^KS=KhL=-4WEYR z_VmmbZFUSUN`8XH)&=6f(MzGlg82`_0`YRhC2#w9Vnzt*3M+L z3ewaj(+d}BRc^^;oB8FIk^I@@zU1~8H(QErv+ahXsXpS>Vf-$meIGNnG@z72NK2za zq+*TK-Yv^JWU+rd^b<`zGf37?t~#AjGuV$I!IC4dV$qN zL|NOS2z|T-Co8=kha2d=+>%fpA()wsFIu>X7<)vkeP^P^8pb9u^9e*)#v{rjUgCc< z<(@6pp-eQFQM|i-LD-8SX6|zK$%XeIetMq9<*?q~B%8WFbt=Fq;YaTYh0d3 zOYablBP{8*{G6Bbp;V{_zjmmf_G)Xb=U97V`|d*Fn`YcEkFtQCay))?7+S&UgL7)z z4SRa{ukCC4LEnjnV~;F?ETcHR8h?VTCAT#KH>>b#hKuNW)C`l4Tt@+8yVPK`@1S(dqWRGY5Z^g z93FcgUY&ufPeE`j3(#Y`%Hxjs!y91AyxKd)PBU)z)-%_Q^Sfhy+Mkfl{QWHl*h-3- zM_&@xzRgc?^RInZI_%rT)ua`5K*gamX`C1$e*X!fA5<+2>3GsP^|8OF0x}WDasA3fa6d2-wtLTKBEc6HJ+Qy34*S`FYGn z%h`eOHk_G@yoMJd^@YvxAOh9}OEMZQaUg+m`0D8gRG9K|RCsok13j~v?rLzZhCrog&bEj&nb|Q|Kv$CJjVAdUkewObVu>Wizg&kt|s%*+U z+6`EFK}$%ocvKyJziq0gMH068>fi`-;$+;*VJTnKLpf6z;ZVT~avMN{@97FMcznG` z5Y0V);`+CeW27!`eu{6~`OvWsc^m?Wce~w@+U5u~YR-D7r?-To;H=paWd$N$1!1_m zr?;^b*+DRxp^<@VI6Wh0!FViv2()FX!i`Gag1#fv&6?06 z^fZy)(GTQcnj&Yf7<%}}oU5{Jxxm`L-;R0DZ+8bLLZJm753Rey0Ng_hydp9JP4N+|T$cZ{02uPxtzJbI3I>1FMXabqYm zQFAzm$;tPvtp#74FoeoTcdLoQJ*}|_@x*Uv&CDt!-Jplc>6u&`9KaIYQv2+rX&gf{1Gj zr!H%G()HCG;+0a{*=f$XqeJr)<=}p!q_K8>k5COAuUep6e%@_+H^I% z4H3{YcN%;zcaT9$VT$F;sGrT`$pyL5l?<;4eco5A3x1A%l~ni*VLnbL@MKQe(>JH2 zuKVz!=6-H~gR}xV*qXL^8yVdWnhj=9KLMQ>9o+OC^IUPWB5*`K`aJOMxm4ri2}Lqt zEV}3X2Fqwr^r^h>p+XQx^K z!Tz0M_oZI#1YnxwVQnK!EH1}Pv9ptjn;|>*#Ri*>Q^$)72n&!(tE)S<6})6h_F*}O z!PcrFkN~_pce+?jCK~Q9M1WlTWh8fNQ>LxAmIdu0)V37R!DmkZE#=ym?H0A5@E5x$jN;(u#l z*y75}cDy}sjy^5HjH?5^MH%DEacZ>%HIc=5@z8J!V9S6he4btt!l}mY>kN=^I9wXF z2-0bNf9h9!uzPUY1LJXfBMpo#n~2!L3mPgh~@QI z`a!jHwh>s^#f5Ff0w=oy5s|9g4wnF;oZm~^Xt-^&yLJR>)_U@1?Z1fSwud_IOs3pP zR95XNHb|eF5vU2BmDos;aIM27bH!fzn=9?8BSiUZDRJXk>Y!s$Y3Ug&H|-Zb@DwXi z1rB}pT6Gv#F(a@Y>3K22!->W10(Cc(+wh}NpD(L`!xQSv>0Ws;eg) zR-!Z*rFa2$nQ6`Y131iSlyC0l@|M%U;B0laoXf-W5yoa%18G7Mh$jmI9QM;qvqx5pLusD%Zr<_KR$TM1&sLbue5=i~z+e)T;*k6^|xv+Z{oFe#| zsNqb1n&Nl1l*SY8#qwPft1*8GR7dbg3qVOBGR}sllo=ah{lh*N;jwghNgnHTUGAOFqQ za7-URrl*rgq3L7pF&jDV4&0%-qDLHVL?=vOzVNb|{WzTUCn``srm&omzy)S@l;$A4 z_%maTW&C;G*^X$8)6HzVYE0cYrH+2MP#HE?v6Acq$E~ws^|qdAh-JQ9IZFWd`E=-&L1&@LkG{l$G}nmY23~?^*dke&lE(VWu^O66r_! zY)UM9mLX1~X#F4Ks3Ju<8FH~*DSS82{5r96mm!oVewzX>l}2ZElrvL=rdK~4?Tc`W z7|P^G1H zA}zh#kb!K;Z99013rD%<`!sSV$LuRSjZ{LPH~>y^eH3OV(iVry^K#%&n@bezth^@U zyLqnvSYYfcG&von_@JKfIS1hlo(2_z-~(z}^xGqZ-DH5Q6vm|VK$M&j1+x=w;IlsC zlu1+`;);)o>Iw`(;#Mfbu`V#IvD#H`GtvkygI)dstB3|Y{x0d{WK;^|5Tk0jbiH@( zLcX_>MWGJZ#E|LlSwH^vEg_CYrFokK``x=Up;|+a!*Yf9Xa4GUjyy*T!Kt7}vDx;U z!r{rybPl7g7E+U*p|!_VFj-FE;lbv>`AK2pPrT>;URZ@_*Zpvb_UJP6yB4+-pL$>{ zI$iEb9LpsF>V^`CQtZv(Zq&mXb_S*j8Sf5%tJ+pzf)K!lX2p&?H+yng%KoGoisY>} z+OJoopRnTj0Je<04W*p-<8=Ti1~7eI05eBtr4I&ydhJ~B~Hu3Yc;OcSj1depwBl9$Izc$Mu9 zh{wmrY$=4HsrbD-nr?-PFYP}LR#@7ISFXQmDE1USSVG$d$z56T%|9HOO$#X69RRKX z%pcK(600FA{f_VOS$-Nz=I7*vs@@kF7^%K1U-- zx3>{<)>PRtDnXu&ea*BpI^fN^lU_`u)k8C`3y*2K0DHEMqLU-sEXHZ>63WZ9k(N`! zSqFvwDrcU@wIHb_!g$GMwF-Gv*ge?n@uf^4phvJ-P6XSNyEix@56Y`5@WEGMW3ne% z(mGg$#K)87G|cL{wP{JKf^Xrh9PpVaHRM(@RzJiua57w7j$aU_(;55wml?83f@lCZ z++-YHcHvyoEsAsAjEOw1qV(sNK>Yftc_z=Rjj zAF_RiUiD-$^*G0{aP~7HG|k7Ts2F!K*-#ESwZWcqeg5;1p=%r$xyjRd0P2+qcy6#_ zKbI=(>()S76y_JLQWm!a;`7AbNgamVq)*)0r`jb=m>X2%hGy{X`;V}KOnlD_eD$G) zbk~ME^V+@{cdr;M0jF6*(|zk`L{18=fdb`OF8l8}O_SGI`BSqeqgwbb(?9rG7Uid< z2jw&5Wo1#?R5=zNH^Oi3Z=9AzymvEMnzhM4V#?iFxMrkA1!xK zi8MOk?eo6|{M&t6I!2*Qhbef^W_pc0s47|xSU?y5pfg?*fAUj{;MV|wX&KhuYUVZ- z)t}qKjlD@dLDzv=41Of$jdTI}hw3xIhBLytlZFR=DtH##vZw_%96ICCR0^5d^El?? zz}hYf5FmI6$L?bc@a-=mBe9rl`28DO*r=3`N0yp!^T6dxBHdpF7q5^Aoh zmrDIS#0CZO^4TlKc%s9d3Q!) z;*Aw{gJ(4rs!g8<@JF&GXpm5Gy5gy~f=pJ5@y7B*!l7l_&7%Q$#nFDSE?a(JeF_VR z@iN}lYa|y`qw7gmZn8FW4uv@lKFQcRX^tvw%+!6+e#RT~IAxfCs z6a19hHBoHhpvRb8B=c>q9IR|`b=P-1S@s}%GvHs?e|6=44{(MkixZOp65F5n7eM?N z`~-3a5|odFvtM~;dTlvS_(B0dhEGEi55S?7L#8ZMVb?t_Ai66C0QOSG#?*cB zG~6AQy z!kYAe2(i0?zg|K=-@p@N{^Sdy$TfqWhW{;SpN}!Z z^p*QwF>ECGP(aXberI;`oh|$tBtCysqw24fx2iBk6B@nCelv91ymSmbSy>YP_4C!s zmxz&(kyX0FT41rUY#}5B+&rYf%S+eGlNE~oIKGyZ29e|AT}{S=l*QdI-<#e{TA7*A zF?{$IGNv5;1@6tYgs-5!<1J|9bNm+Y3{)Dtd^q- zW8)yw>tz?bIO>Pum59BoE0>9Vb%0s%M9b7`>gR_io3HxgpJ%NjkhKSU>cS#EPpH;h z05+w_EIQy!0t&FtMvAp>0|VmahkWKdgv+O~FIOaKRs4E=DOvPpJ-BKAC-ObCG9Y-t zR?c`8d3xqp4oH65+}upa5kr-@cfe&nDtfKvTpKnm$((n8-OF8q`o@OOr1JUj5B31?Y}A_0Cuhs&@*RGHi%iZE@O{tF(8DoNZ2|7 zdGT~s*F+$5@yz5ADzQQWviTpPx%T?r3P_bF2OPk~zR>|Ysl7V(fneZqNM-7YJFeGz z9#ua-90%Me^;sjeCLn(%Hs66eAfgTnex42r`Rxa!GW}-@nhY(esHosT7yzQeJ#2gd z_nbQ@0Z%S|4AbYzIS=tgjGiMWDsC%v&RVwolnFe@;lRN0c6WHz!dw3OPt`j;vLu1$ zI?(-C&Q*yha|gf8nktS#;Ozr}=kqOYHZoS`FmqsG6(P_nH*#cBK*rS~^5J@X!~PkN zPX%mh@s8CFCK;oau5NBGSyht|JpBynj>P9aY(u(olt`?KBS5$mEM_Og(HL3xU#0h+DaL7$we`8SquH&d!rN3_B z@Ypb%BG0xFCOr8m(*_Wq(lIk*FldMvdv1j0Z$%Lj{MvsUuk2nyE}Qnx{cx`Ip9fD< zK@VnF^AEwud18ik$8oGYW!bk||NT-xs5Ntt3`YW?60RRei-5mp=*dsz65}woy$jLgaXp?|b)t&xqYnKeCVc zojWIaBXN;+`F*P4?^Qtx@Ey_)QYCHUU;Gelv_Ll#{py1JXT#kq)WxNZQmgUCX!3(UBusi$3cx$GeRp3`d~`N zk$Q-S8YB|?U{YYhiw|g4)eGngqn?}np-p<6AiTpdy^$8w{Vq`@ZSY-B9HhIPW?m5U ziUp6mB0;K5WTSa3J0}`$L?Jr2vJg`E>?~W`OP58pnS?SV|Ly)EQc;`Xrz^CWrF*ZE zw?ReD>~}5>M1qW zHaz>B2uG{X?ct^=!TDHVsD0l;7h)dJV>T9wnnD_Z($E&l#Wo}l?(>w8KR5pT%-8oQ z%2Fz^LB=GOlH~W1r?&|ncCVNT6LDE%GmOXKZCL{2N1?Yv z<=aQZe8s$-o1eP0vcqTScP$_jsp$Uh3g?#U;n zNGTea({=?*0XP6f9YG%G-dm*orn}|-`TJttQpN1sDNEn`ve!-EAZI22ZHzb$U>son zY==!~CWZdqA^^Cqqc%}IaOJTPz-qlT(YPi-I*~L?L_pp9(c$DgCK%J^=20;$ z6x&n9Y@ijJB#Fb6Xa6>6#p^eT(I&%P=kVbO>|(SV78INVex;443wKwUw{ZiC@IN8-LZ>ENwn1nSbb_FaLJ#>3Sz~Qe@ z{awhAlyHK+00>tNuMrO>&MB+yozmWmU%WC zUE$NYpTGYcST5C^)Oyc2Tl1>^sFX{+EHXOlOtv*hybNy=?R)lip_ETI$ExaB1rQgO zhyhGY4IK2By|!d5wD*#wN%9A!Ojgm12@Gy7Q+Mrf<|bX1(~4YMrf)(~ZM|5)3?JkWT2H zCD*4}0#QVK(>kE`m)7n*DF{QYGu)m__{nKyg8 ztm6S#R~1YRCq<6N+UAN;ZTV>-&)Z3Sbcn#6NHa{CRjA_|$U|5mW((59&AJ@+YhY}5 z&auY8i<^#uReAMS;T?Yw>eobatrVg`4mrNj!{9YyW6Hc5t(gHK{g}GjzgE46Y9<(h zqL&GfMpx0)UJXuvNLM#3zj0U2S^NdG3-Y6`zKYsp-`B*yj`;Q9yMZ=d(vsu=ZL*p8 zG`wEFH*eQ2nfkV030;QM*WZeM*JY@G- z*1HzB^Sk-xzJW*Aj1Xyx4N!5^_G^qExTFbs=`1vt{9gOMc<=m%i^j9}1g~cM!p1a2 z?gmNo*AJ4t+O#Jkml0w>1=9JTy{F3A7!qy;sjU3_&_6B>7+s z$6%7T_=QfQVsnbDYDw5+jb6rE0`{zFIrLeM`6z+96Y0jR>LfpVz!8#XlJNp(H~#r7 z82YldsdIA`23b`bfhc3bG_(zsX{16^66229RA7Zflvy}KD#SelW&J(rCUU5 zH3<>oC0g9?T4<`ub(cbGW9hPh?U@N*KX)P>^qr?^rE8zx#r0E?aYeyY zsJ@T0(BNk1iz%uU3*Tj-eEeW^&SP(2*AremVn1g=ElByHk+aXsPLB4QoQ%wL?ePlZ zy(BYhJ~{y#3FWPVHah-eew`0a;F~C|<9PaTbkb@B3Ub1AAxp~wqQnfZGF$VHLJ<0*cgDIzSU|}7dVCKU%)QXeL3G%jc|Yg0uzeAoxbs;v$Vg3$ z?7o?1%M2>F9ZUwPG*a1<5Js%s-1v^aQQ+>N?K7I|Sg9ithq{-=hWlV2pdwK4I^#^) zw54%zO#q=YFwF&DZqgpxmBEigTSdW{tp_&n2qwG_&w7h+68vR1OqwPNZ?PQ3V?R5M z#$flMvNAik?yFg~HHUxgd$l*5iXQGqJ!l#)JqJh?Rv{|Z41tVvQ8MuUs^vGN@sB*z z68^@)yT+@C7W&Pv`3XKWs>tbRru3H`0h1+dWIIn<_lCrf`gpJD9DCaqSM|8ao7rYbtI*+u`*4P4aKu;Uz468Q7 zDRurt@R4IT^R$kttFZ|{T!!&;%ROKOe9Z5+6R+d@586J=lBFe4_CJcFr@hUixEsP# zD;y`4Q<$?mFL^BI7Bu@EJ@(@Du4F?qr<$&*%a3C`0yW25BeuNzyyV0Rq40Qt_B~Z# zHLGFz*IrZ&O{ch-qlqZb7kvLBiol!sRo&B*bWd=^gNcBVTKhzrFtNZReKt54fW$2l z5AuwshRR|1y-B2;GF6mSG`FvbcI#edvGF7+WX@h?=AWFKBjul9FOpc4kgoKY#d}-2 zl?V-1r!pj9GyIB|;(z%u#9ADWHz}LQkcwVXl(@l#s-Y{x`57osdEf=p2}3p3W<9F~ z=Wfa~Ym#WcR=?;|u5WCC$sNR)`{iAGTePwKxUlw_XjmA#h@r5~lllu8>yM;FG`Lhz zkl!7uz1{Bh4@c8dKhr!*5S1YmMBQCZphiGn`_3!;U>O?Kl6n&?Xxhw=@-_#Neb>@F z`%92s)QJ#Y(YPH>J*!lut8<%Zm%Ak zY^q|%gs1?x+~Q4-EX^;r-BUhi?%L+1M3dU6#t>cHRI*pw&Hzk{Sz*o1JCsY-x34Dq zyiWg15aupO?TsvIBM>oHk#TH@pbFz{5zNAP5A)k0Pc#6Aw!U0H4U1r~O; zy;jDxT|Tya)9N-||6oX5z?6b^v8)SMMjBC&QvP*Xjva8f#$6*%Ct8JVc$AhwL-Xm+ zO5W%BO_s-;h!T*PwY(#+e@{slC@LI^@C@IBuI|o zf!Sf~-p~Pp&NSAe$1P*!#f(P$Fy?nrk#k++6{nhjbtOw4WoFAIC=oZ7R;&E`>9TD1 zZ&Xe17+20VS6>L` zt_pnT*J7c5|IW#R1;pB`2>+ZB1pN zL6wIrH?F8^?DP6cwz+Z{4w9}a1&ekYM5wQW9!bT$&4~?b!tBu(BZ7QD&1!QV2=8D6nc|fG-j!zMCmJEBiSVPW-@Sz+2SJw; zpduk1?=%brhIc_XsPpfqrl+N<_-*5*3IKFMR0@J7C&c*X`(~X@DKjD zcrKnKzw4!sw#<|S&{{A9Z5UFT~dpij4{lc&?h+Y);(E#3874Tq^E?Ogz(%}I+DK*3@**u7&(PCUtvEGI8eZ^^tE6qsav4Okv^4|7 zej)7j-u8hz*z_!f0}`B@?LZCir(XBl0D4sL^ybS z>@Vp8&_l^C2ZT_(8LDZi3h%8ktDel8$Wx)WJz*2au=UWmIVfw(@^CO8)RO)%M6e5EqtW35^ zCZXo?)muJ>zCaQvx9v*E^y9|B5F=e7Yv>v`TZJKDqW~at4C03W2$TubOh2434=F?r~fr_|+#@4VmG-qvK~7%|#ZzF~F_A%*2s@0?Yh(jVplL%A)V zWVi??alQ!mmcq6y z#W$sgJkP)Jqy5r)5^VBcKx4p4&VO!a+Vs--a~1Rx{3a>B?Q2QJFm(U}a5r%9>_h)P zZ3Ez^{&!Qrzf9Z?#P3gP31D7SFBXpdl=##fj1O6QOn_~EJ_VgKi3eM8ZrbjSBO2@) zNt-|GQ2xhBMMq~66VX970S*1Gi;GS_;sEb=&YrVehaJ{V2sx7ZANjB&tRK*HNMBwv za-U{(k32o^zguAfmR_|XPv>;!SHY(~{?8_xl5dqp`0o+m0Tlj!{ar9wcX#)urMLg- z)~8P*ZAv}m|8pot9lia3j;6uYy8r)NT;=RqL3nEV$A6q-8f{<1PcfZYaoj73HGfNL z#mQBF0z{1F$L9MV|G0SG#+bbWga=W1eL(q>4V*-bWo-TS_A+$>RyTH1EkW(D6#anV zYhwHvMi}DK9qk5~)dU2IE^0*pdnKj^{r$|O>3_U>2ndY+nrTB_yA=i8g8o-@WA1Ru z$-`Iw>BjTkh`ng8tkJ!L11U|-lmnjsOsMDPy8pgi;P*eSwtu^wnscl7yr)NI&sEs^ z&CvZ*v8aD9oucm}=LzTF3g?X7J$f9?BS!Z=A7c1tO8w`J2%+x}oHsQIw&wkR#JvSn zl-(CEJYb+8D$=chG$<{NDBay4CEeYKg`{+McMcsQ-Jrlw14<4xNH^Rw_>cGA^?hr7 z-&%Lw`z&AOWuAFrpB=ybJNxXDm6i3X-s9ij1plg`5o?u>Te5+sWsNXCetw73eG8&c z@w_z9?Z@CTx59tNWGT)+gjs{256y6_PCoCwdJ%R9?LQtifOZMjyHBsloh>=(zuH2D z{xIFh!^fw2eFM~zJx`FGNaIbQ`-h%n`kTap3Of=|6flvNg5Mb`PR&Vz4gQ3PXX!mj z`R@(rb($(wiTM?cJnPFHj)4M@bUM=e`*un-E+`Xjgy3imiBaCt3*8*6e)@3 z{XChu_m5XkJHc#qPR48V2$O#k4xG8+Q^`{`=;yGge?djwgYRcH&<) zocXLWbbm}uttqJd_x^$2;Edg$c?#FXdrr>IoX&jYk8p8O(yXqnoxyL5ii}hZUZah= z3rd*|^B7PUznPwng5dtmOe=3}_3E_Qut6jpL3?h?BtANA52X%>QO|Uce&nxiO`rd5 z*S_}i_5IG7Vv}TPH)m>xNjj+_f>!2=QFz&tHRD^6l23T-gj>j4b?m$o@i?y&^v zM(m^qTz}7dS)+pNVGtZF$Nn~1VT?mgzBO(Jr0vv)q8U4IY9jiL3?M@~ciJ;S_OZ&( z`!-2YwVa_^Zqs%`{c-4nzy66T$RJ_)5hyzZ1^yW=Eflm-y<59rWWg0Nu&xalt^}7} zDmC{fmZI06@@Yb$Ku)i+Mjx}X4jE>9czHESbULF>QuDwv z)Qo;t1E57f^a-OUf}u=AyY)x?2VYT@u$l5E0?n%wPoU3_^)IzjL5rSpVv&f#kKUX6U!wjKSc zVvY@<$g&(FPIpU(De>p_3#t|FuRgfeXb-B#)crBp-|O+Js4k2%l7ZaOU-IN=GdmJe zm!uOdV=xEyCWNx&)R^|Et5L7!|7rPCMj%d;@!H@Edn5E|#%p1UT>MVJS|DhP+nfsM z6j@oX6vay6d>>^kEmF$fJY!eP(KHL;zFWAv5b@?{Pn2li2uIg)HRyJH)ZS>F!xPNt zG=(9FEFsECTAdod+l9KJ$XLvLAR_&TJ?CyWw1?3A33@#iWL%gcnD}W+-Mzh~nMUjA zZ@l;u@OChtL3Suu^6QnkSwS)>r0+-Pp1ij| zh_c|AyDMFR5)FMm09v}-=vr5!cjM7$X5TPb0~Uz+o}K7ujrt_M+fx?GY4P<#6p0eL z4<%^XL(#q8>g0q9VgB_SA6`TzD?`jb_Lf5UfZdEg@4-YuMh>BhzgWB;-(Ump%u0Dn z_un))J;TQU#Xu-hBLz)@6+(nprY!&^wZQ5O<^6Xpz^3hf{euu%EC_Zr!<^s5&qZoH zf1RD0V)~Zzl-r*dSt_nRwb7E9zw_DAJa~cLzlzmq1;45FgwGUW1R)CVldvFt?&e8f zd61VuBXOg8jFIAwwP-|?as9b@(H}qUFpe_-Qa}QSffj6tvWQ&;z%!21S9r+@#dq zryZ=iFFzm7c`6bx3LZoeEG4h)hYR^0VigR6$O{iAhF7H;mlpg5~Q zsX?)K>Nk@4scWQqc*3OyT?R-A7qw@_g6gWgTV7bK;dOSzyi-gJcep^?d#Ak}JV8O3 zri+)2Sr6a6%klI4_;$eFB0a&C-inW%sM1T85PP;SR{nQ*&q++?*5dn-suoX44#v%$ zxqdTU`y<}Yzf!9BK<4oBWhY6RM9dAy!~`a;h`bg^l<$fzuMeeTlY}Hiz9Xu5UV4SN zIdH-8oK;Imd)B|c@#7bPqNUD3fQn?LD$|_D9!z{;x}o{J_AsY?DQa%#`1nQAw=qKb zFypC0;BV)JsGHJ_Lfe;$A37nnoRGJa-+9g6K%-ZOm2}Lod=KdoLGz#>6S`dB!s>5 zxJC*e7HXaw>B0=u6-zjN^%UO=*?3Ux!@|T`RwAMed?G>b^cN8`Ba}TxrtLf-38|Vf zL$Ua-^ecqdkk5yffpVgj^+eg{$5UhL+#X3l|g%XW+~^p12dR0dUPVBXD;IL z?n2VdXPFG4uGYU`ki5+gd9p}W8fM0-ZkHVD09HjuZt0+c%uLXq{#8O_4@kBhR}$&e^u23C5W6X7ChHQ`*Gx4Vb+JEn%xs6A6& z4}Bs2-DmAIiMiK;KX5&LjF5g#bxJkxmZPDe;UPYLPD4l)Eap>qN(T`E$`XPUjh4RD z_x^ewsog{I_jZ_Mf1x)$`pTB5Ob!3P@F9T4 zj-leX>vGp%{95;M>i2^cZLRkaqmt6nqGo1vhg&lkC|o@Fk1VACbE;<-Ktn13MYL&P zYt9+doPWQK`a44A2}7(ERNEY?4G=LXNe7)Q??_2WjaWi6CI$YN^UO?5(NI-qXb^(n zTL9*PYNh|>q?g0Yl>DyDnORwA-&nJn3JQ9)UxN|LOtrnvOzv55P`cl$kGb6l!vyNi zD8Xy!{=YFZnds{dBgYCAKYnly>c+sE^qi)OXSIQ1VNkPo*k;0h0!<0KUuBR69tkrV zSC~=8^tTgigosSo3&)J#FAVWF(ZRtvF?zi(_Ww{=l1xMpDsIdYO)2C}G&(x^oFvq> z|9Q|qw1!ITTk!IaKnKWVy%U|Xii#m8$rhC+jhvhusQANjMioN*`)GPJVpI>F4l$Z# z$esYuzR-2nL-JW^IdW)7{hx8ygOITEyn&Yabs$Fn*{+2;2+|c&*r<++11{ zQ`3{5|I8X%k9C=0=A07|OP~p06+tH#2{sr~@R-S6Wj(!FnlBnq;O1Y)_qJ7|`c9*^ zFmnFqp8MwDV!!v-eoiPY1IYZs8S%A$SVqhR5@PfhB%{nJvwWB7QzxosX*YT`rjb;M zmE~C|BgO}uf94~1k{ifTP=&PihB9L&jU4hrBv8pTW? z^EmMza$2hJ$LmKkjIQU*6kuY)0!_wU>Epx6`3WH9POI{thdm<>=TL9^#+IcJlOBr=tjcyo7`VHIOI-oRjf$t$38_A9s$RdBeu+ z2(07a;9z9UIWR2j?KMRHm#C<;6~$w3h@e_+KW1Z#`1lVO6J-99SnK|stp20FHe4+C zkKFdw>k;t1H~+j;!^iYcRFw+Vx_+)4p#%7J&(r2S?Jp6m5%?C6H8AECfD6? zIPIR?3ub?qb*NvCStm52F@!Vogdp=q9n$|GhcKXC_*w*GK7YS+6EX={KRF8KxZnU) zZ2Jr}Qgqc>7jE>^KEmUco2V)cy2Gc}cR69oPqS*GH7KgE%+pTKJT7Ry(O z_&YP^ou2|QnQK{}bcWJr>E%uxVMD6NsBBv3s5f6pJ}vFqiRVYkyvfvS(kzQt`l}3Z z+-r6QPrX09CXt72=<7vC(; zFxdCzeLTD@Nza1nN1-aaqji`snd80Y;vgT4xv`F4y{v%tJR{4Sdaf#>{yeS|akJ2z z1%GXMivbjyL|YQsmOq=Ap-{#H|CPI6KmS-s`8A#N8(Kr)MK{n@H>QjC?G5RHE|@}v zbY<0K+Pv&^M~nnv?~abTFztNihov)`-Ry%*nOlA{jCKb}!&}ogv+3@55dGJFJ$nP4 zMrBnvldMPG0nOVld}(RTxJeGGw)Cq=dG)lvM1mw`@Cavn{Vdzs>lSymMpnnh<&JA3 z#o^O8N6%knmU)xkA~iEk=DsWPXScVXqDN!^)EM>8&>lpbA8Uszd|WuEMh$T$^22o~ z#pJlLFBfj4J7kZygyvcCTst;3JN{TH`CjF6-K&?%SJ1m^V8Vp~;vvxW5qIK1Y6kOi zDctfb3WWU+7o$aZjc~a?TBvz%ru#RoAIb8ZKJdTa#u$sCs2F_uod<0ItcelA zcXiS3PTuuL(6T)ls+@Ls5=c52EP*~Iae>7=aYb0Y`0CmUp8N%*V*YY3Mj{`i;Mpkg zi3aLaklMg8f8G*tV@0P+cYcgisCct7PD*X1TCe8! zgi`l8NNLy4C^T;mz~Pwt!y@B_7OZj3uHNj!(C@{kciH?X6*oz*-NZ-SYs!#bR1(+t zt%w_ND5|vIUjqte;_k;DMjx)irEP#@Pn@?o+QxUwyIBgw7pWlE9)MEf1Hi~`Enn}J zvNIGUTe8-`FRevKgWQP-rgof>8~1 ze$pBLaRGf)lBMnDh&p=whkHi7yJyBLvY?_nYCw-YY5;T!`!h0-TtCFMY8yFRzv2^P ze$X?h-I}iKf<@C?04K#7v-iru{OrP7V|WrJ?d4nGDA-@0DX$(rTYNCJa(fGNFCk^7 zHG|IB!E*oGf~t+n0M`~S?5qIVMA>+L;9O=?F8hEes^s>mZ=X!GQj4SxIXCZ#{`QR9 z)tDWV;!s3PAwvzRo-NtNUGAt6bO;0z7>fo9s~d^7?HuEuBca+^1{pM_vBOlP8fyxBqp) z$0j(GTC7r_t+Ad^T6}yyz$?5dL#djA|GweNwFn~*OG`@yPON|ZN+kSFqo%Fwg2%~n zoJ=GN!&MEq!1T<{w5e;BXX~B#Ja$d$^w7ZgpaoZtvuUhh^G8>B%T)9K46my#%09KZ z)lelceQ{S#zxQp)Fn1?T4cW+gr>W!XG8qv?b(=y}!^E|B@huab$1YjB~2Q@viLF72%Z$1FlD)6O_vstPIO_CSqo2HP>VC+~Q2n4>K1PiT+3iVB0OWMw}dr@#( z9Ax}7dXLtz8k0Jz_~Z0k6NZh&nSqo=>UO2Mi;yQVeZj605J-8|o)39kw#s&6OM*RU z-yv2j;gE5etrDq17cN;Ev7&^02zi|QL7>tbYzOhAth%B?2_S8ua5@^ANoJr74+Xj@ z=f|fvWlP+w)%B|#CC$oIwQC@*tCd=vW7$X?Rc&CYd2OW|4hZs^`ErNgXdPYY+hZpr%FJF3%9_q=UvVc zgxi`0P2fnORe5(2FjD~*1 z&$Vju1bNW70hsj=`|^@t0i5M-A4}=jMCP2H&rG>iA=71ueh-tPxc)nJ7+*}wo2t}! z8-2@&8ot?tZsamMH^tY=xI){XnIjsh$pr^t^*?HcxMzN>6z0;2p++JYP7sT;{v6dSB-Sd415B8AVl zer9jx=&!C8%EcfT3JT;*Eq&r=*nM8+OelE5oJ!R=Y3OSw8wb^r!0B&3Sirj+Q(zAz z*4Te~#S}eEFq=DiLeO_Z$j`cz)Zvz1ok;|Pf#D<@ zc1IA{hFa1i=Pm4%kN!=OTGAR>j04n{uCW$+B`VU5w8h`kVdf1fT%J{(Eq_Suz5EcH z#*uU5d zCTvzVMsA^e!R@p&YR;iH^?P=|GNcr4U#`SnYtd+H(e9|BByf?N8tQ_%f35|!DBX6U z*5&VyHntd6IxTjsm6W13GW*`Uk8?MYM9oSF<{Haj<`K_32PY>@-SdrGo3uBe_B71maA$WK{oi%}c@ybU94qw1`A)cVpCX zXU0#@&`|+INnCE`XZGO%sWV=@E3rm}Wn{~8(&c(XJ8e=7K;Aj=c-%5x;w`0w8f*ny zDI{sD>k8BODa$$|4)e1;cXH@eoI`&sk0A-Ec}`MnGM*w1ju}WKf*OVGE=Ni9M)IR3 zNNJD)OZ&H)_vEk?&}KVUkL|8W&+HTggIC_eu3#yK@ufU5doR+>(`d z&G6=m<@E0EpF0q~YHV(KttiP;=J|=qif?y*5`mmV@Xwyi^d@M}kDKXzgM|vYHXXbb zMuS|a_arR4cJ3Y?9@=6pwAI`N!Tp_`d>P)G*oy3Q_JAl3HBA~`Ds9>OVuj)~GSaRR z5&JGx5*8t|2JhXNp<3x?e#8Bw#HZWozFW*cv1&C6?UloJml$(P5+fO*WaW4#PyM>> z6`Y#wG-M_9DhAP6aO%d8DUV#TFlcG^le*yxXNuJ?q~m&j4}C)lh=1=5d@x2Z&OM;!In6A{<`wMa zTz@vhTmyTLm@01)Ie3}`6`G08u_NZCJa(aO3Fm=`kdfF+Av$+YPd#&TJ^jQOXNh8R zb92k}A3z|djQuobdgby1_;-dWmxZE~#G|Zk8k`nruB#Bawv;V)CY0Wt@N^gbHdp=o zO%^6$o%jKtYlHaJgk4pLHxF%Lm3-zZ2PpXE6?#!{K!Ve;6tLn{Qf05t!5)3_cJ^^O z9#iO-OHoC?9}GDun;#eQT3u#@%{u%z_f?fR`&Pr*cuS5#agRd3>Tk>O0QhLQKdSO# zzPOqqrDsa;`?U6(u!7-e`IX^b0fpa9s%ywS#^ViflY@dqY{)|cR?0{E_hfv0d^%2* z4Ga|Sfk+ToPA0hHoikxZS4YbhT#)(cY-88(ZInbTe;f}DcLim0es`rs|NNoJF$wXW z5F5ET!7(LO{F+t&+bgmY`jDe$F}DUfhIz>3wrBCypIIcONl_`v<@*k`^41zB=C zl*yPH#Mdh2Pl?IO(Cw58)`K`$Iwv1TZ`;e|)`P%^*pkA%DE1nsJ#y)w%bV+AcN63P zNH?;V=TtBHvm=|UX2v_%l61-C)sWr(N0S7NZ*{((_TL6PI)>6qCDC2RM5N}m zrRv@Hv)0Nk5%NGHPuK>WUf%?AS#YmPkenuZY0D(tt@B5g$JS_lg&UHd_kpb4$eu4H zwRk5w8gIt5v=Pad)(s>=ECqz&DXZVhoTFpRW;6(*`=+zc!E6+Z6=JPrn^|S(gYq~F zVnDKVUOj!Bn#an=HP9wDjy`Kv4YAv62$oEJE3JbOzyC#kRblAyjvFtga45Rlh8cIr z)%ko!uViq@nNZe-ioH)}c4lzDh18Yac#&R)?2BGYErZ5nQ)s}X@Zq*jDvXFHh%Bc+ zx~1Sp2K{L5QG^y%Nm#c z)mNv|g2R58Dg!88%exXl@sR>?VoYY2>e6s%*uK^Bu@}2PO?7%thJUYF=f1hi+*#~} zN)`q|o@J47>DsI#RLcvMvnA+MGnnML9KvibWRE|p0+C9$0BpC~I(Wb{vLXJfTW8f_$wh9j{=WE$J62+Z#xvT7xBOqQy_v4I zAfz7Gl$H*%F`Ru9LZP6b5N=-41m%iedk-wE$xZmKhk$7Z{DqK|ou9irWsO1L8aEo` zw^QiCR%kEl;#YoBc_$ZC+@M1*?F_z!x+ncytp2LGVy%GjH4e7*60Y zvs@WS0NF}5T?5GbKr*MP2K>vET%d)lv&GO23<$(Wdop2J9gw1ywzlfvMIJqR#0df! zw6?Z?&AG6?UMn;-6tD04Mm_;q&Ix2}HPv2=?1JSS zUrllOD9r(q3GQD)OtD@k*bdjXLtj!UKeWHCnvLE4Y5CUlV=3+IlCXN|?RvF?#5b6; z5li%XE36@ncFOy6*Fx`j9<&S|pbZhB)WhZZrefRtI@=Z5eo_VAbzjEgX_DH}x%9GF zBQ@$l{e|8|fb-K09RC{4m}mFf zS&)NB&sMM~o6QDVsGb9?%rG>z1JX;CoI;(i>sr&?Jk32`#Jvq+ka}2>i6?z+ow7@3 zitw5BjU?D{ziS+`1FxYU?&CTilg*>jQVo>2i{qNt;6*IIyT>y&kZJ?+N9 zQ$y8<8L7*@T?`|--wLwLG%XRpEtsoHDvtQfBU?~BNH(UMFe(R#ZXUDpT8^<+hX<&R z+cEuV$_x6WiqBWPO$R}NE$z4sTG4kFI#YOkagrOd!=ygN481!Rm(x#U8WxwLkfv_= zLcA&)uP#2dftPXs5xPBW=+eeY^)x_w#zls=FKMaaWh5uVXFD#G0DbPE=!B8*am2e$ zfUWHbKk~4w$3Xd{(htIoV6pQxX#+>3!52b+WT@3SvLvX1fhgH`F4AY(kZH$NR8q z@}F7!8~gUxItyv)X9nH&>-BQT-^iy-;8Ej? z8dfDf681-+vLWdb9$|vlW1oxekn``535mf|O#9d*I`Qn6d`LJSff zLCS{ill03{xi0(%xzIETA>|}xJsyUDpare$P0oUniKFK6G-qjxQuL`(>VNI$8Cg%;cfU2A}OgU8MReS%PN14KQ$<^@83mPhseFjO#ZzZ=hk>p z()taC8tzTSuHA1FxO`cE>==GE(iLo4kx?d(d4`rJBv-o;MY(t?6k5n?lK9_EQ;N(! zeGou4*b4IdXG3E|wClh>K^AT&psazie-w9E!_vn!z{;_Y;#qV*2UR@)5^t#CUhSJ? zdR82Lf{%d+bnHx`X8M$-pG&b|sNyW}py^UClB$vJZ|Jdcw8@VpppwJ;ez^YE=4^WX zz`{^KtpU%lIfe|qr5~koc)N_s>N-@%jA3n{as}nW%HY`tbrRK|n~*7{o*8ZU5|z;W zv(ERw6J=eRorgZsYzquurqn(!Wa3M7KFK`g|`G(uRLT?wz#7a9!#Sh-c zA3^%uX1Y3f6#2SNTjxysln#>K>Uk73NB{ZuO4p8*=UKbcY59eyZ0(c^${}53yR|0` z{4_$b4fnAgS2bO8K>U8TDAzQvkzriwR{Yt8_nExP{uiIz#(Ue`)z!7vk@JZ8zzZI}FV6N`E#T0C}r~ z@9}i{p}N&5le(>@{x9Si6<~4IJfD^F@6OJ!O(A(f?yH-N;d_x-iBWSCM^T?3M1jp1O!H{L?c(s%C3p3 z23{h@l^ij8G+zS5H{Yn5&t~LLo=p$Tb2}1_!1$$OC=LhP5f0I;!9QH6AMpUjv5g89 zU?71XGOeOZ#m8eo-VymDk2%OS9P-%nZ%Y5;PR?MMs~KQ+?_Cel!dIsE2iezL7Map- zyZw^1vdP+$>diAXj}3(U;>NPgM3a+(v*xGMtW<7K?3-cAG@t2ldbhMY5knW` zvlXL0UUTOi$-#0&4AX)aXp=;c-sA92MFbJ@! zvXiEac-(8TCwr8J-?l3#++-XZAn1qQC*zi8PyLP97BsW7Xp}@Su8tu00bScoP?2(%TFwEGgwMFgfxrcJ&a!{_O z=~_Dg!$8XlPiIoP@lr1^aEnwu_b~>sV5QMg$c_wgcxWpP$|KdI?%v9E>s5gu|s>l6hCiWcV zoJvxmWUYYxLkdj17xV)W?fDk2aNoRob9S~Auh5wl;M>fmu~IX4fML`vj>>j_0@UA- zXYnltz?7{M^z79e>CW&A8^h6gCxWHbRsp<+DtK5@TtMV+lGxO_m}`l0MoDA|bRUQZ)P6%A*VB`3>g)x0GQelZf2PIGFk zIa(fdwP@1E@dtDk@LVD0VwdfT^zSwt-roMM08*WqqcwWHiM^@5>ku!9_;-l+rG2%j z5UH0XxiN6Xd`oKhu%|^sVIX!_8mVSmsoFZ|`$%!)#A&rg97SIde?&|%xkw})9B_I5 zTDxJXQpg0;pX;v~7h3aS-egvhm&8?ORW@`DQ1B{s8uqqsu)9-DHqzkB{fN`&ov9x` z;|1QIu2TV=SKQ${Hd3ynePQ}rf zKHHj|>k1{=2t0i(#Du;~ulJU?*E-n@K$|DowU5$S3iDk&(~crTNb@b-XPiph^sY$N z3|sD#Dsin0>xHZ0YnN$p^GwmyV_l`BF@*`Y6md%#i)-5nIlC3FBEBT)&PVP}r!GH5 zCVi=Dva7xYwS^!^0}TP*4%EqAC>6#PS|m}Mfjf5ftWT4qnSVKrl4lrHleBlo{G{W& z+NA{0r)oyl4`^~)fQdml%2_ym+qggxET`FI%e|AdDBKow5?Ft<#u~l9(fjO1;;$;e z=*f?(ck3<_XfSGUcUnD2+FR|B00J#~QLEC|_}lVnff95Aw(V`u9n1l*KSUmrA64Q< zFlsj1{W?xzb86<|bve@F>Dz zS0--vq>B^(5U15k6?>kp+iCu^JK**8g6kD6@y6Nkt>K8H5+!|a@Lw%e?%QLNX z9q#%D3F~KJXZ6Gv4|!FWG=9Sy4C^Fw^Ysn>8MVA|hx2x)o_XQbH)yLKkF31-)6er< z+AbgDCipx<9LTG^hV*E-SGWQ(Nc9EM`GYuIB&5Q)iC~9{LQD;&vf>uRUun-s91lokzSd<&ZLGcJSlxid{av4Y|Na;X}xuen6M@((btc5FR^f zRw^p;%cBao;mMS!CijHF4{1K1*#btqGV6wOQCEtkX1*@>GE5mlTFxRz1Hlm+or#)1=u4ukq`7Y$Vn)ne=oq;1TL@af1W8YnmuJS`g+uh;>*+1?dV6oF*R)E@K;1F*J$t>fRX3 z{~hhQd`d}hbnp!yXL-=m=Z62K1>_nRIcIo(2DI}UkBj_=I!l!t%<+8BvRhXB-zar) zCrjp+&LnDhvL3IEA@lU8w~KLo56O{Iv+~V$QAfJe-TZU7tv2qXTouL{rbfMhj^gIQ z41vukU?LWD{EtYXR9dtdI@zF2@??NJ6wKfreMl`GEtt3~+^DKrcrbN$?iT;zxmJ1o z8SG5Q6~wsXWEC_r(o|7XH=c}HT)ad&Bom_=9KE0=BT1=~WIEPs>hQzfKPVcu! zJ3^jHD&OXVxU^sV6-8p_v$fh;!XWf76=D@q!Dj)>WaPH-bD}&bd*75gFHt2nG|-0_eG=A_g7gdDCR&Zv9RQRzvS5NBh%={miyaLz3W$J*6Zvb?y`YPH{-!)j4i zsAYk*$-~ox+YB&5H+*a>RT>Sd>%~v&FZ&T?!#6t)_sIA6D*!t|D*>K?AU>;ev(yeU zB8(l+Z8P0z?nb3sQX(j13g8f$$59kAS8QF7CrshT3}x!i&s&5Gg@Y|4-KmziBHQ+% zbA%qhz_2N~$}R9hq0qI3&R~+~Z~V8|FDYspJcnRkP+{Ww@c%Dn|0gRWNPCRJ^!|^5 z5VU9)K#_om-?yG?UIhGFWcL3b$0PW!qy${BPU3$zf%O03;&bWdZhv1FJi!}iO^tYU z^WR#GQG%>=D3oPd54=<)=i323{E3k_H&+zHIh1* zDORJhTh#HmMc1Jd|MYxSxa=-2ZGKae?%C0H9L-=n+bhrzFr1H%&l!|TNYux{&)OdN zrpr8QRu#TjQ*hZ`>J5bQJ0nH{p--h=eY&Rv7{=T8$>^o{T-Kw*D20(#v+-=koYs@i zhllCplNABM`TFcp|9`Sbgc`VyhKEOeP*4!qb2xi<*HO>+WSNnkexx&)@WAfs(9ZL0 zt2PThXiF~St?A+6af)XsNGrpw;t#cgcf}p-4nDE&IXo`RES@`@VIXOAb z2JFx8_NNT#N*is=q^$o@kxV1dqLhTsIltxdpiIUQKP^ha2sr79ppJ=-&aN^WUI&YHw_Q&_ zhh=aT58t8TVGU*FNbxW-3(}cKg`o4RWZK21nVObX0q9tMtY_O5LW~-N`O8C|i(eSR zfs>~f*XixGt}s?sRyuc+yCFcO3<;FW=#nwfNDP9?YFeH-WPal z0~)f=&gz0DoY2#YZNF_Uk~VkS)BQCeYR>y)FVeSWYK!voB{g&%K5BTWi;0Pmu$u(x z`y8k#oK3iv_ypa0XFVSha&It!Ljthn(=9%Yu#MHhq`c~CPSiabKz`ng zpSdju2e46s)nvJ$&>0M2_UhGp8U_xIpRR(g-Qt|b{JDuqNoxA~$b)!z{hOxnu`$&& z*8v-%ndPwp6`|FzR^z$Jv=to1cVtQL$e6%p@Ox7{T^=TIZnL4}ohgEq3+pD@uX{bV zUOHuTzjbe9UTY}0bm^<)tMA=;Jxj;&WDQ+E2j6!?V$bLAlRtYy-0f7x(JlHWw>_#)iHOKZvTM<@YdOhP zICD*lSy>%Q72No#&s^SEGo?3tqn_YoW>|0Qm6eXq?<1^q;pdgr{Cs>(?EP`fT2xXs zx5iQr7lNOycM=-hTjz?RQ#?_D0rVtt>BM;G93xxlTO;W1 zFBqN>Y#A)5y@>gCtzw~;(oEi2Y$k)5U87J*^hlwYv(o~cB3&V@3F^A16u1bC30cJOQ!Uv+Y&`AipsM-(6eD`NWA%0(x3OtP`yM7s%orL>c zT{IvYHbkez-v$$~?4GykvH1)pawYQErD`~MgP(kZqnZmUDn^}rr{FIlbyrp0x-H$( zN|gVkSzLyY^Q+g_*Q4pRaUfBpJcN?Jt>PpYxsWC(W0m5|rb4!WR}kK+SQG32twjPi{+r zptHI<#~i(J&5NGK0MB9tRsxX-dRA}kf%?Tn0guYn@Md81atZ8Wz)9~hR1$OiTwPi^ zzA$bqC_t4e;+(xdA4uQ;l|G8%;-3_}!1{i535n0Fx^=$}CPCD0!n`v|kzq-IJPeIS zh6P`C(0}+?WkX6z>asq@fKAFh;I#Kd5r{}yLE(t;EolQ|lbMMLJrk1*xW7>MAFv?> zU9Va`F>rA;cJs)CmXHImz`_H6Y$#2*40z3)yu2c?L}W?a1C6v{^t(vg`}?L7!|Jcf(=1%4r>9ZD6(tWa-w2;8mz9zNWoctm zRc2@BT&f^8IZK@UB7;1RDAtIiT9E`SMuU>##U4o!E*xHRo5& z_S(-puO{IgPAVh_@u}!C`8?D>_}j#AqWmwobcIkT7ou!UAyq(YbvO;CW7oXwmN1$n zu>qV~t&gxcP7hxYw6s*-;Cz9wvB_Zs?!gIG__S{K(m%@0$+_iYoFUA18F(-IE1Wbj zF_DJBlDa-ADT&K=_Jtm31TU2JXHg2s{c>}@)qulnXbgn^`7JXz@bWGT9d}a%y=rEL zw_A)x(q96UVP2*mY;*x!ZtDxr{d{h)AM}~--|7p&{)<(=OEWuH7W!vRe*#c2@YlC@ z=59H|?1Tf`O?&bH{H(9#rG-h~8-Ci{A`bX(V-UMtrcC2bSoBG$Uq7G}V(wKF*g_x_ zb##)pcERBD`CT^6Q(4K&hb7CKbzDAdwf`MJLPFv#zMq?&oevsFQ2Ojgz;;2)gde7H zdbwr32St57J%N&xR1C9QqzrI!!ixxpJwwDu#>>_T&(xcJlS2Sm-tkO%kMUpvM8tCf z(~ue+ZSC_K2k@$Tu2ZJ^t6@OZ+TLSune;qdURjxLDQ(;{|MRO&kAfe3bp$x?i?<@( z@4+rF(kKOT2CmiucqqZjuz&#c6kaFV^nq=1epdu=iORmdEgIPhb`CGdK;iOO>*Yr4 z;W7Q8=CAEUoK3StVbu3k4GhYZvcz3ZwBnMIiava}$;HLxA!KlRif~-q+X0Tq&>%R( zZuPkFoejZ8s{sTdcmw19Sg}@BLsKepzBNtM!|x@kg6EaP}sFWXhV%E-$fhNNOg*EcH&oxCnd*$2P-&kcFhN| zKYsk!|M<+ijiZozln-C_bn z0NKGp@9b4Dd6?bT2_FQ_ub&~3L_l3G34pp9Y~6fdU+ufDjZBp@Q1X0uqZ`TxrV?Qk zl@4Xu+d?MqE|EO!2OuN^g)CFk28mZd9QcvQWfh&4wjBi{4*~t?i29Mq_BvuzJ-Sp`Q TqMJED^wD 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 D$Kqj7 literal 0 HcmV?d00001 diff --git a/server b/server index 046ae83..5f46919 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 046ae83001dadb25c27184e24edddee4afc52eb3 +Subproject commit 5f4691951d4ccac77f5cb72376b80cc77c54e2cc diff --git a/src/commandwindow/CommandWindow.ts b/src/commandwindow/CommandWindow.ts index b9e4d07..0e64907 100644 --- a/src/commandwindow/CommandWindow.ts +++ b/src/commandwindow/CommandWindow.ts @@ -1,6 +1,13 @@ // Copyright 2024-2026 The MathWorks, Inc. import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as fsPromise from 'fs/promises'; +import * as path from 'path'; + +import * as vscodeTextmate from 'vscode-textmate'; +import * as oniguruma from 'vscode-oniguruma'; + import { CompletionList } from 'vscode-languageclient'; import { Notifier } from './MultiClientNotifier'; @@ -9,6 +16,9 @@ import { TextEvent, PromptState } from './MVMInterface'; import Notification from '../notifications/Notifications'; import { createResolvablePromise, ResolvablePromise } from '../utils/ResolvablePromise'; +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable no-control-regex */ + /** * Direction of cursor movement */ @@ -69,8 +79,15 @@ const ACTION_KEYS = { RESTORE_COLORS: ESC + '[27m', RED_FOREGROUND: ESC + '[31m', YELLOW_FOREGROUND: ESC + '[33m', + DEFAULT_FOREGROUND: ESC + '[39m', ALL_DEFAULT_COLORS: ESC + '[0m', - + RGB_FOREGROUND: (hexStr: string) => { + const noHashStr: string = hexStr.substring(1); + const r = Number('0x' + noHashStr.substring(0, 2)).toString(); + const g = Number('0x' + noHashStr.substring(2, 4)).toString(); + const b = Number('0x' + noHashStr.substring(4, 6)).toString(); + return ESC + `[38;2;${r};${g};${b}m` + }, COPY: '\x03', PASTE: '\x16', @@ -81,15 +98,12 @@ const ACTION_KEYS = { QUERY_CURSOR: ESC + '[6n', SET_CURSOR_STYLE_TO_BAR: ESC + '[5 q' }; -// eslint-disable-next-line no-control-regex + const LEFT_REGEX = /^(\x1b\[D)+$/; -// eslint-disable-next-line no-control-regex const RIGHT_REGEX = /^(\x1b\[C)+$/; -// eslint-disable-next-line no-control-regex const WIDE_CHAR_REGEX = /[\u3001-\u3015\u301C\u3040-\u30FF\u3131-\u314E\u3400-\u4DBF\u4e00-\u9FFF\uAC00-\uD7A3\uFF01-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B\uFF3C\uFF3D\uFF3F\uFF5B\uFF5D]/; const RELEASE_REGEX = /^R20([0-9]{2})(a|b)$/; -// eslint-disable-next-line no-control-regex const WARNING_SENTINAL_REGEX = /((?:\[\x08)|(?:\]\x08))/; const PROMPTS = { @@ -99,6 +113,11 @@ const PROMPTS = { BUSY_PROMPT: '' }; +const enum EncodedTokenDataConsts { + FOREGROUND_MASK = 0b00000000111111111000000000000000, + FOREGROUND_OFFSET = 15 +} + // A modification of the word boundary regex being used by VS Code when replacing completions. // The first part splits on numbers. The second/third parts split on quoted strings, ie. plot("Color"| // the fourth part splits on unquoted words (same as VS Code's original regex), @@ -112,6 +131,7 @@ type MatlabData = any; // eslint-disable-line @typescript-eslint/no-explicit-any * Represents command window. Is a pseudoterminal to be used as the input/output processor in a VS Code terminal. */ export default class CommandWindow implements vscode.Pseudoterminal { + private readonly _context: vscode.ExtensionContext; private readonly _writeEmitter: vscode.EventEmitter; private _initialized: boolean = false; @@ -146,9 +166,14 @@ export default class CommandWindow implements vscode.Pseudoterminal { private _currentInputPromptString?: string; + private _tokenizer?: vscodeTextmate.IGrammar; + private _tokenizerRegistry?: vscodeTextmate.Registry; + private readonly _eventHandlers: vscode.Disposable[] = [] - constructor (private readonly _mvm: MVM, private readonly _notifier: Notifier) { + constructor (private readonly _mvm: MVM, private readonly _notifier: Notifier, context: vscode.ExtensionContext) { + this._context = context; + this._eventHandlers.push( this._mvm.on(MVM.Events.output, this.addOutput.bind(this)), this._mvm.on(MVM.Events.clc, this.clear.bind(this)), @@ -168,6 +193,10 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._updateHasSelectionContext(); } + initialize (): void { + void this._initalizeTokenizer(); + } + /** * Called when a terminal with this pseudoterminal is opened. * @@ -231,20 +260,94 @@ export default class CommandWindow implements vscode.Pseudoterminal { this._updateHasSelectionContext(); } - private _writeCurrentPromptLine (): void { - if (this._activeAnchorIndex === undefined) { - this._writeEmitter.fire(this._currentPromptLine) + private _setRGBColor (color?: string): void { + if (color === undefined) { + this._writeEmitter.fire(ACTION_KEYS.DEFAULT_FOREGROUND); } else { - const selectionStart = this._currentPrompt.length + Math.min(this._activeCursorIndex, this._activeAnchorIndex); - const selectionEnd = this._currentPrompt.length + Math.max(this._activeCursorIndex, this._activeAnchorIndex); - const preSelection = this._currentPromptLine.slice(0, selectionStart); - const selection = this._currentPromptLine.slice(selectionStart, selectionEnd); - const postSelection = this._currentPromptLine.slice(selectionEnd); - this._writeEmitter.fire(preSelection); - this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); - this._writeEmitter.fire(selection); - this._writeEmitter.fire(ACTION_KEYS.RESTORE_COLORS); - this._writeEmitter.fire(postSelection); + this._writeEmitter.fire(ACTION_KEYS.RGB_FOREGROUND(color)); + } + } + + private _writeCurrentPromptLine (): void { + const selectionStart = this._activeAnchorIndex === undefined ? Infinity : Math.min(this._activeCursorIndex, this._activeAnchorIndex); + const selectionEnd = this._activeAnchorIndex === undefined ? Infinity : Math.max(this._activeCursorIndex, this._activeAnchorIndex); + + this._writeEmitter.fire(ACTION_KEYS.ALL_DEFAULT_COLORS); + + const line = this._stripCurrentPrompt(this._currentPromptLine); + if ((this._tokenizer == null) || line === '') { + this._writeEmitter.fire(this._currentPromptLine); + return; + } + + this._writeEmitter.fire(this._currentPrompt); + + // Tokenize the prompt line. The tokenizeLine2 call returns the result in binary format containing, + // among other data, the start and end indices and foreground color to use based on the current theme + const lineTokens = this._tokenizer.tokenizeLine2(this._stripCurrentPrompt(this._currentPromptLine), vscodeTextmate.INITIAL); + let selectionInProgress = false; + + // Iterate over each token and display it + for (let j = 0; j < lineTokens.tokens.length / 2; j++) { + // Unpack the current token data + const tokenStart = lineTokens.tokens[2 * j]; + const tokenEnd = lineTokens.tokens[2 * j + 2] ?? line.length; + const tokenMetaData = lineTokens.tokens[2 * j + 1]; + const foregroundId = ((tokenMetaData & EncodedTokenDataConsts.FOREGROUND_MASK) >>> EncodedTokenDataConsts.FOREGROUND_OFFSET); + const color = this._tokenizerRegistry?.getColorMap()[foregroundId]; + + // If the current selection is entirely within the current token + if (selectionStart >= tokenStart && selectionStart < tokenEnd && selectionEnd < tokenEnd) { + const preSelection = line.substring(tokenStart, selectionStart); + const selection = line.substring(selectionStart, selectionEnd); + const postSelection = line.substring(selectionEnd, tokenEnd); + this._setRGBColor(color); + this._writeEmitter.fire(preSelection); + this._setRGBColor(undefined); + this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); + this._writeEmitter.fire(selection); + this._writeEmitter.fire(ACTION_KEYS.RESTORE_COLORS); + this._setRGBColor(color); + this._writeEmitter.fire(postSelection); + + // If the current selection contains the start of the current token + } else if (selectionStart >= tokenStart && selectionStart < tokenEnd) { + const preSelection = line.substring(tokenStart, selectionStart); + const selection = line.substring(selectionStart, tokenEnd); + this._setRGBColor(color); + this._writeEmitter.fire(preSelection); + this._setRGBColor(undefined); + this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); + this._writeEmitter.fire(selection); + selectionInProgress = true; + + // If the current selection contains the end of the current token + } else if (selectionEnd >= tokenStart && selectionEnd < tokenEnd) { + const selection = line.substring(tokenStart, selectionEnd); + const postSelection = line.substring(selectionEnd, tokenEnd); + this._setRGBColor(undefined); + this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); + this._writeEmitter.fire(selection); + this._writeEmitter.fire(ACTION_KEYS.RESTORE_COLORS); + this._setRGBColor(color); + this._writeEmitter.fire(postSelection); + selectionInProgress = false; + + // If the current selection does not contain the start or end of the current selection at all. + } else { + const tokenStr = line.substring(tokenStart, tokenEnd); + if (selectionInProgress) { + this._writeEmitter.fire(ACTION_KEYS.INVERT_COLORS); + } else { + this._setRGBColor(color); + } + this._writeEmitter.fire(tokenStr); + if (selectionInProgress) { + this._writeEmitter.fire(ACTION_KEYS.RESTORE_COLORS); + } + } + + this._writeEmitter.fire(ACTION_KEYS.ALL_DEFAULT_COLORS); } } @@ -821,9 +924,9 @@ export default class CommandWindow implements vscode.Pseudoterminal { } const match = release.match(RELEASE_REGEX); if (match?.[1] !== undefined && Number.parseInt(match[1]) < 25) { - void this._mvm.eval(`try; if usejava('jvm'); com.mathworks.mde.cmdwin.CmdWinMLIF.setCWSize(${this._terminalDimensions.rows}, ${this._terminalDimensions.columns}); end; end;`); + void this._mvm.eval(`try; if usejava('jvm'); com.mathworks.mde.cmdwin.CmdWinMLIF.setCWSize(${this._terminalDimensions.rows}, ${this._terminalDimensions.columns}); end; end;`, false); } else { - void this._mvm.eval(`settings_vscode__ = settings; settings_vscode__.matlab.commandwindow.WindowSize.TemporaryValue = [${this._terminalDimensions.columns}, ${this._terminalDimensions.rows}]; clear settings_vscode__;`); + void this._mvm.eval(`settings_vscode__ = settings; settings_vscode__.matlab.commandwindow.WindowSize.TemporaryValue = [${this._terminalDimensions.columns}, ${this._terminalDimensions.rows}]; clear settings_vscode__;`, false); } this._lastSentTerminalDimensions = this._terminalDimensions; } @@ -1094,9 +1197,108 @@ export default class CommandWindow implements vscode.Pseudoterminal { return WIDE_CHAR_REGEX.test(char); } + /** + * Initialize the textmate tokenizer. This is the same as what the VS Code Editor uses and should guarentee identical results. + */ + private async _initalizeTokenizer (): Promise { + // Load the oniguruma regex library used by the tokenizer. + const wasmBin = fs.readFileSync(this._context.asAbsolutePath('node_modules/vscode-oniguruma/release/onig.wasm')).buffer; + + const vscodeOnigurumaLib = oniguruma.loadWASM(wasmBin as any).then(() => { + return { + createOnigScanner (patterns: any) { return new oniguruma.OnigScanner(patterns); }, + createOnigString (s: any) { return new oniguruma.OnigString(s); } + }; + }); + + const themeName = vscode.workspace.getConfiguration('workbench').get('colorTheme') as string; + + // Create a registry that can create a grammar from a scope name, providing it the MATLAB syntax file and the current theme. + const registry = new vscodeTextmate.Registry({ + theme: this._processTheme(themeName), + onigLib: vscodeOnigurumaLib, + loadGrammar: async () => { + const languageFile = await fsPromise.readFile(this._context.asAbsolutePath('syntaxes/Matlab.tmbundle/Syntaxes/MATLAB.tmLanguage')); + return vscodeTextmate.parseRawGrammar(languageFile.toString()); + } + }); + + // Handle theme changes by reprocessing the new theme and + vscode.window.onDidChangeActiveColorTheme(() => { + const themeName = vscode.workspace.getConfiguration('workbench').get('colorTheme') as string; + registry.setTheme(this._processTheme(themeName)); + }); + this._tokenizerRegistry = registry; + + // Load the MATLAB grammar + this._tokenizer = await registry.loadGrammar('source.matlab') ?? undefined; + } + + /** + * Convert the current VS Code theme into a form that can be used by the textmate tokenizer. + */ + private _processTheme (themeName: string): vscodeTextmate.IRawTheme { + const theme: vscodeTextmate.IRawTheme = { + name: themeName, + settings: [] + }; + + // Find the given theme extension. + let currentThemePath; + for (const extension of vscode.extensions.all) { + const themes = extension.packageJSON.contributes?.themes; + const currentTheme = themes?.find((theme: any) => theme.label === themeName || theme.id === themeName); + if (currentTheme) { + currentThemePath = path.join(extension.extensionPath, currentTheme.path); + break; + } + } + + // Load the theme and all recursively included themes. + const themePaths = []; + const themeFiles = []; + if (currentThemePath) { + themePaths.push(currentThemePath); + } + while (themePaths.length > 0) { + const themePath: string = themePaths.pop()!; + + let themeData: any; + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + themeData = require(themePath); + } catch (err) { + console.log('Error while loading and parsing theme file: ', themePath, err); + // Return the still empty theme in the case of an error. + return theme; + } + + if (themeData !== undefined) { + themeFiles.push(themeData); + if (themeData.include) { + themePaths.push(path.join(path.dirname(themePath), themeData.include)); + } + } + } + + // Create the data structure that maps from textmate paths to colors. + while (themeFiles.length > 0) { + const themeData = themeFiles.pop(); + if (themeData.tokenColors) { + theme.settings.push(...themeData.tokenColors); + } + } + + // Manually add parentheses, brackets, and braces to the theme, because this is usually handled by bracket matching, not syntax highlighting. + theme.settings.push({ scope: ['punctuation.section.parens', 'punctuation.section.brackets', 'punctuation.section.braces'], settings: { foreground: '#FFD703' } }); + + return theme; + } + dispose (): void { this._eventHandlers.forEach(eventHandler => eventHandler.dispose()); this._writeEmitter.dispose(); + this._tokenizerRegistry?.dispose(); } onDidWrite: vscode.Event; diff --git a/src/commandwindow/TerminalService.ts b/src/commandwindow/TerminalService.ts index 45516b0..6d5903c 100644 --- a/src/commandwindow/TerminalService.ts +++ b/src/commandwindow/TerminalService.ts @@ -20,11 +20,15 @@ export default class TerminalService extends BaseService { private _currentMatlabTerminal?: vscode.Terminal; private _terminalCreationPromise?: ResolvablePromise; private _timeout: NodeJS.Timeout | undefined; + private readonly _client: Notifier; - constructor (private readonly _client: Notifier, mvm: MVM) { + constructor (readonly client: Notifier, mvm: MVM, context: vscode.ExtensionContext) { super(); - this._commandWindow = new CommandWindow(mvm, _client); + this._client = client; + + this._commandWindow = new CommandWindow(mvm, this._client, context); + this._commandWindow.initialize(); this._terminalOptions = { name: 'MATLAB', diff --git a/src/extension.ts b/src/extension.ts index 598a1c2..0a39668 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,6 +23,7 @@ import * as LicensingUtils from './utils/LicensingUtils' import BaseService from './services/BaseService' import WorkspaceBrowserProvider from './workspacebrowser/WorkspaceBrowserProvider' import MatlabProjectService from './services/projects/MatlabProjectService' +import MatlabTestService from './services/testing/MatlabTestService' const CONNECTION_STATUS_COMMAND = 'matlab.changeMatlabConnection' const OPEN_SETTINGS_COMMAND = 'workbench.action.openSettings' @@ -82,7 +83,7 @@ class MatlabExtension extends BaseService { // Initialize MVM, Terminal, and Debugger const multiclientNotifier = new MultiClientNotifier(this.client) this.mvm = new MVM(multiclientNotifier) - this.terminalService = new TerminalService(multiclientNotifier, this.mvm) + this.terminalService = new TerminalService(multiclientNotifier, this.mvm, context) this.executionCommandProvider = new ExecutionCommandProvider(this.mvm, this.terminalService, this.telemetryLogger) this.matlabDebugger = new MatlabDebugger(this.mvm, multiclientNotifier, this.telemetryLogger, this.terminalService) @@ -99,6 +100,9 @@ class MatlabExtension extends BaseService { // Initialize MATLAB Project Service const matlabProjectService = new MatlabProjectService(this.client, this.mvm, this.telemetryLogger) + // Initialize MATLAB Test Service + const matlabTestService = new MatlabTestService(this.client, this.mvm, this.telemetryLogger, context) + // Add all disposable services to context subscriptions this.own( this.telemetryLogger, @@ -111,7 +115,8 @@ class MatlabExtension extends BaseService { defaultEditorService, this.sectionModel, sectionStylingService, - matlabProjectService + matlabProjectService, + matlabTestService ) // =============== Setup UI Affordances =============== // @@ -132,7 +137,7 @@ class MatlabExtension extends BaseService { vscode.commands.registerCommand('matlab.runSection', async () => await this.executionCommandProvider.handleRunSection(this.sectionModel)), vscode.commands.registerCommand('matlab.runSelection', async () => await this.executionCommandProvider.handleRunSelection()), vscode.commands.registerCommand('matlab.interrupt', () => this.executionCommandProvider.handleInterrupt()), - vscode.commands.registerCommand('matlab.openCommandWindow', async () => await this.terminalService.openTerminalOrBringToFront()), + vscode.commands.registerCommand('matlab.openCommandWindow', this.handleOpenCommandWindow.bind(this)), vscode.commands.registerCommand('matlab.addFolderToPath', async (uri: vscode.Uri) => await this.executionCommandProvider.handleAddFolderToPath(uri)), vscode.commands.registerCommand('matlab.addFolderAndSubfoldersToPath', async (uri: vscode.Uri) => await this.executionCommandProvider.handleAddFolderAndSubfoldersToPath(uri)), vscode.commands.registerCommand('matlab.changeDirectory', async (uri: vscode.Uri) => await this.executionCommandProvider.handleChangeDirectory(uri)), @@ -169,7 +174,15 @@ class MatlabExtension extends BaseService { * Starts the langauge client */ async start (): Promise { - await this.client.start() + if (vscode.workspace.isTrusted) { + await this.client.start() + } else { + this.showUntrustedWorkspaceError() + + this.own(vscode.workspace.onDidGrantWorkspaceTrust(async () => { + await this.client.start() + })) + } } getConnectionStatusBarItem (): vscode.StatusBarItem { @@ -186,6 +199,22 @@ class MatlabExtension extends BaseService { }) } + /** + * Shows an error message to indicate that the language server cannot be started + * from untrusted workspaces. + */ + private showUntrustedWorkspaceError (): void { + const manageAction = 'Manage Workspace Trust' + void vscode.window.showErrorMessage( + 'MATLAB language server cannot run in an untrusted workspace.', + manageAction + ).then(choice => { + if (choice === manageAction) { + void vscode.commands.executeCommand('workbench.trust.manage') + } + }) + } + /** * Sets up the connection to the MATLAB language server. * Does not start the language client. @@ -246,6 +275,11 @@ class MatlabExtension extends BaseService { * Handles user input about whether to connect or disconnect from MATLAB® */ private handleChangeMatlabConnection (): void { + if (!vscode.workspace.isTrusted) { + this.showUntrustedWorkspaceError() + return + } + const connect = 'Connect to MATLAB' const disconnect = 'Disconnect from MATLAB' const options = [connect, disconnect] @@ -285,6 +319,19 @@ class MatlabExtension extends BaseService { }) } + /** + * Handler for the `matlab.openCommandWindow` command. + * Verifies the workspace is trusted before opening the command window. + */ + private async handleOpenCommandWindow (): Promise { + if (!vscode.workspace.isTrusted) { + this.showUntrustedWorkspaceError() + return + } + + await this.terminalService.openTerminalOrBringToFront() + } + /** * Event handler called when the VS Code configuration is changed by the user */ diff --git a/src/notifications/Notifications.ts b/src/notifications/Notifications.ts index 0b60b80..9c71006 100644 --- a/src/notifications/Notifications.ts +++ b/src/notifications/Notifications.ts @@ -63,7 +63,13 @@ enum Notification { // MATLAB projects ProjectOpened = 'matlab/project/opened', - ProjectClosed = 'matlab/project/closed' + ProjectClosed = 'matlab/project/closed', + + // Testing + TestRunRequest = 'matlab/testing/run/request', + TestRunEvent = 'matlab/testing/run/event', + TestRunOutput = 'matlab/testing/run/output', + TestRunComplete = 'matlab/testing/run/complete' } export default Notification diff --git a/src/services/telemetry/TelemetryLogger.ts b/src/services/telemetry/TelemetryLogger.ts index a84fa0b..56c7146 100644 --- a/src/services/telemetry/TelemetryLogger.ts +++ b/src/services/telemetry/TelemetryLogger.ts @@ -9,7 +9,8 @@ const PRODUCT = 'ML_VS_CODE' const APPLICATION_NAME = 'MATLAB_EXTENSION_FOR_VSCODE' const APPLICATION_KEY = 'OWY3N2FkZTMtYWU1My00MjU3LThjZTktMzFmMTAyYjM0Njc5' -const ENDPOINT = 'https://udc-service.mathworks.com/udc/service/v1/events' +const ENDPOINT = 'https://udc-service-integ3.mathworks.com/udc/service/v1/events' +// const ENDPOINT = 'https://udc-service.mathworks.com/udc/service/v1/events' export interface TelemetryEvent { eventKey: string @@ -35,7 +36,9 @@ export default class TelemetryLogger extends BaseService { if (event.eventKey === 'ML_VS_CODE_SETTING_CHANGE') { // Do log when the `matlab.telemetry` setting changes - return (event.data as { setting_name: string }).setting_name === 'telemetry' + if ((event.data as { setting_name: string }).setting_name === 'telemetry') { + return true + } } // Otherwise, adhere to the `matlab.telemetry` setting diff --git a/src/services/testing/MatlabTestDiscovery.ts b/src/services/testing/MatlabTestDiscovery.ts new file mode 100644 index 0000000..8e8c56d --- /dev/null +++ b/src/services/testing/MatlabTestDiscovery.ts @@ -0,0 +1,381 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as path from 'path' +import * as vscode from 'vscode' + +import BaseService from '../BaseService' +import { MatlabMVMConnectionState, MVM } from '../../commandwindow/MVM' +import TelemetryLogger from '../telemetry/TelemetryLogger' +import { MatlabTestInfo, TestDiscoveryRawResult } from './MatlabTestInterfaces' + +const DISCOVERY_DEBOUNCE_MS = 500 +const WORKSPACE_STATE_KEY = 'matlab.testing.folders' +const WORKSPACE_STATE_FILES_KEY = 'matlab.testing.files' +const WORKSPACE_STATE_EXCLUDED_KEY = 'matlab.testing.excludedFiles' + +const STATUS_ITEM_ID = 'matlab-status-placeholder' + +export default class MatlabTestDiscovery extends BaseService { + private readonly testFolders: Set = new Set() + private readonly testFiles: Set = new Set() + private readonly excludedFiles: Set = new Set() + private debounceTimer: NodeJS.Timeout | undefined + private isDiscovering = false + private rediscoveryRequested = false + private fileWatchers: vscode.Disposable[] = [] + + constructor ( + private readonly controller: vscode.TestController, + private readonly mvm: MVM, + private readonly context: vscode.ExtensionContext, + private readonly telemetryLogger: TelemetryLogger + ) { + super() + + this.restorePersistedState() + this.setupResolveHandler() + this.registerEventListeners() + } + + private restorePersistedState (): void { + this.loadWorkspaceState(WORKSPACE_STATE_KEY, this.testFolders) + this.loadWorkspaceState(WORKSPACE_STATE_FILES_KEY, this.testFiles) + this.loadWorkspaceState(WORKSPACE_STATE_EXCLUDED_KEY, this.excludedFiles) + + if (this.mvm.getMatlabState() !== MatlabMVMConnectionState.CONNECTED && this.hasTestSources()) { + this.showStatusPlaceholder('Connect to MATLAB to discover tests') + } + } + + private loadWorkspaceState (key: string, target: Set): void { + const values = this.context.workspaceState.get(key, []) + values.forEach(v => target.add(v)) + } + + private setupResolveHandler (): void { + this.controller.resolveHandler = async () => { + if (this.mvm.getMatlabState() === MatlabMVMConnectionState.CONNECTED) { + await this.discoverAll() + } + } + } + + private registerEventListeners (): void { + this.own(this.mvm.on(MVM.Events.stateChanged, (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => { + if (newState === MatlabMVMConnectionState.CONNECTED) { + this.removeStatusPlaceholder() + void this.discoverAll() + } else if (newState === MatlabMVMConnectionState.DISCONNECTED) { + this.controller.items.replace([]) + if (this.hasTestSources()) { + this.showStatusPlaceholder('Connect to MATLAB to discover tests') + } + } + })) + + this.refreshFileWatchers() + } + + private refreshFileWatchers (): void { + this.disposeFileWatchers() + + for (const folder of this.testFolders) { + this.addFileWatcher(new vscode.RelativePattern(vscode.Uri.file(folder), '**/*.m')) + } + + for (const file of this.testFiles) { + const dir = vscode.Uri.file(path.dirname(file)) + this.addFileWatcher(new vscode.RelativePattern(dir, path.basename(file))) + } + } + + private addFileWatcher (pattern: vscode.RelativePattern): void { + const watcher = vscode.workspace.createFileSystemWatcher(pattern) + this.fileWatchers.push( + watcher, + watcher.onDidCreate(() => this.debouncedRediscover()), + watcher.onDidDelete(() => this.debouncedRediscover()), + watcher.onDidChange(() => this.debouncedRediscover()) + ) + } + + private disposeFileWatchers (): void { + this.fileWatchers.forEach(d => d.dispose()) + this.fileWatchers = [] + } + + override dispose (): void { + this.disposeFileWatchers() + super.dispose() + } + + /** Discovers tests from all registered folders and files, rebuilding the test tree. */ + async discoverAll (): Promise { + if (this.mvm.getMatlabState() !== MatlabMVMConnectionState.CONNECTED) { + return + } + if (this.isDiscovering) { + this.rediscoveryRequested = true + return + } + + this.isDiscovering = true + try { + this.controller.items.replace([]) + if (this.hasTestSources()) { + this.showStatusPlaceholder('Discovering tests...') + } + + if (this.testFolders.size > 0) { + await this.discoverFromPaths([...this.testFolders], 'folder') + } + + if (this.testFiles.size > 0) { + await this.discoverFromPaths([...this.testFiles], 'file') + } + + this.removeStatusPlaceholder() + } finally { + this.isDiscovering = false + if (this.rediscoveryRequested) { + this.rediscoveryRequested = false + void this.discoverAll() + } + } + } + + /** Prompts the user to select folder(s) and discovers tests within them. */ + async addTestFolder (): Promise { + const selected = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: true, + title: 'Select test folder(s)' + }) + if (selected == null || selected.length === 0) return + + for (const uri of selected) { + this.testFolders.add(uri.fsPath) + for (const excluded of this.excludedFiles) { + if (excluded.startsWith(uri.fsPath)) { + this.excludedFiles.delete(excluded) + } + } + } + await this.context.workspaceState.update(WORKSPACE_STATE_KEY, [...this.testFolders]) + await this.context.workspaceState.update(WORKSPACE_STATE_EXCLUDED_KEY, [...this.excludedFiles]) + this.refreshFileWatchers() + + if (this.mvm.getMatlabState() === MatlabMVMConnectionState.CONNECTED) { + const paths = selected.map(u => u.fsPath) + await this.discoverFromPaths(paths, 'folder') + } + } + + /** Prompts the user to select .m file(s) and discovers tests within them. */ + async addTestFile (): Promise { + const selected = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: true, + filters: { 'MATLAB Files': ['m'] }, + title: 'Select test file(s)' + }) + if (selected == null || selected.length === 0) return + + for (const uri of selected) { + this.testFiles.add(uri.fsPath) + this.excludedFiles.delete(uri.fsPath) + } + await this.context.workspaceState.update(WORKSPACE_STATE_FILES_KEY, [...this.testFiles]) + await this.context.workspaceState.update(WORKSPACE_STATE_EXCLUDED_KEY, [...this.excludedFiles]) + this.refreshFileWatchers() + + if (this.mvm.getMatlabState() === MatlabMVMConnectionState.CONNECTED) { + const paths = selected.map(u => u.fsPath) + await this.discoverFromPaths(paths, 'file') + } + } + + /** Removes a test item from the tree and excludes its file from future discovery. */ + async removeTestItem (target: vscode.TestItem): Promise { + if (target == null) return + + while (target.parent != null) { + target = target.parent + } + + const filePath = target.uri?.fsPath ?? target.id + this.excludedFiles.add(filePath) + this.testFiles.delete(filePath) + + this.controller.items.delete(target.id) + + await this.context.workspaceState.update(WORKSPACE_STATE_EXCLUDED_KEY, [...this.excludedFiles]) + await this.context.workspaceState.update(WORKSPACE_STATE_FILES_KEY, [...this.testFiles]) + this.refreshFileWatchers() + } + + private async discoverFromPaths (paths: string[], mode: 'file' | 'folder'): Promise { + try { + const mdaPaths = { mwtype: 'string', mwsize: [1, paths.length], mwdata: paths } + + const response = await this.mvm.feval( + 'matlabls.handlers.testing.discoverTests', 1, [mdaPaths, mode] + ) + + if ('error' in response) { + const error = (response as { error: Record }).error + const errMsg = typeof error?.msg === 'string' ? error.msg : '' + if (errMsg !== '') { + void vscode.window.showWarningMessage(`Test discovery error: ${errMsg}`) + } + return + } + + const discoveryResult = (response as { result: TestDiscoveryRawResult[] }).result?.[0] + + if (discoveryResult == null) { + void vscode.window.showWarningMessage('Test discovery: unexpected empty response from MATLAB') + return + } + + if (discoveryResult.error != null && discoveryResult.error !== '') { + void vscode.window.showWarningMessage(`Test discovery: ${discoveryResult.error}`) + return + } + + if (discoveryResult.warning != null && discoveryResult.warning !== '') { + void vscode.window.showWarningMessage(`Test discovery: ${discoveryResult.warning}`) + } + + const tests = this.parseRawResult(discoveryResult) + if (tests.length === 0) { + void vscode.window.showInformationMessage('No MATLAB tests found in the selected location.') + return + } + + this.buildTestTree(tests) + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'testing.discover', test_count: tests.length } + }) + } catch { + // MATLAB disconnected or feval rejected — leave tree as-is + } + } + + private parseRawResult (raw: TestDiscoveryRawResult): MatlabTestInfo[] { + const tests: MatlabTestInfo[] = [] + const names = this.unwrapCellArray(raw.names) + const filenames = this.unwrapCellArray(raw.filenames) + const procedureNames = this.unwrapCellArray(raw.procedureNames) + const testParentNames = this.unwrapCellArray(raw.testParentNames) + const parameterizations = this.unwrapCellArray(raw.parameterizations) + + for (let i = 0; i < names.length; i++) { + tests.push({ + name: names[i], + filename: filenames[i], + procedureName: procedureNames[i], + testParentName: testParentNames[i], + parameterization: parameterizations[i] + }) + } + return tests + } + + private unwrapCellArray (value: unknown): string[] { + if (Array.isArray(value)) { + return value as string[] + } + if (value != null && typeof value === 'object' && 'mwdata' in value) { + return (value as { mwdata: string[] }).mwdata + } + return [] + } + + private buildTestTree (tests: MatlabTestInfo[]): void { + const filtered = tests.filter(t => !this.excludedFiles.has(t.filename)) + const groupedByFile = this.groupBy(filtered, t => t.filename) + + for (const [filename, fileTests] of groupedByFile) { + const fileItem = this.getOrCreateFileItem(filename) + this.populateFileItem(fileItem, fileTests) + } + } + + private getOrCreateFileItem (filename: string): vscode.TestItem { + let fileItem = this.controller.items.get(filename) + if (fileItem == null) { + const fileUri = vscode.Uri.file(filename) + fileItem = this.controller.createTestItem(filename, path.basename(filename), fileUri) + fileItem.range = new vscode.Range(0, 0, 0, 0) + this.controller.items.add(fileItem) + } + fileItem.children.replace([]) + return fileItem + } + + private populateFileItem (fileItem: vscode.TestItem, fileTests: MatlabTestInfo[]): void { + const fileUri = fileItem.uri! + const groupedByMethod = this.groupBy(fileTests, t => t.procedureName) + + for (const [methodName, methodTests] of groupedByMethod) { + if (methodTests.length === 1 && methodTests[0].parameterization === '') { + const methodItem = this.controller.createTestItem(methodTests[0].name, methodName, fileUri) + methodItem.range = new vscode.Range(0, 0, 0, 0) + fileItem.children.add(methodItem) + } else { + this.createParameterizedMethodItem(fileItem, fileUri, methodName, methodTests) + } + } + } + + private createParameterizedMethodItem (fileItem: vscode.TestItem, fileUri: vscode.Uri, methodName: string, methodTests: MatlabTestInfo[]): void { + const methodId = `${methodTests[0].testParentName}/${methodName}` + const methodItem = this.controller.createTestItem(methodId, methodName, fileUri) + methodItem.range = new vscode.Range(0, 0, 0, 0) + fileItem.children.add(methodItem) + + for (const test of methodTests) { + const paramLabel = test.parameterization !== '' ? test.parameterization : test.name + const paramItem = this.controller.createTestItem(test.name, paramLabel, fileUri) + paramItem.range = new vscode.Range(0, 0, 0, 0) + methodItem.children.add(paramItem) + } + } + + private groupBy (items: T[], keyFn: (item: T) => string): Map { + const map = new Map() + for (const item of items) { + const key = keyFn(item) + const existing = map.get(key) ?? [] + existing.push(item) + map.set(key, existing) + } + return map + } + + private hasTestSources (): boolean { + return this.testFolders.size > 0 || this.testFiles.size > 0 + } + + private showStatusPlaceholder (label: string): void { + if (this.controller.items.get(STATUS_ITEM_ID) != null) return + const item = this.controller.createTestItem(STATUS_ITEM_ID, label) + item.canResolveChildren = false + this.controller.items.add(item) + } + + private removeStatusPlaceholder (): void { + this.controller.items.delete(STATUS_ITEM_ID) + } + + private debouncedRediscover (): void { + if (this.debounceTimer != null) { + clearTimeout(this.debounceTimer) + } + this.debounceTimer = setTimeout(() => { void this.discoverAll() }, DISCOVERY_DEBOUNCE_MS) + } +} diff --git a/src/services/testing/MatlabTestInterfaces.ts b/src/services/testing/MatlabTestInterfaces.ts new file mode 100644 index 0000000..58da5f2 --- /dev/null +++ b/src/services/testing/MatlabTestInterfaces.ts @@ -0,0 +1,56 @@ +// Copyright 2026 The MathWorks, Inc. + +export interface MatlabTestInfo { + name: string + filename: string + procedureName: string + testParentName: string + parameterization: string +} + +export interface TestDiscoveryRawResult { + names: unknown + filenames: unknown + procedureNames: unknown + testParentNames: unknown + parameterizations: unknown + error: string + warning: string +} + +export interface TestRunEvent { + type: 'started' | 'finished' | 'complete' + testName: string + status?: 'passed' | 'failed' | 'incomplete' + duration?: number + diagnostics?: TestDiagnosticInfo[] +} + +export interface TestDiagnosticInfo { + message: string + failedOnLine: number + failedInFile: string + stack: StackFrame[] +} + +export interface StackFrame { + file: string + name: string + line: number +} + +export interface TestRunRequest { + runId: string + testFiles: string[] + testNames?: string[] +} + +export interface TestRunEventNotification { + runId: string + event: Record +} + +export interface TestRunCompleteNotification { + runId: string + error?: string +} diff --git a/src/services/testing/MatlabTestRunner.ts b/src/services/testing/MatlabTestRunner.ts new file mode 100644 index 0000000..556726a --- /dev/null +++ b/src/services/testing/MatlabTestRunner.ts @@ -0,0 +1,227 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as vscode from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' + +import BaseService from '../BaseService' +import { MatlabMVMConnectionState, MVM } from '../../commandwindow/MVM' +import TelemetryLogger from '../telemetry/TelemetryLogger' +import Notification from '../../notifications/Notifications' +import { TestRunEventNotification, TestRunCompleteNotification } from './MatlabTestInterfaces' + +export default class MatlabTestRunner extends BaseService { + private readonly runProfile: vscode.TestRunProfile + private readonly activeRuns = new Map() + private readonly testItemsByName = new Map() + + constructor ( + private readonly controller: vscode.TestController, + private readonly client: LanguageClient, + private readonly mvm: MVM, + private readonly telemetryLogger: TelemetryLogger + ) { + super() + + this.runProfile = this.controller.createRunProfile( + 'Run Tests', + vscode.TestRunProfileKind.Run, + (request, token) => this.runTests(request, token) + ) + this.own(this.runProfile) + + this.own( + this.client.onNotification(Notification.TestRunEvent, (data: TestRunEventNotification) => { + this.handleTestRunEvent(data) + }), + this.client.onNotification(Notification.TestRunComplete, (data: TestRunCompleteNotification) => { + this.handleTestRunComplete(data) + }), + this.client.onNotification(Notification.TestRunOutput, (data: { runId: string, text: string }) => { + const run = this.activeRuns.get(data.runId) + if (run != null) { + run.appendOutput(data.text.replace(/\r?\n/g, '\r\n')) + } + }), + this.mvm.on(MVM.Events.stateChanged, (oldState: MatlabMVMConnectionState, newState: MatlabMVMConnectionState) => { + if (newState === MatlabMVMConnectionState.DISCONNECTED) { + this.endAllActiveRuns('MATLAB disconnected') + } + }) + ) + } + + public runAll (): void { + const request = new vscode.TestRunRequest() + void this.runTests(request, new vscode.CancellationTokenSource().token) + } + + private async runTests (request: vscode.TestRunRequest, token: vscode.CancellationToken): Promise { + if (this.mvm.getMatlabState() !== MatlabMVMConnectionState.CONNECTED) { + void vscode.window.showWarningMessage('Connect to MATLAB to run tests.') + return + } + + const run = this.controller.createTestRun(request) + const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + + this.activeRuns.set(runId, run) + + const testItems = this.collectTestItems(request) + const testFiles = new Set() + const testNames: string[] = [] + const isSelectiveRun = request.include != null && request.include.length > 0 + + for (const item of testItems) { + run.enqueued(item) + + const filePath = item.uri?.fsPath ?? '' + this.testItemsByName.set(`${filePath}::${item.id}`, item) + + if (item.uri != null) { + testFiles.add(item.uri.fsPath) + } + + if (isSelectiveRun) { + testNames.push(item.id) + } + } + + token.onCancellationRequested(() => { + this.mvm.interrupt() + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'testing.cancel' } + }) + }) + + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'testing.run', test_count: testItems.length } + }) + + void this.client.sendNotification(Notification.TestRunRequest, { + runId, + testFiles: [...testFiles], + testNames: isSelectiveRun ? testNames : undefined + }) + } + + private handleTestRunEvent (data: TestRunEventNotification): void { + const run = this.activeRuns.get(data.runId) + if (run == null) return + + const event = data.event + const testName = event.testName as string + const rawTestFile = (event.testFile as string) ?? '' + const testFile = rawTestFile !== '' ? vscode.Uri.file(rawTestFile).fsPath : '' + const testItem = this.testItemsByName.get(`${testFile}::${testName}`) + if (testItem == null) return + + if (event.type === 'started') { + run.started(testItem) + } else if (event.type === 'finished') { + this.applyTestResult(run, testItem, event) + } + } + + private applyTestResult (run: vscode.TestRun, testItem: vscode.TestItem, event: Record): void { + const durationMs = event.duration != null ? (event.duration as number) * 1000 : undefined + + switch (event.status) { + case 'passed': + run.passed(testItem, durationMs) + break + case 'failed': { + const messages = this.buildTestMessages(event.diagnostics) + run.failed(testItem, messages, durationMs) + break + } + case 'incomplete': { + run.skipped(testItem) + break + } + default: + run.skipped(testItem) + } + } + + private buildTestMessages (diagnostics: unknown): vscode.TestMessage[] { + const diagArray = this.unwrapDiagnostics(diagnostics) + if (diagArray.length === 0) { + return [new vscode.TestMessage('Test failed')] + } + + return diagArray.map((diag: Record) => { + const msgText = (diag.message as string) ?? 'Test failed' + const message = new vscode.TestMessage(msgText) + const failedInFile = diag.failedInFile as string + const failedOnLine = diag.failedOnLine as number + if (failedInFile != null && failedInFile !== '' && failedOnLine > 0) { + message.location = new vscode.Location( + vscode.Uri.file(failedInFile), + new vscode.Position(failedOnLine - 1, 0) + ) + } + return message + }) + } + + private unwrapDiagnostics (diagnostics: unknown): Array> { + if (Array.isArray(diagnostics)) { + return diagnostics as Array> + } + if (diagnostics != null && typeof diagnostics === 'object' && 'mwdata' in diagnostics) { + const mwdata = (diagnostics as { mwdata: unknown }).mwdata + if (Array.isArray(mwdata)) { + return mwdata as Array> + } + } + return [] + } + + private handleTestRunComplete (data: TestRunCompleteNotification): void { + const run = this.activeRuns.get(data.runId) + if (run == null) return + + if (data.error != null) { + void vscode.window.showErrorMessage(`MATLAB test run failed: ${data.error}`) + } + + run.end() + this.activeRuns.delete(data.runId) + } + + private endAllActiveRuns (error: string): void { + for (const [runId, run] of this.activeRuns) { + void vscode.window.showErrorMessage(`MATLAB test run failed: ${error}`) + run.end() + this.activeRuns.delete(runId) + } + } + + private collectTestItems (request: vscode.TestRunRequest): vscode.TestItem[] { + const items: vscode.TestItem[] = [] + + if (request.include != null && request.include.length > 0) { + for (const item of request.include) { + this.collectLeafItems(item, items) + } + } else { + this.controller.items.forEach(item => { + this.collectLeafItems(item, items) + }) + } + + return items + } + + private collectLeafItems (item: vscode.TestItem, result: vscode.TestItem[]): void { + if (item.children.size === 0) { + result.push(item) + } else { + item.children.forEach(child => { + this.collectLeafItems(child, result) + }) + } + } +} diff --git a/src/services/testing/MatlabTestService.ts b/src/services/testing/MatlabTestService.ts new file mode 100644 index 0000000..6a47c7c --- /dev/null +++ b/src/services/testing/MatlabTestService.ts @@ -0,0 +1,48 @@ +// Copyright 2026 The MathWorks, Inc. + +import * as vscode from 'vscode' +import { LanguageClient } from 'vscode-languageclient/node' + +import BaseService from '../BaseService' +import { MVM } from '../../commandwindow/MVM' +import TelemetryLogger from '../telemetry/TelemetryLogger' +import MatlabTestDiscovery from './MatlabTestDiscovery' +import MatlabTestRunner from './MatlabTestRunner' + +export default class MatlabTestService extends BaseService { + private readonly controller: vscode.TestController + private readonly discovery: MatlabTestDiscovery + private readonly runner: MatlabTestRunner + + constructor ( + private readonly client: LanguageClient, + private readonly mvm: MVM, + private readonly telemetryLogger: TelemetryLogger, + private readonly context: vscode.ExtensionContext + ) { + super() + + this.controller = vscode.tests.createTestController('matlab-tests', 'MATLAB Tests') + this.own(this.controller) + + void vscode.commands.executeCommand('setContext', 'matlab.testing.isActive', true) + + this.discovery = new MatlabTestDiscovery(this.controller, this.mvm, this.context, this.telemetryLogger) + this.own(this.discovery) + + this.runner = new MatlabTestRunner(this.controller, this.client, this.mvm, this.telemetryLogger) + this.own(this.runner) + + this.own( + vscode.commands.registerCommand('matlab.testing.runAllTests', () => this.handleRunAllTests()), + vscode.commands.registerCommand('matlab.testing.discoverTests', () => this.discovery.discoverAll()), + vscode.commands.registerCommand('matlab.testing.addTestFolder', () => this.discovery.addTestFolder()), + vscode.commands.registerCommand('matlab.testing.addTestFile', () => this.discovery.addTestFile()), + vscode.commands.registerCommand('matlab.testing.removeTestItem', (item: vscode.TestItem) => this.discovery.removeTestItem(item)) + ) + } + + private handleRunAllTests (): void { + this.runner.runAll() + } +} diff --git a/src/test/test-files/SampleTestClass.m b/src/test/test-files/SampleTestClass.m new file mode 100644 index 0000000..589f4dc --- /dev/null +++ b/src/test/test-files/SampleTestClass.m @@ -0,0 +1,15 @@ +classdef SampleTestClass < matlab.unittest.TestCase + methods (Test) + function testPassing(testCase) + testCase.verifyEqual(1+1, 2); + end + + function testFailing(testCase) + testCase.verifyEqual(1+1, 3); + end + + function testIncomplete(testCase) + testCase.assumeTrue(false); + end + end +end diff --git a/src/test/test-files/project-with-deps/matlab.toml b/src/test/test-files/project-with-deps/matlab.toml new file mode 100644 index 0000000..e45eb77 --- /dev/null +++ b/src/test/test-files/project-with-deps/matlab.toml @@ -0,0 +1,4 @@ +name = "My Project" + +[dependencies] +myPkg1 = "*" diff --git a/src/test/test-files/project-with-deps/repoFolder/myPkg1/resources/mpackage.json b/src/test/test-files/project-with-deps/repoFolder/myPkg1/resources/mpackage.json new file mode 100644 index 0000000..7291e86 --- /dev/null +++ b/src/test/test-files/project-with-deps/repoFolder/myPkg1/resources/mpackage.json @@ -0,0 +1,28 @@ +{ + "name": "myPkg1", + "version": "1.0.0", + "id": "adc86eb1-3725-468b-9bac-e2f6f46dd609", + "formerNames": [], + "displayName": "myPkg1", + "summary": "", + "tags": [], + "readme": "", + "provider": { + "name": "", + "organization": "", + "email": "", + "url": "" + }, + "folders": [], + "dependencies": [], + "releaseCompatibility": "", + "supportedPlatforms": [ + { + "platform": "any", + "architectures": [ + "any" + ] + } + ], + "schemaVersion": "1.2.0" +} diff --git a/src/test/tools/tester/TerminalTester.ts b/src/test/tools/tester/TerminalTester.ts index bbb4944..d5e3832 100644 --- a/src/test/tools/tester/TerminalTester.ts +++ b/src/test/tools/tester/TerminalTester.ts @@ -74,8 +74,127 @@ export class TerminalTester { return content.includes(expected) } + /** + * Assert that the terminal contains yellow-colored text matching the expected string. + */ + public async assertTextIsYellow (expected: string, message: string): Promise { + return await this.vs.poll(this.hasTextColor.bind(this, expected, 'yellow'), true, `Assertion on terminal color: ${message}`) + } + + /** + * Assert that the terminal contains text in the default (white) color. + */ + public async assertTextIsWhite (expected: string, message: string): Promise { + return await this.vs.poll(this.hasTextColor.bind(this, expected, 'white'), true, `Assertion on terminal color: ${message}`) + } + + /** + * Assert that the terminal contains text with inline color styling (from syntax highlighting). + */ + public async assertTextHasColor (expected: string, message: string): Promise { + return await this.vs.poll(this.hasInlineColor.bind(this, expected), true, `Assertion on terminal color: ${message}`) + } + + /** + * Assert that the terminal text matching expected has uniform color (no distinct syntax highlighting). + * When text is selected, xterm applies a single selection foreground color to all spans, + * replacing the per-token syntax colors. + */ + public async assertTextHasUniformColor (expected: string, message: string): Promise { + return await this.vs.poll(this.hasUniformColor.bind(this, expected), true, `Assertion on terminal color: ${message}`) + } + + private async getStylesForText (expected: string): Promise { + try { + const rows = await this.terminal.findElements(vet.By.css('.xterm-rows span')) + const styles: string[] = [] + for (const row of rows) { + const text = await row.getText() + if (text.includes(expected)) { + styles.push(await row.getAttribute('style') ?? '') + } + } + return styles + } catch { + return null + } + } + + private async hasUniformColor (expected: string): Promise { + const styles = await this.getStylesForText(expected) + if (styles == null || styles.length === 0) return false + return new Set(styles).size === 1 + } + + private async hasInlineColor (expected: string): Promise { + const styles = await this.getStylesForText(expected) + if (styles == null) return false + return styles.some(style => style.includes('color:')) + } + + private static readonly COLOR_SELECTORS: Record = { + yellow: '.xterm-fg-3, .xterm-fg-11' + } + + private async hasTextColor (expected: string, color: string): Promise { + if (color === 'white') { + return await this.isTextDefaultColor(expected) + } + const selector = TerminalTester.COLOR_SELECTORS[color] + const elements = await this.terminal.findElements(vet.By.css(selector)) + for (const el of elements) { + if ((await el.getText()).includes(expected)) { + return true + } + } + return false + } + + // White (default) means: text exists in the terminal DOM but has no xterm-fg-* color class + private async isTextDefaultColor (expected: string): Promise { + const rows = await this.terminal.findElements(vet.By.css('.xterm-rows span')) + for (const row of rows) { + const text = await row.getText() + if (text.includes(expected)) { + const classAttr = await row.getAttribute('class') + if (classAttr?.match(/xterm-fg-\d+/) == null) { + return true + } + } + } + return false + } + public async type (text: string): Promise { const container = await this.terminal.findElement(vet.By.className('xterm-helper-textarea')); return await container.sendKeys(text) } + + public async closeTerminal (): Promise { + await this.vs.executeCommand('workbench.action.togglePanel') + await this.assertMATLABTerminalNotVisible() + } + + public async assertMATLABTerminalNotVisible (timeout = 5000): Promise { + await this.vs.poll( + async () => !(await this.isPanelDisplayed()), + true, + 'Expected terminal panel to not be visible', + timeout + ) + } + + public async assertMATLABTerminalVisible (timeout = 30000): Promise { + await this.vs.poll( + async () => await this.isPanelDisplayed(), + true, + 'Expected terminal panel to be visible', + timeout + ) + } + + private async isPanelDisplayed (): Promise { + const panel = new vet.BottomBarPanel() + return await panel.isDisplayed().catch(() => false) + } } diff --git a/src/test/tools/tester/TestExplorerTester.ts b/src/test/tools/tester/TestExplorerTester.ts new file mode 100644 index 0000000..5c9bb58 --- /dev/null +++ b/src/test/tools/tester/TestExplorerTester.ts @@ -0,0 +1,66 @@ +// Copyright 2026 The MathWorks, Inc. +import * as assert from 'assert' +import * as vet from 'vscode-extension-tester' +import { VSCodeTester } from './VSCodeTester' + +export class TestExplorerTester { + private readonly vs: VSCodeTester + + public constructor (vs: VSCodeTester) { + this.vs = vs + } + + public async openTestExplorer (): Promise { + const activityBar = new vet.ActivityBar() + const testingControl = await activityBar.getViewControl('Testing') + assert.ok(testingControl, 'Testing activity bar icon should be present') + const title = await testingControl.getTitle() + assert.ok(title.startsWith('Testing'), `Expected Testing view control in activity bar, got: ${title}`) + await testingControl.openView() + } + + public async assertWelcomeContentVisible (): Promise { + const sideBar = new vet.SideBarView() + await this.vs.poll(async () => { + const elements = await sideBar.findElements(vet.By.xpath('//*[contains(text(), "Add Test Folder") or contains(text(), "Add Test File") or contains(text(), "MATLAB")]')) + return elements.length > 0 + }, true, 'Expected MATLAB Test Explorer content in Testing view', 10000) + } + + public async assertCommandsInPalette (expectedCommands: string[]): Promise { + const prompt = await this.vs.workbench.openCommandPrompt() as vet.InputBox + await prompt.setText('>MATLAB: Add Test') + + let labels: string[] = [] + await this.vs.poll(async () => { + const picks = await prompt.getQuickPicks() + labels = await Promise.all(picks.map(p => p.getLabel())) + return labels.length > 0 + }, true, 'Expected quick picks to appear after filtering', 10000) + + await prompt.cancel() + + for (const command of expectedCommands) { + this.assertCommandPresentOnce(labels, command) + } + } + + private assertCommandPresentOnce (labels: string[], command: string): void { + const matches = labels.filter(label => label.includes(command)) + assert.strictEqual(matches.length, 1, `Expected one "${command}" command`) + } + + public async assertNoErrorNotifications (): Promise { + const workbench = new vet.Workbench() + const notifications = await workbench.getNotifications() + for (const notification of notifications) { + const type = await notification.getType() + const message = await notification.getMessage() + assert.notStrictEqual( + type, + vet.NotificationType.Error, + `Unexpected error notification: ${message}` + ) + } + } +} diff --git a/src/test/tools/tester/TestSuite.ts b/src/test/tools/tester/TestSuite.ts index 02600f0..d928dd1 100644 --- a/src/test/tools/tester/TestSuite.ts +++ b/src/test/tools/tester/TestSuite.ts @@ -30,12 +30,15 @@ export class TestSuite { 'MATLAB.telemetry': false, 'MATLAB.startDebuggerAutomatically': true, 'window.dialogStyle': 'custom', + 'window.titleBarStyle': 'custom', 'terminal.integrated.copyOnSelection': true, + 'terminal.integrated.gpuAcceleration': 'off', // Forces xterm to use DOM renderer so terminal color classes are queryable by tests 'debug.toolBarLocation': 'docked', 'workbench.startupEditor': 'none', 'terminal.integrated.sendKeybindingsToShell': true, 'editor.action.toggleTabFocusMode': false }) + fs.writeFileSync(settingsjson, settings) this.vscodeSettings = settingsjson diff --git a/src/test/tools/tester/VSCodeTester.ts b/src/test/tools/tester/VSCodeTester.ts index 9b3e384..0cea5a7 100644 --- a/src/test/tools/tester/VSCodeTester.ts +++ b/src/test/tools/tester/VSCodeTester.ts @@ -240,6 +240,32 @@ export class VSCodeTester { return contextMenu! } + /** + * Right-click a specific file in the Explorer sidebar and return the context menu + */ + public async openFileContextMenu (filename: string): Promise { + const activityBar = new vet.ActivityBar() + const explorerControl = await activityBar.getViewControl('Explorer') + const view = await explorerControl?.openView() as vet.SideBarView + + let contextMenu: vet.ContextMenu | null = null + await this.poll(async () => { + try { + const content = view.getContent() + const sections = await content.getSections() + const section = sections[0] + await section.expand() + const item = await section.findItem(filename) as vet.ViewItem + if (item == null) return false + contextMenu = await item.openContextMenu() + return true + } catch (e) { + return false + } + }, true, `Expected to find and right-click ${filename} in Explorer`) + return contextMenu! + } + /** * Type text into an open input box and confirm with Enter */ diff --git a/src/test/ui/project.test.ts b/src/test/ui/project.test.ts index 9ac33bf..7b8c804 100644 --- a/src/test/ui/project.test.ts +++ b/src/test/ui/project.test.ts @@ -1,6 +1,7 @@ // Copyright 2026 The MathWorks, Inc. import { VSCodeTester } from '../tools/tester/VSCodeTester' -import { before, afterEach, after } from 'mocha'; +import { Key } from 'vscode-extension-tester' +import { before, after } from 'mocha'; import * as fs from 'fs'; import * as path from 'path'; @@ -8,25 +9,21 @@ suite('Project UI Tests', () => { let vs: VSCodeTester const workspaceFolder = path.resolve(__dirname, '..', '..', '..', '.s') + function cleanProjectFiles (): void { + const tomlFile = path.join(workspaceFolder, 'matlab.toml') + if (fs.existsSync(tomlFile)) fs.unlinkSync(tomlFile) + const repoFolder = path.join(workspaceFolder, 'repoFolder') + if (fs.existsSync(repoFolder)) fs.rmSync(repoFolder, { recursive: true }) + } + before(async () => { vs = new VSCodeTester(); + cleanProjectFiles() await vs.openEditor('hScript1.m') await vs.assertMATLABConnected() await vs.closeActiveEditor() }); - afterEach(async () => { - // Delete project - const projFile = path.join(workspaceFolder, 'TestProject.prj') - if (fs.existsSync(projFile)) { - fs.unlinkSync(projFile) - } - const resourcesDir = path.join(workspaceFolder, 'resources') - if (fs.existsSync(resourcesDir)) { - fs.rmSync(resourcesDir, { recursive: true }) - } - }); - after(async () => { await vs.disconnectFromMATLAB() }); @@ -42,5 +39,61 @@ suite('Project UI Tests', () => { // Close matlab project and verify status bar await vs.executeCommand('MATLAB: Close Project') await vs.assertStatusBarItemNotContains('MATLAB project', 'Expected MATLAB project status bar item to not be visible after closing project') + + // Cleanup + const projFile = path.join(workspaceFolder, 'TestProject.prj') + if (fs.existsSync(projFile)) fs.unlinkSync(projFile) + const resourcesDir = path.join(workspaceFolder, 'resources') + if (fs.existsSync(resourcesDir)) fs.rmSync(resourcesDir, { recursive: true }) + }); + + test('Test prompt when opening project with dependencies', async function () { + await vs.openMATLABTerminal() + + if (await vs.isMatlabVersionLessThan('R2026b')) { + this.skip() + } + + // Copy fixture files into workspace + const fixtureSource = path.resolve(__dirname, '..', 'test-files', 'project-with-deps') + fs.copyFileSync( + path.join(fixtureSource, 'matlab.toml'), + path.join(workspaceFolder, 'matlab.toml') + ) + fs.cpSync( + path.join(fixtureSource, 'repoFolder'), + path.join(workspaceFolder, 'repoFolder'), + { recursive: true } + ) + + // Register the package repository and ensure myPkg1 is not installed + const repoPath = path.join(workspaceFolder, 'repoFolder').replace(/\\/g, '/') + await vs.terminal.executeCommand('try,mpmRemoveRepository("myRepo");end') + await vs.terminal.executeCommand(`mpmAddRepository("myRepo","${repoPath}")`) + await vs.terminal.executeCommand('try,mpmuninstall("myPkg1",Prompt=false,Force=true);end') + + // Hide terminal panel so we can verify the extension brings it forward + await vs.terminal.closeTerminal() + + // Right-click matlab.toml and open project + const menu = await vs.openFileContextMenu('matlab.toml') + await menu.select('MATLAB: Project', 'MATLAB: Open Project...') + + // The extension should automatically bring the terminal forward with the prompt + await vs.terminal.assertMATLABTerminalVisible() + await vs.terminal.assertContains('Do you want to continue? [YES/no]:', 'Expected terminal to show dependency prompt') + + // Accept dependencies and verify the project opens + await vs.terminal.type('YES') + await vs.terminal.type(Key.ENTER) + await vs.assertStatusBarItemContains('MATLAB project', 'Expected project to open after installing dependencies', 60000) + + // Close project and verify + await vs.executeCommand('MATLAB: Close Project') + await vs.assertStatusBarItemNotContains('MATLAB project', 'Expected project status bar item to disappear after closing') + + // Cleanup + await vs.terminal.executeCommand('try,mpmuninstall("myPkg1",Prompt=false,Force=true);end') + cleanProjectFiles() }) }); diff --git a/src/test/ui/terminal.test.ts b/src/test/ui/terminal.test.ts index 6bfeb58..f665ede 100644 --- a/src/test/ui/terminal.test.ts +++ b/src/test/ui/terminal.test.ts @@ -5,6 +5,7 @@ import { Key } from 'selenium-webdriver'; suite('Terminal UI Tests', () => { let vs: VSCodeTester + let skipWarningColorTests: boolean before(async () => { vs = new VSCodeTester(); @@ -12,6 +13,7 @@ suite('Terminal UI Tests', () => { await vs.assertMATLABConnected() await vs.openMATLABTerminal() await vs.closeActiveEditor() + skipWarningColorTests = await vs.isMatlabVersionLessThan('R2025b') }); afterEach(async () => { @@ -101,6 +103,46 @@ suite('Terminal UI Tests', () => { await vs.terminal.assertContains('1 1 1 1 1 1', 'output should not be wrapped') }) + test('Test warning text appears yellow', async function () { + if (skipWarningColorTests) { + this.skip() + } + await vs.terminal.executeCommand("warning('test warning message')") + await vs.terminal.assertTextIsYellow('test warning message', 'warning output should be yellow') + }) + + test('Test normal output is white', async function () { + if (skipWarningColorTests) { + this.skip() + } + await vs.terminal.executeCommand("disp('normal text')") + await vs.terminal.assertTextIsWhite('normal text', 'normal output should be default color') + }) + + test('Test color resets after warning', async function () { + if (skipWarningColorTests) { + this.skip() + } + await vs.terminal.executeCommand("warning('yellow text')") + await vs.terminal.executeCommand("disp('after warning')") + await vs.terminal.assertTextIsYellow('yellow text', 'warning should be yellow') + await vs.terminal.assertTextIsWhite('after warning', 'text after warning should be default color') + }) + + test('Test syntax highlighting colors typed text', async () => { + await vs.terminal.type("x = 'hello'") + await vs.terminal.assertTextHasColor('hello', 'typed text should have syntax coloring') + await vs.terminal.type(Key.ESCAPE) + }) + + test('Test selected text is uncolored', async () => { + await vs.terminal.type("x = 'hello'") + await vs.terminal.assertTextHasColor('hello', 'typed text should have syntax coloring before selection') + await vs.terminal.type(Key.chord(Key.SHIFT, Key.HOME)) + await vs.terminal.assertTextHasUniformColor('hello', 'selected text should have uniform color, not syntax highlighting') + await vs.terminal.type(Key.ESCAPE) + }) + test('Test prompt string from input command', async () => { await vs.terminal.executeCommand('prompt = "Input a color :";') await vs.terminal.executeCommand('clc') diff --git a/src/test/ui/testExplorer.test.ts b/src/test/ui/testExplorer.test.ts new file mode 100644 index 0000000..2268607 --- /dev/null +++ b/src/test/ui/testExplorer.test.ts @@ -0,0 +1,42 @@ +// Copyright 2026 The MathWorks, Inc. +import { VSCodeTester } from '../tools/tester/VSCodeTester' +import { TestExplorerTester } from '../tools/tester/TestExplorerTester' +import { before, after } from 'mocha'; + +suite('Test Explorer UI Tests', () => { + let vs: VSCodeTester + let testExplorer: TestExplorerTester + + before(async () => { + vs = new VSCodeTester(); + testExplorer = new TestExplorerTester(vs) + await vs.openEditor('hScript1.m') + await vs.assertMATLABConnected() + await vs.closeActiveEditor() + }); + + after(async () => { + await vs.disconnectFromMATLAB() + }); + + test('Test Explorer view is available and shows welcome content', async () => { + await testExplorer.openTestExplorer() + await testExplorer.assertWelcomeContentVisible() + }) + + test('Testing commands appear in Command Palette', async () => { + await testExplorer.assertCommandsInPalette(['Add Test Folder', 'Add Test File']) + }) + + test('Run All Tests does not error when no tests are configured', async () => { + await vs.executeCommand('MATLAB: Run All Tests') + await testExplorer.assertNoErrorNotifications() + }) + + test('Welcome view persists after MATLAB disconnect', async () => { + await testExplorer.openTestExplorer() + await vs.disconnectFromMATLAB() + await testExplorer.assertWelcomeContentVisible() + await vs.connectToMATLAB() + }) +}); diff --git a/src/test/unit/.mocharc.js b/src/test/unit/.mocharc.js new file mode 100644 index 0000000..e503f2d --- /dev/null +++ b/src/test/unit/.mocharc.js @@ -0,0 +1,5 @@ +// Copyright 2026 The MathWorks, Inc. +module.exports = { + spec: 'out/test/unit/**/*.test.js', + timeout: 10000 +} diff --git a/src/test/unit/CommandWindow.test.ts b/src/test/unit/CommandWindow.test.ts index 0dba6f3..c30e406 100644 --- a/src/test/unit/CommandWindow.test.ts +++ b/src/test/unit/CommandWindow.test.ts @@ -31,7 +31,7 @@ function createTestCommandWindow (): CommandWindow { mockMvm.getMatlabState = () => MatlabMVMConnectionState.CONNECTED; mockMvm.emit = () => {}; - const cw = new CommandWindow(mockMvm, mockNotifier as any); + const cw = new CommandWindow(mockMvm, mockNotifier as any, null as any); cw.open({ rows: 30, columns: 100 }); // Simulate MATLAB becoming ready — sets prompt to '>> ' and state to READY diff --git a/src/test/unit/MatlabTestDiscovery.test.ts b/src/test/unit/MatlabTestDiscovery.test.ts new file mode 100644 index 0000000..fb105a7 --- /dev/null +++ b/src/test/unit/MatlabTestDiscovery.test.ts @@ -0,0 +1,228 @@ +// Copyright 2026 The MathWorks, Inc. + +/* eslint-disable import/first */ +// Register vscode mock BEFORE any imports that depend on it +// eslint-disable-next-line @typescript-eslint/no-var-requires +const vscode = require('./mocks/vscode') +const Module = require('module') +const originalResolveFilename = Module._resolveFilename +Module._resolveFilename = function (request: string, ...args: any[]) { + if (request === 'vscode') return 'vscode' + return originalResolveFilename.call(this, request, ...args) +}; +(require as any).cache.vscode = { + id: 'vscode', filename: 'vscode', loaded: true, + exports: vscode, children: [], paths: [], path: '', isPreloading: false, require: require +} + +import * as sinon from 'sinon' +import * as assert from 'assert' +import { EventEmitter } from 'events' + +// --- Mock Factories --- + +function createMockMvm (state = 'connected'): any { + const emitter = new EventEmitter() + const mvm = Object.assign(emitter, { + getMatlabState: sinon.stub().returns(state), + feval: sinon.stub().resolves({ + result: [{ + names: ['TestA/testMethod1'], + filenames: ['/test/TestA.m'], + procedureNames: ['testMethod1'], + testParentNames: ['TestA'], + parameterizations: [''], + error: '' + }] + }), + interrupt: sinon.stub() + }) + const originalOn = emitter.on.bind(emitter) + ;(mvm as any).on = (event: string, listener: (...args: any[]) => void) => { + originalOn(event, listener) + return new vscode.Disposable(() => emitter.removeListener(event, listener)) + } + return mvm +} + +function createMockContext (): any { + const state: Record = {} + return { + workspaceState: { + get: sinon.stub().callsFake((key: string, defaultValue?: any) => { + return state[key] ?? defaultValue + }), + update: sinon.stub().callsFake((key: string, value: any) => { + state[key] = value + return Promise.resolve() + }) + }, + _state: state + } +} + +function createMockTelemetryLogger (): any { + return { logEvent: sinon.stub() } +} + +function createMockController (): any { + const items = vscode.createMockTestItemCollection() + return { + items, + createTestItem: (id: string, label: string, uri?: any) => vscode.createMockTestItem(id, label, uri), + resolveHandler: undefined as any, + dispose: sinon.stub() + } +} + +// Track FileSystemWatcher callbacks +interface WatcherCallbacks { + onCreate: Array<() => void> + onDelete: Array<() => void> + onChange: Array<() => void> +} + +function setupFileSystemWatcherMock (): WatcherCallbacks { + const callbacks: WatcherCallbacks = { onCreate: [], onDelete: [], onChange: [] } + const originalCreateFSW = vscode.workspace.createFileSystemWatcher + ;(vscode.workspace as any).createFileSystemWatcher = sinon.stub().callsFake((_pattern: string) => { + return { + onDidCreate: (cb: () => void) => { callbacks.onCreate.push(cb); return new vscode.Disposable(() => {}) }, + onDidDelete: (cb: () => void) => { callbacks.onDelete.push(cb); return new vscode.Disposable(() => {}) }, + onDidChange: (cb: () => void) => { callbacks.onChange.push(cb); return new vscode.Disposable(() => {}) }, + dispose: () => {} + } + }) + return callbacks +} + +/** Imports MatlabTestDiscovery with the mocked vscode module. */ +async function importDiscovery (): Promise { + const mod = await import('../../services/testing/MatlabTestDiscovery') + return mod.default +} + +// --- Tests --- + +describe('MatlabTestDiscovery', () => { + let MatlabTestDiscovery: any + let mockMvm: any + let mockContext: any + let mockTelemetry: any + let controller: any + let watcherCallbacks: WatcherCallbacks + let clock: sinon.SinonFakeTimers + + before(async () => { + MatlabTestDiscovery = await importDiscovery() + }) + + beforeEach(() => { + mockMvm = createMockMvm('connected') + mockContext = createMockContext() + mockTelemetry = createMockTelemetryLogger() + controller = createMockController() + watcherCallbacks = setupFileSystemWatcherMock() + }) + + afterEach(() => { + sinon.restore() + if (clock) { + clock.restore() + } + }) + + describe('FileSystemWatcher', () => { + it('should trigger re-discovery when .m file is modified', async () => { + // Pre-populate workspace state with a test folder so discovery has sources + mockContext._state['matlab.testing.folders'] = ['/workspace/tests'] + + const discovery = new MatlabTestDiscovery(controller, mockMvm, mockContext, mockTelemetry) + + // Reset feval call count from constructor's initial discovery + mockMvm.feval.resetHistory() + + // Install fake timers to control debounce + clock = sinon.useFakeTimers() + + // Simulate .m file change + assert.ok(watcherCallbacks.onChange.length > 0, 'onChange listener should be registered') + watcherCallbacks.onChange[0]() + + // Advance past the 500ms debounce + clock.tick(600) + + // Allow async discoverAll to proceed + await Promise.resolve() + + sinon.assert.called(mockMvm.feval) + }) + + it('should create a RelativePattern watcher for each registered test source', () => { + mockContext._state['matlab.testing.folders'] = ['/workspace/tests', '/external/suite'] + mockContext._state['matlab.testing.files'] = ['/external/StandaloneTest.m'] + + new MatlabTestDiscovery(controller, mockMvm, mockContext, mockTelemetry) + + const createFSW = vscode.workspace.createFileSystemWatcher as sinon.SinonStub + // One watcher per registered source (2 folders + 1 file). + sinon.assert.calledThrice(createFSW) + + const patterns = createFSW.getCalls().map(c => c.args[0]) + patterns.forEach(p => assert.ok(p instanceof vscode.RelativePattern, 'expected a RelativePattern')) + + const folderWatch = patterns.find(p => p.base === '/workspace/tests') + assert.ok(folderWatch != null, 'expected a watcher for the registered folder') + assert.strictEqual(folderWatch.pattern, '**/*.m', 'folders are watched recursively') + + const fileWatch = patterns.find(p => p.base === '/external') + assert.ok(fileWatch != null, 'expected a watcher based on the file\'s directory') + assert.strictEqual(fileWatch.pattern, 'StandaloneTest.m', 'files are watched by exact name') + }) + + it('should recreate watchers when a test source is added or removed', async () => { + const discovery = new MatlabTestDiscovery(controller, mockMvm, mockContext, mockTelemetry) + const createFSW = vscode.workspace.createFileSystemWatcher as sinon.SinonStub + + // No sources registered at construction -> no watchers created. + sinon.assert.notCalled(createFSW) + + sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file('/external/suite')]) + await discovery.addTestFolder() + + // Adding a source spins up a watcher for it. + sinon.assert.calledOnce(createFSW) + assert.ok(createFSW.getCall(0).args[0] instanceof vscode.RelativePattern) + }) + + it('should debounce re-discovery (500ms) to avoid excessive calls', async () => { + mockContext._state['matlab.testing.folders'] = ['/workspace/tests'] + + const discovery = new MatlabTestDiscovery(controller, mockMvm, mockContext, mockTelemetry) + mockMvm.feval.resetHistory() + + clock = sinon.useFakeTimers() + + // Rapid file changes (simulating save-all or formatter) + watcherCallbacks.onChange[0]() + clock.tick(100) + watcherCallbacks.onChange[0]() + clock.tick(100) + watcherCallbacks.onChange[0]() + clock.tick(100) + watcherCallbacks.onChange[0]() + clock.tick(100) + watcherCallbacks.onChange[0]() + + // Only 400ms elapsed since last change — should NOT have discovered yet + assert.strictEqual(mockMvm.feval.callCount, 0) + + // Advance past debounce threshold (500ms from last change) + clock.tick(600) + await Promise.resolve() + + // Should trigger exactly one discovery + sinon.assert.calledOnce(mockMvm.feval) + }) + }) +}) diff --git a/src/test/unit/MatlabTestRunner.test.ts b/src/test/unit/MatlabTestRunner.test.ts new file mode 100644 index 0000000..e85120c --- /dev/null +++ b/src/test/unit/MatlabTestRunner.test.ts @@ -0,0 +1,447 @@ +// Copyright 2026 The MathWorks, Inc. + +/* eslint-disable import/first */ +// Register vscode mock BEFORE any imports that depend on it +// eslint-disable-next-line @typescript-eslint/no-var-requires +const vscode = require('./mocks/vscode') +const Module = require('module') +const originalResolveFilename = Module._resolveFilename +Module._resolveFilename = function (request: string, ...args: any[]) { + if (request === 'vscode') return 'vscode' + return originalResolveFilename.call(this, request, ...args) +}; +(require as any).cache.vscode = { + id: 'vscode', filename: 'vscode', loaded: true, + exports: vscode, children: [], paths: [], path: '', isPreloading: false, require: require +} + +import * as sinon from 'sinon' +import * as assert from 'assert' +import { EventEmitter } from 'events' +import Notification from '../../notifications/Notifications' + +// --- Mock Factories --- + +interface MockMVM extends EventEmitter { + getMatlabState: sinon.SinonStub + feval: sinon.SinonStub + interrupt: sinon.SinonStub + on: (event: string, listener: (...args: any[]) => void) => any +} + +function createMockMvm (state = 'connected'): MockMVM { + const emitter = new EventEmitter() + const mvm = Object.assign(emitter, { + getMatlabState: sinon.stub().returns(state), + feval: sinon.stub().resolves({}), + interrupt: sinon.stub() + }) + // Make on() return a Disposable + const originalOn = emitter.on.bind(emitter) + ;(mvm as any).on = (event: string, listener: (...args: any[]) => void) => { + originalOn(event, listener) + return new vscode.Disposable(() => emitter.removeListener(event, listener)) + } + return mvm as unknown as MockMVM +} + +function createMockClient (): any { + const listeners: Record = {} + return { + onNotification: sinon.stub().callsFake((name: string, handler: Function) => { + listeners[name] = handler + return new vscode.Disposable(() => { delete listeners[name] }) + }), + sendNotification: sinon.stub(), + _trigger: (name: string, data: any) => { + if (listeners[name]) listeners[name](data) + } + } +} + +function createMockTelemetryLogger (): any { + return { logEvent: sinon.stub() } +} + +function createStubbedTestRun (): any { + return { + enqueued: sinon.stub(), + started: sinon.stub(), + passed: sinon.stub(), + failed: sinon.stub(), + errored: sinon.stub(), + skipped: sinon.stub(), + appendOutput: sinon.stub(), + end: sinon.stub() + } +} + +function createStubbedController (run: any): any { + const items = vscode.createMockTestItemCollection() + return { + items, + createTestItem: (id: string, label: string, uri?: any) => vscode.createMockTestItem(id, label, uri), + createRunProfile: sinon.stub().callsFake((_label: string, _kind: any, handler: Function) => { + return { runHandler: handler, dispose: sinon.stub() } + }), + createTestRun: sinon.stub().returns(run), + resolveHandler: undefined, + dispose: sinon.stub() + } +} + +// --- Helpers --- + +/** Imports MatlabTestRunner with the mocked vscode module. */ +async function importRunner (): Promise { + const mod = await import('../../services/testing/MatlabTestRunner') + return mod.default +} + +/** Creates a test item and adds it to the controller. */ +function addTestItemToController (controller: any, id: string, label: string, filePath: string): any { + const uri = vscode.Uri.file(filePath) + const item = vscode.createMockTestItem(id, label, uri) + controller.items.add(item) + return item +} + +// --- Tests --- + +describe('MatlabTestRunner', () => { + let MatlabTestRunner: any + let mockMvm: MockMVM + let mockClient: any + let mockTelemetry: any + let mockRun: any + let controller: any + let runner: any + let runHandler: Function + + before(async () => { + MatlabTestRunner = await importRunner() + }) + + beforeEach(() => { + mockMvm = createMockMvm('connected') + mockClient = createMockClient() + mockTelemetry = createMockTelemetryLogger() + mockRun = createStubbedTestRun() + controller = createStubbedController(mockRun) + + runner = new MatlabTestRunner(controller, mockClient, mockMvm, mockTelemetry) + runHandler = controller.createRunProfile.firstCall.args[2] + }) + + afterEach(() => { + sinon.restore() + }) + + describe('Test Result Counts', () => { + it('should report pass/fail/skip counts correctly after test run completes', async () => { + const item1 = addTestItemToController(controller, 'TestA/testPass', 'testPass', '/test.m') + const item2 = addTestItemToController(controller, 'TestA/testFail', 'testFail', '/test.m') + const item3 = addTestItemToController(controller, 'TestA/testSkip', 'testSkip', '/test.m') + + const request = new vscode.TestRunRequest([item1, item2, item3]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'started', testName: 'TestA/testPass', testFile: '/test.m' } + }) + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'finished', testName: 'TestA/testPass', testFile: '/test.m', status: 'passed', duration: 0.1 } + }) + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'started', testName: 'TestA/testFail', testFile: '/test.m' } + }) + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'finished', testName: 'TestA/testFail', testFile: '/test.m', status: 'failed', diagnostics: [{ message: 'Expected 2, got 3' }] } + }) + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'started', testName: 'TestA/testSkip', testFile: '/test.m' } + }) + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'finished', testName: 'TestA/testSkip', testFile: '/test.m', status: 'incomplete' } + }) + + sinon.assert.calledOnce(mockRun.passed) + sinon.assert.calledOnce(mockRun.failed) + sinon.assert.calledOnce(mockRun.skipped) + }) + + it('should update counts correctly on re-run after fixing a test', async () => { + const item1 = addTestItemToController(controller, 'TestA/testFix', 'testFix', '/test.m') + + // First run: test fails + const request1 = new vscode.TestRunRequest([item1]) + const token1 = new vscode.CancellationTokenSource() + await runHandler(request1, token1.token) + + const runId1 = mockClient.sendNotification.firstCall.args[1].runId + mockClient._trigger(Notification.TestRunEvent, { + runId: runId1, + event: { type: 'finished', testName: 'TestA/testFix', testFile: '/test.m', status: 'failed', diagnostics: [{ message: 'assertion failed' }] } + }) + mockClient._trigger(Notification.TestRunComplete, { runId: runId1 }) + + sinon.assert.calledOnce(mockRun.failed) + assert.strictEqual(mockRun.passed.callCount, 0) + + // Second run: test passes (create fresh run) + const mockRun2 = createStubbedTestRun() + controller.createTestRun.returns(mockRun2) + + const request2 = new vscode.TestRunRequest([item1]) + const token2 = new vscode.CancellationTokenSource() + await runHandler(request2, token2.token) + + const runId2 = mockClient.sendNotification.secondCall.args[1].runId + mockClient._trigger(Notification.TestRunEvent, { + runId: runId2, + event: { type: 'finished', testName: 'TestA/testFix', testFile: '/test.m', status: 'passed', duration: 0.05 } + }) + + sinon.assert.calledOnce(mockRun2.passed) + assert.strictEqual(mockRun2.failed.callCount, 0) + }) + + }) + + describe('Real-time Streaming', () => { + it('should stream results in real-time (not batched at completion)', async () => { + const item1 = addTestItemToController(controller, 'T/test1', 'test1', '/t.m') + const item2 = addTestItemToController(controller, 'T/test2', 'test2', '/t.m') + + const request = new vscode.TestRunRequest([item1, item2]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + // First test starts and finishes — results applied immediately + mockClient._trigger(Notification.TestRunEvent, { runId, event: { type: 'started', testName: 'T/test1', testFile: '/t.m' } }) + sinon.assert.calledOnce(mockRun.started) + sinon.assert.calledWith(mockRun.started, item1) + + mockClient._trigger(Notification.TestRunEvent, { runId, event: { type: 'finished', testName: 'T/test1', testFile: '/t.m', status: 'passed', duration: 0.5 } }) + sinon.assert.calledOnce(mockRun.passed) + + // Second test hasn't started yet — verify no batching + assert.strictEqual(mockRun.started.callCount, 1) + + // Second test starts — incremental update + mockClient._trigger(Notification.TestRunEvent, { runId, event: { type: 'started', testName: 'T/test2', testFile: '/t.m' } }) + assert.strictEqual(mockRun.started.callCount, 2) + sinon.assert.calledWith(mockRun.started.secondCall, item2) + }) + }) + + describe('Diagnostics', () => { + it('should display structured diagnostic message for failed test', async () => { + const item = addTestItemToController(controller, 'T/testFail', 'testFail', '/test.m') + + const request = new vscode.TestRunRequest([item]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { + type: 'finished', + testName: 'T/testFail', + testFile: '/test.m', + status: 'failed', + duration: 0.3, + diagnostics: [{ + message: 'Verification failed: 1+1 is not equal to 3', + failedInFile: '/src/TestClass.m', + failedOnLine: 15, + stack: [{ file: '/src/TestClass.m', name: 'testFail', line: 15 }] + }] + } + }) + + sinon.assert.calledOnce(mockRun.failed) + const failedCall = mockRun.failed.firstCall + assert.strictEqual(failedCall.args[0], item) + + const messages = failedCall.args[1] + assert.strictEqual(messages.length, 1) + assert.strictEqual(messages[0].message, 'Verification failed: 1+1 is not equal to 3') + assert.ok(messages[0].location) + assert.strictEqual(messages[0].location.uri.fsPath, '/src/TestClass.m') + assert.strictEqual(messages[0].location.range.line, 14) // 0-indexed + }) + + it('should show all diagnostics when a single test has multiple failures', async () => { + const item = addTestItemToController(controller, 'T/testMulti', 'testMulti', '/test.m') + + const request = new vscode.TestRunRequest([item]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { + type: 'finished', + testName: 'T/testMulti', + testFile: '/test.m', + status: 'failed', + duration: 0.5, + diagnostics: [ + { message: 'First assertion failed', failedInFile: '/test.m', failedOnLine: 10, stack: [] }, + { message: 'Second assertion failed', failedInFile: '/test.m', failedOnLine: 12, stack: [] }, + { message: 'Third assertion failed', failedInFile: '/test.m', failedOnLine: 14, stack: [] } + ] + } + }) + + sinon.assert.calledOnce(mockRun.failed) + const messages = mockRun.failed.firstCall.args[1] + assert.strictEqual(messages.length, 3) + assert.strictEqual(messages[0].message, 'First assertion failed') + assert.strictEqual(messages[1].message, 'Second assertion failed') + assert.strictEqual(messages[2].message, 'Third assertion failed') + }) + + it('should report incomplete tests as skipped (not errored)', async () => { + const item = addTestItemToController(controller, 'T/testError', 'testError', '/test.m') + + const request = new vscode.TestRunRequest([item]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + // Incomplete with a real diagnostic (not just 'Test failed') -> errored + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { + type: 'finished', + testName: 'T/testError', + testFile: '/test.m', + status: 'incomplete', + duration: 0.1, + diagnostics: [{ + message: 'Error using foo\nUndefined function or variable "x"', + failedInFile: '/src/foo.m', + failedOnLine: 7, + stack: [{ file: '/src/foo.m', name: 'foo', line: 7 }] + }] + } + }) + + sinon.assert.calledOnce(mockRun.skipped) + assert.strictEqual(mockRun.errored.callCount, 0) + }) + + it('should set correct location for navigation with nested stack frames', async () => { + const item = addTestItemToController(controller, 'T/testNested', 'testNested', '/test.m') + + const request = new vscode.TestRunRequest([item]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { + type: 'finished', + testName: 'T/testNested', + testFile: '/test.m', + status: 'failed', + duration: 0.2, + diagnostics: [{ + message: 'Assertion failed in helper', + failedInFile: '/src/helpers/validateInput.m', + failedOnLine: 23, + stack: [ + { file: '/src/helpers/validateInput.m', name: 'validateInput', line: 23 }, + { file: '/src/TestClass.m', name: 'testNested', line: 45 }, + { file: '/src/TestClass.m', name: 'setup', line: 10 } + ] + }] + } + }) + + sinon.assert.calledOnce(mockRun.failed) + const message = mockRun.failed.firstCall.args[1][0] + + // Location should point to failedInFile/failedOnLine (top of stack) + assert.strictEqual(message.location.uri.fsPath, '/src/helpers/validateInput.m') + assert.strictEqual(message.location.range.line, 22) // 0-indexed (23 - 1) + }) + + }) + + describe('Cancellation', () => { + it('should interrupt running tests via mvm.interrupt() when cancelled', async () => { + const item = addTestItemToController(controller, 'T/testLong', 'testLong', '/test.m') + + const request = new vscode.TestRunRequest([item]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + // Verify interrupt not called yet + sinon.assert.notCalled(mockMvm.interrupt) + + // Cancel + tokenSource.cancel() + + sinon.assert.calledOnce(mockMvm.interrupt) + }) + + it('should preserve partial results from completed tests after cancel', async () => { + const item1 = addTestItemToController(controller, 'T/test1', 'test1', '/test.m') + const item2 = addTestItemToController(controller, 'T/test2', 'test2', '/test.m') + const item3 = addTestItemToController(controller, 'T/test3', 'test3', '/test.m') + + const request = new vscode.TestRunRequest([item1, item2, item3]) + const tokenSource = new vscode.CancellationTokenSource() + await runHandler(request, tokenSource.token) + + const runId = mockClient.sendNotification.firstCall.args[1].runId + + // First test completes successfully + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'finished', testName: 'T/test1', testFile: '/test.m', status: 'passed', duration: 0.1 } + }) + + // Second test starts + mockClient._trigger(Notification.TestRunEvent, { + runId, + event: { type: 'started', testName: 'T/test2', testFile: '/test.m' } + }) + + // Cancel mid-run + tokenSource.cancel() + + // Verify first test result is preserved + sinon.assert.calledOnce(mockRun.passed) + sinon.assert.calledWith(mockRun.passed, item1) + + // Complete the run (server sends complete after interrupt) + mockClient._trigger(Notification.TestRunComplete, { runId }) + + sinon.assert.calledOnce(mockRun.end) + // Passed result from test1 is still recorded + assert.strictEqual(mockRun.passed.callCount, 1) + }) + }) +}) diff --git a/src/test/unit/TelemetryLogger.test.ts b/src/test/unit/TelemetryLogger.test.ts new file mode 100644 index 0000000..7ff3212 --- /dev/null +++ b/src/test/unit/TelemetryLogger.test.ts @@ -0,0 +1,215 @@ +// Copyright 2026 The MathWorks, Inc. + +/* eslint-disable import/first */ + +// Use the unified vscode mock (registered by runTest.ts via registerMockVscode) +// eslint-disable-next-line @typescript-eslint/no-var-requires +const vscode = require('./mocks/vscode') + +// Mock node-fetch — capture calls without hitting the network +import mock = require('mock-require') + +const fetchCalls: Array<{ url: string, init: any }> = [] +let fetchResponse: { ok: boolean, status: number, statusText: string } = { + ok: true, + status: 200, + statusText: 'OK' +} +let fetchShouldReject: Error | undefined + +const fetchStub = (url: string, init: any): Promise => { + fetchCalls.push({ url, init }) + if (fetchShouldReject !== undefined) { + return Promise.reject(fetchShouldReject) + } + return Promise.resolve(fetchResponse) +} +mock('node-fetch', { default: fetchStub, __esModule: true }) + +import * as assert from 'assert' +import { suite, test, setup, teardown } from 'mocha' +import TelemetryLogger, { TelemetryEvent } from '../../services/telemetry/TelemetryLogger' + +// Wait for the fetch promise chain (.then/.catch) inside sendEvent to settle. +async function flushMicrotasks (): Promise { + await new Promise(resolve => setImmediate(resolve)) +} + +const EXTENSION_VERSION = '1.2.3' + +function makeEvent (overrides: Partial = {}): TelemetryEvent { + return { + eventKey: 'ML_VS_CODE_SOME_EVENT', + data: { foo: 'bar' }, + ...overrides + } +} + +suite('TelemetryLogger', () => { + let logger: TelemetryLogger + let consoleErrorStub: (...args: unknown[]) => void + let consoleErrorCalls: unknown[][] + let originalConsoleError: typeof console.error + + setup(() => { + // Reset shared mock state + vscode._state.isTelemetryEnabled = true + vscode._state.sessionId = 'test-session-id' + vscode._state.telemetrySetting = true + fetchCalls.length = 0 + fetchResponse = { ok: true, status: 200, statusText: 'OK' } + fetchShouldReject = undefined + + // Silence and capture console.error so failure-path tests can inspect it + consoleErrorCalls = [] + originalConsoleError = console.error + consoleErrorStub = (...args: unknown[]) => { consoleErrorCalls.push(args) } + console.error = consoleErrorStub as typeof console.error + + logger = new TelemetryLogger(EXTENSION_VERSION) + }) + + teardown(() => { + console.error = originalConsoleError + }) + + suite('shouldLogTelemetry gating', () => { + test('does not send when VS Code telemetry is disabled', async () => { + vscode._state.isTelemetryEnabled = false + + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 0) + }) + + test('does not send when VS Code telemetry is disabled, even for the telemetry setting change event', async () => { + vscode._state.isTelemetryEnabled = false + + logger.logEvent({ + eventKey: 'ML_VS_CODE_SETTING_CHANGE', + data: { setting_name: 'telemetry' } + }) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 0) + }) + + test('sends when both VS Code telemetry and matlab.telemetry are enabled', async () => { + vscode._state.isTelemetryEnabled = true + vscode._state.telemetrySetting = true + + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 1) + }) + + test('does not send when matlab.telemetry is disabled', async () => { + vscode._state.telemetrySetting = false + + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 0) + }) + + test('sends the telemetry setting-change event even when matlab.telemetry is disabled', async () => { + vscode._state.telemetrySetting = false + + logger.logEvent({ + eventKey: 'ML_VS_CODE_SETTING_CHANGE', + data: { setting_name: 'telemetry' } + }) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 1) + }) + }) + + suite('sendEvent request shape', () => { + test('POSTs to the configured endpoint', async () => { + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(fetchCalls.length, 1) + assert.strictEqual(fetchCalls[0].init.method, 'POST') + }) + + test('includes all required UDC headers with the extension version', async () => { + logger.logEvent(makeEvent()) + await flushMicrotasks() + + const headers = fetchCalls[0].init.headers + assert.strictEqual(headers['Content-Type'], 'application/json') + assert.strictEqual(headers['x-mw-udc-client-version'], '1.0') + assert.strictEqual(headers['x-mw-udc-application-name'], 'MATLAB_EXTENSION_FOR_VSCODE') + assert.strictEqual(headers['x-mw-udc-application-version'], EXTENSION_VERSION) + }) + + test('wraps event data in the UDC envelope with the correct product', async () => { + const data = { setting_name: 'installPath', new_value: '/opt/matlab' } + logger.logEvent({ eventKey: 'ML_VS_CODE_SETTING_CHANGE', data }) + await flushMicrotasks() + + const body = JSON.parse(fetchCalls[0].init.body) + assert.strictEqual(body.Event.length, 1) + + const entry = body.Event[0] + assert.strictEqual(entry.sessionKey, 'test-session-id') + assert.strictEqual(entry.eventKey, 'ML_VS_CODE_SETTING_CHANGE') + assert.ok(typeof entry.eventDate === 'string' && entry.eventDate.length > 0) + + const eventData = JSON.parse(entry.eventData) + assert.deepStrictEqual(eventData, { + logDDUXData: { + product: 'ML_VS_CODE', + keyValues: data + } + }) + }) + + test('event date is a valid ISO-8601 timestamp with the trailing Z sliced off', async () => { + logger.logEvent(makeEvent()) + await flushMicrotasks() + + const eventDate: string = JSON.parse(fetchCalls[0].init.body).Event[0].eventDate + // Should look like "2026-07-06T12:34:56.789" — 23 chars, no trailing Z + assert.ok(!eventDate.endsWith('Z'), `eventDate should not end in Z, got "${eventDate}"`) + assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$/.test(eventDate), + `eventDate should be ISO-8601 without trailing Z, got "${eventDate}"`) + }) + }) + + suite('failure handling', () => { + test('logs to console.error when the response is not ok', async () => { + fetchResponse = { ok: false, status: 500, statusText: 'Internal Server Error' } + + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(consoleErrorCalls.length, 1) + const message = String(consoleErrorCalls[0][0]) + assert.ok(message.includes('500'), `expected message to include status code, got: ${message}`) + assert.ok(message.includes('Internal Server Error'), + `expected message to include status text, got: ${message}`) + }) + + test('logs to console.error when fetch itself rejects', async () => { + fetchShouldReject = new Error('network down') + + logger.logEvent(makeEvent()) + await flushMicrotasks() + + assert.strictEqual(consoleErrorCalls.length, 1) + assert.strictEqual(consoleErrorCalls[0][0], 'Telemetry post error: ') + assert.strictEqual((consoleErrorCalls[0][1] as Error).message, 'network down') + }) + + test('does not throw synchronously when fetch rejects', () => { + fetchShouldReject = new Error('network down') + + assert.doesNotThrow(() => logger.logEvent(makeEvent())) + }) + }) +}) diff --git a/src/test/unit/mock-vscode.ts b/src/test/unit/mock-vscode.ts index 1820eca..90a088b 100644 --- a/src/test/unit/mock-vscode.ts +++ b/src/test/unit/mock-vscode.ts @@ -1,72 +1,25 @@ // Copyright 2026 The MathWorks, Inc. -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-explicit-any */ /** - * Minimal mock of the 'vscode' module for unit testing CommandWindow. - * Must be registered before importing CommandWindow. + * Registers the unified vscode mock into Node's module cache. + * Must be called before importing any module that depends on 'vscode'. */ -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() - } - } -}; +const mockVscode = require('./mocks/vscode') 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 'vscode'; } return originalResolveFilename.call(this, request, ...args); }; - require.cache.vscode = { + (require as any).cache.vscode = { id: 'vscode', filename: 'vscode', loaded: true, diff --git a/src/test/unit/mocks/vscode.ts b/src/test/unit/mocks/vscode.ts new file mode 100644 index 0000000..5402aae --- /dev/null +++ b/src/test/unit/mocks/vscode.ts @@ -0,0 +1,235 @@ +// Copyright 2026 The MathWorks, Inc. +// Unified mock of the 'vscode' module for all unit tests. + +/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-explicit-any */ + +export class Disposable { + private readonly callOnDispose: () => void + constructor (callOnDispose: () => void = () => {}) { + this.callOnDispose = callOnDispose + } + + dispose (): void { + this.callOnDispose() + } +} + +export class EventEmitter { + 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 = [] + } +} + +export class Uri { + readonly fsPath: string + readonly scheme: string + + private constructor (fsPath: string) { + this.fsPath = fsPath + this.scheme = 'file' + } + + static file (path: string): Uri { + return new Uri(path) + } + + toString (): string { + return `file://${this.fsPath}` + } +} + +export class RelativePattern { + readonly baseUri: Uri + readonly base: string + + constructor (base: Uri | string, public readonly pattern: string) { + if (typeof base === 'string') { + this.base = base + this.baseUri = Uri.file(base) + } else { + this.baseUri = base + this.base = base.fsPath + } + } +} + +export class Position { + constructor (public readonly line: number, public readonly character: number) {} +} + +export class Range { + constructor ( + public readonly startLine: number, + public readonly startCharacter: number, + public readonly endLine: number, + public readonly endCharacter: number + ) {} +} + +export class Location { + constructor (public readonly uri: Uri, public readonly range: Position | Range) {} +} + +export class TestMessage { + public location?: Location + + constructor (public readonly message: string) {} + + static diff (message: string, _expected: string, _actual: string): TestMessage { + return new TestMessage(message) + } +} + +export class CancellationTokenSource { + private _listeners: Array<() => void> = [] + private _isCancelled = false + + token = { + isCancellationRequested: false, + onCancellationRequested: (listener: () => void) => { + this._listeners.push(listener) + return new Disposable(() => {}) + } + } + + cancel (): void { + this._isCancelled = true + this.token.isCancellationRequested = true + this._listeners.forEach(l => l()) + } + + dispose (): void {} +} + +export enum TestRunProfileKind { + Run = 1, + Debug = 2, + Coverage = 3 +} + +// Mutable state for tests that need to control vscode.env / workspace.getConfiguration +export const _state = { + isTelemetryEnabled: true, + sessionId: 'test-session-id', + telemetrySetting: true as boolean | undefined +} + +export const env = { + get isTelemetryEnabled () { return _state.isTelemetryEnabled }, + get sessionId () { return _state.sessionId }, + clipboard: { + readText: async () => '', + writeText: async () => {} + } +} + +export const window = { + showWarningMessage: async (_msg: string) => undefined, + showErrorMessage: async (_msg: string) => undefined, + showInformationMessage: async (_msg: string) => undefined, + showOpenDialog: async (_options: any) => undefined, + onDidOpenTerminal: () => ({ dispose: () => {} }), + onDidCloseTerminal: () => ({ dispose: () => {} }), + onDidChangeActiveTerminal: () => ({ dispose: () => {} }), + createTerminal: () => ({}), + registerTerminalProfileProvider: () => ({ dispose: () => {} }) +} + +export const workspace = { + createFileSystemWatcher: (_pattern: string) => { + const watcher: any = { + onDidCreate: (_cb: () => void) => new Disposable(() => {}), + onDidDelete: (_cb: () => void) => new Disposable(() => {}), + onDidChange: (_cb: () => void) => new Disposable(() => {}), + dispose: () => {} + } + return watcher + }, + getConfiguration: (_section?: string) => ({ + get: (_key: string) => _state.telemetrySetting, + update: () => Promise.resolve() + }), + workspaceFolders: undefined as any +} + +export const tests = { + createTestController: (_id: string, _label: string) => createMockTestController() +} + +export const commands = { + registerCommand: (_command: string, _callback: (...args: any[]) => any) => new Disposable(() => {}), + executeCommand: async (_command: string, ..._args: any[]) => undefined +} + +export function createMockTestController (): any { + const items = createMockTestItemCollection() + return { + items, + createTestItem: (id: string, label: string, uri?: Uri) => createMockTestItem(id, label, uri), + createRunProfile: (_label: string, _kind: TestRunProfileKind, handler: any) => { + return { runHandler: handler, dispose: () => {} } + }, + createTestRun: (_request: any) => createMockTestRun(), + resolveHandler: undefined as any, + dispose: () => {} + } +} + +export function createMockTestRun (): any { + return { + enqueued: () => {}, + started: () => {}, + passed: () => {}, + failed: () => {}, + errored: () => {}, + skipped: () => {}, + end: () => {} + } +} + +export function createMockTestItem (id: string, label: string, uri?: Uri): any { + const children = createMockTestItemCollection() + return { + id, + label, + uri, + range: undefined as any, + canResolveChildren: false, + children, + parent: undefined as any + } +} + +export function createMockTestItemCollection (): any { + const map = new Map() + return { + get: (id: string) => map.get(id), + add: (item: any) => map.set(item.id, item), + delete: (id: string) => map.delete(id), + replace: (items: any[]) => { + map.clear() + items.forEach((item: any) => map.set(item.id, item)) + }, + forEach: (cb: (item: any) => void) => map.forEach(cb), + get size () { return map.size } + } +} + +export class TestRunRequest { + constructor ( + public readonly include?: any[], + public readonly exclude?: any[], + public readonly profile?: any + ) {} +} diff --git a/src/test/unit/runTest.ts b/src/test/unit/runTest.ts index b1352f3..1306e0d 100644 --- a/src/test/unit/runTest.ts +++ b/src/test/unit/runTest.ts @@ -5,12 +5,10 @@ import * as path from 'path' import * as Mocha from 'mocha' import * as glob from 'glob' -// Register mock before any test imports that depend on vscode registerMockVscode() - async function runTests (): Promise { const mocha = new Mocha({ - ui: 'tdd', + ui: 'bdd', reporter: 'spec' }) diff --git a/src/test/workspacebrowser/icons.test.ts b/src/test/workspacebrowser/icons.test.ts index 0efa43c..481f70c 100644 --- a/src/test/workspacebrowser/icons.test.ts +++ b/src/test/workspacebrowser/icons.test.ts @@ -21,6 +21,7 @@ suite('getIconFilename', () => { ['char', 'wsCharacter.svg'], ['logical', 'wsCheck.svg'], ['duration', 'wsClock.svg'], + ['dictionary', 'wsDataDictionary.svg'], ['datetime', 'wsDate.svg'], ['default', 'wsDefault.svg'], ['categorical', 'wsDots.svg'], diff --git a/src/test/workspacebrowser/provider/lifecycle.test.ts b/src/test/workspacebrowser/provider/lifecycle.test.ts index 687386d..916d4ef 100644 --- a/src/test/workspacebrowser/provider/lifecycle.test.ts +++ b/src/test/workspacebrowser/provider/lifecycle.test.ts @@ -21,10 +21,12 @@ function createProviderComponents (): { on: sinon.SinonStub } stateChangedCallback: (oldState: string, newState: string) => void + promptChangeCallback: (state: string, isIdle: boolean) => void sendNotification: sinon.SinonStub telemetryLogger: { logEvent: sinon.SinonStub } } { let stateChangedCallback: ((oldState: string, newState: string) => void) | undefined + let promptChangeCallback: ((state: string, isIdle: boolean) => void) | undefined const sendNotification = sinon.stub() const onNotification = sinon.stub().returns({ dispose: () => {} }) @@ -39,6 +41,8 @@ function createProviderComponents (): { on: sinon.stub().callsFake((event: string, cb: (...args: any[]) => void) => { if (event === 'stateChanged') { stateChangedCallback = cb + } else if (event === 'promptChange') { + promptChangeCallback = cb } return { dispose: () => {} } }) @@ -53,7 +57,7 @@ function createProviderComponents (): { 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 } + return { provider, mvm, stateChangedCallback: stateChangedCallback!, promptChangeCallback: promptChangeCallback!, sendNotification, telemetryLogger } } function createWebviewView (): { webviewView: any, postMessage: sinon.SinonStub, triggerDispose: () => void } { @@ -105,35 +109,6 @@ suite('WorkspaceBrowserProvider — lifecycle', () => { 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') @@ -204,4 +179,90 @@ suite('WorkspaceBrowserProvider — lifecycle', () => { expect(webviewView.webview.html).to.include('R2023a') }) }) + + // ── Prompt-Idle Priming ───────────────────────────────────────── + // The prime workaround only targets R2023a/R2023b where the backend's change + // listener goes dormant after `clear`. On R2024a+ the eval must be skipped. + + suite('prompt-idle priming', () => { + const PRIME_CMD = 'workspace__init__2981022=1;clear workspace__init__2981022;' + let clock: sinon.SinonFakeTimers + + setup(() => { + clock = sinon.useFakeTimers() + }) + + teardown(() => { + clock.restore() + }) + + test('fires eval for R2023b on idle', () => { + const { mvm, stateChangedCallback, promptChangeCallback } = createProviderComponents() + mvm.getMatlabRelease.returns('R2023b') + stateChangedCallback('disconnected', 'connected') + mvm.eval.resetHistory() + + promptChangeCallback('', true) + clock.tick(300) + + expect(mvm.eval.calledWith(PRIME_CMD, false)).to.be.true + }) + + test('fires eval for R2023a on idle', () => { + const { mvm, stateChangedCallback, promptChangeCallback } = createProviderComponents() + mvm.getMatlabRelease.returns('R2023a') + stateChangedCallback('disconnected', 'connected') + mvm.eval.resetHistory() + + promptChangeCallback('', true) + clock.tick(300) + + expect(mvm.eval.calledWith(PRIME_CMD, false)).to.be.true + }) + + test('does NOT fire eval for R2024a on idle', () => { + const { mvm, stateChangedCallback, promptChangeCallback } = createProviderComponents() + mvm.getMatlabRelease.returns('R2024a') + stateChangedCallback('disconnected', 'connected') + mvm.eval.resetHistory() + + promptChangeCallback('', true) + clock.tick(300) + + const primeCall = mvm.eval.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0] === PRIME_CMD + ) + expect(primeCall).to.be.undefined + }) + + test('does NOT fire eval when release is null', () => { + const { mvm, stateChangedCallback, promptChangeCallback } = createProviderComponents() + mvm.getMatlabRelease.returns(null) + stateChangedCallback('disconnected', 'connected') + mvm.eval.resetHistory() + + promptChangeCallback('', true) + clock.tick(300) + + const primeCall = mvm.eval.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0] === PRIME_CMD + ) + expect(primeCall).to.be.undefined + }) + + test('does NOT fire eval when not idle', () => { + const { mvm, stateChangedCallback, promptChangeCallback } = createProviderComponents() + mvm.getMatlabRelease.returns('R2023b') + stateChangedCallback('disconnected', 'connected') + mvm.eval.resetHistory() + + promptChangeCallback('', false) + clock.tick(300) + + const primeCall = mvm.eval.getCalls().find( + (c: sinon.SinonSpyCall) => c.args[0] === PRIME_CMD + ) + expect(primeCall).to.be.undefined + }) + }) }) diff --git a/src/test/workspacebrowser/provider/telemetry.test.ts b/src/test/workspacebrowser/provider/telemetry.test.ts new file mode 100644 index 0000000..3b23d2d --- /dev/null +++ b/src/test/workspacebrowser/provider/telemetry.test.ts @@ -0,0 +1,312 @@ +// Copyright 2026 The MathWorks, Inc. + +// Tests for all WSB telemetry events: panel open/close, edit, rename, delete, +// truncation shown, and change-limit clicked. + +import { expect } from 'chai' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { createProviderTestHarness } from './helpers' + +suite('WorkspaceBrowserProvider — telemetry', () => { + teardown(() => { + sinon.restore() + }) + + // ── Panel Open/Close ──────────────────────────────────────────── + + suite('wsbPanelOpened / wsbPanelClosed', () => { + test('logs wsbPanelOpened on resolveWebviewView', () => { + const { telemetryLogger } = createProviderTestHarness() + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelOpened', result: '' } + })).to.be.true + }) + + test('logs wsbPanelClosed on dispose', () => { + const { webviewView, telemetryLogger } = createProviderTestHarness() + telemetryLogger.logEvent.resetHistory() + + const disposeCallback = webviewView.onDidDispose.firstCall.args[0] + disposeCallback() + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbPanelClosed', result: '' } + })).to.be.true + }) + }) + + // ── wsbEditValue ──────────────────────────────────────────────── + + suite('wsbEditValue', () => { + test('logs success when eval completes without error', async () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'editValue', variable: 'x', newValue: '42' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbEditValue', result: 'success' } + })).to.be.true + }) + + test('logs notReady when MATLAB is not connected', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.getReadyPromise.rejects(new Error('not ready')) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'editValue', variable: 'x', newValue: '1' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbEditValue', result: 'notReady' } + })).to.be.true + }) + + test('logs error when feval returns error response', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.resolves({ error: { id: 'MATLAB:error', msg: 'bad', status: 'error' } }) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'editValue', variable: 'x', newValue: 'bad' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbEditValue', result: 'error' } + })).to.be.true + }) + + test('logs error when feval throws', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.rejects(new Error('connection lost')) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'editValue', variable: 'x', newValue: '1' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbEditValue', result: 'error' } + })).to.be.true + }) + }) + + // ── wsbRenameVariable ─────────────────────────────────────────── + + suite('wsbRenameVariable', () => { + test('logs success when rename completes without error', async () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'y' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbRenameVariable', result: 'success' } + })).to.be.true + }) + + test('logs invalidName when new name is not a valid identifier', async () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: '123bad' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbRenameVariable', result: 'invalidName' } + })).to.be.true + }) + + test('logs duplicate when new name already exists in cached rows', async () => { + const { serverHandler, webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + + // Populate cache with a variable named 'y' + 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', Value: '1', Size: '1x1', Class: 'double' }, { Name: 'y', Value: '2', Size: '1x1', Class: 'double' }] + }) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'y' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbRenameVariable', result: 'duplicate' } + })).to.be.true + }) + + test('logs notReady when MATLAB is not connected', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.getReadyPromise.rejects(new Error('not ready')) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'y' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbRenameVariable', result: 'notReady' } + })).to.be.true + }) + + test('logs error when feval returns error response', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + mvm.feval.resolves({ error: { id: 'MATLAB:error', msg: 'bad', status: 'error' } }) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'renameVariable', variable: 'x', newName: 'z' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbRenameVariable', result: 'error' } + })).to.be.true + }) + }) + + // ── wsbDeleteVariable ─────────────────────────────────────────── + + suite('wsbDeleteVariable', () => { + test('logs cancelled when user dismisses the confirmation dialog', async () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbDeleteVariable', result: 'cancelled' } + })).to.be.true + }) + + test('logs success when delete completes without error', async () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbDeleteVariable', result: 'success' } + })).to.be.true + }) + + test('logs notReady when MATLAB is not connected', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + mvm.getReadyPromise.rejects(new Error('not ready')) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbDeleteVariable', result: 'notReady' } + })).to.be.true + }) + + test('logs error when feval returns error response', async () => { + const { webviewHandler, mvm, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + sinon.stub(vscode.window, 'showWarningMessage').resolves('Delete' as any) + mvm.feval.resolves({ error: { id: 'MATLAB:error', msg: 'bad', status: 'error' } }) + sinon.stub(vscode.window, 'showErrorMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'deleteVariable', variable: 'x' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbDeleteVariable', result: 'error' } + })).to.be.true + }) + }) + + // ── wsbTruncationShown ────────────────────────────────────────── + + suite('wsbTruncationShown', () => { + test('logs when workspace exceeds variable limit', () => { + const { serverHandler, telemetryLogger } = createProviderTestHarness() + sinon.stub(vscode.window, 'showInformationMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + // Simulate server reporting size > default limit (500) + serverHandler({ type: 'Size', rowCount: 600, columnCount: 4 }) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbTruncationShown', result: '' } + })).to.be.true + }) + + test('does not re-fire on subsequent size updates', () => { + const { serverHandler, telemetryLogger } = createProviderTestHarness() + sinon.stub(vscode.window, 'showInformationMessage').resolves(undefined) + telemetryLogger.logEvent.resetHistory() + + serverHandler({ type: 'Size', rowCount: 600, columnCount: 4 }) + serverHandler({ type: 'Size', rowCount: 700, columnCount: 4 }) + + const truncationCalls = telemetryLogger.logEvent.getCalls().filter( + (c: sinon.SinonSpyCall) => c.args[0]?.data?.action_type === 'wsbTruncationShown' + ) + expect(truncationCalls).to.have.length(1) + }) + }) + + // ── wsbChangeLimitClicked ─────────────────────────────────────── + + suite('wsbChangeLimitClicked', () => { + test('logs when user clicks change-limit via webview message', () => { + const { webviewHandler, telemetryLogger } = createProviderTestHarness({ captureWebviewHandler: true }) + telemetryLogger.logEvent.resetHistory() + + webviewHandler({ type: 'openMaxVariablesSetting' }) + + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbChangeLimitClicked', result: '' } + })).to.be.true + }) + + test('logs when user clicks change-limit via info message toast', async () => { + const { serverHandler, telemetryLogger } = createProviderTestHarness() + const showInfo = sinon.stub(vscode.window, 'showInformationMessage').resolves('Change Maximum Variable Count' as any) + telemetryLogger.logEvent.resetHistory() + + serverHandler({ type: 'Size', rowCount: 600, columnCount: 4 }) + await new Promise(resolve => setTimeout(resolve, 0)) + + expect(showInfo.calledOnce).to.be.true + expect(telemetryLogger.logEvent.calledWith({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbChangeLimitClicked', result: '' } + })).to.be.true + }) + }) +}) diff --git a/src/test/workspacebrowser/provider/versionGate.test.ts b/src/test/workspacebrowser/provider/versionGate.test.ts index 085c2bb..55be0b5 100644 --- a/src/test/workspacebrowser/provider/versionGate.test.ts +++ b/src/test/workspacebrowser/provider/versionGate.test.ts @@ -58,6 +58,36 @@ suite('WorkspaceBrowserProvider — version gate', () => { }) }) + suite('needsPrime', () => { + test('R2023a needs priming', () => { + expect(WorkspaceBrowserProvider.needsPrime('R2023a')).to.be.true + }) + + test('R2023b needs priming', () => { + expect(WorkspaceBrowserProvider.needsPrime('R2023b')).to.be.true + }) + + test('R2024a does not need priming', () => { + expect(WorkspaceBrowserProvider.needsPrime('R2024a')).to.be.false + }) + + test('R2025a does not need priming', () => { + expect(WorkspaceBrowserProvider.needsPrime('R2025a')).to.be.false + }) + + test('R2022b does not need priming (below minimum)', () => { + expect(WorkspaceBrowserProvider.needsPrime('R2022b')).to.be.false + }) + + test('null release does not need priming', () => { + expect(WorkspaceBrowserProvider.needsPrime(null)).to.be.false + }) + + test('empty string does not need priming', () => { + expect(WorkspaceBrowserProvider.needsPrime('')).to.be.false + }) + }) + suite('getUnsupportedHtml', () => { test('returns HTML mentioning R2023a', () => { const html = getUnsupportedHtml() diff --git a/src/workspacebrowser/WorkspaceBrowserProvider.ts b/src/workspacebrowser/WorkspaceBrowserProvider.ts index 6479054..1147420 100644 --- a/src/workspacebrowser/WorkspaceBrowserProvider.ts +++ b/src/workspacebrowser/WorkspaceBrowserProvider.ts @@ -15,6 +15,9 @@ export const WSB_DEFAULT_MAX_VARIABLES = 500 // Debounce interval for coalescing rapid DataChanged events from the server const DATA_THROTTLE_MS = 300 +// Minimum interval between workspace browser backend refresh commands +const PRIME_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' @@ -45,6 +48,13 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc private dataRequestPending: boolean = false private dataRequestTimer: ReturnType | undefined + // Prevents overlapping backend refresh commands + private primePending: boolean = false + private primeThrottleTimer: ReturnType | undefined + + // Maintains whether the connected MATLAB release requires a refresh after each idle transition + private requiresPrime: boolean = false + // Prevents duplicate max-variables warnings during a single connection session private maxVarsMessageShown: boolean = false @@ -73,6 +83,16 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc }) ) + // MATLAB releases R2023a/b lose workspace change tracking after certain commands. + // Refresh the backend each time MATLAB becomes idle to keep updates flowing. + this.own( + mvm.on(MVM.Events.promptChange, (_state: string, isIdle: boolean) => { + if (isIdle && this.requiresPrime) { + this.schedulePrime() + } + }) + ) + // Re-render icons when the VS Code theme changes this.own( vscode.window.onDidChangeActiveColorTheme(() => { @@ -171,8 +191,10 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc private onMatlabStateChanged (newState: MatlabMVMConnectionState): void { if (newState === MatlabMVMConnectionState.CONNECTED) { + this.requiresPrime = WorkspaceBrowserProvider.needsPrime(this.mvm.getMatlabRelease()) this.onMatlabConnected() } else { + this.requiresPrime = false this.onMatlabDisconnected() } } @@ -208,6 +230,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc this.numColumns = 0 this.maxVarsMessageShown = false this.cancelPendingDataRequest() + this.cancelPendingPrime() if (this.view != null) { this.view.webview.html = getDisconnectedHtml() @@ -222,6 +245,11 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc return release >= WSB_MINIMUM_RELEASE } + static needsPrime (release: string | null): boolean { + if (release == null || release === '') return false + return release >= WSB_MINIMUM_RELEASE && release <= 'R2023b' + } + // 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) @@ -356,6 +384,10 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc this.handleStateChanged(msg.state as SavedState) break case 'openMaxVariablesSetting': + this.telemetryLogger.logEvent({ + eventKey: 'ML_VS_CODE_ACTIONS', + data: { action_type: 'wsbChangeLimitClicked', result: '' } + }) void vscode.commands.executeCommand('workbench.action.openSettings', MAX_VARS_SETTING_ID) break } @@ -385,6 +417,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc try { await this.mvm.getReadyPromise() } catch { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbEditValue', result: 'notReady' } }) this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message: 'MATLAB is not ready' }) return } @@ -392,11 +425,15 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc try { const response = await this.evalInWorkspace(`${variable} = ${newValue};`) if ('error' in response) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbEditValue', result: 'error' } }) const message = this.extractErrorMessage(response.error) this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message }) this.showMatlabError('editValue', message) + } else { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbEditValue', result: 'success' } }) } } catch (e: unknown) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbEditValue', result: 'error' } }) const message = e instanceof Error ? e.message : String(e) this.postToWebview({ type: 'operationError', operation: 'editValue', variable, message }) this.showMatlabError('editValue', message) @@ -406,6 +443,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc // 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)) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'invalidName' } }) 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 }) @@ -415,6 +453,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc // Client-side duplicate check against cached workspace state if (this.cachedRows?.some((row: WorkspaceVariable) => row.name === newName) === true) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'duplicate' } }) const message = `A variable named "${newName}" already exists` this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) this.showMatlabError('rename', message) @@ -424,6 +463,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc try { await this.mvm.getReadyPromise() } catch { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'notReady' } }) this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message: 'MATLAB is not ready' }) return } @@ -431,11 +471,15 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc try { const response = await this.evalInWorkspace(`${newName} = ${oldName}; clear('${oldName}');`) if ('error' in response) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'error' } }) const message = this.extractErrorMessage(response.error) this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) this.showMatlabError('rename', message) + } else { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'success' } }) } } catch (e: unknown) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbRenameVariable', result: 'error' } }) const message = e instanceof Error ? e.message : String(e) this.postToWebview({ type: 'operationError', operation: 'rename', variable: oldName, message }) this.showMatlabError('rename', message) @@ -449,11 +493,15 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc { modal: true }, 'Delete' ) - if (confirmation !== 'Delete') return + if (confirmation !== 'Delete') { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbDeleteVariable', result: 'cancelled' } }) + return + } try { await this.mvm.getReadyPromise() } catch { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbDeleteVariable', result: 'notReady' } }) this.postToWebview({ type: 'operationError', operation: 'delete', variable, message: 'MATLAB is not ready' }) return } @@ -461,11 +509,15 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc try { const response = await this.evalInWorkspace(`clear('${variable}');`) if ('error' in response) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbDeleteVariable', result: 'error' } }) const message = this.extractErrorMessage(response.error) this.postToWebview({ type: 'operationError', operation: 'delete', variable, message }) this.showMatlabError('delete', message) + } else { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbDeleteVariable', result: 'success' } }) } } catch (e: unknown) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbDeleteVariable', result: 'error' } }) const message = e instanceof Error ? e.message : String(e) this.postToWebview({ type: 'operationError', operation: 'delete', variable, message }) this.showMatlabError('delete', message) @@ -496,6 +548,27 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc return await this.mvm.feval('evalin', 0, ['base', command], false) } + // ── Prime Throttling ─────────────────────────────────────────────── + + private schedulePrime (): void { + if (this.primePending) return + this.primePending = true + this.primeThrottleTimer = setTimeout(() => { + this.primeThrottleTimer = undefined + void this.mvm.eval('workspace__init__2981022=1;clear workspace__init__2981022;', false).then(() => { + this.primePending = false + }) + }, PRIME_THROTTLE_MS) + } + + private cancelPendingPrime (): void { + if (this.primeThrottleTimer != null) { + clearTimeout(this.primeThrottleTimer) + this.primeThrottleTimer = undefined + } + this.primePending = false + } + // ── Data Request Throttling ────────────────────────────────────── // Coalesces rapid DataChanged events to avoid flooding the server with GetData requests @@ -579,6 +652,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc const max = this.getMaxVariableCount() if (this.numRows > max && !this.maxVarsMessageShown) { this.maxVarsMessageShown = true + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbTruncationShown', result: '' } }) const message = MAX_VARIABLES_MESSAGE_TEMPLATE .replace('{currentCount}', this.numRows.toString()) .replace('{maxVariables}', max.toString()) @@ -586,6 +660,7 @@ export default class WorkspaceBrowserProvider extends BaseService implements vsc void vscode.window.showInformationMessage(message, MAX_VARS_BUTTON_TEXT) .then((selection: string | undefined) => { if (selection === MAX_VARS_BUTTON_TEXT) { + this.telemetryLogger.logEvent({ eventKey: 'ML_VS_CODE_ACTIONS', data: { action_type: 'wsbChangeLimitClicked', result: '' } }) void vscode.commands.executeCommand('workbench.action.openSettings', MAX_VARS_SETTING_ID) } }) diff --git a/src/workspacebrowser/icons.ts b/src/workspacebrowser/icons.ts index 911b6c0..bb1efff 100644 --- a/src/workspacebrowser/icons.ts +++ b/src/workspacebrowser/icons.ts @@ -9,6 +9,7 @@ const ICON_MAP: Record = { char: 'wsCharacter.svg', logical: 'wsCheck.svg', duration: 'wsClock.svg', + dictionary: 'wsDataDictionary.svg', datetime: 'wsDate.svg', default: 'wsDefault.svg', categorical: 'wsDots.svg', diff --git a/src/workspacebrowser/resources/icons/dark/wsDataDictionary.svg b/src/workspacebrowser/resources/icons/dark/wsDataDictionary.svg new file mode 100644 index 0000000..522b75b --- /dev/null +++ b/src/workspacebrowser/resources/icons/dark/wsDataDictionary.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/workspacebrowser/resources/icons/light/wsDataDictionary.svg b/src/workspacebrowser/resources/icons/light/wsDataDictionary.svg new file mode 100644 index 0000000..7e6169f --- /dev/null +++ b/src/workspacebrowser/resources/icons/light/wsDataDictionary.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +