diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5e8e50dc..f52378a3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,7 @@ assignees: '' --- + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7927858a..3c446f2c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,6 +7,7 @@ assignees: '' --- + diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e2131603..e7a738b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,7 +69,7 @@ jobs: run: xvfb-run npm test - name: Build package run: | - npx vsce package -o ${{ steps.set-version.outputs.name }}.vsix + npx @vscode/vsce package -o ${{ steps.set-version.outputs.name }}.vsix - uses: actions/upload-artifact@v6 if: github.event_name != 'release' with: @@ -142,7 +142,7 @@ jobs: git push - name: Build package run: | - npx vsce package -o ${{ steps.set-version.outputs.name }}.vsix + npx @vscode/vsce package -o ${{ steps.set-version.outputs.name }}.vsix - name: Upload Release Asset id: upload-release-asset uses: softprops/action-gh-release@v3 @@ -153,7 +153,7 @@ jobs: - name: Publish to VSCode Marketplace run: | [ -n "${{ secrets.VSCE_TOKEN }}" ] && \ - npx vsce publish --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true + npx @vscode/vsce publish --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true - name: Publish to Open VSX Registry timeout-minutes: 5 run: | diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 52e21d61..15f36e91 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -64,7 +64,7 @@ jobs: run: xvfb-run npm test - name: Build pre-release package run: | - npx vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix + npx @vscode/vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix - uses: actions/upload-artifact@v6 if: github.event_name != 'release' with: @@ -138,7 +138,7 @@ jobs: git push - name: Build pre-release package run: | - npx vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix + npx @vscode/vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix - name: Upload Release Asset id: upload-release-asset uses: softprops/action-gh-release@v3 @@ -149,4 +149,4 @@ jobs: - name: Publish to VSCode Marketplace run: | [ -n "${{ secrets.VSCE_TOKEN }}" ] && \ - npx vsce publish --pre-release --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true + npx @vscode/vsce publish --pre-release --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true diff --git a/.vscode/settings.json b/.vscode/settings.json index ebd59163..e7bd40cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,7 @@ "editor.formatOnSave": true }, "js/ts.tsdk.path": "node_modules/typescript/lib", + "git.branchProtection": [ + "master" + ], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 867fa4da..ea477c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Change Log +## [3.8.3] 11-Sep-2026 (Consistem) +- Enhancements + - Synchronize the Consistem fork with upstream through `v3.8.6-beta.8`. + - Preserve Consistem navigation, conversion, namespace activation and internal unit-test integrations. +- Fixes + - Incorporate upstream authentication, connection lifecycle, Podman and dependency updates. + +## [3.8.5] 02-Sep-2026 +- Fixes + - Fix crash when opening a file with no workspace folder open (#1834) + - Skip credential resolution for inactive connections during activation (#1835) + - Fix crash from unresolved auth on named-server connections (#1836) + - Fix intermittent issues when importing a client-side folder on Windows (#1839) + - Restore unauthenticated-connection support (regression from OAuth2 rewrite) (#1840) + - Fix server-side projects when connected to a very old server (#1842) + - `serverForUri()` should either return a usable server definition or `undefined` (#1843) + - Deprecate configuring a server host/port/credentials in `objectscript.conn` in favor of using Server Manager (#1844) + - Relax server-side URI validation for routines whose dotted section before the extension is only digits (#1845) + - Passwords stored in plaintext in `objectscript.conn` should be passed to other extensions in `serverForUri()` (#1846) + - Upgrade dependencies + +## [3.8.4] 20-Aug-2026 +- Fixes + - Fix issue where exported `serverForUri()` function may expose a password (#1824) + - Sending stale cookies can cause authorization failures (#1824) + - Support abstract documents when importing a client-side folder (#1827) + - Fix issue where user is intermittently unable to connect to server (#1830) + - Telemetry changes: + - Remove `clientSideFileSynced` event + - Add `usesServerManager` key to `workspaceFolder` event for client-side folders + +## [3.8.3] 11-Aug-2026 +- Enhancements + - Support OAuth2 for authorizing with InterSystems servers (#1799, #1820) + - Update `Open InterSystems Document...` command to show a flat list of documents (#1804) +- Fixes + - Always overwrite entire class for Pull Server Changes when there's a conflict between the local copy and server copy (#1797) + - Improve management of web sessions when connection changes (#1808) + - Improve performance of server-side project query (#1809) + - Only timeout server connection check during extension activation (#1814) + - Sync server-side web app files as raw bytes (#1818) + - Upgrade dependencies + ## [3.8.2] 04-Jun-2026 - Enhancements - Add `Import Files...` command that supports many file types (#1764) diff --git a/README.md b/README.md index ca1d5c49..f869068d 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,11 @@ # Consistem ObjectScript extension for VS Code -[![](https://img.shields.io/visual-studio-marketplace/v/consistem-sistemas.consistem-vscode-objectscript.svg)](https://marketplace.visualstudio.com/items?itemName=consistem-sistemas.consistem-vscode-objectscript) -[![](https://img.shields.io/visual-studio-marketplace/i/consistem-sistemas.consistem-vscode-objectscript.svg)](https://marketplace.visualstudio.com/items?itemName=consistem-sistemas.consistem-vscode-objectscript) -[![Known Vulnerabilities](https://snyk.io/test/github/intersystems-community/vscode-objectscript/badge.svg)](https://snyk.io/test/github/intersystems-community/vscode-objectscript) +[![Visual Studio Marketplace Version](https://vsmarketplacebadges.dev/version/consistem-sistemas.consistem-vscode-objectscript.svg)](https://marketplace.visualstudio.com/items?itemName=consistem-sistemas.consistem-vscode-objectscript) +[![Visual Studio Marketplace Installs](https://vsmarketplacebadges.dev/installs/consistem-sistemas.consistem-vscode-objectscript.svg)](https://marketplace.visualstudio.com/items?itemName=consistem-sistemas.consistem-vscode-objectscript) [![Consistem](https://img.shields.io/badge/Consistem-Website-brightgreen)](https://consistem.com.br/) [![](https://img.shields.io/badge/InterSystems-IRIS-blue.svg)](https://www.intersystems.com/products/intersystems-iris/) -[![](https://img.shields.io/badge/InterSystems-Cach%C3%A9-blue.svg)](https://www.intersystems.com/products/cache/) -[![](https://img.shields.io/badge/InterSystems-Ensemble-blue.svg)](https://www.intersystems.com/products/ensemble/) > **Note:** The best way to install and use this extension is by installing the [InterSystems ObjectScript Extension Pack](https://marketplace.visualstudio.com/items?itemName=intersystems-community.objectscript-pack) > and following the [documentation here](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO). @@ -74,12 +71,8 @@ To unlock these features (optional): 1. Download and install a beta version from GitHub. This is necessary because Marketplace does not allow publication of extensions that use proposed APIs. - Go to https://github.com/intersystems-community/vscode-objectscript/releases - - Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.8.2`, look for `3.8.3-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs. - - Download the VSIX file (for example `vscode-objectscript-3.8.3-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code. - - - Go to https://github.com/intersystems-community/vscode-objectscript/releases - - Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.4.0`, look for `3.4.1-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs. - - Download the VSIX file (for example `vscode-objectscript-3.4.1-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code. + - Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.8.5`, look for `3.8.6-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs. + - Download the VSIX file (for example `vscode-objectscript-3.8.6-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code. 2. From [Command Palette](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) choose `Preferences: Configure Runtime Arguments`. 3. In the argv.json file that opens, add this line (required for both Stable and Insiders versions of VS Code): diff --git a/package-lock.json b/package-lock.json index 3dc28eec..65477553 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "consistem-vscode-objectscript", - "version": "3.8.3-SNAPSHOT", + "version": "3.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "consistem-vscode-objectscript", - "version": "3.8.3-SNAPSHOT", + "version": "3.8.3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@vscode/debugadapter": "^1.68.0", "@vscode/debugprotocol": "^1.68.0", "@vscode/extension-telemetry": "^1.5.2", - "@xmldom/xmldom": "^0.9.10", - "axios": "^1.17.0", + "@xmldom/xmldom": "^0.9.12", + "axios": "^1.20.0", "core-js": "^3.41.0", "iconv-lite": "^0.7.2", "istextorbinary": "^7.0.0", @@ -24,7 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@intersystems-community/intersystems-servermanager": "^3.10.2", + "@intersystems-community/intersystems-servermanager": "^3.14.1", "@types/istextorbinary": "2.3.1", "@types/minimatch": "6.0.0", "@types/mocha": "^10.0.10", @@ -47,7 +47,7 @@ "eslint-plugin-promise": "^7.2.1", "glob": "^11.1.0", "globals": "^15.12.0", - "mocha": "^11.7.5", + "mocha": "^12.0.0", "path-browserify": "^1.0.1", "prettier": "^3.3.1", "ts-loader": "^9.5.1", @@ -302,9 +302,9 @@ } }, "node_modules/@intersystems-community/intersystems-servermanager": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@intersystems-community/intersystems-servermanager/-/intersystems-servermanager-3.10.2.tgz", - "integrity": "sha512-gwXxtvh+22MbCg3ZS8+evMCOuMvoDmApeYIghhFe3+NePIHhEC0na/UxeoA65xe4Rc97CJ+B2bCca/UF6iDI1A==", + "version": "3.14.1", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/@intersystems-community/intersystems-servermanager/-/intersystems-servermanager-3.14.1.tgz", + "integrity": "sha512-sGOq2agsIdN0dO6AwqciSbt8bV/y2gLBuRpiyq7hUR/7liE7s5a2EbYryN6A+h6J2lHLo/akiFz1ipmckIXvvg==", "dev": true, "license": "MIT" }, @@ -590,17 +590,6 @@ ], "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "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" - } - }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -1216,9 +1205,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "version": "0.9.12", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/@xmldom/xmldom/-/xmldom-0.9.12.tgz", + "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==", "license": "MIT", "engines": { "node": ">=14.6" @@ -1525,7 +1514,7 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, @@ -1555,13 +1544,13 @@ } }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://nexus.consistem.com.br/repository/npm-public/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "version": "1.20.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -1828,19 +1817,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001768", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", @@ -1879,16 +1855,16 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1930,21 +1906,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "8.0.1", - "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", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -1979,7 +1940,7 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { @@ -2111,19 +2072,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2166,7 +2114,7 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { @@ -2174,9 +2122,9 @@ } }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "9.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3180,16 +3128,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "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" @@ -3244,16 +3192,6 @@ "node": ">= 0.4" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3463,9 +3401,10 @@ } }, "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://nexus.consistem.com.br/repository/npm-public/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3473,16 +3412,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3971,23 +3900,16 @@ } }, "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "version": "4.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-plain-object": { @@ -4103,7 +4025,7 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", @@ -4425,23 +4347,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", @@ -4482,19 +4387,21 @@ } }, "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "version": "2.1.35", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { - "mime-db": "1.51.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -4555,137 +4462,163 @@ } }, "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://nexus.consistem.com.br/repository/npm-public/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "12.0.1", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/mocha/-/mocha-12.0.1.tgz", + "integrity": "sha512-/ILgtFHV+R7EJLwN0OkrPPOu6wbJyrtuRtTJ5szdFHv2A3mPmkbu3r1H2q6FhaAfywDJCulcQQ2S9qDGfZW6dw==", "dev": true, "license": "MIT", "dependencies": { "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", + "chokidar": "^5.0.0", "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", + "diff": "^9.0.0", + "find-up": "^8.0.0", + "glob": "^13.0.0", + "is-path-inside": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", "ms": "^2.1.3", "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", + "serialize-javascript": "^7.1.1", + "strip-json-comments": "^5.0.3", "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" + "workerpool": "^10.0.0" }, "bin": { - "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/mocha/node_modules/find-up": { + "version": "8.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/find-up/-/find-up-8.0.0.tgz", + "integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "locate-path": "^8.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mocha/node_modules/glob": { - "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", + "version": "13.0.6", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/mocha/node_modules/js-yaml": { + "version": "5.4.1", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", "dev": true, - "license": "BlueOak-1.0.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "argparse": "^2.0.1" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "bin": { + "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/mocha/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/mocha/node_modules/locate-path": { + "version": "8.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/locate-path/-/locate-path-8.0.0.tgz", + "integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/mocha/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.2" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mocha/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/mocha/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "p-limit": "^4.0.0" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mocha/node_modules/supports-color": { @@ -4704,6 +4637,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5082,16 +5028,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://nexus.consistem.com.br/repository/npm-public/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" @@ -5202,15 +5149,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -5239,13 +5177,13 @@ "dev": true }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.1.1", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -5321,16 +5259,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5425,26 +5353,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5556,13 +5464,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.1.1", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/serialize-javascript/-/serialize-javascript-7.1.1.tgz", + "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-function-length": { @@ -6337,6 +6245,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6642,30 +6563,12 @@ } }, "node_modules/workerpool": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", - "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "version": "10.0.3", + "resolved": "https://nexus.consistem.com.br/repository/npm-public/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", "dev": true, "license": "Apache-2.0" }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", @@ -6705,61 +6608,6 @@ } } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "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" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 3480813e..9c68ceee 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "consistem-vscode-objectscript", "displayName": "Consistem ObjectScript", "description": "InterSystems ObjectScript language support for Visual Studio Code, with integrations and internal standards adopted by Consistem.", - "version": "3.8.3-SNAPSHOT", + "version": "3.8.3", "icon": "images/logo.png", "aiKey": "InstrumentationKey=9cd75d51-697c-406c-a929-2bcf46e97c64;IngestionEndpoint=https://eastus2-4.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus2.livediagnostics.monitor.azure.com/;ApplicationId=a431c56f-8ccc-4b99-b5e9-fce3763215b1", "categories": [ @@ -1526,6 +1526,7 @@ "port": { "type": "integer", "description": "TCP port number the web server listens on.", + "deprecationMessage": "It is recommended to define a server connection in `#intersystems.servers#` instead of here.", "minimum": 1, "maximum": 65535 }, @@ -1890,12 +1891,12 @@ "type": "boolean", "default": false } - }, - "objectscript.insertStubContent": { - "description": "If true, stub content will be inserted when a blank new class or routine file is created in a client-side workspace folder.", - "type": "boolean", - "default": true - } + }, + "objectscript.insertStubContent": { + "description": "If true, stub content will be inserted when a blank new class or routine file is created in a client-side workspace folder.", + "type": "boolean", + "default": true + } } ], "views": { @@ -2048,7 +2049,7 @@ "vscode:prepublish": "webpack --mode production", "webpack": "webpack --mode development", "webpack-dev": "webpack --mode development --watch", - "package": "vsce package", + "package": "npx @vscode/vsce package", "compile": "webpack --mode production && tsc -p ./", "test-compile": "webpack --mode development && tsc -p ./", "watch": "tsc -w -p ./tsconfig.json", @@ -2061,7 +2062,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@intersystems-community/intersystems-servermanager": "^3.10.2", + "@intersystems-community/intersystems-servermanager": "^3.14.1", "@types/istextorbinary": "2.3.1", "@types/minimatch": "6.0.0", "@types/mocha": "^10.0.10", @@ -2084,7 +2085,7 @@ "eslint-plugin-promise": "^7.2.1", "glob": "^11.1.0", "globals": "^15.12.0", - "mocha": "^11.7.5", + "mocha": "^12.0.0", "path-browserify": "^1.0.1", "prettier": "^3.3.1", "ts-loader": "^9.5.1", @@ -2096,8 +2097,8 @@ "@vscode/debugadapter": "^1.68.0", "@vscode/debugprotocol": "^1.68.0", "@vscode/extension-telemetry": "^1.5.2", - "@xmldom/xmldom": "^0.9.10", - "axios": "^1.17.0", + "@xmldom/xmldom": "^0.9.12", + "axios": "^1.20.0", "core-js": "^3.41.0", "iconv-lite": "^0.7.2", "istextorbinary": "^7.0.0", diff --git a/src/api/index.ts b/src/api/index.ts index 58147030..ac55e861 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,15 +2,14 @@ import axios from "axios"; import * as httpsModule from "https"; import * as vscode from "vscode"; import * as semver from "semver"; -import { +import BasicAuthorization, { getResolvedConnectionSpec, config, extensionContext, workspaceState, panel, - checkConnection, + ensureConnection, schemas, - checkingConnection, inactiveServerIds, } from "../extension"; import { currentWorkspaceFolder, outputChannel, outputConsole } from "../utils"; @@ -19,27 +18,48 @@ const DEFAULT_API_VERSION = 1; const DEFAULT_SERVER_VERSION = "2016.2.0"; import * as Atelier from "./atelier"; import { isfsConfig } from "../utils/FileProviderUtil"; +import { Authorization, IServerSpec } from "@intersystems-community/intersystems-servermanager"; -// Map of the authRequest promises for each username@host:port/pathPrefix target to avoid concurrency issues +// Map of the authRequest promises for each `username@http(s)://host:port/pathPrefix` target to avoid concurrency issues const authRequestMap = new Map>(); -/** Map of `username@host:port/pathPrefix` to cookies */ -const cookiesMap = new Map(); +/** Map of `username@http(s)://host:port/pathPrefix` to cookies */ +export const cookiesMap = new Map(); + +/** Log out of CSP sessions specified in the `sessions` array, or all known sessions if no argument was passed. */ +export async function logoutOfSessions(sessions?: string[]): Promise { + const httpsAgent = new httpsModule.Agent({ + rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), + }); + return Promise.allSettled( + (sessions ?? Array.from(cookiesMap.keys())).map((session) => { + const cookie = cookiesMap.get(session); + if (!cookie) return; + cookiesMap.delete(session); + const url = session.slice(session.indexOf("@") + 1); + return axios.head(`${url}/api/atelier/?CacheLogout=end`, { + headers: { + Cookie: cookie, + }, + httpsAgent, + }); + }) + ).then(() => {}); // Returned object isn't needed +} -interface ConnectionSettings { +export interface ConnectionSettings { serverName: string; active: boolean; - apiVersion: number; - serverVersion: string; + apiVersion?: number; + serverVersion?: string; https: boolean; host: string; port: number; superserverPort?: number; - pathPrefix: string; - ns: string; - username: string; - password: string; - docker: boolean; + pathPrefix?: string; + ns?: string; + auth: Authorization; + docker?: boolean; dockerService?: string; } @@ -59,7 +79,8 @@ export class AtelierAPI { } public get config(): ConnectionSettings { - const { serverName, active = false, https = false, pathPrefix = "", username } = this._config; + const { serverName, active = false, https = false, pathPrefix = "" } = this._config; + const auth = this._config.auth.clone(); const ns = this.namespace || this._config.ns; const wsKey = this.configName.toLowerCase(); const host = this.externalServer ? this._config.host : workspaceState.get(wsKey + ":host", this._config.host); @@ -67,7 +88,7 @@ export class AtelierAPI { const superserverPort = this.externalServer ? this._config.superserverPort : workspaceState.get(wsKey + ":superserverPort", this._config.superserverPort); - const password = workspaceState.get(wsKey + ":password", this._config.password); + auth.resolve({ accessToken: workspaceState.get(wsKey + ":password", undefined) }); const apiVersion = workspaceState.get(wsKey + ":apiVersion", DEFAULT_API_VERSION); const serverVersion = workspaceState.get(wsKey + ":serverVersion", DEFAULT_SERVER_VERSION); const docker = workspaceState.get(wsKey + ":docker", false); @@ -83,8 +104,7 @@ export class AtelierAPI { superserverPort, pathPrefix, ns, - username, - password, + auth, docker, dockerService, }; @@ -100,10 +120,8 @@ export class AtelierAPI { return filename; } - public constructor(wsOrFile?: string | vscode.Uri, retryAfter401 = true) { - if (retryAfter401) { - this.wsOrFile = wsOrFile; - } + public constructor(wsOrFile?: string | vscode.Uri) { + this.wsOrFile = wsOrFile; let workspaceFolderName = ""; let namespace = ""; if (wsOrFile) { @@ -147,14 +165,12 @@ export class AtelierAPI { * Manually set the connection spec for this object, * where `connSpec` is the return value of `getResolvedConnectionSpec()`. */ - public setConnSpec(serverName: string, connSpec: any): void { + public setConnSpec(serverName: string, connSpec: IServerSpec): void { const { webServer: { scheme, host, port, pathPrefix = "" }, - username, - password, + auth, } = connSpec; - this._config.username = username; - this._config.password = password; + this._config.auth = auth ?? new BasicAuthorization(); this._config.https = scheme == "https"; this._config.host = host; this._config.port = port; @@ -175,10 +191,6 @@ export class AtelierAPI { return cookiesMap.get(this.mapKey()) ?? []; } - public clearCookies(): void { - cookiesMap.delete(this.mapKey()); - } - public xdebugUrl(): string { const { host, https, port, apiVersion, pathPrefix } = this.config; const proto = https ? "wss" : "ws"; @@ -187,7 +199,7 @@ export class AtelierAPI { public terminalUrl(): string { const { host, https, port, apiVersion, pathPrefix } = this.config; - return apiVersion >= 7 + return apiVersion! >= 7 ? `${https ? "wss" : "ws"}://${host}:${port}${pathPrefix}/api/atelier/v${apiVersion}/%25SYS/terminal` : ""; } @@ -208,18 +220,22 @@ export class AtelierAPI { } /** Return the key for getting values from connection-specific Maps for this connection */ - private mapKey(): string { - const { host, port, username } = this.config; + public mapKey(): string { + const { host, https, port, auth } = this.config; let pathPrefix = this._config.pathPrefix || ""; if (pathPrefix.length && !pathPrefix.startsWith("/")) { pathPrefix = "/" + pathPrefix; } - return `${username}@${host}:${port}${pathPrefix}`; + return `${auth.username}@http${https ? "s" : ""}://${host}:${port}${pathPrefix}`; } private setConnection(workspaceFolderName: string, namespace?: string): void { this.configName = workspaceFolderName; - const conn = config("conn", workspaceFolderName); + const rawConn = config("conn", workspaceFolderName); + const conn = { + ...rawConn, + auth: new BasicAuthorization(rawConn.username, rawConn.password), + }; let serverName = workspaceFolderName.toLowerCase(); if (config("intersystems.servers", workspaceFolderName).has(serverName)) { this.externalServer = true; @@ -235,14 +251,13 @@ export class AtelierAPI { serverName = ""; } - const ns = namespace ? namespace.toUpperCase() : conn.ns ? (conn.ns as string).toUpperCase() : undefined; + const ns = namespace?.toUpperCase() || conn.ns?.toUpperCase(); if (serverName !== "") { const { webServer: { scheme, host, port, pathPrefix = "" }, - username, - password, + auth, superServer, - } = getResolvedConnectionSpec(serverName, config("intersystems.servers", workspaceFolderName).get(serverName)); + } = getResolvedConnectionSpec(serverName, config("intersystems.servers", workspaceFolderName).get(serverName))!; this._config = { serverName, active: this.externalServer ? !inactiveServerIds.has(serverName) : conn.active, @@ -253,8 +268,7 @@ export class AtelierAPI { host, port, superserverPort: superServer?.port, - username, - password, + auth, pathPrefix, docker: false, }; @@ -264,8 +278,7 @@ export class AtelierAPI { if (resolvedSpec) { const { webServer: { scheme, host, port, pathPrefix = "" }, - username, - password, + auth, superServer, } = resolvedSpec; this._config = { @@ -278,21 +291,24 @@ export class AtelierAPI { host, port, superserverPort: superServer?.port, - username, - password, + auth, pathPrefix, docker: true, dockerService: conn["docker-compose"].service, }; } else { - this._config = conn; - this._config.ns = ns; - this._config.serverName = ""; + this._config = { + ...conn, + ns, + serverName: "", + }; } } else { - this._config = conn; - this._config.ns = ns; - this._config.serverName = ""; + this._config = { + ...conn, + ns, + serverName: "", + }; } } @@ -307,20 +323,27 @@ export class AtelierAPI { return serverName && serverName !== "" ? serverName : `${host}:${port}${pathPrefix}`; } - public async request( + private async request( minVersion: number, - method: string, + method: "GET" | "HEAD" | "PUT" | "POST" | "DELETE", path?: string, body?: any, params?: any, headers?: any, - options?: any + options?: { + /** Abort the request if it hasn't completed within this many milliseconds. */ + timeout?: number; + /** Suppress writing this request/response to the ObjectScript output channel, even when `objectscript.outputRESTTraffic` is on. */ + noOutput?: boolean; + /** On a 401 response, suppress the automatic single retry with fresh credentials. */ + _retriedAfter401?: boolean; + } ): Promise { - const { active, apiVersion, host, port, username, password, https } = this.config; + const { active, apiVersion, host, port, https } = this.config; if (!active || !port || !host) { return Promise.reject(); } - if (minVersion > apiVersion) { + if (minVersion > apiVersion!) { return Promise.reject(`${path} not supported by API version ${apiVersion}`); } const originalPath = path; @@ -335,7 +358,7 @@ export class AtelierAPI { if (!params) { return ""; } - const result = []; + const result: string[] = []; Object.keys(params).forEach((key) => { const value = params[key]; if (typeof value === "boolean") { @@ -346,7 +369,6 @@ export class AtelierAPI { }); return result.length ? "?" + result.join("&") : ""; }; - method = method.toUpperCase(); if (body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } @@ -366,15 +388,14 @@ export class AtelierAPI { const cookies = this.cookies; const mapKey = this.mapKey(); - let auth: Promise; + let auth: Promise | undefined; let authRequest = authRequestMap.get(mapKey); if (cookies.length || (method === "HEAD" && !originalPath)) { - auth = Promise.resolve(cookies); - // Only send basic authorization if username and password specified (including blank, for unauthenticated access) - if (typeof username === "string" && typeof password === "string") { - headers["Authorization"] = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; + if (!cookies.length && this.config.auth.resolved()) { + headers["Authorization"] = this.config.auth.httpAuthorizationHeader; } + auth = Promise.resolve(cookies); } else if (!cookies.length) { if (!authRequest) { // Recursion point @@ -405,7 +426,7 @@ export class AtelierAPI { } }; try { - cookie = await auth; + cookie = await auth!; reqTs = new Date(); const response = await axios.request({ method, @@ -446,10 +467,17 @@ export class AtelierAPI { } if (response.status === 401) { authRequestMap.delete(mapKey); - if (this.wsOrFile && !checkingConnection) { + cookiesMap.delete(mapKey); + if (this.wsOrFile) { + if (!options?._retriedAfter401) { + return this.request(minVersion, method, originalPath, body, params, headers, { + ...options, + _retriedAfter401: true, + }); + } setTimeout(() => { - checkConnection( - password ? true : false, + ensureConnection( + this.config.auth.resolved(), typeof this.wsOrFile === "object" ? this.wsOrFile : undefined, true ); @@ -457,7 +485,7 @@ export class AtelierAPI { } throw { statusCode: response.status, message: response.statusText }; } - await this.updateCookies(response.headers["set-cookie"] || []); + this.updateCookies(response.headers["set-cookie"] || []); if (method === "HEAD") { if (!originalPath) { authRequestMap.delete(mapKey); @@ -564,13 +592,14 @@ export class AtelierAPI { // In some cases schedule an automatic retry. // ENOTFOUND occurs if, say, the VPN to the server's network goes down. if (["ECONNREFUSED", "ENOTFOUND", "ECONNABORTED", "ERR_CANCELED"].includes(error.code)) { + // The cached cookie is unusable once the connection itself has failed; discard it + // so the next request re-authenticates instead of resending a stale cookie. + cookiesMap.delete(mapKey); panel.text = `${this.connInfo} $(debug-disconnect)`; panel.tooltip = "Disconnected"; workspaceState.update(this.configName.toLowerCase() + ":host", undefined); workspaceState.update(this.configName.toLowerCase() + ":port", undefined); - if (!checkingConnection) { - setTimeout(() => checkConnection(false, undefined, true), 30000); - } + setTimeout(() => ensureConnection(false, undefined, true), 30000); } throw error; } @@ -586,7 +615,7 @@ export class AtelierAPI { .slice(data.version.indexOf(") ") + 2) .split(" ") .shift() - ).version; + )!.version; if (this.ns && this.ns.length && !data.namespaces.includes(this.ns) && checkNs) { throw { code: "WrongNamespace", @@ -631,12 +660,13 @@ export class AtelierAPI { name: string, scope: vscode.Uri | string, mtime?: number, - storageOnly: boolean = false + storageOnly = false, + forceBinary = false ): Promise> { - let params, headers; + const params: Record = {}; name = this.transformNameIfCsp(name); if ( - this.config.apiVersion >= 4 && + this.config.apiVersion! >= 4 && vscode.workspace .getConfiguration( "objectscript", @@ -647,17 +677,19 @@ export class AtelierAPI { ) .get("multilineMethodArgs") ) { - params = { format: "udl-multiline" }; - } else { - params = {}; - } - if (storageOnly) { - params["storageOnly"] = "1"; - } - if (mtime && mtime > 0) { - headers = { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() }; + params.format = "udl-multiline"; } - return this.request(1, "GET", `${this.ns}/doc/${name}`, null, params, headers); + if (storageOnly) params.storageOnly = "1"; + if (forceBinary) params.binary = "1"; + return this.request( + 1, + "GET", + `${this.ns}/doc/${name}`, + null, + params, + // headers + mtime && mtime > 0 ? { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() } : undefined + ); } // api v1+ diff --git a/src/ccs/commands/analizarVersaoItem.ts b/src/ccs/commands/analizarVersaoItem.ts index 022ab092..fe2f567a 100644 --- a/src/ccs/commands/analizarVersaoItem.ts +++ b/src/ccs/commands/analizarVersaoItem.ts @@ -31,7 +31,7 @@ export async function analizarVersaoItem(): Promise { return; } - const { username, password } = api.config; + const { username, password } = api.config.auth; if (typeof username !== "string" || typeof password !== "string") { void vscode.window.showErrorMessage("Credenciais não disponíveis para analizar versão do item."); diff --git a/src/ccs/providers/PrioritizedDefinitionProvider.ts b/src/ccs/providers/PrioritizedDefinitionProvider.ts index fdffc809..26e30a51 100644 --- a/src/ccs/providers/PrioritizedDefinitionProvider.ts +++ b/src/ccs/providers/PrioritizedDefinitionProvider.ts @@ -29,6 +29,6 @@ export class PrioritizedDefinitionProvider implements vscode.DefinitionProvider return location; } - return this.delegate.provideDefinition(document, position, token); + return (await this.delegate.provideDefinition(document, position, token)) ?? undefined; } } diff --git a/src/ccs/sourcecontrol/client.ts b/src/ccs/sourcecontrol/client.ts index 89f36c95..265bc819 100644 --- a/src/ccs/sourcecontrol/client.ts +++ b/src/ccs/sourcecontrol/client.ts @@ -30,7 +30,8 @@ export class SourceControlApi { } public static fromAtelierApi(api: AtelierAPI): SourceControlApi { - const { host, port, username, password, https: useHttps, pathPrefix } = api.config; + const { host, port, auth: authorization, https: useHttps, pathPrefix } = api.config; + const { username, password } = authorization; if (!host || !port) { throw new Error( @@ -77,11 +78,11 @@ export class SourceControlApi { return new SourceControlApi(client); } - public post>( + public post( route: string, data?: unknown, config?: AxiosRequestConfig - ): Promise { - return this.client.post(route, data, config); + ): Promise> { + return this.client.post(route, data, config); } } diff --git a/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts b/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts index b2875f63..cca9e502 100644 --- a/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts +++ b/src/ccs/sourcecontrol/clients/resolveDefinitionClient.ts @@ -63,7 +63,8 @@ export class ResolveDefinitionClient { token: vscode.CancellationToken ): Promise { const api = new AtelierAPI(document.uri); - const { host, port, username, password } = api.config; + const { host, port, auth } = api.config; + const { username, password } = auth; const namespace = api.ns; if (!api.active || !namespace || !host || !port || !username || !password) { diff --git a/src/commands/addServerNamespaceToWorkspace.ts b/src/commands/addServerNamespaceToWorkspace.ts index 907d7ab8..cc48f294 100644 --- a/src/commands/addServerNamespaceToWorkspace.ts +++ b/src/commands/addServerNamespaceToWorkspace.ts @@ -17,7 +17,9 @@ import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil"; * @param message The prefix of the message to show when the server manager API can't be found. * @returns An object containing `serverName` and `namespace`, or `undefined`. */ -async function pickServerAndNamespace(message?: string): Promise<{ serverName: string; namespace: string }> { +async function pickServerAndNamespace( + message?: string +): Promise<{ serverName: string; namespace: string } | undefined> { if (!serverManagerApi) { vscode.window.showErrorMessage( `${ @@ -29,7 +31,7 @@ async function pickServerAndNamespace(message?: string): Promise<{ serverName: s } // Get user's choice of server const options: vscode.QuickPickOptions = { ignoreFocusOut: true }; - const serverName: string = await serverManagerApi.pickServer(undefined, options); + const serverName: string | undefined = await serverManagerApi.pickServer(undefined, options); if (!serverName) { return; } @@ -40,13 +42,19 @@ async function pickServerAndNamespace(message?: string): Promise<{ serverName: s return { serverName, namespace }; } -async function pickNamespaceOnServer(serverName: string): Promise { +async function pickNamespaceOnServer(serverName: string): Promise { // Get its namespace list const uri = vscode.Uri.parse(`isfs://${serverName}:%sys/`); await resolveConnectionSpec(serverName); // Prepare a displayable form of its connection spec as a hint to the user. - // This will never return the default value (second parameter) because we only just resolved the connection spec. const connSpec = getResolvedConnectionSpec(serverName, undefined); + if (!connSpec) { + vscode.window.showErrorMessage( + `Failed to resolve connection details for server '${serverName}'. Could not list its namespaces.`, + "Dismiss" + ); + return; + } const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`; // Connect and fetch namespaces const api = new AtelierAPI(uri); @@ -81,9 +89,9 @@ async function pickNamespaceOnServer(serverName: string): Promise { export async function addServerNamespaceToWorkspace(resource?: vscode.Uri): Promise { const TITLE = "Add server namespace to workspace"; let serverName = ""; - let namespace = ""; - if (filesystemSchemas.includes(resource?.scheme)) { - serverName = resource.authority.split(":")[0]; + let namespace: string | undefined = ""; + if (filesystemSchemas.includes(resource?.scheme as string)) { + serverName = resource!.authority.split(":")[0]; if (serverName) { const ANOTHER = "Choose another server"; const choice = await vscode.window.showQuickPick([`Add a '${serverName}' namespace`, ANOTHER], { @@ -111,7 +119,7 @@ export async function addServerNamespaceToWorkspace(resource?: vscode.Uri): Prom } } const wsFolders = vscode.workspace.workspaceFolders ?? []; - let scheme: string; + let scheme: string | undefined; if (wsFolders.length && wsFolders.some((wf) => notIsfs(wf.uri))) { // Don't allow the creation of an editable ISFS folder // if the workspace contains non-ISFS folders already @@ -221,7 +229,7 @@ async function modifyWsFolderUri(uri: vscode.Uri): Promise((resolve) => { - let result: string; + let result: string | undefined; const allItem: vscode.QuickPickItem = { label: "All" }; const quickPick = vscode.window.createQuickPick(); quickPick.title = "Pick a specific web application to show, or show all"; @@ -352,7 +360,7 @@ async function modifyWsFolderUri(uri: vscode.Uri): Promise { - let wsFolder: vscode.WorkspaceFolder; + let wsFolder: vscode.WorkspaceFolder | null | undefined; if (!wsFolderUri) { // Select a workspace folder to modify wsFolder = await getWsFolder("Pick the workspace folder to modify", false, true); diff --git a/src/commands/compile.ts b/src/commands/compile.ts index d688ca9e..fae1072d 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -14,7 +14,6 @@ import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { base64EncodeContent, compileErrorMsg, - cspAppsForUri, CurrentBinaryFile, currentFile, currentFileFromContent, @@ -34,7 +33,7 @@ import { } from "../utils"; import { StudioActions } from "./studio"; import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; -import { getUrisForDocument, updateIndex } from "../utils/documentIndex"; +import { getUrisForDocument, updateIndex, urisInFolder } from "../utils/documentIndex"; import { Document } from "../api/atelier"; const compileWaiters = new Map void>>(); @@ -127,7 +126,7 @@ export async function importFile( if (!file) return; const api = new AtelierAPI(file.uri); if (!api.active) return Promise.reject(); - if (file.name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) { + if (file.name.split(".").pop()!.toLowerCase() === "cls" && !skipDeplCheck) { if (await isClassDeployed(file.name, api)) { vscode.window.showErrorMessage(`Cannot import ${file.name} because it is deployed on the server.`, "Dismiss"); return Promise.reject(); @@ -224,7 +223,7 @@ What do you want to do?`, // Overwrite return importFile(file, willCompile, true, true); case "Pull Server Changes": - loadChanges([file]); + loadChanges([file], true); return Promise.reject(); case "Cancel": return Promise.reject(); @@ -245,10 +244,10 @@ function updateOthers(others: string[], baseUri: vscode.Uri) { } others.forEach((item) => { const uri = DocumentContentProvider.getUri(item, undefined, undefined, undefined, workspaceFolder?.uri); - if (filesystemSchemas.includes(uri.scheme)) { - fileSystemProvider.fireFileChanged(uri); - } else if (uri.scheme == OBJECTSCRIPT_FILE_SCHEMA) { - documentContentProvider.update(uri); + if (filesystemSchemas.includes(uri!.scheme)) { + fileSystemProvider.fireFileChanged(uri!); + } else if (uri!.scheme == OBJECTSCRIPT_FILE_SCHEMA) { + documentContentProvider.update(uri!); } }); } @@ -272,7 +271,7 @@ export async function loadChanges( Promise.allSettled( data.result.content.map(async (doc) => { if (doc.status.length) return; - const file = files.find((f) => f.name == doc.name); + const file = files.find((f) => f.name == doc.name)!; const mtime = Number(new Date(doc.ts + "Z")); workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); if (notIsfs(file.uri)) { @@ -350,8 +349,8 @@ function updateStorage(content: string[], storage: string[]): string[] { function storageToMap(storage: string[]): Map { const map: Map = new Map(); - let k: string; - let v = []; + let k: string | undefined; + let v: string[] = []; for (const line of storage) { if (line.startsWith("Storage ")) { k = line.slice("Storage ".length, line.length); @@ -460,7 +459,7 @@ export async function importAndCompile(document?: vscode.TextDocument, askFlags } } -export async function compileOnly(document?: vscode.TextDocument, askFlags = false): Promise { +export async function compileOnly(document?: vscode.TextDocument | null, askFlags = false): Promise { document = document || (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document @@ -516,12 +515,14 @@ export async function namespaceCompile(): Promise { .then(() => { // Always fetch server changes, even when compile failed or got cancelled const file = currentFile(); - return loadChanges([file]); + return loadChanges([file!]); }) ); } async function importFiles(files: vscode.Uri[], noCompile = false) { + if (!files.length) return; + const textDecoder = new TextDecoder(); const toCompile: (CurrentTextFile | CurrentBinaryFile)[] = []; const rateLimiter = new RateLimiter(50); await Promise.allSettled( @@ -531,7 +532,7 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { const curFile = currentFileFromContent( uri, isText(uri.path.split("/").pop(), Buffer.from(contentBytes)) - ? new TextDecoder().decode(contentBytes) + ? textDecoder.decode(contentBytes) : Buffer.from(contentBytes) ); if (curFile) { @@ -548,7 +549,6 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { if (!noCompile && toCompile.length > 0) { await compile(toCompile); } - return; } export async function importFolder(uri: vscode.Uri, noCompile = false): Promise { @@ -557,16 +557,7 @@ export async function importFolder(uri: vscode.Uri, noCompile = false): Promise< if ((await vscode.workspace.fs.stat(uri)).type != vscode.FileType.Directory) { return importFiles([uri], noCompile); } - let globpattern = "*.{cls,inc,int,mac}"; - if (cspAppsForUri(uri).findIndex((cspApp) => uri.path.includes(cspApp + "/") || uri.path.endsWith(cspApp)) != -1) { - // This folder is a CSP application, so import all files - // We need to include eveything because CSP applications can - // include non-InterSystems files - globpattern = "*"; - } - vscode.workspace - .findFiles(new vscode.RelativePattern(uri, `**/${globpattern}`)) - .then((files) => importFiles(files, noCompile)); + importFiles(urisInFolder(uri), noCompile); } export async function compileExplorerItems(nodes: NodeBase[]): Promise { @@ -574,7 +565,7 @@ export async function compileExplorerItems(nodes: NodeBase[]): Promise { const conf = vscode.workspace.getConfiguration("objectscript", wsFolder); const api = new AtelierAPI(wsFolder.uri); if (namespace) api.setNamespace(namespace); - const docs = []; + const docs: string[] = []; for (const node of nodes) { if (node instanceof PackageNode) { switch (node.category) { @@ -707,7 +698,7 @@ export async function importArbitraryFiles(): Promise { }); if (!uris?.length) return; // Filter out non-importable files - uris = uris.filter((uri) => supportedExts.includes(uri.path.split(".").pop().toLowerCase())); + uris = uris.filter((uri) => supportedExts.includes(uri.path.split(".").pop()!.toLowerCase())); if (uris.length == 0) { vscode.window.showErrorMessage("No selected files are importable.", "Dismiss"); return; @@ -737,6 +728,7 @@ export async function importArbitraryFiles(): Promise { } }) .filter(notNull) + .map((f) => f!) ); if (filesToList.length == 0) { vscode.window.showErrorMessage("Failed to read the text of every selected file.", "Dismiss"); @@ -802,9 +794,9 @@ export async function importArbitraryFiles(): Promise { } }); if (readOnly.length) { - docsToImport = docsToImport.filter((qpi) => { + docsToImport = docsToImport!.filter((qpi) => { const nameSplit = qpi.label.split("."); - return !readOnly.includes(`${nameSplit.slice(0, -1).join(".")}.${nameSplit.pop().toUpperCase()}`); + return !readOnly.includes(`${nameSplit.slice(0, -1).join(".")}.${nameSplit.pop()!.toUpperCase()}`); }); } }); diff --git a/src/commands/connectFolderToServerNamespace.ts b/src/commands/connectFolderToServerNamespace.ts index bb16f14d..e2f16197 100644 --- a/src/commands/connectFolderToServerNamespace.ts +++ b/src/commands/connectFolderToServerNamespace.ts @@ -7,7 +7,7 @@ import { serverManagerApi, resolveUsernameAndPassword, } from "../extension"; -import { handleError, isUnauthenticated, notIsfs, displayableUri } from "../utils"; +import { handleError, notIsfs, displayableUri } from "../utils"; interface ConnSettings { server: string; @@ -32,7 +32,7 @@ export async function connectFolderToServerNamespace(): Promise { .filter((folder) => notIsfs(folder.uri)) .map((folder) => { const config = vscode.workspace.getConfiguration("objectscript", folder); - const conn: ConnSettings = config.get("conn"); + const conn: ConnSettings = config.get("conn")!; return { label: folder.name, description: folder.uri.fsPath, @@ -47,40 +47,46 @@ export async function connectFolderToServerNamespace(): Promise { return; } const pick = - items.length == 1 && !items[0].detail.startsWith("Currently") + items.length == 1 && !items[0].detail!.startsWith("Currently") ? items[0] : await vscode.window.showQuickPick(items, { title: "Pick a folder" }); if (!pick) return; const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label); // Get user's choice of server const options: vscode.QuickPickOptions = {}; - const serverName: string = await serverManagerApi.pickServer(folder, options); + const serverName: string | undefined = await serverManagerApi.pickServer(folder, options); if (!serverName) { return; } await resolveConnectionSpec(serverName, undefined, folder); // Prepare a displayable form of its connection spec as a hint to the user - // This will never return the default value (second parameter) because we only just resolved the connection spec. const connSpec = getResolvedConnectionSpec(serverName, undefined); - const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`; + if (!connSpec) { + vscode.window.showErrorMessage( + `Failed to resolve connection details for server '${serverName}'. Folder was not connected.`, + "Dismiss" + ); + return; + } + const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}${connSpec.webServer.pathPrefix}`; // Connect and fetch namespaces const api = new AtelierAPI(vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`)); const serverConf = vscode.workspace .getConfiguration("intersystems", folder) .inspect<{ [key: string]: any }>("servers"); if ( - serverConf.workspaceFolderValue && - typeof serverConf.workspaceFolderValue[serverName] == "object" && - !(serverConf.workspaceValue && typeof serverConf.workspaceValue[serverName] == "object") + serverConf!.workspaceFolderValue && + typeof serverConf!.workspaceFolderValue[serverName] == "object" && + !(serverConf!.workspaceValue && typeof serverConf!.workspaceValue[serverName] == "object") ) { // Need to manually set connection info if the server is defined at the workspace folder level api.setConnSpec(serverName, connSpec); } - const allNamespaces: string[] = await api + const allNamespaces: string[] | undefined = await api .serverInfo(false) .then((data) => data.result.content.namespaces) .catch(async (error) => { - if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) { + if (error?.statusCode == 401 && !api.config.auth.resolved()) { // Attempt to resolve username and password and try again const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec); if (newSpec) { @@ -131,7 +137,7 @@ export async function connectFolderToServerNamespace(): Promise { // the server may be configured at the workspace folder level. const answer = await vscode.window.showQuickPick( [ - { label: `Workspace Folder ${folder.name}`, detail: displayableUri(folder.uri) }, + { label: `Workspace Folder ${folder!.name}`, detail: displayableUri(folder!.uri) }, { label: "Workspace File", detail: displayableUri(vscode.workspace.workspaceFile) }, ], { title: "Store the server connection at the workspace or folder level?" } @@ -139,7 +145,7 @@ export async function connectFolderToServerNamespace(): Promise { if (!answer) return; if (answer.label == "Workspace File") { // Enable the connection at the workspace level - const conn: any = config.inspect("conn").workspaceValue; + const conn: any = config.inspect("conn")!.workspaceValue; await config.update( "conn", { ...conn, server: serverName, ns: namespace, active: true }, @@ -149,7 +155,7 @@ export async function connectFolderToServerNamespace(): Promise { } } // Enable the connection at the workspace folder level - const conn: any = config.inspect("conn").workspaceFolderValue; + const conn: any = config.inspect("conn")!.workspaceFolderValue; await config.update( "conn", { ...conn, server: serverName, ns: namespace, active: true }, diff --git a/src/commands/delete.ts b/src/commands/delete.ts index b0ea3051..676886fb 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -19,7 +19,7 @@ function deleteList(items: string[], wsFolder: vscode.WorkspaceFolder, namespace if (file.result.ext && wsFolder.uri.scheme == FILESYSTEM_SCHEMA) { // Only process source control output if we're in an isfs folder const uri = DocumentContentProvider.getUri(file.result.name); - fireOtherStudioAction(OtherStudioAction.DeletedDocument, uri, file.result.ext); + fireOtherStudioAction(OtherStudioAction.DeletedDocument, uri!, file.result.ext); } }); outputChannel.appendLine(`Deleted items: ${files.filter((el) => el.result).length}`); diff --git a/src/commands/export.ts b/src/commands/export.ts index 29b7cee5..40f5751c 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -22,8 +22,8 @@ import { pickDocuments } from "../utils/documentPicker"; import { NodeBase } from "../explorer/nodes"; import { updateIndex } from "../utils/documentIndex"; -export function getCategory(fileName: string, addCategory: any | boolean): string { - const fileExt = fileName.split(".").pop().toLowerCase(); +export function getCategory(fileName: string, addCategory: any | boolean): string | null { + const fileExt = fileName.split(".").pop()!.toLowerCase(); if (typeof addCategory === "object") { for (const pattern of Object.keys(addCategory)) { if (new RegExp(`^${pattern}$`).test(fileName)) { @@ -49,11 +49,13 @@ export function getCategory(fileName: string, addCategory: any | boolean): strin export function getFileName( folder: string, name: string, - split: boolean, - addCategory: boolean, - map: { - [key: string]: string; - }, + split: boolean | undefined, + addCategory: boolean | undefined, + map: + | { + [key: string]: string; + } + | undefined, sep = path.sep ): string { if (name.includes("/")) { @@ -75,7 +77,7 @@ export function getFileName( } } fileNameArray = name.split("."); - fileExt = fileNameArray.pop().toLowerCase(); + fileExt = fileNameArray.pop()!.toLowerCase(); } else { // This is some other type of file (LUT,HL7,...) const lastDot = name.lastIndexOf("."); @@ -200,7 +202,7 @@ export async function exportAll(): Promise { } else if (filterIsValid(filter)) { filters.push(`Name LIKE '%${filter}%'`); } - let files: vscode.QuickPickItem[] = await api + let files: vscode.QuickPickItem[] | undefined = await api .actionQuery("SELECT Name FROM %Library.RoutineMgr_StudioOpenDialog('*',1,1,?,1,0,?,?,0,?)", [ api.ns == "%SYS" ? "1" : "0", generated ? "1" : "0", @@ -268,7 +270,7 @@ export async function exportCurrentFile(): Promise { // Only export files opened from the explorer return; } - return exportList([currentFile(openDoc).name], vscode.workspace.getWorkspaceFolder(openDoc.uri)); + return exportList([currentFile(openDoc)!.name], vscode.workspace.getWorkspaceFolder(openDoc.uri)!); } export async function exportDocumentsToXMLFile(): Promise { @@ -293,7 +295,7 @@ export async function exportDocumentsToXMLFile(): Promise { } const api = new AtelierAPI(wsFolder.uri); // Make sure the server has the xml endpoints - if (api.config.apiVersion < 7) { + if (api.config.apiVersion! < 7) { vscode.window.showErrorMessage( "'Export Documents to XML File...' command requires InterSystems IRIS version 2023.2 or above.", "Dismiss" diff --git a/src/commands/jumpToTagAndOffset.ts b/src/commands/jumpToTagAndOffset.ts index a07782ea..7b60c736 100644 --- a/src/commands/jumpToTagAndOffset.ts +++ b/src/commands/jumpToTagAndOffset.ts @@ -72,7 +72,7 @@ export async function openErrorLocation(): Promise { if (!location) { return; } - const [, label, offset, routine] = location.trim().match(regex); + const [, label, offset, routine] = location.trim().match(regex)!; // Get the uri for the routine const uri = DocumentContentProvider.getUri(`${routine}.int`); if (!uri) { diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index 7e153daf..0568afd6 100644 --- a/src/commands/newFile.ts +++ b/src/commands/newFile.ts @@ -364,7 +364,7 @@ export async function newFile(type: NewFileType): Promise { } // Check if workspace folder has an active connection - let api = new AtelierAPI(wsFolder.uri); + let api: AtelierAPI | undefined = new AtelierAPI(wsFolder.uri); if (!api.active) { if (wsFolder.uri.scheme == FILESYSTEM_SCHEMA) { vscode.window.showErrorMessage( @@ -538,8 +538,8 @@ export async function newFile(type: NewFileType): Promise { ); // Create the type-specific elements prompts, then use them to generate the content - let clsContent: string; - let cls: string; + let clsContent: string | undefined; + let cls: string | undefined; if (type == NewFileType.BusinessOperation) { // Create the prompts for the invocation style and adapter class inputSteps.push( @@ -809,7 +809,7 @@ ClassMethod Transform(source As ${sourceCls}, ByRef target As ${targetCls}) As % const [, desc, assistCls] = results; // Determine the context class, if possible - let contextClass: string; + let contextClass: string | undefined; switch (assistCls) { case "Ens.Alerting.Rule.CreateAlertAssist": contextClass = "Ens.Alerting.Context.CreateAlert"; @@ -829,9 +829,9 @@ ClassMethod Transform(source As ${sourceCls}, ByRef target As ${targetCls}) As % } // Prompt for the production, if required - let production: string; + let production: string | undefined; if (Object.keys(ruleAssists).length && assistCls in ruleAssists && ruleAssists[assistCls].hasProduction) { - const productions: string[] = await api + const productions: string[] = await api! .getEnsClassList(11) .then((data) => data.result.content) .catch(() => []); @@ -972,7 +972,7 @@ ClassMethod %OnDashboardAction(pAction As %String, pContext As %ZEN.proxyObject) cls = results[0]; const [, desc, msgType] = results; - let respClass: string; + let respClass: string | undefined; if (msgType == "Request") { // Prompt the user for the response type const respClasses: vscode.QuickPickItem[] = api @@ -1079,10 +1079,10 @@ Class ${cls}${superclass ? ` Extends ${superclass}` : ""} let clsUri: vscode.Uri; if (wsFolder.uri.scheme == FILESYSTEM_SCHEMA) { // Generate the URI - clsUri = DocumentContentProvider.getUri(`${cls}.cls`, undefined, undefined, undefined, wsFolder.uri); + clsUri = DocumentContentProvider.getUri(`${cls}.cls`, undefined, undefined, undefined, wsFolder.uri)!; } else { // Try to infer the URI from the document index - clsUri = inferDocUri(`${cls}.cls`, wsFolder) ?? (await promptForDocUri(cls, wsFolder)); + clsUri = inferDocUri(`${cls}.cls`, wsFolder) ?? (await promptForDocUri(cls!, wsFolder))!; } if (clsUri && clsContent) { diff --git a/src/commands/project.ts b/src/commands/project.ts index 60f4933a..d51928b0 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -514,7 +514,7 @@ async function pickAdditions( handleError(error, "Failed to get namespace contents."); }); }; - const expandItem = (itemIdx: number): Promise => { + const expandItem = (itemIdx: number): Promise | undefined => { const selected = quickPick.selectedItems; const item = quickPick.items[itemIdx]; quickPick.items[itemIdx].buttons = [ @@ -1084,10 +1084,10 @@ async function handleCommandArg( api = new AtelierAPI(connUri); } if (!project) { - project = await pickProject(api); + project = await pickProject(api!); if (!project) return; } - return { node, api, project }; + return { node, api: api!, project }; } export async function modifyProjectMetadata(nodeOrUri: NodeBase | vscode.Uri | undefined): Promise { diff --git a/src/commands/restDebugPanel.ts b/src/commands/restDebugPanel.ts index ae3344eb..f7234e21 100644 --- a/src/commands/restDebugPanel.ts +++ b/src/commands/restDebugPanel.ts @@ -72,7 +72,7 @@ export class RESTDebugPanel { vscode.window.showErrorMessage("REST service debugging webview requires an active server connection.", "Dismiss"); return; } - if (api.config.apiVersion < 2) { + if (api.config.apiVersion! < 2) { vscode.window.showErrorMessage( "REST service debugging webview requires Atelier API version 2 or above.", "Dismiss" @@ -83,7 +83,7 @@ export class RESTDebugPanel { if (this.currentPanel !== undefined) { // Can only have one panel open at once if (!this.currentPanel._panel.visible) { - if (openEditor.document.uri.toString() == this._file.toString()) { + if (openEditor.document.uri.toString() == this._file!.toString()) { // The open panel is for this document, so show it this.currentPanel._panel.reveal(vscode.ViewColumn.Active); return; @@ -501,8 +501,8 @@ export class RESTDebugPanel { form.onchange = () => sendData(false); button.onclick = () => sendData(true); // Bubble change events up to the form - bodyContent.onchange = headersText.onchange = - paramsText.onchange = path.onchange = + bodyContent.onchange = headersText.onchange = + paramsText.onchange = path.onchange = () => form.dispatchEvent(new Event("change")); @@ -522,7 +522,7 @@ export class RESTDebugPanel { // Make sure the original document is the active text editor this._panel.dispose(); - await vscode.window.showTextDocument(RESTDebugPanel._file, { + await vscode.window.showTextDocument(RESTDebugPanel._file!, { preview: false, viewColumn: vscode.ViewColumn.Active, }); @@ -548,16 +548,7 @@ export class RESTDebugPanel { .trim(); } }); - if ( - headers["authorization"] == undefined && - typeof api.config.username === "string" && - typeof api.config.password === "string" - ) { - // Use the server connection's auth if the user didn't specify any - headers["authorization"] = `Basic ${Buffer.from(`${api.config.username}:${api.config.password}`).toString( - "base64" - )}`; - } + headers["authorization"] = headers["authorization"] ?? (api.config.auth.httpAuthorizationHeader || ""); const hasBody = typeof message.bodyContent == "string" && message.bodyContent != "" && message.bodyType != "No Body"; if (hasBody) { @@ -601,7 +592,7 @@ export class RESTDebugPanel { await new Promise((resolve) => setTimeout(resolve, 500)); // Start the debugging session - await vscode.debug.startDebugging(vscode.workspace.getWorkspaceFolder(RESTDebugPanel._file), { + await vscode.debug.startDebugging(vscode.workspace.getWorkspaceFolder(RESTDebugPanel._file!), { type: "objectscript", request: "attach", name: "REST", diff --git a/src/commands/serverActions.ts b/src/commands/serverActions.ts index a9a14fdb..aeec30b1 100644 --- a/src/commands/serverActions.ts +++ b/src/commands/serverActions.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import { config, workspaceState, - checkConnection, + ensureConnection, explorerProvider, filesystemSchemas, FILESYSTEM_SCHEMA, @@ -25,7 +25,7 @@ type ServerAction = { detail: string; id: string; label: string; rawLink?: strin export async function serverActions(): Promise { const { apiTarget, configName: workspaceFolder } = connectionTarget(); const api = new AtelierAPI(apiTarget); - const { active, host = "", ns = "", https, port = 0, pathPrefix, username, docker } = api.config; + const { active, host = "", ns = "", https, port = 0, pathPrefix, auth, docker } = api.config; const explorerCount = (await explorerProvider.getChildren()).length; if (!explorerCount && (!docker || host === "")) { await vscode.commands.executeCommand("ObjectScriptExplorer.focus"); @@ -57,7 +57,7 @@ export async function serverActions(): Promise { }); } } - const connectionActionsHandler = async (action: ServerAction): Promise => { + const connectionActionsHandler = async (action: ServerAction): Promise => { if (!action) { return; } @@ -72,7 +72,7 @@ export async function serverActions(): Promise { return connConfig.update("conn", { ...targetConfig, active: !active }, target); } case "refreshConnection": { - await checkConnection(true, undefined, true); + await ensureConnection(true, undefined, true); break; } case "switchNamespace": { @@ -95,7 +95,7 @@ export async function serverActions(): Promise { } // Filter out the current namespace - allNamespaces = allNamespaces.filter((ns) => ns.toLowerCase() != api.config.ns.toLowerCase()); + allNamespaces = allNamespaces.filter((ns) => ns.toLowerCase() != api.config.ns!.toLowerCase()); if (!allNamespaces.length) { vscode.window.showErrorMessage(`You don't have access to any other namespaces.`, "Dismiss"); return; @@ -152,7 +152,7 @@ export async function serverActions(): Promise { .replace("${serverAuth}", "") .replace("${ns}", nsEncoded) .replace("${namespace}", ns == "%SYS" ? "sys" : nsEncoded.toLowerCase()) - .replace("${username}", username) + .replace("${username}", auth.username) .replace("${classname}", classname) .replace("${classnameEncoded}", classnameEncoded) .replace("${project}", project); @@ -204,7 +204,7 @@ export async function serverActions(): Promise { } if ( (!vscode.window.activeTextEditor && wsUri && filesystemSchemas.includes(wsUri.scheme)) || - filesystemSchemas.includes(vscode.window.activeTextEditor?.document.uri.scheme) + filesystemSchemas.includes(vscode.window.activeTextEditor?.document.uri.scheme as string) ) { actions.push({ id: "serverCommandMenu", @@ -248,7 +248,10 @@ export async function serverActions(): Promise { if (addin) { sendStudioAddinTelemetryEvent(addin.label); let params = `Namespace=${nsEncoded}`; - params += `&User=${encodeURIComponent(username)}`; + const username = auth.username; + if (!username.includes("*")) { + params += `&User=${encodeURIComponent(username)}`; + } if (project != "") { params += `&Project=${encodeURIComponent(project)}`; } diff --git a/src/commands/showAllClassMembers.ts b/src/commands/showAllClassMembers.ts index dba84ec4..13b4cfe3 100644 --- a/src/commands/showAllClassMembers.ts +++ b/src/commands/showAllClassMembers.ts @@ -166,7 +166,7 @@ SELECT Name, Origin, 'x' AS MemberType, Parent, Internal, 0 AS NotInheritable, M const position = vscode.extensions.getExtension(lsExtensionId)?.isActive ? symbol.selectionRange.start : symbol.range.start; - await vscode.window.showTextDocument(targetUri, { + await vscode.window.showTextDocument(targetUri!, { selection: new vscode.Range(position, position), preview: false, }); diff --git a/src/commands/showPlanPanel.ts b/src/commands/showPlanPanel.ts index ce87b1d9..ddfb1d7b 100644 --- a/src/commands/showPlanPanel.ts +++ b/src/commands/showPlanPanel.ts @@ -8,7 +8,7 @@ import { iscIcon } from "../extension"; const viewType = "isc-show-plan"; const viewTitle = "Show Plan"; -let panel: vscode.WebviewPanel; +let panel: vscode.WebviewPanel | undefined; /** Escape any HTML characters so they are rendered literally */ function htmlEncode(str: string): string { @@ -82,7 +82,7 @@ export async function showPlanWebview(args: { vscode.window.showErrorMessage("Show Plan requires an active server connection.", "Dismiss"); return; } - if (lt(api.config.serverVersion, "2024.1.0")) { + if (lt(api.config.serverVersion!, "2024.1.0")) { vscode.window.showErrorMessage("Show Plan requires InterSystems IRIS version 2024.1 or above.", "Dismiss"); return; } @@ -111,7 +111,7 @@ export async function showPlanWebview(args: { const planXML: string = await api .actionQuery("SELECT %SYSTEM.QUERY_PLAN(?,,,,,?) XML", [ args.sqlQuery.trimEnd(), - `{${!lt(api.config.serverVersion, "2026.1.0") ? '"format":"LINEAR-XML",' : ""}"selectmode":"${args.selectMode}"${args.imports.length ? `,"packages":"$LFS(\\"${[...new Set(args.imports)].join(",")}\\")"` : ""}${args.includes.length ? `,"includeFiles":"$LFS(\\"${[...new Set(args.includes)].join(",")}\\")"` : ""}}`, + `{${!lt(api.config.serverVersion!, "2026.1.0") ? '"format":"LINEAR-XML",' : ""}"selectmode":"${args.selectMode}"${args.imports.length ? `,"packages":"$LFS(\\"${[...new Set(args.imports)].join(",")}\\")"` : ""}${args.includes.length ? `,"includeFiles":"$LFS(\\"${[...new Set(args.includes)].join(",")}\\")"` : ""}}`, ]) .then((data) => data?.result?.content[0]?.XML) .catch((error) => { @@ -129,7 +129,7 @@ export async function showPlanWebview(args: { // Loop through the child elements of the plan let capturePlan = false; let planText = ""; - let planChild = (planElem.firstChild); + let planChild = (planElem!.firstChild); while (planChild) { switch (planChild.nodeName) { case "sql": @@ -140,16 +140,16 @@ export async function showPlanWebview(args: { planHTML += `\n
\n`; break; case "warning": - planHTML += `

Warning

\n

\n${formatTextBlock(planChild.textContent)}

\n
\n`; + planHTML += `

Warning

\n

\n${formatTextBlock(planChild.textContent!)}

\n
\n`; break; case "info": - planHTML += `

Information

\n${formatTextBlock(planChild.textContent)}
\n`; + planHTML += `

Information

\n${formatTextBlock(planChild.textContent!)}
\n`; break; case "cost": { planHTML += `

Relative Cost `; // The plan might not have a cost const cost = planChild.attributes.getNamedItem("value")?.value; - planHTML += +cost ? `= ${cost}` : "Unavailable"; + planHTML += +cost! ? `= ${cost}` : "Unavailable"; planHTML += "

\n"; capturePlan = true; break; @@ -171,7 +171,7 @@ export async function showPlanWebview(args: { moduleText += moduleChild.textContent; moduleChild = moduleChild.nextSibling; } - planHTML += `

Module ${planChild.attributes.item(0).value}

\n${formatTextBlock(moduleText)}
\n`; + planHTML += `

Module ${planChild.attributes.item(0)!.value}

\n${formatTextBlock(moduleText)}
\n`; break; } case "subquery": { @@ -181,7 +181,7 @@ export async function showPlanWebview(args: { subqueryText += subqueryChild.textContent; subqueryChild = subqueryChild.nextSibling; } - planHTML += `

Subquery ${planChild.attributes.item(0).value}

\n${formatTextBlock(subqueryText)}
\n`; + planHTML += `

Subquery ${planChild.attributes.item(0)!.value}

\n${formatTextBlock(subqueryText)}
\n`; break; } } diff --git a/src/commands/studio.ts b/src/commands/studio.ts index 787b28ec..6543b9ea 100644 --- a/src/commands/studio.ts +++ b/src/commands/studio.ts @@ -239,11 +239,11 @@ export class StudioActions { } } - const fileExt = fileName.split(".").pop().toLowerCase(); + const fileExt = fileName.split(".").pop()!.toLowerCase(); const isCorrectMethod = (text: string) => fileExt === "cls" ? text.match("Method " + method) : text.startsWith(method); - vscode.window.showTextDocument(DocumentContentProvider.getUri(fileName), { preview: false }).then( + vscode.window.showTextDocument(DocumentContentProvider.getUri(fileName)!, { preview: false }).then( (newEditor) => { if (method) { const document = newEditor.document; @@ -289,7 +289,7 @@ export class StudioActions { return Promise.resolve(); } - private userAction(action, afterUserAction = false, answer = "", msg = "", type = 0): Thenable { + private userAction(action, afterUserAction = false, answer = "", msg = "", type = 0): Thenable | undefined { if (!action || action.id == "") { return; } @@ -515,7 +515,7 @@ export class StudioActions { public getServerInfo(): { server: string; namespace: string } { return { server: `${this.api.config.host}:${this.api.config.port}${this.api.config.pathPrefix}`, - namespace: this.api.config.ns, + namespace: this.api.config.ns!, }; } } @@ -574,7 +574,7 @@ export async function fireOtherStudioAction( action: OtherStudioAction, uri: vscode.Uri, userAction?: UserAction -): Promise { +): Promise { if (vscode.workspace.getConfiguration("objectscript.serverSourceControl", uri)?.get("disableOtherActionTriggers")) { return; } diff --git a/src/commands/studioMigration.ts b/src/commands/studioMigration.ts index 1de246ea..df759fe3 100644 --- a/src/commands/studioMigration.ts +++ b/src/commands/studioMigration.ts @@ -166,7 +166,7 @@ export async function loadStudioSnippets(): Promise { * * Store the editor background color in the user `settings.json` file under `workbench.colorCustomizations`. * * Activate the modified theme. */ -export async function loadStudioColors(languageServerExt: vscode.Extension | undefined): Promise { +export async function loadStudioColors(languageServerExt: vscode.Extension | null | undefined): Promise { // Check that we're on windows if (process.platform != "win32") { vscode.window.showErrorMessage("Loading Studio syntax colors is only supported on Windows.", "Dismiss"); @@ -202,7 +202,7 @@ export async function loadStudioColors(languageServerExt: vscode.Extension ); await vscode.workspace.fs.writeFile(tempRoutineUri, new TextEncoder().encode("ROUTINE temp\n")); await vscode.workspace.openTextDocument(tempRoutineUri); - let legend: vscode.SemanticTokensLegend = await vscode.commands + let legend: vscode.SemanticTokensLegend | undefined = await vscode.commands .executeCommand("vscode.provideDocumentSemanticTokensLegend", tempRoutineUri) // Swallow any errors .then( @@ -267,7 +267,7 @@ export async function loadStudioColors(languageServerExt: vscode.Extension } if (!line.startsWith(" ")) { // This is a header line so check if it's the start of a Language - const langMatch = lineTrim.split("\\").pop().match(langRegex); + const langMatch = lineTrim.split("\\").pop()!.match(langRegex); if (langMatch != null) { currentLanguage = Number(langMatch[1]); } @@ -325,10 +325,10 @@ export async function loadStudioColors(languageServerExt: vscode.Extension // Modify the theme const editorConfig = vscode.workspace.getConfiguration("editor"); const workbenchConfig = vscode.workspace.getConfiguration("workbench"); - const tokensConfig = editorConfig.get("semanticTokenColorCustomizations"); + const tokensConfig: any = editorConfig.get("semanticTokenColorCustomizations"); tokensConfig[`[${themeName}]`] = { rules }; await editorConfig.update("semanticTokenColorCustomizations", tokensConfig, true); - const colorsConfig = workbenchConfig.get("colorCustomizations"); + const colorsConfig: any = workbenchConfig.get("colorCustomizations"); colorsConfig[`[${themeName}]`] = { "editor.background": editorBackground }; await workbenchConfig.update("colorCustomizations", colorsConfig, true); diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index 138bd0ad..570b8eed 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -277,7 +277,9 @@ function applyTestResultDecorations( } for (const { item, result, durationText } of methodResults) { - const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === item.uri.toString()); + const editor = vscode.window.visibleTextEditors.find( + (e) => item.uri && e.document.uri.toString() === item.uri.toString() + ); if (!editor || !item.range) { continue; } @@ -415,10 +417,10 @@ function labelFromAssertion(assertion: LegacyAssertion): string | undefined { /** Find the root `TestItem` for `uri` */ function rootItemForItem(testController: vscode.TestController, uri: vscode.Uri): vscode.TestItem | undefined { - let rootItem: vscode.TestItem; + let rootItem: vscode.TestItem | undefined; const uriString = uri.toString(); for (const [, i] of testController.items) { - if (uriIsAncestorOf(i.uri, uri) || uriString == i.uri.toString()) { + if (uriIsAncestorOf(i.uri!, uri) || uriString == i.uri!.toString()) { rootItem = i; break; } @@ -435,14 +437,14 @@ async function addTestItemsForClass(testController: vscode.TestController, paren parent.uri ); if (parentSymbols?.length == 1 && parentSymbols[0].kind == vscode.SymbolKind.Class) { - const rootItem = rootItemForItem(testController, parent.uri); + const rootItem = rootItemForItem(testController, parent.uri!); if (rootItem) { // Add this class to our cache // Need to do this here because we need the // DocumentSymbols to accurately determine the class const classes = classesForRoot.get(rootItem); - classes.set(parentSymbols[0].name, parent); - classesForRoot.set(rootItem, classes); + classes!.set(parentSymbols[0].name, parent); + classesForRoot.set(rootItem, classes!); } parent.range = parentSymbols[0].range; // Add an item for each Test* method defined in this class @@ -459,10 +461,10 @@ async function addTestItemsForClass(testController: vscode.TestController, paren parent.children.add(newItem); } }); - if (filesystemSchemas.includes(parent.uri.scheme)) { + if (filesystemSchemas.includes(parent.uri!.scheme)) { // Query the server to find inherited Test* methods - const api = new AtelierAPI(parent.uri); - const workspaceFolder = vscode.workspace.getWorkspaceFolder(parent.uri).name; + const api = new AtelierAPI(parent.uri!); + const workspaceFolder = vscode.workspace.getWorkspaceFolder(parent.uri!)!.name; const methodsMap: Map = new Map(); const inheritedMethods: { Name: string; Origin: string }[] = await api .actionQuery( @@ -644,7 +646,7 @@ function createRootItemsForWorkspaceFolder( ? "Server connection is inactive" : api.ns == "%SYS" ? "Connected to the %SYS namespace" - : api.config.apiVersion < 8 + : api.config.apiVersion! < 8 ? "Must be connected to InterSystems IRIS version 2023.3 or above" : filesystemSchemas.includes(folder.uri.scheme) && csp ? "Web application folder" @@ -712,20 +714,20 @@ async function getTestItemForClass( uri: vscode.Uri, create = false ): Promise { - let item: vscode.TestItem; + let item: vscode.TestItem | undefined; const rootItem = rootItemForItem(testController, uri); if (rootItem && !rootItem.error) { // Walk the directory path until we reach a dead end or the TestItem for this class - let docPath = uri.path.slice(rootItem.uri.path.length); + let docPath = uri.path.slice(rootItem.uri!.path.length); docPath = docPath.startsWith("/") ? docPath.slice(1) : docPath; const docPathParts = docPath.split("/"); item = rootItem; for (const part of docPathParts) { - const currUri = item.uri.with({ path: `${item.uri.path}${!item.uri.path.endsWith("/") ? "/" : ""}${part}` }); + const currUri = item.uri!.with({ path: `${item.uri!.path}${!item.uri!.path.endsWith("/") ? "/" : ""}${part}` }); let currItem = item.children.get(currUri.toString()); if (!currItem && create) { // We're allowed to create non-existent directory TestItems as we walk the path - await testController.resolveHandler(item); + await testController.resolveHandler!(item); currItem = item.children.get(currUri.toString()); } item = currItem; @@ -753,10 +755,10 @@ function replaceRootTestItems(testController: vscode.TestController): void { async function childrenForServerSideFolderItem( item: vscode.TestItem ): Promise>> { - const { project, system, generated, mapped } = isfsConfig(item.uri); + const { project, system, generated, mapped } = isfsConfig(item.uri!); let query: string; let parameters: string[]; - let folder = !item.uri.path.endsWith("/") ? item.uri.path + "/" : item.uri.path; + let folder = !item.uri!.path.endsWith("/") ? item.uri!.path + "/" : item.uri!.path; folder = folder.startsWith("/") ? folder.slice(1) : folder; if (folder == "/") { // Treat this the same as an empty folder @@ -764,7 +766,7 @@ async function childrenForServerSideFolderItem( } folder = folder.replace(/\//g, "."); const folderLen = String(folder.length + 1); // Need the + 1 because SUBSTR is 1 indexed - const api = new AtelierAPI(item.uri); + const api = new AtelierAPI(item.uri!); if (project) { query = "SELECT DISTINCT CASE " + @@ -788,7 +790,7 @@ async function childrenForServerSideFolderItem( folderLen, folderLen, folderLen, - fileSpecFromURI(item.uri), + fileSpecFromURI(item.uri!), "1", "1", system ? "1" : "0", @@ -806,8 +808,8 @@ async function childrenForServerSideFolderItem( /** Create a child `TestItem` of `item` with label `child`. */ function addChildItem(testController: vscode.TestController, item: vscode.TestItem, child: string): void { - const newUri = item.uri.with({ - path: `${item.uri.path}${!item.uri.path.endsWith("/") ? "/" : ""}${child}`, + const newUri = item.uri!.with({ + path: `${item.uri!.path}${!item.uri!.path.endsWith("/") ? "/" : ""}${child}`, }); if (!item.children.get(newUri.toString())) { // Only add the item if it doesn't already exist @@ -819,7 +821,7 @@ function addChildItem(testController: vscode.TestController, item: vscode.TestIt /** Determine the class name of `item` in `root` */ function classNameForItem(item: vscode.TestItem, root: vscode.TestItem): string | undefined { - let cls: string; + let cls: string | undefined; const classes = classesForRoot.get(root); if (classes) { for (const element of classes) { @@ -854,7 +856,7 @@ async function addItemForClassUri(testController: vscode.TestController, uri: vs const item = await getTestItemForClass(testController, uri, true); if (item && item.canResolveChildren && !item.children.size) { // Resolve the methods - testController.resolveHandler(item); + testController.resolveHandler!(item); } } } @@ -965,15 +967,15 @@ async function executeLegacyRunner( action: string, showOutput: boolean ): Promise { - const generateBase = await promptGenerateLegacyBase(root.uri); + const generateBase = await promptGenerateLegacyBase(root.uri!); if (generateBase === undefined) { return true; } - const unitTestConfiguration = vscode.workspace.getConfiguration("objectscript.unitTest", root.uri); + const unitTestConfiguration = vscode.workspace.getConfiguration("objectscript.unitTest", root.uri!); const configuredLegacyTimeout = unitTestConfiguration.get("legacyRequestTimeout"); const legacyRequestTimeout = Number.isFinite(configuredLegacyTimeout) - ? Math.max(0, Math.floor(configuredLegacyTimeout)) + ? Math.max(0, Math.floor(configuredLegacyTimeout!)) : DEFAULT_LEGACY_REQUEST_TIMEOUT; const testRun = testController.createTestRun(request, undefined, true); @@ -1008,7 +1010,7 @@ async function executeLegacyRunner( generateBaseUT: generateBase, console: asyncRequest.console, namespace: api.ns, - username: api.config.username ?? "", + username: api.config.auth.username, }, { signal, @@ -1056,7 +1058,7 @@ async function executeLegacyRunner( const knownStatuses: WeakMap = new WeakMap(); const classes = classesForRoot.get(root) ?? new Map(); - const workspaceFolder = vscode.workspace.getWorkspaceFolder(root.uri); + const workspaceFolder = vscode.workspace.getWorkspaceFolder(root.uri!); const documentSymbols: Map = new Map(); const filesText: Map = new Map(); const requestedMethodsByClass: Map | undefined> = new Map(); @@ -1189,9 +1191,9 @@ async function executeLegacyRunner( if (failure.location) { if (failure.location.document.toLowerCase().endsWith(".cls") && workspaceFolder) { - let locationUri: vscode.Uri; + let locationUri: vscode.Uri | null | undefined; if (classes.has(failure.location.document.slice(0, -4))) { - locationUri = classes.get(failure.location.document.slice(0, -4)).uri; + locationUri = classes.get(failure.location.document.slice(0, -4))!.uri; } else { locationUri = DocumentContentProvider.getUri( failure.location.document, @@ -1224,8 +1226,8 @@ async function executeLegacyRunner( const locationLine = methodOffsetToLine( locationSymbols, fileText, - failure.location.label, - failure.location.offset + failure.location.label ?? "", + failure.location.offset ?? 0 ); if (locationLine != undefined) { message.location = new vscode.Location( @@ -1327,7 +1329,7 @@ async function runHandler( debug = false ): Promise { const action = debug ? "debug" : "run"; - let root: vscode.TestItem; + let root: vscode.TestItem | undefined; const asyncRequest: Atelier.AsyncUnitTestRequest = { request: "unittest", tests: [], @@ -1337,9 +1339,9 @@ async function runHandler( try { // Determine the test root for this run - let roots: vscode.TestItem[]; + let roots: (vscode.TestItem | undefined)[]; if (request.include?.length) { - roots = [...new Set(request.include.map((i) => rootItemForItem(testController, i.uri)))]; + roots = [...new Set(request.include.map((i) => rootItemForItem(testController, i.uri!)))]; } else { // Run was launched from controller's root level // Ignore any roots that have errors @@ -1351,8 +1353,8 @@ async function runHandler( const picked = await vscode.window.showQuickPick( roots.map((i) => { return { - label: i.label, - detail: displayableUri(i.uri), + label: i!.label, + detail: displayableUri(i!.uri!), item: i, }; }), @@ -1372,14 +1374,14 @@ async function runHandler( // Need a root to continue return; } - sendUnitTestTelemetryEvent(root.uri, debug); + sendUnitTestTelemetryEvent(root.uri!, debug); // Add the initial items to the queue to process const queue: vscode.TestItem[] = []; - const rootUriString = root.uri.toString(); + const rootUriString = root.uri!.toString(); if (request.include?.length) { request.include.forEach((i) => { - if (uriIsAncestorOf(root.uri, i.uri) || i.uri.toString() == rootUriString) { + if (uriIsAncestorOf(root!.uri!, i.uri!) || i.uri!.toString() == rootUriString) { queue.push(i); } }); @@ -1389,16 +1391,16 @@ async function runHandler( // Get the autoload configuration for the root const autoload = vscode.workspace.getConfiguration("objectscript.unitTest.autoload", root.uri); - const autoloadFolder: string = autoload.get("folder"); - const autoloadXml: boolean = autoload.get("xml"); - const autoloadUdl: boolean = autoload.get("udl"); - const autoloadEnabled: boolean = autoloadFolder != "" && (autoloadXml || autoloadUdl) && notIsfs(root.uri); + const autoloadFolder: string = autoload.get("folder")!; + const autoloadXml: boolean = autoload.get("xml")!; + const autoloadUdl: boolean = autoload.get("udl")!; + const autoloadEnabled: boolean = autoloadFolder != "" && (autoloadXml || autoloadUdl) && notIsfs(root.uri!); const autoloadProcessed: string[] = []; // Process every test that was queued // Recurse down to leaves (methods) and build a map of their parents (classes) while (queue.length > 0 && !token.isCancellationRequested) { - const test = queue.pop(); + const test = queue.pop()!; // Skip tests the user asked to exclude if (request.exclude?.length && request.exclude.some((excludedTest) => excludedTest.id === test.id)) { @@ -1407,8 +1409,8 @@ async function runHandler( if (autoloadEnabled) { // Process any autoload folders needed by this item - const basePath = root.uri.path.endsWith("/") ? root.uri.path.slice(0, -1) : root.uri.path; - const directories = ["", ...test.uri.path.slice(basePath.length + 1).split("/")]; + const basePath = root.uri!.path.endsWith("/") ? root.uri!.path.slice(0, -1) : root.uri!.path; + const directories = ["", ...test.uri!.path.slice(basePath.length + 1).split("/")]; if (directories[directories.length - 1].toLowerCase().endsWith(".cls")) { // Remove the class name directories.pop(); @@ -1421,7 +1423,7 @@ async function runHandler( // Look for XML or UDL files in the autoload folder const files = await vscode.workspace.findFiles( new vscode.RelativePattern( - test.uri.with({ path: `${basePath}${testPath}/${autoloadFolder}` }), + test.uri!.with({ path: `${basePath}${testPath}/${autoloadFolder}` }), `**/*.{${autoloadXml ? "xml,XML" : ""}${autoloadXml && autoloadUdl ? "," : ""}${ autoloadUdl ? "cls,CLS,mac,MAC,int,INT,inc,INC" : "" }}` @@ -1444,16 +1446,16 @@ async function runHandler( // Resolve children if not already done if (test.canResolveChildren && !test.children.size) { - await testController.resolveHandler(test); + await testController.resolveHandler!(test); } - if (test.uri.path.toLowerCase().endsWith(".cls")) { + if (test.uri!.path.toLowerCase().endsWith(".cls")) { if (test.id.includes(methodIdSeparator)) { // This is a method item // Will only reach this code if this item is in request.include // Look up the name of this class - const cls = classNameForItem(test.parent, root); + const cls = classNameForItem(test.parent!, root); if (cls) { // Check if there's a test object for the parent class already const clsObjIdx = asyncRequest.tests.findIndex((t) => t.class == cls); @@ -1470,15 +1472,15 @@ async function runHandler( class: cls, methods: [test.label], }); - if (notIsfs(test.parent.uri)) { + if (notIsfs(test.parent!.uri!)) { // Add this class to the list to load if (asyncRequest.load == undefined) asyncRequest.load = []; asyncRequest.load.push({ - file: test.parent.uri.fsPath, - content: textDecoder.decode(await vscode.workspace.fs.readFile(test.parent.uri)).split(/\r?\n/), + file: test.parent!.uri!.fsPath, + content: textDecoder.decode(await vscode.workspace.fs.readFile(test.parent!.uri!)).split(/\r?\n/), }); } - clsItemsRun.push(test.parent); + clsItemsRun.push(test.parent!); } } } else { @@ -1494,8 +1496,8 @@ async function runHandler( // Determine the methods to run clsObj.methods = []; test.children.forEach((i) => { - if (!request.exclude.some((excludedTest) => excludedTest.id === i.id)) { - clsObj.methods.push(i.label); + if (!request.exclude!.some((excludedTest) => excludedTest.id === i.id)) { + clsObj.methods!.push(i.label); } }); if (clsObj.methods.length == 0) { @@ -1507,12 +1509,12 @@ async function runHandler( delete clsObj.methods; } } - if (notIsfs(test.uri)) { + if (notIsfs(test.uri!)) { // Add this class to the list to load if (asyncRequest.load == undefined) asyncRequest.load = []; asyncRequest.load.push({ - file: test.uri.fsPath, - content: textDecoder.decode(await vscode.workspace.fs.readFile(test.uri)).split(/\r?\n/), + file: test.uri!.fsPath, + content: textDecoder.decode(await vscode.workspace.fs.readFile(test.uri!)).split(/\r?\n/), }); } asyncRequest.tests.push(clsObj); @@ -1565,7 +1567,7 @@ async function runHandler( ); // Send the queue request - const queueResp: Atelier.Response = await api.queueAsync(asyncRequest, true).catch((error) => { + const queueResp: Atelier.Response | undefined = await api.queueAsync(asyncRequest, true).catch((error) => { handleError(error, `Error creating job to ${action} tests.`); return undefined; }); @@ -1602,7 +1604,7 @@ async function runHandler( let currentOutputItem: vscode.TestItem | undefined; // The workspace folder that we're running tests in - const workspaceFolder = vscode.workspace.getWorkspaceFolder(root.uri); + const workspaceFolder = vscode.workspace.getWorkspaceFolder(root.uri!); // A map of all documents that we've computed symbols for const documentSymbols: Map = new Map(); @@ -1620,19 +1622,19 @@ async function runHandler( if (indent == 4) { if (consoleLine.endsWith("...")) { // This is the beginning of a class - currentOutputItem = classes.get(consoleLine.trim().split(" ")[0]); + currentOutputItem = classes!.get(consoleLine.trim().split(" ")[0]); } else { // This is the end of a class if (currentOutputItem != undefined && currentOutputItem.id.includes(methodIdSeparator)) { - currentOutputItem = currentOutputItem.parent; + currentOutputItem = currentOutputItem.parent!; } } } else if (indent == 6 && consoleLine.endsWith("...")) { // This is the beginning of a method if (currentOutputItem != undefined) { if (currentOutputItem.id.includes(methodIdSeparator)) { - currentOutputItem = currentOutputItem.parent.children.get( - `${currentOutputItem.parent.id}${methodIdSeparator}${consoleLine.trim().slice(4).split("(")[0]}` + currentOutputItem = currentOutputItem.parent!.children.get( + `${currentOutputItem.parent!.id}${methodIdSeparator}${consoleLine.trim().slice(4).split("(")[0]}` ); } else { currentOutputItem = currentOutputItem.children.get( @@ -1647,7 +1649,7 @@ async function runHandler( if (currentOutputItem != undefined) { testRun.appendOutput( `${consoleLine}\r\n`, - new vscode.Location(currentOutputItem.uri, currentOutputItem.range), + new vscode.Location(currentOutputItem.uri!, currentOutputItem.range!), currentOutputItem ); } else { @@ -1663,7 +1665,7 @@ async function runHandler( if (Array.isArray(pollResp.result)) { // Process results for (const testResult of pollResp.result) { - const clsItem = classes.get(testResult.class); + const clsItem = classes!.get(testResult.class); if (clsItem) { if (testResult.method) { // This is a method's result @@ -1687,10 +1689,10 @@ async function runHandler( ); if (failure.location) { if (failure.location.document.toLowerCase().endsWith(".cls")) { - let locationUri: vscode.Uri; - if (classes.has(failure.location.document.slice(0, -4))) { + let locationUri: vscode.Uri | null | undefined; + if (classes!.has(failure.location.document.slice(0, -4))) { // This is one of the known test classes - locationUri = classes.get(failure.location.document.slice(0, -4)).uri; + locationUri = classes!.get(failure.location.document.slice(0, -4))!.uri; } else { // This is some other class. There's a chance that // the class won't exist after the tests are run @@ -1698,7 +1700,7 @@ async function runHandler( // because it will often be useful to the user. locationUri = DocumentContentProvider.getUri( failure.location.document, - workspaceFolder.name, + workspaceFolder!.name, failure.location.namespace ); } @@ -1727,7 +1729,7 @@ async function runHandler( const locationLine = methodOffsetToLine( locationSymbols, fileText, - failure.location.label, + failure.location.label!, failure.location.offset ); if (locationLine != undefined) { @@ -1751,7 +1753,7 @@ async function runHandler( // because it will often be useful to the user. const locationUri = DocumentContentProvider.getUri( failure.location.document, - workspaceFolder.name, + workspaceFolder!.name, failure.location.namespace ); if (locationUri) { @@ -1837,7 +1839,7 @@ async function runHandler( } else if (debug && queueResp.result.content?.debugId && pollResp.result?.content?.debugReady) { // Make sure the activeTextEditor's document is in the same workspace folder as the test // root so the debugger connects to the correct server and runs in the correct namespace - const rootWsFolderIdx = vscode.workspace.getWorkspaceFolder(root.uri)?.index; + const rootWsFolderIdx = vscode.workspace.getWorkspaceFolder(root.uri!)?.index; if ( !vscode.window.activeTextEditor?.document.uri || vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri)?.index != rootWsFolderIdx @@ -1853,7 +1855,9 @@ async function runHandler( } if (!shown) { // Show the first test class. Ugly but necessary. - await vscode.window.showTextDocument(classesForRoot.get(root).get(asyncRequest.tests[0].class)?.uri); + await vscode.window.showTextDocument( + classesForRoot.get(root)!.get(asyncRequest.tests[0].class)?.uri as vscode.Uri + ); } } // Start the debugging session @@ -1929,30 +1933,30 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di if (!item) return; // Can't resolve "undefined" item.busy = true; try { - if (item.uri.path.toLowerCase().endsWith(".cls")) { + if (item.uri!.path.toLowerCase().endsWith(".cls")) { // Compute items for the Test* methods in this class await addTestItemsForClass(testController, item); } else { - if (notIsfs(item.uri)) { + if (notIsfs(item.uri!)) { // Read the local directory for non-autoload subdirectories and classes const autoload = vscode.workspace.getConfiguration("objectscript.unitTest.autoload", item.uri); - const autoloadFolder: string = autoload.get("folder"); - const autoloadEnabled: boolean = autoloadFolder != "" && (autoload.get("xml") || autoload.get("udl")); - const workspaceFolder = vscode.workspace.getWorkspaceFolder(item.uri); + const autoloadFolder: string = autoload.get("folder")!; + const autoloadEnabled: boolean = autoloadFolder != "" && (autoload.get("xml") || autoload.get("udl"))!; + const workspaceFolder = vscode.workspace.getWorkspaceFolder(item.uri!); let workspaceRootItem: vscode.TestItem | undefined; if (workspaceFolder) { for (const [, root] of testController.items) { - if (root.uri.toString() === workspaceFolder.uri.toString()) { + if (root.uri!.toString() === workspaceFolder.uri.toString()) { workspaceRootItem = root; break; } } } - const entries = await vscode.workspace.fs.readDirectory(item.uri); + const entries = await vscode.workspace.fs.readDirectory(item.uri!); for (const element of entries) { - const childUri = item.uri.with({ - path: `${item.uri.path}${!item.uri.path.endsWith("/") ? "/" : ""}${element[0]}`, + const childUri = item.uri!.with({ + path: `${item.uri!.path}${!item.uri!.path.endsWith("/") ? "/" : ""}${element[0]}`, }); // Aplica o filtro dos roots configurados @@ -1989,7 +1993,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Create new roots replaceRootTestItems(testController); // Resolve children for the roots - testController.items.forEach((item) => testController.resolveHandler(item)); + testController.items.forEach((item) => testController.resolveHandler!(item)); }; // Create the run and debug profiles const runProfile = testController.createRunProfile( @@ -2028,7 +2032,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di : r ); Promise.allSettled([ - vscode.extensions.getExtension(extensionId).activate(), + vscode.extensions.getExtension(extensionId)!.activate(), languageServer && !languageServer.isActive ? Promise.allSettled([languageServer.activate(), waitForResponse(1)]) : Promise.resolve(), @@ -2049,7 +2053,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Remove from our cache of classes const classes = classesForRoot.get(rootItem); if (classes) { - let cls: string; + let cls: string | undefined; for (const element of classes) { if (element[1].id == item.id) { cls = element[0]; @@ -2062,7 +2066,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di } } } - item.parent.children.delete(uri.toString()); + item.parent!.children.delete(uri.toString()); result = true; } } @@ -2079,7 +2083,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Update root items if needed e.removed.forEach((wf) => { testController.items.forEach((i) => { - if (uriIsAncestorOf(wf.uri, i.uri)) { + if (uriIsAncestorOf(wf.uri, i.uri!)) { // Remove this TestItem classesForRoot.delete(i); testController.items.delete(i.id); @@ -2099,7 +2103,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di const replace: vscode.TestItem[] = []; testController.items.forEach((item) => { if ( - (notIsfs(item.uri) && e.affectsConfiguration("objectscript.unitTest", item.uri)) || + (notIsfs(item.uri!) && e.affectsConfiguration("objectscript.unitTest", item.uri)) || e.affectsConfiguration("objectscript.conn", item.uri) || e.affectsConfiguration("intersystems.servers", item.uri) ) { @@ -2110,7 +2114,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di replace.forEach((item) => { classesForRoot.delete(item); testController.items.delete(item.id); - const folder = vscode.workspace.getWorkspaceFolder(item.uri); + const folder = vscode.workspace.getWorkspaceFolder(item.uri!); if (folder) { const newItems = createRootItemsForWorkspaceFolder(testController, folder); newItems.forEach((i) => { @@ -2136,7 +2140,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di testController.invalidateTestResults(item); if (item.canResolveChildren) { // Resolve the methods - testController.resolveHandler(item); + testController.resolveHandler!(item); } } } diff --git a/src/commands/viewOthers.ts b/src/commands/viewOthers.ts index fdce801b..b7a493be 100644 --- a/src/commands/viewOthers.ts +++ b/src/commands/viewOthers.ts @@ -18,9 +18,9 @@ export async function viewOthers(forceEditable = false): Promise { item = item.slice(0, colonidx); let uri: vscode.Uri; if (forceEditable) { - uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable); + uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable)!; } else { - uri = DocumentContentProvider.getUri(item); + uri = DocumentContentProvider.getUri(item)!; } if (item.endsWith(".cls")) { @@ -75,9 +75,9 @@ export async function viewOthers(forceEditable = false): Promise { } else { let uri: vscode.Uri; if (forceEditable) { - uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable); + uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable)!; } else { - uri = DocumentContentProvider.getUri(item); + uri = DocumentContentProvider.getUri(item)!; } vscode.window.showTextDocument(uri); } @@ -90,11 +90,11 @@ export async function viewOthers(forceEditable = false): Promise { const api = new AtelierAPI(file.uri); if (!api.active) return; let indexarg: string = file.name; - const cursorpos: vscode.Position = vscode.window.activeTextEditor.selection.active; - const fileExt: string = file.name.split(".").pop().toLowerCase(); + const cursorpos: vscode.Position = vscode.window.activeTextEditor!.selection.active; + const fileExt: string = file.name.split(".").pop()!.toLowerCase(); if ( - api.config.apiVersion >= 4 && + api.config.apiVersion! >= 4 && (fileExt === "cls" || fileExt === "mac" || fileExt === "int") && !/^%sqlcq/i.test(indexarg) ) { @@ -108,7 +108,7 @@ export async function viewOthers(forceEditable = false): Promise { symbols = symbols[0].children; } - let currentSymbol: vscode.DocumentSymbol; + let currentSymbol: vscode.DocumentSymbol | undefined; for (const symbol of symbols) { if (symbol.range.contains(cursorpos)) { currentSymbol = symbol; @@ -128,7 +128,7 @@ export async function viewOthers(forceEditable = false): Promise { let isObjectScript = true; if (fileExt === "cls") { - const memberInfo = parseClassMemberDefinition(vscode.window.activeTextEditor.document, currentSymbol); + const memberInfo = parseClassMemberDefinition(vscode.window.activeTextEditor!.document, currentSymbol); if (memberInfo) { const { defEndLine, language } = memberInfo; offset = cursorpos.line - defEndLine; @@ -157,7 +157,7 @@ export async function viewOthers(forceEditable = false): Promise { open(listOthers[0], forceEditable); } else { vscode.window.showQuickPick(listOthers).then((item) => { - open(item, forceEditable); + open(item!, forceEditable); }); } }) diff --git a/src/commands/webSocketTerminal.ts b/src/commands/webSocketTerminal.ts index 9b71df36..c5b138fe 100644 --- a/src/commands/webSocketTerminal.ts +++ b/src/commands/webSocketTerminal.ts @@ -36,18 +36,12 @@ const actions = { }; /** Data received from the WebSocket */ -interface WebSocketMessage { - /** The type of the message */ - type: "prompt" | "read" | "error" | "output" | "init" | "color"; - /** The text of the message. Present for all types but "read" and "init". */ - text?: string; - /** The WebSocket protocol version. Only present for "init". */ - protocol?: number; - /** The InterSystems IRIS `$ZVERSION`. Only present for "init". */ - version?: string; - /** The current namespace. Only present for "prompt" on IRIS 2025.3+. */ - ns?: string; -} +type WebSocketMessage = + /** `ns` is only sent by IRIS 2025.3+ */ + | { type: "prompt"; text: string; ns?: string } + | { type: "error" | "output" | "color"; text: string } + | { type: "read" } + | { type: "init"; protocol: number; version: string }; class WebSocketTerminal implements vscode.Pseudoterminal { private _writeEmitter = new vscode.EventEmitter(); @@ -99,7 +93,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { private _colorsRegex = /\x1b[^m]*?m/g; /** The terminal's current namespace */ - public currentNs: string; + public currentNs?: string; constructor( public readonly targetUri: vscode.Uri, @@ -198,7 +192,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { */ private _moveCursorToLastLine(): void { const currRow = (this._cursorCol - (this._cursorCol % this._cols)) / this._cols; - const newRow = Math.ceil((this._margin + this._input.split("\r\n").pop().length + 1) / this._cols) - 1; + const newRow = Math.ceil((this._margin + this._input.split("\r\n").pop()!.length + 1) / this._cols) - 1; const rowDelta = newRow - currRow; if (rowDelta) this._hideCursorWrite(`\x1b[${rowDelta}B`); } @@ -224,8 +218,12 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // Set terminal properties this._hideCursorWrite("\x1b]633;P;HasRichCommandDetection=True\x07"); // Print the opening message + const username = api.config.auth.username; + const identity = username.includes("*") + ? `using \x1b[0m\x1b[3m${username.slice(1, -1)}\x1b[0m\r\n` + : `as \x1b[0m\x1b[3m${username}\x1b[0m\r\n`; this._hideCursorWrite( - `\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m as \x1b[0m\x1b[3m${api.config.username}\x1b[0m\r\n` + `\x1b[32mConnected to \x1b[0m\x1b[4m${api.config.host}:${api.config.port}${api.config.pathPrefix}\x1b[0m\x1b[32m ${identity}` ); // Add event handlers to the socket this._socket @@ -266,7 +264,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { this._promptExitCode = ";1"; } } - this._margin = this._cursorCol = message.text.split("\r\n").pop().length; + this._margin = this._cursorCol = message.text.split("\r\n").pop()!.length; this._hideCursorWrite(message.text); break; case "prompt": @@ -297,6 +295,8 @@ class WebSocketTerminal implements vscode.Pseudoterminal { ); break; case "color": { + // Outdated: the input is no longer on screen + if (this._state != "prompt") break; // Replace the input with the syntax colored text, keeping the cursor at the same spot let cursorLine = Math.ceil((this._cursorCol + 1) / this._cols) - 1; if (message.text.includes("\r\n")) { @@ -513,7 +513,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // User can't move cursor return; } - if (this._cursorCol < this._margin + this._input.split("\r\n").pop().length) { + if (this._cursorCol < this._margin + this._input.split("\r\n").pop()!.length) { this._cursorCol++; if (this._cursorCol % this._cols == 0) { // Move the cursor to the beginning of the next line @@ -549,7 +549,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { case keys.ctrlE: { if (this._state == "prompt") { // Move the cursor to the end of the input - const lineLength = this._input.split("\r\n").pop().length; + const lineLength = this._input.split("\r\n").pop()!.length; if (lineLength > this._cursorCol) { this._moveCursor(lineLength - this._cursorCol); } @@ -654,7 +654,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal { // Add a blank "line" to move the cursor to the next viewport row return [""]; } - const chunks = []; + const chunks: string[] = []; for (let i = 0; i < line.length; i += this._cols) { chunks.push(line.slice(i, i + this._cols)); } @@ -666,7 +666,12 @@ class WebSocketTerminal implements vscode.Pseudoterminal { } // Save the cursor position, write the text, restore the cursor position, then move the cursor manually this._hideCursorWrite(`\x1b7${eraseAfterCursor}${char}\x1b8${rowStr}${colStr}`); + if (this._input != "" && this._state == "prompt") { + this._socket.send(JSON.stringify({ type: "color", input: this._input })); + } if (submit) { + // Let the coloring arrive before submitting moves the input off its line + await new Promise((resolve) => setTimeout(resolve, 100)); if (this._state == "prompt") { // Reset historyIdx this._historyIdx = -1; @@ -699,9 +704,6 @@ class WebSocketTerminal implements vscode.Pseudoterminal { this._input = ""; this._state = "eval"; this._margin = this._cursorCol = 0; - } else if (this._input != "" && this._state == "prompt") { - // Syntax color input - this._socket.send(JSON.stringify({ type: "color", input: this._input })); } } } @@ -755,7 +757,7 @@ function terminalConfigForUri( return; } // Make sure the server has the terminal endpoint - if (api.config.apiVersion < 7) { + if (api.config.apiVersion! < 7) { reportError("Lite Terminal requires InterSystems IRIS version 2023.2 or above.", throwErrors); return; } @@ -778,7 +780,7 @@ function terminalConfigForUri( }; } -export async function launchWebSocketTerminal(targetUri?: vscode.Uri, nsOverride?: string): Promise { +export async function launchWebSocketTerminal(targetUri?: vscode.Uri | null, nsOverride?: string): Promise { // Determine the server to connect to if (targetUri) { // Uri passed as command argument might be for a server we haven't yet resolved @@ -811,7 +813,7 @@ export async function launchWebSocketTerminal(targetUri?: vscode.Uri, nsOverride export class WebSocketTerminalProfileProvider implements vscode.TerminalProfileProvider { async provideTerminalProfile(): Promise { // Determine the server connection to use - const uri: vscode.Uri = await getWsServerConnection("2023.2.0"); + const uri: vscode.Uri | null | undefined = await getWsServerConnection("2023.2.0"); if (uri) { const api = new AtelierAPI(uri); @@ -819,7 +821,7 @@ export class WebSocketTerminalProfileProvider implements vscode.TerminalProfileP await api.serverInfo(); // Get the terminal configuration. Will throw if there's an error. const terminalOpts = terminalConfigForUri(api, uri, true); - return new vscode.TerminalProfile(terminalOpts); + return new vscode.TerminalProfile(terminalOpts!); } else if (uri === undefined) { throw new Error(NO_ELIGIBLE_CONNECTIONS); } else { diff --git a/src/commands/xmlToUdl.ts b/src/commands/xmlToUdl.ts index 66dfbec0..18da70d4 100644 --- a/src/commands/xmlToUdl.ts +++ b/src/commands/xmlToUdl.ts @@ -92,7 +92,7 @@ export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise } try { // Determine the workspace folder - let wsFolder: vscode.WorkspaceFolder; + let wsFolder: vscode.WorkspaceFolder | null | undefined; if (xmlUri) { wsFolder = vscode.workspace.getWorkspaceFolder(xmlUri); } else { @@ -128,7 +128,7 @@ export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise return; } xmlUri = uris[0]; - if (xmlUri.path.split(".").pop().toLowerCase() != "xml") { + if (xmlUri.path.split(".").pop()!.toLowerCase() != "xml") { vscode.window.showErrorMessage("The selected file was not XML.", "Dismiss"); return; } diff --git a/src/debug/dbgp.ts b/src/debug/dbgp.ts index 3a95440d..515bb27a 100644 --- a/src/debug/dbgp.ts +++ b/src/debug/dbgp.ts @@ -68,7 +68,7 @@ export class DbgpConnection extends EventEmitter { private _handleDataChunk() { if (!this._messages.length) return; // Shouldn't ever happen - const data: Buffer = this._messages.shift(); + const data: Buffer = this._messages.shift()!; if (this._parsingState === ParsingState.DataLength) { // does data contain a NULL byte? const separatorIndex = data.indexOf("|"); diff --git a/src/debug/debugAdapterFactory.ts b/src/debug/debugAdapterFactory.ts index 738a8870..0c8a29fe 100644 --- a/src/debug/debugAdapterFactory.ts +++ b/src/debug/debugAdapterFactory.ts @@ -16,12 +16,12 @@ export class ObjectScriptDebugAdapterDescriptorFactory // pickProcess may have added a suffix to inform us which folder's connection it used const workspaceFolderIndex = (session.configuration.processId as string)?.split("@")[1]; const workspaceFolderUri = workspaceFolderIndex - ? vscode.workspace.workspaceFolders[parseInt(workspaceFolderIndex)]?.uri + ? vscode.workspace.workspaceFolders![parseInt(workspaceFolderIndex)]?.uri : undefined; debugSession.setupAPI(workspaceFolderUri); const serverId = debugSession.serverId; - let server = this.serverMap.get(serverId); + let server = this.serverMap.get(serverId!); if (!server) { // start listening on a random port server = net @@ -30,12 +30,12 @@ export class ObjectScriptDebugAdapterDescriptorFactory debugSession.start(socket as NodeJS.ReadableStream, socket); }) .listen(0); - this.serverMap.set(serverId, server); + this.serverMap.set(serverId!, server); } // make VS Code connect to this debug server const address = server.address(); - const port = typeof address !== "string" ? address.port : 9000; + const port = typeof address !== "string" ? address!.port : 9000; return new vscode.DebugAdapterServer(port); } diff --git a/src/debug/debugSession.ts b/src/debug/debugSession.ts index 8e592504..91b07449 100644 --- a/src/debug/debugSession.ts +++ b/src/debug/debugSession.ts @@ -40,9 +40,9 @@ interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { } /** converts a uri from VS Code to a server-side XDebug file URI with respect to source root settings */ -function convertClientPathToDebugger(uri: vscode.Uri, namespace: string): string { +function convertClientPathToDebugger(uri: vscode.Uri, namespace: string): string | undefined { const { scheme, path } = uri; - let fileName: string; + let fileName: string | undefined; if (scheme && schemas.includes(scheme)) { const { ns } = isfsConfig(uri); if (ns) namespace = ns; @@ -69,7 +69,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { private _statuses = new Map(); - private _connection: xdebug.Connection; + private _connection: xdebug.Connection | null | undefined; private _namespace: string; @@ -91,7 +91,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { private _evalResultProperties = new Map(); - private _workspace: string; + private _workspace: string | undefined; /** If this is a CSPDEBUG session */ private _isCsp = false; @@ -164,8 +164,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { /** Check if the target is stopped */ private async _isStopped(): Promise { - return this._connection - .sendStepIntoCommand() + return this._connection!.sendStepIntoCommand() .then((resp: xdebug.StatusResponse) => { if (resp.status == "stopped") { // Target unattached, terminate session @@ -199,16 +198,16 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { }; try { - if (!this._api.active) { + if (!this._api!.active) { throw new Error("Connection not active"); } - this._namespace = this._api.ns; - this._url = this._api.xdebugUrl(); + this._namespace = this._api!.ns; + this._url = this._api!.xdebugUrl(); const socket = new WebSocket(this._url, { rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"), headers: { - cookie: this._api.cookies, + cookie: this._api!.cookies, }, }); @@ -216,8 +215,8 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { if (!this._connection) { return; } - this.sendEvent(new ThreadEvent("exited", this._connection.id)); - this._connection.close(); + this.sendEvent(new ThreadEvent("exited", this._connection!.id)); + this._connection!.close(); this._connection = null; }; this._connection = new xdebug.Connection(socket) @@ -230,15 +229,15 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { this.sendEvent(new OutputEvent(data, "stdout")); }); - await this._connection.waitForInitPacket(); + await this._connection!.waitForInitPacket(); - await this._connection.sendFeatureSetCommand("max_data", 8192); - await this._connection.sendFeatureSetCommand("max_children", 32); - await this._connection.sendFeatureSetCommand("max_depth", 2); - await this._connection.sendFeatureSetCommand("notify_ok", 1); - await this._connection.sendFeatureSetCommand( + await this._connection!.sendFeatureSetCommand("max_data", 8192); + await this._connection!.sendFeatureSetCommand("max_children", 32); + await this._connection!.sendFeatureSetCommand("max_depth", 2); + await this._connection!.sendFeatureSetCommand("notify_ok", 1); + await this._connection!.sendFeatureSetCommand( "step_granularity", - vscode.workspace.getConfiguration("objectscript.debug").get("stepGranularity") + vscode.workspace.getConfiguration("objectscript.debug").get("stepGranularity")! ); this.sendResponse(response); @@ -262,7 +261,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { this._debugTargetSet = false; this._isLaunch = true; const debugTarget = `${this._namespace}:${args.program}`; - await this._connection.sendFeatureSetCommand("debug_target", debugTarget, true); + await this._connection!.sendFeatureSetCommand("debug_target", debugTarget, true); sendDebuggerTelemetryEvent("launch"); } catch (error) { this.sendErrorResponse(response, error); @@ -276,18 +275,18 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { try { this._debugTargetSet = this._isLaunch = false; const debugTarget = - args.cspDebugId != undefined ? `CSPDEBUG:${args.cspDebugId}` : `PID:${args.processId.split("@")[0]}`; - await this._connection.sendFeatureSetCommand("debug_target", debugTarget); + args.cspDebugId != undefined ? `CSPDEBUG:${args.cspDebugId}` : `PID:${args.processId!.split("@")[0]}`; + await this._connection!.sendFeatureSetCommand("debug_target", debugTarget); if (args.cspDebugId != undefined) { if (args.isUnitTest) { // Set a watchpoint so the target breaks after the unit tests have finished - await this._connection.sendBreakpointSetCommand( + await this._connection!.sendBreakpointSetCommand( new xdebug.Watchpoint("QQQZZZDebugWatchpointTriggerVar", this._unitTestWatchpointCondition) ); this._isUnitTest = true; } else { // Set a watchpoint so the target breaks after the REST response is sent - await this._connection.sendBreakpointSetCommand(new xdebug.Watchpoint("ok", this._cspWatchpointCondition)); + await this._connection!.sendBreakpointSetCommand(new xdebug.Watchpoint("ok", this._cspWatchpointCondition)); this._isCsp = true; } this.sendResponse(response); @@ -314,7 +313,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.PauseArguments ): Promise { try { - const xdebugResponse = await this._connection.sendBreakCommand(); + const xdebugResponse = await this._connection!.sendBreakCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -330,15 +329,15 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { if (!this._isLaunch && !this._isCsp) { // The debug agent ignores the first run command // for non-CSP attaches, so send one right away - await this._connection.sendRunCommand(); + await this._connection!.sendRunCommand(); // Tell VS Code that we're stopped this.sendResponse(response); - const event: DebugProtocol.StoppedEvent = new StoppedEvent("entry", this._connection.id); + const event: DebugProtocol.StoppedEvent = new StoppedEvent("entry", this._connection!.id); event.body.allThreadsStopped = false; this.sendEvent(event); } else { // Tell the debugger to run the target process - const xdebugResponse = await this._connection.sendRunCommand(); + const xdebugResponse = await this._connection!.sendRunCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } @@ -357,7 +356,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { // If attach, it will detach from the target // If launch, it will terminate the target try { - const xdebugResponse = await this._connection.sendDetachCommand(); + const xdebugResponse = await this._connection!.sendDetachCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -377,13 +376,13 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { // args.source.path is a file path if the file is local and is a stringified Uri if the file is virtual const uri = - (this._workspaceFolderUri ?? vscode.workspace.workspaceFolders[0]?.uri)?.scheme == "file" - ? vscode.Uri.file(args.source.path) - : vscode.Uri.parse(args.source.path); + (this._workspaceFolderUri ?? vscode.workspace.workspaceFolders![0]?.uri)?.scheme == "file" + ? vscode.Uri.file(args.source.path!) + : vscode.Uri.parse(args.source.path!); const wsFolder = vscode.workspace.getWorkspaceFolder(uri); if (!wsFolder || (this._workspaceFolderUri && wsFolder.uri.toString() != this._workspaceFolderUri.toString())) { response.body = { - breakpoints: args.breakpoints.map(() => { + breakpoints: args.breakpoints!.map(() => { return { verified: false, message: "This file is not from the same workspace folder as the debug target", @@ -397,7 +396,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { const xdebugUri = convertClientPathToDebugger(uri, this._namespace); if (!xdebugUri) { response.body = { - breakpoints: args.breakpoints.map(() => { + breakpoints: args.breakpoints!.map(() => { return { verified: false, message: "Failed to determine the class or routine name of this file", @@ -408,11 +407,11 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { this.sendResponse(response); return; } - const [, fileName] = xdebugUri.match(/\|([^|]+)$/); - const fileExt = fileName.split(".").pop().toLowerCase(); + const [, fileName] = xdebugUri.match(/\|([^|]+)$/)!; + const fileExt = fileName.split(".").pop()!.toLowerCase(); const languageServer: boolean = vscode.extensions.getExtension(lsExtensionId)?.isActive ?? false; - const currentList = await this._connection.sendBreakpointListCommand(); + const currentList = await this._connection!.sendBreakpointListCommand(); currentList.breakpoints .filter((breakpoint) => { if (breakpoint instanceof xdebug.LineBreakpoint) { @@ -421,7 +420,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { return false; }) .map((breakpoint) => { - this._connection.sendBreakpointRemoveCommand(breakpoint); + this._connection!.sendBreakpointRemoveCommand(breakpoint); }); let xdebugBreakpoints: (xdebug.ConditionalBreakpoint | xdebug.ClassLineBreakpoint | xdebug.LineBreakpoint)[] = []; @@ -433,11 +432,11 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { )[0].children; } xdebugBreakpoints = await Promise.all( - args.breakpoints.map(async (breakpoint) => { + args.breakpoints!.map(async (breakpoint) => { const line = breakpoint.line; if (fileExt == "cls") { // Find the class member that this breakpoint is in - let currentSymbol: vscode.DocumentSymbol; + let currentSymbol: vscode.DocumentSymbol | undefined; for (const symbol of symbols) { if (symbol.range.contains(new vscode.Position(line, 0))) { currentSymbol = symbol; @@ -541,7 +540,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { reason: "failed", }; } else { - await this._connection.sendBreakpointSetCommand(breakpoint); + await this._connection!.sendBreakpointSetCommand(breakpoint); vscodeBreakpoints[index] = { verified: true, line: breakpoint.line }; } } catch (error) { @@ -573,7 +572,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { ): void { if ( args.variablesReference !== undefined && - [0, 1].includes(this._contexts.get(args.variablesReference).id) && + [0, 1].includes(this._contexts.get(args.variablesReference)!.id) && !args.name.includes("(") ) { // This is an unsubscripted private or public local variable @@ -600,7 +599,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { try { await this._waitForDebugTarget(); - const currentList = await this._connection.sendBreakpointListCommand(); + const currentList = await this._connection!.sendBreakpointListCommand(); currentList.breakpoints .filter((breakpoint) => { if (breakpoint instanceof xdebug.Watchpoint) { @@ -609,7 +608,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { return false; }) .map((breakpoint) => { - this._connection.sendBreakpointRemoveCommand(breakpoint); + this._connection!.sendBreakpointRemoveCommand(breakpoint); }); let xdebugWatchpoints: xdebug.Watchpoint[] = []; @@ -623,7 +622,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { await Promise.all( xdebugWatchpoints.map(async (breakpoint, index) => { try { - await this._connection.sendBreakpointSetCommand(breakpoint); + await this._connection!.sendBreakpointSetCommand(breakpoint); vscodeWatchpoints[index] = { verified: true, instructionReference: breakpoint.variable }; } catch (error) { vscodeWatchpoints[index] = { @@ -651,7 +650,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { // runtime supports now threads so just return a default thread. response.body = { - threads: [new Thread(this._connection.id, `Thread ${this._connection.id}]`)], + threads: [new Thread(this._connection!.id, `Thread ${this._connection!.id}]`)], }; this.sendResponse(response); } @@ -661,16 +660,16 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.StackTraceArguments ): Promise { try { - const stack = await this._connection.sendStackGetCommand(); + const stack = await this._connection!.sendStackGetCommand(); /** Is set to true if we're at the CSP or unit test ending watchpoint. * We need to do this so VS Code doesn't try to open the source of * a stack frame before the debug session terminates. */ let noStack = false; const stackFrames = await Promise.all( - stack.stack.map(async (stackFrame: xdebug.StackFrame, index): Promise => { + stack.stack.map(async (stackFrame: xdebug.StackFrame, index): Promise => { if (noStack) return; // Stack frames won't be sent - const [, namespace, docName] = decodeURI(stackFrame.fileUri).match(/^dbgp:\/\/\|([^|]+)\|(.*)$/); + const [, namespace, docName] = decodeURI(stackFrame.fileUri).match(/^dbgp:\/\/\|([^|]+)\|(.*)$/)!; const fileUri = DocumentContentProvider.getUri( docName, this._workspace, @@ -678,7 +677,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { undefined, this._workspaceFolderUri ); - const source = new Source(docName, fileUri.toString()); + const source = new Source(docName, fileUri!.toString()); let line = stackFrame.line + 1; const place = `${stackFrame.method}+${stackFrame.methodOffset}`; const stackFrameId = this._stackFrameIdCounter++; @@ -687,19 +686,19 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { const unitTest = this._isUnitTest && source.name.startsWith("%Api.Atelier.v"); if (csp || unitTest) { // Check if we're at our special watchpoint - const { result } = await this._connection.sendEvalCommand( + const { result } = await this._connection!.sendEvalCommand( csp ? this._cspWatchpointCondition : this._unitTestWatchpointCondition ); if (result.type == "int" && result.value == "1") { // Stop the debugging session - const xdebugResponse = await this._connection.sendDetachCommand(); + const xdebugResponse = await this._connection!.sendDetachCommand(); this._checkStatus(xdebugResponse); noStack = true; return; } } } - const fileText = await this._getFileText(fileUri); + const fileText = await this._getFileText(fileUri!); const hasCmdLoc = typeof stackFrame.cmdBeginLine == "number"; if (!fileText.length) { // Can't get the source for the document @@ -738,15 +737,15 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { return { id: stackFrameId, name: place, - source: noSource ? null : source, + source: (noSource ? null : source)!, line, - column: hasCmdLoc ? stackFrame.cmdBeginPos + 1 : 0, - endLine: hasCmdLoc ? stackFrame.cmdEndLine + lineDiff : undefined, + column: hasCmdLoc ? stackFrame.cmdBeginPos! + 1 : 0, + endLine: hasCmdLoc ? stackFrame.cmdEndLine! + lineDiff : undefined, endColumn: hasCmdLoc ? (stackFrame.cmdEndPos == 0 ? // A command that ends at position zero means "rest of this line" - fileText.split(/\r?\n/)[stackFrame.cmdEndLine + lineDiff - 1].length - : stackFrame.cmdEndPos) + 1 + fileText.split(/\r?\n/)[stackFrame.cmdEndLine! + lineDiff - 1]!.length + : stackFrame.cmdEndPos)! + 1 : undefined, }; }) @@ -755,7 +754,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { this._break = false; if (!noStack) { response.body = { - stackFrames, + stackFrames: stackFrames as StackFrame[], }; } this.sendResponse(response); @@ -805,10 +804,10 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { if (this._contexts.has(variablesReference)) { // VS Code is requesting the variables for a SCOPE, so we have to do a context_get const context = this._contexts.get(variablesReference); - properties = await context.getProperties(); + properties = await context!.getProperties(); } else if (this._properties.has(variablesReference)) { // VS Code is requesting the subelements for a variable, so we have to do a property_get - const property = this._properties.get(variablesReference); + const property = this._properties.get(variablesReference)!; if (property.hasChildren) { if (property.children.length === property.numberOfChildren) { properties = property.children; @@ -820,7 +819,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { } } else if (this._evalResultProperties.has(variablesReference)) { // the children of properties returned from an eval command are always inlined, so we simply resolve them - const property = this._evalResultProperties.get(variablesReference); + const property = this._evalResultProperties.get(variablesReference)!; properties = property.hasChildren ? property.children : []; } else { throw new Error("Unknown variable reference"); @@ -900,7 +899,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.ContinueArguments ): Promise { try { - const xdebugResponse = await this._connection.sendRunCommand(); + const xdebugResponse = await this._connection!.sendRunCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -910,7 +909,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { protected async nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): Promise { try { - const xdebugResponse = await this._connection.sendStepOverCommand(); + const xdebugResponse = await this._connection!.sendStepOverCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -923,7 +922,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.StepInArguments ): Promise { try { - const xdebugResponse = await this._connection.sendStepIntoCommand(); + const xdebugResponse = await this._connection!.sendStepIntoCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -936,7 +935,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.StepOutArguments ): Promise { try { - const xdebugResponse = await this._connection.sendStepOutCommand(); + const xdebugResponse = await this._connection!.sendStepOutCommand(); this.sendResponse(response); this._checkStatus(xdebugResponse); } catch (error) { @@ -949,7 +948,7 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { args: DebugProtocol.EvaluateArguments ): Promise { try { - const { result } = await this._connection.sendEvalCommand(args.expression); + const { result } = await this._connection!.sendEvalCommand(args.expression); if (result) { const displayValue = formatPropertyValue(result); let variablesReference: number; @@ -976,18 +975,18 @@ export class ObjectScriptDebugSession extends LoggingDebugSession { ): Promise { try { const { value, name, variablesReference } = args; - let property = null; + let property: xdebug.Property | null | undefined = null; if (this._contexts.has(variablesReference)) { // VS Code is requesting the variables for a SCOPE, so we have to do a context_get const context = this._contexts.get(variablesReference); - const properties = await context.getProperties(); + const properties = await context!.getProperties(); property = properties.find((el) => el.name === name); } else if (this._properties.has(variablesReference)) { // VS Code is requesting the subelements for a variable, so we have to do a property_get property = this._properties.get(variablesReference); } - property.value = value; - await this._connection.sendPropertySetCommand(property); + property!.value = value; + await this._connection!.sendPropertySetCommand(property!); response.body = { value: args.value, diff --git a/src/debug/utils.ts b/src/debug/utils.ts index 3ba3a3b3..54718ec3 100644 --- a/src/debug/utils.ts +++ b/src/debug/utils.ts @@ -15,7 +15,7 @@ export function formatPropertyValue(property: BaseProperty): string { } } else { // for null, uninitialized, resource, etc. show the type - displayValue = property.value || property.type === "string" ? property.value : property.type; + displayValue = property.value || property.type === "string" ? property.value! : property.type; if (property.type === "string") { displayValue = '"' + displayValue + '"'; } else if (property.type === "bool") { diff --git a/src/debug/xdebugConnection.ts b/src/debug/xdebugConnection.ts index bc513045..d09b121b 100644 --- a/src/debug/xdebugConnection.ts +++ b/src/debug/xdebugConnection.ts @@ -24,11 +24,11 @@ export class InitPacket { */ public constructor(document: XMLDocument, connection: Connection) { const documentElement = document.documentElement; - this.fileUri = documentElement.getAttribute("fileuri"); - this.language = documentElement.getAttribute("language"); - this.protocolVersion = documentElement.getAttribute("protocol_version"); - this.ideKey = documentElement.getAttribute("idekey"); - this.engineVersion = documentElement.getElementsByTagName("engine")[0].getAttribute("version"); + this.fileUri = documentElement.getAttribute("fileuri")!; + this.language = documentElement.getAttribute("language")!; + this.protocolVersion = documentElement.getAttribute("protocol_version")!; + this.ideKey = documentElement.getAttribute("idekey")!; + this.engineVersion = documentElement.getElementsByTagName("engine")[0].getAttribute("version")!; this.connection = connection; } } @@ -62,12 +62,12 @@ export class Response { const documentElement = document.documentElement; if (documentElement.firstChild && documentElement.firstChild.nodeName === "error") { const errorNode = documentElement.firstChild as Element; - const code = parseInt(errorNode.getAttribute("code"), 10); - const message = errorNode.textContent; + const code = parseInt(errorNode.getAttribute("code")!, 10); + const message = errorNode.textContent!; throw new XDebugError(message, code); } - this.transactionId = parseInt(documentElement.getAttribute("transaction_id"), 10); - this.command = documentElement.getAttribute("command"); + this.transactionId = parseInt(documentElement.getAttribute("transaction_id")!, 10); + this.command = documentElement.getAttribute("command")!; this.connection = connection; } } @@ -91,24 +91,24 @@ export class StatusResponse extends Response { public constructor(document: XMLDocument, connection: Connection) { super(document, connection); const documentElement = document.documentElement; - this.status = documentElement.getAttribute("status"); - this.reason = documentElement.getAttribute("reason"); + this.status = documentElement.getAttribute("status")!; + this.reason = documentElement.getAttribute("reason")!; if (documentElement.hasChildNodes()) { const messageNode = documentElement.firstChild as Element; if (messageNode.hasAttribute("exception")) { this.exception = { - message: messageNode.textContent, - name: messageNode.getAttribute("exception"), + message: messageNode.textContent!, + name: messageNode.getAttribute("exception")!, }; if (messageNode.hasAttribute("code")) { - this.exception.code = parseInt(messageNode.getAttribute("code"), 10); + this.exception.code = parseInt(messageNode.getAttribute("code")!, 10); } } if (messageNode.hasAttribute("filename")) { - this.fileUri = messageNode.getAttribute("filename"); + this.fileUri = messageNode.getAttribute("filename")!; } if (messageNode.hasAttribute("lineno")) { - this.line = parseInt(messageNode.getAttribute("lineno"), 10); + this.line = parseInt(messageNode.getAttribute("lineno")!, 10); } } } @@ -121,7 +121,7 @@ export type BreakpointState = "enabled" | "disabled"; export abstract class Breakpoint { /** dynamically detects the type of breakpoint and returns the appropiate object */ public static fromXml(breakpointNode: Element, connection: Connection): Breakpoint { - switch (breakpointNode.getAttribute("type")) { + switch (breakpointNode.getAttribute("type")!) { case "line": return new LineBreakpoint(breakpointNode, connection); case "conditional": @@ -129,7 +129,7 @@ export abstract class Breakpoint { case "watch": return new Watchpoint(breakpointNode, connection); default: - throw new Error(`Invalid type ${breakpointNode.getAttribute("type")}`); + throw new Error(`Invalid type ${breakpointNode.getAttribute("type")!}`); } } /** Unique ID which is used for modifying the breakpoint (only when received through breakpoint_list) */ @@ -151,9 +151,9 @@ export abstract class Breakpoint { // from XML const breakpointNode: Element = rest[0]; this.connection = rest[1]; - this.type = breakpointNode.getAttribute("type") as BreakpointType; - this.id = parseInt(breakpointNode.getAttribute("id"), 10); - this.state = breakpointNode.getAttribute("state") as BreakpointState; + this.type = breakpointNode.getAttribute("type")! as BreakpointType; + this.id = parseInt(breakpointNode.getAttribute("id")!, 10); + this.state = breakpointNode.getAttribute("state")! as BreakpointState; } else { this.type = rest[0]; if (rest[1] !== undefined) { @@ -183,8 +183,8 @@ export class LineBreakpoint extends Breakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.line = parseInt(breakpointNode.getAttribute("lineno"), 10); - this.fileUri = breakpointNode.getAttribute("filename"); + this.line = parseInt(breakpointNode.getAttribute("lineno")!, 10); + this.fileUri = breakpointNode.getAttribute("filename")!; } else { // construct from arguments super("line", rest[2]); @@ -205,8 +205,8 @@ export class ClassLineBreakpoint extends LineBreakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.line = parseInt(breakpointNode.getAttribute("lineno"), 10); - this.fileUri = breakpointNode.getAttribute("filename"); + this.line = parseInt(breakpointNode.getAttribute("lineno")!, 10); + this.fileUri = breakpointNode.getAttribute("filename")!; } else { super(rest[0], rest[1], rest[4]); this.method = rest[2]; @@ -226,8 +226,8 @@ export class RoutineLineBreakpoint extends LineBreakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.line = parseInt(breakpointNode.getAttribute("lineno"), 10); - this.fileUri = breakpointNode.getAttribute("filename"); + this.line = parseInt(breakpointNode.getAttribute("lineno")!, 10); + this.fileUri = breakpointNode.getAttribute("filename")!; } else { super(rest[0], rest[1], rest[4]); this.method = rest[2]; @@ -254,7 +254,7 @@ export class ConditionalBreakpoint extends Breakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.expression = breakpointNode.getAttribute("expression"); // Base64 encoded? + this.expression = breakpointNode.getAttribute("expression")!; // Base64 encoded? } else { // from arguments super("conditional", rest[3]); @@ -283,7 +283,7 @@ export class ClassConditionalBreakpoint extends ConditionalBreakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.expression = breakpointNode.getAttribute("expression"); // Base64 encoded? + this.expression = breakpointNode.getAttribute("expression")!; // Base64 encoded? } else { super(rest[0], rest[1], rest[2], rest[5]); this.method = rest[3]; @@ -310,7 +310,7 @@ export class RoutineConditionalBreakpoint extends ConditionalBreakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - this.expression = breakpointNode.getAttribute("expression"); // Base64 encoded? + this.expression = breakpointNode.getAttribute("expression")!; // Base64 encoded? } else { super(rest[0], rest[1], rest[2], rest[5]); this.method = rest[3]; @@ -335,7 +335,7 @@ export class Watchpoint extends Breakpoint { const breakpointNode: Element = rest[0]; const connection: Connection = rest[1]; super(breakpointNode, connection); - const expr = breakpointNode.getAttribute("expression"); // Base64 encoded? + const expr = breakpointNode.getAttribute("expression")!; // Base64 encoded? if (expr.includes("|")) { this.variable = expr.slice(0, expr.indexOf("|")); this.expression = expr.slice(expr.indexOf("|") + 1); @@ -356,7 +356,7 @@ export class BreakpointSetResponse extends Response { public breakpointId: number; public constructor(document: XMLDocument, connection: Connection) { super(document, connection); - this.breakpointId = parseInt(document.documentElement.getAttribute("id"), 10); + this.breakpointId = parseInt(document.documentElement.getAttribute("id")!, 10); } } @@ -405,14 +405,14 @@ export class StackFrame { * @param {Connection} connection */ public constructor(stackFrameNode: Element, connection: Connection) { - this.method = iconv.encode(stackFrameNode.getAttribute("method"), ENCODING) + ""; - this.fileUri = iconv.encode(stackFrameNode.getAttribute("filename"), ENCODING) + ""; - this.type = stackFrameNode.getAttribute("type"); - this.line = parseInt(stackFrameNode.getAttribute("lineno"), 10); - this.methodOffset = parseInt(stackFrameNode.getAttribute("methodoffset"), 10); - this.level = parseInt(stackFrameNode.getAttribute("level"), 10); - const cmdBegin = stackFrameNode.getAttribute("cmdbegin"); - const cmdEnd = stackFrameNode.getAttribute("cmdend"); + this.method = iconv.encode(stackFrameNode.getAttribute("method")!, ENCODING) + ""; + this.fileUri = iconv.encode(stackFrameNode.getAttribute("filename")!, ENCODING) + ""; + this.type = stackFrameNode.getAttribute("type")!; + this.line = parseInt(stackFrameNode.getAttribute("lineno")!, 10); + this.methodOffset = parseInt(stackFrameNode.getAttribute("methodoffset")!, 10); + this.level = parseInt(stackFrameNode.getAttribute("level")!, 10); + const cmdBegin = stackFrameNode.getAttribute("cmdbegin")!; + const cmdEnd = stackFrameNode.getAttribute("cmdend")!; if (cmdBegin && cmdEnd) { const [cmdBeginLine, cmdBeginPos] = cmdBegin.split(":"); const [cmdEndLine, cmdEndPos] = cmdEnd.split(":"); @@ -449,7 +449,7 @@ export class SourceResponse extends Response { public source: string; public constructor(document: XMLDocument, connection: Connection) { super(document, connection); - this.source = Buffer.from(document.documentElement.textContent, "base64").toString(); + this.source = Buffer.from(document.documentElement.textContent!, "base64").toString(); } } @@ -466,8 +466,8 @@ export class Context { * @param {StackFrame} stackFrame */ public constructor(contextNode: Element, stackFrame: StackFrame) { - this.id = parseInt(contextNode.getAttribute("id"), 10); - this.name = contextNode.getAttribute("name"); + this.id = parseInt(contextNode.getAttribute("id")!, 10); + this.name = contextNode.getAttribute("name")!; this.stackFrame = stackFrame; } /** @@ -511,27 +511,27 @@ export abstract class BaseProperty { /** the number of children this property has, if any. Useful for showing array length. */ public numberOfChildren: number; /** the value of the property for primitive types */ - public value: string; + public value: string | undefined; /** children that were already included in the response */ public children: BaseProperty[]; public constructor(propertyNode: Element) { if (propertyNode.hasAttribute("name")) { - this.name = iconv.encode(propertyNode.getAttribute("name"), ENCODING) + ""; + this.name = iconv.encode(propertyNode.getAttribute("name")!, ENCODING) + ""; } - this.type = propertyNode.getAttribute("type"); + this.type = propertyNode.getAttribute("type")!; if (propertyNode.hasAttribute("classname")) { - this.class = propertyNode.getAttribute("classname"); + this.class = propertyNode.getAttribute("classname")!; } - this.hasChildren = !!parseInt(propertyNode.getAttribute("children"), 10); + this.hasChildren = !!parseInt(propertyNode.getAttribute("children")!, 10); if (this.hasChildren) { - this.numberOfChildren = parseInt(propertyNode.getAttribute("numchildren"), 10); + this.numberOfChildren = parseInt(propertyNode.getAttribute("numchildren")!, 10); } else { - const encoding = propertyNode.getAttribute("encoding"); + const encoding = propertyNode.getAttribute("encoding")!; if (encoding && encoding !== "none") { - this.value = iconv.encode(propertyNode.textContent, encoding) + ""; + this.value = iconv.encode(propertyNode.textContent!, encoding) + ""; } else { - this.value = iconv.encode(propertyNode.textContent, ENCODING) + ""; + this.value = iconv.encode(propertyNode.textContent!, ENCODING) + ""; } } if (this.value === "") { @@ -559,7 +559,7 @@ export class Property extends BaseProperty { */ public constructor(propertyNode: Element, context: Context) { super(propertyNode); - this.fullName = propertyNode.getAttribute("fullname"); + this.fullName = propertyNode.getAttribute("fullname")!; this.context = context; if (this.hasChildren) { this.children = Array.from(propertyNode.childNodes).map( @@ -605,7 +605,7 @@ export class PropertyGetResponse extends Response { */ public constructor(document: XMLDocument, property: Property) { super(document, property.context.stackFrame.connection); - this.children = Array.from(document.documentElement.firstChild.childNodes).map( + this.children = Array.from(document.documentElement.firstChild!.childNodes).map( (propertyNode: Element): Property => new Property(propertyNode, property.context) ); } @@ -662,7 +662,7 @@ export class FeatureSetResponse extends Response { public feature: string; public constructor(document: XMLDocument, connection: Connection) { super(document, connection); - this.feature = document.documentElement.getAttribute("feature"); + this.feature = document.documentElement.getAttribute("feature")!; } } @@ -673,8 +673,8 @@ export class FeatureGetResponse extends Response { public supported: boolean; public constructor(document: XMLDocument, connection: Connection) { super(document, connection); - this.feature = document.documentElement.getAttribute("feature"); - this.supported = document.documentElement.getAttribute("supported") === "1"; + this.feature = document.documentElement.getAttribute("feature")!; + this.supported = document.documentElement.getAttribute("supported")! === "1"; } } @@ -754,20 +754,20 @@ export class Connection extends DbgpConnection { if (response.documentElement.nodeName === "init") { this._initPromiseResolveFn(new InitPacket(response, this)); } else { - const transactionId = parseInt(response.documentElement.getAttribute("transaction_id"), 10); + const transactionId = parseInt(response.documentElement.getAttribute("transaction_id")!, 10); if (this._pendingCommands.has(transactionId)) { - const command = this._pendingCommands.get(transactionId); + const command = this._pendingCommands.get(transactionId)!; this._pendingCommands.delete(transactionId); this._pendingExecuteCommand = false; command.resolveFn(response); } if (this._commandQueue.length > 0) { - const command = this._commandQueue.shift(); + const command = this._commandQueue.shift()!; this._executeCommand(command).catch(command.rejectFn); } if (response.documentElement.nodeName === "stream") { - const type = response.documentElement.getAttribute("type"); - const data = Buffer.from(response.documentElement.textContent, "base64").toString(); + const type = response.documentElement.getAttribute("type")!; + const data = Buffer.from(response.documentElement.textContent!, "base64").toString(); this.sendEvent(type, data); } } @@ -976,7 +976,7 @@ export class Connection extends DbgpConnection { /** Sends a property_get command */ public async sendPropertySetCommand(property: Property): Promise { - const value = Buffer.from(property.value).toString("base64"); + const value = Buffer.from(property.value!).toString("base64"); return new PropertySetResponse( await this._enqueueCommand( "property_set", diff --git a/src/explorer/explorer.ts b/src/explorer/explorer.ts index 22efd95c..5a905f51 100644 --- a/src/explorer/explorer.ts +++ b/src/explorer/explorer.ts @@ -75,7 +75,7 @@ export function registerExplorerOpen(): vscode.Disposable { } // Remove the item from the project let prjFileName = fullName.startsWith("/") ? fullName.slice(1) : fullName; - const ext = prjFileName.split(".").pop().toLowerCase(); + const ext = prjFileName.split(".").pop()!.toLowerCase(); prjFileName = ext == "cls" ? prjFileName.slice(0, -4) : prjFileName; const prjType = prjFileName.includes("/") ? "CSP" @@ -135,13 +135,13 @@ function wasDoubleClick(uri: vscode.Uri): boolean { export class ObjectScriptExplorerProvider implements vscode.TreeDataProvider { public onDidChange?: vscode.Event; - public onDidChangeTreeData: vscode.Event; + public onDidChangeTreeData: vscode.Event; - private _onDidChangeTreeData: vscode.EventEmitter; + private _onDidChangeTreeData: vscode.EventEmitter; private _showExtraForWorkspace: { [key: string]: string[] }[] = []; public constructor() { - this._onDidChangeTreeData = new vscode.EventEmitter(); + this._onDidChangeTreeData = new vscode.EventEmitter(); this.onDidChangeTreeData = this._onDidChangeTreeData.event; } @@ -173,7 +173,7 @@ export class ObjectScriptExplorerProvider implements vscode.TreeDataProvider= 0) { diff --git a/src/explorer/nodes.ts b/src/explorer/nodes.ts index 05a77fcb..9bb64ecb 100644 --- a/src/explorer/nodes.ts +++ b/src/explorer/nodes.ts @@ -15,7 +15,7 @@ interface NodeOptions { /** Get the URI for this leaf node */ function getLeafNodeUri(node: NodeBase): vscode.Uri { - return DocumentContentProvider.getUri(node.fullName, undefined, node?.namespace, false, node.wsFolder.uri, true); + return DocumentContentProvider.getUri(node.fullName, undefined, node?.namespace, false, node.wsFolder.uri, true)!; } const inactiveMsg = "Server connection is inactive"; @@ -61,7 +61,7 @@ export class RootNode extends NodeBase { public readonly contextValue: string; private readonly _category: string; private readonly isCsp: boolean; - private readonly iconPath: vscode.ThemeIcon; + private readonly iconPath: vscode.ThemeIcon | undefined; public constructor( label: string, @@ -97,7 +97,7 @@ export class RootNode extends NodeBase { const path = this instanceof PackageNode || this.isCsp ? this.fullName + "/" : ""; return this.getList(path, this._category, false) .then((data) => - data + data! .filter((el) => { if (this._category === "OTH") { return el.Type === "100"; @@ -148,7 +148,7 @@ export class RootNode extends NodeBase { path: string, category: string, flat: boolean - ): Promise<{ Name: string; Type: string; fullName: string }[]> { + ): Promise<{ Name: string; Type: string; fullName: string }[] | undefined> { const sql = "SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,?,?,?,?,?,?)"; let spec = ""; switch (category) { @@ -192,9 +192,9 @@ export class RootNode extends NodeBase { let nsCspApps: string[] | undefined = cspApps.get(cspAppsKey); if (nsCspApps == undefined) { nsCspApps = await api.getCSPApps().then((data) => data.result.content || []); - cspApps.set(cspAppsKey, nsCspApps); + cspApps.set(cspAppsKey, nsCspApps!); } - return nsCspApps.map((cspApp) => { + return nsCspApps!.map((cspApp) => { return { Name: cspApp.slice(1), fullName: cspApp.slice(1), Type: "10" }; }); } else { @@ -226,7 +226,7 @@ export class RootNode extends NodeBase { public getItemsForExport(): Promise { const path = this instanceof PackageNode || this.isCsp ? this.fullName + "/" : ""; const cat = this.isCsp ? "CSP" : "ALL"; - return this.getList(path, cat, true).then((data) => data.map((el) => el.Name)); + return this.getList(path, cat, true).then((data) => data!.map((el) => el.Name)); } } @@ -324,9 +324,9 @@ export class RoutineNode extends NodeBase { } export class WorkspaceNode extends NodeBase { - public eventEmitter: vscode.EventEmitter; + public eventEmitter: vscode.EventEmitter; public uniqueId: string; - public constructor(label: string, eventEmitter: vscode.EventEmitter, options: NodeOptions) { + public constructor(label: string, eventEmitter: vscode.EventEmitter, options: NodeOptions) { super(label, label, options); this.uniqueId = `serverNode:${this.namespace}:${this.namespace ? ":extra:" : ""}`; this.options.generated = workspaceState.get(`ExplorerGenerated:${this.uniqueId}`); @@ -335,7 +335,7 @@ export class WorkspaceNode extends NodeBase { } public getTreeItem(): vscode.TreeItem { - const flags = []; + const flags: string[] = []; this.options.generated && flags.push(":generated:"); this.options.system && flags.push(":system:"); const api = new AtelierAPI(this.wsFolder.uri); @@ -356,7 +356,7 @@ export class WorkspaceNode extends NodeBase { public async getChildren(_element: NodeBase): Promise { if (!new AtelierAPI(this.wsFolder.uri).active) return [new InactiveNode("", "", { wsFolder: this.wsFolder })]; - const children = []; + const children: RootNode[] = []; let node: RootNode; node = new RootNode( @@ -426,7 +426,7 @@ export class ProjectNode extends NodeBase { } public async getChildren(_element: NodeBase): Promise { - const children = []; + const children: ProjectRootNode[] = []; let node: ProjectRootNode; // Technically a project is a "document", so tell the server that we're opening it @@ -523,9 +523,9 @@ export class ProjectRootNode extends RootNode { "SELECT DISTINCT $PIECE(SUBSTR(sod.Name,?+1),'/') AS Name FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,1,0,1) AS sod " + "JOIN %Studio.Project_ProjectItemsList(?,1) AS pil ON SUBSTR(sod.Name,2) %STARTSWITH ? AND (" + "(pil.Type = 'DIR' AND SUBSTR(sod.Name,2) %STARTSWITH pil.Name||'/') OR (pil.Type = 'CSP' AND SUBSTR(sod.Name,2) = pil.Name))"; - parameters = [l, this.options.project, this.fullName + "/"]; + parameters = [l, this.options.project!, this.fullName + "/"]; } else { - parameters = [l, l, l, this.options.project, this.fullName + "."]; + parameters = [l, l, l, this.options.project!, this.fullName + "."]; if (this.category == "CLS") { query = "SELECT DISTINCT CASE " + @@ -556,7 +556,7 @@ export class ProjectRootNode extends RootNode { "WHEN Type = 'CSP' OR Type = 'DIR' THEN $PIECE(Name,'/') " + "WHEN (Type != 'CSP' AND Type != 'DIR' AND $LENGTH(Name,'.') > 2) OR Type = 'CLS' OR Type = 'PKG' THEN $PIECE(Name,'.') " + "ELSE Name END Name FROM %Studio.Project_ProjectItemsList(?,1) WHERE "; - parameters = [this.options.project]; + parameters = [this.options.project!]; if (this.category == "CLS") { query += "Type = 'PKG' OR Type = 'CLS'"; } else if (this.category == "RTN") { @@ -594,7 +594,7 @@ export class ProjectRootNode extends RootNode { } } else { if (entry.includes(".")) { - if (["mac", "int", "inc"].includes(entry.split(".").pop().toLowerCase())) { + if (["mac", "int", "inc"].includes(entry.split(".").pop()!.toLowerCase())) { return new RoutineNode(entry, fullName, this.options); } else { return new ClassNode(entry, fullName, this.options); @@ -613,11 +613,11 @@ export class ProjectRootNode extends RootNode { } export class ProjectsServerNsNode extends NodeBase { - public eventEmitter: vscode.EventEmitter; + public eventEmitter: vscode.EventEmitter; public constructor( label: string, - eventEmitter: vscode.EventEmitter, + eventEmitter: vscode.EventEmitter, wsFolder: vscode.WorkspaceFolder, namespace?: string ) { diff --git a/src/explorer/projectsExplorer.ts b/src/explorer/projectsExplorer.ts index 1dcfec2e..b6c421e1 100644 --- a/src/explorer/projectsExplorer.ts +++ b/src/explorer/projectsExplorer.ts @@ -4,8 +4,8 @@ import { handleError, notIsfs, notNull } from "../utils"; import { NodeBase, ProjectsServerNsNode } from "./nodes"; export class ProjectsExplorerProvider implements vscode.TreeDataProvider { - public onDidChangeTreeData: vscode.Event; - private _onDidChangeTreeData: vscode.EventEmitter; + public onDidChangeTreeData: vscode.Event; + private _onDidChangeTreeData: vscode.EventEmitter; /** Connection info for all workspace folder roots */ private readonly _roots: string[] = []; @@ -13,7 +13,7 @@ export class ProjectsExplorerProvider implements vscode.TreeDataProvider(); + this._onDidChangeTreeData = new vscode.EventEmitter(); this.onDidChangeTreeData = this._onDidChangeTreeData.event; } @@ -60,7 +60,7 @@ export class ProjectsExplorerProvider implements vscode.TreeDataProvider(); -export const config = (setting?: string, workspaceFolderName?: string): vscode.WorkspaceConfiguration | any => { +type ConnConfig = Pick & { + "docker-compose"?: any; + server?: any; + links?: any; + username?: string; + password?: string; +}; + +export function config(setting: "conn", workspaceFolderName?: string): ConnConfig; +export function config(setting: "intersystems.servers", workspaceFolderName?: string): vscode.WorkspaceConfiguration; +export function config(setting?: string, workspaceFolderName?: string): any; +export function config(setting?: string, workspaceFolderName?: string): any { workspaceFolderName = workspaceFolderName || currentWorkspaceFolder(); if ( vscode.workspace.workspaceFolders?.length && @@ -196,7 +207,7 @@ export const config = (setting?: string, workspaceFolderName?: string): vscode.W } let prefix: string; const workspaceFolder = vscode.workspace.workspaceFolders?.find( - (el) => el.name.toLowerCase() === workspaceFolderName.toLowerCase() + (el) => el.name.toLowerCase() === workspaceFolderName!.toLowerCase() ); if (setting && setting.startsWith("intersystems")) { return vscode.workspace.getConfiguration(setting, workspaceFolder); @@ -204,9 +215,13 @@ export const config = (setting?: string, workspaceFolderName?: string): vscode.W prefix = "objectscript"; } - if (["conn", "export"].includes(setting)) { + if (["conn", "export"].includes(setting!)) { if (workspaceFolderName && workspaceFolderName !== "") { - if (workspaceFolderName.match(/.+:\d+$/)) { + const portSeparator = workspaceFolderName.lastIndexOf(":"); + const portText = workspaceFolderName.slice(portSeparator + 1); + const hasNumericPort = + portSeparator > 0 && portText.length > 0 && [...portText].every((char) => char >= "0" && char <= "9"); + if (hasNumericPort) { const { port, hostname: host, auth, query } = url.parse("http://" + workspaceFolderName, true); const { ns = "USER", https = false } = query; const [username, password] = (auth || "_SYSTEM:SYS").split(":"); @@ -216,10 +231,10 @@ export const config = (setting?: string, workspaceFolderName?: string): vscode.W https, ns, host, - port, + port: +port!, username, password, - }; + } as ConnConfig; } else if (setting == "export") { return {}; } @@ -228,20 +243,25 @@ export const config = (setting?: string, workspaceFolderName?: string): vscode.W } const result = vscode.workspace.getConfiguration(prefix, workspaceFolder?.uri); return setting && setting.length ? result.get(setting) : result; -}; - -let reporter: TelemetryReporter; +} -export let checkingConnection = false; +let reporter: TelemetryReporter | undefined; export let serverManagerApi: serverManager.ServerManagerAPI; +type ConnSpec = serverManager.IServerSpec & { + auth: serverManager.Authorization; +}; + /** Map of the intersystems.server connection specs we have resolved via the API to that extension */ -const resolvedConnSpecs = new Map(); +const resolvedConnSpecs = new Map(); /** * If servermanager extension is available, fetch the connection spec unless already cached. * Prompt for credentials if necessary. + * + * Best-effort only: never contacts the server, so a no-username spec is assumed unauthenticated + * and cached as-is. If wrong, `resolveUsernameAndPassword` corrects it later on an actual 401. * @param serverName authority element of an isfs uri, or `objectscript.conn.server` property, or the name of a root folder with an `objectscript.conn.docker-compose` property object * @param uri if passed, re-check the `objectscript.conn.docker-compose` case in case servermanager API couldn't do that because we're still running our own `activate` method. */ @@ -263,8 +283,16 @@ export async function resolveConnectionSpec( return; } } - - let connSpec = await serverManagerApi.getServerSpec(serverName, scope); + const rawConnSpec = await serverManagerApi.getServerSpec(serverName, scope); + let connSpec: ConnSpec | undefined; + if (rawConnSpec) { + connSpec = { + ...rawConnSpec, + // Some old server managers does not set name as the types suggest. + name: rawConnSpec.name ?? serverName, + auth: rawConnSpec.auth ?? new BasicAuthorization(rawConnSpec.username, rawConnSpec.password), + }; + } if (!connSpec && uri) { // Caller passed uri as a signal to process any docker-compose settings @@ -281,30 +309,34 @@ export async function resolveConnectionSpec( pathPrefix: serverForUri.pathPrefix, }, superServer: { - port: serverForUri.superserverPort, + port: serverForUri.superserverPort!, }, - username: serverForUri.username, - password: serverForUri.password ? serverForUri.password : undefined, description: `Server for workspace folder '${serverName}'`, + auth: serverForUri.auth ?? new BasicAuthorization(), }; } } } if (connSpec) { - await resolvePassword(connSpec); - resolvedConnSpecs.set(serverName, connSpec); + if (connSpec.auth.resolved() || isUnknownUser(connSpec.auth.username)) { + resolvedConnSpecs.set(serverName, connSpec); + } else { + const accessToken = await resolvePassword(connSpec); + if (connSpec.auth.resolve({ accessToken })) { + resolvedConnSpecs.set(serverName, connSpec); + } + } } } -async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promise { - if ( - // Connection isn't unauthenticated - (!isUnauthenticated(serverSpec.username) || ignoreUnauthenticated) && - // A password is missing - typeof serverSpec.password == "undefined" - ) { - const scopes = [serverSpec.name, serverSpec.username || ""]; +function isUnknownUser(username: string | undefined): boolean { + return (username || "unknownuser").toLowerCase() == "unknownuser"; +} + +async function resolvePassword(serverSpec: serverManager.IServerSpec): Promise { + if (!serverSpec.auth?.resolved()) { + const scopes = [serverSpec.name, serverSpec.auth?.username || ""]; // Handle Server Manager extension version < 3.8.0 const account = serverManagerApi.getAccount ? serverManagerApi.getAccount(serverSpec) : undefined; @@ -320,40 +352,43 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi }); } if (session) { - // If original spec lacked username use the one obtained from the user by the authprovider (exact case) - serverSpec.username = serverSpec.username || session.scopes[1]; - serverSpec.password = session.accessToken; + return session.accessToken; } } } /** Resolve credentials for `serverName` and returned the complete connection spec if successful */ -export async function resolveUsernameAndPassword(serverName: string, oldSpec: any): Promise { - const newSpec: { name: string; username?: string; password?: string } = { - name: serverName, - username: oldSpec?.username, - }; - await resolvePassword(newSpec, true); - if (newSpec.password) { +export async function resolveUsernameAndPassword( + serverName: string, + oldSpec: serverManager.IServerSpec +): Promise { + const { auth: _auth, ...newSpec } = oldSpec; + newSpec.name = serverName; + const auth = _auth?.clone(); + + const accessToken = await resolvePassword({ ...newSpec, auth }); + if (auth?.resolve({ accessToken })) { // Update the connection spec resolvedConnSpecs.set(serverName, { ...oldSpec, - username: newSpec.username, - password: newSpec.password, + auth, }); return resolvedConnSpecs.get(serverName); } } /** Accessor for the cache of resolved connection specs */ -export function getResolvedConnectionSpec(key: string, dflt: any): any { +export function getResolvedConnectionSpec( + key: string, + dflt: serverManager.IServerSpec | undefined +): ConnSpec | undefined { let spec = resolvedConnSpecs.get(key); if (spec) { return spec; } // Try a case-insensitive match - key = resolvedConnSpecs.keys().find((oneKey) => oneKey.toLowerCase() === key.toLowerCase()); + key = resolvedConnSpecs.keys().find((oneKey) => oneKey.toLowerCase() === key.toLowerCase())!; if (key) { spec = resolvedConnSpecs.get(key); if (spec) { @@ -362,26 +397,40 @@ export function getResolvedConnectionSpec(key: string, dflt: any): any { } // Return the default if not found - return dflt; + if (dflt) { + return { ...dflt, auth: new BasicAuthorization(dflt.username, dflt.password) }; + } } /** The `api.serverId`s of all servers that are known to be inactive */ export const inactiveServerIds: Set = new Set(); -export async function checkConnection( - clearCookies = false, +/** `configName`s for which an `ensureConnection` call is currently in progress */ +const ensuringConnection: Set = new Set(); + +/** + * Verify `uri`'s connection works, repairing it if not (may show a modal credential prompt). + * Reflects the outcome in the status bar, `objectscript.conn.active`, and `workspaceState`. + * No-ops if a call is already in progress. + * @param clearState Discard cached connection details first, forcing fresh resolution. + * @param triggerRefreshes Refresh the explorer/projects views once settled. + * @param withTimeout Time out the check so it can't hang extension activation. + */ +export async function ensureConnection( + clearState = false, uri?: vscode.Uri, - triggerRefreshes?: boolean + triggerRefreshes?: boolean, + withTimeout = false ): Promise { - // Do nothing if already checking the connection - if (checkingConnection) { + const { apiTarget, configName } = connectionTarget(uri); + // Do nothing if already checking this connection + if (ensuringConnection.has(configName)) { return; } - const { apiTarget, configName } = connectionTarget(uri); const wsKey = configName.toLowerCase(); - if (clearCookies) { - /// clean-up cached values + if (clearState) { + // clean-up cached values await workspaceState.update(wsKey + ":host", undefined); await workspaceState.update(wsKey + ":port", undefined); await workspaceState.update(wsKey + ":superserverPort", undefined); @@ -391,8 +440,8 @@ export async function checkConnection( await workspaceState.update(wsKey + ":docker", undefined); _onDidChangeConnection.fire(); } - let api = new AtelierAPI(apiTarget, false); - const { active, host = "", port = 0, superserverPort = 0, username, ns = "" } = api.config; + let api = new AtelierAPI(apiTarget); + const { active, host = "", port = 0, superserverPort = 0, ns = "", auth } = api.config; vscode.commands.executeCommand("setContext", "vscode-objectscript.connectActive", active); if (!panel.text) { panel.text = `${PANEL_LABEL}`; @@ -450,16 +499,12 @@ export async function checkConnection( } } - if (clearCookies) { - api.clearCookies(); - } - // Before recreating the api object (in case something has updated connection details since we last fetched them?) // if this is an external server, remove it from the inactive list because its presence there would block the reconnect that we want to be attempting if (api.externalServer) { inactiveServerIds.delete(api.serverId); } - api = new AtelierAPI(apiTarget, false); + api = new AtelierAPI(apiTarget); if (!api.config.host || !api.config.port || !api.config.ns) { const message = "'host', 'port' and 'ns' must be specified."; @@ -470,16 +515,19 @@ export async function checkConnection( if (!api.externalServer) await setConnectionState(configName, false); return; } - checkingConnection = true; + ensuringConnection.add(configName); + + const username = auth.username || "UnknownUser"; + const identity = username.startsWith("*") ? `using ${username.slice(1, -1)}` : `as user \`${username}\``; // What we do when api.serverInfo call succeeds const gotServerInfo = async (info: Response>) => { panel.text = api.connInfo; const { serverName, host, port, pathPrefix } = api.config; if (serverName) { - panel.tooltip = new vscode.MarkdownString(`Connected to \`${host}:${port}${pathPrefix}\` as \`${username}\``); + panel.tooltip = new vscode.MarkdownString(`Connected to \`${host}:${port}${pathPrefix}\` ${identity}`); } else { - panel.tooltip = new vscode.MarkdownString(`Connected as \`${username}\``); + panel.tooltip = new vscode.MarkdownString(`Connected ${identity}`); } inactiveServerIds.delete(api.serverId); if (!api.externalServer) { @@ -490,7 +538,9 @@ export async function checkConnection( }; // Do the check - const serverInfoTimeout = 5000; + // Only time out requests when called from activate() + // Timeout is needed in that case to prevent extension activation from hanging + const serverInfoTimeout = withTimeout ? 5000 : undefined; return api .serverInfo(true, serverInfoTimeout) .then(gotServerInfo) @@ -501,8 +551,7 @@ export async function checkConnection( let success = false; message = "Not Authorized."; errorMessage = `Authorization error: Check your credentials in Settings, and that you have sufficient privileges on the /api/atelier web application on ${connInfo}`; - const username = api.config.username; - if (isUnauthenticated(username)) { + if (!api.config.auth.resolved()) { vscode.window.showErrorMessage( `Unauthenticated access rejected by '${api.serverId}'.${ !api.config.serverName ? " Connection has been disabled." : "" @@ -515,11 +564,11 @@ export async function checkConnection( api.config.serverName, vscode.workspace.getConfiguration("intersystems.servers", uri).get(api.config.serverName) ); - const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec); + const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec!); if (newSpec) { // We were able to resolve credentials, so try again - await workspaceState.update(wsKey + ":password", newSpec.password); - api = new AtelierAPI(apiTarget, false); + await workspaceState.update(wsKey + ":password", newSpec.auth?.accessToken); + api = new AtelierAPI(apiTarget); await api .serverInfo(true, serverInfoTimeout) .then(async (info) => { @@ -532,9 +581,6 @@ export async function checkConnection( if (error?.statusCode != 401) errorMessage = undefined; await workspaceState.update(wsKey + ":password", undefined); success = false; - }) - .finally(() => { - checkingConnection = false; }); } } else { @@ -546,7 +592,7 @@ export async function checkConnection( vscode.window .showInputBox({ password: true, - title: `Not Authorized. Enter password to connect as user '${username}' to ${connInfo}`, + title: `Not Authorized. Enter password to connect ${identity} to ${connInfo}`, prompt: !api.externalServer ? "If no password is entered the connection will be disabled." : "", ignoreFocusOut: true, }) @@ -567,9 +613,6 @@ export async function checkConnection( await workspaceState.update(wsKey + ":password", undefined); return false; }) - .finally(() => { - checkingConnection = false; - }) ); } else { inactiveServerIds.add(api.serverId); @@ -595,7 +638,7 @@ export async function checkConnection( if (!api.externalServer) await setConnectionState(configName, false); }) .finally(() => { - checkingConnection = false; + ensuringConnection.delete(configName); if (triggerRefreshes) { setTimeout(() => { explorerProvider.refresh(); @@ -616,15 +659,15 @@ export async function checkConnection( */ function setConnectionState(configName: string, active: boolean) { const connConfig: vscode.WorkspaceConfiguration = config("", configName); - const target: vscode.ConfigurationTarget = connConfig.inspect("conn").workspaceFolderValue + const target: vscode.ConfigurationTarget = connConfig.inspect("conn")!.workspaceFolderValue ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; const targetConfig: any = - connConfig.inspect("conn").workspaceFolderValue || connConfig.inspect("conn").workspaceValue; + connConfig.inspect("conn")!.workspaceFolderValue || connConfig.inspect("conn")!.workspaceValue; return connConfig.update("conn", { ...targetConfig, active }, target); } -function languageServer(install = true): vscode.Extension { +function languageServer(install = true): vscode.Extension | undefined { let extension = vscode.extensions.getExtension(lsExtensionId); async function languageServerInstall() { @@ -700,7 +743,7 @@ function proposedApiPrompt(active: boolean, added?: readonly vscode.WorkspaceFol const systemModes: Map = new Map(); /** Output a message notifying the user of the SystemMode of any servers they are connected to. */ -async function systemModeWarning(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { +async function systemModeWarning(wsFolders: readonly vscode.WorkspaceFolder[] | undefined): Promise { if (!wsFolders || wsFolders.length == 0) return; for (const wsFolder of wsFolders) { const api = new AtelierAPI(wsFolder.uri), @@ -733,7 +776,7 @@ async function systemModeWarning(wsFolders: readonly vscode.WorkspaceFolder[]): ); outputChannel.show(true); } - systemModes.set(mapKey, systemMode); + systemModes.set(mapKey, systemMode!); } } @@ -742,7 +785,7 @@ async function systemModeWarning(wsFolders: readonly vscode.WorkspaceFolder[]): * that are showing the contents of a server-side project. * This must be done because technically a project is a "document". */ -async function fireOpenProjectUserAction(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { +async function fireOpenProjectUserAction(wsFolders: readonly vscode.WorkspaceFolder[] | undefined): Promise { if (!wsFolders || wsFolders.length == 0) return; for (const wsFolder of wsFolders) { if (notIsfs(wsFolder.uri)) return; @@ -778,7 +821,7 @@ function setExplorerContextKeys(): void { } /** Cache the lists of web apps and abstract document types for all server-namespaces in `wsFolders` */ -async function updateWebAndAbstractDocsCaches(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { +async function updateWebAndAbstractDocsCaches(wsFolders: readonly vscode.WorkspaceFolder[] | undefined): Promise { if (!wsFolders?.length) return; const keys: Set = new Set(); const connections: { key: string; api: AtelierAPI }[] = []; @@ -823,14 +866,14 @@ export function sendStudioAddinTelemetryEvent(addInName: string): void { } /** Send a telemetry event with details of each folder in `wsFolders` */ -function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[], added = false): void { +function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[] | undefined, added = false): void { if (!reporter || !wsFolders?.length) return; wsFolders.forEach((wsFolder) => { const api = new AtelierAPI(wsFolder.uri); const { csp, project, ns } = isfsConfig(wsFolder.uri); const serverSide = filesystemSchemas.includes(wsFolder.uri.scheme); const conf = vscode.workspace.getConfiguration("objectscript", wsFolder); - reporter.sendTelemetryEvent("workspaceFolder", { + reporter!.sendTelemetryEvent("workspaceFolder", { scheme: wsFolder.uri.scheme, added: String(added), isWeb: serverSide ? String(csp) : undefined, @@ -842,6 +885,7 @@ function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[] "config.conn.links": String(Object.keys(conf.get("conn.links", {})).length), "config.refreshClassesOnSync": !serverSide ? conf.get("refreshClassesOnSync") : undefined, "config.insertStubContent": !serverSide ? conf.get("insertStubContent") : undefined, + usesServerManager: !serverSide ? String(api.config.serverName != "") : undefined, }); }); } @@ -851,11 +895,6 @@ export function sendUnitTestTelemetryEvent(root: vscode.Uri, debug: boolean): vo reporter?.sendTelemetryEvent("unitTestRun", { scheme: root.scheme, debug: String(debug) }); } -/** Send a telemetry event that a non-class or routine client-side file was saved */ -export function sendClientSideSyncTelemetryEvent(fileExt: string): void { - reporter?.sendTelemetryEvent("clientSideFileSynced", { fileExt }); -} - /** Send a telemetry event that a low-code editor was opened */ export function sendLowCodeTelemetryEvent(editorType: string, scheme: string): void { reporter?.sendTelemetryEvent("lowCodeEditorOpened", { editorType, scheme }); @@ -881,7 +920,7 @@ let macLangConf: vscode.Disposable; let incLangConf: vscode.Disposable; let intLangConf: vscode.Disposable; -export async function activate(context: vscode.ExtensionContext): Promise { +export async function activate(context: vscode.ExtensionContext): Promise { if (!packageJson.version.includes("-") || packageJson.version.includes("-beta.")) { // Don't send telemetry for development builds try { @@ -903,7 +942,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { workspaceState.update("workspaceFolder", undefined); // Get api for servermanager extension - const smExt = vscode.extensions.getExtension(smExtensionId); + const smExt = vscode.extensions.getExtension(smExtensionId)!; if (!smExt.isActive) await smExt.activate(); serverManagerApi = smExt.exports; @@ -930,6 +969,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { const toCheck = new Map(); vscode.workspace.workspaceFolders?.map((workspaceFolder) => { const uri = workspaceFolder.uri; + if (notIsfs(uri) && !vscode.workspace.getConfiguration("objectscript.conn", workspaceFolder).get("active")) return; // Don't check inactive connections const { configName } = connectionTarget(uri); const conn = config("conn", configName); @@ -937,7 +977,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { const serverName = notIsfs(uri) && !conn["docker-compose"] ? conn.server : configName; toCheck.set(serverName, uri); }); - for await (const oneToCheck of toCheck) { + for (const oneToCheck of toCheck) { const serverName = oneToCheck[0]; const uri = oneToCheck[1]; try { @@ -946,7 +986,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { // Necessary because we are in our activate method, so its call to the Server Manager API cannot call back to our API to do that. await resolveConnectionSpec(serverName, uri); } finally { - await checkConnection(true, uri, true); + await ensureConnection(true, uri, true, true); } } catch (_) { // Ignore any failure @@ -960,7 +1000,10 @@ export async function activate(context: vscode.ExtensionContext): Promise { xmlContentProvider = new XmlContentProvider(); const documentSelector = (...list) => - ["file", ...schemas].reduce((acc, scheme) => acc.concat(list.map((language) => ({ scheme, language }))), []); + ["file", ...schemas].reduce( + (acc, scheme) => acc.concat(list.map((language) => ({ scheme, language }))), + [] as { scheme: string; language: any }[] + ); const definitionDocumentLinkProvider = new DefinitionDocumentLinkProvider([ clsLangId, @@ -1172,14 +1215,14 @@ export async function activate(context: vscode.ExtensionContext): Promise { // Migrate removed importOnSave setting to new, more generic syncLocalChanges const conf = vscode.workspace.getConfiguration("objectscript"); const importOnSave = conf.inspect("importOnSave"); - if (typeof importOnSave.globalValue == "boolean") { - if (!importOnSave.globalValue) { + if (typeof importOnSave!.globalValue == "boolean") { + if (!importOnSave!.globalValue) { conf.update("syncLocalChanges", "off", vscode.ConfigurationTarget.Global); } conf.update("importOnSave", undefined, vscode.ConfigurationTarget.Global); } - if (typeof importOnSave.workspaceValue == "boolean") { - if (!importOnSave.workspaceValue) { + if (typeof importOnSave!.workspaceValue == "boolean") { + if (!importOnSave!.workspaceValue) { conf.update("syncLocalChanges", "off", vscode.ConfigurationTarget.Workspace); } conf.update("importOnSave", undefined, vscode.ConfigurationTarget.Workspace); @@ -1207,7 +1250,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { fireOtherStudioAction(OtherStudioAction.AttemptedEdit, event.document.uri); } if (!event.document.isDirty) { - checkChangedOnServer(currentFile(event.document)); + checkChangedOnServer(currentFile(event.document)!); } if ( notIsfs(event.document.uri) && @@ -1220,19 +1263,19 @@ export async function activate(context: vscode.ExtensionContext): Promise { } }), vscode.window.onDidChangeActiveTextEditor(async (editor) => { - if (vscode.workspace.workspaceFolders?.length > 1) { + if ((vscode.workspace.workspaceFolders?.length ?? 0) > 1) { const workspaceFolder = currentWorkspaceFolder(); if (workspaceFolder && workspaceFolder != workspaceState.get("workspaceFolder")) { await workspaceState.update("workspaceFolder", workspaceFolder); // Only need to check when editor is undefined because // we will always check when editor is defined below - if (!editor) await checkConnection(false); + if (!editor) await ensureConnection(false); } } if (editor) { const conf = vscode.workspace.getConfiguration("objectscript"); const uriString = editor.document.uri.toString(); - await checkConnection(false, editor.document.uri); + await ensureConnection(false, editor.document.uri); if ( conf.get("openClassContracted") && editor.document.languageId == clsLangId && @@ -1343,7 +1386,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { vscode.commands.registerCommand("vscode-objectscript.pickProcess", async (config) => { sendCommandTelemetryEvent("pickProcess"); const system = config.system; - let connectionUri = vscode.window.activeTextEditor?.document.uri; + let connectionUri: vscode.Uri | null | undefined = vscode.window.activeTextEditor?.document.uri; if (connectionUri) { // Ignore active editor if its document is outside the workspace (e.g. user settings.json) connectionUri = vscode.workspace.getWorkspaceFolder(connectionUri)?.uri; @@ -1408,8 +1451,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { }) .then((value) => { if (value) { - const workspaceFolderIndex = vscode.workspace.workspaceFolders.findIndex( - (folder) => folder.uri.toString() === connectionUri.toString() + const workspaceFolderIndex = vscode.workspace.workspaceFolders!.findIndex( + (folder) => folder.uri.toString() === connectionUri!.toString() ); return workspaceFolderIndex < 0 ? value.label : `${value.label}@${workspaceFolderIndex}`; } @@ -1508,7 +1551,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { for (const folderUri of result.activatedFolderUris) { const { apiTarget, configName } = connectionTarget(folderUri); - const api = new AtelierAPI(apiTarget, false); + const api = new AtelierAPI(apiTarget); try { await api.serverInfo(true, 5000); successCount++; @@ -1598,7 +1641,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { ), vscode.commands.registerCommand("vscode-objectscript.previewXml", () => { sendCommandTelemetryEvent("previewXml"); - previewXMLAsUDL(vscode.window.activeTextEditor); + previewXMLAsUDL(vscode.window.activeTextEditor!); }), vscode.commands.registerCommand("vscode-objectscript.addServerNamespaceToWorkspace", (resource?: vscode.Uri) => { sendCommandTelemetryEvent("addServerNamespaceToWorkspace"); @@ -1675,7 +1718,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } // Generate the new content const defaultName = inferDocName(uri)?.slice(0, -4); - const fileExt = uri.path.split(".").pop().toLowerCase(); + const fileExt = uri.path.split(".").pop()!.toLowerCase(); const newContent = fileExt == "cls" ? [`Class \${1${defaultName ? `:${defaultName}` : ""}} Extends %RegisteredObject`, "{", "$0", "}", ""] @@ -1763,38 +1806,57 @@ export async function activate(context: vscode.ExtensionContext): Promise { supportsMultipleEditorsPerDocument: false, }), vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => { - if (affectsConfiguration("objectscript.conn") || affectsConfiguration("intersystems.servers")) { - if (affectsConfiguration("intersystems.servers")) { - // Gather the server names previously resolved - const resolvedServers: string[] = []; - resolvedConnSpecs.forEach((v, k) => resolvedServers.push(k)); - // Clear the cache - resolvedConnSpecs.clear(); - // Resolve them again, sequentially in case user needs to be prompted for credentials - for await (const serverName of resolvedServers) { - await resolveConnectionSpec(serverName); - } - } - // Check connections sequentially for each workspace folder - let refreshFilesExplorer = false; - for await (const folder of vscode.workspace.workspaceFolders ?? []) { - if (schemas.includes(folder.uri.scheme)) { - refreshFilesExplorer = true; - } - try { - await checkConnection(true, folder.uri, true); - } catch (_) { - continue; + // Loop through all ws folder connections and see if any changed + // Find all "sessions" that are new or orphaned. + // For new ones, establish a connection? + // For orphans, logout and clear the cookies. + let refreshFilesExplorer = false; + const newConnections = new Set(); + const affectedWsFolders: vscode.WorkspaceFolder[] = []; + for (const wsFolder of vscode.workspace.workspaceFolders ?? []) { + const api = new AtelierAPI(wsFolder.uri); + newConnections.add(api.mapKey()); + const { serverName } = api.config; + if (notIsfs(wsFolder.uri) && affectsConfiguration("objectscript.conn", wsFolder)) { + // Connection info changed + affectedWsFolders.push(wsFolder); + } else if (serverName && affectsConfiguration("intersystems.servers", wsFolder)) { + const oldSpec = getResolvedConnectionSpec(serverName, undefined); + const newSpec = vscode.workspace.getConfiguration("intersystems.servers", wsFolder).get(serverName); + if ( + !oldSpec || + !( + oldSpec.webServer.host == newSpec.webServer.host && + oldSpec.webServer.port == newSpec.webServer.port && + oldSpec.webServer.scheme == newSpec.webServer.scheme && + oldSpec.webServer.pathPrefix == newSpec.webServer.pathPrefix + ) + ) { + // Connection info changed + resolvedConnSpecs.delete(serverName); + affectedWsFolders.push(wsFolder); + if (filesystemSchemas.includes(wsFolder.uri.scheme)) refreshFilesExplorer = true; } } - explorerProvider.refresh(); - projectsExplorerProvider.refresh(); - if (refreshFilesExplorer) { - // This unavoidably switches to the File Explorer view, so only do it if isfs folders were found - vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); + } + // Log out of any sessions that are orphaned + await logoutOfSessions(Array.from(cookiesMap.keys()).filter((k) => !newConnections.has(k))); + // Update the connection info for affected workspace folders + // This should create new CSP sessions if needed + for (const wsFolder of affectedWsFolders) { + try { + await ensureConnection(true, wsFolder.uri, true); + } catch { + // Errors are handled by ensureConnection() } - updateWebAndAbstractDocsCaches(vscode.workspace.workspaceFolders); } + explorerProvider.refresh(); + projectsExplorerProvider.refresh(); + if (refreshFilesExplorer) { + // This unavoidably switches to the File Explorer view, so only do it if isfs folders were found + vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); + } + updateWebAndAbstractDocsCaches(affectedWsFolders); if (affectsConfiguration("objectscript.commentToken")) { // Update the language configuration for "objectscript" and "objectscript-macros" macLangConf?.dispose(); @@ -1826,7 +1888,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { ) { throw undefined; } - const file = currentFile(document); + const file = currentFile(document)!; const nameMatch = file.name.match(/(.*)\.(int|mac)$/i); if (!nameMatch) throw undefined; const [, routine] = nameMatch; @@ -1940,6 +2002,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { const toCheck = new Map(); e.added.map((workspaceFolder) => { const uri = workspaceFolder.uri; + if (notIsfs(uri) && !vscode.workspace.getConfiguration("objectscript.conn", workspaceFolder).get("active")) + return; // Don't check inactive connections const { configName } = connectionTarget(uri); toCheck.set(configName, uri); }); @@ -2033,7 +2097,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { const doc = await pickDocument(api, "Open a document"); if (!doc) return; vscode.window.showTextDocument( - DocumentContentProvider.getUri(doc, undefined, undefined, undefined, wsFolder.uri) + DocumentContentProvider.getUri(doc, undefined, undefined, undefined, wsFolder.uri)! ); }), vscode.window.tabGroups.onDidChangeTabs((e) => { @@ -2139,7 +2203,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { ), /* Anything we use from the VS Code proposed API */ - ...proposed + ...(proposed as vscode.Disposable[]) ); // Send the activation and workspace folders telemetry events @@ -2181,7 +2245,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { return _onDidChangeConnection.event; }, getUriForDocument(document: string): vscode.Uri { - return DocumentContentProvider.getUri(document); + return DocumentContentProvider.getUri(document)!; }, }; @@ -2191,7 +2255,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { // This function is exported as one of our API functions but is also used internally // for example to implement the async variant capable of resolving docker port number. -function serverForUri(uri: vscode.Uri): any { +function serverForUri(uri: vscode.Uri): serverManager.ServerForUri | undefined { const { apiTarget, configName } = connectionTarget(uri); const configNameLower = configName.toLowerCase(); const api = new AtelierAPI(apiTarget); @@ -2199,64 +2263,60 @@ function serverForUri(uri: vscode.Uri): any { // This function intentionally no longer exposes the password for a named server UNLESS it is already exposed as plaintext in settings. // API client extensions should use Server Manager 3's authentication provider to request a missing password themselves, // which will require explicit user consent to divulge the password to the requesting extension. - const { - serverName, - active, - host = "", - https, - port, - superserverPort, - pathPrefix, - username, - password, - ns = "", - apiVersion, - serverVersion, - } = api.config; - return { - serverName, - active, - scheme: https ? "https" : "http", - host, - port, - superserverPort, - pathPrefix, - username, - password: - serverName === "" - ? password - : vscode.workspace - .getConfiguration( - `intersystems.servers.${serverName.toLowerCase()}`, - // objectscript(xml):// URIs are not in any workspace folder, - // so make sure we resolve the server definition with the proper - // granularity. This is needed to prevent other extensions like - // Language Server prompting for a passwoord when it's not needed. - [OBJECTSCRIPT_FILE_SCHEMA, OBJECTSCRIPTXML_FILE_SCHEMA].includes(uri.scheme) - ? vscode.workspace.workspaceFolders?.find((f) => f.name.toLowerCase() == configNameLower)?.uri - : uri - ) - .get("password"), - namespace: ns, - apiVersion: active ? apiVersion : undefined, - serverVersion: active ? serverVersion : undefined, - }; + const { serverName, active, host, https, port, superserverPort, pathPrefix, auth, ns, apiVersion, serverVersion } = + api.config; + auth.clear() as void; + const password = + config("conn", configName).password || + vscode.workspace + .getConfiguration( + `intersystems.servers.${serverName.toLowerCase()}`, + // objectscript(xml):// URIs are not in any workspace folder, + // so make sure we resolve the server definition with the proper + // granularity. This is needed to prevent other extensions like + // Language Server prompting for a passwoord when it's not needed. + [OBJECTSCRIPT_FILE_SCHEMA, OBJECTSCRIPTXML_FILE_SCHEMA].includes(uri.scheme) + ? vscode.workspace.workspaceFolders?.find((f) => f.name.toLowerCase() == configNameLower)?.uri + : uri + ) + .get("password"); + password && auth.resolve({ accessToken: password }); + if (ns) { + return { + serverName, + active, + ...(https ? ({ https: true, scheme: "https" } as const) : ({ scheme: "http" } as const)), + host, + port, + superserverPort, + pathPrefix: pathPrefix || "", + auth, + username: auth.username, + password: auth.password, + namespace: ns, + apiVersion: (active ? apiVersion : undefined)!, + serverVersion: (active ? serverVersion : undefined)!, + }; + } } // An async variant capable of resolving docker port number. // It is exported as one of our API functions but is also used internally. -async function asyncServerForUri(uri: vscode.Uri): Promise { +async function asyncServerForUri(uri: vscode.Uri): Promise { const server = serverForUri(uri); + if (!server) { + return; + } if (!server.port) { let { apiTarget } = connectionTarget(uri); if (apiTarget instanceof vscode.Uri) { - apiTarget = vscode.workspace.getWorkspaceFolder(apiTarget)?.name; + apiTarget = vscode.workspace.getWorkspaceFolder(apiTarget)?.name as string; } const { port: dockerPort, superserverPort: dockerSuperserverPort, docker: withDocker, - } = await portFromDockerCompose(apiTarget); + } = await portFromDockerCompose(apiTarget as string | undefined); if (withDocker && dockerPort && dockerSuperserverPort) { server.port = dockerPort; server.superserverPort = dockerSuperserverPort; @@ -2281,25 +2341,61 @@ export async function deactivate(): Promise { intLangConf?.dispose(); disposeDocumentIndex(); // Log out of all CSP sessions - const loggedOut: Set = new Set(); - const promises: Promise[] = []; - for (const f of vscode.workspace.workspaceFolders ?? []) { - const api = new AtelierAPI(f.uri); - if (!api.active || !api.cookies.length) continue; - const sessionCookie = api.cookies.find((c) => c.startsWith("CSPSESSIONID-")); - if (!sessionCookie || loggedOut.has(sessionCookie)) continue; - loggedOut.add(sessionCookie); - promises.push( - api.request( - 0, - "HEAD", - undefined, - undefined, - // Prefer IRISLogout for servers that support it - semver.lt(api.config.serverVersion, "2018.2.0") ? { CacheLogout: "end" } : { IRISLogout: "end" } - ) - ); + await logoutOfSessions(); +} + +// A copy of the BasicAuthorization class from ServerManager +// We use it to patch older version of getServerSpec. +export default class BasicAuthorization implements Authorization { + #username?: string; + #password?: string; + constructor(username?: string, password?: string) { + this.#username = username; + this.#password = password; + } + + public get username(): string { + return this.#username || ""; + } + + public get password(): string | undefined { + return this.#password; + } + + public get accessToken(): string | undefined { + return this.#password; + } + + public get httpAuthorizationHeader(): string { + return `Basic ${Buffer.from(`${this.#username}:${this.#password}`).toString("base64")}`; + } + + public resolved(): this is ResolvedAuthorization { + return this.username !== "" && this.#password !== undefined; + } + + public resolve(param: { accessToken: string; username?: string }): this is ResolvedAuthorization { + this.#username = param.username ?? this.#username; + this.#password = param.accessToken ?? this.#password; + return this.resolved(); + } + + public clear(): asserts this is Authorization { + this.#password = undefined; + } + + public get credentials(): { auth: { username: string; password: string }; headers?: Record } { + return { + auth: { + username: this.username, + password: this.password!, + }, + headers: {}, + }; + } + + public clone(): BasicAuthorization { + return new BasicAuthorization(this.#username, this.#password); } - await Promise.allSettled(promises); } export { outputChannel }; diff --git a/src/providers/DocumentContentProvider.ts b/src/providers/DocumentContentProvider.ts index c048a143..143818fa 100644 --- a/src/providers/DocumentContentProvider.ts +++ b/src/providers/DocumentContentProvider.ts @@ -9,8 +9,8 @@ import { getUrisForDocument } from "../utils/documentIndex"; import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil"; export function compareConns( - conn1: { ns: any; server: any; host: any; port: any; "docker-compose": any }, - conn2: { ns: any; server: any; host: any; port: any; "docker-compose": any } + conn1: { ns?: string; server?: any; host: any; port: any; "docker-compose"?: any }, + conn2: { ns?: string; server?: any; host: any; port: any; "docker-compose"?: any } ): boolean { if (conn1.ns === conn2.ns) { // Same namespace name @@ -41,8 +41,8 @@ export function compareConns( /** Returns `true` if both connections point to the same server, regardless of namespace */ export function compareSameServer( - conn1: { server: any; host: any; port: any; "docker-compose": any }, - conn2: { server: any; host: any; port: any; "docker-compose": any } + conn1: { server?: any; host?: any; port?: any; "docker-compose"?: any }, + conn2: { server?: any; host?: any; port?: any; "docker-compose"?: any } ): boolean { if (conn1.server && conn2.server) { return conn1.server === conn2.server; @@ -62,9 +62,9 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid } /** Returns the `Uri` of `name` in `workspaceFolder` if it exists */ - private static findLocalUri(name: string, workspaceFolder: string): vscode.Uri { + private static findLocalUri(name: string, workspaceFolder: string | undefined): vscode.Uri | undefined { if (!workspaceFolder) return; - const wsFolder = vscode.workspace.workspaceFolders.find((wf) => wf.name == workspaceFolder); + const wsFolder = vscode.workspace.workspaceFolders!.find((wf) => wf.name == workspaceFolder); if (!wsFolder) return; if (!notIsfs(wsFolder.uri)) return; const conf = vscode.workspace.getConfiguration("objectscript.export", wsFolder); @@ -138,7 +138,7 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid vfs?: boolean, wFolderUri?: vscode.Uri, forceServerCopy = false - ): vscode.Uri { + ): vscode.Uri | null { let scheme = vfs ? FILESYSTEM_SCHEMA : OBJECTSCRIPT_FILE_SCHEMA; const isCsp = name.includes("/"); @@ -148,8 +148,8 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid wFolderUri = uriOfWorkspaceFolder(workspaceFolder); } else if (!workspaceFolder) { // Make sure workspaceFolder is set correctly if only wFolderUri was passed - workspaceFolder = vscode.workspace.workspaceFolders.find( - (wf) => wf.uri.toString() == wFolderUri.toString() + workspaceFolder = vscode.workspace.workspaceFolders!.find( + (wf) => wf.uri.toString() == wFolderUri!.toString() )?.name; } let uri: vscode.Uri; @@ -161,7 +161,7 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid namespace = ""; } const params = new URLSearchParams(wFolderUri.query); - const cspParam = params.has(IsfsUriParam.CSP) && ["", "1"].includes(params.get(IsfsUriParam.CSP)); + const cspParam = params.has(IsfsUriParam.CSP) && ["", "1"].includes(params.get(IsfsUriParam.CSP)!); const lastDot = name.lastIndexOf("."); let uriPath = isCsp ? name : name.slice(0, lastDot).replace(/\./g, "/") + "." + name.slice(lastDot + 1); if (!isCsp && /.\.G?[1-9]\.int$/i.test(name)) { @@ -232,7 +232,7 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid const fileName = name .split(".") .slice(0, -1) - .join(fileExt.match(/cls/i) ? "/" : "."); + .join(fileExt!.match(/cls/i) ? "/" : "."); name = fileName + "." + fileExt; uri = vscode.Uri.file(name).with({ scheme: scheme, diff --git a/src/providers/DocumentFormattingEditProvider.ts b/src/providers/DocumentFormattingEditProvider.ts index 1f4b24ef..b100a50c 100644 --- a/src/providers/DocumentFormattingEditProvider.ts +++ b/src/providers/DocumentFormattingEditProvider.ts @@ -19,7 +19,7 @@ export class DocumentFormattingEditProvider implements vscode.DocumentFormatting } private commands(document: vscode.TextDocument, options: vscode.FormattingOptions): vscode.TextEdit[] { - const edits = []; + const edits: vscode.TextEdit[] = []; let indent = 1; const isClass = document.fileName.toLowerCase().endsWith(".cls"); @@ -166,8 +166,8 @@ export class DocumentFormattingEditProvider implements vscode.DocumentFormatting } // keep strings and comments - const keepList = []; - const restorePattern = []; + const keepList: string[] = []; + const restorePattern: string[] = []; const toKeep = (str) => { keepList.push(str); restorePattern.push(String.fromCharCode(keepList.length)); @@ -225,13 +225,13 @@ export class DocumentFormattingEditProvider implements vscode.DocumentFormatting } private functions(document: vscode.TextDocument, options: vscode.FormattingOptions): vscode.TextEdit[] { - const edits = []; + const edits: vscode.TextEdit[] = []; for (let i = 0; i < document.lineCount; i++) { const line = document.lineAt(i); const pattern = /(? { + public async resolveDocumentLink( + link: StudioLink, + token: vscode.CancellationToken + ): Promise { const editor = await vscode.window .showTextDocument(link.uri) .then(undefined, (error) => handleError(error, "Failed to resolve DocumentLink to a specific location.")); @@ -66,7 +69,7 @@ export class DocumentLinkProvider implements vscode.DocumentLinkProvider { // add the offset of the method if it is a class if (link.methodname) { - const symbols = await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", link.uri); + const symbols: any = await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", link.uri); const method = symbols[0].children.find( (info) => (info.detail === "ClassMethod" || info.detail === "Method") && info.name === link.methodname ); diff --git a/src/providers/FileSystemProvider/FileSearchProvider.ts b/src/providers/FileSystemProvider/FileSearchProvider.ts index 5f519c00..1936ebf5 100644 --- a/src/providers/FileSystemProvider/FileSearchProvider.ts +++ b/src/providers/FileSystemProvider/FileSearchProvider.ts @@ -9,7 +9,7 @@ export class FileSearchProvider implements vscode.FileSearchProvider { query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken - ): Promise { + ): Promise { let counter = 0; // Replace all back slashes with forward slashes let pattern = query.pattern.replace(/\\/g, "/"); @@ -28,18 +28,19 @@ export class FileSearchProvider implements vscode.FileSearchProvider { for (const c of pattern) regexStr += `${[".", "/"].includes(c) ? "[./]" : c}.*`; const patternRegex = new RegExp(regexStr, "i"); if (token.isCancellationRequested) return; - return projectContentsFromUri(options.folder, true).then((docs) => - docs - .map((doc: ProjectItem) => - !token.isCancellationRequested && - // The document matches the query - (!pattern.length || patternRegex.test(doc.Name)) && - // We haven't hit the max number of results - (!options.maxResults || ++counter <= options.maxResults) - ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) - : null - ) - .filter(notNull) + return projectContentsFromUri(options.folder, true).then( + (docs) => + docs! + .map((doc: ProjectItem) => + !token.isCancellationRequested && + // The document matches the query + (!pattern.length || patternRegex.test(doc.Name)) && + // We haven't hit the max number of results + (!options.maxResults || ++counter <= options.maxResults) + ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) + : null + ) + .filter(notNull) as vscode.Uri[] ); } // When this is called without a query.pattern every file is supposed to be returned, so do not provide a filter @@ -48,16 +49,17 @@ export class FileSearchProvider implements vscode.FileSearchProvider { ? `Name LIKE '${!csp ? likePattern.replace(/\//g, ".") : likePattern}' ESCAPE '\\'` : ""; if (token.isCancellationRequested) return; - return studioOpenDialogFromURI(options.folder, { flat: true, filter }).then((data) => - data.result.content - .map((doc: { Name: string; Type: number }) => - !token.isCancellationRequested && - // We haven't hit the max number of results - (!options.maxResults || ++counter <= options.maxResults) - ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) - : null - ) - .filter(notNull) + return studioOpenDialogFromURI(options.folder, { flat: true, filter })!.then( + (data) => + data.result.content + .map((doc: { Name: string; Type: number }) => + !token.isCancellationRequested && + // We haven't hit the max number of results + (!options.maxResults || ++counter <= options.maxResults) + ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) + : null + ) + .filter(notNull) as vscode.Uri[] ); } } diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index 709d03f4..22a90d26 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as vscode from "vscode"; -import { isText } from "istextorbinary"; import { AtelierAPI } from "../../api"; import { fireOtherStudioAction, OtherStudioAction } from "../../commands/studio"; import { isfsConfig, projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/FileProviderUtil"; @@ -89,7 +88,7 @@ export function generateFileContent( } } - const fileExt = fileName.split(".").pop().toLowerCase(); + const fileExt = fileName.split(".").pop()!.toLowerCase(); const csp = fileName.startsWith("/"); if (fileExt === "cls" && !csp) { const className = fileName.split(".").slice(0, -1).join("."); @@ -105,12 +104,12 @@ export function generateFileContent( // Replace that with one to match fileName. while (sourceLines.length > 0) { const nextLine = sourceLines.shift(); - const classNameMatch = nextLine.match(classNameRegex); + const classNameMatch = nextLine!.match(classNameRegex); if (classNameMatch) { - content.push(...preamble, nextLine.replace(classNameMatch[1], fileName.slice(0, -4)), ...sourceLines); + content.push(...preamble, nextLine!.replace(classNameMatch[1], fileName.slice(0, -4)), ...sourceLines); break; } - preamble.push(nextLine); + preamble.push(nextLine!); } if (!content.length) { // Transfer sourceLines verbatim in cases where no class header line is found @@ -193,12 +192,12 @@ export function isCSP(uri: vscode.Uri): boolean { path: path.dirname(uri.path), }) .toString(); - if (cspFilesInProjectFolder.has(parent) && cspFilesInProjectFolder.get(parent).includes(path.basename(uri.path))) { + if (cspFilesInProjectFolder.has(parent) && cspFilesInProjectFolder.get(parent)!.includes(path.basename(uri.path))) { return true; } // Read the parent directory and file is not CSP OR haven't read the parent directory yet // Use the file extension to guess if it's a web app file - const additionalExts: string[] = vscode.workspace + const additionalExts: string[] | undefined = vscode.workspace .getConfiguration("objectscript.projects", uri) .get("webAppFileExtensions"); return [ @@ -221,8 +220,8 @@ export function isCSP(uri: vscode.Uri): boolean { "ico", "xml", "txt", - ...additionalExts, - ].includes(uri.path.split(".").pop().toLowerCase()); + ...additionalExts!, + ].includes(uri.path.split(".").pop()!.toLowerCase()); } return csp; } @@ -238,7 +237,7 @@ export function isfsDocumentName(uri: vscode.Uri, csp?: boolean, pkg = false): s if (csp == undefined) csp = isCSP(uri); const doc = csp ? uri.path : uri.path.slice(1).replace(/\//g, "."); // Add the .PKG extension to non-web folders if called from StudioActions - return pkg && !csp && !doc.split("/").pop().includes(".") ? `${doc}.PKG` : doc; + return pkg && !csp && !doc.split("/").pop()!.includes(".") ? `${doc}.PKG` : doc; } /** @@ -252,7 +251,7 @@ export function isfsDocumentName(uri: vscode.Uri, csp?: boolean, pkg = false): s * error, or a more descriptive custom error when `descriptiveError` is `true`, * when `uri`'s path is not in "canonical form". */ -function validateUriIsCanonical(uri: vscode.Uri, descriptiveError = false): void { +function validateUriIsCanonical(uri: vscode.Uri): void { const numDots = uri.path.split(".").length - 1; const lastFour = uri.path.slice(-4); if (!isfsConfig(uri).csp && [".cls", ".mac", ".int", ".inc"].includes(lastFour.toLowerCase())) { @@ -260,10 +259,12 @@ function validateUriIsCanonical(uri: vscode.Uri, descriptiveError = false): void ? "File extension must be in all lowercase" : uri.path.startsWith("/%") && lastFour == ".cls" && numDots == 1 && uri.path.split("/").length == 2 ? "Must use the full name of %Library classes" - : numDots > 1 && !(numDots == 2 && /\.G?\d\.int$/.test(uri.path)) - ? "Packages must use forward slashes as delimiters instead of dots" + : numDots > 1 && !((numDots == 2 && /\.G?\d\.int$/.test(uri.path)) || /\.\d+\.(mac|int|inc)$/.test(uri.path)) + ? // Need to allow URIs for routines with last dotted sections that are entirely digits + // due to a server-side bug. These routines are not properly split into folders. + "Packages must use forward slashes as delimiters instead of dots" : undefined; - if (msg) throw descriptiveError ? new vscode.FileSystemError(msg) : vscode.FileSystemError.FileNotFound(uri); + if (msg) throw new vscode.FileSystemError(msg); } } @@ -340,7 +341,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (entryPromise instanceof File) { // previously resolved as a file result = entryPromise; - } else if (entryPromise instanceof Promise && uri.path.split("/").pop()?.split(".").length > 1) { + } else if (entryPromise instanceof Promise && uri.path.split("/").pop()!.split(".").length > 1) { // apparently a file, so resolve ahead of adding permissions result = await entryPromise; } else { @@ -352,7 +353,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const serverName = isfsDocumentName(uri); if (serverName.slice(-4).toLowerCase() == ".cls") { if (await isClassDeployed(serverName, api)) { - result.permissions |= vscode.FilePermission.Readonly; + result.permissions = result.permissions! | vscode.FilePermission.Readonly; return result; } } @@ -363,7 +364,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const statusObj = await api.actionQuery(query, [serverName]); const docStatus = statusObj.result?.content?.pop(); if (docStatus) { - result.permissions = docStatus.editable ? undefined : result.permissions | vscode.FilePermission.Readonly; + result.permissions = docStatus.editable ? undefined : result.permissions! | vscode.FilePermission.Readonly; } } } @@ -382,7 +383,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (project) { // Get all items in the project return projectContentsFromUri(uri).then((entries) => - entries.map((entry) => { + entries!.map((entry) => { const csp = ["CSP", "DIR"].includes(entry.Type); if (!entry.Name.includes(".")) { if (!parent.entries.has(entry.Name)) { @@ -404,7 +405,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const mapkey = uri.toString(); let mapvalue: string[] = []; if (cspFilesInProjectFolder.has(mapkey)) { - mapvalue = cspFilesInProjectFolder.get(mapkey); + mapvalue = cspFilesInProjectFolder.get(mapkey)!; } mapvalue.push(entry.Name); cspFilesInProjectFolder.set(mapkey, mapvalue); @@ -435,7 +436,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } } const cspSubfolders = Array.from(cspSubfolderMap.entries()); - return studioOpenDialogFromURI(uri) + return studioOpenDialogFromURI(uri)! .then((data) => data.result.content || []) .then((data) => { const results = data @@ -476,10 +477,12 @@ export class FileSystemProvider implements vscode.FileSystemProvider { .catch((error) => { if (error) { if (error.errorText.includes(" #5540:")) { - const message = `User '${api.config.username}' cannot list ${ + const username = api.config.auth.username; + const identity = username.includes("*") ? `Users using ${username.slice(1, -1)}` : `User '${username}'`; + const message = `${identity} cannot list ${ csp ? `web application '${uri.path}'` : "namespace" - } contents. If they do not have READ permission on the default code database of the ${api.config.ns.toUpperCase()} namespace then grant it and retry. If the problem remains then execute the following SQL in that namespace:\n\t GRANT EXECUTE ON %Library.RoutineMgr_StudioOpenDialog TO ${ - api.config.username + } contents. If they do not have READ permission on the default code database of the ${api.config.ns!.toUpperCase()} namespace then grant it and retry. If the problem remains then execute the following SQL in that namespace:\n\t GRANT EXECUTE ON %Library.RoutineMgr_StudioOpenDialog TO ${ + username.includes("*") ? "" : username }`; handleError(message); } @@ -509,7 +512,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { validateUriIsCanonical(uri); // Use _lookup() instead of _lookupAsFile() so we send // our cached mtime with the GET /doc request if we have it - return this._lookup(uri, true).then((file: File) => file.data); + return this._lookup(uri, true).then((file: File) => file.data!); } public writeFile( @@ -525,7 +528,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const originalUri = vscode.Uri.parse(originalUriString); this._needsUpdate.delete(originalUriString); uri = redirectDotvscodeRoot(uri, new vscode.FileSystemError("Server does not have a /_vscode web application")); - validateUriIsCanonical(uri, true); + validateUriIsCanonical(uri); const csp = isCSP(uri); const fileName = isfsDocumentName(uri, csp); if (fileName.startsWith(".")) { @@ -534,14 +537,14 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const api = new AtelierAPI(uri); let created = false; let update = false; - const fileExt = fileName.split(".").pop().toLowerCase(); + const fileExt = fileName.split(".").pop()!.toLowerCase(); // Use _lookup() instead of _lookupAsFile() so we send // our cached mtime with the GET /doc request if we have it return this._lookup(uri) .then( async (entry: File) => { const contentBuffer = Buffer.from(content); - const putContent = isText(uri.path.split("/").pop(), contentBuffer) + const putContent = !csp // Web app files must always be written as raw bytes ? { content: new TextDecoder().decode(content).split(/\r?\n/), enc: false, @@ -579,15 +582,6 @@ export class FileSystemProvider implements vscode.FileSystemProvider { update = true; } } - if ( - csp && - !putContent.enc && - putContent.content.length > 1 && - putContent.content[putContent.content.length - 1] == "" - ) { - // Avoid appending a blank line on every save, which would cause a web app file to grow each time - putContent.content.pop(); - } // By the time we get here VS Code's built-in conflict resolution mechanism will already have interacted with the user. // Therefore, it's safe to ignore any conflicts. return api @@ -703,11 +697,11 @@ export class FileSystemProvider implements vscode.FileSystemProvider { }) .toString(); const mapvalue = cspFilesInProjectFolder.get(parentUriStr); - const idx = mapvalue.indexOf(path.basename(uri.path)); + const idx = mapvalue!.indexOf(path.basename(uri.path)); if (idx != -1) { - mapvalue.splice(idx, 1); - if (mapvalue.length) { - cspFilesInProjectFolder.set(parentUriStr, mapvalue); + mapvalue!.splice(idx, 1); + if (mapvalue!.length) { + cspFilesInProjectFolder.set(parentUriStr, mapvalue!); } else { cspFilesInProjectFolder.delete(parentUriStr); } @@ -736,7 +730,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { // Ignore the recursive flag for project folders toDeletePromise = projectContentsFromUri(uri, true); } else { - toDeletePromise = studioOpenDialogFromURI(uri, options.recursive ? { flat: true } : undefined).then( + toDeletePromise = studioOpenDialogFromURI(uri, options.recursive ? { flat: true } : undefined)!.then( (data) => data.result.content ); } @@ -764,7 +758,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (doc.status == "") { this.processDeletedDoc( doc, - DocumentContentProvider.getUri(doc.name, undefined, undefined, true, uri), + DocumentContentProvider.getUri(doc.name, undefined, undefined, true, uri)!, doc.name.includes("/"), project.length > 0 ); @@ -809,25 +803,25 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } public async rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): Promise { - if (!oldUri.path.split("/").pop().includes(".")) { + if (!oldUri.path.split("/").pop()!.includes(".")) { throw new vscode.FileSystemError("Cannot rename a package/folder"); } - if (oldUri.path.split(".").pop().toLowerCase() != newUri.path.split(".").pop().toLowerCase()) { + if (oldUri.path.split(".").pop()!.toLowerCase() != newUri.path.split(".").pop()!.toLowerCase()) { throw new vscode.FileSystemError("Cannot change a file's extension during rename"); } if (vscode.workspace.getWorkspaceFolder(oldUri) != vscode.workspace.getWorkspaceFolder(newUri)) { throw new vscode.FileSystemError("Cannot rename a file across workspace folders"); } validateUriIsCanonical(oldUri); - validateUriIsCanonical(newUri, true); + validateUriIsCanonical(newUri); // Check if the destination exists - let newFileStat: vscode.FileStat; + let newFileStat: vscode.FileStat | undefined; try { newFileStat = await vscode.workspace.fs.stat(newUri); if (!options.overwrite) { // If it does and we can't overwrite it, throw an error throw vscode.FileSystemError.FileExists(newUri); - } else if (newFileStat.permissions & vscode.FilePermission.Readonly) { + } else if (newFileStat.permissions! & vscode.FilePermission.Readonly) { // If the file is read-only, throw an error // This can happen if the target class is deployed, // or the document is marked read-only by source control @@ -909,7 +903,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (isfsConfig(uri).project) { compileListPromise = projectContentsFromUri(uri, true); } else { - compileListPromise = studioOpenDialogFromURI(uri, { flat: true }).then((data) => data.result.content); + compileListPromise = studioOpenDialogFromURI(uri, { flat: true })!.then((data) => data.result.content); } compileList.push(...(await compileListPromise.then((data) => data.map((e) => e.Name)))); } else { @@ -972,7 +966,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { ...filesToUpdate.map((f) => { return { type: vscode.FileChangeType.Changed, - uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, originalUri), + uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, originalUri)!, }; }) ); @@ -990,7 +984,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { ).map((f: string) => { return { type: vscode.FileChangeType.Changed, - uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, originalUri), + uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, originalUri)!, }; }) ); @@ -1006,8 +1000,8 @@ export class FileSystemProvider implements vscode.FileSystemProvider { private async _lookup(uri: vscode.Uri, fillInPath?: boolean): Promise { const api = new AtelierAPI(uri); const config = api.config; - const rootName = `${config.username}@${config.host}:${config.port}${config.pathPrefix}/${config.ns.toUpperCase()}`; - let entry: Entry = this.superRoot.entries.get(rootName); + const rootName = `${config.auth.username}@${config.host}:${config.port}${config.pathPrefix}/${config.ns!.toUpperCase()}`; + let entry: Entry | undefined = this.superRoot.entries.get(rootName); if (!entry) { entry = new Directory(rootName, ""); this.superRoot.entries.set(rootName, entry); @@ -1075,7 +1069,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { const fileName = isfsDocumentName(uri, csp); const api = new AtelierAPI(uri); return api - .getDoc(fileName, uri, cachedFile?.mtime) + .getDoc(fileName, uri, cachedFile?.mtime, undefined, csp) // Web app files must always be read as raw bytes .then((data) => data.result) .then( ({ ts, content }) => diff --git a/src/providers/FileSystemProvider/TextSearchProvider.ts b/src/providers/FileSystemProvider/TextSearchProvider.ts index d9e62636..0929c54d 100644 --- a/src/providers/FileSystemProvider/TextSearchProvider.ts +++ b/src/providers/FileSystemProvider/TextSearchProvider.ts @@ -34,9 +34,9 @@ function searchMatchToLine( let line = match.line ? Number(match.line) : null; if (match.member !== undefined) { // This is an attribute of a class member - if (match.member == "Storage" && match.attr.includes(",") && match.attrline == undefined) { + if (match.member == "Storage" && match.attr!.includes(",") && match.attrline == undefined) { // This is inside a Storage definition - const xmlTags = match.attr.split(","); + const xmlTags = match.attr!.split(","); const storageRegex = new RegExp(`^Storage ${xmlTags[0]}`); let inStorage = false; for (let i = 0; i < content.length; i++) { @@ -90,14 +90,14 @@ function searchMatchToLine( } else { if (match.attr === "Description") { // This is in the description - line = descLineToDocLine(content, match.attrline, i); - } else if (match.attrline || ["Code", "Data", "SqlQuery"].includes(match.attr)) { - if (["Code", "Data", "SqlQuery"].includes(match.attr)) { + line = descLineToDocLine(content, match.attrline!, i); + } else if (match.attrline || ["Code", "Data", "SqlQuery"].includes(match.attr!)) { + if (["Code", "Data", "SqlQuery"].includes(match.attr!)) { // This is in the implementation line = memend + (match.attrline ?? 1); } else { // This is a keyword with a multiline value - line = i + (match.attrline - 1 || 0); + line = i + (match.attrline! - 1 || 0); } } else { // This is in the class member definition @@ -163,7 +163,7 @@ function searchMatchToLine( if (content[i].match(classMatchPattern)) { if (match.attr == "Description") { // This is in the class description - line = descLineToDocLine(content, match.attrline, i); + line = descLineToDocLine(content, match.attrline!, i); break; } else if (match.attr == "Super" || match.attr == "Name") { // This is in the class definition line @@ -220,7 +220,7 @@ async function processSearchResults( results: number, maxResults: number, token: vscode.CancellationToken -): Promise { +): Promise { if (token.isCancellationRequested) { return; } @@ -228,7 +228,7 @@ async function processSearchResults( if (token.isCancellationRequested) { return; } - let message: vscode.TextSearchCompleteMessage; + let message: vscode.TextSearchCompleteMessage | undefined; const rejected = fileResults.filter((r) => r.status == "rejected").length; if (rejected > 0) { outputChannel.appendLine("Search errors:"); @@ -258,7 +258,7 @@ async function processSearchResults( function removeConfigExcludes(folder: vscode.Uri, excludes: string[]): string[] { // Function to get one of the two kinds of exclude settings as an array const getConfigExcludes = (key: string) => { - return Object.entries(vscode.workspace.getConfiguration(key, folder).get("exclude")) + return Object.entries(vscode.workspace.getConfiguration(key, folder).get("exclude")!) .filter((value) => value[1] === true) .map((value) => value[0]); }; @@ -289,7 +289,7 @@ export class TextSearchProvider implements vscode.TextSearchProvider { options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken - ): Promise { + ): Promise { const api = new AtelierAPI(options.folder); const rateLimiter = new RateLimiter(50); if (!api.active) { @@ -359,13 +359,13 @@ export class TextSearchProvider implements vscode.TextSearchProvider { return; } - const uri = DocumentContentProvider.getUri(file.doc, "", "", true, options.folder); + const uri = DocumentContentProvider.getUri(file.doc, "", "", true, options.folder)!; const content = decoder.decode(await vscode.workspace.fs.readFile(uri)).split(/\r?\n/); const contentLength = content.length; // Find all lines that we have matches on const multilineMethodArgs: boolean = vscode.workspace .getConfiguration("objectscript", options.folder) - .get("multilineMethodArgs"); + .get("multilineMethodArgs")!; const lines = file.matches .map((match: SearchMatch) => token.isCancellationRequested ? null : searchMatchToLine(content, match, file.doc, multilineMethodArgs) @@ -378,26 +378,26 @@ export class TextSearchProvider implements vscode.TextSearchProvider { if (token.isCancellationRequested) { return; } - const text = content[line]; + const text = content[line!]; const regex = new RegExp( query.isRegExp ? query.pattern : query.pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), query.isCaseSensitive ? "g" : "gi" ); - let regexMatch: RegExpExecArray; + let regexMatch: RegExpExecArray | null; const matchRanges: vscode.Range[] = []; const previewRanges: vscode.Range[] = []; - while ((regexMatch = regex.exec(text)) !== null && counter < options.maxResults) { + while ((regexMatch = regex.exec(text)) !== null && counter < options.maxResults!) { const start = regexMatch.index; const end = start + regexMatch[0].length; - matchRanges.push(new vscode.Range(line, start, line, end)); + matchRanges.push(new vscode.Range(line!, start, line!, end)); previewRanges.push(new vscode.Range(0, start, 0, end)); counter++; } if (matchRanges.length && previewRanges.length) { if (options.beforeContext) { // Add preceding context lines that aren't themselves result lines - const previewFrom = Math.max(line - options.beforeContext, 0); - for (let i = previewFrom; i < line; i++) { + const previewFrom = Math.max(line! - options.beforeContext, 0); + for (let i = previewFrom; i < line!; i++) { if (!matchedLines.has(i)) { progress.report({ uri, @@ -417,8 +417,8 @@ export class TextSearchProvider implements vscode.TextSearchProvider { }); if (options.afterContext) { // Add following context lines that aren't themselves result lines - const previewTo = Math.min(line + options.afterContext, contentLength - 1); - for (let i = line + 1; i <= previewTo; i++) { + const previewTo = Math.min(line! + options.afterContext, contentLength - 1); + for (let i = line! + 1; i <= previewTo; i++) { if (!matchedLines.has(i)) { progress.report({ uri, @@ -444,7 +444,7 @@ export class TextSearchProvider implements vscode.TextSearchProvider { options.includes = deduplicateGlobArray(options.includes); options.excludes = deduplicateGlobArray(removeConfigExcludes(options.folder, options.excludes)); - if (api.config.apiVersion >= 6) { + if (api.config.apiVersion! >= 6) { // Build the request object const request: AsyncSearchRequest = { request: "search", @@ -559,8 +559,8 @@ export class TextSearchProvider implements vscode.TextSearchProvider { let groupLen = 0; let group: string[] = []; for (const doc of prjContents) { - group.push(doc); - groupLen += doc.length; + group.push(doc!); + groupLen += doc!.length; if (groupLen >= 1300) { // Be conservative because we really don't want ugly 414 errors requestGroups.push(group); diff --git a/src/providers/LowCodeEditorProvider.ts b/src/providers/LowCodeEditorProvider.ts index 46009619..24215474 100644 --- a/src/providers/LowCodeEditorProvider.ts +++ b/src/providers/LowCodeEditorProvider.ts @@ -36,14 +36,14 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { return this._errorMessage(`${document.fileName} is a malformed class definition.`); } const api = new AtelierAPI(document.uri); - if (!vscode.workspace.fs.isWritableFileSystem(document.uri.scheme) && lt(api.config.serverVersion, "2025.3.0")) { + if (!vscode.workspace.fs.isWritableFileSystem(document.uri.scheme) && lt(api.config.serverVersion!, "2025.3.0")) { return this._errorMessage(`File system '${document.uri.scheme}' is read-only.`); } const className = file.name.slice(0, -4); if (!api.active) { return this._errorMessage("Server connection is not active."); } - if (lt(api.config.serverVersion, "2023.1.0")) { + if (lt(api.config.serverVersion!, "2023.1.0")) { return this._errorMessage( "Opening a low-code editor in VS Code requires InterSystems IRIS version 2023.1 or above." ); @@ -66,14 +66,14 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { } else if (queryData.result.content[0].Rule) { webApp = this._rule; } else if (queryData.result.content[0].DTL) { - if (lt(api.config.serverVersion, "2025.1.0")) { + if (lt(api.config.serverVersion!, "2025.1.0")) { return this._errorMessage( "Opening the DTL editor in VS Code requires InterSystems IRIS version 2025.1 or above." ); } webApp = this._dtl; } else if (queryData.result.content[0].BPL) { - if (lt(api.config.serverVersion, "2026.1.0")) { + if (lt(api.config.serverVersion!, "2026.1.0")) { return this._errorMessage( "Opening the BPL editor in VS Code requires InterSystems IRIS version 2026.1 or above." ); @@ -208,13 +208,19 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { if (!editorCompatible) { this._errorMessage("This low-code editor does not support embedding in VS Code."); } else { - // Editor is compatible so send the credentials - webviewPanel.webview.postMessage({ - direction: "editor", - type: "auth", - username: api.config.username, - password: api.config.password, - }); + const username = api.config.auth.username; + if (username.includes("*")) { + vscode.window.showWarningMessage( + `Cannot automatically log into low-code editors if VS Code is logged in using ${username.slice(1, -1)}.` + ); + } else { + webviewPanel.webview.postMessage({ + direction: "editor", + type: "auth", + username, + password: api.config.auth.password, + }); + } } return; case "changed": diff --git a/src/providers/ObjectScriptClassSymbolProvider.ts b/src/providers/ObjectScriptClassSymbolProvider.ts index cb2c5bf4..10db56c3 100644 --- a/src/providers/ObjectScriptClassSymbolProvider.ts +++ b/src/providers/ObjectScriptClassSymbolProvider.ts @@ -6,7 +6,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro token: vscode.CancellationToken ): Thenable { return new Promise((resolve) => { - let classItSelf = null; + let classItSelf: vscode.DocumentSymbol | null = null; let symbols: vscode.DocumentSymbol[] = []; let inComment = false; @@ -66,7 +66,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro } } symbols.push({ - children: undefined, + children: undefined!, detail: method[1], kind: vscode.SymbolKind.Method, name: method[2].replace(/"/g, ""), @@ -78,7 +78,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro const index = line.text.match(/^(Index|ForegnKey) (%?\b\w+\b)/i); if (index) { symbols.push({ - children: undefined, + children: undefined!, detail: index[1], kind: vscode.SymbolKind.Key, name: index[2], @@ -99,7 +99,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro } } symbols.push({ - children: undefined, + children: undefined!, detail: property[1], kind: vscode.SymbolKind.Property, name: property[2], @@ -111,7 +111,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro const parameter = line.text.match(/^(Parameter) (%?\b\w+\b)/i); if (parameter) { symbols.push({ - children: undefined, + children: undefined!, detail: parameter[1], kind: vscode.SymbolKind.Constant, name: parameter[2], @@ -134,7 +134,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro } } symbols.push({ - children: undefined, + children: undefined!, detail: other[1], kind: vscode.SymbolKind.Struct, name: other[2], @@ -144,7 +144,7 @@ export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolPro } } - resolve([classItSelf]); + resolve([classItSelf!]); }); } } diff --git a/src/providers/ObjectScriptCodeLensProvider.ts b/src/providers/ObjectScriptCodeLensProvider.ts index f73a9052..a81b69dd 100644 --- a/src/providers/ObjectScriptCodeLensProvider.ts +++ b/src/providers/ObjectScriptCodeLensProvider.ts @@ -81,7 +81,7 @@ function getSqlQuery(document: vscode.TextDocument, startLine: number, startChar if ( result.length && !["SELECT", "DECLARE", "UPDATE", "DELETE", "TRUNCATE", "INSERT"].includes( - result.trimStart().split(/\s+/).shift().toUpperCase() + result.trimStart().split(/\s+/).shift()!.toUpperCase() ) ) { // Can only generate plans for certain SQL statements @@ -109,8 +109,8 @@ function scanCodeBlock( let inCStyleComment = false; for (let i = start; i < end; i++) { const line = document.lineAt(i).text; - let commentStart: number; - let commentEnd: number; + let commentStart: number | undefined; + let commentEnd: number | undefined; if (!inCStyleComment) { const commentMatch = line.match(commentRegex); if (commentMatch) { @@ -138,20 +138,20 @@ function scanCodeBlock( // Check if the match is commented out or in a string literal if ( ((commentStart == undefined && commentEnd == undefined) || - (commentStart != undefined && eSqlMatch.index < commentStart) || - (commentEnd != undefined && eSqlMatch.index > commentEnd)) && + (commentStart != undefined && eSqlMatch.index! < commentStart) || + (commentEnd != undefined && eSqlMatch.index! > commentEnd)) && // There are an even number of, or zero, quotes preceding the match - line.slice(0, eSqlMatch.index).split('"').length % 2 == 1 + line.slice(0, eSqlMatch.index!).split('"').length % 2 == 1 ) { const sqlQuery = getSqlQuery( document, i, - eSqlMatch.index + eSqlMatch[0].length, + eSqlMatch.index! + eSqlMatch[0].length, `)${(eSqlMatch[1] ?? "").split("").reverse().join("")}` ); if (sqlQuery) { result.push( - new vscode.CodeLens(new vscode.Range(i, eSqlMatch.index, i, eSqlMatch.index + eSqlMatch[0].length), { + new vscode.CodeLens(new vscode.Range(i, eSqlMatch.index!, i, eSqlMatch.index! + eSqlMatch[0].length), { title: "Show Plan", tooltip: "Show the plan for this query", command: "vscode-objectscript.showPlanWebview", @@ -187,7 +187,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { public async provideCodeLenses( document: vscode.TextDocument, token: vscode.CancellationToken - ): Promise { + ): Promise { if (![clsLangId, macLangId, intLangId].includes(document.languageId)) return; const file = currentFile(document); if (!file) return; // Document is malformed @@ -198,9 +198,9 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { if (!symbols?.length || token.isCancellationRequested) return; const api = new AtelierAPI(document.uri); const conf = vscode.workspace.getConfiguration("objectscript.debug"); - const debugThisMethod: boolean = conf.get("debugThisMethod") && api.active; - const copyToClipboard: boolean = conf.get("copyToClipboard"); - const showPlan = api.active && gte(api.config.serverVersion, "2024.1.0"); + const debugThisMethod: boolean = conf.get("debugThisMethod")! && api.active; + const copyToClipboard: boolean = conf.get("copyToClipboard")!; + const showPlan = api.active && gte(api.config.serverVersion!, "2024.1.0"); const result: vscode.CodeLens[] = []; if (document.languageId == clsLangId) { if (!symbols[0].children.length) return; @@ -245,7 +245,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { symbols[0].children.forEach((symbol, idx) => { const type = symbol.detail.toLowerCase(); if (!["xdata", "method", "classmethod", "query", "trigger"].includes(type)) return; - let symbolLine: number; + let symbolLine: number | undefined; if (languageServer) { symbolLine = symbol.selectionRange.start.line; } else { @@ -259,17 +259,17 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { switch (type) { case "xdata": { if (api.active) { - let cmd: vscode.Command; + let cmd: vscode.Command | undefined; if ( (symbol.name == "RuleDefinition" && superclasses.includes("Ens.Rule.Definition") && - gte(api.config.serverVersion, "2023.1.0")) || + gte(api.config.serverVersion!, "2023.1.0")) || (symbol.name == "DTL" && superclasses.includes("Ens.DataTransformDTL") && - gte(api.config.serverVersion, "2025.1.0")) || + gte(api.config.serverVersion!, "2025.1.0")) || (symbol.name == "BPL" && superclasses.includes("Ens.BusinessProcessBPL") && - gte(api.config.serverVersion, "2026.1.0")) + gte(api.config.serverVersion!, "2026.1.0")) ) { cmd = { title: "Reopen in Low-Code Editor", @@ -285,7 +285,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { arguments: [`/${className}.cls`, document.uri], }; } - if (cmd) result.push(new vscode.CodeLens(this.range(symbolLine), cmd)); + if (cmd) result.push(new vscode.CodeLens(this.range(symbolLine!), cmd)); } break; } @@ -294,7 +294,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { case "trigger": case "query": { // Capture the entire text of the class member definition up to the implementation - const memberInfo = parseClassMemberDefinition(document, symbol, symbolLine); + const memberInfo = parseClassMemberDefinition(document, symbol, symbolLine!); if (!memberInfo) break; const { definition, defEndLine, language, isPrivate } = memberInfo; if (showPlan) { @@ -307,7 +307,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { const sqlQuery = getSqlQuery(document, defEndLine + 1, 0, "}"); if (sqlQuery) { result.push( - new vscode.CodeLens(this.range(symbolLine), { + new vscode.CodeLens(this.range(symbolLine!), { title: "Show Plan", tooltip: "Show the plan for this query", command: "vscode-objectscript.showPlanWebview", @@ -342,7 +342,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { ) { const argsMatch = definition.match(new RegExp(`${displayName}\\(([^)]*)\\)`)); result.push( - this.addDebugThisMethod(symbolLine, [ + this.addDebugThisMethod(symbolLine!, [ `##class(${className}).${displayName}`, argsMatch && typeof argsMatch[1] == "string" && argsMatch[1].trim().length > 0, ]) @@ -354,7 +354,7 @@ export class ObjectScriptCodeLensProvider implements vscode.CodeLensProvider { (type == "classmethod" || (type == "query" && displayName[0] != '"')) ) { result.push( - this.addCopyToClipboard(symbolLine, [ + this.addCopyToClipboard(symbolLine!, [ `##class(${className}).${displayName}${type == "query" ? "Func" : ""}()`, ]) ); diff --git a/src/providers/ObjectScriptCompletionItemProvider.ts b/src/providers/ObjectScriptCompletionItemProvider.ts index 670001a7..230cfb20 100644 --- a/src/providers/ObjectScriptCompletionItemProvider.ts +++ b/src/providers/ObjectScriptCompletionItemProvider.ts @@ -40,7 +40,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem ); } } - const completions = [] + const completions: any[] = ([] as any[]) .concat( this.classes(document, position, token, context), this.macrolist(document, position, token, context), @@ -99,11 +99,11 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem const range = document.getWordRangeAtPosition(position, pattern); const text = range ? document.getText(range) : ""; if (range) { - const [, prefix, macro = ""] = text.toLowerCase().match(pattern); + const [, prefix, macro = ""] = text.toLowerCase().match(pattern)!; const file = currentFile(); const api = new AtelierAPI(); return api - .getmacrolist(file.name, []) + .getmacrolist(file!.name, []) .then((data) => data.result.content.macros) .then((list) => list.filter((el) => el.toLowerCase().startsWith(macro))) .then((list) => list.map((el) => "$$$" + el)) @@ -174,7 +174,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem return { ...el, label: this._formatter.function(el.label as string), - insertText: this._formatter.function(el.insertText), + insertText: this._formatter.function(el.insertText!), range, }; }), @@ -236,7 +236,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext - ): vscode.CompletionItem[] { + ): vscode.CompletionItem[] | null { const range = document.getWordRangeAtPosition(position, /%?\b\w+[\w\d]*\b/); const kind = vscode.CompletionItemKind.Variable; if (context.triggerKind === vscode.CompletionTriggerKind.Invoke) { @@ -273,14 +273,14 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem const searchText = document.getText(range); const method = (el) => ({ - documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null, + documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null!, insertText: new vscode.SnippetString(`${el.name}($0)`), kind: vscode.CompletionItemKind.Method, label: el.name, }); const parameter = (el) => ({ - documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null, + documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null!, insertText: new vscode.SnippetString(`${el.name}`), kind: vscode.CompletionItemKind.Constant, label: `${el.name}`, @@ -288,7 +288,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem }); const property = (el) => ({ - documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null, + documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null!, insertText: new vscode.SnippetString(`${el.name}`), kind: vscode.CompletionItemKind.Property, label: el.name, @@ -306,10 +306,10 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem return classDef.methods("class").then((data) => data.filter(search).map(method)); } - if (curFile.fileName.endsWith("cls")) { + if (curFile!.fileName.endsWith("cls")) { const selfRef = textBefore.match(/(? data.filter(search).map(parameter)); } @@ -333,7 +333,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem let pattern = /##class\(([^)]*)\)/i; let range = document.getWordRangeAtPosition(position, pattern); let text = range ? document.getText(range) : ""; - let [, className] = range ? text.match(pattern) : ""; + let [, className] = (range ? text.match(pattern)! : "") as [string, string | undefined]; if (!range) { pattern = /(\b(?:Of|As)\b (%?\b[a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]+)*\b\.?)?(?! of))/i; range = document.getWordRangeAtPosition(position, pattern); @@ -353,18 +353,18 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem className = text.split(/\s|\(/).pop(); } if (range) { - const percent = className.startsWith("%"); - const library = percent && className.indexOf(".") < 0; + const percent = className!.startsWith("%"); + const library = percent && className!.indexOf(".") < 0; className = className || ""; const searchName = className.replace(/(^%|")/, "").toLowerCase(); const part = className.split(".").length; - const params = []; + const params: string[] = []; let sql = ""; /// Classes from the current class's package - if (part === 1 && curFile.fileName.endsWith("cls")) { - const packageName = curFile.name.split(".").slice(0, -2).join("."); - const className2 = curFile.name.split(".").slice(0, -1).join("."); + if (part === 1 && curFile!.fileName.endsWith("cls")) { + const packageName = curFile!.name.split(".").slice(0, -2).join("."); + const className2 = curFile!.name.split(".").slice(0, -1).join("."); const part2 = packageName.split(".").length + 1; sql += ` SELECT @@ -442,7 +442,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem ): vscode.ProviderResult { const range = document.getWordRangeAtPosition(position, /\$system(\.\b\w+\b)?(\.\b\w+\b)?\./i); const text = range ? document.getText(range) : ""; - const [, className] = text.match(/\$system(\.\b\w+\b)?(\.\b\w+\b)?\./i); + const [, className] = text.match(/\$system(\.\b\w+\b)?(\.\b\w+\b)?\./i)!; const api = new AtelierAPI(); if (!className) { @@ -466,7 +466,7 @@ export class ObjectScriptCompletionItemProvider implements vscode.CompletionItem .content.methods.filter((el) => !el.private) .filter((el) => !el.internal) .map((el) => ({ - documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null, + documentation: el.desc.length ? new vscode.MarkdownString(el.desc.join("")) : null!, insertText: new vscode.SnippetString(`${el.name}($0)`), kind: vscode.CompletionItemKind.Method, label: el.name, diff --git a/src/providers/ObjectScriptDefinitionProvider.ts b/src/providers/ObjectScriptDefinitionProvider.ts index 1012e4af..feb403d3 100644 --- a/src/providers/ObjectScriptDefinitionProvider.ts +++ b/src/providers/ObjectScriptDefinitionProvider.ts @@ -24,7 +24,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider if (selfRef) { const selfEntity = document.getText(selfRef).substr(2); const range = new vscode.Range(position.line, selfRef.start.character + 2, position.line, selfRef.end.character); - const classDefinition = new ClassDefinition(file.name); + const classDefinition = new ClassDefinition(file!.name); return classDefinition.getMemberLocations(selfEntity).then((locations): vscode.DefinitionLink[] => locations.map( (location): vscode.DefinitionLink => ({ @@ -41,9 +41,9 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider const macroMatch = macroText.match(/^\${3}(\b\w+\b)$/); if (macroMatch) { const [, macro] = macroMatch; - return this.macro(workspaceFolderName, currentFile(), macro).then((data) => + return this.macro(workspaceFolderName, currentFile()!, macro).then((data) => data && data.document.length - ? new vscode.Location(DocumentContentProvider.getUri(data.document), new vscode.Position(data.line, 0)) + ? new vscode.Location(DocumentContentProvider.getUri(data.document)!, new vscode.Position(data.line, 0)) : null ); } @@ -77,7 +77,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider const [keyword, name] = part.split(" "); const start = pos + keyword.length + 1; if (this.isValid(position, start, name.length)) { - return [this.makePropertyDefinition(document, name)]; + return [this.makePropertyDefinition(document, name)!]; } } pos += part.length; @@ -87,7 +87,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider pos = 0; for (const part of parts) { if (part.match(onPropertyList)) { - const listProperties = /\(([^)]+)\)/.exec(part)[1].split(/\s*,\s*/); + const listProperties = /\(([^)]+)\)/.exec(part)![1].split(/\s*,\s*/); return listProperties .map((name) => { name = name.trim(); @@ -105,7 +105,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider pos = 0; for (const part of parts) { if (part.match(asClassList)) { - const listClasses = /\(([^)]+)\)/.exec(part)[1].split(/\s*,\s*/); + const listClasses = /\(([^)]+)\)/.exec(part)![1].split(/\s*,\s*/); return listClasses .map((name) => { name = name.trim(); @@ -149,7 +149,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider pos = 0; for (const part of parts) { if (part.match(asRoutineList)) { - const listRoutines = /\(([^)]+)\)/.exec(part)[1].split(","); + const listRoutines = /\(([^)]+)\)/.exec(part)![1].split(","); for (let name of listRoutines) { name = name.trim(); const start = pos + part.indexOf(name); @@ -174,7 +174,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider pos = 0; for (const part of parts) { if (part.match(asLabelRoutineCall)) { - const [, routine] = part.match(/\^(%?\b\w+\b)/); + const [, routine] = part.match(/\^(%?\b\w+\b)/)!; const start = pos + part.indexOf(routine) - 1; const length = routine.length + 1; return this.getFullRoutineName(routine).then((routineName) => [ @@ -195,7 +195,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider const classRef = /##class\(([^)]+)\)(?:\\$this)?\.(#?%?[a-zA-Z][a-zA-Z0-9]*)/i; const classRefRange = document.getWordRangeAtPosition(position, classRef); if (classRefRange) { - const [, className, entity] = document.getText(classRefRange).match(classRef); + const [, className, entity] = document.getText(classRefRange).match(classRef)!; const start = classRefRange.start.character + 8; if (this.isValid(position, start, className.length)) { return [ @@ -274,11 +274,11 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider new vscode.Position(position.line, start + length) ), targetRange: new vscode.Range(firstLinePos, firstLinePos), - targetUri: DocumentContentProvider.getUri(name, workspaceFolder), + targetUri: DocumentContentProvider.getUri(name, workspaceFolder)!, }; } - public makePropertyDefinition(document: vscode.TextDocument, name: string): vscode.DefinitionLink { + public makePropertyDefinition(document: vscode.TextDocument, name: string): vscode.DefinitionLink | null { const property = new RegExp(`(?<=^Property\\s)\\b${name}\\b`, "i"); let descrLine = -1; for (let i = 0; i < document.lineCount; i++) { @@ -292,12 +292,12 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider const propertyMatch = line.match(property); if (propertyMatch) { const targetSelectionRange = new vscode.Range( - new vscode.Position(i, propertyMatch.index), - new vscode.Position(i, propertyMatch.index + name.length) + new vscode.Position(i, propertyMatch.index!), + new vscode.Position(i, propertyMatch.index! + name.length) ); const targetRange = new vscode.Range( new vscode.Position(descrLine >= 0 ? descrLine : i, 0), - new vscode.Position(i, propertyMatch.index + line.length) + new vscode.Position(i, propertyMatch.index! + line.length) ); return { targetUri: document.uri, @@ -323,7 +323,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider new vscode.Position(position.line, start + length) ), targetRange: new vscode.Range(firstLinePos, firstLinePos), - targetUri: DocumentContentProvider.getUri(name, workspaceFolder), + targetUri: DocumentContentProvider.getUri(name, workspaceFolder)!, }; } @@ -334,7 +334,7 @@ export class ObjectScriptDefinitionProvider implements vscode.DefinitionProvider ): Promise<{ document: string; line: number }> { const fileName = file.name; const api = new AtelierAPI(); - let includes = []; + let includes: string[] = []; if (fileName.toLowerCase().endsWith("cls")) { const classDefinition = new ClassDefinition(fileName); includes = await classDefinition.includeCode(); diff --git a/src/providers/ObjectScriptDiagnosticProvider.ts b/src/providers/ObjectScriptDiagnosticProvider.ts index a903827e..2941ffd7 100644 --- a/src/providers/ObjectScriptDiagnosticProvider.ts +++ b/src/providers/ObjectScriptDiagnosticProvider.ts @@ -324,12 +324,12 @@ export class ObjectScriptDiagnosticProvider { } const pattern = /(? { const word = document.getWordRangeAtPosition(position); const text = document.getText( - new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word.end.character)) + new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word!.end.character)) ); const file = currentFile(); @@ -32,9 +32,9 @@ export class ObjectScriptHoverProvider implements vscode.HoverProvider { const range = document.getWordRangeAtPosition(position, /\^?\$+\b\w+\b$/); let search = dollarsMatch.shift(); const [dollars, value] = dollarsMatch; - search = search.toUpperCase(); + search = search!.toUpperCase(); if (dollars === "$$$") { - return this.macro(file.name, value).then((contents) => ({ + return this.macro(file!.name, value).then((contents) => ({ contents: [contents.join("")], range, })); @@ -44,7 +44,7 @@ export class ObjectScriptHoverProvider implements vscode.HoverProvider { found = found || structuredSystemVariables.find((el) => el.label === search || el.alias.includes(search)); if (found) { return { - contents: [found.documentation.join(""), this.documentationLink(found.link)], + contents: [found.documentation.join(""), this.documentationLink(found.link)!], range, }; } @@ -56,7 +56,7 @@ export class ObjectScriptHoverProvider implements vscode.HoverProvider { public async macro(fileName: string, macro: string): Promise { const api = new AtelierAPI(); - let includes = []; + let includes: string[] = []; if (fileName.toLowerCase().endsWith(".cls")) { const classDefinition = new ClassDefinition(fileName); includes = await classDefinition.includeCode(); @@ -74,7 +74,7 @@ export class ObjectScriptHoverProvider implements vscode.HoverProvider { public commands(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { const word = document.getWordRangeAtPosition(position); const text = document.getText( - new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word.end.character)) + new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word!.end.character)) ); const commandMatch = text.match(/^\s+\b[a-z]+\b$/i); if (commandMatch) { @@ -82,14 +82,14 @@ export class ObjectScriptHoverProvider implements vscode.HoverProvider { const command = commands.find((el) => el.label === search || el.alias.includes(search)); if (search) { return { - contents: [command.documentation.join(""), this.documentationLink(command.link)], + contents: [command!.documentation.join(""), this.documentationLink(command!.link)!], range: word, }; } } } - public documentationLink(link: string): string | null { + public documentationLink(link: string | undefined): string | null | undefined { if (link) { return `[Online documentation](${ link.startsWith("http") ? "" : "https://docs.intersystems.com/irislatest" diff --git a/src/providers/WorkspaceSymbolProvider.ts b/src/providers/WorkspaceSymbolProvider.ts index b1b991ac..28596e33 100644 --- a/src/providers/WorkspaceSymbolProvider.ts +++ b/src/providers/WorkspaceSymbolProvider.ts @@ -35,7 +35,7 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { * because we aren't including ranges. They will be resolved later. */ private _queryResultToSymbols(data: any, wsFolder: vscode.WorkspaceFolder): any[] { - const result = []; + const result: any[] = []; const uris: Map = new Map(); for (const element of data.result.content) { const kind: vscode.SymbolKind = (() => { @@ -68,7 +68,7 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { let uri: vscode.Uri; if (uris.has(element.Parent)) { - uri = uris.get(element.Parent); + uri = uris.get(element.Parent)!; } else { uri = DocumentContentProvider.getUri( `${element.Parent}.cls`, @@ -78,7 +78,7 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { wsFolder.uri, // Only "file" scheme is fully supported for client-side editing wsFolder.uri.scheme != "file" - ); + )!; uris.set(element.Parent, uri); } @@ -97,7 +97,7 @@ export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { public async provideWorkspaceSymbols( query: string, token: vscode.CancellationToken - ): Promise { + ): Promise { if (!vscode.workspace.workspaceFolders?.length) return; // Convert query to a LIKE compatible pattern const pattern = queryToFuzzyLike(query); diff --git a/src/test/runTest.ts b/src/test/runTest.ts index f18c548e..20b711a6 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -16,7 +16,8 @@ async function main() { // The path to the workspace file const workspace = path.resolve("test-fixtures", "test.code-workspace"); - const vscodeExecutablePath = await downloadAndUnzipVSCode("stable"); + const vscodeExecutablePath = + process.env.VSCODE_EXECUTABLE_PATH ?? (await downloadAndUnzipVSCode({ version: "stable", timeout: 60_000 })); const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); const installExtension = (extId) => @@ -29,10 +30,16 @@ async function main() { installExtension("consistem-sistemas.consistem-servermanager"); installExtension("consistem-sistemas.consistem-language-server"); - const launchArgs = ["-n", workspace, "--enable-proposed-api", "consistem-sistemas.consistem-vscode-objectscript"]; + const launchArgs = [workspace, "--enable-proposed-api", "consistem-sistemas.consistem-vscode-objectscript"]; // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs }); + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath, + extensionTestsPath, + launchArgs, + extensionTestsEnv: { ELECTRON_RUN_AS_NODE: undefined }, + }); } catch (err) { console.error("Failed to run tests", err); process.exit(1); diff --git a/src/utils/FileProviderUtil.ts b/src/utils/FileProviderUtil.ts index 099a0eb8..d97df511 100644 --- a/src/utils/FileProviderUtil.ts +++ b/src/utils/FileProviderUtil.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { gte } from "semver"; import { AtelierAPI } from "../api"; import { ProjectItem } from "../commands/project"; @@ -32,12 +33,12 @@ export function isfsConfig(uri: vscode.Uri): IsfsUriConfig { mapped: params.get(IsfsUriParam.Mapped) != "0", filter: params.get(IsfsUriParam.Filter) ?? "", project: params.get(IsfsUriParam.Project) ?? "", - csp: ["", "1"].includes(params.get(IsfsUriParam.CSP)), + csp: ["", "1"].includes(params.get(IsfsUriParam.CSP)!), ns: params.get(IsfsUriParam.NS) || undefined, }; } -export async function projectContentsFromUri(uri: vscode.Uri, flat = false): Promise { +export async function projectContentsFromUri(uri: vscode.Uri, flat = false): Promise { const api = new AtelierAPI(uri); if (!api.active) { return; @@ -125,13 +126,19 @@ export async function projectContentsFromUri(uri: vscode.Uri, flat = false): Pro query = `SELECT DISTINCT BY (${nameCol}) ${nameCol} ` + "Name, Type FROM %Studio.Project_ProjectItemsList(?,1) AS pil WHERE " + - "(Type = 'MAC' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.mac,*.int,*.inc,*.bas,*.mvi',1,1,1,1,0,1) AS sod WHERE pil.Name = sod.Name)) OR " + - "(Type = 'CSP' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,1,0,1) AS sod WHERE '/'||pil.Name = sod.Name)) OR " + - "(Type NOT IN ('CLS','PKG','MAC','CSP','DIR','GBL') AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.other',1,1,1,1,0,1) AS sod WHERE " + - "$PIECE(sod.Name,'.',1,$LENGTH(sod.Name,'.')-1) = $PIECE(pil.Name,'.',1,$LENGTH(pil.Name,'.')-1) AND UPPER($PIECE(sod.Name,'.',$LENGTH(sod.Name,'.'))) = $PIECE(pil.Name,'.',$LENGTH(pil.Name,'.')))) OR " + - "(Type = 'CLS' AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID = pil.Name)) OR " + - "(Type = 'PKG' AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID %STARTSWITH pil.Name||'.')) OR " + - "(Type = 'DIR' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,0,0,1) AS sod WHERE pil.Name %STARTSWITH sod.Name||'/' OR pil.Name = sod.Name))"; + (gte(api.config.serverVersion!, "2021.1.0") + ? "(Type NOT IN ('CLS','PKG','DIR','GBL') AND EXISTS (SELECT Size FROM %Library.RoutineMgr_StudioOpenDialog(pil.Name,1,1,1,1,0,1))) OR " + + "(Type = 'CLS' AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID = pil.Name)) OR " + + "(Type = 'PKG' AND EXISTS (SELECT TOP 1 dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID %STARTSWITH pil.Name||'.')) OR " + + "(Type = 'DIR' AND EXISTS (SELECT TOP 1 Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,0,0,1,'Name %STARTSWITH '''||pil.Name||'/''')))" + : // Need to use slower version of query on very old servers + "(Type = 'MAC' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.mac,*.int,*.inc,*.bas,*.mvi',1,1,1,1,0,1) AS sod WHERE pil.Name = sod.Name)) OR " + + "(Type = 'CSP' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,1,0,1) AS sod WHERE '/'||pil.Name = sod.Name)) OR " + + "(Type NOT IN ('CLS','PKG','MAC','CSP','DIR','GBL') AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.other',1,1,1,1,0,1) AS sod WHERE " + + "$PIECE(sod.Name,'.',1,$LENGTH(sod.Name,'.')-1) = $PIECE(pil.Name,'.',1,$LENGTH(pil.Name,'.')-1) AND UPPER($PIECE(sod.Name,'.',$LENGTH(sod.Name,'.'))) = $PIECE(pil.Name,'.',$LENGTH(pil.Name,'.')))) OR " + + "(Type = 'CLS' AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID = pil.Name)) OR " + + "(Type = 'PKG' AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID %STARTSWITH pil.Name||'.')) OR " + + "(Type = 'DIR' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,0,0,1) AS sod WHERE pil.Name %STARTSWITH sod.Name||'/' OR pil.Name = sod.Name))"); parameters = [project]; } } @@ -165,7 +172,7 @@ export function fileSpecFromURI(uri: vscode.Uri): string { export function studioOpenDialogFromURI( uri: vscode.Uri, overrides: { flat?: boolean; filter?: string } = { flat: false, filter: "" } -): Promise { +): Promise | undefined { const api = new AtelierAPI(uri); if (!api.active) return; const { system, generated, mapped } = isfsConfig(uri); @@ -177,7 +184,7 @@ export function studioOpenDialogFromURI( overrides?.flat ? "1" : "0", "0", // NotStudio (0 means hide globals and OBJ files) generated ? "1" : "0", - overrides.filter, + overrides.filter!, "0", // RoundTime (0 means no rounding) mapped ? "1" : "0", ]); diff --git a/src/utils/classDefinition.ts b/src/utils/classDefinition.ts index d4e841ca..2c477d76 100644 --- a/src/utils/classDefinition.ts +++ b/src/utils/classDefinition.ts @@ -5,7 +5,7 @@ import { DocumentContentProvider } from "../providers/DocumentContentProvider"; export class ClassDefinition { public get uri(): vscode.Uri { - return DocumentContentProvider.getUri(this._classFileName, this._workspaceFolder, this._namespace); + return DocumentContentProvider.getUri(this._classFileName, this._workspaceFolder, this._namespace)!; } public static normalizeClassName(className: string, withExtension = false): string { @@ -13,8 +13,8 @@ export class ClassDefinition { } private _className: string; private _classFileName: string; - private _workspaceFolder: string; - private _namespace: string; + private _workspaceFolder: string | undefined; + private _namespace: string | undefined; public constructor(className: string, workspaceFolder?: string, namespace?: string) { this._workspaceFolder = workspaceFolder; @@ -31,11 +31,11 @@ export class ClassDefinition { } public async methods(scope: "any" | "class" | "instance" = "any"): Promise { - const methods = []; + const methods: any[] = []; const filterScope = (method) => scope === "any" || method.scope === scope; const api = new AtelierAPI(this.uri); const getMethods = (content) => { - const extend = []; + const extend: any[] = []; content.forEach((el) => { methods.push(...el.content.methods); extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); @@ -49,10 +49,10 @@ export class ClassDefinition { } public async properties(): Promise { - const properties = []; + const properties: any[] = []; const api = new AtelierAPI(this.uri); const getProperties = (content) => { - const extend = []; + const extend: any[] = []; content.forEach((el) => { properties.push(...el.content.properties); extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); @@ -66,10 +66,10 @@ export class ClassDefinition { } public async parameters(): Promise { - const parameters = []; + const parameters: any[] = []; const api = new AtelierAPI(this.uri); const getParameters = (content) => { - const extend = []; + const extend: any[] = []; content.forEach((el) => { parameters.push(...el.content.parameters); extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); @@ -116,7 +116,7 @@ export class ClassDefinition { .then((data) => data); } - public async getMemberLocation(name: string): Promise { + public async getMemberLocation(name: string): Promise { let pattern; if (name.startsWith("#")) { pattern = `(Parameter) ${name.substr(1)}(?=[( ;])`; diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 922f2970..3450fbc9 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -17,7 +17,6 @@ import { import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; import { compile, importFile } from "../commands/compile"; -import { sendClientSideSyncTelemetryEvent } from "../extension"; interface WSFolderIndex { /** The `FileSystemWatcher` for this workspace folder */ @@ -52,7 +51,7 @@ async function getCurrentFile( uri: vscode.Uri, forceText = false, content?: string[] | Buffer -): Promise { +): Promise { if (content) { // forceText is always true when content is passed return currentFileFromContent(uri, Buffer.isBuffer(content) ? textDecoder.decode(content) : content.join("\n")); @@ -191,10 +190,11 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // Index classes and routines that currently exist vscode.workspace.findFiles(new vscode.RelativePattern(wsFolder, "{**/*}")).then((files) => files.forEach((file) => - fsRateLimiter.call(() => { + fsRateLimiter.call(async () => { if (isClassOrRtn(file.path) || isImportableLocalFile(file)) { return updateIndexInternal(file, documents, uris, true); } + return undefined; }) ) ); @@ -217,7 +217,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (notToSync(uri)) { return; } - if (!uri.path.split("/").pop().includes(".")) { + if (!uri.path.split("/").pop()!.includes(".")) { // Ignore creation and change events for folders return; } @@ -254,7 +254,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr } const api = new AtelierAPI(uri); const conf = vscode.workspace.getConfiguration("objectscript", wsFolder); - const syncLocalChanges: string = conf.get("syncLocalChanges"); + const syncLocalChanges: string = conf.get("syncLocalChanges")!; const vscodeChange = touchedByVSCode.has(uriString); const sync = api.active && (syncLocalChanges == "all" || (syncLocalChanges == "vscodeOnly" && vscodeChange)); touchedByVSCode.delete(uriString); @@ -263,8 +263,8 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (change.addedOrChanged) { // Create or update the document on the server try { - const willCompile = conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name); - await importFile(change.addedOrChanged, willCompile); + const willCompile = conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name); + await importFile(change.addedOrChanged, willCompile!); outputImport(change.addedOrChanged.name, uri); if (willCompile) { // Compile right away if this document is in the active text editor. @@ -293,7 +293,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr const api = new AtelierAPI(uri); const syncLocalChanges: string = vscode.workspace .getConfiguration("objectscript", wsFolder) - .get("syncLocalChanges"); + .get("syncLocalChanges")!; const sync: boolean = api.active && (syncLocalChanges == "all" || (syncLocalChanges == "vscodeOnly" && touchedByVSCode.has(uriString))); for (const subUriString of uris.keys()) { @@ -351,9 +351,6 @@ async function updateIndexInternal( const file = await getCurrentFile(uri, true, content); if (!file) return result; result.addedOrChanged = file; - if (isImportableLocalFile(uri) && sync) { - sendClientSideSyncTelemetryEvent(file.fileName.split(".").pop().toLowerCase()); - } const documentUris = documents.get(file.name) ?? []; if (documentUris.some((u) => u.toString() == uriString)) { // No need to update the index since this document is already present @@ -424,8 +421,10 @@ export function allDocumentsInWorkspace(wsFolder: vscode.WorkspaceFolder): strin } /** Get the class/routine name of the document in `uri` */ -export function getDocumentForUri(uri: vscode.Uri): string { - return wsFolderIndex.get(vscode.workspace.getWorkspaceFolder(uri)?.uri.toString())?.uris.get(uri.toString()); +export function getDocumentForUri(uri: vscode.Uri): string | undefined { + return wsFolderIndex + .get(vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() as string) + ?.uris.get(uri.toString()); } /** @@ -444,7 +443,7 @@ export function inferDocName(uri: vscode.Uri): string | undefined { const wsFolder = vscode.workspace.getWorkspaceFolder(uri); if (!wsFolder) return; const index = wsFolderIndex.get(wsFolder.uri.toString()); - if (!index || !index.uris.size) return; + if (!index?.uris.size) return; // Get a list of all unique paths containing classes or routines that // do not contribute to the name of the documents contained within const containingPaths: Set = new Set(); @@ -468,7 +467,7 @@ export function inferDocName(uri: vscode.Uri): string | undefined { // is necessary for the rare situaions for documents in /foo/bar/ have a different mapping than /foo/ // and the target URI is in /foo/bar/ or a subfolder of it. const containingPathsSorted = Array.from(containingPaths).sort((a, b) => b.length - a.length); - let result: string; + let result: string | undefined; for (const prefix of containingPathsSorted) { if (uri.path.startsWith(prefix)) { // We've identified the leading path segments that don't contribute to the document @@ -496,7 +495,7 @@ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): const docExt = docName.slice(-4).toLowerCase(); if (!exts.includes(docExt)) return; const index = wsFolderIndex.get(wsFolder.uri.toString()); - if (!index || !index.uris.size) return; + if (!index?.uris.size) return; const docNameNoExt = docName.slice(0, -4); // remove extension const docPkgSegments = docNameNoExt.split(".").slice(0, -1); // remove class/routine name // For each indexed document, compute its containing path and measure how @@ -532,3 +531,27 @@ export function inferDocUri(docName: string, wsFolder: vscode.WorkspaceFolder): // Convert the document name to a file path and prepend the prefix return wsFolder.uri.with({ path: `${bestPathPrefix}${docNameNoExt.replaceAll(".", "/")}${docExt}` }); } + +/** + * Returns the `Uri`s of all indexed documents in folder `uri`. + * Assumes `uri` is a folder URI. Returns documents in nested folders. + */ +export function urisInFolder(uri: vscode.Uri): vscode.Uri[] { + const wsFolder = vscode.workspace.getWorkspaceFolder(uri); + if (!wsFolder) return []; + const index = wsFolderIndex.get(wsFolder.uri.toString()); + if (!index?.uris.size) return []; + // Need to round-trip the argument through the Uri parser + // because it can change the case of Windows drive letters + // and the check below is case-sensitive. I found this + // preferable to adding a Windows-only case-insensitive + // carve-out in the logic below. + let folderPath = vscode.Uri.parse(uri.toString()).path; + if (!folderPath.endsWith("/")) folderPath = `${folderPath}/`; + const result: vscode.Uri[] = []; + index.uris.forEach((indexDocName, indexDocUriStr) => { + const indexDocUri = vscode.Uri.parse(indexDocUriStr); + if (indexDocUri.path.startsWith(folderPath)) result.push(indexDocUri); + }); + return result; +} diff --git a/src/utils/documentPicker.ts b/src/utils/documentPicker.ts index 3f8a4f77..cca9b28e 100644 --- a/src/utils/documentPicker.ts +++ b/src/utils/documentPicker.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../api"; -import { cspAppsForApi, handleError } from "."; +import { cspAppsForApi, handleError, notIsfs } from "."; +import { isfsConfig } from "./FileProviderUtil"; interface DocumentPickerItem extends vscode.QuickPickItem { /** The full name of this item, including its parent(s). */ @@ -38,7 +39,7 @@ function createMultiSelectItem( delim = "/"; } result.fullName = parent + delim + item.Name; - result.label = " ".repeat(parentPad + 2) + result.label; + result.label = " ".repeat(parentPad! + 2) + result.label; result.description = result.fullName; } if (item.Type == 9 || item.Type == 10) { @@ -53,31 +54,23 @@ function createMultiSelectItem( return result; } -function createSingleSelectItem( - item: { Name: string; Type: number }, - parent?: string, - delimiter?: string -): DocumentPickerItem { - const result: DocumentPickerItem = { label: item.Name, fullName: item.Name }; - // Add the icon +function createSingleSelectItem(item: { Name: string; Type: number }): vscode.QuickPickItem { + // Determine the icon + let icon = "$(symbol-file)"; if (item.Type == 0) { if (item.Name.endsWith(".inc")) { - result.label = "$(file-symlink-file) " + result.label; + icon = "$(file-symlink-file)"; } else if (item.Name.endsWith(".int") || item.Name.endsWith(".mac")) { - result.label = "$(note) " + result.label; + icon = "$(note)"; } else { - result.label = "$(symbol-misc) " + result.label; + icon = "$(symbol-misc)"; } } else if (item.Type == 4) { - result.label = "$(symbol-class) " + result.label; - } else if (![9, 10].includes(item.Type)) { - result.label = "$(symbol-file) " + result.label; - } - if (parent) { - // Update the full name if this is a nested item - result.fullName = parent + delimiter + item.Name; + icon = "$(symbol-class)"; + } else { + icon = "$(symbol-file)"; } - return result; + return { label: `${icon} ${item.Name}` }; } /** @@ -200,11 +193,11 @@ export async function pickDocuments(api: AtelierAPI, prompt?: string): Promise i.fullName === event.item.fullName); - if (event.button.tooltip.charAt(0) == "E") { + if (event.button.tooltip!.charAt(0) == "E") { // Expand this item expandItem(itemIdx); } else { @@ -240,8 +233,8 @@ export async function pickDocuments(api: AtelierAPI, prompt?: string): Promise { +): Promise { let sys: "0" | "1" = "0"; let gen: "0" | "1" = "0"; let map: "0" | "1" = "1"; - const query = "SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,?,0,0,?,,0,?)"; - const webApps = (typeSuffix ?? "csp") == "csp" ? cspAppsForApi(api) : []; - const webAppRootItems = webApps.map((app: string) => { - return { - label: app, - fullName: app, - }; - }); + const query = "SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,?,1,0,?,,0,?)"; + // Only show web app files in the list if we're in a server-side web app folder. + // Hiding these files in other cases will improve performance. + const showWeb = typeof api.wsOrFile == "object" && !notIsfs(api.wsOrFile) && isfsConfig(api.wsOrFile).csp; - return new Promise((resolve) => { - const quickPick = vscode.window.createQuickPick(); + return new Promise((resolve) => { + const quickPick = vscode.window.createQuickPick(); quickPick.prompt = - "Select a package or folder to view its contents. Selecting '..' shows the previous level's contents. You may also type a full document name into the filter box and press 'Enter' to select that document."; + "You may also type a full document name with extension into the filter box and press 'Enter' to select it."; quickPick.title = `${prompt ? prompt : "Select a document"} ${numberOfSteps ? `(${step}/${numberOfSteps})` : `in namespace '${api.ns}' on server '${api.serverId}'`}`; quickPick.ignoreFocusOut = true; quickPick.buttons = [ @@ -337,32 +326,23 @@ export async function pickDocument( }, ]; - const getRootItems = (): Promise => { + const getItems = (): Promise => { + quickPick.busy = true; return api - .actionQuery(`${query} WHERE Type != 5 AND Type != 10`, [ - typeSuffix ? `*.${typeSuffix}` : "*,'*.prj", + .actionQuery(query, [ + typeSuffix + ? `*.${typeSuffix}` + : showWeb + ? "*,'*.prj,'*.bpl,'*.dtl" + : "*.cls,*.mac,*.int,*.inc,*.other,'*.bpl,'*.dtl", sys, gen, map, ]) .then((data) => { - const rootitems: DocumentPickerItem[] = data.result.content.map((i) => createSingleSelectItem(i)); - const findLastIndex = (): number => { - let l = rootitems.length; - while (l--) { - if (!rootitems[l].label.startsWith("$(")) return l; - } - return -1; - }; - rootitems.splice(findLastIndex() + 1, 0, ...webAppRootItems); - return rootitems; - }) - .then((items) => { - quickPick.items = items; + quickPick.items = data.result.content.map((i) => createSingleSelectItem(i)); quickPick.selectedItems = []; - quickPick.value = ""; quickPick.busy = false; - quickPick.enabled = true; }) .catch((error) => { quickPick.hide(); @@ -371,122 +351,93 @@ export async function pickDocument( }; quickPick.onDidTriggerButton((button) => { - quickPick.busy = true; - quickPick.enabled = false; if (button === vscode.QuickInputButtons.Back) { resolve(""); // signal "go back" to the caller quickPick.hide(); } if (button.tooltip == "System") { - sys = button.toggle.checked ? "1" : "0"; + sys = button.toggle!.checked ? "1" : "0"; } else if (button.tooltip == "Generated") { - gen = button.toggle.checked ? "1" : "0"; + gen = button.toggle!.checked ? "1" : "0"; } else { - map = button.toggle.checked ? "1" : "0"; + map = button.toggle!.checked ? "1" : "0"; } // Refresh the items list - getRootItems(); + getItems(); }); quickPick.onDidAccept(() => { quickPick.busy = true; quickPick.enabled = false; const item = quickPick.selectedItems[0]; - if (!item || item.label.startsWith("$(")) { - let doc = item?.fullName ?? quickPick.value.trim(); - if (!item) { - // The document name came from the value text, so validate it first - // Normalize the file extension case for classes and routines - doc = [".cls", ".mac", ".int", ".inc"].includes(doc.slice(-4).toLowerCase()) - ? doc.slice(0, -3) + doc.slice(-3).toLowerCase() - : doc; - // Expand the short form of %Library classes to the long form - doc = - doc.startsWith("%") && doc.split(".").length == 2 && doc.slice(-4) == ".cls" - ? `%Library.${doc.slice(1)}` - : doc; - if (doc.endsWith(".cls")) { - // Use StudioOpenDialog for classes so we don't expose Hidden ones - api - .actionQuery("SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,1,1,0,1,,0,1)", [doc]) - .then((data) => { - if (data.result.content?.length) { - // doc is the name of a class that exists and is visible by the user - resolve(doc); - } else { - vscode.window.showErrorMessage( - `Class '${doc.slice(0, -4)}' does not exist, or is Hidden.`, - "Dismiss" - ); - resolve(undefined); - } - }) - .catch(() => { - vscode.window.showErrorMessage( - `Internal Server Error encountered trying to validate class name '${doc.slice(0, -4)}'.`, - "Dismiss" - ); - resolve(undefined); - }) - .finally(() => quickPick.hide()); - } else { - api - .headDoc(doc) - .then(() => resolve(doc)) - .catch((error) => { - vscode.window.showErrorMessage( - error?.statusCode == 400 - ? `'${doc}' is an invalid document name.` - : error?.statusCode == 404 - ? `Document '${doc}' does not exist.` - : `Internal Server Error encountered trying to validate document '${doc}'.`, - "Dismiss" - ); - resolve(undefined); - }) - .finally(() => quickPick.hide()); - } - } else { - // The document name came from an item so no validation is required - resolve(doc); - quickPick.hide(); + let doc = item?.label.slice(item?.label.indexOf(" ") + 1) ?? quickPick.value.trim(); + if (!item) { + if (typeSuffix && !new RegExp(`\\.${typeSuffix}$`, "i").test(doc)) { + // The text the user entered does not match the required type + vscode.window.showErrorMessage( + `Selected document '${doc}' does not have required extension '${typeSuffix}'.`, + "Dismiss" + ); + resolve(undefined); } - } else { - // Replace the items with the folder's contents - if (item.fullName == "") { - getRootItems(); - } else { + // The document name came from the value text, so validate it first + // Normalize the file extension case for classes and routines + doc = [".cls", ".mac", ".int", ".inc"].includes(doc.slice(-4).toLowerCase()) + ? doc.slice(0, -3) + doc.slice(-3).toLowerCase() + : doc; + // Expand the short form of %Library classes to the long form + doc = + doc.startsWith("%") && doc.split(".").length == 2 && doc.slice(-4) == ".cls" + ? `%Library.${doc.slice(1)}` + : doc; + if (doc.endsWith(".cls")) { + // Use StudioOpenDialog for classes so we don't expose Hidden ones api - .actionQuery(query, [`${item.fullName}/*${typeSuffix ? `.${typeSuffix}` : ""}`, sys, gen, map]) + .actionQuery("SELECT Name, Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,1,1,0,1,,0,1)", [doc]) .then((data) => { - const delim = item.fullName.includes("/") ? "/" : "."; - const newItems: DocumentPickerItem[] = data.result.content.map((i) => - createSingleSelectItem(i, item.fullName, delim) + if (data.result.content?.length) { + // doc is the name of a class that exists and is visible by the user + resolve(doc); + } else { + vscode.window.showErrorMessage(`Class '${doc.slice(0, -4)}' does not exist, or is Hidden.`, "Dismiss"); + resolve(undefined); + } + }) + .catch(() => { + vscode.window.showErrorMessage( + `Internal Server Error encountered trying to validate class name '${doc.slice(0, -4)}'.`, + "Dismiss" ); - let parentFullName = - delim == "/" && webApps.includes(item.fullName) - ? "" - : item.fullName.split(delim).slice(0, -1).join(delim); - if (parentFullName == "/") parentFullName = ""; - quickPick.items = [{ label: "..", fullName: parentFullName }].concat(newItems); - quickPick.value = ""; - quickPick.selectedItems = []; - quickPick.busy = false; - quickPick.enabled = true; + resolve(undefined); }) + .finally(() => quickPick.hide()); + } else { + api + .headDoc(doc) + .then(() => resolve(doc)) .catch((error) => { - quickPick.hide(); - handleError(error, "Failed to get namespace contents."); - }); + vscode.window.showErrorMessage( + error?.statusCode == 400 + ? `'${doc}' is an invalid document name.` + : error?.statusCode == 404 + ? `Document '${doc}' does not exist.` + : `Internal Server Error encountered trying to validate document '${doc}'.`, + "Dismiss" + ); + resolve(undefined); + }) + .finally(() => quickPick.hide()); } + } else { + // The document name came from an item so no validation is required + resolve(doc); + quickPick.hide(); } }); quickPick.onDidHide(() => { resolve(undefined); quickPick.dispose(); }); - quickPick.busy = true; - quickPick.enabled = false; quickPick.show(); - getRootItems(); + getItems(); }); } diff --git a/src/utils/index.ts b/src/utils/index.ts index ba7ee6cc..ad944d9e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -171,10 +171,10 @@ function otherDocExtsForUri(uri: vscode.Uri): string[] { } /** Determine the server name of a non-`isfs` non-ObjectScript file (any file that's not CLS,MAC,INT,INC). */ -export function getServerDocName(uri: vscode.Uri): string { +export function getServerDocName(uri: vscode.Uri): string | undefined { const wsFolder = vscode.workspace.getWorkspaceFolder(uri); if (!wsFolder) return; - const cspIdx = uri.path.lastIndexOf(cspAppsForUri(uri).find((cspApp) => uri.path.includes(cspApp + "/"))); + const cspIdx = uri.path.lastIndexOf(cspAppsForUri(uri).find((cspApp) => uri.path.includes(cspApp + "/"))!); if (cspIdx != -1) { return uri.path.slice(cspIdx); } else if (uri.path.toLowerCase().endsWith(".dfi")) { @@ -184,8 +184,8 @@ export function getServerDocName(uri: vscode.Uri): string { if (relativeFilePath == "") return; // uri isn't in the workspace folder. Should never happen. // Check for matching export settings first. If no match, use base name. const config = vscode.workspace.getConfiguration("objectscript.export", uri); - const folder: string = config.get("folder"); - const addCategory: boolean = config.get("addCategory"); + const folder: string = config.get("folder")!; + const addCategory: boolean = config.get("addCategory")!; let root = [ typeof folder == "string" && folder.length ? folder : null, addCategory ? getCategory(uri.fsPath, addCategory) : null, @@ -218,7 +218,7 @@ export function isImportableLocalFile(uri: vscode.Uri): boolean { if (!vscode.workspace.getWorkspaceFolder(uri)) return false; return ( cspAppsForUri(uri).some((cspApp) => uri.path.includes(cspApp + "/")) || - otherDocExtsForUri(uri).includes(uri.path.split(".").pop().toLowerCase()) + otherDocExtsForUri(uri).includes(uri.path.split(".").pop()!.toLowerCase()) ); } @@ -228,14 +228,17 @@ export const classNameRegex = /^[ \t]*Class[ \t]+(%?[\p{L}\d\u{100}-\u{ffff}]+(? /** A regex for extracting the name and type of a routine from its content */ export const routineNameTypeRegex = /^ROUTINE ([^\s]+)(?:\s*\[\s*Type\s*=\s*\b([a-z]{3})\b)?/i; -export function currentFileFromContent(uri: vscode.Uri, content: string | Buffer): CurrentTextFile | CurrentBinaryFile { +export function currentFileFromContent( + uri: vscode.Uri, + content: string | Buffer +): CurrentTextFile | CurrentBinaryFile | null { const fileName = uri.fsPath; const workspaceFolder = workspaceFolderOfUri(uri); if (!workspaceFolder) { // No workspace folders are open return null; } - const fileExt = fileName.split(".").pop().toLowerCase(); + const fileExt = fileName.split(".").pop()!.toLowerCase(); if ( notIsfs(uri) && !isClassOrRtn(uri.path) && @@ -258,7 +261,7 @@ export function currentFileFromContent(uri: vscode.Uri, content: string | Buffer [, name, ext = "mac"] = match; } } else { - name = notIsfs(uri) ? getServerDocName(uri) : isfsDocumentName(uri); + name = notIsfs(uri) ? getServerDocName(uri)! : isfsDocumentName(uri); } if (!name) { return null; @@ -288,7 +291,7 @@ export function currentFileFromContent(uri: vscode.Uri, content: string | Buffer } } -export function currentFile(document?: vscode.TextDocument): CurrentTextFile { +export function currentFile(document?: vscode.TextDocument | null): CurrentTextFile | null { document = document || (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document @@ -298,7 +301,7 @@ export function currentFile(document?: vscode.TextDocument): CurrentTextFile { return null; } const fileName = document.fileName; - const fileExt = fileName.split(".").pop().toLowerCase(); + const fileExt = fileName.split(".").pop()!.toLowerCase(); if ( notIsfs(document.uri) && !isClassOrRtn(document.uri.path) && @@ -324,7 +327,7 @@ export function currentFile(document?: vscode.TextDocument): CurrentTextFile { [, name, ext = "mac"] = match; } } else { - name = notIsfs(uri) ? getServerDocName(uri) : isfsDocumentName(uri); + name = notIsfs(uri) ? getServerDocName(uri)! : isfsDocumentName(uri); } if (!name) { return null; @@ -385,7 +388,7 @@ export function connectionTarget(uri?: vscode.Uri): ConnectionTarget { result.configName = parts.length === 2 ? parts[0] : firstFolder.uri.authority; result.apiTarget = firstFolder.uri; } else { - result.configName = workspaceState.get("workspaceFolder") || firstFolder ? firstFolder.name : ""; + result.configName = workspaceState.get("workspaceFolder") || firstFolder ? firstFolder!.name : ""; result.apiTarget = result.configName; } } @@ -411,7 +414,7 @@ export function currentWorkspaceFolder(document?: vscode.TextDocument): string { if (firstFolder && schemas.includes(firstFolder.uri.scheme)) { return firstFolder.uri.authority; } else { - return workspaceState.get("workspaceFolder") || firstFolder ? firstFolder.name : ""; + return workspaceState.get("workspaceFolder") || firstFolder ? firstFolder!.name : ""; } } @@ -426,7 +429,7 @@ export function workspaceFolderOfUri(uri: vscode.Uri): string { return vscode.workspace.getWorkspaceFolder(uri)?.name ?? ""; } else { const rootUri = uri.with({ path: "/" }).toString(); - const foundFolder = vscode.workspace.workspaceFolders.find( + const foundFolder = vscode.workspace.workspaceFolders!.find( (workspaceFolder) => workspaceFolder.uri.toString() == rootUri ); return foundFolder ? foundFolder.name : uri.authority; @@ -456,7 +459,7 @@ export function notNull(el: any): boolean { return el !== null; } -/** Determine the compose command to use (`docker-compose` or `docker compose`). */ +/** Determine the compose command to use (`docker-compose`, `docker compose`, `podman compose`, or `podman-compose`). */ async function composeCommand(cwd?: string): Promise { return new Promise((resolve) => { let cmd = "docker compose"; @@ -465,11 +468,89 @@ async function composeCommand(cwd?: string): Promise { // 'docker compose' is not present, so default to 'docker-compose' cmd = "docker-compose"; } - resolve(cmd); + exec(`${cmd} version`, { cwd }, (error) => { + if (error) { + // Neither Docker Compose variant is present; try Podman Compose. 'podman compose' can locate a + // provider that a plain PATH lookup for 'podman-compose' wouldn't, so try it first. + cmd = "podman compose"; + } + exec(`${cmd} version`, { cwd }, (error) => { + if (error) { + cmd = "podman-compose"; + } + resolve(cmd); + }); + }); }); }); } +/** + * Podman's compose tooling doesn't support the same `ps --services --filter status=running` and + * `port` output format as Docker Compose (bare port number instead of a host:port pair, and no + * `--services`/`--filter` flags), so it needs its own port/running-state resolution. + */ +async function portFromPodmanCompose( + cmd: string, + cwd: string, + file: string, + service: string, + internalPort: number, + internalSuperserverPort: number, + dockerCompose: { internalSuperserverPort?: number }, + result: { port: number | null; superserverPort: number | null; docker: boolean; service?: string } +): Promise { + const ps = await run("ps --format json"); + let containers: { State?: string; Labels?: Record }[] = []; + try { + containers = JSON.parse(ps); + } catch { + // Leave containers empty; treated as not running below + } + if (!containers.some((c) => c.State === "running" && c.Labels?.["com.docker.compose.service"] === service)) { + throw `Service '${service}' not found in '${path.join(cwd, file)}', or not running.`; + } + const port = parsePort(await run(`port --protocol=tcp ${service} ${internalPort}`)); + if (!port) { + throw `Webserver port ${internalPort} not published for service '${service}' in '${path.join(cwd, file)}'.`; + } + result.port = parseInt(port, 10); + let superserverPort: string | undefined; + try { + superserverPort = parsePort(await run(`port --protocol=tcp ${service} ${internalSuperserverPort}`)); + } catch (error) { + // Not an error if we were merely looking for the default port and the container doesn't publish it + if (!dockerCompose.internalSuperserverPort) return result; + throw error; + } + if (!superserverPort) { + throw `Superserver port ${internalSuperserverPort} not published for service '${service}' in '${path.join(cwd, file)}'.`; + } + result.superserverPort = parseInt(superserverPort, 10); + return result; + + // Runs a compose subcommand and returns its stdout, rejecting with a plain string on failure. + function run(args: string): Promise { + return new Promise((resolve, reject) => { + exec(`${cmd} ${args}`, { cwd }, (error, stdout) => { + if (error) { + reject(error.message); + return; + } + resolve(stdout); + }); + }); + } + + function parsePort(stdout: string) { + return stdout + .trim() + .split("\n") + .pop() + ?.match(/(\d+)$/)?.[1]; + } +} + export async function portFromDockerCompose( workspaceFolderName?: string ): Promise<{ port: number | null; superserverPort: number | null; docker: boolean; service?: string }> { @@ -491,14 +572,19 @@ export async function portFromDockerCompose( return { docker: false, port: null, superserverPort: null }; } - const result = { port: null, superserverPort: null, docker: true, service }; + const result: { port: number | null; superserverPort: number | null; docker: boolean; service?: string } = { + port: null, + superserverPort: null, + docker: true, + service, + }; const workspaceFolder = uriOfWorkspaceFolder(workspaceFolderName); if (!workspaceFolder) { // No workspace folders are open return { docker: false, port: null, superserverPort: null }; } const workspaceFolderPath = workspaceFolder.fsPath; - const workspaceRootPath = vscode.workspace.workspaceFolders[0].uri.fsPath; + const workspaceRootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; const cwd: string = await fileExists(vscode.Uri.file(path.join(workspaceFolderPath, file))).then(async (exists) => { if (exists) { @@ -521,6 +607,10 @@ export async function portFromDockerCompose( const envFileParam = envFile ? `--env-file ${envFile}` : ""; const cmd = `${await composeCommand(cwd)} -f ${file} ${envFileParam} `; + if (cmd.startsWith("podman compose ") || cmd.startsWith("podman-compose ")) { + return portFromPodmanCompose(cmd, cwd, file, service, internalPort, internalSuperserverPort, dockerCompose, result); + } + return new Promise((resolve, reject) => { exec(`${cmd} ps --services --filter status=running`, { cwd }, (error, stdout) => { if (error) { @@ -572,7 +662,7 @@ export async function terminalWithDocker(): Promise { if (!terminal) { let exe = await composeCommand(); const argsArr: string[] = []; - if (exe == "docker compose") { + if (exe.includes(" ")) { const exeSplit = exe.split(" "); exe = exeSplit[0]; argsArr.push(exeSplit[1]); @@ -607,7 +697,7 @@ export async function shellWithDocker(): Promise { if (!terminal) { let exe = await composeCommand(); const argsArr: string[] = []; - if (exe == "docker compose") { + if (exe.includes(" ")) { const exeSplit = exe.split(" "); exe = exeSplit[0]; argsArr.push(exeSplit[1]); @@ -631,7 +721,9 @@ interface WSServerRootFolderData { const wsServerRootFolders = new Map(); /** Cache information about redirection of `.vscode` folder contents for server-side folders */ -export async function addWsServerRootFolderData(wsFolders: readonly vscode.WorkspaceFolder[]): Promise { +export async function addWsServerRootFolderData( + wsFolders: readonly vscode.WorkspaceFolder[] | undefined +): Promise { if (!wsFolders?.length) return; return Promise.allSettled( wsFolders.map(async (wsFolder) => { @@ -662,10 +754,10 @@ export async function addWsServerRootFolderData(wsFolders: readonly vscode.Works webApps = await api .getCSPApps(false, "%SYS") .then((data) => data.result.content ?? []) - .catch(() => []); - cspApps.set(key, webApps); + .catch((): string[] => []); + cspApps.set(key, webApps!); } - value.canRedirectDotvscode = webApps.includes("/_vscode"); + value.canRedirectDotvscode = webApps!.includes("/_vscode"); } wsServerRootFolders.set(wsFolder.uri.toString(), value); }) @@ -772,7 +864,7 @@ export function parseClassMemberDefinition( document: vscode.TextDocument, symbol: vscode.DocumentSymbol, symbolLine?: number -): { definition: string; defEndLine: number; language: string; isPrivate: boolean } { +): { definition: string; defEndLine: number; language: string; isPrivate: boolean } | undefined { const languageServer: boolean = vscode.extensions.getExtension(lsExtensionId)?.isActive ?? false; if (symbolLine == undefined) { if (languageServer) { @@ -786,9 +878,9 @@ export function parseClassMemberDefinition( } } } - let definition: string; - let defEndLine: number; - for (let defLine = symbolLine; defLine < document.lineCount; defLine++) { + let definition: string | undefined; + let defEndLine: number | undefined; + for (let defLine = symbolLine!; defLine < document.lineCount; defLine++) { const line = document.lineAt(defLine); if (line.text.trimEnd().endsWith("{")) { definition = document.getText( @@ -804,7 +896,7 @@ export function parseClassMemberDefinition( const privateMatch = definitionNoDelimitedValues.match(privateRegex); return { definition, - defEndLine, + defEndLine: defEndLine!, language: languageMatch && languageMatch[1] ? languageMatch[1].toLowerCase() : "objectscript", isPrivate: privateMatch != null, }; @@ -838,10 +930,10 @@ export function methodOffsetToLine( method: string, offset = 0 ): number | undefined { - let line: number; + let line: number | undefined; const languageServer: boolean = vscode.extensions.getExtension(lsExtensionId)?.isActive ?? false; // Find the DocumentSymbol for this method - let currentSymbol: vscode.DocumentSymbol; + let currentSymbol: vscode.DocumentSymbol | undefined; for (const symbol of members) { if (stripClassMemberNameQuotes(symbol.name) === method && symbol.detail.toLowerCase().includes("method")) { currentSymbol = symbol; @@ -871,11 +963,6 @@ export function methodOffsetToLine( return line; } -/** Return `true` if this username signals unauthenticated access */ -export function isUnauthenticated(username: string): boolean { - return username == undefined || username == "" || username.toLowerCase() == "unknownuser"; -} - /** Returns `true` if `uri.scheme` is neither `isfs` nor `isfs-readonly` */ export function notIsfs(uri: vscode.Uri): boolean { return !filesystemSchemas.includes(uri.scheme); @@ -886,7 +973,7 @@ export function base64EncodeContent(content: Buffer): string[] { // Output is 4 chars for each 3 input, so 24573/3*4 = 32764 const chunkSize = 24573; let start = 0; - const result = []; + const result: string[] = []; while (start < content.byteLength) { result.push(content.toString("base64", start, start + chunkSize)); start += chunkSize; @@ -896,12 +983,12 @@ export function base64EncodeContent(content: Buffer): string[] { /** Returns `true` if `uri` has a class file extension */ export function isClass(uriOrName: string): boolean { - return "cls" == uriOrName.split(".").pop().toLowerCase(); + return "cls" == uriOrName.split(".").pop()!.toLowerCase(); } /** Returns `true` if `uri` has a class or routine file extension */ export function isClassOrRtn(uriOrName: string): boolean { - return ["cls", "mac", "int", "inc"].includes(uriOrName.split(".").pop().toLowerCase()); + return ["cls", "mac", "int", "inc"].includes(uriOrName.split(".").pop()!.toLowerCase()); } interface ConnQPItem extends vscode.QuickPickItem { @@ -925,10 +1012,10 @@ export async function getWsServerConnection(minVersion?: string): Promise(fn: () => Promise): Promise { + async call(fn: () => Promise | undefined): Promise { // Acquire a slot in the semaphore. Will not reject. await this._semaphore.acquire(); try { // Execute the provided function - return await fn(); + return (await fn())!; } finally { // Always release the slot in the semaphore after the function completes this._semaphore.release(); diff --git a/tsconfig.base.json b/tsconfig.base.json index 0a87024f..583acc95 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,6 +5,7 @@ "noUnusedLocals": true, "noUnusedParameters": false, "experimentalDecorators": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "strictNullChecks": true, } }