diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 5821317c3..a3778250a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,7 +13,7 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ RUN mkdir -p /usr/local/nvm ENV NVM_DIR /usr/local/nvm -ENV NODE_VERSION v18.19.0 +ENV NODE_VERSION v20.18.2 RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION" ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/bin diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6b96f71bb..fba366480 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -27,7 +27,7 @@ "extensions": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", - "vadimcn.vscode-lldb" + "llvm-vs-code-extensions.lldb-dap" ] } }, diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 42220e111..999f93b1f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,14 +16,15 @@ jobs: uses: actions/checkout@v4 - name: Build Extension run: | - export NODE_VERSION=v18.19.0 - export NODE_PATH=/usr/local/nvm/versions/node/v18.19.0/bin + export NODE_VERSION=v20.18.2 + export NODE_PATH=/usr/local/nvm/versions/node/v20.18.2/bin export NVM_DIR=/usr/local/nvm . .github/workflows/scripts/setup-linux.sh [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" npm ci npm run compile npm run package + npm run preview-package - name: Archive production artifacts uses: actions/upload-artifact@v4 if: always() @@ -37,8 +38,8 @@ jobs: with: # Linux linux_env_vars: | - NODE_VERSION=v18.19.0 - NODE_PATH=/usr/local/nvm/versions/node/v18.19.0/bin + NODE_VERSION=v20.18.2 + NODE_PATH=/usr/local/nvm/versions/node/v20.18.2/bin NVM_DIR=/usr/local/nvm CI=1 linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh @@ -46,7 +47,6 @@ jobs: # Windows windows_env_vars: | CI=1 - VSCODE_TEST=1 windows_pre_build_command: .github\workflows\scripts\windows\install-nodejs.ps1 windows_build_command: scripts\test_windows.ps1 enable_windows_docker: false @@ -56,21 +56,19 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: # Linux - linux_exclude_swift_versions: '[{"swift_version": "5.8"}, {"swift_version": "5.9"}, {"swift_version": "5.10"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly-main"}]' + linux_exclude_swift_versions: '[{"swift_version": "5.8"}, {"swift_version": "5.9"}, {"swift_version": "5.10"}, {"swift_version": "6.0"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly-main"}]' linux_env_vars: | - NODE_VERSION=v18.19.0 - NODE_PATH=/usr/local/nvm/versions/node/v18.19.0/bin + NODE_VERSION=v20.18.2 + NODE_PATH=/usr/local/nvm/versions/node/v20.18.2/bin NVM_DIR=/usr/local/nvm CI=1 - VSCODE_TEST=1 VSCODE_VERSION=insiders linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh linux_build_command: ./scripts/test.sh # Windows - windows_exclude_swift_versions: '[{"swift_version": "5.9"}, {"swift_version": "nightly-6.0"}, {"swift_version": "nightly"}]' + windows_exclude_swift_versions: '[{"swift_version": "5.9"}, {"swift_version": "6.0"}, {"swift_version": "nightly-6.1"}, {"swift_version": "nightly"}]' windows_env_vars: | CI=1 - VSCODE_TEST=1 VSCODE_VERSION=insiders windows_pre_build_command: .github\workflows\scripts\windows\install-nodejs.ps1 windows_build_command: scripts\test_windows.ps1 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9e37c43e8..e71060c70 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,19 +12,17 @@ jobs: # Linux linux_exclude_swift_versions: '[{"swift_version": "nightly-6.1"},{"swift_version": "nightly-main"}]' linux_env_vars: | - NODE_VERSION=v18.19.0 - NODE_PATH=/usr/local/nvm/versions/node/v18.19.0/bin + NODE_VERSION=v20.18.2 + NODE_PATH=/usr/local/nvm/versions/node/v20.18.2/bin NVM_DIR=/usr/local/nvm CI=1 - VSCODE_TEST=1 FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}} linux_pre_build_command: . .github/workflows/scripts/setup-linux.sh linux_build_command: ./scripts/test.sh # Windows - windows_exclude_swift_versions: '[{"swift_version": "nightly-6.0"},{"swift_version": "nightly"}]' + windows_exclude_swift_versions: '[{"swift_version": "nightly-6.1"},{"swift_version": "nightly"}]' windows_env_vars: | CI=1 - VSCODE_TEST=1 FAST_TEST_RUN=${{ contains(github.event.pull_request.labels.*.name, 'full-test-run') && '0' || '1'}} windows_pre_build_command: .github\workflows\scripts\windows\install-nodejs.ps1 windows_build_command: scripts\test_windows.ps1 diff --git a/.github/workflows/scripts/setup-linux.sh b/.github/workflows/scripts/setup-linux.sh index 82e70d523..6d660d94e 100755 --- a/.github/workflows/scripts/setup-linux.sh +++ b/.github/workflows/scripts/setup-linux.sh @@ -13,8 +13,8 @@ ## ##===----------------------------------------------------------------------===## -export NODE_VERSION=v18.19.0 -export NODE_PATH=/usr/local/nvm/versions/node/v18.19.0/bin +export NODE_VERSION=v20.18.2 +export NODE_PATH=/usr/local/nvm/versions/node/v20.18.2/bin export NVM_DIR=/usr/local/nvm apt-get update && apt-get install -y rsync curl gpg libasound2 libgbm1 libgtk-3-0 libnss3 xvfb build-essential diff --git a/.github/workflows/scripts/windows/install-nodejs.ps1 b/.github/workflows/scripts/windows/install-nodejs.ps1 index 2105d63ad..f314cc473 100644 --- a/.github/workflows/scripts/windows/install-nodejs.ps1 +++ b/.github/workflows/scripts/windows/install-nodejs.ps1 @@ -1,4 +1,4 @@ -$NODEJS='https://nodejs.org/dist/v18.20.4/node-v18.20.4-x64.msi' +$NODEJS='https://nodejs.org/dist/v20.18.2/node-v20.18.2-x64.msi' $NODEJS_SHA256='c2654d3557abd59de08474c6dd009b1d358f420b8e4010e4debbf130b1dfb90a' Set-Variable ErrorActionPreference Stop Set-Variable ProgressPreference SilentlyContinue diff --git a/.gitignore b/.gitignore index a09ca1cb4..d42891036 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ default.profraw assets/documentation-webview assets/test/**/Package.resolved assets/swift-docc-render +ud +userdocs/userdocs.docc/.docc-build diff --git a/.nvmrc b/.nvmrc index 436d5c5d1..6263619f7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.0 \ No newline at end of file +20.18.2 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 42ce3550c..a7aad951d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,3 +18,6 @@ node_modules/ /coverage/ /dist/ /snippets/ + +# macOS CI +/ud/ \ No newline at end of file diff --git a/.vscode-test.js b/.vscode-test.js index 5fa7ce6ba..f244bd1c2 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -18,12 +18,28 @@ const path = require("path"); const isCIBuild = process.env["CI"] === "1"; const isFastTestRun = process.env["FAST_TEST_RUN"] === "1"; +const dataDir = process.env["VSCODE_DATA_DIR"]; + // "env" in launch.json doesn't seem to work with vscode-test const isDebugRun = !(process.env["_"] ?? "").endsWith("node_modules/.bin/vscode-test"); // so tests don't timeout when a breakpoint is hit const timeout = isDebugRun ? Number.MAX_SAFE_INTEGER : 3000; +const launchArgs = [ + "--disable-updates", + "--disable-crash-reporter", + "--disable-workspace-trust", + "--disable-telemetry", +]; +if (dataDir) { + launchArgs.push("--user-data-dir", dataDir); +} +// GPU hardware acceleration not working on Darwin for intel +if (process.platform === "darwin" && process.arch === "x64") { + launchArgs.push("--disable-gpu"); +} + module.exports = defineConfig({ tests: [ { @@ -31,12 +47,7 @@ module.exports = defineConfig({ files: ["dist/test/common.js", "dist/test/integration-tests/**/*.test.js"], version: process.env["VSCODE_VERSION"] ?? "stable", workspaceFolder: "./assets/test", - launchArgs: [ - "--disable-updates", - "--disable-crash-reporter", - "--disable-workspace-trust", - "--disable-telemetry", - ], + launchArgs, mocha: { ui: "tdd", color: true, @@ -53,19 +64,13 @@ module.exports = defineConfig({ }, }, reuseMachineInstall: !isCIBuild, - installExtensions: ["vadimcn.vscode-lldb"], + installExtensions: ["vadimcn.vscode-lldb", "llvm-vs-code-extensions.lldb-dap"], }, { label: "unitTests", files: ["dist/test/common.js", "dist/test/unit-tests/**/*.test.js"], version: process.env["VSCODE_VERSION"] ?? "stable", - launchArgs: [ - "--disable-extensions", - "--disable-updates", - "--disable-crash-reporter", - "--disable-workspace-trust", - "--disable-telemetry", - ], + launchArgs: launchArgs.concat("--disable-extensions"), mocha: { ui: "tdd", color: true, diff --git a/.vscode/launch.json b/.vscode/launch.json index 50413336b..6c72352c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,8 +25,7 @@ "args": ["--profile=testing-debug"], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "env": { - "VSCODE_DEBUG": "1", - "VSCODE_TEST": "1" + "VSCODE_DEBUG": "1" }, "preLaunchTask": "compile-tests" }, @@ -45,6 +44,13 @@ "request": "launch", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", "runtimeArgs": ["${workspaceFolder}/scripts/update_swift_docc_render.ts"] + }, + { + "name": "Preview Package", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "runtimeArgs": ["${workspaceFolder}/scripts/preview_package.ts"] } ] } diff --git a/.vscode/testing-debug.code-profile b/.vscode/testing-debug.code-profile index 37035a549..d9d74b288 100644 --- a/.vscode/testing-debug.code-profile +++ b/.vscode/testing-debug.code-profile @@ -1,4 +1,4 @@ { "name": "testing-debug", - "extensions": "[{\"identifier\":{\"id\":\"ms-vscode-remote.remote-containers\",\"uuid\":\"93ce222b-5f6f-49b7-9ab1-a0463c6238df\"},\"displayName\":\"Dev Containers\"},{\"identifier\":{\"id\":\"vadimcn.vscode-lldb\",\"uuid\":\"bee31e34-a44b-4a76-9ec2-e9fd1439a0f6\"},\"displayName\":\"CodeLLDB\"}]" + "extensions": "[{\"identifier\":{\"id\":\"ms-vscode-remote.remote-containers\",\"uuid\":\"93ce222b-5f6f-49b7-9ab1-a0463c6238df\"},\"displayName\":\"Dev Containers\"},{\"identifier\":{\"id\":\"llvm-vs-code-extensions.lldb-dap\",\"uuid\":\"8f0e51b3-cc69-4cf9-abab-97289d29d6de\"},\"displayName\":\"LLDB DAP\"}]" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9698985..c031228e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 2.2.0 - 2025-04-07 + +### Added + +- Convert the Dependencies View into the Project Panel to view all aspects of your Swift project ([#1382](https://github.com/swiftlang/vscode-swift/pull/1382)) +- Use the LLDB DAP extension to debug when using a Swift 6 toolchain ([#1384](https://github.com/swiftlang/vscode-swift/pull/1384)) +- Added run and debug buttons to Swift editors ([#1378](https://github.com/swiftlang/vscode-swift/pull/1378)) +- Educational notes from compiler diagnostics can be viewed directly in VS Code ([#1423](https://github.com/swiftlang/vscode-swift/pull/1423)) +- Swift settings now support variable substitutions ([#1439](https://github.com/swiftlang/vscode-swift/pull/1439)) +- SwiftPM plugin tasks are now configurable via settings ([#1409](https://github.com/swiftlang/vscode-swift/pull/1409)) +- Added the `swift.scriptSwiftLanguageVersion` setting to choose Swift language mode when running scripts (thanks @louisunlimited) ([#1476](https://github.com/swiftlang/vscode-swift/pull/1476)) + +### Fixed + +- Prevent duplicate reload extension notifications from appearing ([#1473](https://github.com/swiftlang/vscode-swift/pull/1473)) +- "Actual" and "Expected" values are shown in the right order on test failure ([#1444](https://github.com/swiftlang/vscode-swift/issues/1444)) +- Correctly set the `DEVELOPER_DIR` environment variable when selecting between two Xcode installs ([#1433](https://github.com/swiftlang/vscode-swift/pull/1433)) +- Prompt to reload the extension when swiftEnvironmentVariables is changed ([#1430](https://github.com/swiftlang/vscode-swift/pull/1430)) +- Search for Swift packages in sub-folders of the workspace ([#1425](https://github.com/swiftlang/vscode-swift/pull/1425)) +- Fix missing test result output on Linux when using print ([#1401](https://github.com/swiftlang/vscode-swift/pull/1401)) +- Stop all actively running tests when stop button is pressed ([#1391](https://github.com/swiftlang/vscode-swift/pull/1391)) +- Properly set `--swift-sdk` when using `Swift: Select Target Platform` on Swift 6.1 ([#1390](https://github.com/swiftlang/vscode-swift/pull/1390)) + +## 2.0.2 - 2025-02-20 + +### Fixed + +- Fix debugging of Swift tests when using Xcode 16.1 Beta ([#1396](https://github.com/swiftlang/vscode-swift/pull/1396)) + ## 2.0.1 - 2025-02-10 Rename the `displayName` of the extension back to `Swift` now that the old `sswg` extension has been renamed to `Swift (Deprecated)`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7f02d894..1dcca8aaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ When you first open the project in VS Code you will be recommended to also insta To run your version of the Swift extension while in VS Code, press `F5`. This will open up another instance of VS Code with it running. You can use the original version of VS Code to debug it. -### Installing a pre-released version +### Installing Pre-Release Builds If you'd like to try out a change during your day to day work that has not yet been released to the VS Code Marketplace you can build and install your own `.vsix` package from this repository. @@ -54,6 +54,18 @@ Alternatively you can install the extension from the Extensions panel by clickin If you'd like to return to using the released version of the extension you can uninstall then reinstall Swift for VS Code from the Extensions panel. +#### Pre-Release Builds on the Marketplace + +Occasionally, pre-release builds will be published to the VS Code Marketplace. You can switch to the pre-release version by clicking on the `Switch to Pre-Release Version` button in the Extensions View: + +![](docs/images/install-pre-release.png) + +Switching back to the release version can be done by clicking on the `Switch to Release Version` button. + +Release builds for the extension will always have an even minor version number (e.g. `2.0.2`). Pre-release versions will always be one minor version above the latest release version with a patch version set to the day that the VSIX was built (e.g. `2.1.20250327`). These rules are enforced by CI. + +The version number in the [package.json](package.json) should always match the most recently published build on the VS Code Marketplace. + ## Submitting a bug or issue Please ensure to include the following in your bug report: diff --git a/README.md b/README.md index 7f8069c26..c3dd6b31f 100644 --- a/README.md +++ b/README.md @@ -10,116 +10,13 @@ This extension adds language support for Swift to Visual Studio Code, providing * Package dependency view * Test Explorer view -This extension uses [SourceKit LSP](https://github.com/apple/sourcekit-lsp) for the [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/), which powers code completion. It also has a dependency on [CodeLLDB](https://github.com/vadimcn/vscode-lldb) for debugging. +# Documentation -To propose new features, you can post on the [swift.org forums](https://forums.swift.org) in the [VS Code Swift Extension category](https://forums.swift.org/c/related-projects/vscode-swift-extension/). If you run into something that doesn't work the way you'd expect, you can [file an issue in the GitHub repository](https://github.com/swiftlang/vscode-swift/issues/new). - -## Installation - -The Swift extension is supported on macOS, Linux, and Windows. To install, firstly ensure you have [Swift installed on your system](https://www.swift.org/install/). Then [install the Swift extension](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode). Once your machine is ready, you can get started with the **Swift: Create New Project...** command. - -## Features - -### Language features - -The extension provides language features such as code completion and jump to definition via [SourceKit-LSP](https://github.com/apple/sourcekit-lsp). To ensure the extension functions correctly, it’s important to first build the project so that SourceKit-LSP has access to all the symbol data. Whenever you add a new dependency to your project, make sure to rebuild it so that SourceKit-LSP can update its information. - -### Automatic task creation - -For workspaces that contain a **Package.swift** file, this extension will add the following tasks: - -- **Build All**: Build all targets in the Package -- **Build Debug **: Each executable in a Package.swift get a task for building a debug build -- **Build Release **: Each executable in a Package.swift get a task for building a release build - -These tasks are available via **Terminal ▸ Run Task...** and **Terminal ▸ Run Build Task...**. - -### Commands - -The extension adds the following commands, available via the command palette. - -#### Configuration - -- **Create New Project...**: Create a new Swift project using a template. This opens a dialog to guide you through creating a new project structure. -- **Create New Swift File...**: Create a new `.swift` file in the current workspace. -- **Select Toolchain**: Select the locally installed Swift toolchain (including Xcode toolchains on macOS) that you want to use Swift tools from. - -The following command is only available on macOS: - -- **Select Target Platform**: This is an experimental command that offers code completion for iOS and tvOS projects. - -#### Building and Debugging - -- **Run Build**: Run `swift build` for the package associated with the open file. -- **Debug Build**: Run `swift build` with debugging enabled for the package associated with the open file, launching the binary and attaching the debugger. -- **Attach to Process...**: Attach the debugger to an already running process for debugging. -- **Clean Build Folder**: Clean the build folder for the package associated with the open file, removing all previously built products. - -#### Dependency Management - -- **Resolve Package Dependencies**: Run `swift package resolve` on packages associated with the open file. -- **Update Package Dependencies**: Run `swift package update` on packages associated with the open file. -- **Reset Package Dependencies**: Run `swift package reset` on packages associated with the open file. -- **Add to Workspace**: Add the current package to the active workspace in VS Code. -- **Clean Build**: Run `swift package clean` on packages associated with the open file. -- **Open Package.swift**: Open `Package.swift` for the package associated with the open file. -- **Use Local Version**: Switch the package dependency to use a local version of the package instead of the remote repository version. -- **Edit Locally**: Make the package dependency editable locally, allowing changes to the dependency to be reflected immediately. -- **Revert To Original Version**: Revert the package dependency to its original, unedited state after local changes have been made. -- **View Repository**: Open the external repository of the selected Swift package in a browser. +The official documentation for this extension is available at [vscode-swift](https://docs.swift.org/vscode/documentation/userdocs) -#### Testing +This extension uses [SourceKit LSP](https://github.com/apple/sourcekit-lsp) for the [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/), which powers code completion. It also has a dependency on [LLDB DAP](https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.lldb-dap) for debugging. -- **Test: Run All Tests**: Run all the tests across all test targes in the open project. -- **Test: Rerun Last Run**: Perform the last test run again. -- **Test: Open Coverage**: Open the last generated coverage report, if one exists. -- **Test: Run All Tests in Parallel**: Run all tests in parallel. This action only affects XCTests. Swift-testing tests are parallel by default, and their parallelism [is controlled in code](https://developer.apple.com/documentation/testing/parallelization). - -#### Snippets and Scripts - -- **Insert Function Comment**: Insert a standard comment block for documenting a Swift function in the current file. -- **Run Swift Script**: Run the currently open file, as a Swift script. The file must not be part of a build target. If the file has not been saved it will save it to a temporary file so it can be run. -- **Run Swift Snippet**: If the currently open file is a Swift snippet then run it. -- **Debug Swift Snippet**: If the currently open file is a Swift snippet then debug it. - -#### Diagnostics - -- **Capture VS Code Swift Diagnostic Bundle**: Capture a diagnostic bundle from VS Code, containing logs and information to aid in troubleshooting Swift-related issues. -- **Clear Diagnostics Collection**: Clear all collected diagnostics in the current workspace to start fresh. -- **Restart LSP Server**: Restart the Swift Language Server Protocol (LSP) server for the current workspace. -- **Re-Index Project**: Force a re-index of the project to refresh code completion and symbol navigation support. - -### Package dependencies - -If your workspace contains a package that has dependencies, this extension will add a **Package Dependencies** view to the Explorer: - -![](images/package-dependencies.png) - -Additionally, the extension will monitor `Package.swift` and `Package.resolved` for changes, resolve any changes to the dependencies, and update the view as needed. - -### Debugging - -The Swift extension uses the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension for debugging. - -When you open a Swift package (a directory containing a `Package.swift` file), the extension automatically generates build tasks and launch configurations for each executable within the package. Additionally, if the package includes tests, the extension creates a configuration specifically designed to run those tests. These configurations all leverage the CodeLLDB extension as the debugger of choice. - -Use the **Run > Start Debugging** menu item to run an executable and start debugging. If you have multiple launch configurations you can choose which launch configuration to use in the debugger view. - -CodeLLDB includes a version of `lldb` that it uses by default for debugging, but this version of `lldb` doesn’t support Swift. The Swift extension will automatically identify the required version and offer to update the CodeLLDB configuration as necessary so that debugging is supported. - -### Test Explorer - -If your package contains tests then they can be viewed, run and debugged in the Test Explorer. - -![](images/test-explorer.png) - -Once your project is built, the Test Explorer will list all your tests. These tests are grouped by package, then test target, and finally, by XCTestCase class. From the Test Explorer, you can initiate a test run, debug a test run, and if a file has already been opened, you can jump to the source code for a test. - -### Documentation - -* [Extension Settings](docs/settings.md) -* [Test Coverage](docs/test-coverage.md) -* [Visual Studio Code Dev Containers](docs/remote-dev.md) +To propose new features, you can post on the [swift.org forums](https://forums.swift.org) in the [VS Code Swift Extension category](https://forums.swift.org/c/related-projects/vscode-swift-extension/). If you run into something that doesn't work the way you'd expect, you can [file an issue in the GitHub repository](https://github.com/swiftlang/vscode-swift/issues/new). ## Contributing diff --git a/assets/test/.vscode/launch.json b/assets/test/.vscode/launch.json index 152e39f4a..90b9c8da4 100644 --- a/assets/test/.vscode/launch.json +++ b/assets/test/.vscode/launch.json @@ -1,7 +1,7 @@ { "configurations": [ { - "type": "swift-lldb", + "type": "swift", "request": "launch", "name": "Debug PackageExe (defaultPackage)", "program": "${workspaceFolder:test}/defaultPackage/.build/debug/PackageExe", @@ -12,7 +12,7 @@ "initCommands": ["settings set target.disable-aslr false"], }, { - "type": "swift-lldb", + "type": "swift", "request": "launch", "name": "Release PackageExe (defaultPackage)", "program": "${workspaceFolder:test}/defaultPackage/.build/release/PackageExe", diff --git a/assets/test/.vscode/settings.json b/assets/test/.vscode/settings.json index 4a09afde5..db1acbde4 100644 --- a/assets/test/.vscode/settings.json +++ b/assets/test/.vscode/settings.json @@ -1,7 +1,8 @@ { "swift.disableAutoResolve": true, "swift.autoGenerateLaunchConfigurations": false, - "swift.debugger.useDebugAdapterFromToolchain": true, + "swift.debugger.debugAdapter": "lldb-dap", + "swift.debugger.setupCodeLLDB": "alwaysUpdateGlobal", "swift.additionalTestArguments": [ "-Xswiftc", "-DTEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING" diff --git a/assets/test/.vscode/tasks.json b/assets/test/.vscode/tasks.json index d91e6ff96..406dec54f 100644 --- a/assets/test/.vscode/tasks.json +++ b/assets/test/.vscode/tasks.json @@ -35,6 +35,7 @@ "command": "command_plugin", "args": ["--foo"], "cwd": "command-plugin", + "disableSandbox": true, "problemMatcher": [ "$swiftc" ], diff --git a/assets/test/ModularPackage/Module1/Package.swift b/assets/test/ModularPackage/Module1/Package.swift new file mode 100644 index 000000000..871dbc947 --- /dev/null +++ b/assets/test/ModularPackage/Module1/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "Module1", + products: [ + .executable(name: "Module1Demo", targets: ["Module1Demo"]), + ], + targets: [ + .testTarget( + name: "Module1Tests", + dependencies: ["Module1"] + ), + .executableTarget( + name: "Module1Demo", + dependencies: ["Module1"] + ), + .target( + name: "Module1" + ), + ] +) diff --git a/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift b/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift new file mode 100644 index 000000000..32c3e62fa --- /dev/null +++ b/assets/test/ModularPackage/Module1/Sources/Module1/Module1.swift @@ -0,0 +1,7 @@ +public struct Module1 { + public init() {} + + public func add(_ x: Int, _ y: Int) -> Int { + x + y + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift b/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift new file mode 100644 index 000000000..941beed71 --- /dev/null +++ b/assets/test/ModularPackage/Module1/Sources/Module1Demo/Module1Demo.swift @@ -0,0 +1,11 @@ +import Module1 + +private let module = Module1() + +@MainActor +func check(_ x: Int, _ y: Int) { + print(module.add(x, y)) +} + +check(1, 2) +check(2, 3) diff --git a/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift b/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift new file mode 100644 index 000000000..e7df7b33c --- /dev/null +++ b/assets/test/ModularPackage/Module1/Tests/Module1Tests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import Module1 + +internal final class Module1Tests: XCTestCase { + private var sut: Module1! + + override internal func setUp() { + super.setUp() + sut = .init() + } + + override internal func tearDown() { + sut = nil + super.tearDown() + } + + internal func test_add_with1And2_shouldReturn3() { + XCTAssertEqual(sut.add(1, 2), 3) + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module2/Package.swift b/assets/test/ModularPackage/Module2/Package.swift new file mode 100644 index 000000000..9ccde0e86 --- /dev/null +++ b/assets/test/ModularPackage/Module2/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "Module2", + products: [ + .executable(name: "Module2Demo", targets: ["Module2Demo"]), + ], + targets: [ + .testTarget( + name: "Module2Tests", + dependencies: ["Module2"] + ), + .executableTarget( + name: "Module2Demo", + dependencies: ["Module2"] + ), + .target( + name: "Module2" + ), + ] +) diff --git a/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift b/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift new file mode 100644 index 000000000..07541013e --- /dev/null +++ b/assets/test/ModularPackage/Module2/Sources/Module2/Module2.swift @@ -0,0 +1,7 @@ +public struct Module2 { + public init() {} + + public func subtract(_ x: Int, _ y: Int) -> Int { + x - y + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift b/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift new file mode 100644 index 000000000..6d33bf22a --- /dev/null +++ b/assets/test/ModularPackage/Module2/Sources/Module2Demo/main.swift @@ -0,0 +1,11 @@ +import Module2 + +private let module = Module2() + +@MainActor +func check(_ x: Int, _ y: Int) { + print(module.subtract(x, y)) +} + +check(1, 2) +check(2, 3) diff --git a/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift b/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift new file mode 100644 index 000000000..795376105 --- /dev/null +++ b/assets/test/ModularPackage/Module2/Tests/Module2Tests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import Module2 + +internal final class Module2Tests: XCTestCase { + private var sut: Module2! + + override internal func setUp() { + super.setUp() + sut = .init() + } + + override internal func tearDown() { + sut = nil + super.tearDown() + } + + internal func test_add_with5And2_shouldReturn3() { + XCTAssertEqual(sut.subtract(5, 2), 3) + } +} \ No newline at end of file diff --git a/assets/test/ModularPackage/Package.swift b/assets/test/ModularPackage/Package.swift new file mode 100644 index 000000000..c8af6a378 --- /dev/null +++ b/assets/test/ModularPackage/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:6.0 + +import PackageDescription + +internal let package = Package( + name: "ModularPackage", + dependencies: [ + .package(path: "Module1"), + .package(path: "Module2"), + ] +) diff --git a/assets/test/defaultPackage/.vscode/launch.json b/assets/test/defaultPackage/.vscode/launch.json index c0eb8e90d..a2ecfe301 100644 --- a/assets/test/defaultPackage/.vscode/launch.json +++ b/assets/test/defaultPackage/.vscode/launch.json @@ -1,7 +1,7 @@ { "configurations": [ { - "type": "swift-lldb", + "type": "swift", "request": "launch", "name": "Debug package1", "program": "${workspaceFolder:defaultPackage}/.build/debug/package1", @@ -10,7 +10,7 @@ "preLaunchTask": "swift: Build Debug package1" }, { - "type": "swift-lldb", + "type": "swift", "request": "launch", "name": "Release package1", "program": "${workspaceFolder:defaultPackage}/.build/release/package1", diff --git a/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift b/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift index 835e7f127..68d430378 100644 --- a/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift +++ b/assets/test/defaultPackage/Tests/PackageTests/PackageTests.swift @@ -42,13 +42,9 @@ final class DebugReleaseTestSuite: XCTestCase { } } -#if swift(>=6.1) -@_spi(Experimental) import Testing -#elseif swift(>=6.0) +#if swift(>=6.0) import Testing -#endif -#if swift(>=6.0) @Test func topLevelTestPassing() { print("A print statement in a test.") #if !TEST_ARGUMENT_SET_VIA_TEST_BUILD_ARGUMENTS_SETTING @@ -102,14 +98,23 @@ struct MixedSwiftTestingSuite { } #expect(2 == 3) } -#endif -#if swift(>=6.1) -@Test func testAttachment() throws { - Attachment("Hello, world!", named: "hello.txt").attach() +@Test func testLotsOfOutput() { + var string = "" + for i in 1...100_000 { + string += "\(i)\n" + } + print(string) } #endif +// Disabled until Attachments are formalized and released. +// #if swift(>=6.1) +// @Test func testAttachment() throws { +// Attachment("Hello, world!", named: "hello.txt").attach() +// } +// #endif + final class DuplicateSuffixTests: XCTestCase { func testPassing() throws {} func testPassingSuffix() throws {} diff --git a/assets/test/dependencies/Package.swift b/assets/test/dependencies/Package.swift index 51f61f4ca..ba5acdb59 100644 --- a/assets/test/dependencies/Package.swift +++ b/assets/test/dependencies/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: [ .executableTarget( name: "dependencies", - dependencies: [.product(name: "MarkdownLib", package: "swift-markdown")], + dependencies: [.product(name: "Markdown", package: "swift-markdown")], path: "Sources"), ] ) diff --git a/assets/test/dependencies/Sources/main.swift b/assets/test/dependencies/Sources/main.swift index 8bf0d5043..1581c5f07 100644 --- a/assets/test/dependencies/Sources/main.swift +++ b/assets/test/dependencies/Sources/main.swift @@ -1,4 +1,4 @@ -import MarkdownLib +import Markdown print("Test Asset:(dependencies)") print(a) \ No newline at end of file diff --git a/assets/test/diagnostics/.vscode/launch.json b/assets/test/diagnostics/.vscode/launch.json index aca14e524..e86c37d2e 100644 --- a/assets/test/diagnostics/.vscode/launch.json +++ b/assets/test/diagnostics/.vscode/launch.json @@ -1,7 +1,7 @@ { "configurations": [ { - "type": "swift-lldb", + "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:diagnostics}", @@ -10,7 +10,7 @@ "preLaunchTask": "swift: Build Debug diagnostics" }, { - "type": "swift-lldb", + "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:diagnostics}", @@ -19,7 +19,7 @@ "preLaunchTask": "swift: Build Release diagnostics" }, { - "type": "swift-lldb", + "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:diagnostics}", @@ -28,7 +28,7 @@ "preLaunchTask": "swift: Build Debug diagnostics" }, { - "type": "swift-lldb", + "type": "swift", "request": "launch", "args": [], "cwd": "${workspaceFolder:diagnostics}", diff --git a/assets/test/identity-different/Package.resolved b/assets/test/identity-different/Package.resolved index fe4f52231..0b7133f7d 100644 --- a/assets/test/identity-different/Package.resolved +++ b/assets/test/identity-different/Package.resolved @@ -2,12 +2,12 @@ "object": { "pins": [ { - "package": "cmark-gfm", - "repositoryURL": "https://github.com/apple/swift-cmark.git", + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", "state": { - "branch": "gfm", - "revision": "bfdc057b5a02fc65af20771a7ba08f9c944eb117", - "version": null + "branch": null, + "revision": "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version": "1.6.2" } } ] diff --git a/assets/test/identity-different/Package.swift b/assets/test/identity-different/Package.swift index 2d9818878..5bec37a19 100644 --- a/assets/test/identity-different/Package.swift +++ b/assets/test/identity-different/Package.swift @@ -13,13 +13,13 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(name: "cmark", url: "https://github.com/apple/swift-cmark.git", .branch("gfm")), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "identity-different", - dependencies: ["cmark"]), + dependencies: [.product(name: "Logging", package: "swift-log")]), ] ) diff --git a/assets/test/targets/Package.swift b/assets/test/targets/Package.swift new file mode 100644 index 000000000..35cda10ab --- /dev/null +++ b/assets/test/targets/Package.swift @@ -0,0 +1,48 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "targets", + products: [ + .library( + name: "LibraryTarget", + targets: ["LibraryTarget"] + ), + .executable( + name: "ExecutableTarget", + targets: ["ExecutableTarget"] + ), + .plugin( + name: "PluginTarget", + targets: ["PluginTarget"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-markdown.git", branch: "main"), + .package(path: "../defaultPackage"), + ], + targets: [ + .target( + name: "LibraryTarget" + ), + .executableTarget( + name: "ExecutableTarget" + ), + .plugin( + name: "PluginTarget", + capability: .command( + intent: .custom(verb: "testing", description: "A plugin for testing plugins") + ) + ), + .testTarget( + name: "TargetsTests", + dependencies: ["LibraryTarget"] + ), + .testTarget( + name: "AnotherTests", + dependencies: ["LibraryTarget"] + ), + ] +) diff --git a/assets/test/targets/Plugins/PluginTarget/main.swift b/assets/test/targets/Plugins/PluginTarget/main.swift new file mode 100644 index 000000000..8a2a8680f --- /dev/null +++ b/assets/test/targets/Plugins/PluginTarget/main.swift @@ -0,0 +1,9 @@ +import PackagePlugin +import Foundation + +@main +struct MyCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) throws { + print("Plugin Target Hello World") + } +} \ No newline at end of file diff --git a/assets/test/targets/Snippets/AnotherSnippet.swift b/assets/test/targets/Snippets/AnotherSnippet.swift new file mode 100644 index 000000000..25f53dfa6 --- /dev/null +++ b/assets/test/targets/Snippets/AnotherSnippet.swift @@ -0,0 +1 @@ +print("Another Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Snippets/Snippet.swift b/assets/test/targets/Snippets/Snippet.swift new file mode 100644 index 000000000..cdd7d267c --- /dev/null +++ b/assets/test/targets/Snippets/Snippet.swift @@ -0,0 +1 @@ +print("Snippet Hello World") \ No newline at end of file diff --git a/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift b/assets/test/targets/Sources/CommandPluginTarget/CommandPluginTarget.swift new file mode 100644 index 000000000..e69de29bb diff --git a/assets/test/targets/Sources/ExecutableTarget/main.swift b/assets/test/targets/Sources/ExecutableTarget/main.swift new file mode 100644 index 000000000..2fcea7ab3 --- /dev/null +++ b/assets/test/targets/Sources/ExecutableTarget/main.swift @@ -0,0 +1 @@ +print("Executable Target Hello World!") \ No newline at end of file diff --git a/assets/test/targets/Sources/LibraryTarget/Targets.swift b/assets/test/targets/Sources/LibraryTarget/Targets.swift new file mode 100644 index 000000000..37c4f8832 --- /dev/null +++ b/assets/test/targets/Sources/LibraryTarget/Targets.swift @@ -0,0 +1,9 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +public func foo() { + print("foo") +} +public func bar() { + print("bar") +} \ No newline at end of file diff --git a/assets/test/targets/Tests/AnotherTests/AnotherTests.swift b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift new file mode 100644 index 000000000..8aa96db8b --- /dev/null +++ b/assets/test/targets/Tests/AnotherTests/AnotherTests.swift @@ -0,0 +1,8 @@ +import LibraryTarget +import XCTest + +class AnotherTests: XCTestCase { + func testExample() { + bar() + } +} \ No newline at end of file diff --git a/assets/test/targets/Tests/TargetsTests/TargetsTests.swift b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift new file mode 100644 index 000000000..089304193 --- /dev/null +++ b/assets/test/targets/Tests/TargetsTests/TargetsTests.swift @@ -0,0 +1,8 @@ +import LibraryTarget +import XCTest + +class TargetsTests: XCTestCase { + func testExample() { + foo() + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a30c01e0d..bff572f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "swift-vscode", - "version": "2.0.1", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "swift-vscode", - "version": "2.0.1", + "version": "2.2.0", "hasInstallScript": true, "dependencies": { "@vscode/codicons": "^0.0.36", @@ -18,43 +18,47 @@ "devDependencies": { "@types/chai": "^4.3.19", "@types/chai-as-promised": "^7.1.8", - "@types/chai-subset": "^1.3.5", + "@types/chai-subset": "^1.3.6", "@types/glob": "^7.1.6", "@types/lcov-parse": "^1.0.2", + "@types/lodash.debounce": "^4.0.9", + "@types/lodash.throttle": "^4.1.9", "@types/mocha": "^10.0.10", "@types/mock-fs": "^4.13.4", - "@types/node": "^18.19.75", + "@types/node": "^20.17.30", "@types/plist": "^3.0.5", - "@types/semver": "^7.5.8", - "@types/sinon": "^17.0.3", + "@types/semver": "^7.7.0", + "@types/sinon": "^17.0.4", "@types/sinon-chai": "^3.2.12", "@types/vscode": "^1.88.0", "@types/xml2js": "^0.4.14", - "@typescript-eslint/eslint-plugin": "^8.23.0", - "@typescript-eslint/parser": "^8.23.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", "@vscode/debugprotocol": "^1.68.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^2.32.0", + "@vscode/vsce": "^3.3.2", "chai": "^4.5.0", "chai-as-promised": "^7.1.2", "chai-subset": "^1.6.0", "del-cli": "^6.0.0", - "esbuild": "^0.24.2", + "esbuild": "^0.25.2", "eslint": "^8.57.0", - "eslint-config-prettier": "^10.0.1", + "eslint-config-prettier": "^10.1.1", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", "mocha": "^10.8.2", - "mock-fs": "^5.4.1", + "mock-fs": "^5.5.0", "node-pty": "^1.0.0", - "prettier": "^3.4.2", + "prettier": "^3.5.3", "semver": "^7.7.1", "simple-git": "^3.27.0", - "sinon": "^19.0.2", + "sinon": "^20.0.0", "sinon-chai": "^3.7.0", "source-map-support": "^0.5.21", "strip-ansi": "^6.0.1", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "tsx": "^4.19.3", + "typescript": "^5.8.3" }, "engines": { "vscode": "^1.88.0" @@ -293,13 +297,14 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -309,13 +314,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -325,13 +331,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -341,13 +348,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -357,13 +365,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -373,13 +382,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -389,13 +399,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -405,13 +416,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -421,13 +433,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -437,13 +450,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -453,13 +467,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -469,13 +484,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -485,13 +501,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -501,13 +518,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -517,13 +535,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -533,13 +552,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -549,13 +569,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -565,13 +586,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -581,13 +603,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -597,13 +620,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -613,13 +637,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -629,13 +654,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -645,13 +671,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -661,13 +688,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -677,13 +705,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1004,9 +1033,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", - "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1025,13 +1054,6 @@ "type-detect": "^4.1.0" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/@types/chai": { "version": "4.3.19", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", @@ -1050,13 +1072,13 @@ } }, "node_modules/@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/chai": "*" + "peerDependencies": { + "@types/chai": "<5.2.0" } }, "node_modules/@types/glob": { @@ -1081,6 +1103,32 @@ "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -1104,13 +1152,13 @@ } }, "node_modules/@types/node": { - "version": "18.19.75", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", - "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/plist": { @@ -1124,15 +1172,16 @@ } }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/sinon": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", - "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", "dev": true, "license": "MIT", "dependencies": { @@ -1173,17 +1222,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", + "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/type-utils": "8.29.1", + "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1199,20 +1248,20 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", + "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4" }, "engines": { @@ -1224,18 +1273,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", + "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1246,14 +1295,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", + "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/utils": "8.29.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1266,13 +1315,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", + "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", "dev": true, "license": "MIT", "engines": { @@ -1284,14 +1333,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", + "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1307,7 +1356,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1337,16 +1386,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1357,17 +1406,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", + "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.29.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1631,10 +1680,11 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", - "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", "dev": true, + "license": "MIT", "dependencies": { "@azure/identity": "^4.1.0", "@vscode/vsce-sign": "^2.0.0", @@ -1642,19 +1692,19 @@ "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", - "commander": "^6.2.1", + "commander": "^12.1.0", "form-data": "^4.0.0", - "glob": "^7.0.6", + "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^7.5.2", - "tmp": "^0.2.1", + "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", @@ -1665,7 +1715,7 @@ "vsce": "vsce" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "optionalDependencies": { "keytar": "^7.7.0" @@ -1818,6 +1868,16 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@vscode/vsce/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1856,6 +1916,46 @@ "node": ">=0.8.0" } }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/vsce/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1865,6 +1965,49 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@vscode/vsce/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==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@vscode/vsce/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2438,12 +2581,13 @@ } }, "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/concat-map": { @@ -2828,11 +2972,12 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2840,31 +2985,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/escalade": { @@ -2944,12 +3089,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", "dev": true, + "license": "MIT", "bin": { - "eslint-config-prettier": "build/bin/cli.js" + "eslint-config-prettier": "bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -3915,13 +4061,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -3995,12 +4134,13 @@ } }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/locate-path": { @@ -4018,6 +4158,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4073,6 +4219,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "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", @@ -4127,35 +4280,42 @@ } }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" }, "node_modules/meow": { "version": "13.2.0", @@ -4378,10 +4538,11 @@ } }, "node_modules/mock-fs": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz", - "integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", + "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" } @@ -4417,20 +4578,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, "node_modules/node-abi": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", @@ -4813,16 +4960,6 @@ "node": "14 || >=16.14" } }, - "node_modules/path-to-regexp": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", - "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/path-type": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", @@ -4921,10 +5058,11 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -4961,6 +5099,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", @@ -5344,17 +5492,16 @@ } }, "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-20.0.0.tgz", + "integrity": "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/fake-timers": "^13.0.5", "@sinonjs/samsam": "^8.0.1", "diff": "^7.0.0", - "nise": "^6.1.1", "supports-color": "^7.2.0" }, "funding": { @@ -5593,10 +5740,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "chownr": "^1.1.1", @@ -5644,15 +5792,13 @@ "dev": true }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/to-regex-range": { @@ -5687,12 +5833,13 @@ "dev": true }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -5705,489 +5852,66 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, - "optional": true, - "os": [ - "aix" - ], "engines": { - "node": ">=18" + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "optional": true, - "os": [ - "android" - ], + "dependencies": { + "safe-buffer": "^5.0.1" + }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=4" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-rest-client": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "dependencies": { "qs": "^6.9.1", @@ -6196,10 +5920,11 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6209,10 +5934,11 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" }, "node_modules/underscore": { "version": "1.13.6", @@ -6221,10 +5947,11 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -6700,177 +6427,177 @@ "dev": true }, "@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "dev": true, "optional": true }, "@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "dev": true, "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "dev": true, "optional": true }, @@ -7100,9 +6827,9 @@ } }, "@sinonjs/fake-timers": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", - "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1" @@ -7119,12 +6846,6 @@ "type-detect": "^4.1.0" } }, - "@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true - }, "@types/chai": { "version": "4.3.19", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", @@ -7141,13 +6862,11 @@ } }, "@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", "dev": true, - "requires": { - "@types/chai": "*" - } + "requires": {} }, "@types/glob": { "version": "7.2.0", @@ -7171,6 +6890,30 @@ "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", "dev": true }, + "@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true + }, + "@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -7193,12 +6936,12 @@ } }, "@types/node": { - "version": "18.19.75", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", - "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", + "version": "20.17.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", + "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", "dev": true, "requires": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "@types/plist": { @@ -7212,15 +6955,15 @@ } }, "@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true }, "@types/sinon": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", - "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", "dev": true, "requires": { "@types/sinonjs__fake-timers": "*" @@ -7258,16 +7001,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", + "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/type-utils": "8.29.1", + "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7275,54 +7018,54 @@ } }, "@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", + "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", + "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", "dev": true, "requires": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1" } }, "@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", + "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/utils": "8.29.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" } }, "@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", + "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", + "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", "dev": true, "requires": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7352,24 +7095,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1" } }, "@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", + "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", "dev": true, "requires": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.29.1", "eslint-visitor-keys": "^4.2.0" }, "dependencies": { @@ -7566,9 +7309,9 @@ } }, "@vscode/vsce": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", - "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", "dev": true, "requires": { "@azure/identity": "^4.1.0", @@ -7577,20 +7320,20 @@ "chalk": "^2.4.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", - "commander": "^6.2.1", + "commander": "^12.1.0", "form-data": "^4.0.0", - "glob": "^7.0.6", + "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "keytar": "^7.7.0", "leven": "^3.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "semver": "^7.5.2", - "tmp": "^0.2.1", + "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", @@ -7607,6 +7350,15 @@ "color-convert": "^1.9.0" } }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -7639,12 +7391,62 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, + "glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, + "jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, + "lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true + }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -8139,9 +7941,9 @@ } }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true }, "concat-map": { @@ -8416,36 +8218,36 @@ "dev": true }, "esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "escalade": { @@ -8507,9 +8309,9 @@ } }, "eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", + "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", "dev": true, "requires": {} }, @@ -9212,12 +9014,6 @@ "setimmediate": "^1.0.5" } }, - "just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true - }, "jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -9281,12 +9077,12 @@ } }, "linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "requires": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "locate-path": { @@ -9298,6 +9094,12 @@ "p-locate": "^5.0.0" } }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -9352,6 +9154,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9390,30 +9198,31 @@ } }, "markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "requires": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "dependencies": { "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true } } }, "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, "meow": { @@ -9578,9 +9387,9 @@ } }, "mock-fs": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz", - "integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", + "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", "dev": true }, "ms": { @@ -9614,19 +9423,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, "node-abi": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", @@ -9903,12 +9699,6 @@ } } }, - "path-to-regexp": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", - "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==", - "dev": true - }, "path-type": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", @@ -9978,9 +9768,9 @@ "dev": true }, "prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true }, "process-nextick-args": { @@ -10006,6 +9796,12 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "qs": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", @@ -10254,16 +10050,15 @@ } }, "sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-20.0.0.tgz", + "integrity": "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/fake-timers": "^13.0.5", "@sinonjs/samsam": "^8.0.1", "diff": "^7.0.0", - "nise": "^6.1.1", "supports-color": "^7.2.0" }, "dependencies": { @@ -10432,9 +10227,9 @@ "dev": true }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "dev": true, "optional": true, "requires": { @@ -10479,13 +10274,10 @@ "dev": true }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true }, "to-regex-range": { "version": "5.0.1", @@ -10510,216 +10302,14 @@ "dev": true }, "tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "requires": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "fsevents": "~2.3.3", "get-tsconfig": "^4.7.5" - }, - "dependencies": { - "@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "dev": true, - "optional": true - }, - "esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - } } }, "tunnel": { @@ -10771,15 +10361,15 @@ } }, "typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true }, "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, "underscore": { @@ -10789,9 +10379,9 @@ "dev": true }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "unicorn-magic": { diff --git a/package.json b/package.json index 2518ea3f8..30c25a97e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "swift-vscode", "displayName": "Swift", "description": "Swift Language Support for Visual Studio Code.", - "version": "2.0.1", + "version": "2.2.0", "publisher": "swiftlang", "icon": "icon.png", "repository": { @@ -111,6 +111,11 @@ "title": "Create New Project...", "category": "Swift" }, + { + "command": "swift.openEducationalNote", + "title": "Open Educational Note...", + "category": "Swift" + }, { "command": "swift.newFile", "title": "Create New Swift File...", @@ -129,6 +134,18 @@ "icon": "$(refresh)", "category": "Swift" }, + { + "command": "swift.flatDependenciesList", + "title": "Flat Dependencies List View", + "icon": "$(list-flat)", + "category": "Swift" + }, + { + "command": "swift.nestedDependenciesList", + "title": "Nested Dependencies List View", + "icon": "$(list-tree)", + "category": "Swift" + }, { "command": "swift.cleanBuild", "title": "Clean Build Folder", @@ -136,13 +153,15 @@ }, { "command": "swift.run", - "title": "Run Build", - "category": "Swift" + "title": "Run Swift executable", + "category": "Swift", + "icon": "$(play)" }, { "command": "swift.debug", - "title": "Debug Build", - "category": "Swift" + "title": "Debug Swift executable", + "category": "Swift", + "icon": "$(debug)" }, { "command": "swift.resetPackage", @@ -208,12 +227,14 @@ { "command": "swift.runSnippet", "title": "Run Swift Snippet", - "category": "Swift" + "category": "Swift", + "icon": "$(play)" }, { "command": "swift.debugSnippet", "title": "Debug Swift Snippet", - "category": "Swift" + "category": "Swift", + "icon": "$(debug)" }, { "command": "swift.runPluginTask", @@ -250,10 +271,40 @@ "title": "Run Until Failure...", "category": "Swift" }, + { + "command": "swift.pickProcess", + "title": "Pick Process...", + "category": "Swift" + }, { "command": "swift.runAllTestsParallel", - "title": "Run All Tests in Parallel", - "category": "Test" + "title": "Run Tests in Parallel", + "category": "Test", + "icon": "$(testing-run-all-icon)" + }, + { + "command": "swift.runAllTests", + "title": "Run Tests", + "category": "Test", + "icon": "$(testing-run-icon)" + }, + { + "command": "swift.debugAllTests", + "title": "Debug Tests", + "category": "Test", + "icon": "$(testing-debug-icon)" + }, + { + "command": "swift.coverAllTests", + "title": "Run Tests with Coverage", + "category": "Test", + "icon": "$(debug-coverage)" + }, + { + "command": "swift.openDocumentation", + "title": "Open Documentation", + "category": "Swift", + "icon": "$(book)" } ], "configuration": [ @@ -273,6 +324,24 @@ }, "markdownDescription": "Additional arguments to pass to `swift build` and `swift test`. Keys and values should be provided as individual entries in the list. If you have created a copy of the build task in `tasks.json` then these build arguments will not be propagated to that task." }, + "swift.scriptSwiftLanguageVersion": { + "type": "string", + "enum": [ + "6", + "5", + "4.2", + "4", + "Ask Every Run" + ], + "enumDescriptions": [ + "Use Swift 6 when running Swift scripts.", + "Use Swift 5 when running Swift scripts.", + "Prompt to select the Swift version each time a script is run." + ], + "default": "6", + "markdownDescription": "The default Swift version to use when running Swift scripts.", + "scope": "machine-overridable" + }, "swift.packageArguments": { "type": "array", "default": [], @@ -404,6 +473,30 @@ "default": true, "markdownDescription": "Controls whether or not the extension will contribute environment variables defined in `Swift: Environment Variables` to the integrated terminal. If this is set to `true` and a custom `Swift: Path` is also set then the swift path is appended to the terminal's `PATH`." }, + "swift.pluginArguments": { + "default": [], + "markdownDescription": "Configure a list of arguments to pass to command invocations. This can either be an array of arguments, which will apply to all command invocations, or an object with command names as the key where the value is an array of arguments.", + "scope": "machine-overridable", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "patternProperties": { + "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?)$": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, "swift.pluginPermissions": { "type": "object", "default": {}, @@ -646,10 +739,20 @@ { "title": "Debugger", "properties": { - "swift.debugger.useDebugAdapterFromToolchain": { - "type": "boolean", - "default": false, - "markdownDescription": "Use the LLDB debug adapter packaged with the Swift toolchain as your debug adapter. Note: this is only available starting with Swift 6. The CodeLLDB extension will be used if your Swift toolchain does not contain lldb-dap.", + "swift.debugger.debugAdapter": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "lldb-dap", + "CodeLLDB" + ], + "enumDescriptions": [ + "Automatically select which debug adapter to use based on your Swift toolchain version.", + "Use the `lldb-dap` executable from the toolchain. Requires Swift 6 or later.", + "Use the CodeLLDB extension's debug adapter." + ], + "markdownDescription": "Select which debug adapter to use to debus Swift executables.", "order": 1 }, "swift.debugger.path": { @@ -657,6 +760,31 @@ "default": "", "markdownDescription": "Path to lldb debug adapter.", "order": 2 + }, + "swift.debugger.setupCodeLLDB": { + "type": "string", + "default": "prompt", + "enum": [ + "prompt", + "alwaysUpdateGlobal", + "alwaysUpdateWorkspace", + "never" + ], + "enumDescriptions": [ + "Prompt to update CodeLLDB settings when they are incorrect.", + "Always automatically update CodeLLDB settings globally when they are incorrect.", + "Always automatically update CodeLLDB settings in the workspace when they are incorrect.", + "Never automatically update CodeLLDB settings when they are incorrect." + ], + "markdownDescription": "Choose how CodeLLDB settings are updated when debugging Swift executables.", + "order": 3 + }, + "swift.debugger.useDebugAdapterFromToolchain": { + "type": "boolean", + "default": false, + "markdownDeprecationMessage": "**Deprecated**: Use the `swift.debugger.debugAdapter` setting instead. This will be removed in future versions of the Swift extension.", + "markdownDescription": "Use the LLDB debug adapter packaged with the Swift toolchain as your debug adapter. Note: this is only available starting with Swift 6. The CodeLLDB extension will be used if your Swift toolchain does not contain lldb-dap.", + "order": 4 } } }, @@ -689,7 +817,12 @@ "swift.swiftSDK": { "type": "string", "default": "", - "markdownDescription": "The [Swift SDK](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md) to compile against (`--swift-sdk` parameter).", + "markdownDescription": "The [Swift SDK](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md) to compile against (`--swift-sdk` parameter)." + }, + "swift.disableSandox": { + "type": "boolean", + "default": false, + "markdownDescription": "Disable sandboxing when running SwiftPM commands. In most cases you should keep the sandbox enabled and leave this setting set to `false`", "order": 4 }, "swift.diagnostics": { @@ -775,7 +908,7 @@ }, { "command": "swift.switchPlatform", - "when": "swift.isActivated && isMac" + "when": "swift.isActivated && isMac && swift.switchPlatformAvailable" }, { "command": "swift.insertFunctionComment", @@ -793,6 +926,14 @@ "command": "swift.openPackage", "when": "swift.hasPackage" }, + { + "command": "swift.flatDependenciesList", + "when": "false" + }, + { + "command": "swift.nestedDependenciesList", + "when": "false" + }, { "command": "swift.useLocalDependency", "when": "false" @@ -845,9 +986,29 @@ "command": "swift.reindexProject", "when": "swift.supportsReindexing" }, + { + "command": "swift.pickProcess", + "when": "false" + }, { "command": "swift.runAllTestsParallel", "when": "swift.isActivated" + }, + { + "command": "swift.runAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.debugAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.coverAllTests", + "when": "swift.isActivated" + }, + { + "command": "swift.openEducationalNote", + "when": "false" } ], "editor/context": [ @@ -862,6 +1023,18 @@ "group": "navigation" } ], + "editor/title/run": [ + { + "command": "swift.run", + "group": "navigation@0", + "when": "resourceLangId == swift && swift.currentTargetType == 'executable'" + }, + { + "command": "swift.debug", + "group": "navigation@0", + "when": "resourceLangId == swift && swift.currentTargetType == 'executable'" + } + ], "swift.editor": [ { "command": "swift.run", @@ -897,40 +1070,95 @@ "view/title": [ { "command": "swift.updateDependencies", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@1" }, { "command": "swift.resolveDependencies", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@2" }, { "command": "swift.resetPackage", - "when": "view == packageDependencies", - "group": "navigation" + "when": "view == projectPanel", + "group": "navigation@3" + }, + { + "command": "swift.flatDependenciesList", + "when": "view == projectPanel && !swift.flatDependenciesList", + "group": "navigation@4" + }, + { + "command": "swift.nestedDependenciesList", + "when": "view == projectPanel && swift.flatDependenciesList", + "group": "navigation@5" + }, + { + "command": "swift.openDocumentation", + "when": "view == projectPanel", + "group": "navigation@6" } ], "view/item/context": [ { "command": "swift.useLocalDependency", - "when": "view == packageDependencies && viewItem == remote" + "when": "view == projectPanel && viewItem == remote" }, { "command": "swift.uneditDependency", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == editing" + "when": "view == projectPanel && viewItem == editing" }, { "command": "swift.openInWorkspace", - "when": "view == packageDependencies && viewItem == local" + "when": "view == projectPanel && viewItem == local" }, { "command": "swift.openExternal", - "when": "view == packageDependencies && viewItem != local" + "when": "view == projectPanel && (viewItem == 'editing' || viewItem == 'remote')" + }, + { + "command": "swift.run", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@0" + }, + { + "command": "swift.debug", + "when": "view == projectPanel && viewItem == 'runnable'", + "group": "inline@1" + }, + { + "command": "swift.runSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugSnippet", + "when": "view == projectPanel && viewItem == 'snippet_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@0" + }, + { + "command": "swift.debugAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@1" + }, + { + "command": "swift.runAllTestsParallel", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@2" + }, + { + "command": "swift.coverAllTests", + "when": "view == projectPanel && viewItem == 'test_runnable'", + "group": "inline@3" } ] }, @@ -1127,8 +1355,8 @@ "views": { "explorer": [ { - "id": "packageDependencies", - "name": "Package Dependencies", + "id": "projectPanel", + "name": "Swift Project", "icon": "$(archive)", "when": "swift.hasPackage" } @@ -1166,8 +1394,11 @@ ], "debuggers": [ { - "type": "swift-lldb", - "label": "Swift LLDB Debugger", + "type": "swift", + "label": "Swift Debugger", + "variables": { + "pickProcess": "swift.pickProcess" + }, "configurationAttributes": { "launch": { "required": [ @@ -1216,6 +1447,10 @@ "description": "Don't retrieve STDIN, STDOUT and STDERR as the program is running.", "default": false }, + "testType": { + "type": "string", + "description": "If the program is a test, set this to the type of test (`XCTest` or `swift-testing`). This is typically set automatically and is only required when your launch program uses a non standard filename." + }, "shellExpandArguments": { "type": "boolean", "description": "Expand program arguments as a shell would without actually launching the program in a shell.", @@ -1296,6 +1531,7 @@ }, "pid": { "type": [ + "string", "number" ], "description": "System process ID to attach to." @@ -1366,12 +1602,47 @@ } } } - } + }, + "configurationSnippets": [ + { + "label": "Swift: Launch", + "description": "", + "body": { + "type": "swift", + "request": "launch", + "name": "${2:Launch Swift Executable}", + "program": "^\"\\${workspaceRoot}/.build/debug/${1:}\"", + "args": [], + "env": {}, + "cwd": "^\"\\${workspaceRoot}\"" + } + }, + { + "label": "Swift: Attach to Process", + "description": "", + "body": { + "type": "swift", + "request": "attach", + "name": "${1:Attach to Swift Executable}", + "pid": "^\"\\${command:pickProcess}\"" + } + }, + { + "label": "Swift: Attach", + "description": "", + "body": { + "type": "swift", + "request": "attach", + "name": "${2:Attach to Swift Executable}", + "program": "^\"\\${workspaceRoot}/.build/debug/${1:}\"" + } + } + ] } ] }, "extensionDependencies": [ - "vadimcn.vscode-lldb" + "llvm-vs-code-extensions.lldb-dap" ], "scripts": { "vscode:prepublish": "npm run bundle", @@ -1388,57 +1659,62 @@ "postinstall": "npm run update-swift-docc-render", "pretest": "npm run compile-tests", "soundness": "scripts/soundness.sh", + "check-package-json": "tsx ./scripts/check_package_json.ts", "test": "vscode-test", "integration-test": "npm test -- --label integrationTests", "unit-test": "npm test -- --label unitTests", "coverage": "npm test -- --coverage", "compile-tests": "del-cli ./assets/test/**/.build && npm run compile", "package": "vsce package", - "dev-package": "vsce package --no-update-package-json 2.0.1-dev", - "preview-package": "vsce package --pre-release", + "dev-package": "tsx ./scripts/dev_package.ts", + "preview-package": "tsx ./scripts/preview_package.ts", "tag": "./scripts/tag_release.sh $npm_package_version", "contributors": "./scripts/generate_contributors_list.sh" }, "devDependencies": { "@types/chai": "^4.3.19", "@types/chai-as-promised": "^7.1.8", - "@types/chai-subset": "^1.3.5", + "@types/chai-subset": "^1.3.6", "@types/glob": "^7.1.6", "@types/lcov-parse": "^1.0.2", + "@types/lodash.throttle": "^4.1.9", + "@types/lodash.debounce": "^4.0.9", "@types/mocha": "^10.0.10", "@types/mock-fs": "^4.13.4", - "@types/node": "^18.19.75", + "@types/node": "^20.17.30", "@types/plist": "^3.0.5", - "@types/semver": "^7.5.8", - "@types/sinon": "^17.0.3", + "@types/semver": "^7.7.0", + "@types/sinon": "^17.0.4", "@types/sinon-chai": "^3.2.12", "@types/vscode": "^1.88.0", "@types/xml2js": "^0.4.14", - "@typescript-eslint/eslint-plugin": "^8.23.0", - "@typescript-eslint/parser": "^8.23.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", "@vscode/debugprotocol": "^1.68.0", "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "^2.4.1", - "@vscode/vsce": "^2.32.0", + "@vscode/vsce": "^3.3.2", "chai": "^4.5.0", "chai-as-promised": "^7.1.2", "chai-subset": "^1.6.0", "del-cli": "^6.0.0", - "esbuild": "^0.24.2", + "esbuild": "^0.25.2", "eslint": "^8.57.0", - "eslint-config-prettier": "^10.0.1", + "eslint-config-prettier": "^10.1.1", + "lodash.throttle": "^4.1.1", + "lodash.debounce": "^4.0.8", "mocha": "^10.8.2", - "mock-fs": "^5.4.1", + "mock-fs": "^5.5.0", "node-pty": "^1.0.0", - "prettier": "^3.4.2", + "prettier": "^3.5.3", "semver": "^7.7.1", "simple-git": "^3.27.0", - "sinon": "^19.0.2", + "sinon": "^20.0.0", "sinon-chai": "^3.7.0", "source-map-support": "^0.5.21", "strip-ansi": "^6.0.1", - "tsx": "^4.19.2", - "typescript": "^5.7.3" + "tsx": "^4.19.3", + "typescript": "^5.8.3" }, "dependencies": { "@vscode/codicons": "^0.0.36", diff --git a/scripts/check_package_json.ts b/scripts/check_package_json.ts new file mode 100644 index 000000000..394708a67 --- /dev/null +++ b/scripts/check_package_json.ts @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ + +import { getExtensionVersion, main } from "./lib/utilities"; + +main(async () => { + const version = await getExtensionVersion(); + if (version.minor % 2 !== 0) { + throw new Error( + `Invalid version number in package.json. ${version.toString()} does not have an even numbered minor version.` + ); + } +}); diff --git a/scripts/dev_package.ts b/scripts/dev_package.ts new file mode 100644 index 000000000..9fec4c630 --- /dev/null +++ b/scripts/dev_package.ts @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ + +import { exec, getExtensionVersion, getRootDirectory, main } from "./lib/utilities"; + +main(async () => { + const rootDirectory = getRootDirectory(); + const version = await getExtensionVersion(); + // Increment the patch version from the package.json + const patch = version.patch + 1; + const devVersion = `${version.major}.${version.minor}.${patch}-dev`; + // Use VSCE to package the extension + await exec("npx", ["vsce", "package", "--no-update-package-json", devVersion], { + cwd: rootDirectory, + }); +}); diff --git a/scripts/lib/utilities.ts b/scripts/lib/utilities.ts new file mode 100644 index 000000000..3af9b17d0 --- /dev/null +++ b/scripts/lib/utilities.ts @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ + +import * as child_process from "child_process"; +import { readFile } from "fs/promises"; +import * as path from "path"; +import * as semver from "semver"; + +/** + * Executes the provided main function for the script while logging any errors. + * + * If an error is caught then the process will exit with code 1. + * + * @param mainFn The main function of the script that will be run. + */ +export async function main(mainFn: () => Promise): Promise { + try { + await mainFn(); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +/** + * Returns the root directory of the repository. + */ +export function getRootDirectory(): string { + return path.join(__dirname, "..", ".."); +} + +/** + * Retrieves the version number from the package.json. + */ +export async function getExtensionVersion(): Promise { + const packageJSON = JSON.parse( + await readFile(path.join(getRootDirectory(), "package.json"), "utf-8") + ); + if (typeof packageJSON.version !== "string") { + throw new Error("Version number in package.json is not a string"); + } + const version = semver.parse(packageJSON.version); + if (version === null) { + throw new Error("Unable to parse version number in package.json"); + } + return version; +} + +/** + * Executes the given command, inheriting the current process' stdio. + * + * @param command The command to execute. + * @param args The arguments to provide to the command. + * @param options The options for executing the command. + */ +export async function exec( + command: string, + args: string[], + options: child_process.SpawnOptionsWithoutStdio = {} +): Promise { + let logMessage = "> " + command; + if (args.length > 0) { + logMessage += " " + args.join(" "); + } + console.log(logMessage + "\n"); + return new Promise((resolve, reject) => { + const childProcess = child_process.spawn(command, args, { stdio: "inherit", ...options }); + childProcess.once("error", reject); + childProcess.once("close", (code, signal) => { + if (signal !== null) { + reject(new Error(`Process exited due to signal '${signal}'`)); + } else if (code !== 0) { + reject(new Error(`Process exited with code ${code}`)); + } else { + resolve(); + } + console.log(""); + }); + }); +} diff --git a/scripts/preview_package.ts b/scripts/preview_package.ts new file mode 100644 index 000000000..a2216d963 --- /dev/null +++ b/scripts/preview_package.ts @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +/* eslint-disable no-console */ + +import { exec, getExtensionVersion, getRootDirectory, main } from "./lib/utilities"; + +/** + * Formats the given date as a string in the form "YYYYMMdd". + * + * @param date The date to format as a string. + * @returns The formatted date. + */ +function formatDate(date: Date): string { + const year = date.getUTCFullYear().toString().padStart(4, "0"); + const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); + const day = date.getUTCDate().toString().padStart(2, "0"); + return year + month + day; +} + +main(async () => { + const rootDirectory = getRootDirectory(); + const version = await getExtensionVersion(); + // Increment the minor version and set the patch version to today's date + const minor = version.minor + 1; + const patch = formatDate(new Date()); + const previewVersion = `${version.major}.${minor}.${patch}`; + // Make sure that the new minor version is odd + if (minor % 2 !== 1) { + throw new Error( + `The minor version for the pre-release extension is even (${previewVersion}).` + + " The version in the package.json has probably been incorrectly set to an odd minor version." + ); + } + // Use VSCE to package the extension + await exec( + "npx", + ["vsce", "package", "--pre-release", "--no-update-package-json", previewVersion], + { cwd: rootDirectory } + ); +}); diff --git a/scripts/soundness.sh b/scripts/soundness.sh index 894b7e875..e877feebb 100755 --- a/scripts/soundness.sh +++ b/scripts/soundness.sh @@ -36,6 +36,9 @@ function replace_acceptable_years() { sed -e 's/20[12][0123456789]-20[12][0123456789]/YEARS/' -e 's/20[12][0123456789]/YEARS/' } +printf "=> Checking package.json..." +npm run check-package-json + printf "=> Checking license headers... " tmp=$(mktemp /tmp/.vscode-swift-soundness_XXXXXX) @@ -100,7 +103,7 @@ EOF \( \! -path './assets/*' -a \ \( \! -path './coverage/*' -a \ \( "${matching_files[@]}" \) \ - \) \) \) \) \) \) \) \) + \) \) \) \) \) \) \) } | while read -r line; do if [[ "$(replace_acceptable_years < "$line" | head -n "$expected_lines" | shasum)" != "$expected_sha" ]]; then printf "\033[0;31mmissing headers in file '%s'!\033[0m\n" "$line" diff --git a/scripts/test_windows.ps1 b/scripts/test_windows.ps1 index 78a45704f..06e93e25e 100644 --- a/scripts/test_windows.ps1 +++ b/scripts/test_windows.ps1 @@ -12,16 +12,94 @@ ## ##===----------------------------------------------------------------------===## -$env:CI = "1" -$env:FAST_TEST_RUN = "1" +function Update-SwiftBuildAndPackageArguments { + param ( + [string]$jsonFilePath = "./assets/test/.vscode/settings.json", + [string]$windowsSdkVersion = "10.0.22000.0", + [string]$vcToolsVersion = "14.43.34808" + ) + + $windowsSdkRoot = "C:\Program Files (x86)\Windows Kits\10\" + + try { + $jsonContent = Get-Content -Raw -Path $jsonFilePath | ConvertFrom-Json + } catch { + Write-Host "Invalid JSON content in $jsonFilePath" + exit 1 + } + + if ($jsonContent.PSObject.Properties['swift.buildArguments']) { + $jsonContent.PSObject.Properties.Remove('swift.buildArguments') + } + + $jsonContent | Add-Member -MemberType NoteProperty -Name "swift.buildArguments" -Value @( + "-Xbuild-tools-swiftc", "-windows-sdk-root", "-Xbuild-tools-swiftc", $windowsSdkRoot, + "-Xbuild-tools-swiftc", "-windows-sdk-version", "-Xbuild-tools-swiftc", $windowsSdkVersion, + "-Xbuild-tools-swiftc", "-visualc-tools-version", "-Xbuild-tools-swiftc", $vcToolsVersion, + "-Xswiftc", "-windows-sdk-root", "-Xswiftc", $windowsSdkRoot, + "-Xswiftc", "-windows-sdk-version", "-Xswiftc", $windowsSdkVersion, + "-Xswiftc", "-visualc-tools-version", "-Xswiftc", $vcToolsVersion + ) + + if ($jsonContent.PSObject.Properties['swift.packageArguments']) { + $jsonContent.PSObject.Properties.Remove('swift.packageArguments') + } + + $jsonContent | Add-Member -MemberType NoteProperty -Name "swift.packageArguments" -Value @( + "-Xbuild-tools-swiftc", "-windows-sdk-root", "-Xbuild-tools-swiftc", $windowsSdkRoot, + "-Xbuild-tools-swiftc", "-windows-sdk-version", "-Xbuild-tools-swiftc", $windowsSdkVersion, + "-Xbuild-tools-swiftc", "-visualc-tools-version", "-Xbuild-tools-swiftc", $vcToolsVersion, + "-Xswiftc", "-windows-sdk-root", "-Xswiftc", $windowsSdkRoot, + "-Xswiftc", "-windows-sdk-version", "-Xswiftc", $windowsSdkVersion, + "-Xswiftc", "-visualc-tools-version", "-Xswiftc", $vcToolsVersion + ) + + $jsonContent | ConvertTo-Json -Depth 32 | Set-Content -Path $jsonFilePath + + Write-Host "Contents of ${jsonFilePath}:" + Get-Content -Path $jsonFilePath +} + +$swiftVersionOutput = & swift --version +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to execute 'swift --version'" + exit 1 +} + +Write-Host "Swift version:" +Write-Host "$swiftVersionOutput" + +$versionLine = $swiftVersionOutput[0] +if ($versionLine -match "Swift version (\d+)\.(\d+)") { + Write-Host "Matched Swift version: $($matches[0]), $($matches[1]), $($matches[2])" + + $majorVersion = [int]$matches[1] + $minorVersion = [int]$matches[2] + + # In newer Visual C++ Tools they've added compiler intrinsics headers in wchar.h + # that end up creating a cyclic dependency between the `ucrt` and compiler intrinsics modules. + + # Newer versions of swift (>=6.1) have a fixed modulemap that resolves the issue: https://github.com/swiftlang/swift/pull/79751 + # As a workaround we can pin the tools/SDK versions to older versions that are present in the GH Actions Windows image. + # In the future we may only want to apply this workaround to older versions of Swift that don't have the fixed module map. + if ($majorVersion -lt 6 -or ($majorVersion -eq 6 -and $minorVersion -lt 1)) { + Write-Host "Swift version is < 6.1, injecting windows SDK build arguments" + Update-SwiftBuildAndPackageArguments + } +} else { + Write-Host "Match failed for output: `"$versionLine`"" + Write-Host "Unable to determine Swift version" + exit 1 +} + npm ci -ignore-script node-pty npm run lint npm run format npm run package -$Process = Start-Process npm "run integration-test" -Wait -PassThru -NoNewWindow -if ($Process.ExitCode -eq 0) { +npm run test +if ($LASTEXITCODE -eq 0) { Write-Host 'SUCCESS' } else { - Write-Host ('FAILED ({0})' -f $Process.ExitCode) + Write-Host ('FAILED ({0})' -f $LASTEXITCODE) exit 1 } diff --git a/scripts/update_swift_docc_render.ts b/scripts/update_swift_docc_render.ts index 689f0dc9d..f7aef5878 100644 --- a/scripts/update_swift_docc_render.ts +++ b/scripts/update_swift_docc_render.ts @@ -14,11 +14,11 @@ /* eslint-disable no-console */ import simpleGit, { ResetMode } from "simple-git"; -import { spawn } from "child_process"; import { stat, mkdtemp, mkdir, rm, readdir } from "fs/promises"; import * as path from "path"; import { tmpdir } from "os"; import * as semver from "semver"; +import { exec, getRootDirectory, main } from "./lib/utilities"; function checkNodeVersion() { const nodeVersion = semver.parse(process.versions.node); @@ -27,9 +27,9 @@ function checkNodeVersion() { "Unable to determine the version of NodeJS that this script is running under." ); } - if (!semver.satisfies(nodeVersion, "18")) { + if (!semver.satisfies(nodeVersion, "20")) { throw new Error( - `Cannot build swift-docc-render with NodeJS v${nodeVersion.raw}. Please install and use NodeJS v18.` + `Cannot build swift-docc-render with NodeJS v${nodeVersion.raw}. Please install and use NodeJS v20.` ); } } @@ -57,34 +57,8 @@ async function cloneSwiftDocCRender(buildDirectory: string): Promise { return swiftDocCRenderDirectory; } -async function exec( - command: string, - args: string[], - options: { cwd?: string; env?: { [key: string]: string } } = {} -): Promise { - let logMessage = "> " + command; - if (args.length > 0) { - logMessage += " " + args.join(" "); - } - console.log(logMessage + "\n"); - return new Promise((resolve, reject) => { - const childProcess = spawn(command, args, { stdio: "inherit", ...options }); - childProcess.once("error", reject); - childProcess.once("close", (code, signal) => { - if (signal !== null) { - reject(new Error(`Process exited due to signal '${signal}'`)); - } else if (code !== 0) { - reject(new Error(`Process exited with code ${code}`)); - } else { - resolve(); - } - console.log(""); - }); - }); -} - -(async () => { - const outputDirectory = path.join(__dirname, "..", "assets", "swift-docc-render"); +main(async () => { + const outputDirectory = path.join(getRootDirectory(), "assets", "swift-docc-render"); if (process.argv.includes("postinstall")) { try { await stat(outputDirectory); @@ -114,7 +88,4 @@ async function exec( console.error(error); }); } -})().catch(error => { - console.error(error); - process.exit(1); }); diff --git a/src/BackgroundCompilation.ts b/src/BackgroundCompilation.ts index 583788b08..f54f48534 100644 --- a/src/BackgroundCompilation.ts +++ b/src/BackgroundCompilation.ts @@ -13,63 +13,67 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import * as path from "path"; -import { isPathInsidePath } from "./utilities/filesystem"; import { getBuildAllTask } from "./tasks/SwiftTaskProvider"; import configuration from "./configuration"; import { FolderContext } from "./FolderContext"; -import { WorkspaceContext } from "./WorkspaceContext"; import { TaskOperation } from "./tasks/TaskQueue"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import debounce = require("lodash.debounce"); -export class BackgroundCompilation { - private waitingToRun = false; +export class BackgroundCompilation implements vscode.Disposable { + private workspaceFileWatcher?: vscode.FileSystemWatcher; + private configurationEventDisposable?: vscode.Disposable; + private validFileTypes = ["swift", "c", "cpp", "h", "hpp", "m", "mm"]; + private disposables: vscode.Disposable[] = []; - constructor(private folderContext: FolderContext) {} - - /** - * Start onDidSave handler which will kick off compilation tasks - * - * The task works out which folder the saved file is in and then - * will call `runTask` on the background compilation attached to - * that folder. - * */ - static start(workspaceContext: WorkspaceContext): vscode.Disposable { - const onDidSaveDocument = vscode.workspace.onDidSaveTextDocument(event => { - if (configuration.backgroundCompilation === false) { - return; - } - - // is document a valid type for rebuild - const languages = ["swift", "c", "cpp", "objective-c", "objective-cpp"]; - let foundLanguage = false; - languages.forEach(lang => { - if (event.languageId === lang) { - foundLanguage = true; + constructor(private folderContext: FolderContext) { + // We only want to configure the file watcher if background compilation is enabled. + this.configurationEventDisposable = vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration("swift.backgroundCompilation", folderContext.folder)) { + if (configuration.backgroundCompilation) { + this.setupFileWatching(); + } else { + this.stopFileWatching(); } - }); - if (foundLanguage === false) { - return; } + }); + + if (configuration.backgroundCompilation) { + this.setupFileWatching(); + } + } - // is editor document in any of the current FolderContexts - const folderContext = workspaceContext.folders.find(context => { - return isPathInsidePath(event.uri.fsPath, context.folder.fsPath); - }); + private setupFileWatching() { + const fileTypes = this.validFileTypes.join(","); + const rootFolders = ["Sources", "Tests", "Snippets", "Plugins"].join(","); + this.disposables.push( + (this.workspaceFileWatcher = vscode.workspace.createFileSystemWatcher( + `**/{${rootFolders}}/**/*.{${fileTypes}}` + )) + ); - if (!folderContext) { - return; - } + // Throttle events since many change events can be recieved in a short time if the user + // does a "Save All" or a process writes several files in quick succession. + this.disposables.push( + this.workspaceFileWatcher.onDidChange( + debounce( + () => { + this.runTask(); + }, + 100 /* 10 times per second */, + { trailing: true } + ) + ) + ); + } - // don't run auto-build if saving Package.swift as it clashes with the resolve - // that is run after the Package.swift is saved - if (path.join(folderContext.folder.fsPath, "Package.swift") === event.uri.fsPath) { - return; - } + private stopFileWatching() { + this.disposables.forEach(disposable => disposable.dispose()); + } - // run background compilation task - folderContext.backgroundCompilation.runTask(); - }); - return { dispose: () => onDidSaveDocument.dispose() }; + dispose() { + this.configurationEventDisposable?.dispose(); + this.disposables.forEach(disposable => disposable.dispose()); } /** diff --git a/src/DiagnosticsManager.ts b/src/DiagnosticsManager.ts index 7f2ea1578..c087f777d 100644 --- a/src/DiagnosticsManager.ts +++ b/src/DiagnosticsManager.ts @@ -142,6 +142,24 @@ export class DiagnosticsManager implements vscode.Disposable { d1 => isSwiftc(d1) && !!removedDiagnostics.find(d2 => isEqual(d1, d2)) ); } + + for (const diagnostic of newDiagnostics) { + if ( + diagnostic.code && + typeof diagnostic.code !== "string" && + typeof diagnostic.code !== "number" + ) { + if (diagnostic.code.target.fsPath.endsWith(".md")) { + diagnostic.code = { + target: vscode.Uri.parse( + `command:swift.openEducationalNote?${encodeURIComponent(JSON.stringify(diagnostic.code.target))}` + ), + value: "More Information...", + }; + } + } + } + // Append the new diagnostics we just received allDiagnostics.push(...newDiagnostics); this.allDiagnostics.set(uri.fsPath, allDiagnostics); @@ -356,7 +374,7 @@ export class DiagnosticsManager implements vscode.Disposable { ): ParsedDiagnostic | vscode.DiagnosticRelatedInformation | undefined { const diagnosticRegex = /^(?:\S+\s+)?(.*?):(\d+)(?::(\d+))?:\s+(warning|error|note):\s+(.*)$/g; - const switfcExtraWarningsRegex = /\[-W.*?\]/g; + const switfcExtraWarningsRegex = /\[(-W|#).*?\]/g; const match = diagnosticRegex.exec(line); if (!match) { return; diff --git a/src/FolderContext.ts b/src/FolderContext.ts index f95fd8a66..f9299038e 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -22,6 +22,7 @@ import { WorkspaceContext, FolderOperation } from "./WorkspaceContext"; import { BackgroundCompilation } from "./BackgroundCompilation"; import { TaskQueue } from "./tasks/TaskQueue"; import { isPathInsidePath } from "./utilities/filesystem"; +import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; export class FolderContext implements vscode.Disposable { private packageWatcher: PackageWatcher; @@ -54,6 +55,7 @@ export class FolderContext implements vscode.Disposable { this.linuxMain?.dispose(); this.packageWatcher.dispose(); this.testExplorer?.dispose(); + this.backgroundCompilation.dispose(); } /** @@ -128,13 +130,14 @@ export class FolderContext implements vscode.Disposable { await this.swiftPackage.reloadPackageResolved(); } + /** reload workspace-state.json for this folder */ + async reloadWorkspaceState() { + await this.swiftPackage.reloadWorkspaceState(); + } + /** Load Swift Plugins and store in Package */ - async loadSwiftPlugins() { - const plugins = await SwiftPackage.loadPlugins( - this.folder, - this.workspaceContext.toolchain - ); - this.swiftPackage.plugins = plugins; + async loadSwiftPlugins(outputChannel: SwiftOutputChannel) { + await this.swiftPackage.loadSwiftPlugins(this.workspaceContext.toolchain, outputChannel); } /** diff --git a/src/PackageWatcher.ts b/src/PackageWatcher.ts index 518811d55..86b4840e2 100644 --- a/src/PackageWatcher.ts +++ b/src/PackageWatcher.ts @@ -15,6 +15,7 @@ import * as vscode from "vscode"; import { FolderContext } from "./FolderContext"; import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; +import { BuildFlags } from "./toolchain/BuildFlags"; /** * Watches for changes to **Package.swift** and **Package.resolved**. @@ -25,6 +26,8 @@ import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; export class PackageWatcher { private packageFileWatcher?: vscode.FileSystemWatcher; private resolvedFileWatcher?: vscode.FileSystemWatcher; + private workspaceStateFileWatcher?: vscode.FileSystemWatcher; + private snippetWatcher?: vscode.FileSystemWatcher; constructor( private folderContext: FolderContext, @@ -38,6 +41,8 @@ export class PackageWatcher { install() { this.packageFileWatcher = this.createPackageFileWatcher(); this.resolvedFileWatcher = this.createResolvedFileWatcher(); + this.workspaceStateFileWatcher = this.createWorkspaceStateFileWatcher(); + this.snippetWatcher = this.createSnippetFileWatcher(); } /** @@ -47,6 +52,8 @@ export class PackageWatcher { dispose() { this.packageFileWatcher?.dispose(); this.resolvedFileWatcher?.dispose(); + this.workspaceStateFileWatcher?.dispose(); + this.snippetWatcher?.dispose(); } private createPackageFileWatcher(): vscode.FileSystemWatcher { @@ -69,6 +76,29 @@ export class PackageWatcher { return watcher; } + private createWorkspaceStateFileWatcher(): vscode.FileSystemWatcher { + const uri = vscode.Uri.joinPath( + vscode.Uri.file( + BuildFlags.buildDirectoryFromWorkspacePath(this.folderContext.folder.fsPath, true) + ), + "workspace-state.json" + ); + const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath); + watcher.onDidCreate(async () => await this.handleWorkspaceStateChange()); + watcher.onDidChange(async () => await this.handleWorkspaceStateChange()); + watcher.onDidDelete(async () => await this.handleWorkspaceStateChange()); + return watcher; + } + + private createSnippetFileWatcher(): vscode.FileSystemWatcher { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this.folderContext.folder, "Snippets/*.swift") + ); + watcher.onDidCreate(async () => await this.handlePackageSwiftChange()); + watcher.onDidDelete(async () => await this.handlePackageSwiftChange()); + return watcher; + } + /** * Handles a create or change event for **Package.swift**. * @@ -95,4 +125,14 @@ export class PackageWatcher { this.workspaceContext.fireEvent(this.folderContext, FolderOperation.resolvedUpdated); } } + + /** + * Handles a create or change event for **.build/workspace-state.json**. + * + * This will resolve any changes in the workspace-state. + */ + private async handleWorkspaceStateChange() { + await this.folderContext.reloadWorkspaceState(); + this.workspaceContext.fireEvent(this.folderContext, FolderOperation.workspaceStateUpdated); + } } diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index d645fbebc..cb35ca9b8 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -19,6 +19,7 @@ import { execSwift, getErrorDescription, hashString } from "./utilities/utilitie import { isPathInsidePath } from "./utilities/filesystem"; import { SwiftToolchain } from "./toolchain/toolchain"; import { BuildFlags } from "./toolchain/BuildFlags"; +import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; /** Swift Package Manager contents */ export interface PackageContents { @@ -41,16 +42,24 @@ export interface Target { c99name: string; path: string; sources: string[]; - type: "executable" | "test" | "library" | "snippet"; + type: "executable" | "test" | "library" | "snippet" | "plugin"; } /** Swift Package Manager dependency */ export interface Dependency { identity: string; - type?: string; // fileSystem, sourceControl or registry + type?: string; requirement?: object; url?: string; path?: string; + dependencies: Dependency[]; +} + +export interface ResolvedDependency extends Dependency { + version: string; + type: string; + path: string; + location: string; } /** Swift Package.resolved file */ @@ -187,7 +196,8 @@ export class SwiftPackage implements PackageContents { private constructor( readonly folder: vscode.Uri, private contents: SwiftPackageState, - public resolved: PackageResolved | undefined + public resolved: PackageResolved | undefined, + private workspaceState: WorkspaceState | undefined ) {} /** @@ -195,10 +205,14 @@ export class SwiftPackage implements PackageContents { * @param folder folder package is in * @returns new SwiftPackage */ - static async create(folder: vscode.Uri, toolchain: SwiftToolchain): Promise { + public static async create( + folder: vscode.Uri, + toolchain: SwiftToolchain + ): Promise { const contents = await SwiftPackage.loadPackage(folder, toolchain); const resolved = await SwiftPackage.loadPackageResolved(folder); - return new SwiftPackage(folder, contents, resolved); + const workspaceState = await SwiftPackage.loadWorkspaceState(folder); + return new SwiftPackage(folder, contents, resolved, workspaceState); } /** @@ -211,15 +225,28 @@ export class SwiftPackage implements PackageContents { toolchain: SwiftToolchain ): Promise { try { - let { stdout } = await execSwift(["package", "describe", "--type", "json"], toolchain, { + // Use swift package describe to describe the package targets, products, and platforms + const describe = await execSwift(["package", "describe", "--type", "json"], toolchain, { cwd: folder.fsPath, }); - // remove lines from `swift package describe` until we find a "{" - while (!stdout.startsWith("{")) { - const firstNewLine = stdout.indexOf("\n"); - stdout = stdout.slice(firstNewLine + 1); - } - return JSON.parse(stdout); + const packageState = JSON.parse( + SwiftPackage.trimStdout(describe.stdout) + ) as PackageContents; + + // Use swift package show-dependencies to get the dependencies in a tree format + const dependencies = await execSwift( + ["package", "show-dependencies", "--format", "json"], + toolchain, + { + cwd: folder.fsPath, + } + ); + + packageState.dependencies = JSON.parse( + SwiftPackage.trimStdout(dependencies.stdout) + ).dependencies; + + return packageState; } catch (error) { const execError = error as { stderr: string }; // if caught error and it begins with "error: root manifest" then there is no Package.swift @@ -237,7 +264,9 @@ export class SwiftPackage implements PackageContents { } } - static async loadPackageResolved(folder: vscode.Uri): Promise { + private static async loadPackageResolved( + folder: vscode.Uri + ): Promise { try { const uri = vscode.Uri.joinPath(folder, "Package.resolved"); const contents = await fs.readFile(uri.fsPath, "utf8"); @@ -248,9 +277,10 @@ export class SwiftPackage implements PackageContents { } } - static async loadPlugins( + private static async loadPlugins( folder: vscode.Uri, - toolchain: SwiftToolchain + toolchain: SwiftToolchain, + outputChannel: SwiftOutputChannel ): Promise { try { const { stdout } = await execSwift(["package", "plugin", "--list"], toolchain, { @@ -270,7 +300,8 @@ export class SwiftPackage implements PackageContents { } } return plugins; - } catch { + } catch (error) { + outputChannel.appendLine(`Failed to laod plugins: ${error}`); // failed to load resolved file return undefined return []; } @@ -280,12 +311,12 @@ export class SwiftPackage implements PackageContents { * Load workspace-state.json file for swift package * @returns Workspace state */ - public async loadWorkspaceState(): Promise { + private static async loadWorkspaceState( + folder: vscode.Uri + ): Promise { try { const uri = vscode.Uri.joinPath( - vscode.Uri.file( - BuildFlags.buildDirectoryFromWorkspacePath(this.folder.fsPath, true) - ), + vscode.Uri.file(BuildFlags.buildDirectoryFromWorkspacePath(folder.fsPath, true)), "workspace-state.json" ); const contents = await fs.readFile(uri.fsPath, "utf8"); @@ -306,6 +337,14 @@ export class SwiftPackage implements PackageContents { this.resolved = await SwiftPackage.loadPackageResolved(this.folder); } + public async reloadWorkspaceState() { + this.workspaceState = await SwiftPackage.loadWorkspaceState(this.folder); + } + + public async loadSwiftPlugins(toolchain: SwiftToolchain, outputChannel: SwiftOutputChannel) { + this.plugins = await SwiftPackage.loadPlugins(this.folder, toolchain, outputChannel); + } + /** Return if has valid contents */ public get isValid(): boolean { return isPackage(this.contents); @@ -325,6 +364,88 @@ export class SwiftPackage implements PackageContents { return this.contents !== undefined; } + public rootDependencies(): ResolvedDependency[] { + // Correlate the root dependencies found in the Package.swift with their + // checked out versions in the workspace-state.json. + const result = this.dependencies.map(dependency => + this.resolveDependencyAgainstWorkspaceState(dependency) + ); + return result; + } + + private resolveDependencyAgainstWorkspaceState(dependency: Dependency): ResolvedDependency { + const workspaceStateDep = this.workspaceState?.object.dependencies.find( + dep => dep.packageRef.identity === dependency.identity + ); + return { + ...dependency, + version: workspaceStateDep?.state.checkoutState?.version ?? "", + path: workspaceStateDep + ? this.dependencyPackagePath(workspaceStateDep, this.folder.fsPath) + : "", + type: workspaceStateDep ? this.dependencyType(workspaceStateDep) : "", + location: workspaceStateDep ? workspaceStateDep.packageRef.location : "", + }; + } + + public childDependencies(dependency: Dependency): ResolvedDependency[] { + return dependency.dependencies.map(dep => this.resolveDependencyAgainstWorkspaceState(dep)); + } + + /** + * * Get package source path of dependency + * `editing`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath + * `local`: dependency.packageRef.location + * `remote`: buildDirectory + checkouts + dependency.packageRef.location + * @param dependency + * @param workspaceFolder + * @return the package path based on the type + */ + private dependencyPackagePath( + dependency: WorkspaceStateDependency, + workspaceFolder: string + ): string { + const type = this.dependencyType(dependency); + if (type === "editing") { + return ( + dependency.state.path ?? path.join(workspaceFolder, "Packages", dependency.subpath) + ); + } else if (type === "local") { + return dependency.state.path ?? dependency.packageRef.location; + } else { + // remote + const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath( + workspaceFolder, + true + ); + if (dependency.packageRef.kind === "registry") { + return path.join(buildDirectory, "registry", "downloads", dependency.subpath); + } else { + return path.join(buildDirectory, "checkouts", dependency.subpath); + } + } + } + + /** + * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local + * @param dependency + * @return "local" | "remote" | "editing" + */ + private dependencyType(dependency: WorkspaceStateDependency): "local" | "remote" | "editing" { + if (dependency.state.name === "edited") { + return "editing"; + } else if ( + dependency.packageRef.kind === "local" || + dependency.packageRef.kind === "fileSystem" + ) { + // need to check for both "local" and "fileSystem" as swift 5.5 and earlier + // use "local" while 5.6 and later use "fileSystem" + return "local"; + } else { + return "remote"; + } + } + /** name of Swift Package */ get name(): string { return (this.contents as PackageContents)?.name ?? ""; @@ -375,6 +496,15 @@ export class SwiftPackage implements PackageContents { const filePath = path.relative(this.folder.fsPath, file); return this.targets.find(target => isPathInsidePath(filePath, target.path)); } + + private static trimStdout(stdout: string): string { + // remove lines from `swift package describe` until we find a "{" + while (!stdout.startsWith("{")) { + const firstNewLine = stdout.indexOf("\n"); + stdout = stdout.slice(firstNewLine + 1); + } + return stdout; + } } export enum TargetType { diff --git a/src/SwiftSnippets.ts b/src/SwiftSnippets.ts index f7fa4cccb..22898b94a 100644 --- a/src/SwiftSnippets.ts +++ b/src/SwiftSnippets.ts @@ -48,29 +48,44 @@ export function setSnippetContextKey(ctx: WorkspaceContext) { * If current file is a Swift Snippet run it * @param ctx Workspace Context */ -export async function runSnippet(ctx: WorkspaceContext): Promise { - return await debugSnippetWithOptions(ctx, { noDebug: true }); +export async function runSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, { noDebug: true }, snippet); } /** * If current file is a Swift Snippet run it in the debugger * @param ctx Workspace Context */ -export async function debugSnippet(ctx: WorkspaceContext): Promise { - return await debugSnippetWithOptions(ctx, {}); +export async function debugSnippet( + ctx: WorkspaceContext, + snippet?: string +): Promise { + return await debugSnippetWithOptions(ctx, {}, snippet); } export async function debugSnippetWithOptions( ctx: WorkspaceContext, - options: vscode.DebugSessionOptions + options: vscode.DebugSessionOptions, + snippet?: string ): Promise { + // create build task + let snippetName: string; + if (snippet) { + snippetName = snippet; + } else if (ctx.currentDocument) { + snippetName = path.basename(ctx.currentDocument.fsPath, ".swift"); + } else { + return false; + } + const folderContext = ctx.currentFolder; - if (!ctx.currentDocument || !folderContext) { - return; + if (!folderContext) { + return false; } - // create build task - const snippetName = path.basename(ctx.currentDocument.fsPath, ".swift"); const snippetBuildTask = createSwiftTask( ["build", "--product", snippetName], `Build ${snippetName}`, @@ -84,26 +99,29 @@ export async function debugSnippetWithOptions( }, ctx.toolchain ); - + const snippetDebugConfig = createSnippetConfiguration(snippetName, folderContext); try { + ctx.buildStarted(snippetName, snippetDebugConfig, options); + // queue build task and when it is complete run executable in the debugger return await folderContext.taskQueue .queueOperation(new TaskOperation(snippetBuildTask)) .then(result => { if (result === 0) { - const snippetDebugConfig = createSnippetConfiguration( - snippetName, - folderContext - ); return debugLaunchConfig( folderContext.workspaceFolder, snippetDebugConfig, options ); } + }) + .then(result => { + ctx.buildFinished(snippetName, snippetDebugConfig, options); + return result; }); - } catch { + } catch (error) { + ctx.outputChannel.appendLine(`Failed to debug snippet: ${error}`); // ignore error if task failed to run - return; + return false; } } diff --git a/src/TestExplorer/LSPTestDiscovery.ts b/src/TestExplorer/LSPTestDiscovery.ts index fa2ebf88f..b91517d91 100644 --- a/src/TestExplorer/LSPTestDiscovery.ts +++ b/src/TestExplorer/LSPTestDiscovery.ts @@ -20,7 +20,10 @@ import { WorkspaceTestsRequest, } from "../sourcekit-lsp/extensions"; import { SwiftPackage, TargetType } from "../SwiftPackage"; -import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager"; +import { + checkExperimentalCapability, + LanguageClientManager, +} from "../sourcekit-lsp/LanguageClientManager"; import { LanguageClient } from "vscode-languageclient/node"; /** @@ -45,7 +48,7 @@ export class LSPTestDiscovery { return await this.languageClient.useLanguageClient(async (client, token) => { // Only use the lsp for this request if it supports the // textDocument/tests method, and is at least version 2. - if (this.checkExperimentalCapability(client, TextDocumentTestsRequest.method, 2)) { + if (checkExperimentalCapability(client, TextDocumentTestsRequest.method, 2)) { const testsInDocument = await client.sendRequest( TextDocumentTestsRequest.type, { textDocument: { uri: document.toString() } }, @@ -66,7 +69,7 @@ export class LSPTestDiscovery { return await this.languageClient.useLanguageClient(async (client, token) => { // Only use the lsp for this request if it supports the // workspace/tests method, and is at least version 2. - if (this.checkExperimentalCapability(client, WorkspaceTestsRequest.method, 2)) { + if (checkExperimentalCapability(client, WorkspaceTestsRequest.method, 2)) { const tests = await client.sendRequest(WorkspaceTestsRequest.type, token); return this.transformToTestClass(client, swiftPackage, tests); } else { @@ -75,23 +78,6 @@ export class LSPTestDiscovery { }); } - /** - * Returns `true` if the LSP supports the supplied `method` at or - * above the supplied `minVersion`. - */ - private checkExperimentalCapability( - client: LanguageClient, - method: string, - minVersion: number - ) { - const experimentalCapability = client.initializeResult?.capabilities.experimental; - if (!experimentalCapability) { - throw new Error(`${method} requests not supported`); - } - const targetCapability = experimentalCapability[method]; - return (targetCapability?.version ?? -1) >= minVersion; - } - /** * Convert from `LSPTestItem[]` to `TestDiscovery.TestClass[]`, * updating the format of the location. diff --git a/src/TestExplorer/TestExplorer.ts b/src/TestExplorer/TestExplorer.ts index e4123ca20..15e62221e 100644 --- a/src/TestExplorer/TestExplorer.ts +++ b/src/TestExplorer/TestExplorer.ts @@ -412,6 +412,9 @@ export class TestExplorer { * @param errorDescription Error description to display */ private setErrorTestItem(errorDescription: string | undefined, title = "Test Discovery Error") { + this.folderContext.workspaceContext.outputChannel.log( + `Test Discovery Error: ${errorDescription}` + ); this.controller.items.forEach(item => { this.controller.items.delete(item.id); }); diff --git a/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts b/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts index 7a354a928..aa0fc973d 100644 --- a/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts +++ b/src/TestExplorer/TestParsers/SwiftTestingOutputParser.ts @@ -530,7 +530,7 @@ export class SymbolRenderer { case TestSymbol.skip: case TestSymbol.passWithKnownIssue: case TestSymbol.fail: - return "\u{00D7}"; // Unicode: MULTIPLICATION SIGN + return "\u{279C}"; // Unicode: HEAVY ROUND-TIPPED RIGHTWARDS ARROW case TestSymbol.pass: return "\u{221A}"; // Unicode: SQUARE ROOT case TestSymbol.difference: @@ -551,7 +551,7 @@ export class SymbolRenderer { case TestSymbol.skip: case TestSymbol.passWithKnownIssue: case TestSymbol.fail: - return "\u{2718}"; // Unicode: HEAVY BALLOT X + return "\u{279C}"; // Unicode: HEAVY ROUND-TIPPED RIGHTWARDS ARROW case TestSymbol.pass: return "\u{2714}"; // Unicode: HEAVY CHECK MARK case TestSymbol.difference: diff --git a/src/TestExplorer/TestParsers/XCTestOutputParser.ts b/src/TestExplorer/TestParsers/XCTestOutputParser.ts index e373e8517..ed6a7a861 100644 --- a/src/TestExplorer/TestParsers/XCTestOutputParser.ts +++ b/src/TestExplorer/TestParsers/XCTestOutputParser.ts @@ -92,8 +92,9 @@ export class ParallelXCTestOutputParser implements IXCTestOutputParser { public parseResult(output: string, runState: ITestRunState) { // From 5.7 to 5.10 running with the --parallel option dumps the test results out // to the console with no newlines, so it isn't possible to distinguish where errors - // begin and end. Consequently we can't record them, and so we manually mark them - // as passed or failed here with a manufactured issue. + // begin and end. Consequently we can't record them. For these versions we rely on the + // generated xunit XML, which we can parse and mark tests as passed or failed here with + // manufactured issues. // Don't attempt to parse the console output of parallel tests between 5.7 and 5.10 // as it doesn't have newlines. You might get lucky and find the output is split // in the right spot, but more often than not we wont be able to parse it. @@ -112,8 +113,42 @@ export class ParallelXCTestOutputParser implements IXCTestOutputParser { /* eslint-disable @typescript-eslint/no-unused-vars */ class ParallelXCTestRunStateProxy implements ITestRunState { + // Note this must remain stateless as its recreated on + // every `parseResult` call in `ParallelXCTestOutputParser` constructor(private runState: ITestRunState) {} + get excess(): typeof this.runState.excess { + return this.runState.excess; + } + + set excess(value: typeof this.runState.excess) { + this.runState.excess = value; + } + + get activeSuite(): typeof this.runState.activeSuite { + return this.runState.activeSuite; + } + + set activeSuite(value: typeof this.runState.activeSuite) { + this.runState.activeSuite = value; + } + + get pendingSuiteOutput(): typeof this.runState.pendingSuiteOutput { + return this.runState.pendingSuiteOutput; + } + + set pendingSuiteOutput(value: typeof this.runState.pendingSuiteOutput) { + this.runState.pendingSuiteOutput = value; + } + + get failedTest(): typeof this.runState.failedTest { + return this.runState.failedTest; + } + + set failedTest(value: typeof this.runState.failedTest) { + this.runState.failedTest = value; + } + getTestItemIndex(id: string, filename: string | undefined): number { return this.runState.getTestItemIndex(id, filename); } @@ -427,8 +462,8 @@ export class XCTestOutputParser implements IXCTestOutputParser { const match = message.match(regex); if (match && match[1] !== match[2]) { return { - expected: match[1], - actual: match[2], + actual: match[1], + expected: match[2], }; } diff --git a/src/TestExplorer/TestRunArguments.ts b/src/TestExplorer/TestRunArguments.ts index 027e3c2ec..d6d4a5fa6 100644 --- a/src/TestExplorer/TestRunArguments.ts +++ b/src/TestExplorer/TestRunArguments.ts @@ -93,11 +93,12 @@ export class TestRunArguments { const terminator = hasChildren ? "/" : "$"; // Debugging XCTests requires exact matches, so we don't need a trailing terminator. return isDebug ? arg.id : `${arg.id}${terminator}`; - } else { + } else if (hasChildren) { // Append a trailing slash to match a suite name exactly. // This prevents TestTarget.MySuite matching TestTarget.MySuite2. return `${arg.id}/`; } + return arg.id; }); } diff --git a/src/TestExplorer/TestRunner.ts b/src/TestExplorer/TestRunner.ts index 1e13e3486..35a4bb644 100644 --- a/src/TestExplorer/TestRunner.ts +++ b/src/TestExplorer/TestRunner.ts @@ -122,6 +122,8 @@ export class TestRunProxy { if (this.runStarted) { return; } + + this.resetTags(this.controller); this.runStarted = true; // When a test run starts we need to do several things: @@ -191,6 +193,14 @@ export class TestRunProxy { const attachments = this.attachments[testIndex] ?? []; attachments.push(attachment); this.attachments[testIndex] = attachments; + + const testItem = this.testItems[testIndex]; + if (testItem) { + testItem.tags = [ + ...testItem.tags, + new vscode.TestTag(TestRunProxy.Tags.HAS_ATTACHMENT), + ]; + } }; public getTestIndex(id: string, filename?: string): number { @@ -214,6 +224,8 @@ export class TestRunProxy { } public skipped(test: vscode.TestItem) { + test.tags = [...test.tags, new vscode.TestTag(TestRunProxy.Tags.SKIPPED)]; + this.runState.skipped.push(test); this.testRun?.skipped(test); } @@ -323,6 +335,20 @@ export class TestRunProxy { // Compute final coverage numbers if any coverage info has been captured during the run. await this.coverage.computeCoverage(this.testRun); } + + private static Tags = { + SKIPPED: "skipped", + HAS_ATTACHMENT: "hasAttachment", + }; + + // Remove any tags that were added due to test results + private resetTags(controller: vscode.TestController) { + function removeTestRunTags(_acc: void, test: vscode.TestItem) { + const tags = Object.values(TestRunProxy.Tags); + test.tags = test.tags.filter(tag => !tags.includes(tag.id)); + } + reduceTestItemChildren(controller.items, removeTestRunTags, void 0); + } } /** Class used to run tests */ @@ -520,6 +546,18 @@ export class TestRunner { ]; } + /** + * Extracts a list of unique test Targets from the list of test items. + */ + private testTargets(items: vscode.TestItem[]): string[] { + const targets = new Set(); + for (const item of items) { + const target = item.id.split(".")[0]; + targets.add(target); + } + return Array.from(targets); + } + /** * Test run handler. Run a series of tests and extracts the results from the output * @param shouldDebug Should we run the debugger @@ -527,6 +565,9 @@ export class TestRunner { * @returns When complete */ async runHandler() { + const testTargets = this.testTargets(this.testArgs.testItems); + this.workspaceContext.testsStarted(this.folderContext, this.testKind, testTargets); + const runState = new TestRunnerTestRunState(this.testRun); const cancellationDisposable = this.testRun.token.onCancellationRequested(() => { @@ -551,6 +592,8 @@ export class TestRunner { cancellationDisposable.dispose(); await this.testRun.end(); + + this.workspaceContext.testsFinished(this.folderContext, this.testKind, testTargets); } /** Run test session without attaching to a debugger */ @@ -577,7 +620,7 @@ export class TestRunner { fifoPipePath, attachmentFolder ); - const testBuildConfig = await TestingConfigurationFactory.swiftTestingConfig( + const testBuildConfig = TestingConfigurationFactory.swiftTestingConfig( this.folderContext, swiftTestingArgs, this.testKind, @@ -612,7 +655,7 @@ export class TestRunner { } if (this.testArgs.hasXCTests) { - const testBuildConfig = await TestingConfigurationFactory.xcTestConfig( + const testBuildConfig = TestingConfigurationFactory.xcTestConfig( this.folderContext, this.testKind, this.testArgs.xcTestArgs, @@ -715,7 +758,8 @@ export class TestRunner { presentationOptions: { reveal: vscode.TaskRevealKind.Never }, }, this.folderContext.workspaceContext.toolchain, - { ...process.env, ...testBuildConfig.env } + { ...process.env, ...testBuildConfig.env }, + { readOnlyTerminal: process.platform !== "win32" } ); task.execution.onDidWrite(str => { @@ -729,7 +773,15 @@ export class TestRunner { outputStream.write(replaced); }); + // If the test run is iterrupted by a cancellation request from VS Code, ensure the task is terminated. + const cancellationDisposable = this.testRun.token.onCancellationRequested(() => { + task.execution.terminate("SIGINT"); + reject("Test run cancelled"); + }); + task.execution.onDidClose(code => { + cancellationDisposable.dispose(); + // undefined or 0 are viewed as success if (!code) { resolve(); @@ -854,7 +906,7 @@ export class TestRunner { attachmentFolder ); - const swiftTestBuildConfig = await TestingConfigurationFactory.swiftTestingConfig( + const swiftTestBuildConfig = TestingConfigurationFactory.swiftTestingConfig( this.folderContext, swiftTestingArgs, this.testKind, @@ -888,7 +940,7 @@ export class TestRunner { // create launch config for testing if (this.testArgs.hasXCTests) { - const xcTestBuildConfig = await TestingConfigurationFactory.xcTestConfig( + const xcTestBuildConfig = TestingConfigurationFactory.xcTestConfig( this.folderContext, this.testKind, this.testArgs.xcTestArgs, @@ -925,7 +977,6 @@ export class TestRunner { return; } - // add cancelation const startSession = vscode.debug.onDidStartDebugSession(session => { if (config.testType === TestLibrary.xctest) { this.testRun.testRunStarted(); @@ -937,6 +988,8 @@ export class TestRunner { ); const outputHandler = this.testOutputHandler(config.testType, runState); + outputHandler(`> ${config.program} ${config.args.join(" ")}\n\n\r`); + LoggingDebugAdapterTracker.setDebugSessionCallback( session, this.workspaceContext.outputChannel, @@ -945,6 +998,7 @@ export class TestRunner { } ); + // add cancellation const cancellation = this.testRun.token.onCancellationRequested(() => { this.workspaceContext.outputChannel.logDiagnostic( "Test Debugging Cancelled", @@ -971,11 +1025,6 @@ export class TestRunner { ); } - // show test results pane - vscode.commands.executeCommand( - "testing.showMostRecentOutput" - ); - const terminateSession = vscode.debug.onDidTerminateDebugSession(() => { this.workspaceContext.outputChannel.logDiagnostic( diff --git a/src/TestExplorer/TestXUnitParser.ts b/src/TestExplorer/TestXUnitParser.ts index 8ef064d92..8a4e19062 100644 --- a/src/TestExplorer/TestXUnitParser.ts +++ b/src/TestExplorer/TestXUnitParser.ts @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// import * as xml2js from "xml2js"; -import { TestRunnerTestRunState } from "./TestRunner"; -import { OutputChannel } from "vscode"; +import { ITestRunState } from "./TestParsers/TestRunState"; +import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; export interface TestResults { tests: number; @@ -49,8 +49,8 @@ export class TestXUnitParser { async parse( buffer: string, - runState: TestRunnerTestRunState, - outputChannel: OutputChannel + runState: ITestRunState, + outputChannel: SwiftOutputChannel ): Promise { const xml = await xml2js.parseStringPromise(buffer); try { @@ -63,7 +63,8 @@ export class TestXUnitParser { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async parseXUnit(xUnit: XUnit, runState: TestRunnerTestRunState): Promise { + async parseXUnit(xUnit: XUnit, runState: ITestRunState): Promise { + // eslint-disable-next-line no-console let tests = 0; let failures = 0; let errors = 0; @@ -77,7 +78,7 @@ export class TestXUnitParser { testsuite.testcase.forEach(testcase => { className = testcase.$.classname; const id = `${className}/${testcase.$.name}`; - const index = runState.getTestItemIndex(id); + const index = runState.getTestItemIndex(id, undefined); // From 5.7 to 5.10 running with the --parallel option dumps the test results out // to the console with no newlines, so it isn't possible to distinguish where errors @@ -86,7 +87,8 @@ export class TestXUnitParser { if (!!testcase.failure && !this.hasMultiLineParallelTestOutput) { runState.recordIssue( index, - testcase.failure.shift()?.$.message ?? "Test Failed" + testcase.failure.shift()?.$.message ?? "Test Failed", + false ); } runState.completed(index, { duration: testcase.$.time }); diff --git a/src/WorkspaceContext.ts b/src/WorkspaceContext.ts index 474412f5a..ed5d26b8c 100644 --- a/src/WorkspaceContext.ts +++ b/src/WorkspaceContext.ts @@ -17,24 +17,23 @@ import * as path from "path"; import { FolderContext } from "./FolderContext"; import { StatusItem } from "./ui/StatusItem"; import { SwiftOutputChannel } from "./ui/SwiftOutputChannel"; -import { swiftLibraryPathKey, getErrorDescription } from "./utilities/utilities"; -import { pathExists, isPathInsidePath } from "./utilities/filesystem"; -import { getLLDBLibPath } from "./debugger/lldb"; +import { swiftLibraryPathKey } from "./utilities/utilities"; +import { isPathInsidePath } from "./utilities/filesystem"; import { LanguageClientManager } from "./sourcekit-lsp/LanguageClientManager"; import { TemporaryFolder } from "./utilities/tempFolder"; import { TaskManager } from "./tasks/TaskManager"; -import { BackgroundCompilation } from "./BackgroundCompilation"; import { makeDebugConfigurations } from "./debugger/launch"; import configuration from "./configuration"; import contextKeys from "./contextKeys"; import { setSnippetContextKey } from "./SwiftSnippets"; import { CommentCompletionProviders } from "./editor/CommentCompletion"; -import { DebugAdapter, LaunchConfigType } from "./debugger/debugAdapter"; import { SwiftBuildStatus } from "./ui/SwiftBuildStatus"; import { SwiftToolchain } from "./toolchain/toolchain"; import { DiagnosticsManager } from "./DiagnosticsManager"; import { DocumentationManager } from "./documentation/DocumentationManager"; import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp/extensions"; +import { TestKind } from "./TestExplorer/TestKind"; +import { isValidWorkspaceFolder, searchForPackages } from "./utilities/workspace"; /** * Context for whole workspace. Holds array of contexts for each workspace folder @@ -55,6 +54,17 @@ export class WorkspaceContext implements vscode.Disposable { private lastFocusUri: vscode.Uri | undefined; private initialisationFinished = false; + private readonly testStartEmitter = new vscode.EventEmitter(); + private readonly testFinishEmitter = new vscode.EventEmitter(); + + public onDidStartTests = this.testStartEmitter.event; + public onDidFinishTests = this.testFinishEmitter.event; + + private readonly buildStartEmitter = new vscode.EventEmitter(); + private readonly buildFinishEmitter = new vscode.EventEmitter(); + public onDidStartBuild = this.buildStartEmitter.event; + public onDidFinishBuild = this.buildFinishEmitter.event; + private constructor( extensionContext: vscode.ExtensionContext, public tempFolder: TemporaryFolder, @@ -110,7 +120,6 @@ export class WorkspaceContext implements vscode.Disposable { }); } }); - const backgroundCompilationOnDidSave = BackgroundCompilation.start(this); const contextKeysUpdate = this.onDidChangeFolders(event => { switch (event.operation) { case FolderOperation.remove: @@ -163,7 +172,6 @@ export class WorkspaceContext implements vscode.Disposable { swiftFileWatcher, onDidEndTask, this.commentCompletionProvider, - backgroundCompilationOnDidSave, contextKeysUpdate, onChangeConfig, this.tasks, @@ -338,6 +346,30 @@ export class WorkspaceContext implements vscode.Disposable { await this.fireEvent(folderContext, FolderOperation.focus); } + public testsFinished(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testFinishEmitter.fire({ kind, folder, targets }); + } + + public testsStarted(folder: FolderContext, kind: TestKind, targets: string[]) { + this.testStartEmitter.fire({ kind, folder, targets }); + } + + public buildStarted( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildStartEmitter.fire({ targetName, launchConfig, options }); + } + + public buildFinished( + targetName: string, + launchConfig: vscode.DebugConfiguration, + options: vscode.DebugSessionOptions + ) { + this.buildFinishEmitter.fire({ targetName, launchConfig, options }); + } + /** * catch workspace folder changes and add or remove folders based on those changes * @param event workspace folder event @@ -357,38 +389,19 @@ export class WorkspaceContext implements vscode.Disposable { * @param folder folder being added */ async addWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) { - await this.searchForPackages(workspaceFolder.uri, workspaceFolder); - - if (this.getActiveWorkspaceFolder(vscode.window.activeTextEditor) === workspaceFolder) { - await this.focusTextEditor(vscode.window.activeTextEditor); - } - } + const folders = await searchForPackages( + workspaceFolder.uri, + configuration.disableSwiftPMIntegration, + configuration.folder(workspaceFolder).searchSubfoldersForPackages + ); - async searchForPackages(folder: vscode.Uri, workspaceFolder: vscode.WorkspaceFolder) { - // add folder if Package.swift/compile_commands.json/compile_flags.txt/buildServer.json exists - if (await this.isValidWorkspaceFolder(folder.fsPath)) { + for (const folder of folders) { await this.addPackageFolder(folder, workspaceFolder); - return; - } - // should I search sub-folders for more Swift Packages - if (!configuration.folder(workspaceFolder).searchSubfoldersForPackages) { - return; } - await vscode.workspace.fs.readDirectory(folder).then(async entries => { - for (const entry of entries) { - if ( - entry[1] === vscode.FileType.Directory && - entry[0][0] !== "." && - entry[0] !== "Packages" - ) { - await this.searchForPackages( - vscode.Uri.joinPath(folder, entry[0]), - workspaceFolder - ); - } - } - }); + if (this.getActiveWorkspaceFolder(vscode.window.activeTextEditor) === workspaceFolder) { + await this.focusTextEditor(vscode.window.activeTextEditor); + } } public async addPackageFolder( @@ -444,76 +457,6 @@ export class WorkspaceContext implements vscode.Disposable { return { dispose: () => this.swiftFileObservers.delete(listener) }; } - /** find LLDB version and setup path in CodeLLDB */ - async setLLDBVersion() { - // check we are using CodeLLDB - if (DebugAdapter.getLaunchConfigType(this.swiftVersion) !== LaunchConfigType.CODE_LLDB) { - return; - } - const libPathResult = await getLLDBLibPath(this.toolchain); - if (!libPathResult.success) { - // if failure message is undefined then fail silently - if (!libPathResult.failure) { - return; - } - const errorMessage = `Error: ${getErrorDescription(libPathResult.failure)}`; - vscode.window.showErrorMessage( - `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` - ); - this.outputChannel.log(`Failed to setup CodeLLDB: ${errorMessage}`); - return; - } - - const libPath = libPathResult.success; - const lldbConfig = vscode.workspace.getConfiguration("lldb"); - const configLLDBPath = lldbConfig.get("library"); - const expressions = lldbConfig.get("launch.expressions"); - if (configLLDBPath === libPath && expressions === "native") { - return; - } - - // show dialog for setting up LLDB - vscode.window - .showInformationMessage( - "The Swift extension needs to update some CodeLLDB settings to enable debugging features. Do you want to set this up in your global settings or the workspace settings?", - "Global", - "Workspace", - "Cancel" - ) - .then(result => { - switch (result) { - case "Global": - lldbConfig.update("library", libPath, vscode.ConfigurationTarget.Global); - lldbConfig.update( - "launch.expressions", - "native", - vscode.ConfigurationTarget.Global - ); - // clear workspace setting - lldbConfig.update( - "library", - undefined, - vscode.ConfigurationTarget.Workspace - ); - // clear workspace setting - lldbConfig.update( - "launch.expressions", - undefined, - vscode.ConfigurationTarget.Workspace - ); - break; - case "Workspace": - lldbConfig.update("library", libPath, vscode.ConfigurationTarget.Workspace); - lldbConfig.update( - "launch.expressions", - "native", - vscode.ConfigurationTarget.Workspace - ); - break; - } - }); - } - /** set focus based on the file a TextEditor is editing */ async focusTextEditor(editor?: vscode.TextEditor) { await this.focusUri(editor?.document.uri); @@ -633,13 +576,7 @@ export class WorkspaceContext implements vscode.Disposable { * Package.swift or a CMake compile_commands.json, compile_flags.txt, or a BSP buildServer.json. */ async isValidWorkspaceFolder(folder: string): Promise { - return ( - ((await pathExists(folder, "Package.swift")) && - !configuration.disableSwiftPMIntegration) || - (await pathExists(folder, "compile_commands.json")) || - (await pathExists(folder, "compile_flags.txt")) || - (await pathExists(folder, "buildServer.json")) - ); + return await isValidWorkspaceFolder(folder, configuration.disableSwiftPMIntegration); } /** send unfocus event to current focussed folder and clear current folder */ @@ -666,6 +603,20 @@ export class WorkspaceContext implements vscode.Disposable { private swiftFileObservers = new Set<(listener: SwiftFileEvent) => unknown>(); } +/** Test events for test run begin/end */ +interface TestEvent { + kind: TestKind; + folder: FolderContext; + targets: string[]; +} + +/** Build events for build + run start/stop */ +interface BuildEvent { + targetName: string; + launchConfig: vscode.DebugConfiguration; + options: vscode.DebugSessionOptions; +} + /** Workspace Folder Operation types */ export enum FolderOperation { // Package folder has been added @@ -680,6 +631,12 @@ export enum FolderOperation { packageUpdated = "packageUpdated", // Package.resolved has been updated resolvedUpdated = "resolvedUpdated", + // .build/workspace-state.json has been updated + workspaceStateUpdated = "workspaceStateUpdated", + // .build/workspace-state.json has been updated + packageViewUpdated = "packageViewUpdated", + // Package plugins list has been updated + pluginsUpdated = "pluginsUpdated", } /** Workspace Folder Event */ diff --git a/src/commands.ts b/src/commands.ts index 12c38cdd9..9bd986ef6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -12,9 +12,10 @@ // //===----------------------------------------------------------------------===// +import * as path from "path"; import * as vscode from "vscode"; import { WorkspaceContext } from "./WorkspaceContext"; -import { PackageNode } from "./ui/PackageDependencyProvider"; +import { PackageNode } from "./ui/ProjectPanelProvider"; import { SwiftToolchain } from "./toolchain/toolchain"; import { debugSnippet, runSnippet } from "./SwiftSnippets"; import { showToolchainSelectionQuickPick } from "./ui/ToolchainSelection"; @@ -31,6 +32,7 @@ import { openInExternalEditor } from "./commands/openInExternalEditor"; import { switchPlatform } from "./commands/switchPlatform"; import { insertFunctionComment } from "./commands/insertFunctionComment"; import { createNewProject } from "./commands/createNewProject"; +import { openEducationalNote } from "./commands/openEducationalNote"; import { openPackage } from "./commands/openPackage"; import { resolveDependencies } from "./commands/dependencies/resolve"; import { resetPackage } from "./commands/resetPackage"; @@ -38,7 +40,12 @@ import { updateDependencies } from "./commands/dependencies/update"; import { runPluginTask } from "./commands/runPluginTask"; import { runTestMultipleTimes } from "./commands/testMultipleTimes"; import { newSwiftFile } from "./commands/newFile"; -import { runAllTestsParallel } from "./commands/runParallelTests"; +import { runAllTests } from "./commands/runAllTests"; +import { updateDependenciesViewList } from "./commands/dependencies/updateDepViewList"; +import { runTask } from "./commands/runTask"; +import { TestKind } from "./TestExplorer/TestKind"; +import { pickProcess } from "./commands/pickProcess"; +import { openDocumentation } from "./commands/openDocumentation"; /** * References: @@ -61,6 +68,9 @@ export function registerToolchainCommands( vscode.commands.registerCommand("swift.selectToolchain", () => showToolchainSelectionQuickPick(toolchain) ), + vscode.commands.registerCommand("swift.pickProcess", configuration => + pickProcess(configuration) + ), ]; } @@ -69,13 +79,23 @@ export enum Commands { DEBUG = "swift.debug", CLEAN_BUILD = "swift.cleanBuild", RESOLVE_DEPENDENCIES = "swift.resolveDependencies", + SHOW_FLAT_DEPENDENCIES_LIST = "swift.flatDependenciesList", + SHOW_NESTED_DEPENDENCIES_LIST = "swift.nestedDependenciesList", UPDATE_DEPENDENCIES = "swift.updateDependencies", RUN_TESTS_MULTIPLE_TIMES = "swift.runTestsMultipleTimes", RESET_PACKAGE = "swift.resetPackage", USE_LOCAL_DEPENDENCY = "swift.useLocalDependency", UNEDIT_DEPENDENCY = "swift.uneditDependency", + RUN_TASK = "swift.runTask", RUN_PLUGIN_TASK = "swift.runPluginTask", + RUN_SNIPPET = "swift.runSnippet", + DEBUG_SNIPPET = "swift.debugSnippet", PREVIEW_DOCUMENTATION = "swift.previewDocumentation", + RUN_ALL_TESTS = "swift.runAllTests", + RUN_ALL_TESTS_PARALLEL = "swift.runAllTestsParallel", + DEBUG_ALL_TESTS = "swift.debugAllTests", + COVER_ALL_TESTS = "swift.coverAllTests", + OPEN_MANIFEST = "swift.openManifest", } /** @@ -90,8 +110,12 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand(Commands.UPDATE_DEPENDENCIES, () => updateDependencies(ctx) ), - vscode.commands.registerCommand(Commands.RUN, () => runBuild(ctx)), - vscode.commands.registerCommand(Commands.DEBUG, () => debugBuild(ctx)), + vscode.commands.registerCommand(Commands.RUN, target => + runBuild(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand(Commands.DEBUG, target => + debugBuild(ctx, ...unwrapTreeItem(target)) + ), vscode.commands.registerCommand(Commands.CLEAN_BUILD, () => cleanBuild(ctx)), vscode.commands.registerCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, item => { if (ctx.currentFolder) { @@ -103,8 +127,10 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return runTestMultipleTimes(ctx.currentFolder, item, true); } }), - // Note: This is only available on macOS (gated in `package.json`) because its the only OS that has the iOS SDK available. - vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform()), + // Note: switchPlatform is only available on macOS and Swift 6.1 or later + // (gated in `package.json`) because it's the only OS and toolchain combination that + // has Darwin SDKs available and supports code editing with SourceKit-LSP + vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform(ctx)), vscode.commands.registerCommand(Commands.RESET_PACKAGE, () => resetPackage(ctx)), vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)), vscode.commands.registerCommand("swift.openPackage", () => { @@ -112,9 +138,14 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return openPackage(ctx.toolchain.swiftVersion, ctx.currentFolder.folder); } }), - vscode.commands.registerCommand("swift.runSnippet", () => runSnippet(ctx)), - vscode.commands.registerCommand("swift.debugSnippet", () => debugSnippet(ctx)), + vscode.commands.registerCommand(Commands.RUN_SNIPPET, target => + runSnippet(ctx, ...unwrapTreeItem(target)) + ), + vscode.commands.registerCommand(Commands.DEBUG_SNIPPET, target => + debugSnippet(ctx, ...unwrapTreeItem(target)) + ), vscode.commands.registerCommand(Commands.RUN_PLUGIN_TASK, () => runPluginTask()), + vscode.commands.registerCommand(Commands.RUN_TASK, name => runTask(ctx, name)), vscode.commands.registerCommand("swift.restartLSPServer", () => ctx.languageClientManager.restart() ), @@ -147,18 +178,57 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return openInExternalEditor(item); } }), - vscode.commands.registerCommand("swift.attachDebugger", () => attachDebugger(ctx)), + vscode.commands.registerCommand("swift.attachDebugger", attachDebugger), vscode.commands.registerCommand("swift.clearDiagnosticsCollection", () => ctx.diagnostics.clear() ), vscode.commands.registerCommand("swift.captureDiagnostics", () => captureDiagnostics(ctx)), vscode.commands.registerCommand( - "swift.runAllTestsParallel", - async () => await runAllTestsParallel(ctx) + Commands.RUN_ALL_TESTS_PARALLEL, + async item => await runAllTests(ctx, TestKind.parallel, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.RUN_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.standard, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.DEBUG_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.debug, ...unwrapTreeItem(item)) + ), + vscode.commands.registerCommand( + Commands.COVER_ALL_TESTS, + async item => await runAllTests(ctx, TestKind.coverage, ...unwrapTreeItem(item)) ), vscode.commands.registerCommand( Commands.PREVIEW_DOCUMENTATION, async () => await ctx.documentation.launchDocumentationPreview() ), + vscode.commands.registerCommand(Commands.SHOW_FLAT_DEPENDENCIES_LIST, () => + updateDependenciesViewList(ctx, true) + ), + vscode.commands.registerCommand(Commands.SHOW_NESTED_DEPENDENCIES_LIST, () => + updateDependenciesViewList(ctx, false) + ), + vscode.commands.registerCommand("swift.openEducationalNote", uri => + openEducationalNote(uri) + ), + vscode.commands.registerCommand(Commands.OPEN_MANIFEST, (uri: vscode.Uri) => { + const packagePath = path.join(uri.fsPath, "Package.swift"); + vscode.commands.executeCommand("vscode.open", vscode.Uri.file(packagePath)); + }), + vscode.commands.registerCommand("swift.openDocumentation", () => openDocumentation()), ]; } + +/** + * Certain commands can be called via a vscode TreeView, which will pass a {@link CommandNode} object. + * If the command is called via a command palette or other means, the target will be a string. + */ +function unwrapTreeItem(target?: string | { args: string[] }): string[] { + if (typeof target === "object" && target !== null && "args" in target) { + return target.args ?? []; + } else if (typeof target === "string") { + return [target]; + } + return []; +} diff --git a/src/commands/attachDebugger.ts b/src/commands/attachDebugger.ts index a25ac9763..558910015 100644 --- a/src/commands/attachDebugger.ts +++ b/src/commands/attachDebugger.ts @@ -13,9 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { WorkspaceContext } from "../WorkspaceContext"; -import { getLldbProcess } from "../debugger/lldb"; -import { LaunchConfigType } from "../debugger/debugAdapter"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "../debugger/debugAdapter"; /** * Attaches the LLDB debugger to a running process selected by the user. @@ -29,20 +27,11 @@ import { LaunchConfigType } from "../debugger/debugAdapter"; * * @throws Will display an error message if no processes are available, or if the debugger fails to attach to the selected process. */ -export async function attachDebugger(ctx: WorkspaceContext) { - const processPickItems = await getLldbProcess(ctx); - if (processPickItems !== undefined) { - const picked = await vscode.window.showQuickPick(processPickItems, { - placeHolder: "Select Process", - }); - if (picked) { - const debugConfig: vscode.DebugConfiguration = { - type: LaunchConfigType.SWIFT_EXTENSION, - request: "attach", - name: "Attach", - pid: picked.pid, - }; - await vscode.debug.startDebugging(undefined, debugConfig); - } - } +export async function attachDebugger() { + await vscode.debug.startDebugging(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + name: "Attach", + pid: "${command:pickProcess}", + }); } diff --git a/src/commands/build.ts b/src/commands/build.ts index 03e66489a..1ce2744d3 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -18,19 +18,20 @@ import { createSwiftTask, SwiftTaskProvider } from "../tasks/SwiftTaskProvider"; import { debugLaunchConfig, getLaunchConfiguration } from "../debugger/launch"; import { executeTaskWithUI } from "./utilities"; import { FolderContext } from "../FolderContext"; +import { Target } from "../SwiftPackage"; /** * Executes a {@link vscode.Task task} to run swift target. */ -export async function runBuild(ctx: WorkspaceContext) { - return await debugBuildWithOptions(ctx, { noDebug: true }); +export async function runBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, { noDebug: true }, target); } /** * Executes a {@link vscode.Task task} to debug swift target. */ -export async function debugBuild(ctx: WorkspaceContext) { - return await debugBuildWithOptions(ctx, {}); +export async function debugBuild(ctx: WorkspaceContext, target?: string) { + return await debugBuildWithOptions(ctx, {}, target); } /** @@ -70,7 +71,8 @@ export async function folderCleanBuild(folderContext: FolderContext) { */ export async function debugBuildWithOptions( ctx: WorkspaceContext, - options: vscode.DebugSessionOptions + options: vscode.DebugSessionOptions, + targetName?: string ) { const current = ctx.currentFolder; if (!current) { @@ -80,13 +82,19 @@ export async function debugBuildWithOptions( return; } - const file = vscode.window.activeTextEditor?.document.fileName; - if (!file) { - ctx.outputChannel.appendLine("debugBuildWithOptions: No active text editor"); - return; + let target: Target | undefined; + if (targetName) { + target = current.swiftPackage.targets.find(target => target.name === targetName); + } else { + const file = vscode.window.activeTextEditor?.document.fileName; + if (!file) { + ctx.outputChannel.appendLine("debugBuildWithOptions: No active text editor"); + return; + } + + target = current.swiftPackage.getTarget(file); } - const target = current.swiftPackage.getTarget(file); if (!target) { ctx.outputChannel.appendLine("debugBuildWithOptions: No active target"); return; @@ -101,6 +109,9 @@ export async function debugBuildWithOptions( const launchConfig = getLaunchConfiguration(target.name, current); if (launchConfig) { - return debugLaunchConfig(current.workspaceFolder, launchConfig, options); + ctx.buildStarted(target.name, launchConfig, options); + const result = await debugLaunchConfig(current.workspaceFolder, launchConfig, options); + ctx.buildFinished(target.name, launchConfig, options); + return result; } } diff --git a/src/commands/dependencies/unedit.ts b/src/commands/dependencies/unedit.ts index 521d71afd..720709edd 100644 --- a/src/commands/dependencies/unedit.ts +++ b/src/commands/dependencies/unedit.ts @@ -44,7 +44,12 @@ async function uneditFolderDependency( ) { try { const uneditOperation = new SwiftExecOperation( - ["package", "unedit", ...args, identifier], + ctx.toolchain.buildFlags.withAdditionalFlags([ + "package", + "unedit", + ...args, + identifier, + ]), folder, `Finish editing ${identifier}`, { showStatusItem: true, checkAlreadyRunning: false, log: "Unedit" }, diff --git a/src/commands/dependencies/updateDepViewList.ts b/src/commands/dependencies/updateDepViewList.ts new file mode 100644 index 000000000..04cbb4400 --- /dev/null +++ b/src/commands/dependencies/updateDepViewList.ts @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import contextKeys from "../../contextKeys"; +import { FolderOperation, WorkspaceContext } from "../../WorkspaceContext"; + +export function updateDependenciesViewList(ctx: WorkspaceContext, flatList: boolean) { + if (ctx.currentFolder) { + contextKeys.flatDependenciesList = flatList; + ctx.fireEvent(ctx.currentFolder, FolderOperation.packageViewUpdated); + } +} diff --git a/src/commands/dependencies/useLocal.ts b/src/commands/dependencies/useLocal.ts index e108cee71..c75a3c6c2 100644 --- a/src/commands/dependencies/useLocal.ts +++ b/src/commands/dependencies/useLocal.ts @@ -50,7 +50,13 @@ export async function useLocalDependency( folder = folders[0]; } const task = createSwiftTask( - ["package", "edit", "--path", folder.fsPath, identifier], + ctx.toolchain.buildFlags.withAdditionalFlags([ + "package", + "edit", + "--path", + folder.fsPath, + identifier, + ]), "Edit Package Dependency", { scope: currentFolder.workspaceFolder, diff --git a/src/commands/openDocumentation.ts b/src/commands/openDocumentation.ts new file mode 100644 index 000000000..db703ce2a --- /dev/null +++ b/src/commands/openDocumentation.ts @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; + +/** + * Handle the user requesting to show the vscode-swift documentation. + */ +export async function openDocumentation(): Promise { + return await vscode.env.openExternal( + vscode.Uri.parse("https://www.swift.org/vscode/documentation/vscode") + ); +} diff --git a/src/commands/openEducationalNote.ts b/src/commands/openEducationalNote.ts new file mode 100644 index 000000000..ea3dbe677 --- /dev/null +++ b/src/commands/openEducationalNote.ts @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; + +/** + * Handle the user requesting to show an educational note. + * + * The default behaviour is to open it in a markdown preview to the side. + */ +export async function openEducationalNote(markdownFile: vscode.Uri | undefined): Promise { + await vscode.commands.executeCommand("markdown.showPreviewToSide", markdownFile); +} diff --git a/src/commands/openInExternalEditor.ts b/src/commands/openInExternalEditor.ts index 29f4114ab..6dc621765 100644 --- a/src/commands/openInExternalEditor.ts +++ b/src/commands/openInExternalEditor.ts @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { PackageNode } from "../ui/PackageDependencyProvider"; +import { PackageNode } from "../ui/ProjectPanelProvider"; /** * Opens the supplied `PackageNode` externally using the default application. diff --git a/src/commands/openInWorkspace.ts b/src/commands/openInWorkspace.ts index 7b4b9601d..dda3903c0 100644 --- a/src/commands/openInWorkspace.ts +++ b/src/commands/openInWorkspace.ts @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { PackageNode } from "../ui/PackageDependencyProvider"; +import { PackageNode } from "../ui/ProjectPanelProvider"; /** * Open a local package in workspace diff --git a/src/commands/pickProcess.ts b/src/commands/pickProcess.ts new file mode 100644 index 000000000..e18e8b791 --- /dev/null +++ b/src/commands/pickProcess.ts @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as path from "path"; +import * as vscode from "vscode"; +import { createProcessList } from "../process-list"; + +interface ProcessQuickPick extends vscode.QuickPickItem { + processId?: number; +} + +/** + * Prompts the user to select a running process. + * + * The return value must be a string so that it is compatible with VS Code's + * string substitution infrastructure. The value will eventually be converted + * to a number by the debug configuration provider. + * + * @param configuration The related debug configuration, if any + * @returns The pid of the process as a string or undefined if cancelled. + */ +export async function pickProcess( + configuration?: vscode.DebugConfiguration +): Promise { + const processList = createProcessList(); + const selectedProcess = await vscode.window.showQuickPick( + processList.listAllProcesses().then((processes): ProcessQuickPick[] => { + // Sort by start date in descending order + processes.sort((a, b) => b.start - a.start); + // Filter by program if requested + if (typeof configuration?.program === "string") { + const program = configuration.program; + const programBaseName = path.basename(program); + processes = processes + .filter(proc => path.basename(proc.command) === programBaseName) + .sort((a, b) => { + // Bring exact command matches to the top + const aIsExactMatch = a.command === program ? 1 : 0; + const bIsExactMatch = b.command === program ? 1 : 0; + return bIsExactMatch - aIsExactMatch; + }); + // Show a better message if all processes were filtered out + if (processes.length === 0) { + return [ + { + label: "No processes matched the debug configuration's program", + }, + ]; + } + } + // Convert to a QuickPickItem + return processes.map(proc => { + return { + processId: proc.id, + label: path.basename(proc.command), + description: proc.id.toString(), + detail: proc.arguments, + } satisfies ProcessQuickPick; + }); + }), + { + placeHolder: "Select a process to attach the debugger to", + matchOnDetail: true, + matchOnDescription: true, + } + ); + return selectedProcess?.processId?.toString(); +} diff --git a/src/commands/resetPackage.ts b/src/commands/resetPackage.ts index 6d621dee2..84c0c9141 100644 --- a/src/commands/resetPackage.ts +++ b/src/commands/resetPackage.ts @@ -35,7 +35,10 @@ export async function resetPackage(ctx: WorkspaceContext) { */ export async function folderResetPackage(folderContext: FolderContext) { const task = createSwiftTask( - ["package", "reset"], + folderContext.workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + "reset", + ]), "Reset Package Dependencies", { cwd: folderContext.folder, diff --git a/src/commands/runParallelTests.ts b/src/commands/runAllTests.ts similarity index 64% rename from src/commands/runParallelTests.ts rename to src/commands/runAllTests.ts index 11970b803..f629417ad 100644 --- a/src/commands/runParallelTests.ts +++ b/src/commands/runAllTests.ts @@ -17,23 +17,29 @@ import { TestKind } from "../TestExplorer/TestKind"; import { WorkspaceContext } from "../WorkspaceContext"; import { flattenTestItemCollection } from "../TestExplorer/TestUtils"; -export async function runAllTestsParallel(ctx: WorkspaceContext) { +export async function runAllTests(ctx: WorkspaceContext, testKind: TestKind, target?: string) { const testExplorer = ctx.currentFolder?.testExplorer; if (testExplorer === undefined) { return; } - const profile = testExplorer.testRunProfiles.find( - profile => profile.label === TestKind.parallel - ); + const profile = testExplorer.testRunProfiles.find(profile => profile.label === testKind); if (profile === undefined) { return; } - const tests = flattenTestItemCollection(testExplorer.controller.items); + let tests = flattenTestItemCollection(testExplorer.controller.items); + + // If a target is specified, filter the tests to only run those that match the target. + if (target) { + const targetRegex = new RegExp(`^${target}(\\.|$)`); + tests = tests.filter(test => targetRegex.test(test.id)); + } const tokenSource = new vscode.CancellationTokenSource(); await profile.runHandler( new vscode.TestRunRequest(tests, undefined, profile), tokenSource.token ); + + await vscode.commands.executeCommand("testing.showMostRecentOutput"); } diff --git a/src/commands/runSwiftScript.ts b/src/commands/runSwiftScript.ts index 5eb77bfc9..713534160 100644 --- a/src/commands/runSwiftScript.ts +++ b/src/commands/runSwiftScript.ts @@ -18,6 +18,7 @@ import * as fs from "fs/promises"; import { createSwiftTask } from "../tasks/SwiftTaskProvider"; import { WorkspaceContext } from "../WorkspaceContext"; import { Version } from "../utilities/version"; +import configuration from "../configuration"; /** * Run the active document through the Swift REPL @@ -40,6 +41,29 @@ export async function runSwiftScript(ctx: WorkspaceContext) { return; } + let target: string; + + const defaultVersion = configuration.scriptSwiftLanguageVersion; + if (defaultVersion === "Ask Every Run") { + const picked = await vscode.window.showQuickPick( + [ + // Potentially add more versions here + { value: "5", label: "Swift 5" }, + { value: "6", label: "Swift 6" }, + ], + { + placeHolder: "Select a target Swift version", + } + ); + + if (!picked) { + return; + } + target = picked.value; + } else { + target = defaultVersion; + } + let filename = document.fileName; let isTempFile = false; if (document.isUntitled) { @@ -52,9 +76,8 @@ export async function runSwiftScript(ctx: WorkspaceContext) { // otherwise save document await document.save(); } - const runTask = createSwiftTask( - [filename], + ["-swift-version", target, filename], `Run ${filename}`, { scope: vscode.TaskScope.Global, diff --git a/src/commands/runTask.ts b/src/commands/runTask.ts new file mode 100644 index 000000000..3bca73535 --- /dev/null +++ b/src/commands/runTask.ts @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { TaskOperation } from "../tasks/TaskQueue"; +import { SwiftPluginTaskProvider } from "../tasks/SwiftPluginTaskProvider"; + +export const runTask = async (ctx: WorkspaceContext, name: string) => { + if (!ctx.currentFolder) { + return; + } + + const tasks = await vscode.tasks.fetchTasks(); + let task = tasks.find(task => task.name === name); + if (!task) { + const pluginTaskProvider = new SwiftPluginTaskProvider(ctx); + const pluginTasks = await pluginTaskProvider.provideTasks( + new vscode.CancellationTokenSource().token + ); + task = pluginTasks.find(task => task.name === name); + } + + if (!task) { + vscode.window.showErrorMessage(`Task "${name}" not found`); + return; + } + + return ctx.currentFolder.taskQueue + .queueOperation(new TaskOperation(task)) + .then(result => result === 0); +}; diff --git a/src/commands/switchPlatform.ts b/src/commands/switchPlatform.ts index 821a8ccfb..e65dfda2c 100644 --- a/src/commands/switchPlatform.ts +++ b/src/commands/switchPlatform.ts @@ -13,13 +13,18 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { DarwinCompatibleTarget, SwiftToolchain } from "../toolchain/toolchain"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "../toolchain/toolchain"; import configuration from "../configuration"; +import { WorkspaceContext } from "../WorkspaceContext"; /** - * Switches the target SDK to the platform selected in a QuickPick UI. + * Switches the appropriate SDK setting to the platform selected in a QuickPick UI. */ -export async function switchPlatform() { +export async function switchPlatform(ctx: WorkspaceContext) { const picked = await vscode.window.showQuickPick( [ { value: undefined, label: "macOS" }, @@ -29,28 +34,34 @@ export async function switchPlatform() { { value: DarwinCompatibleTarget.visionOS, label: "visionOS" }, ], { - placeHolder: "Select a new target", + placeHolder: "Select a new target platform", } ); if (picked) { + // show a status item as getSDKForTarget can sometimes take a long while to run xcrun to find the SDK + const statusItemText = `Setting target platform to ${picked.label}`; + ctx.statusItem.start(statusItemText); try { - const sdkForTarget = picked.value - ? await SwiftToolchain.getSDKForTarget(picked.value) - : ""; - if (sdkForTarget !== undefined) { - if (sdkForTarget !== "") { - configuration.sdk = sdkForTarget; - vscode.window.showWarningMessage( - `Selecting the ${picked.label} SDK will provide code editing support, but compiling with this SDK will have undefined results.` - ); - } else { - configuration.sdk = undefined; - } + if (picked.value) { + // verify that the SDK for the platform actually exists + await SwiftToolchain.getSDKForTarget(picked.value); + } + const swiftSDKTriple = picked.value ? getDarwinTargetTriple(picked.value) : ""; + if (swiftSDKTriple !== "") { + // set a swiftSDK for non-macOS Darwin platforms so that SourceKit-LSP can provide syntax highlighting + configuration.swiftSDK = swiftSDKTriple; + vscode.window.showWarningMessage( + `Selecting the ${picked.label} target platform will provide code editing support, but compiling with a ${picked.label} SDK will have undefined results.` + ); } else { - vscode.window.showErrorMessage("Unable to obtain requested SDK path"); + // set swiftSDK to an empty string for macOS and other platforms + configuration.swiftSDK = ""; } } catch { - vscode.window.showErrorMessage("Unable to obtain requested SDK path"); + vscode.window.showErrorMessage( + `Unable set the Swift SDK setting to ${picked.label}, verify that the SDK exists` + ); } + ctx.statusItem.end(statusItemText); } } diff --git a/src/configuration.ts b/src/configuration.ts index d282378d1..1cdfceca4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -13,10 +13,18 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; +import * as os from "os"; +import * as path from "path"; -type CFamilySupportOptions = "enable" | "disable" | "cpptools-inactive"; -type ActionAfterBuildError = "Focus Problems" | "Focus Terminal" | "Do Nothing"; -type OpenAfterCreateNewProjectOptions = +export type DebugAdapters = "auto" | "lldb-dap" | "CodeLLDB"; +export type SetupCodeLLDBOptions = + | "prompt" + | "alwaysUpdateGlobal" + | "alwaysUpdateWorkspace" + | "never"; +export type CFamilySupportOptions = "enable" | "disable" | "cpptools-inactive"; +export type ActionAfterBuildError = "Focus Problems" | "Focus Terminal" | "Do Nothing"; +export type OpenAfterCreateNewProjectOptions = | "always" | "alwaysNewWindow" | "whenNoFolderOpen" @@ -47,10 +55,14 @@ export interface LSPConfiguration { /** debugger configuration */ export interface DebuggerConfiguration { - /** Whether or not to use CodeLLDB for debugging instead of lldb-dap */ - readonly useDebugAdapterFromToolchain: boolean; + /** Get the underlying debug adapter type requested by the user. */ + readonly debugAdapter: DebugAdapters; /** Return path to debug adapter */ readonly customDebugAdapterPath: string; + /** Whether or not to disable setting up the debugger */ + readonly disable: boolean; + /** User choices for updating CodeLLDB settings */ + readonly setupCodeLLDB: SetupCodeLLDBOptions; } /** workspace folder configuration */ @@ -68,7 +80,9 @@ export interface FolderConfiguration { /** location to save swift-testing attachments */ readonly attachmentsPath: string; /** look up saved permissions for the supplied plugin */ - pluginPermissions(pluginId: string): PluginPermissionConfiguration; + pluginPermissions(pluginId?: string): PluginPermissionConfiguration; + /** look up saved arguments for the supplied plugin, or global plugin arguments if no plugin id is provided */ + pluginArguments(pluginId?: string): string[]; } export interface PluginPermissionConfiguration { @@ -94,14 +108,17 @@ const configuration = { get lsp(): LSPConfiguration { return { get serverPath(): string { - return vscode.workspace - .getConfiguration("swift.sourcekit-lsp") - .get("serverPath", ""); + return substituteVariablesInString( + vscode.workspace + .getConfiguration("swift.sourcekit-lsp") + .get("serverPath", "") + ); }, get serverArguments(): string[] { return vscode.workspace .getConfiguration("swift.sourcekit-lsp") - .get("serverArguments", []); + .get("serverArguments", []) + .map(substituteVariablesInString); }, get inlayHintsEnabled(): boolean { return vscode.workspace @@ -133,6 +150,42 @@ const configuration = { }, folder(workspaceFolder: vscode.WorkspaceFolder): FolderConfiguration { + function pluginSetting( + setting: string, + pluginId?: string, + resultIsArray: boolean = false + ): T | undefined { + if (!pluginId) { + // Check for * as a wildcard plugin ID for configurations that want both + // global arguments as well as specific additional arguments for a plugin. + const wildcardSetting = pluginSetting(setting, "*", resultIsArray) as T | undefined; + if (wildcardSetting) { + return wildcardSetting; + } + + // Check if there is a global setting like `"swift.pluginArguments": ["-c", "release"]` + // that should apply to all plugins. + const args = vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get(setting); + + if (resultIsArray && Array.isArray(args)) { + return args; + } else if ( + !resultIsArray && + args !== null && + typeof args === "object" && + Object.keys(args).length !== 0 + ) { + return args; + } + return undefined; + } + + return vscode.workspace.getConfiguration("swift", workspaceFolder).get<{ + [key: string]: T; + }>(setting, {})[pluginId]; + } return { /** Environment variables to set when running tests */ get testEnvironmentVariables(): { [key: string]: string } { @@ -144,7 +197,8 @@ const configuration = { get additionalTestArguments(): string[] { return vscode.workspace .getConfiguration("swift", workspaceFolder) - .get("additionalTestArguments", []); + .get("additionalTestArguments", []) + .map(substituteVariablesInString); }, /** auto-generate launch.json configurations */ get autoGenerateLaunchConfigurations(): boolean { @@ -165,39 +219,64 @@ const configuration = { .get("searchSubfoldersForPackages", false); }, get attachmentsPath(): string { - return vscode.workspace - .getConfiguration("swift", workspaceFolder) - .get("attachmentsPath", "./.build/attachments"); - }, - pluginPermissions(pluginId: string): PluginPermissionConfiguration { - return ( - vscode.workspace.getConfiguration("swift", workspaceFolder).get<{ - [key: string]: PluginPermissionConfiguration; - }>("pluginPermissions", {})[pluginId] ?? {} + return substituteVariablesInString( + vscode.workspace + .getConfiguration("swift", workspaceFolder) + .get("attachmentsPath", "./.build/attachments") ); }, + pluginPermissions(pluginId?: string): PluginPermissionConfiguration { + return pluginSetting("pluginPermissions", pluginId, false) ?? {}; + }, + pluginArguments(pluginId?: string): string[] { + return pluginSetting("pluginArguments", pluginId, true) ?? []; + }, }; }, /** debugger configuration */ get debugger(): DebuggerConfiguration { return { - get useDebugAdapterFromToolchain(): boolean { - // Enabled by default only when we're on Windows arm64 since CodeLLDB does not support - // this platform and gives an awful error message. + get debugAdapter(): DebugAdapters { + // Use inspect to determine if the user has explicitly set swift.debugger.useDebugAdapterFromToolchain + const inspectUseDebugAdapterFromToolchain = vscode.workspace + .getConfiguration("swift.debugger") + .inspect("useDebugAdapterFromToolchain"); + let useDebugAdapterFromToolchain = + inspectUseDebugAdapterFromToolchain?.workspaceValue ?? + inspectUseDebugAdapterFromToolchain?.globalValue; + // On Windows arm64 we enable swift.debugger.useDebugAdapterFromToolchain by default since CodeLLDB does + // not support this platform and gives an awful error message. if (process.platform === "win32" && process.arch === "arm64") { - // We need to use inspect to find out if the value is explicitly set. - const inspect = vscode.workspace - .getConfiguration("swift.debugger") - .inspect("useDebugAdapterFromToolchain"); - return inspect?.workspaceValue ?? inspect?.globalValue ?? true; + useDebugAdapterFromToolchain = useDebugAdapterFromToolchain ?? true; } - return vscode.workspace + const selectedAdapter = vscode.workspace .getConfiguration("swift.debugger") - .get("useDebugAdapterFromToolchain", false); + .get("debugAdapter", "auto"); + switch (selectedAdapter) { + case "auto": + if (useDebugAdapterFromToolchain !== undefined) { + return useDebugAdapterFromToolchain ? "lldb-dap" : "CodeLLDB"; + } + return "auto"; + default: + return selectedAdapter; + } }, get customDebugAdapterPath(): string { - return vscode.workspace.getConfiguration("swift.debugger").get("path", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift.debugger").get("path", "") + ); + }, + get disable(): boolean { + return vscode.workspace + .getConfiguration("swift.debugger") + .get("disable", false); + }, + get setupCodeLLDB(): SetupCodeLLDBOptions { + return vscode.workspace + .getConfiguration("swift.debugger") + .get("setupCodeLLDB", "prompt"); }, }; }, @@ -205,7 +284,8 @@ const configuration = { get excludeFromCodeCoverage(): string[] { return vscode.workspace .getConfiguration("swift") - .get("excludeFromCodeCoverage", []); + .get("excludeFromCodeCoverage", []) + .map(substituteVariablesInString); }, /** Files and directories to exclude from the Package Dependencies view. */ get excludePathsFromPackageDependencies(): string[] { @@ -215,15 +295,21 @@ const configuration = { }, /** Path to folder that include swift executable */ get path(): string { - return vscode.workspace.getConfiguration("swift").get("path", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("path", "") + ); }, /** Path to folder that include swift runtime */ get runtimePath(): string { - return vscode.workspace.getConfiguration("swift").get("runtimePath", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("runtimePath", "") + ); }, /** Path to custom --sdk */ get sdk(): string { - return vscode.workspace.getConfiguration("swift").get("SDK", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("SDK", "") + ); }, set sdk(value: string | undefined) { vscode.workspace.getConfiguration("swift").update("SDK", value); @@ -237,18 +323,31 @@ const configuration = { }, /** swift build arguments */ get buildArguments(): string[] { - return vscode.workspace.getConfiguration("swift").get("buildArguments", []); + return vscode.workspace + .getConfiguration("swift") + .get("buildArguments", []) + .map(substituteVariablesInString); + }, + get scriptSwiftLanguageVersion(): string { + return vscode.workspace + .getConfiguration("swift") + .get("scriptSwiftLanguageVersion", "6"); }, /** swift package arguments */ get packageArguments(): string[] { - return vscode.workspace.getConfiguration("swift").get("packageArguments", []); + return vscode.workspace + .getConfiguration("swift") + .get("packageArguments", []) + .map(substituteVariablesInString); }, /** thread/address sanitizer */ get sanitizer(): string { return vscode.workspace.getConfiguration("swift").get("sanitizer", "off"); }, get buildPath(): string { - return vscode.workspace.getConfiguration("swift").get("buildPath", ""); + return substituteVariablesInString( + vscode.workspace.getConfiguration("swift").get("buildPath", "") + ); }, get disableSwiftPMIntegration(): boolean { return vscode.workspace @@ -362,6 +461,45 @@ const configuration = { .getConfiguration("swift") .get("enableTerminalEnvironment", true); }, + /** Whether or not to disable SwiftPM sandboxing */ + get disableSandbox(): boolean { + return vscode.workspace.getConfiguration("swift").get("disableSandbox", false); + }, }; +const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g); +function substituteVariablesInString(val: string): string { + return val.replace(vsCodeVariableRegex, (substring: string, varName: string) => + typeof varName === "string" ? computeVscodeVar(varName) || substring : substring + ); +} + +function computeVscodeVar(varName: string): string | null { + const workspaceFolder = () => { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor) { + const documentUri = activeEditor.document.uri; + const folder = vscode.workspace.getWorkspaceFolder(documentUri); + if (folder) { + return folder.uri.fsPath; + } + } + + // If there is no active editor then return the first workspace folder + return vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath ?? ""; + }; + + // https://code.visualstudio.com/docs/editor/variables-reference + // Variables to be substituted should be added here. + const supportedVariables: { [k: string]: () => string } = { + workspaceFolder, + workspaceFolderBasename: () => path.basename(workspaceFolder()), + cwd: () => process.cwd(), + userHome: () => os.homedir(), + pathSeparator: () => path.sep, + }; + + return varName in supportedVariables ? supportedVariables[varName]() : null; +} + export default configuration; diff --git a/src/contextKeys.ts b/src/contextKeys.ts index e17c15ba9..ed735af9c 100644 --- a/src/contextKeys.ts +++ b/src/contextKeys.ts @@ -38,6 +38,11 @@ interface ContextKeys { */ packageHasDependencies: boolean; + /** + * Whether the dependencies list is displayed in a nested or flat view. + */ + flatDependenciesList: boolean; + /** * Whether the Swift package has any plugins. */ @@ -72,12 +77,18 @@ interface ContextKeys { * Whether the SourceKit-LSP server supports documentation live preview. */ supportsDocumentationLivePreview: boolean; + + /** + * Whether the swift.switchPlatform command is available. + */ + switchPlatformAvailable: boolean; } /** Creates the getters and setters for the VS Code Swift extension's context keys. */ function createContextKeys(): ContextKeys { let isActivated: boolean = false; let hasPackage: boolean = false; + let flatDependenciesList: boolean = false; let packageHasDependencies: boolean = false; let packageHasPlugins: boolean = false; let currentTargetType: string | undefined = undefined; @@ -86,6 +97,7 @@ function createContextKeys(): ContextKeys { let createNewProjectAvailable: boolean = false; let supportsReindexing: boolean = false; let supportsDocumentationLivePreview: boolean = false; + let switchPlatformAvailable: boolean = false; return { get isActivated() { @@ -115,6 +127,15 @@ function createContextKeys(): ContextKeys { vscode.commands.executeCommand("setContext", "swift.packageHasDependencies", value); }, + get flatDependenciesList() { + return flatDependenciesList; + }, + + set flatDependenciesList(value: boolean) { + flatDependenciesList = value; + vscode.commands.executeCommand("setContext", "swift.flatDependenciesList", value); + }, + get packageHasPlugins() { return packageHasPlugins; }, @@ -185,6 +206,15 @@ function createContextKeys(): ContextKeys { value ); }, + + get switchPlatformAvailable() { + return switchPlatformAvailable; + }, + + set switchPlatformAvailable(value: boolean) { + switchPlatformAvailable = value; + vscode.commands.executeCommand("setContext", "swift.switchPlatformAvailable", value); + }, }; } diff --git a/src/debugger/buildConfig.ts b/src/debugger/buildConfig.ts index f31ea8a2e..a066a4bf7 100644 --- a/src/debugger/buildConfig.ts +++ b/src/debugger/buildConfig.ts @@ -20,7 +20,7 @@ import configuration from "../configuration"; import { FolderContext } from "../FolderContext"; import { BuildFlags } from "../toolchain/BuildFlags"; import { regexEscapedString, swiftRuntimeEnv } from "../utilities/utilities"; -import { DebugAdapter } from "./debugAdapter"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; import { TargetType } from "../SwiftPackage"; import { Version } from "../utilities/version"; import { TestLibrary } from "../TestExplorer/TestRunner"; @@ -186,13 +186,13 @@ export class SwiftTestingConfigurationSetup { * and `xcTestConfig` functions to create */ export class TestingConfigurationFactory { - public static async swiftTestingConfig( + public static swiftTestingConfig( ctx: FolderContext, buildArguments: SwiftTestingBuildAguments, testKind: TestKind, testList: string[], expandEnvVariables = false - ): Promise { + ): vscode.DebugConfiguration | null { return new TestingConfigurationFactory( ctx, testKind, @@ -203,12 +203,12 @@ export class TestingConfigurationFactory { ).build(); } - public static async xcTestConfig( + public static xcTestConfig( ctx: FolderContext, testKind: TestKind, testList: string[], expandEnvVariables = false - ): Promise { + ): vscode.DebugConfiguration | null { return new TestingConfigurationFactory( ctx, testKind, @@ -219,11 +219,11 @@ export class TestingConfigurationFactory { ).build(); } - public static async testExecutableOutputPath( + public static testExecutableOutputPath( ctx: FolderContext, testKind: TestKind, testLibrary: TestLibrary - ): Promise { + ): string { return new TestingConfigurationFactory( ctx, testKind, @@ -251,7 +251,7 @@ export class TestingConfigurationFactory { * - Test Kind (coverage, debugging) * - Test Library (XCTest, swift-testing) */ - private async build(): Promise { + private build(): vscode.DebugConfiguration | null { if (!this.hasTestTarget) { return null; } @@ -267,7 +267,7 @@ export class TestingConfigurationFactory { } /* eslint-disable no-case-declarations */ - private async buildWindowsConfig(): Promise { + private buildWindowsConfig(): vscode.DebugConfiguration | null { if (isDebugging(this.testKind)) { const testEnv = { ...swiftRuntimeEnv(), @@ -288,8 +288,8 @@ export class TestingConfigurationFactory { return { ...this.baseConfig, - program: await this.testExecutableOutputPath(), - args: await this.debuggingTestExecutableArgs(), + program: this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), env: testEnv, }; } else { @@ -298,12 +298,12 @@ export class TestingConfigurationFactory { } /* eslint-disable no-case-declarations */ - private async buildLinuxConfig(): Promise { + private buildLinuxConfig(): vscode.DebugConfiguration | null { if (isDebugging(this.testKind) && this.testLibrary === TestLibrary.xctest) { return { ...this.baseConfig, - program: await this.testExecutableOutputPath(), - args: await this.debuggingTestExecutableArgs(), + program: this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), env: { ...swiftRuntimeEnv( process.env, @@ -313,11 +313,11 @@ export class TestingConfigurationFactory { }, }; } else { - return await this.buildDarwinConfig(); + return this.buildDarwinConfig(); } } - private async buildDarwinConfig(): Promise { + private buildDarwinConfig(): vscode.DebugConfiguration | null { switch (this.testLibrary) { case TestLibrary.swiftTesting: switch (this.testKind) { @@ -362,8 +362,8 @@ export class TestingConfigurationFactory { const result = { ...this.baseConfig, - program: await this.testExecutableOutputPath(), - args: await this.debuggingTestExecutableArgs(), + program: this.testExecutableOutputPath(), + args: this.debuggingTestExecutableArgs(), env: { ...this.testEnv, ...this.sanitizerRuntimeEnvironment, @@ -515,7 +515,7 @@ export class TestingConfigurationFactory { }).map(([key, value]) => `settings set target.env-vars ${key}="${value}"`); return { - type: DebugAdapter.getLaunchConfigType(this.ctx.workspaceContext.swiftVersion), + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "custom", name: `Test ${this.ctx.swiftPackage.name}`, targetCreateCommands: [`file -a ${arch} ${xctestPath}/xctest`], @@ -535,7 +535,7 @@ export class TestingConfigurationFactory { } const swiftTestingArgs = [ - ...args, + ...this.ctx.workspaceContext.toolchain.buildFlags.withAdditionalFlags(args), "--enable-swift-testing", "--event-stream-version", "0", @@ -631,45 +631,6 @@ export class TestingConfigurationFactory { ); } - private swiftTestingOutputPath(): string { - return path.join( - this.buildDirectory, - this.artifactFolderForTestKind, - `${this.ctx.swiftPackage.name}PackageTests.swift-testing` - ); - } - - private buildDescriptionPath(): string { - return path.join(this.buildDirectory, this.artifactFolderForTestKind, "description.json"); - } - - private async isUnifiedTestingBinary(): Promise { - // Toolchains that contain https://github.com/swiftlang/swift-package-manager/commit/844bd137070dcd18d0f46dd95885ef7907ea0697 - // no longer produce a .swift-testing binary, instead we want to use `unifiedTestingOutputPath`. - // In order to determine if we're working with a unified binary we need to check if the .swift-testing - // binary was produced by the latest build. If it was then we are not using a unified binary. - - // TODO: When Swift 6 is released and enough time has passed that we're sure no one is building the .swift-testing - // binary anymore this workaround can be removed and `swiftTestingPath` can be returned, and the build config - // generation can be made synchronous again. - - try { - const buildDescriptionStr = await fs.readFile(this.buildDescriptionPath(), "utf-8"); - const buildDescription = JSON.parse(buildDescriptionStr); - const testProducts = buildDescription.builtTestProducts as { binaryPath: string }[]; - if (!testProducts) { - return false; - } - const testBinaryPaths = testProducts.map(({ binaryPath }) => binaryPath); - const swiftTestingBinaryRealPath = await fs.realpath(this.swiftTestingOutputPath()); - return !testBinaryPaths.includes(swiftTestingBinaryRealPath); - } catch { - // If the .swift-testing binary wasn't produced by the latest build then we assume the - // swift-testing tests are in the unified binary. - return true; - } - } - private unifiedTestingOutputPath(): string { // The unified binary that contains both swift-testing and XCTests // is named the same as the old style .xctest binary. The swiftpm-testing-helper @@ -686,24 +647,19 @@ export class TestingConfigurationFactory { } } - private async testExecutableOutputPath(): Promise { + private testExecutableOutputPath(): string { switch (this.testLibrary) { case TestLibrary.swiftTesting: - return (await this.isUnifiedTestingBinary()) - ? this.unifiedTestingOutputPath() - : this.swiftTestingOutputPath(); + return this.unifiedTestingOutputPath(); case TestLibrary.xctest: return this.xcTestOutputPath(); } } - private async debuggingTestExecutableArgs(): Promise { + private debuggingTestExecutableArgs(): string[] { switch (this.testLibrary) { case TestLibrary.swiftTesting: { - const isUnifiedBinary = await this.isUnifiedTestingBinary(); - const swiftTestingArgs = isUnifiedBinary - ? ["--testing-library", "swift-testing"] - : []; + const swiftTestingArgs = ["--testing-library", "swift-testing"]; return this.addBuildOptionsToArgs( this.addTestsToArgs(this.addSwiftTestingFlagsArgs(swiftTestingArgs)) @@ -738,7 +694,7 @@ export class TestingConfigurationFactory { function getBaseConfig(ctx: FolderContext, expandEnvVariables: boolean) { const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, expandEnvVariables); return { - type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion), + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", sourceLanguages: ["swift"], name: `Test ${ctx.swiftPackage.name}`, diff --git a/src/debugger/debugAdapter.ts b/src/debugger/debugAdapter.ts index fa7713c99..fdaa7b03d 100644 --- a/src/debugger/debugAdapter.ts +++ b/src/debugger/debugAdapter.ts @@ -12,19 +12,22 @@ // //===----------------------------------------------------------------------===// -import * as vscode from "vscode"; import configuration from "../configuration"; -import contextKeys from "../contextKeys"; -import { fileExists } from "../utilities/filesystem"; import { Version } from "../utilities/version"; import { SwiftToolchain } from "../toolchain/toolchain"; -import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; /** - * The supported {@link vscode.DebugConfiguration.type Debug Configuration Type} for auto-generation of launch configurations + * The launch configuration type added by the Swift extension that will delegate to the appropriate + * LLDB debug adapter when launched. + */ +export const SWIFT_LAUNCH_CONFIG_TYPE = "swift"; + +/** + * The supported {@link vscode.DebugConfiguration.type Debug Configuration Types} that can handle + * LLDB launch requests. */ export const enum LaunchConfigType { - SWIFT_EXTENSION = "swift-lldb", + LLDB_DAP = "lldb-dap", CODE_LLDB = "lldb", } @@ -40,10 +43,12 @@ export class DebugAdapter { * @returns the type of launch configuration used by the given Swift toolchain version */ public static getLaunchConfigType(swiftVersion: Version): LaunchConfigType { - return swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) && - configuration.debugger.useDebugAdapterFromToolchain - ? LaunchConfigType.SWIFT_EXTENSION - : LaunchConfigType.CODE_LLDB; + const lldbDapIsAvailable = swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)); + if (lldbDapIsAvailable && configuration.debugger.debugAdapter !== "CodeLLDB") { + return LaunchConfigType.LLDB_DAP; + } else { + return LaunchConfigType.CODE_LLDB; + } } /** @@ -52,59 +57,11 @@ export class DebugAdapter { * @param toolchain The Swift toolchain to use * @returns A path to the debug adapter for the user's toolchain and configuration **/ - public static async debugAdapterPath(toolchain: SwiftToolchain): Promise { + public static async getLLDBDebugAdapterPath(toolchain: SwiftToolchain): Promise { const customDebugAdapterPath = configuration.debugger.customDebugAdapterPath; if (customDebugAdapterPath.length > 0) { return customDebugAdapterPath; } - - const debugAdapter = this.getLaunchConfigType(toolchain.swiftVersion); - switch (debugAdapter) { - case LaunchConfigType.SWIFT_EXTENSION: - return toolchain.getLLDBDebugAdapter(); - case LaunchConfigType.CODE_LLDB: - return toolchain.getLLDB(); - } - } - - /** - * Verify that the toolchain debug adapter exists and display an error message to the user - * if it doesn't. - * - * Has the side effect of setting the `swift.lldbVSCodeAvailable` context key depending - * on the result. - * - * @param workspace WorkspaceContext - * @param quiet Whether or not the dialog should be displayed if the adapter does not exist - * @returns Whether or not the debug adapter exists - */ - public static async verifyDebugAdapterExists( - toolchain: SwiftToolchain, - outputChannel: SwiftOutputChannel, - quiet = false - ): Promise { - const lldbDebugAdapterPath = await this.debugAdapterPath(toolchain).catch(error => { - outputChannel.log(error); - return undefined; - }); - - if (!lldbDebugAdapterPath || !(await fileExists(lldbDebugAdapterPath))) { - if (!quiet) { - const debugAdapterName = this.getLaunchConfigType(toolchain.swiftVersion); - vscode.window.showErrorMessage( - configuration.debugger.customDebugAdapterPath.length > 0 - ? `Cannot find ${debugAdapterName} debug adapter specified in setting Swift.Debugger.Path.` - : `Cannot find ${debugAdapterName} debug adapter in your Swift toolchain.` - ); - } - if (lldbDebugAdapterPath) { - outputChannel.log(`Failed to find ${lldbDebugAdapterPath}`); - } - contextKeys.lldbVSCodeAvailable = false; - return false; - } - - contextKeys.lldbVSCodeAvailable = true; - return true; + return toolchain.getLLDBDebugAdapter(); } } diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 8a264aa39..1c2da9fa5 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -15,11 +15,14 @@ import * as vscode from "vscode"; import * as path from "path"; import { WorkspaceContext } from "../WorkspaceContext"; -import { DebugAdapter, LaunchConfigType } from "./debugAdapter"; -import { Version } from "../utilities/version"; +import { DebugAdapter, LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; import { registerLoggingDebugAdapterTracker } from "./logTracker"; import { SwiftToolchain } from "../toolchain/toolchain"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; +import { fileExists } from "../utilities/filesystem"; +import { getLLDBLibPath } from "./lldb"; +import { getErrorDescription } from "../utilities/utilities"; +import configuration from "../configuration"; /** * Registers the active debugger with the extension, and reregisters it @@ -28,41 +31,13 @@ import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; * @returns A disposable to be disposed when the extension is deactivated */ export function registerDebugger(workspaceContext: WorkspaceContext): vscode.Disposable { - let subscriptions: vscode.Disposable[] = []; - const register = async () => { - subscriptions.map(sub => sub.dispose()); - subscriptions = [ - registerLoggingDebugAdapterTracker(workspaceContext.toolchain.swiftVersion), - registerLLDBDebugAdapter(workspaceContext.toolchain, workspaceContext.outputChannel), - ]; - - await workspaceContext.setLLDBVersion(); - - // Verify that the adapter exists, but only after registration. This async method - // is basically an unstructured task so we don't want to run it before the adapter - // registration above as it could cause code executing immediately after register() - // to use the incorrect adapter. - DebugAdapter.verifyDebugAdapterExists( - workspaceContext.toolchain, - workspaceContext.outputChannel, - true - ).catch(error => { - workspaceContext.outputChannel.log(error); - }); - }; - - const changeMonitor = vscode.workspace.onDidChangeConfiguration(event => { - if (event.affectsConfiguration("swift.debugger.useDebugAdapterFromToolchain")) { - register(); - } - }); - - // Perform the initial registration, then reregister every time the settings change. - register(); + const subscriptions: vscode.Disposable[] = [ + registerLoggingDebugAdapterTracker(), + registerLLDBDebugAdapter(workspaceContext.toolchain, workspaceContext.outputChannel), + ]; return { dispose: () => { - changeMonitor.dispose(); subscriptions.map(sub => sub.dispose()); }, }; @@ -77,55 +52,10 @@ function registerLLDBDebugAdapter( toolchain: SwiftToolchain, outputChannel: SwiftOutputChannel ): vscode.Disposable { - const debugAdpaterFactory = vscode.debug.registerDebugAdapterDescriptorFactory( - LaunchConfigType.SWIFT_EXTENSION, - new LLDBDebugAdapterExecutableFactory(toolchain, outputChannel) - ); - - const debugConfigProvider = vscode.debug.registerDebugConfigurationProvider( - LaunchConfigType.SWIFT_EXTENSION, - new LLDBDebugConfigurationProvider(process.platform, toolchain.swiftVersion) + return vscode.debug.registerDebugConfigurationProvider( + SWIFT_LAUNCH_CONFIG_TYPE, + new LLDBDebugConfigurationProvider(process.platform, toolchain, outputChannel) ); - - return { - dispose: () => { - debugConfigProvider.dispose(); - debugAdpaterFactory.dispose(); - }, - }; -} - -/** - * A factory class for creating and providing the executable descriptor for the LLDB Debug Adapter. - * This class implements the vscode.DebugAdapterDescriptorFactory interface and is responsible for - * determining the path to the LLDB Debug Adapter executable and ensuring it exists before launching - * a debug session. - * - * This class uses the workspace context to: - * - Resolve the path to the debug adapter executable. - * - Verify that the debug adapter exists in the toolchain. - * - * The main method of this class, `createDebugAdapterDescriptor`, is invoked by VS Code to supply - * the debug adapter executable when a debug session is started. The executable parameter by default - * will be provided in package.json > contributes > debuggers > program if defined, but since we will - * determine the executable via the toolchain anyway, this is now redundant and will be ignored. - * - * @implements {vscode.DebugAdapterDescriptorFactory} - */ -export class LLDBDebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory { - private toolchain: SwiftToolchain; - private outputChannel: SwiftOutputChannel; - - constructor(toolchain: SwiftToolchain, outputChannel: SwiftOutputChannel) { - this.toolchain = toolchain; - this.outputChannel = outputChannel; - } - - async createDebugAdapterDescriptor(): Promise { - const path = await DebugAdapter.debugAdapterPath(this.toolchain); - await DebugAdapter.verifyDebugAdapterExists(this.toolchain, this.outputChannel); - return new vscode.DebugAdapterExecutable(path, [], {}); - } } /** Provide configurations for lldb-vscode/lldb-dap @@ -140,37 +70,179 @@ export class LLDBDebugAdapterExecutableFactory implements vscode.DebugAdapterDes export class LLDBDebugConfigurationProvider implements vscode.DebugConfigurationProvider { constructor( private platform: NodeJS.Platform, - private swiftVersion: Version + private toolchain: SwiftToolchain, + private outputChannel: SwiftOutputChannel ) {} - async resolveDebugConfiguration( + async resolveDebugConfigurationWithSubstitutedVariables( _folder: vscode.WorkspaceFolder | undefined, launchConfig: vscode.DebugConfiguration - ): Promise { - launchConfig.env = this.convertEnvironmentVariables(launchConfig.env); + ): Promise { // Fix the program path on Windows to include the ".exe" extension if ( this.platform === "win32" && launchConfig.testType === undefined && - path.extname(launchConfig.program) !== ".exe" + path.extname(launchConfig.program) !== ".exe" && + path.extname(launchConfig.program) !== ".xctest" ) { launchConfig.program += ".exe"; } - // Delegate to CodeLLDB if that's the debug adapter we have selected - if (DebugAdapter.getLaunchConfigType(this.swiftVersion) === LaunchConfigType.CODE_LLDB) { - launchConfig.type = LaunchConfigType.CODE_LLDB; + // Convert "pid" property from a string to a number to make the process picker work. + if ("pid" in launchConfig) { + const pid = Number.parseInt(launchConfig.pid, 10); + if (isNaN(pid)) { + return await vscode.window + .showErrorMessage( + "Failed to launch debug session", + { + modal: true, + detail: `Invalid process ID: "${launchConfig.pid}" is not a valid integer. Please update your launch configuration`, + }, + "Configure" + ) + .then(userSelection => { + if (userSelection === "Configure") { + return null; // Opens the launch configuration when returned from a DebugConfigurationProvider + } + return undefined; // Only stops the debug session from starting + }); + } + launchConfig.pid = pid; + } + + // Delegate to the appropriate debug adapter extension + launchConfig.type = DebugAdapter.getLaunchConfigType(this.toolchain.swiftVersion); + if (launchConfig.type === LaunchConfigType.CODE_LLDB) { launchConfig.sourceLanguages = ["swift"]; + if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { + if (!(await this.promptToInstallCodeLLDB())) { + return undefined; + } + } + if (!(await this.promptForCodeLldbSettings())) { + return undefined; + } + } else if (launchConfig.type === LaunchConfigType.LLDB_DAP) { + if (launchConfig.env) { + launchConfig.env = this.convertEnvironmentVariables(launchConfig.env); + } + const lldbDapPath = await DebugAdapter.getLLDBDebugAdapterPath(this.toolchain); + // Verify that the debug adapter exists or bail otherwise + if (!(await fileExists(lldbDapPath))) { + vscode.window.showErrorMessage( + `Cannot find the LLDB debug adapter in your Swift toolchain: No such file or directory "${lldbDapPath}"` + ); + return undefined; + } + launchConfig.debugAdapterExecutable = lldbDapPath; } + return launchConfig; } - convertEnvironmentVariables( - map: { [key: string]: string } | undefined - ): { [key: string]: string } | string[] | undefined { - if (map === undefined) { - return undefined; + private async promptToInstallCodeLLDB(): Promise { + const selection = await vscode.window.showErrorMessage( + "The CodeLLDB extension is required to debug with Swift toolchains prior to Swift 6.0. Please install the extension to continue.", + { modal: true }, + "Install CodeLLDB", + "View Extension" + ); + switch (selection) { + case "Install CodeLLDB": + await vscode.commands.executeCommand( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ); + return true; + case "View Extension": + await vscode.commands.executeCommand( + "workbench.extensions.search", + "@id:vadimcn.vscode-lldb" + ); + await vscode.commands.executeCommand( + "workbench.extensions.action.showReleasedVersion", + "vadimcn.vscode-lldb" + ); + return false; + case undefined: + return false; + } + } + + private async promptForCodeLldbSettings(): Promise { + const libLldbPathResult = await getLLDBLibPath(this.toolchain); + if (!libLldbPathResult.success) { + const errorMessage = `Error: ${getErrorDescription(libLldbPathResult.failure)}`; + vscode.window.showWarningMessage( + `Failed to setup CodeLLDB for debugging of Swift code. Debugging may produce unexpected results. ${errorMessage}` + ); + this.outputChannel.log(`Failed to setup CodeLLDB: ${errorMessage}`); + return true; + } + const libLldbPath = libLldbPathResult.success; + const lldbConfig = vscode.workspace.getConfiguration("lldb"); + if ( + lldbConfig.get("library") === libLldbPath && + lldbConfig.get("launch.expressions") === "native" + ) { + return true; } + let userSelection: "Global" | "Workspace" | "Run Anyway" | undefined = undefined; + switch (configuration.debugger.setupCodeLLDB) { + case "prompt": + userSelection = await vscode.window.showInformationMessage( + "The Swift extension needs to update some CodeLLDB settings to enable debugging features. Do you want to set this up in your global settings or workspace settings?", + { modal: true }, + "Global", + "Workspace", + "Run Anyway" + ); + break; + case "alwaysUpdateGlobal": + userSelection = "Global"; + break; + case "alwaysUpdateWorkspace": + userSelection = "Workspace"; + break; + case "never": + userSelection = "Run Anyway"; + break; + } + switch (userSelection) { + case "Global": + await lldbConfig.update("library", libLldbPath, vscode.ConfigurationTarget.Global); + await lldbConfig.update( + "launch.expressions", + "native", + vscode.ConfigurationTarget.Global + ); + // clear workspace setting + await lldbConfig.update("library", undefined, vscode.ConfigurationTarget.Workspace); + // clear workspace setting + await lldbConfig.update( + "launch.expressions", + undefined, + vscode.ConfigurationTarget.Workspace + ); + break; + case "Workspace": + await lldbConfig.update( + "library", + libLldbPath, + vscode.ConfigurationTarget.Workspace + ); + await lldbConfig.update( + "launch.expressions", + "native", + vscode.ConfigurationTarget.Workspace + ); + break; + } + return true; + } + + private convertEnvironmentVariables(map: { [key: string]: string }): string[] { return Object.entries(map).map(([key, value]) => `${key}=${value}`); } } diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 549d8d13a..b91836828 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -17,7 +17,7 @@ import * as vscode from "vscode"; import { FolderContext } from "../FolderContext"; import { BuildFlags } from "../toolchain/BuildFlags"; import { stringArrayInEnglish, swiftLibraryPathKey, swiftRuntimeEnv } from "../utilities/utilities"; -import { DebugAdapter } from "./debugAdapter"; +import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; import { getFolderAndNameSuffix } from "./buildConfig"; import configuration from "../configuration"; import { CI_DISABLE_ASLR } from "./lldb"; @@ -136,7 +136,7 @@ function createExecutableConfigurations(ctx: FolderContext): vscode.DebugConfigu return executableProducts.flatMap(product => { const baseConfig = { - type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion), + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", args: [], cwd: folder, @@ -174,13 +174,14 @@ export function createSnippetConfiguration( const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true); return { - type: DebugAdapter.getLaunchConfigType(ctx.workspaceContext.swiftVersion), + type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", name: `Run ${snippetName}`, program: path.posix.join(buildDirectory, "debug", snippetName), args: [], cwd: folder, env: swiftRuntimeEnv(true), + runType: "snippet", ...CI_DISABLE_ASLR, }; } diff --git a/src/debugger/lldb.ts b/src/debugger/lldb.ts index 803913c27..91d3b5179 100644 --- a/src/debugger/lldb.ts +++ b/src/debugger/lldb.ts @@ -17,9 +17,7 @@ import * as path from "path"; import * as fs from "fs/promises"; -import * as vscode from "vscode"; -import { WorkspaceContext } from "../WorkspaceContext"; -import { execFile, getErrorDescription } from "../utilities/utilities"; +import { execFile } from "../utilities/utilities"; import { Result } from "../utilities/result"; import { SwiftToolchain } from "../toolchain/toolchain"; @@ -33,8 +31,8 @@ export const CI_DISABLE_ASLR = : {}; /** - * Get LLDB library for given LLDB executable - * @param executable LLDB executable + * Get the path to the LLDB library. + * * @returns Library path for LLDB */ export async function getLLDBLibPath(toolchain: SwiftToolchain): Promise> { @@ -114,46 +112,3 @@ export async function findFileByPattern(path: string, pattern: RegExp): Promise< } return null; } - -/** - * Retrieves a list of LLDB processes from the system using LLDB. - * - * This function executes an LLDB command to list all processes on the system, - * including their arguments, and returns them in an array of objects where each - * object contains the `pid` and a `label` describing the process. - * - * @param {WorkspaceContext} ctx - The workspace context, which includes the toolchain needed to run LLDB. - * @returns {Promise | undefined>} - * A promise that resolves to an array of processes, where each process is represented by an object with a `pid` and a `label`. - * If an error occurs or no processes are found, it returns `undefined`. - * - * @throws Will display an error message in VS Code if the LLDB command fails. - */ -export async function getLldbProcess( - ctx: WorkspaceContext -): Promise | undefined> { - try { - // use LLDB to get list of processes - const lldb = await ctx.toolchain.getLLDB(); - const { stdout } = await execFile(lldb, [ - "--batch", - "--no-lldbinit", - "--one-line", - "platform process list --show-args --all-users", - ]); - const entries = stdout.split("\n"); - const processes = entries.flatMap(line => { - const match = /^(\d+)\s+\d+\s+\S+\s+\S+\s+(.+)$/.exec(line); - if (match) { - return [{ pid: parseInt(match[1]), label: `${match[1]}: ${match[2]}` }]; - } else { - return []; - } - }); - return processes; - } catch (error) { - const errorMessage = `Failed to run LLDB: ${getErrorDescription(error)}`; - ctx.outputChannel.log(errorMessage); - vscode.window.showErrorMessage(errorMessage); - } -} diff --git a/src/debugger/logTracker.ts b/src/debugger/logTracker.ts index b5dde65e1..459f909e1 100644 --- a/src/debugger/logTracker.ts +++ b/src/debugger/logTracker.ts @@ -13,8 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { DebugAdapter } from "./debugAdapter"; -import { Version } from "../utilities/version"; +import { LaunchConfigType } from "./debugAdapter"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; /** @@ -44,28 +43,19 @@ interface DebugMessage { * Register the LoggingDebugAdapterTrackerFactory with the VS Code debug adapter tracker * @returns A disposable to be disposed when the extension is deactivated */ -export function registerLoggingDebugAdapterTracker(swiftVersion: Version): vscode.Disposable { - const register = () => - vscode.debug.registerDebugAdapterTrackerFactory( - DebugAdapter.getLaunchConfigType(swiftVersion), - new LoggingDebugAdapterTrackerFactory() - ); - - // Maintains the disposable for the last registered debug adapter. - let debugAdapterDisposable = register(); - const changeMonitor = vscode.workspace.onDidChangeConfiguration(event => { - if (event.affectsConfiguration("swift.debugger.useDebugAdapterFromToolchain")) { - // Dispose the old adapter and reconfigure with the new settings. - debugAdapterDisposable.dispose(); - debugAdapterDisposable = register(); - } - }); +export function registerLoggingDebugAdapterTracker(): vscode.Disposable { + // Register the factory for both lldb-dap and CodeLLDB since either could be used when + // resolving a Swift launch configuration. + const trackerFactory = new LoggingDebugAdapterTrackerFactory(); + const subscriptions: vscode.Disposable[] = [ + vscode.debug.registerDebugAdapterTrackerFactory(LaunchConfigType.CODE_LLDB, trackerFactory), + vscode.debug.registerDebugAdapterTrackerFactory(LaunchConfigType.LLDB_DAP, trackerFactory), + ]; // Return a disposable that cleans everything up. return { dispose() { - changeMonitor.dispose(); - debugAdapterDisposable.dispose(); + subscriptions.forEach(sub => sub.dispose()); }, }; } diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index d9cd177e8..2b2a8fb2f 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -19,6 +19,8 @@ import { RenderNode, WebviewContent, WebviewMessage } from "./webview/WebviewMes import { WorkspaceContext } from "../WorkspaceContext"; import { DocCDocumentationRequest, DocCDocumentationResponse } from "../sourcekit-lsp/extensions"; import { LSPErrorCodes, ResponseError } from "vscode-languageclient"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import throttle = require("lodash.throttle"); export enum PreviewEditorConstant { VIEW_TYPE = "swift.previewDocumentationEditor", @@ -94,6 +96,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable { } private activeTextEditor?: vscode.TextEditor; + private activeTextEditorSelection?: vscode.Selection; private subscriptions: vscode.Disposable[] = []; private disposeEmitter = new vscode.EventEmitter(); @@ -108,11 +111,11 @@ export class DocumentationPreviewEditor implements vscode.Disposable { this.subscriptions.push( this.webviewPanel.webview.onDidReceiveMessage(this.receiveMessage, this), vscode.window.onDidChangeActiveTextEditor(this.handleActiveTextEditorChange, this), + vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange, this), vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange, this), this.webviewPanel.onDidDispose(this.dispose, this) ); - // Reveal the editor, but don't change the focus of the active text editor - webviewPanel.reveal(undefined, true); + this.reveal(); } /** An event that is fired when the Documentation Preview Editor is disposed */ @@ -125,7 +128,8 @@ export class DocumentationPreviewEditor implements vscode.Disposable { onDidRenderContent = this.renderEmitter.event; reveal() { - this.webviewPanel.reveal(); + // Reveal the editor, but don't change the focus of the active text editor + this.webviewPanel.reveal(undefined, true); } dispose() { @@ -161,82 +165,98 @@ export class DocumentationPreviewEditor implements vscode.Disposable { return; } this.activeTextEditor = activeTextEditor; + this.activeTextEditorSelection = activeTextEditor.selection; this.convertDocumentation(activeTextEditor); } + private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) { + if ( + this.activeTextEditor !== event.textEditor || + this.activeTextEditorSelection === event.textEditor.selection + ) { + return; + } + this.activeTextEditorSelection = event.textEditor.selection; + this.convertDocumentation(event.textEditor); + } + private handleDocumentChange(event: vscode.TextDocumentChangeEvent) { if (this.activeTextEditor?.document === event.document) { this.convertDocumentation(this.activeTextEditor); } } - private async convertDocumentation(textEditor: vscode.TextEditor): Promise { - const document = textEditor.document; - if ( - document.uri.scheme !== "file" || - !["markdown", "tutorial", "swift"].includes(document.languageId) - ) { - this.postMessage({ - type: "update-content", - content: { - type: "error", - errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE, - }, - }); - return; - } + private convertDocumentation = throttle( + async (textEditor: vscode.TextEditor): Promise => { + const document = textEditor.document; + if ( + document.uri.scheme !== "file" || + !["markdown", "tutorial", "swift"].includes(document.languageId) + ) { + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE, + }, + }); + return; + } - try { - const response = await this.context.languageClientManager.useLanguageClient( - async (client): Promise => { - return await client.sendRequest(DocCDocumentationRequest.type, { - textDocument: { - uri: document.uri.toString(), - }, - position: textEditor.selection.start, - }); + try { + const response = await this.context.languageClientManager.useLanguageClient( + async (client): Promise => { + return await client.sendRequest(DocCDocumentationRequest.type, { + textDocument: { + uri: document.uri.toString(), + }, + position: textEditor.selection.start, + }); + } + ); + this.postMessage({ + type: "update-content", + content: { + type: "render-node", + renderNode: this.parseRenderNode(response.renderNode), + }, + }); + } catch (error) { + // Update the preview editor to reflect what error occurred + let livePreviewErrorMessage = "An internal error occurred"; + const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `; + if (error instanceof ResponseError) { + if (error.code === LSPErrorCodes.RequestCancelled) { + // We can safely ignore cancellations + return undefined; + } + switch (error.code) { + case LSPErrorCodes.RequestFailed: + // RequestFailed response errors can be shown to the user + livePreviewErrorMessage = error.message; + break; + default: + // We should log additional info for other response errors + this.context.outputChannel.log( + baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) + ); + break; + } + } else { + this.context.outputChannel.log(baseLogErrorMessage + `${error}`); } - ); - this.postMessage({ - type: "update-content", - content: { - type: "render-node", - renderNode: this.parseRenderNode(response.renderNode), - }, - }); - } catch (error) { - // Update the preview editor to reflect what error occurred - let livePreviewErrorMessage = "An internal error occurred"; - const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `; - if (error instanceof ResponseError) { - if (error.code === LSPErrorCodes.RequestCancelled) { - // We can safely ignore cancellations - return undefined; - } - switch (error.code) { - case LSPErrorCodes.RequestFailed: - // RequestFailed response errors can be shown to the user - livePreviewErrorMessage = error.message; - break; - default: - // We should log additional info for other response errors - this.context.outputChannel.log( - baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) - ); - break; - } - } else { - this.context.outputChannel.log(baseLogErrorMessage + `${error}`); + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: livePreviewErrorMessage, + }, + }); } - this.postMessage({ - type: "update-content", - content: { - type: "error", - errorMessage: livePreviewErrorMessage, - }, - }); - } - } + }, + 100 /* 10 times per second */, + { trailing: true } + ); private parseRenderNode(content: string): RenderNode { const renderNode: RenderNode = JSON.parse(content); diff --git a/src/extension.ts b/src/extension.ts index 612ea9e3b..4b3202c28 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,7 +18,7 @@ import "source-map-support/register"; import * as vscode from "vscode"; import * as commands from "./commands"; import * as debug from "./debugger/launch"; -import { PackageDependenciesProvider } from "./ui/PackageDependencyProvider"; +import { ProjectPanelProvider } from "./ui/ProjectPanelProvider"; import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider"; import { FolderOperation, WorkspaceContext } from "./WorkspaceContext"; import { FolderContext } from "./FolderContext"; @@ -56,7 +56,7 @@ export interface Api { */ export async function activate(context: vscode.ExtensionContext): Promise { try { - const outputChannel = new SwiftOutputChannel("Swift", !process.env["VSCODE_TEST"]); + const outputChannel = new SwiftOutputChannel("Swift"); outputChannel.log("Activating Swift for Visual Studio Code..."); checkAndWarnAboutWindowsSymlinks(outputChannel); @@ -76,12 +76,16 @@ export async function activate(context: vscode.ExtensionContext): Promise { contextKeys.createNewProjectAvailable = toolchain.swiftVersion.isGreaterThanOrEqual( new Version(5, 8, 0) ); + contextKeys.switchPlatformAvailable = toolchain.swiftVersion.isGreaterThanOrEqual( + new Version(6, 1, 0) + ); return toolchain; }) .catch(error => { outputChannel.log("Failed to discover Swift toolchain"); outputChannel.log(error); contextKeys.createNewProjectAvailable = false; + contextKeys.switchPlatformAvailable = false; return undefined; }); @@ -97,16 +101,15 @@ export async function activate(context: vscode.ExtensionContext): Promise { showReloadExtensionNotification( "Changing the Swift path requires Visual Studio Code be reloaded." ); - } - // on sdk config change, restart sourcekit-lsp - if ( + } else if ( + // on sdk config change, restart sourcekit-lsp event.affectsConfiguration("swift.SDK") || event.affectsConfiguration("swift.swiftSDK") ) { - // FIXME: There is a bug stopping us from restarting SourceKit-LSP directly. - // As long as it's fixed we won't need to reload on newer versions. + vscode.commands.executeCommand("swift.restartLSPServer"); + } else if (event.affectsConfiguration("swift.swiftEnvironmentVariables")) { showReloadExtensionNotification( - "Changing the Swift SDK path requires the project be reloaded." + "Changing environment variables requires the project be reloaded." ); } }) @@ -119,7 +122,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { outputChannel, activate: () => activate(context), deactivate: async () => { - await workspaceContext.stop(); await deactivate(context); }, }; @@ -128,7 +130,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { const workspaceContext = await WorkspaceContext.create(context, outputChannel, toolchain); context.subscriptions.push(...commands.register(workspaceContext)); context.subscriptions.push(workspaceContext); - context.subscriptions.push(registerDebugger(workspaceContext)); + if (!configuration.debugger.disable) { + context.subscriptions.push(registerDebugger(workspaceContext)); + } context.subscriptions.push(new SelectedXcodeWatcher(outputChannel)); // listen for workspace folder changes and active text editor changes @@ -158,13 +162,13 @@ export async function activate(context: vscode.ExtensionContext): Promise { ); }); - // dependency view - const dependenciesProvider = new PackageDependenciesProvider(workspaceContext); - const dependenciesView = vscode.window.createTreeView("packageDependencies", { - treeDataProvider: dependenciesProvider, + // project panel provider + const projectPanelProvider = new ProjectPanelProvider(workspaceContext); + const dependenciesView = vscode.window.createTreeView("projectPanel", { + treeDataProvider: projectPanelProvider, showCollapseAll: true, }); - dependenciesProvider.observeFolders(dependenciesView); + projectPanelProvider.observeFolders(dependenciesView); // observer that will resolve package and build launch configurations const resolvePackageObserver = workspaceContext.onDidChangeFolders( @@ -187,6 +191,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } else { await resolveFolderDependencies(folder, true); } + if ( workspace.toolchain.swiftVersion.isGreaterThanOrEqual( new Version(5, 6, 0) @@ -197,8 +202,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { folder.workspaceFolder.uri )})`, async () => { - await folder.loadSwiftPlugins(); + await folder.loadSwiftPlugins(outputChannel); workspace.updatePluginContextKey(); + folder.fireEvent(FolderOperation.pluginsUpdated); } ); } @@ -250,7 +256,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { testExplorerObserver, swiftModuleDocumentProvider, dependenciesView, - dependenciesProvider, + projectPanelProvider, logObserver, languageStatusItem, pluginTaskProvider, diff --git a/src/process-list/BaseProcessList.ts b/src/process-list/BaseProcessList.ts new file mode 100644 index 000000000..0e5f621bd --- /dev/null +++ b/src/process-list/BaseProcessList.ts @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as util from "util"; +import * as child_process from "child_process"; +import { Process, ProcessList } from "."; + +const exec = util.promisify(child_process.execFile); + +/** Parses process information from a given line of process output. */ +export type ProcessListParser = (line: string) => Process | undefined; + +/** + * Implements common behavior between the different {@link ProcessList} implementations. + */ +export abstract class BaseProcessList implements ProcessList { + /** + * Get the command responsible for collecting all processes on the system. + */ + protected abstract getCommand(): string; + + /** + * Get the list of arguments used to launch the command. + */ + protected abstract getCommandArguments(): string[]; + + /** + * Create a new parser that can read the process information from stdout of the process + * spawned by {@link spawnProcess spawnProcess()}. + */ + protected abstract createParser(): ProcessListParser; + + async listAllProcesses(): Promise { + const execCommand = exec(this.getCommand(), this.getCommandArguments(), { + maxBuffer: 10 * 1024 * 1024, // Increase the max buffer size to 10Mb + }); + const parser = this.createParser(); + return (await execCommand).stdout.split("\n").flatMap(line => { + const process = parser(line.toString()); + if (!process || process.id === execCommand.child.pid) { + return []; + } + return [process]; + }); + } +} diff --git a/src/process-list/index.ts b/src/process-list/index.ts new file mode 100644 index 000000000..e1efa2cd6 --- /dev/null +++ b/src/process-list/index.ts @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { DarwinProcessList } from "./platforms/DarwinProcessList"; +import { LinuxProcessList } from "./platforms/LinuxProcessList"; +import { WindowsProcessList } from "./platforms/WindowsProcessList"; + +/** + * Represents a single process running on the system. + */ +export interface Process { + /** Process ID */ + id: number; + + /** Command that was used to start the process */ + command: string; + + /** The full command including arguments that was used to start the process */ + arguments: string; + + /** The date when the process was started */ + start: number; +} + +export interface ProcessList { + listAllProcesses(): Promise; +} + +/** Returns a {@link ProcessList} based on the current platform. */ +export function createProcessList(): ProcessList { + switch (process.platform) { + case "darwin": + return new DarwinProcessList(); + case "win32": + return new WindowsProcessList(); + default: + return new LinuxProcessList(); + } +} diff --git a/src/process-list/platforms/DarwinProcessList.ts b/src/process-list/platforms/DarwinProcessList.ts new file mode 100644 index 000000000..2fbc60033 --- /dev/null +++ b/src/process-list/platforms/DarwinProcessList.ts @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { LinuxProcessList } from "./LinuxProcessList"; + +export class DarwinProcessList extends LinuxProcessList { + protected override getCommandArguments(): string[] { + return [ + "-axo", + // The length of comm must be large enough or data will be truncated. + `pid=PID,state=STATE,lstart=START,comm=${"COMMAND".padEnd(256, "-")},args=ARGUMENTS`, + ]; + } +} diff --git a/src/process-list/platforms/LinuxProcessList.ts b/src/process-list/platforms/LinuxProcessList.ts new file mode 100644 index 000000000..9db94a8e7 --- /dev/null +++ b/src/process-list/platforms/LinuxProcessList.ts @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { BaseProcessList, ProcessListParser } from "../BaseProcessList"; + +export class LinuxProcessList extends BaseProcessList { + protected override getCommand(): string { + return "ps"; + } + + protected override getCommandArguments(): string[] { + return [ + "-axo", + // The length of exe must be large enough or data will be truncated. + `pid=PID,state=STATE,lstart=START,exe:128=COMMAND,args=ARGUMENTS`, + ]; + } + + protected override createParser(): ProcessListParser { + let commandOffset: number | undefined; + let argumentsOffset: number | undefined; + return line => { + if (!commandOffset || !argumentsOffset) { + commandOffset = line.indexOf("COMMAND"); + argumentsOffset = line.indexOf("ARGUMENTS"); + return; + } + + const pidAndState = /^\s*([0-9]+)\s+([a-zA-Z<>+]+)\s+/.exec(line); + if (!pidAndState) { + return; + } + + // Make sure the process isn't in a trace/debug or zombie state as we cannot attach to them + const state = pidAndState[2]; + if (state.includes("X") || state.includes("Z")) { + return; + } + + // ps will list "-" as the command if it does not know where the executable is located + const command = line.slice(commandOffset, argumentsOffset).trim(); + if (command === "-") { + return; + } + + return { + id: Number(pidAndState[1]), + command, + arguments: line.slice(argumentsOffset).trim(), + start: Date.parse(line.slice(pidAndState[0].length, commandOffset).trim()), + }; + }; + } +} diff --git a/src/process-list/platforms/WindowsProcessList.ts b/src/process-list/platforms/WindowsProcessList.ts new file mode 100644 index 000000000..ce7d77698 --- /dev/null +++ b/src/process-list/platforms/WindowsProcessList.ts @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { BaseProcessList, ProcessListParser } from "../BaseProcessList"; + +export class WindowsProcessList extends BaseProcessList { + protected override getCommand(): string { + return "PowerShell"; + } + + protected override getCommandArguments(): string[] { + return [ + "-Command", + 'Get-CimInstance -ClassName Win32_Process | Format-Table ProcessId, @{Label="CreationDate";Expression={"{0:yyyyMddHHmmss}" -f $_.CreationDate}}, CommandLine | Out-String -width 9999', + ]; + } + + protected override createParser(): ProcessListParser { + const lineRegex = /^([0-9]+)\s+([0-9]+)\s+(.*)$/; + + return line => { + const matches = lineRegex.exec(line.trim()); + if (!matches || matches.length !== 4) { + return; + } + + const id = Number(matches[1]); + const start = Number(matches[2]); + const fullCommandLine = matches[3].trim(); + if (isNaN(id) || !fullCommandLine) { + return; + } + // Extract the command from the full command line + let command = fullCommandLine; + if (fullCommandLine[0] === '"') { + const end = fullCommandLine.indexOf('"', 1); + if (end > 0) { + command = fullCommandLine.slice(1, end); + } + } else { + const end = fullCommandLine.indexOf(" "); + if (end > 0) { + command = fullCommandLine.slice(0, end); + } + } + + return { id, command, arguments: fullCommandLine, start }; + }; + } +} diff --git a/src/sourcekit-lsp/LanguageClientManager.ts b/src/sourcekit-lsp/LanguageClientManager.ts index 8379b9027..cf2d69c40 100644 --- a/src/sourcekit-lsp/LanguageClientManager.ts +++ b/src/sourcekit-lsp/LanguageClientManager.ts @@ -47,6 +47,8 @@ import { activateGetReferenceDocument } from "./getReferenceDocument"; import { uriConverters } from "./uriConverters"; import { LanguageClientFactory } from "./LanguageClientFactory"; import { SourceKitLogMessageNotification, SourceKitLogMessageParams } from "./extensions"; +import { LSPActiveDocumentManager } from "./didChangeActiveDocument"; +import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; /** * Manages the creation and destruction of Language clients as we move between @@ -136,6 +138,7 @@ export class LanguageClientManager implements vscode.Disposable { private legacyInlayHints?: vscode.Disposable; private peekDocuments?: vscode.Disposable; private getReferenceDocument?: vscode.Disposable; + private didChangeActiveDocument?: vscode.Disposable; private restartedPromise?: Promise; private currentWorkspaceFolder?: vscode.Uri; private waitingOnRestartCount: number; @@ -151,6 +154,7 @@ export class LanguageClientManager implements vscode.Disposable { public subFolderWorkspaces: vscode.Uri[]; private namedOutputChannels: Map = new Map(); private swiftVersion: Version; + private activeDocumentManager = new LSPActiveDocumentManager(); /** Get the current state of the underlying LanguageClient */ public get state(): State { @@ -532,8 +536,10 @@ export class LanguageClientManager implements vscode.Disposable { documentSelector: LanguageClientManager.documentSelector, revealOutputChannelOn: RevealOutputChannelOn.Never, workspaceFolder: workspaceFolder, - outputChannel: new SwiftOutputChannel("SourceKit Language Server", false), + outputChannel: new SwiftOutputChannel("SourceKit Language Server"), middleware: { + didOpen: this.activeDocumentManager.didOpen.bind(this.activeDocumentManager), + didClose: this.activeDocumentManager.didClose.bind(this.activeDocumentManager), provideCodeLenses: async (document, token, next) => { const result = await next(document, token); return result?.map(codelens => { @@ -666,6 +672,13 @@ export class LanguageClientManager implements vscode.Disposable { }; } + if (this.swiftVersion.isGreaterThanOrEqual(new Version(6, 1, 0))) { + options = { + ...options, + "window/didChangeActiveDocument": true, // the client can send `window/didChangeActiveDocument` notifications + }; + } + if (configuration.swiftSDK !== "") { options = { ...options, @@ -715,6 +728,21 @@ export class LanguageClientManager implements vscode.Disposable { this.peekDocuments = activatePeekDocuments(client); this.getReferenceDocument = activateGetReferenceDocument(client); this.workspaceContext.subscriptions.push(this.getReferenceDocument); + try { + if ( + checkExperimentalCapability( + client, + DidChangeActiveDocumentNotification.method, + 1 + ) + ) { + this.didChangeActiveDocument = + this.activeDocumentManager.activateDidChangeActiveDocument(client); + this.workspaceContext.subscriptions.push(this.didChangeActiveDocument); + } + } catch { + // do nothing + } }) .catch(reason => { this.workspaceContext.outputChannel.log(`${reason}`); @@ -846,3 +874,20 @@ type SourceKitDocumentSelector = { scheme: string; language: string; }[]; + +/** + * Returns `true` if the LSP supports the supplied `method` at or + * above the supplied `minVersion`. + */ +export function checkExperimentalCapability( + client: LanguageClient, + method: string, + minVersion: number +) { + const experimentalCapability = client.initializeResult?.capabilities.experimental; + if (!experimentalCapability) { + throw new Error(`${method} requests not supported`); + } + const targetCapability = experimentalCapability[method]; + return (targetCapability?.version ?? -1) >= minVersion; +} diff --git a/src/sourcekit-lsp/didChangeActiveDocument.ts b/src/sourcekit-lsp/didChangeActiveDocument.ts new file mode 100644 index 000000000..dc26625b3 --- /dev/null +++ b/src/sourcekit-lsp/didChangeActiveDocument.ts @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as langclient from "vscode-languageclient/node"; +import { checkExperimentalCapability } from "./LanguageClientManager"; +import { DidChangeActiveDocumentNotification } from "./extensions/DidChangeActiveDocumentRequest"; + +/** + * Monitors the active document and notifies the LSP whenever it changes. + * Only sends notifications for documents that produce `textDocument/didOpen`/`textDocument/didClose` + * requests to the client. + */ +export class LSPActiveDocumentManager { + private openDocuments = new Set(); + private lastActiveDocument: langclient.TextDocumentIdentifier | null = null; + + // These are LSP middleware functions that listen for document open and close events. + public async didOpen( + document: vscode.TextDocument, + next: (data: vscode.TextDocument) => Promise + ) { + this.openDocuments.add(document.uri); + next(document); + } + + public async didClose( + document: vscode.TextDocument, + next: (data: vscode.TextDocument) => Promise + ) { + this.openDocuments.add(document.uri); + next(document); + } + + public activateDidChangeActiveDocument(client: langclient.LanguageClient): vscode.Disposable { + // Fire an inital notification on startup if there is an open document. + this.sendNotification(client, vscode.window.activeTextEditor?.document); + + // Listen for the active editor to change and send a notification. + return vscode.window.onDidChangeActiveTextEditor(event => { + this.sendNotification(client, event?.document); + }); + } + + private sendNotification( + client: langclient.LanguageClient, + document: vscode.TextDocument | undefined + ) { + if (checkExperimentalCapability(client, DidChangeActiveDocumentNotification.method, 1)) { + const textDocument = + document && this.openDocuments.has(document.uri) + ? client.code2ProtocolConverter.asTextDocumentIdentifier(document) + : null; + + // Avoid sending multiple identical notifications in a row. + if (textDocument !== this.lastActiveDocument) { + client.sendNotification(DidChangeActiveDocumentNotification.method, { + textDocument: textDocument, + }); + } + this.lastActiveDocument = textDocument; + } + } +} diff --git a/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts b/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts new file mode 100644 index 000000000..75189c5bd --- /dev/null +++ b/src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest.ts @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { MessageDirection, NotificationType, TextDocumentIdentifier } from "vscode-languageclient"; + +// We use namespaces to store request information just like vscode-languageclient +/* eslint-disable @typescript-eslint/no-namespace */ + +export interface DidChangeActiveDocumentParams { + /** + * The document that is being displayed in the active editor. + */ + textDocument?: TextDocumentIdentifier; +} + +/** + * Notify the server that the active document has changed. + */ +export namespace DidChangeActiveDocumentNotification { + export const method = "window/didChangeActiveDocument" as const; + export const messageDirection: MessageDirection = MessageDirection.clientToServer; + export const type = new NotificationType(method); +} diff --git a/src/tasks/SwiftExecution.ts b/src/tasks/SwiftExecution.ts index d7b23e515..3cb73495f 100644 --- a/src/tasks/SwiftExecution.ts +++ b/src/tasks/SwiftExecution.ts @@ -13,11 +13,12 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; -import { SwiftProcess, SwiftPtyProcess } from "./SwiftProcess"; +import { ReadOnlySwiftProcess, SwiftProcess, SwiftPtyProcess } from "./SwiftProcess"; import { SwiftPseudoterminal } from "./SwiftPseudoterminal"; export interface SwiftExecutionOptions extends vscode.ProcessExecutionOptions { presentation?: vscode.TaskPresentationOptions; + readOnlyTerminal?: boolean; } /** @@ -30,11 +31,15 @@ export class SwiftExecution extends vscode.CustomExecution { public readonly command: string, public readonly args: string[], public readonly options: SwiftExecutionOptions, - swiftProcess: SwiftProcess = new SwiftPtyProcess(command, args, options) + private readonly swiftProcess: SwiftProcess = options.readOnlyTerminal + ? new ReadOnlySwiftProcess(command, args, options) + : new SwiftPtyProcess(command, args, options) ) { super(async () => { return new SwiftPseudoterminal(swiftProcess, options.presentation || {}); }); + + this.swiftProcess = swiftProcess; this.onDidWrite = swiftProcess.onDidWrite; this.onDidClose = swiftProcess.onDidClose; } @@ -54,4 +59,11 @@ export class SwiftExecution extends vscode.CustomExecution { * @see {@link SwiftProcess.onDidClose} */ onDidClose: vscode.Event; + + /** + * Terminate the underlying executable. + */ + terminate(signal?: NodeJS.Signals) { + this.swiftProcess.terminate(signal); + } } diff --git a/src/tasks/SwiftPluginTaskProvider.ts b/src/tasks/SwiftPluginTaskProvider.ts index f0339b2de..7aaca41f4 100644 --- a/src/tasks/SwiftPluginTaskProvider.ts +++ b/src/tasks/SwiftPluginTaskProvider.ts @@ -82,7 +82,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { task.definition.command, ...task.definition.args, ]; - swiftArgs = this.workspaceContext.toolchain.buildFlags.withSwiftSDKFlags(swiftArgs); + swiftArgs = this.workspaceContext.toolchain.buildFlags.withAdditionalFlags(swiftArgs); const cwd = resolveTaskCwd(task, task.definition.cwd); const newTask = new vscode.Task( @@ -122,7 +122,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { plugin.command, ...definition.args, ]; - swiftArgs = this.workspaceContext.toolchain.buildFlags.withSwiftSDKFlags(swiftArgs); + swiftArgs = this.workspaceContext.toolchain.buildFlags.withAdditionalFlags(swiftArgs); const presentation = config?.presentationOptions ?? {}; const task = new vscode.Task( @@ -202,7 +202,7 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { * are keyed by either plugin command name (package), or in the form `name:command`. * User-configured permissions take precedence over the hardcoded permissions, and the more * specific form of `name:command` takes precedence over the more general form of `name`. - * @param folderContext The folder context to search for the `swift.pluginPermissions` key. + * @param folderContext The folder context to search for the `swift.pluginPermissions` and `swift.pluginArguments` keys. * @param taskDefinition The task definition to search for the `disableSandbox` and `allowWritingToPackageDirectory` keys. * @param plugin The plugin to generate arguments for. * @returns A list of permission related arguments to pass when invoking the plugin. @@ -213,9 +213,14 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { plugin: PackagePlugin ): string[] { const config = configuration.folder(folderContext); + const globalPackageConfig = config.pluginPermissions(); const packageConfig = config.pluginPermissions(plugin.package); const commandConfig = config.pluginPermissions(`${plugin.package}:${plugin.command}`); + const globalPackageArgs = config.pluginArguments(); + const packageArgs = config.pluginArguments(plugin.package); + const commandArgs = config.pluginArguments(`${plugin.package}:${plugin.command}`); + const taskDefinitionConfiguration: PluginPermissionConfiguration = {}; if (taskDefinition.disableSandbox) { taskDefinitionConfiguration.disableSandbox = true; @@ -232,11 +237,17 @@ export class SwiftPluginTaskProvider implements vscode.TaskProvider { taskDefinition.allowNetworkConnections; } - return this.pluginArguments({ - ...packageConfig, - ...commandConfig, - ...taskDefinitionConfiguration, - }); + return [ + ...globalPackageArgs, + ...packageArgs, + ...commandArgs, + ...this.pluginArguments({ + ...globalPackageConfig, + ...packageConfig, + ...commandConfig, + ...taskDefinitionConfiguration, + }), + ]; } private pluginArguments(config: PluginPermissionConfiguration): string[] { diff --git a/src/tasks/SwiftProcess.ts b/src/tasks/SwiftProcess.ts index 1c0ce05e8..012587834 100644 --- a/src/tasks/SwiftProcess.ts +++ b/src/tasks/SwiftProcess.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import type * as nodePty from "node-pty"; +import * as child_process from "child_process"; import * as vscode from "vscode"; import { requireNativeModule } from "../utilities/native"; @@ -153,3 +154,92 @@ export class SwiftPtyProcess implements SwiftProcess { onDidClose: vscode.Event = this.closeEmitter.event; } + +/** + * A {@link SwiftProcess} that spawns a child process and does not bind to stdio. + * + * Use this for Swift tasks that do not need to accept input, as its lighter weight and + * less error prone than using a spawned node-pty process. + * + * Specifically node-pty on Linux suffers from a long standing issue where the last chunk + * of output before a program exits is sometimes dropped, especially if that program produces + * a lot of output immediately before exiting. See https://github.com/microsoft/node-pty/issues/72 + */ +export class ReadOnlySwiftProcess implements SwiftProcess { + private readonly spawnEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly writeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly errorEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private readonly closeEmitter: vscode.EventEmitter = new vscode.EventEmitter< + number | void + >(); + + private spawnedProcess: child_process.ChildProcessWithoutNullStreams | undefined; + + constructor( + public readonly command: string, + public readonly args: string[], + private readonly options: vscode.ProcessExecutionOptions = {} + ) {} + + spawn(): void { + try { + this.spawnedProcess = child_process.spawn(this.command, this.args, { + cwd: this.options.cwd, + env: { ...process.env, ...this.options.env }, + }); + this.spawnEmitter.fire(); + + this.spawnedProcess.stdout.on("data", data => { + this.writeEmitter.fire(data.toString()); + }); + + this.spawnedProcess.stderr.on("data", data => { + this.writeEmitter.fire(data.toString()); + }); + + this.spawnedProcess.on("error", error => { + this.errorEmitter.fire(new Error(`${error}`)); + this.closeEmitter.fire(); + }); + + this.spawnedProcess.once("exit", code => { + this.closeEmitter.fire(code ?? undefined); + this.dispose(); + }); + } catch (error) { + this.errorEmitter.fire(new Error(`${error}`)); + this.closeEmitter.fire(); + this.dispose(); + } + } + + handleInput(_s: string): void { + // Do nothing + } + + terminate(signal?: NodeJS.Signals): void { + if (!this.spawnedProcess) { + return; + } + this.spawnedProcess.kill(signal); + this.dispose(); + } + + setDimensions(_dimensions: vscode.TerminalDimensions): void { + // Do nothing + } + + dispose(): void { + this.spawnedProcess?.stdout.removeAllListeners(); + this.spawnedProcess?.stderr.removeAllListeners(); + this.spawnedProcess?.removeAllListeners(); + } + + onDidSpawn: vscode.Event = this.spawnEmitter.event; + + onDidWrite: vscode.Event = this.writeEmitter.event; + + onDidThrowError: vscode.Event = this.errorEmitter.event; + + onDidClose: vscode.Event = this.closeEmitter.event; +} diff --git a/src/tasks/SwiftTaskProvider.ts b/src/tasks/SwiftTaskProvider.ts index fbf5ba946..e0d02c86c 100644 --- a/src/tasks/SwiftTaskProvider.ts +++ b/src/tasks/SwiftTaskProvider.ts @@ -268,10 +268,11 @@ export function createSwiftTask( name: string, config: TaskConfig, toolchain: SwiftToolchain, - cmdEnv: { [key: string]: string } = {} + cmdEnv: { [key: string]: string } = {}, + options: { readOnlyTerminal: boolean } = { readOnlyTerminal: false } ): SwiftTask { const swift = toolchain.getToolchainExecutable("swift"); - args = toolchain.buildFlags.withSwiftPackageFlags(toolchain.buildFlags.withSwiftSDKFlags(args)); + args = toolchain.buildFlags.withAdditionalFlags(args); // Add relative path current working directory const cwd = config.cwd.fsPath; @@ -313,6 +314,7 @@ export function createSwiftTask( cwd: fullCwd, env: env, presentation, + readOnlyTerminal: options.readOnlyTerminal, }) ); // This doesn't include any quotes added by VS Code. @@ -422,7 +424,8 @@ export class SwiftTaskProvider implements vscode.TaskProvider { resolveTask(task: vscode.Task, token: vscode.CancellationToken): vscode.Task { // We need to create a new Task object here. // Reusing the task parameter doesn't seem to work. - const swift = this.workspaceContext.toolchain.getToolchainExecutable("swift"); + const toolchain = this.workspaceContext.toolchain; + const swift = toolchain.getToolchainExecutable("swift"); // platform specific let platform: TaskPlatformSpecificConfig | undefined; if (process.platform === "win32") { @@ -436,6 +439,11 @@ export class SwiftTaskProvider implements vscode.TaskProvider { const args = platform?.args ?? task.definition.args; const env = platform?.env ?? task.definition.env; const fullCwd = resolveTaskCwd(task, platform?.cwd ?? task.definition.cwd); + const fullEnv = { + ...configuration.swiftEnvironmentVariables, + ...swiftRuntimeEnv(), + ...env, + }; const presentation = task.definition.presentation ?? task.presentationOptions ?? {}; const newTask = new vscode.Task( @@ -445,7 +453,7 @@ export class SwiftTaskProvider implements vscode.TaskProvider { "swift", new SwiftExecution(swift, args, { cwd: fullCwd, - env: { ...env, ...swiftRuntimeEnv() }, + env: fullEnv, presentation, }), task.problemMatchers diff --git a/src/toolchain/BuildFlags.ts b/src/toolchain/BuildFlags.ts index 95f376dab..1c4f3a6ee 100644 --- a/src/toolchain/BuildFlags.ts +++ b/src/toolchain/BuildFlags.ts @@ -37,7 +37,7 @@ export class BuildFlags { * * @param args original commandline arguments */ - withSwiftSDKFlags(args: string[]): string[] { + private withSwiftSDKFlags(args: string[]): string[] { switch (args[0]) { case "package": { const subcommand = args.splice(0, 2).concat(this.buildPathFlags()); @@ -71,11 +71,14 @@ export class BuildFlags { withSwiftPackageFlags(args: string[]): string[] { switch (args[0]) { - case "package": - if (args[1] === "resolve" || args[1] === "update") { - return [...args, ...configuration.packageArguments]; + case "package": { + if (args[1] === "init") { + return args; } - return args; + const newArgs = [...args]; + newArgs.splice(1, 0, ...configuration.packageArguments); + return newArgs; + } case "build": case "run": case "test": @@ -192,6 +195,38 @@ export class BuildFlags { return indirect ? args.flatMap(arg => ["-Xswiftc", arg]) : args; } + /** + * Get modified swift arguments with new arguments for disabling + * sandboxing if the `swift.disableSandbox` setting is enabled. + * + * @param args original commandline arguments + */ + private withDisableSandboxFlags(args: string[]): string[] { + if (!configuration.disableSandbox) { + return args; + } + const disableSandboxFlags = ["--disable-sandbox", "-Xswiftc", "-disable-sandbox"]; + switch (args[0]) { + case "package": { + return [args[0], ...disableSandboxFlags, ...args.slice(1)]; + } + case "build": + case "run": + case "test": { + return [...args, ...disableSandboxFlags]; + } + default: + // Do nothing for other commands + return args; + } + } + + withAdditionalFlags(args: string[]): string[] { + return this.withSwiftPackageFlags( + this.withDisableSandboxFlags(this.withSwiftSDKFlags(args)) + ); + } + /** * Filter argument list * @param args argument list diff --git a/src/toolchain/ToolchainVersion.ts b/src/toolchain/ToolchainVersion.ts new file mode 100644 index 000000000..752d9199b --- /dev/null +++ b/src/toolchain/ToolchainVersion.ts @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export interface SwiftlyConfig { + installedToolchains: string[]; + inUse: string; + version: string; +} + +/** + * This code is a port of the toolchain version parsing in Swiftly. + * Until Swiftly can report the location of the toolchains under its management + * use `ToolchainVersion.parse(versionString)` to reconstruct the directory name of the toolchain on disk. + * https://github.com/swiftlang/swiftly/blob/bd6884316817e400a0ec512599f046fa437e9760/Sources/SwiftlyCore/ToolchainVersion.swift# + */ +// +// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05). +export class ToolchainVersion { + private type: "stable" | "snapshot"; + private value: StableRelease | Snapshot; + + constructor( + value: + | { + type: "stable"; + major: number; + minor: number; + patch: number; + } + | { + type: "snapshot"; + branch: Branch; + date: string; + } + ) { + if (value.type === "stable") { + this.type = "stable"; + this.value = new StableRelease(value.major, value.minor, value.patch); + } else { + this.type = "snapshot"; + this.value = new Snapshot(value.branch, value.date); + } + } + + private static stableRegex = /^(?:Swift )?(\d+)\.(\d+)\.(\d+)$/; + private static mainSnapshotRegex = /^main-snapshot-(\d{4}-\d{2}-\d{2})$/; + private static releaseSnapshotRegex = /^(\d+)\.(\d+)-snapshot-(\d{4}-\d{2}-\d{2})$/; + + /** + * Parse a toolchain version from the provided string + **/ + static parse(string: string): ToolchainVersion { + let match: RegExpMatchArray | null; + + // Try to match as stable release + match = string.match(this.stableRegex); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + const patch = parseInt(match[3], 10); + + if (isNaN(major) || isNaN(minor) || isNaN(patch)) { + throw new Error(`invalid stable version: ${string}`); + } + + return new ToolchainVersion({ + type: "stable", + major, + minor, + patch, + }); + } + + // Try to match as main snapshot + match = string.match(this.mainSnapshotRegex); + if (match) { + return new ToolchainVersion({ + type: "snapshot", + branch: Branch.main(), + date: match[1], + }); + } + + // Try to match as release snapshot + match = string.match(this.releaseSnapshotRegex); + if (match) { + const major = parseInt(match[1], 10); + const minor = parseInt(match[2], 10); + + if (isNaN(major) || isNaN(minor)) { + throw new Error(`invalid release snapshot version: ${string}`); + } + + return new ToolchainVersion({ + type: "snapshot", + branch: Branch.release(major, minor), + date: match[3], + }); + } + + throw new Error(`invalid toolchain version: "${string}"`); + } + + get name(): string { + if (this.type === "stable") { + const release = this.value as StableRelease; + return `${release.major}.${release.minor}.${release.patch}`; + } else { + const snapshot = this.value as Snapshot; + if (snapshot.branch.type === "main") { + return `main-snapshot-${snapshot.date}`; + } else { + return `${snapshot.branch.major}.${snapshot.branch.minor}-snapshot-${snapshot.date}`; + } + } + } + + get identifier(): string { + if (this.type === "stable") { + const release = this.value as StableRelease; + if (release.patch === 0) { + if (release.minor === 0) { + return `swift-${release.major}-RELEASE`; + } + return `swift-${release.major}.${release.minor}-RELEASE`; + } + return `swift-${release.major}.${release.minor}.${release.patch}-RELEASE`; + } else { + const snapshot = this.value as Snapshot; + if (snapshot.branch.type === "main") { + return `swift-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`; + } else { + return `swift-${snapshot.branch.major}.${snapshot.branch.minor}-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`; + } + } + } + + get description(): string { + return this.value.description; + } +} + +class Branch { + static main(): Branch { + return new Branch("main", null, null); + } + + static release(major: number, minor: number): Branch { + return new Branch("release", major, minor); + } + + private constructor( + public type: "main" | "release", + public _major: number | null, + public _minor: number | null + ) {} + + get description(): string { + switch (this.type) { + case "main": + return "main"; + case "release": + return `${this._major}.${this._minor} development`; + } + } + + get name(): string { + switch (this.type) { + case "main": + return "main"; + case "release": + return `${this._major}.${this._minor}`; + } + } + + get major(): number | null { + return this._major; + } + + get minor(): number | null { + return this._minor; + } +} + +// Snapshot class +class Snapshot { + // Branch enum + + branch: Branch; + date: string; + + constructor(branch: Branch, date: string) { + this.branch = branch; + this.date = date; + } + + get description(): string { + if (this.branch.type === "main") { + return `main-snapshot-${this.date}`; + } else { + return `${this.branch.major}.${this.branch.minor}-snapshot-${this.date}`; + } + } +} + +class StableRelease { + major: number; + minor: number; + patch: number; + + constructor(major: number, minor: number, patch: number) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + get description(): string { + return `Swift ${this.major}.${this.minor}.${this.patch}`; + } +} diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 9ee7b3938..f765f692c 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -24,6 +24,7 @@ import { expandFilePathTilde, pathExists } from "../utilities/filesystem"; import { Version } from "../utilities/version"; import { BuildFlags } from "./BuildFlags"; import { Sanitizer } from "./Sanitizer"; +import { SwiftlyConfig, ToolchainVersion } from "./ToolchainVersion"; /** * Contents of **Info.plist** on Windows. @@ -120,24 +121,28 @@ export class SwiftToolchain { const swiftFolderPath = await this.getSwiftFolderPath(); const toolchainPath = await this.getToolchainPath(swiftFolderPath); const targetInfo = await this.getSwiftTargetInfo(); - const swiftVersion = await this.getSwiftVersion(targetInfo); - const runtimePath = await this.getRuntimePath(targetInfo); - const defaultSDK = await this.getDefaultSDK(); + const swiftVersion = this.getSwiftVersion(targetInfo); + const [runtimePath, defaultSDK] = await Promise.all([ + this.getRuntimePath(targetInfo), + this.getDefaultSDK(), + ]); const customSDK = this.getCustomSDK(); - const xcTestPath = await this.getXCTestPath( - targetInfo, - swiftFolderPath, - swiftVersion, - runtimePath, - customSDK ?? defaultSDK - ); - const swiftTestingPath = await this.getSwiftTestingPath( - targetInfo, - swiftVersion, - runtimePath, - customSDK ?? defaultSDK - ); - const swiftPMTestingHelperPath = await this.getSwiftPMTestingHelperPath(toolchainPath); + const [xcTestPath, swiftTestingPath, swiftPMTestingHelperPath] = await Promise.all([ + this.getXCTestPath( + targetInfo, + swiftFolderPath, + swiftVersion, + runtimePath, + customSDK ?? defaultSDK + ), + this.getSwiftTestingPath( + targetInfo, + swiftVersion, + runtimePath, + customSDK ?? defaultSDK + ), + this.getSwiftPMTestingHelperPath(toolchainPath), + ]); return new SwiftToolchain( swiftFolderPath, @@ -221,16 +226,21 @@ export class SwiftToolchain { const { stdout: xcodes } = await execFile("mdfind", [ `kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`, ]); + // An empty string means no Xcodes are installed. + if (xcodes.length === 0) { + return []; + } return xcodes.trimEnd().split("\n"); } /** - * Reads the swiftly configuration file to find a list of installed toolchains. + * Finds the list of toolchains managed by Swiftly. * * @returns an array of toolchain paths */ public static async getSwiftlyToolchainInstalls(): Promise { // Swiftly is only available on Linux right now + // TODO: Add support for macOS if (process.platform !== "linux") { return []; } @@ -239,12 +249,8 @@ export class SwiftToolchain { if (!swiftlyHomeDir) { return []; } - const swiftlyConfigRaw = await fs.readFile( - path.join(swiftlyHomeDir, "config.json"), - "utf-8" - ); - const swiftlyConfig: unknown = JSON.parse(swiftlyConfigRaw); - if (!(swiftlyConfig instanceof Object) || !("installedToolchains" in swiftlyConfig)) { + const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig(); + if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) { return []; } const installedToolchains = swiftlyConfig.installedToolchains; @@ -259,6 +265,23 @@ export class SwiftToolchain { } } + /** + * Reads the Swiftly configuration file, if it exists. + * + * @returns A parsed Swiftly configuration. + */ + private static async getSwiftlyConfig(): Promise { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (!swiftlyHomeDir) { + return; + } + const swiftlyConfigRaw = await fs.readFile( + path.join(swiftlyHomeDir, "config.json"), + "utf-8" + ); + return JSON.parse(swiftlyConfigRaw); + } + /** * Checks common directories for available swift toolchain installations. * @@ -268,9 +291,11 @@ export class SwiftToolchain { if (process.platform !== "darwin") { return []; } + // TODO: If Swiftly is managing these toolchains then omit them return Promise.all([ this.findToolchainsIn("/Library/Developer/Toolchains/"), this.findToolchainsIn(path.join(os.homedir(), "Library/Developer/Toolchains/")), + this.findCommandLineTools(), ]).then(results => results.flatMap(a => a)); } @@ -349,6 +374,22 @@ export class SwiftToolchain { return result; } + /** + * Returns the path to the CommandLineTools toolchain if its installed. + */ + public static async findCommandLineTools(): Promise { + const commandLineToolsPath = "/Library/Developer/CommandLineTools"; + if (!(await pathExists(commandLineToolsPath))) { + return []; + } + + const toolchainSwiftPath = path.join(commandLineToolsPath, "usr", "bin", "swift"); + if (!(await pathExists(toolchainSwiftPath))) { + return []; + } + return [commandLineToolsPath]; + } + /** * Return fullpath for toolchain executable */ @@ -462,7 +503,9 @@ export class SwiftToolchain { if (!base) { return undefined; } - return path.join(base, "Library/Frameworks"); + const frameworks = path.join(base, "Library/Frameworks"); + const privateFrameworks = path.join(base, "Library/PrivateFrameworks"); + return `${frameworks}:${privateFrameworks}`; } get diagnostics(): string { @@ -579,6 +622,12 @@ export class SwiftToolchain { if (configuration.path !== "") { return path.dirname(configuration.path); } + + const swiftlyToolchainLocation = await this.swiftlyToolchainLocation(); + if (swiftlyToolchainLocation) { + return swiftlyToolchainLocation; + } + const { stdout } = await execFile("xcrun", ["--find", "swift"], { env: configuration.swiftEnvironmentVariables, }); @@ -594,6 +643,31 @@ export class SwiftToolchain { } } + /** + * Determine if Swiftly is being used to manage the active toolchain and if so, return + * the path to the active toolchain. + * @returns The location of the active toolchain if swiftly is being used to manage it. + */ + private static async swiftlyToolchainLocation(): Promise { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (swiftlyHomeDir) { + const { stdout: swiftLocation } = await execFile("which", ["swift"]); + if (swiftLocation.indexOf(swiftlyHomeDir) === 0) { + const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig(); + if (swiftlyConfig) { + const version = ToolchainVersion.parse(swiftlyConfig.inUse); + return path.join( + os.homedir(), + "Library/Developer/Toolchains/", + `${version.identifier}.xctoolchain`, + "usr" + ); + } + } + } + return undefined; + } + /** * @param targetInfo swift target info * @returns path to Swift runtime @@ -767,7 +841,7 @@ export class SwiftToolchain { const plistKey = type === "XCTest" ? "XCTEST_VERSION" : "SWIFT_TESTING_VERSION"; const version = infoPlist.DefaultProperties[plistKey]; if (!version) { - new SwiftOutputChannel("swift", true).appendLine( + new SwiftOutputChannel("swift").appendLine( `Warning: ${platformManifest} is missing the ${plistKey} key.` ); return undefined; diff --git a/src/ui/PackageDependencyProvider.ts b/src/ui/PackageDependencyProvider.ts deleted file mode 100644 index feab34bac..000000000 --- a/src/ui/PackageDependencyProvider.ts +++ /dev/null @@ -1,400 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2021 the VS Code Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VS Code Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import * as vscode from "vscode"; -import * as fs from "fs/promises"; -import * as path from "path"; -import configuration from "../configuration"; -import { WorkspaceContext } from "../WorkspaceContext"; -import { FolderOperation } from "../WorkspaceContext"; -import { FolderContext } from "../FolderContext"; -import contextKeys from "../contextKeys"; -import { - Dependency, - PackageContents, - SwiftPackage, - WorkspaceState, - WorkspaceStateDependency, -} from "../SwiftPackage"; -import { BuildFlags } from "../toolchain/BuildFlags"; - -/** - * References: - * - * - Contributing views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.views - * - Contributing welcome views: - * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome - * - Implementing a TreeView: - * https://code.visualstudio.com/api/extension-guides/tree-view - */ - -/** - * A package in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageNode { - constructor( - public name: string, - public path: string, - public location: string, - public version: string, - public type: "local" | "remote" | "editing" - ) {} - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); - item.id = this.path; - item.description = this.version; - item.iconPath = - this.type === "editing" - ? new vscode.ThemeIcon("edit") - : new vscode.ThemeIcon("package"); - item.contextValue = this.type; - item.accessibilityInformation = { label: `Package ${this.name}` }; - return item; - } -} - -/** - * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class FileNode { - constructor( - public name: string, - public path: string, - public isDirectory: boolean - ) {} - - toTreeItem(): vscode.TreeItem { - const item = new vscode.TreeItem( - this.name, - this.isDirectory - ? vscode.TreeItemCollapsibleState.Collapsed - : vscode.TreeItemCollapsibleState.None - ); - item.id = this.path; - item.resourceUri = vscode.Uri.file(this.path); - if (!this.isDirectory) { - item.command = { - command: "vscode.open", - arguments: [item.resourceUri], - title: "Open File", - }; - item.accessibilityInformation = { label: `File ${this.name}` }; - } else { - item.accessibilityInformation = { label: `Folder ${this.name}` }; - } - return item; - } -} - -/** - * A node in the Package Dependencies {@link vscode.TreeView TreeView}. - * - * Can be either a {@link PackageNode} or a {@link FileNode}. - */ -type TreeNode = PackageNode | FileNode; - -/** - * A {@link vscode.TreeDataProvider TreeDataProvider} for the Package Dependencies {@link vscode.TreeView TreeView}. - */ -export class PackageDependenciesProvider implements vscode.TreeDataProvider { - private didChangeTreeDataEmitter = new vscode.EventEmitter< - TreeNode | undefined | null | void - >(); - private workspaceObserver?: vscode.Disposable; - - onDidChangeTreeData = this.didChangeTreeDataEmitter.event; - - constructor(private workspaceContext: WorkspaceContext) { - // default context key to false. These will be updated as folders are given focus - contextKeys.hasPackage = false; - contextKeys.packageHasDependencies = false; - } - - dispose() { - this.workspaceObserver?.dispose(); - } - - observeFolders(treeView: vscode.TreeView) { - this.workspaceObserver = this.workspaceContext.onDidChangeFolders( - ({ folder, operation }) => { - switch (operation) { - case FolderOperation.focus: - if (!folder) { - return; - } - treeView.title = `Package Dependencies (${folder.name})`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderOperation.unfocus: - treeView.title = `Package Dependencies`; - this.didChangeTreeDataEmitter.fire(); - break; - case FolderOperation.resolvedUpdated: - if (!folder) { - return; - } - if (folder === this.workspaceContext.currentFolder) { - this.didChangeTreeDataEmitter.fire(); - } - } - } - ); - } - - getTreeItem(element: TreeNode): vscode.TreeItem { - return element.toTreeItem(); - } - - async getChildren(element?: TreeNode): Promise { - const folderContext = this.workspaceContext.currentFolder; - if (!folderContext) { - return []; - } - if (!element) { - const workspaceState = await folderContext.swiftPackage.loadWorkspaceState(); - return await this.getDependencyGraph(workspaceState, folderContext); - } - - return this.getNodesInDirectory(element.path); - } - - private async getDependencyGraph( - workspaceState: WorkspaceState | undefined, - folderContext: FolderContext - ): Promise { - if (!workspaceState) { - return []; - } - const inUseDependencies = await this.getInUseDependencies(workspaceState, folderContext); - return ( - workspaceState?.object.dependencies - .filter(dependency => - inUseDependencies.has(dependency.packageRef.identity.toLowerCase()) - ) - .map(dependency => { - const type = this.dependencyType(dependency); - const version = this.dependencyDisplayVersion(dependency); - const packagePath = this.dependencyPackagePath( - dependency, - folderContext.folder.fsPath - ); - const location = dependency.packageRef.location; - return new PackageNode( - dependency.packageRef.identity, - packagePath, - location, - version, - type - ); - }) ?? [] - ); - } - - /** - * * Returns a set of all dependencies that are in use in the workspace. - * Why tranverse is necessary here? - * * If we have an implicit local dependency of a dependency, you may not be able to see it in either `Package.swift` or `Package.resolved` unless tranversing from root Package.swift. - * Why not using `swift package show-dependencies`? - * * it costs more time and it triggers the file change of `workspace-state.json` which is not necessary - * Why not using `workspace-state.json` directly? - * * `workspace-state.json` contains all necessary dependencies but it also contains dependencies that are not in use. - * Here is the implementation details: - * 1. local/remote/edited dependency has remote/edited dependencies, Package.resolved covers them - * 2. remote/edited dependency has a local dependency, the local dependency must have been declared in root Package.swift - * 3. local dependency has a local dependency, traverse it and find the local dependencies only recursively - * 4. pins include all remote and edited packages for 1, 2 - */ - private async getInUseDependencies( - workspaceState: WorkspaceState, - folderContext: FolderContext - ): Promise> { - const localDependencies = await this.getLocalDependencySet(workspaceState, folderContext); - const remoteDependencies = this.getRemoteDependencySet(folderContext); - const editedDependencies = this.getEditedDependencySet(workspaceState); - return new Set([ - ...localDependencies, - ...remoteDependencies, - ...editedDependencies, - ]); - } - - private getRemoteDependencySet(folderContext: FolderContext | undefined): Set { - return new Set(folderContext?.swiftPackage.resolved?.pins.map(pin => pin.identity)); - } - - private getEditedDependencySet(workspaceState: WorkspaceState): Set { - return new Set( - workspaceState.object.dependencies - .filter(dependency => this.dependencyType(dependency) === "editing") - .map(dependency => dependency.packageRef.identity) - ); - } - - /** - * @param workspaceState the workspace state read from `Workspace-state.json` - * @param folderContext the folder context of the current folder - * @returns all local in-use dependencies - */ - private async getLocalDependencySet( - workspaceState: WorkspaceState, - folderContext: FolderContext - ): Promise> { - const rootDependencies = folderContext.swiftPackage.dependencies ?? []; - const workspaceStateDependencies = workspaceState.object.dependencies ?? []; - const workspacePath = folderContext.folder.fsPath; - - const showingDependencies: Set = new Set(); - const stack: Dependency[] = rootDependencies.slice(); - - while (stack.length > 0) { - const top = stack.pop(); - if (!top) { - continue; - } - - if (showingDependencies.has(top.identity)) { - continue; - } - - if (top.type !== "local" && top.type !== "fileSystem") { - continue; - } - - showingDependencies.add(top.identity); - const workspaceStateDependency = workspaceStateDependencies.find( - workspaceStateDependency => - workspaceStateDependency.packageRef.identity === top.identity - ); - if (!workspaceStateDependency) { - continue; - } - - const packagePath = this.dependencyPackagePath(workspaceStateDependency, workspacePath); - const childDependencyContents = (await SwiftPackage.loadPackage( - vscode.Uri.file(packagePath), - folderContext.workspaceContext.toolchain - )) as PackageContents; - - stack.push(...childDependencyContents.dependencies); - } - return showingDependencies; - } - - /** - * Returns a {@link FileNode} for every file or subdirectory - * in the given directory. - */ - private async getNodesInDirectory(directoryPath: string): Promise { - const contents = await fs.readdir(directoryPath); - const results: FileNode[] = []; - const excludes = configuration.excludePathsFromPackageDependencies; - for (const fileName of contents) { - if (excludes.includes(fileName)) { - continue; - } - const filePath = path.join(directoryPath, fileName); - const stats = await fs.stat(filePath); - results.push(new FileNode(fileName, filePath, stats.isDirectory())); - } - return results.sort((first, second) => { - if (first.isDirectory === second.isDirectory) { - // If both nodes are of the same type, sort them by name. - return first.name.localeCompare(second.name); - } else { - // Otherwise, sort directories first. - return first.isDirectory ? -1 : 1; - } - }); - } - - /// - Dependency display helpers - - /** - * Get type of WorkspaceStateDependency for displaying in the tree: real version | edited | local - * @param dependency - * @return "local" | "remote" | "editing" - */ - private dependencyType(dependency: WorkspaceStateDependency): "local" | "remote" | "editing" { - if (dependency.state.name === "edited") { - return "editing"; - } else if ( - dependency.packageRef.kind === "local" || - dependency.packageRef.kind === "fileSystem" - ) { - // need to check for both "local" and "fileSystem" as swift 5.5 and earlier - // use "local" while 5.6 and later use "fileSystem" - return "local"; - } else { - return "remote"; - } - } - - /** - * Get version of WorkspaceStateDependency for displaying in the tree - * @param dependency - * @return real version | editing | local - */ - private dependencyDisplayVersion(dependency: WorkspaceStateDependency): string { - const type = this.dependencyType(dependency); - if (type === "editing") { - return "editing"; - } else if (type === "local") { - return "local"; - } else { - return ( - dependency.state.checkoutState?.version ?? - dependency.state.checkoutState?.branch ?? - dependency.state.checkoutState?.revision.substring(0, 7) ?? - dependency.state.version ?? - "unknown" - ); - } - } - - /** - * * Get package source path of dependency - * `editing`: dependency.state.path ?? workspacePath + Packages/ + dependency.subpath - * `local`: dependency.packageRef.location - * `remote`: buildDirectory + checkouts + dependency.packageRef.location - * @param dependency - * @param workspaceFolder - * @return the package path based on the type - */ - private dependencyPackagePath( - dependency: WorkspaceStateDependency, - workspaceFolder: string - ): string { - const type = this.dependencyType(dependency); - if (type === "editing") { - return ( - dependency.state.path ?? path.join(workspaceFolder, "Packages", dependency.subpath) - ); - } else if (type === "local") { - return dependency.state.path ?? dependency.packageRef.location; - } else { - // remote - const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath( - workspaceFolder, - true - ); - if (dependency.packageRef.kind === "registry") { - return path.join(buildDirectory, "registry", "downloads", dependency.subpath); - } else { - return path.join(buildDirectory, "checkouts", dependency.subpath); - } - } - } -} diff --git a/src/ui/ProjectPanelProvider.ts b/src/ui/ProjectPanelProvider.ts new file mode 100644 index 000000000..8a923cd10 --- /dev/null +++ b/src/ui/ProjectPanelProvider.ts @@ -0,0 +1,623 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as fs from "fs/promises"; +import * as path from "path"; +import configuration from "../configuration"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { FolderOperation } from "../WorkspaceContext"; +import contextKeys from "../contextKeys"; +import { Dependency, ResolvedDependency, Target } from "../SwiftPackage"; +import { SwiftPluginTaskProvider } from "../tasks/SwiftPluginTaskProvider"; +import { FolderContext } from "../FolderContext"; + +const LOADING_ICON = "loading~spin"; +/** + * References: + * + * - Contributing views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.views + * - Contributing welcome views: + * https://code.visualstudio.com/api/references/contribution-points#contributes.viewsWelcome + * - Implementing a TreeView: + * https://code.visualstudio.com/api/extension-guides/tree-view + */ + +/** + * Returns a {@link FileNode} for every file or subdirectory + * in the given directory. + */ +async function getChildren(directoryPath: string, parentId?: string): Promise { + const contents = await fs.readdir(directoryPath); + const results: FileNode[] = []; + const excludes = configuration.excludePathsFromPackageDependencies; + for (const fileName of contents) { + if (excludes.includes(fileName)) { + continue; + } + const filePath = path.join(directoryPath, fileName); + const stats = await fs.stat(filePath); + results.push(new FileNode(fileName, filePath, stats.isDirectory(), parentId)); + } + return results.sort((first, second) => { + if (first.isDirectory === second.isDirectory) { + // If both nodes are of the same type, sort them by name. + return first.name.localeCompare(second.name); + } else { + // Otherwise, sort directories first. + return first.isDirectory ? -1 : 1; + } + }); +} + +/** + * A package in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class PackageNode { + private id: string; + + constructor( + private dependency: ResolvedDependency, + private childDependencies: (dependency: Dependency) => ResolvedDependency[], + private parentId?: string + ) { + this.id = + (this.parentId ? `${this.parentId}->` : "") + + `${this.name}-${this.dependency.version ?? ""}`; + } + + get name(): string { + return this.dependency.identity; + } + + get location(): string { + return this.dependency.location; + } + + get type(): string { + return this.dependency.type; + } + + get path(): string { + return this.dependency.path ?? ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = this.id; + item.description = this.dependency.version; + item.iconPath = new vscode.ThemeIcon(this.icon()); + item.contextValue = this.dependency.type; + item.accessibilityInformation = { label: `Package ${this.name}` }; + item.tooltip = this.path; + return item; + } + + icon() { + if (this.dependency.type === "editing") { + return "edit"; + } + if (this.dependency.type === "local") { + return "notebook-render-output"; + } + return "package"; + } + + async getChildren(): Promise { + const [childDeps, files] = await Promise.all([ + this.childDependencies(this.dependency), + getChildren(this.dependency.path, this.id), + ]); + const childNodes = childDeps.map( + dep => new PackageNode(dep, this.childDependencies, this.id) + ); + + // Show dependencies first, then files. + return [...childNodes, ...files]; + } +} + +/** + * A file or directory in the Package Dependencies {@link vscode.TreeView TreeView}. + */ +export class FileNode { + private id: string; + + constructor( + public name: string, + public path: string, + public isDirectory: boolean, + private parentId?: string + ) { + this.id = (this.parentId ? `${this.parentId}->` : "") + `${this.path}`; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem( + this.name, + this.isDirectory + ? vscode.TreeItemCollapsibleState.Collapsed + : vscode.TreeItemCollapsibleState.None + ); + item.id = this.id; + item.resourceUri = vscode.Uri.file(this.path); + item.tooltip = this.path; + if (!this.isDirectory) { + item.command = { + command: "vscode.open", + arguments: [item.resourceUri], + title: "Open File", + }; + item.accessibilityInformation = { label: `File ${this.name}` }; + } else { + item.accessibilityInformation = { label: `Folder ${this.name}` }; + } + return item; + } + + async getChildren(): Promise { + return await getChildren(this.path, this.id); + } +} + +class TaskNode { + constructor( + public type: string, + public id: string, + public name: string, + private active: boolean + ) {} + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `${this.type}-${this.id}`; + item.iconPath = new vscode.ThemeIcon(this.active ? LOADING_ICON : "play"); + item.contextValue = "task"; + item.accessibilityInformation = { label: this.name }; + item.command = { + command: "swift.runTask", + arguments: [this.name], + title: "Run Task", + }; + return item; + } + + getChildren(): TreeNode[] { + return []; + } +} + +/* + * Prefix a unique string on the test target name to avoid confusing it + * with another target that may share the same name. Targets can't start with % + * so this is guarenteed to be unique. + */ +function testTaskName(name: string): string { + return `%test-${name}`; +} + +function snippetTaskName(name: string): string { + return `%snippet-${name}`; +} + +class TargetNode { + constructor( + public target: Target, + private activeTasks: Set + ) {} + + get name(): string { + return this.target.name; + } + + get args(): string[] { + return [this.name]; + } + + toTreeItem(): vscode.TreeItem { + const name = this.target.name; + const hasChildren = this.getChildren().length > 0; + const item = new vscode.TreeItem( + name, + hasChildren + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.None + ); + item.id = `${this.target.type}:${name}`; + item.iconPath = new vscode.ThemeIcon(this.icon()); + item.contextValue = this.contextValue(); + item.accessibilityInformation = { label: name }; + return item; + } + + private icon(): string { + if (this.activeTasks.has(this.name)) { + return LOADING_ICON; + } + + switch (this.target.type) { + case "executable": + return "output"; + case "library": + return "library"; + case "test": + if (this.activeTasks.has(testTaskName(this.name))) { + return LOADING_ICON; + } + return "test-view-icon"; + case "snippet": + if (this.activeTasks.has(snippetTaskName(this.name))) { + return LOADING_ICON; + } + return "notebook"; + case "plugin": + return "plug"; + } + } + + private contextValue(): string | undefined { + switch (this.target.type) { + case "executable": + return "runnable"; + case "snippet": + return "snippet_runnable"; + case "test": + return "test_runnable"; + default: + return undefined; + } + } + + getChildren(): TreeNode[] { + return []; + } +} + +class HeaderNode { + constructor( + private id: string, + public name: string, + private icon: string, + private _getChildren: () => Promise + ) {} + + get path(): string { + return ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.Collapsed); + item.id = `${this.id}-${this.name}`; + item.iconPath = new vscode.ThemeIcon(this.icon); + item.contextValue = "header"; + item.accessibilityInformation = { label: this.name }; + return item; + } + + getChildren(): Promise { + return this._getChildren(); + } +} + +class ErrorNode { + constructor( + public name: string, + private folder: vscode.Uri + ) {} + + get path(): string { + return ""; + } + + toTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem(this.name, vscode.TreeItemCollapsibleState.None); + item.id = `error-${this.folder.fsPath}`; + item.iconPath = new vscode.ThemeIcon("error", new vscode.ThemeColor("errorForeground")); + item.contextValue = "error"; + item.accessibilityInformation = { label: this.name }; + item.tooltip = + "Could not build the Package.swift, fix the error to refresh the project panel"; + + item.command = { + command: "swift.openManifest", + arguments: [this.folder], + title: "Open Manifest", + }; + return item; + } + + getChildren(): Promise { + return Promise.resolve([]); + } +} + +/** + * A node in the Package Dependencies {@link vscode.TreeView TreeView}. + * + * Can be either a {@link PackageNode}, {@link FileNode}, {@link TargetNode}, {@link TaskNode}, {@link ErrorNode} or {@link HeaderNode}. + */ +export type TreeNode = PackageNode | FileNode | HeaderNode | TaskNode | TargetNode | ErrorNode; + +/** + * A {@link vscode.TreeDataProvider TreeDataProvider} for project dependencies, tasks and commands {@link vscode.TreeView TreeView}. + */ +export class ProjectPanelProvider implements vscode.TreeDataProvider { + private didChangeTreeDataEmitter = new vscode.EventEmitter< + TreeNode | undefined | null | void + >(); + private workspaceObserver?: vscode.Disposable; + private disposables: vscode.Disposable[] = []; + private activeTasks: Set = new Set(); + private lastComputedNodes: TreeNode[] = []; + + onDidChangeTreeData = this.didChangeTreeDataEmitter.event; + + constructor(private workspaceContext: WorkspaceContext) { + // default context key to false. These will be updated as folders are given focus + contextKeys.hasPackage = false; + contextKeys.packageHasDependencies = false; + + this.observeTasks(workspaceContext); + } + + dispose() { + this.workspaceObserver?.dispose(); + } + + observeTasks(ctx: WorkspaceContext) { + this.disposables.push( + vscode.tasks.onDidStartTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.add(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + vscode.tasks.onDidEndTask(e => { + const taskId = e.execution.task.detail ?? e.execution.task.name; + this.activeTasks.delete(taskId); + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartBuild(e => { + if (e.launchConfig.runType === "snippet") { + this.activeTasks.add(snippetTaskName(e.targetName)); + } else { + this.activeTasks.add(e.targetName); + } + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishBuild(e => { + if (e.launchConfig.runType === "snippet") { + this.activeTasks.delete(snippetTaskName(e.targetName)); + } else { + this.activeTasks.delete(e.targetName); + } + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidStartTests(e => { + for (const target of e.targets) { + this.activeTasks.add(testTaskName(target)); + } + this.didChangeTreeDataEmitter.fire(); + }), + ctx.onDidFinishTests(e => { + for (const target of e.targets) { + this.activeTasks.delete(testTaskName(target)); + } + this.didChangeTreeDataEmitter.fire(); + }) + ); + } + + observeFolders(treeView: vscode.TreeView) { + this.workspaceObserver = this.workspaceContext.onDidChangeFolders( + ({ folder, operation }) => { + switch (operation) { + case FolderOperation.focus: + if (!folder) { + return; + } + treeView.title = `Swift Project (${folder.name})`; + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.unfocus: + treeView.title = `Swift Project`; + this.didChangeTreeDataEmitter.fire(); + break; + case FolderOperation.workspaceStateUpdated: + case FolderOperation.resolvedUpdated: + case FolderOperation.packageViewUpdated: + case FolderOperation.pluginsUpdated: + if (!folder) { + return; + } + if (folder === this.workspaceContext.currentFolder) { + this.didChangeTreeDataEmitter.fire(); + } + } + } + ); + } + + getTreeItem(element: TreeNode): vscode.TreeItem { + return element.toTreeItem(); + } + + async getChildren(element?: TreeNode): Promise { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + + if (!element && folderContext.hasResolveErrors) { + return [ + new ErrorNode("Error Parsing Package.swift", folderContext.folder), + ...this.lastComputedNodes, + ]; + } + + const nodes = await this.computeChildren(folderContext, element); + + // If we're fetching the root nodes then save them in case we have an error later, + // in which case we show the ErrorNode along with the last known good nodes. + if (!element) { + this.lastComputedNodes = nodes; + } + return nodes; + } + + async computeChildren(folderContext: FolderContext, element?: TreeNode): Promise { + if (element) { + return element.getChildren(); + } + + const dependencies = this.dependencies(); + const snippets = this.snippets(); + const commands = await this.commands(); + + // TODO: Control ordering + return [ + ...(dependencies.length > 0 + ? [ + new HeaderNode( + "dependencies", + "Dependencies", + "circuit-board", + this.wrapInAsync(this.dependencies.bind(this)) + ), + ] + : []), + new HeaderNode("targets", "Targets", "book", this.wrapInAsync(this.targets.bind(this))), + new HeaderNode( + "tasks", + "Tasks", + "debug-continue-small", + this.tasks.bind(this, folderContext) + ), + ...(snippets.length > 0 + ? [ + new HeaderNode("snippets", "Snippets", "notebook", () => + Promise.resolve(snippets) + ), + ] + : []), + ...(commands.length > 0 + ? [ + new HeaderNode("commands", "Commands", "debug-line-by-line", () => + Promise.resolve(commands) + ), + ] + : []), + ]; + } + + private dependencies(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + const pkg = folderContext.swiftPackage; + if (contextKeys.flatDependenciesList) { + const existenceMap = new Map(); + const gatherChildren = (dependencies: ResolvedDependency[]): ResolvedDependency[] => { + const result: ResolvedDependency[] = []; + for (const dep of dependencies) { + if (!existenceMap.has(dep.identity)) { + result.push(dep); + existenceMap.set(dep.identity, true); + } + const childDeps = pkg.childDependencies(dep); + result.push(...gatherChildren(childDeps)); + } + return result; + }; + + const rootDeps = pkg.rootDependencies(); + const allDeps = gatherChildren(rootDeps); + return allDeps.map(dependency => new PackageNode(dependency, () => [])); + } else { + const childDeps = pkg.childDependencies.bind(pkg); + return pkg.rootDependencies().map(dep => new PackageNode(dep, childDeps)); + } + } + + private targets(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + const targetSort = (node: TargetNode) => `${node.target.type}-${node.name}`; + return ( + folderContext.swiftPackage.targets + // Snipepts are shown under the Snippets header + .filter(target => target.type !== "snippet") + .map(target => new TargetNode(target, this.activeTasks)) + .sort((a, b) => targetSort(a).localeCompare(targetSort(b))) + ); + } + + private async tasks(folderContext: FolderContext): Promise { + const tasks = await vscode.tasks.fetchTasks(); + + return ( + tasks + // Plugin tasks are shown under the Commands header + .filter( + task => + task.definition.cwd === folderContext.folder.fsPath && + task.source !== "swift-plugin" + ) + .map( + (task, i) => + new TaskNode( + "task", + `${task.definition.cwd}-${task.name}-${task.detail ?? ""}-${i}`, + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } + + private async commands(): Promise { + const provider = new SwiftPluginTaskProvider(this.workspaceContext); + const tasks = await provider.provideTasks(new vscode.CancellationTokenSource().token); + return tasks + .map( + (task, i) => + new TaskNode( + "command", + `${task.definition.cwd}-${task.name}-${task.detail ?? ""}-${i}`, + task.name, + this.activeTasks.has(task.detail ?? task.name) + ) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + private snippets(): TreeNode[] { + const folderContext = this.workspaceContext.currentFolder; + if (!folderContext) { + return []; + } + return folderContext.swiftPackage.targets + .filter(target => target.type === "snippet") + .flatMap(target => new TargetNode(target, this.activeTasks)) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + private wrapInAsync(fn: () => T): () => Promise { + return async () => fn(); + } +} diff --git a/src/ui/ReloadExtension.ts b/src/ui/ReloadExtension.ts index b8d201589..a23de1628 100644 --- a/src/ui/ReloadExtension.ts +++ b/src/ui/ReloadExtension.ts @@ -14,23 +14,49 @@ import * as vscode from "vscode"; import { Workbench } from "../utilities/commands"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import debounce = require("lodash.debounce"); /** * Prompts the user to reload the extension in cases where we are unable to do - * so automatically. + * so automatically. Only one of these prompts will be shown at a time. * * @param message the warning message to display to the user * @param items extra buttons to display * @returns the selected button or undefined if cancelled */ -export async function showReloadExtensionNotification( - message: string, - ...items: T[] -): Promise<"Reload Extensions" | T | undefined> { - const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; - const selected = await vscode.window.showWarningMessage(message, ...buttons); - if (selected === "Reload Extensions") { - await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); - } - return selected; +export function showReloadExtensionNotificationInstance() { + let inFlight: Promise<"Reload Extensions" | T | undefined> | null = null; + + return async function ( + message: string, + ...items: T[] + ): Promise<"Reload Extensions" | T | undefined> { + if (inFlight) { + return inFlight; + } + + const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; + inFlight = (async () => { + try { + const selected = await vscode.window.showWarningMessage(message, ...buttons); + if (selected === "Reload Extensions") { + await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); + } + return selected; + } finally { + inFlight = null; + } + })(); + + return inFlight; + }; } + +// In case the user closes the dialog immediately we want to debounce showing it again +// for 10 seconds to prevent another popup perhaps immediately appearing. +export const showReloadExtensionNotification = debounce( + showReloadExtensionNotificationInstance(), + 10_000, + { leading: true } +); diff --git a/src/ui/StatusItem.ts b/src/ui/StatusItem.ts index 107429845..3d78ec29a 100644 --- a/src/ui/StatusItem.ts +++ b/src/ui/StatusItem.ts @@ -116,9 +116,9 @@ export class StatusItem { private showTask(task: RunningTask, message?: string) { message = message ?? task.name; if (typeof task.task !== "string") { - this.show(`$(sync~spin) ${message}`, message, "workbench.action.tasks.showTasks"); + this.show(`$(loading~spin) ${message}`, message, "workbench.action.tasks.showTasks"); } else { - this.show(`$(sync~spin) ${message}`, message); + this.show(`$(loading~spin) ${message}`, message); } } diff --git a/src/ui/SwiftOutputChannel.ts b/src/ui/SwiftOutputChannel.ts index 4dfe482ab..ecad10f33 100644 --- a/src/ui/SwiftOutputChannel.ts +++ b/src/ui/SwiftOutputChannel.ts @@ -25,11 +25,9 @@ export class SwiftOutputChannel implements vscode.OutputChannel { */ constructor( public name: string, - private logToConsole: boolean = true, logStoreLinesSize: number = 250_000 // default to capturing 250k log lines ) { this.name = name; - this.logToConsole = process.env["CI"] !== "1" && logToConsole; this.channel = vscode.window.createOutputChannel(name, "Swift"); this.logStore = new RollingLog(logStoreLinesSize); } @@ -37,21 +35,11 @@ export class SwiftOutputChannel implements vscode.OutputChannel { append(value: string): void { this.channel.append(value); this.logStore.append(value); - - if (this.logToConsole) { - // eslint-disable-next-line no-console - console.log(value); - } } appendLine(value: string): void { this.channel.appendLine(value); this.logStore.appendLine(value); - - if (this.logToConsole) { - // eslint-disable-next-line no-console - console.log(value); - } } replace(value: string): void { diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index f21d8b94a..e2381f90d 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -63,7 +63,7 @@ export async function selectToolchainFolder() { if (!selected || selected.length !== 1) { return; } - await setToolchainPath(selected[0].fsPath, "public"); + await setToolchainPath(selected[0].fsPath); } /** @@ -275,15 +275,25 @@ export async function showToolchainSelectionQuickPick(activeToolchain: SwiftTool } if (selected?.type === "toolchain") { // Select an Xcode to build with - let developerDir: string | undefined; - if (selected.category === "xcode") { - developerDir = selected.xcodePath; - } else if (xcodePaths.length === 1) { - developerDir = xcodePaths[0]; - } else if (process.platform === "darwin" && xcodePaths.length > 1) { - developerDir = await showDeveloperDirQuickPick(xcodePaths); - if (!developerDir) { - return; + let developerDir: string | undefined = undefined; + if (process.platform === "darwin") { + let selectedXcodePath: string | undefined = undefined; + if (selected.category === "xcode") { + selectedXcodePath = selected.xcodePath; + } else if (xcodePaths.length === 1) { + selectedXcodePath = xcodePaths[0]; + } else if (xcodePaths.length > 1) { + selectedXcodePath = await showDeveloperDirQuickPick(xcodePaths); + if (!selectedXcodePath) { + return; + } + } + // Find the actual DEVELOPER_DIR based on the selected Xcode app + if (selectedXcodePath) { + developerDir = await SwiftToolchain.getXcodeDeveloperDir({ + ...process.env, + DEVELOPER_DIR: selectedXcodePath, + }); } } // Update the toolchain path @@ -376,7 +386,7 @@ async function removeToolchainPath() { */ async function setToolchainPath( swiftFolderPath: string | undefined, - developerDir: string | undefined + developerDir?: string ): Promise { let target: vscode.ConfigurationTarget | undefined; const items: (vscode.QuickPickItem & { @@ -410,17 +420,15 @@ async function setToolchainPath( } const swiftConfiguration = vscode.workspace.getConfiguration("swift"); await swiftConfiguration.update("path", swiftFolderPath, target); - if (developerDir) { - const swiftEnv = configuration.swiftEnvironmentVariables; - await swiftConfiguration.update( - "swiftEnvironmentVariables", - { - ...swiftEnv, - DEVELOPER_DIR: developerDir, - }, - target - ); - } + const swiftEnv = configuration.swiftEnvironmentVariables; + await swiftConfiguration.update( + "swiftEnvironmentVariables", + { + ...swiftEnv, + DEVELOPER_DIR: developerDir, + }, + target + ); await checkAndRemoveWorkspaceSetting(target); return true; } diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index 31122bea1..f33e8de0c 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -109,14 +109,15 @@ export async function execFile( options.env = { ...(options.env ?? process.env), ...runtimeEnv }; } } - return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { cp.execFile(executable, args, options, (error, stdout, stderr) => { if (error) { reject(new ExecFileError(error, stdout, stderr)); + } else { + resolve({ stdout, stderr }); } - resolve({ stdout, stderr }); - }) - ); + }); + }); } export async function execFileStreamOutput( @@ -187,7 +188,7 @@ export async function execSwift( swift = toolchain.getToolchainExecutable("swift"); } if (toolchain !== "default") { - args = toolchain.buildFlags.withSwiftSDKFlags(args); + args = toolchain.buildFlags.withAdditionalFlags(args); } if (Object.keys(configuration.swiftEnvironmentVariables).length > 0) { // when adding environment vars we either combine with vars passed diff --git a/src/utilities/workspace.ts b/src/utilities/workspace.ts new file mode 100644 index 000000000..cc0a6fa69 --- /dev/null +++ b/src/utilities/workspace.ts @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2022 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { pathExists } from "./filesystem"; + +export async function searchForPackages( + folder: vscode.Uri, + disableSwiftPMIntegration: boolean, + searchSubfoldersForPackages: boolean +): Promise> { + const folders: Array = []; + + async function search(folder: vscode.Uri) { + // add folder if Package.swift/compile_commands.json/compile_flags.txt/buildServer.json exists + if (await isValidWorkspaceFolder(folder.fsPath, disableSwiftPMIntegration)) { + folders.push(folder); + } + // should I search sub-folders for more Swift Packages + if (!searchSubfoldersForPackages) { + return; + } + + await vscode.workspace.fs.readDirectory(folder).then(async entries => { + for (const entry of entries) { + if ( + entry[1] === vscode.FileType.Directory && + entry[0][0] !== "." && + entry[0] !== "Packages" + ) { + await search(vscode.Uri.joinPath(folder, entry[0])); + } + } + }); + } + + await search(folder); + + return folders; +} + +export async function isValidWorkspaceFolder( + folder: string, + disableSwiftPMIntegration: boolean +): Promise { + return ( + (!disableSwiftPMIntegration && (await pathExists(folder, "Package.swift"))) || + (await pathExists(folder, "compile_commands.json")) || + (await pathExists(folder, "compile_flags.txt")) || + (await pathExists(folder, "buildServer.json")) + ); +} diff --git a/test/integration-tests/BackgroundCompilation.test.ts b/test/integration-tests/BackgroundCompilation.test.ts index 90c8660a4..13df067aa 100644 --- a/test/integration-tests/BackgroundCompilation.test.ts +++ b/test/integration-tests/BackgroundCompilation.test.ts @@ -58,5 +58,5 @@ suite("BackgroundCompilation Test Suite", () => { await vscode.workspace.save(uri); await taskPromise; - }).timeout(120000); + }).timeout(180000); }); diff --git a/test/integration-tests/DiagnosticsManager.test.ts b/test/integration-tests/DiagnosticsManager.test.ts index 5855a5d99..825fd88c8 100644 --- a/test/integration-tests/DiagnosticsManager.test.ts +++ b/test/integration-tests/DiagnosticsManager.test.ts @@ -24,6 +24,7 @@ import { FolderContext } from "../../src/FolderContext"; import { Version } from "../../src/utilities/version"; import { Workbench } from "../../src/utilities/commands"; import { activateExtensionForSuite, folderInRootWorkspace } from "./utilities/testutilities"; +import { expect } from "chai"; const isEqual = (d1: vscode.Diagnostic, d2: vscode.Diagnostic) => { return ( @@ -59,7 +60,7 @@ function assertWithoutDiagnostic(uri: vscode.Uri, expected: vscode.Diagnostic) { suite("DiagnosticsManager Test Suite", async function () { // Was hitting a timeout in suiteSetup during CI build once in a while - this.timeout(5000); + this.timeout(15000); const swiftConfig = vscode.workspace.getConfiguration("swift"); @@ -125,7 +126,7 @@ suite("DiagnosticsManager Test Suite", async function () { activateExtensionForSuite({ async setup(ctx) { - this.timeout(60000 * 2); + this.timeout(60000 * 5); workspaceContext = ctx; toolchain = workspaceContext.toolchain; @@ -216,7 +217,7 @@ suite("DiagnosticsManager Test Suite", async function () { // after first build and can cause intermittent // failure if `swiftc` diagnostic is fixed suiteSetup(async function () { - this.timeout(2 * 60 * 1000); // Allow 2 minutes to build + this.timeout(3 * 60 * 1000); // Allow 3 minutes to build const task = createBuildAllTask(folderContext); // This return exit code and output for the task but we will omit it here // because the failures are expected and we just want the task to build @@ -555,6 +556,116 @@ suite("DiagnosticsManager Test Suite", async function () { await swiftConfig.update("diagnosticsCollection", undefined); }); + suite("markdownLinks", () => { + let diagnostic: vscode.Diagnostic; + + setup(async () => { + workspaceContext.diagnostics.clear(); + diagnostic = new vscode.Diagnostic( + new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 8)), // Note swiftc provides empty range + "Cannot assign to value: 'bar' is a 'let' constant", + vscode.DiagnosticSeverity.Error + ); + diagnostic.source = "SourceKit"; + }); + + test("ignore strings", async () => { + diagnostic.code = "string"; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", "string"); + }); + + test("ignore numbers", async () => { + diagnostic.code = 1; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", 1); + }); + + test("target without markdown link", async () => { + const diagnosticCode = { + value: "string", + target: vscode.Uri.file("/some/path/md/readme.txt"), + }; + diagnostic.code = diagnosticCode; + + // Now provide identical SourceKit diagnostic + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + // check diagnostic hasn't changed + assertHasDiagnostic(mainUri, diagnostic); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code", diagnostic.code); + }); + + test("target with markdown link", async () => { + const pathToMd = "/some/path/md/readme.md"; + diagnostic.code = { + value: "string", + target: vscode.Uri.file(pathToMd), + }; + + workspaceContext.diagnostics.handleDiagnostics( + mainUri, + DiagnosticsManager.isSourcekit, + [diagnostic] + ); + + const diagnostics = vscode.languages.getDiagnostics(mainUri); + const matchingDiagnostic = diagnostics.find(findDiagnostic(diagnostic)); + + expect(matchingDiagnostic).to.have.property("code"); + expect(matchingDiagnostic?.code).to.have.property("value", "More Information..."); + + if ( + matchingDiagnostic && + matchingDiagnostic.code && + typeof matchingDiagnostic.code !== "string" && + typeof matchingDiagnostic.code !== "number" + ) { + expect(matchingDiagnostic.code.target.scheme).to.equal("command"); + expect(matchingDiagnostic.code.target.path).to.equal( + "swift.openEducationalNote" + ); + expect(matchingDiagnostic.code.target.query).to.contain(pathToMd); + } else { + assert.fail("Diagnostic target not replaced with markdown command"); + } + }); + }); + suite("keepAll", () => { setup(async () => { await swiftConfig.update("diagnosticsCollection", "keepAll"); @@ -1062,7 +1173,7 @@ suite("DiagnosticsManager Test Suite", async function () { assertHasDiagnostic(mainUri, expectedDiagnostic1); assertHasDiagnostic(mainUri, expectedDiagnostic2); - }).timeout(2 * 60 * 1000); // Allow 2 minutes to build + }).timeout(3 * 60 * 1000); // Allow 3 minutes to build test("Provides clang diagnostics", async () => { // Build for indexing @@ -1099,6 +1210,6 @@ suite("DiagnosticsManager Test Suite", async function () { assertHasDiagnostic(cUri, expectedDiagnostic1); assertHasDiagnostic(cUri, expectedDiagnostic2); - }).timeout(2 * 60 * 1000); // Allow 2 minutes to build + }).timeout(3 * 60 * 1000); // Allow 3 minutes to build }); }); diff --git a/test/integration-tests/SwiftPackage.test.ts b/test/integration-tests/SwiftPackage.test.ts index 72f9db0f9..2a73e78a0 100644 --- a/test/integration-tests/SwiftPackage.test.ts +++ b/test/integration-tests/SwiftPackage.test.ts @@ -18,9 +18,10 @@ import { SwiftPackage } from "../../src/SwiftPackage"; import { SwiftToolchain } from "../../src/toolchain/toolchain"; import { Version } from "../../src/utilities/version"; -let toolchain: SwiftToolchain; +suite("SwiftPackage Test Suite", function () { + this.timeout(5 * 60 * 1000); // 5 minute timeout + let toolchain: SwiftToolchain; -suite("SwiftPackage Test Suite", () => { setup(async () => { toolchain = await SwiftToolchain.create(); }); @@ -28,13 +29,13 @@ suite("SwiftPackage Test Suite", () => { test("No package", async () => { const spmPackage = await SwiftPackage.create(testAssetUri("empty-folder"), toolchain); assert.strictEqual(spmPackage.foundPackage, false); - }).timeout(10000); + }); test("Invalid package", async () => { const spmPackage = await SwiftPackage.create(testAssetUri("invalid-package"), toolchain); assert.strictEqual(spmPackage.foundPackage, true); assert.strictEqual(spmPackage.isValid, false); - }).timeout(10000); + }); test("Library package", async () => { const spmPackage = await SwiftPackage.create(testAssetUri("package2"), toolchain); @@ -43,16 +44,23 @@ suite("SwiftPackage Test Suite", () => { assert.strictEqual(spmPackage.libraryProducts[0].name, "package2"); assert.strictEqual(spmPackage.dependencies.length, 0); assert.strictEqual(spmPackage.targets.length, 2); - }).timeout(10000); + }); - test("Package resolve v2", async () => { - if (toolchain && toolchain.swiftVersion.isLessThan(new Version(5, 6, 0))) { + test("Package resolve v2", async function () { + if (!toolchain) { return; } + if ( + (process.platform === "win32" && + toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) || + toolchain.swiftVersion.isLessThan(new Version(5, 6, 0)) + ) { + this.skip(); + } const spmPackage = await SwiftPackage.create(testAssetUri("package5.6"), toolchain); assert.strictEqual(spmPackage.isValid, true); assert(spmPackage.resolved !== undefined); - }).timeout(15000); + }); test("Identity case-insensitivity", async () => { const spmPackage = await SwiftPackage.create(testAssetUri("identity-case"), toolchain); @@ -61,7 +69,7 @@ suite("SwiftPackage Test Suite", () => { assert(spmPackage.resolved !== undefined); assert.strictEqual(spmPackage.resolved.pins.length, 1); assert.strictEqual(spmPackage.resolved.pins[0].identity, "yams"); - }).timeout(10000); + }); test("Identity different from name", async () => { const spmPackage = await SwiftPackage.create(testAssetUri("identity-different"), toolchain); @@ -69,6 +77,6 @@ suite("SwiftPackage Test Suite", () => { assert.strictEqual(spmPackage.dependencies.length, 1); assert(spmPackage.resolved !== undefined); assert.strictEqual(spmPackage.resolved.pins.length, 1); - assert.strictEqual(spmPackage.resolved.pins[0].identity, "swift-cmark"); - }).timeout(10000); + assert.strictEqual(spmPackage.resolved.pins[0].identity, "swift-log"); + }); }); diff --git a/test/integration-tests/SwiftSnippet.test.ts b/test/integration-tests/SwiftSnippet.test.ts index 72c594b7d..82b0342f4 100644 --- a/test/integration-tests/SwiftSnippet.test.ts +++ b/test/integration-tests/SwiftSnippet.test.ts @@ -37,7 +37,7 @@ function normalizePath(...segments: string[]): string { } suite("SwiftSnippet Test Suite @slow", function () { - this.timeout(120000); + this.timeout(180000); const uri = testAssetUri("defaultPackage/Snippets/hello.swift"); const breakpoints = [ @@ -62,6 +62,7 @@ suite("SwiftSnippet Test Suite @slow", function () { // Set a breakpoint vscode.debug.addBreakpoints(breakpoints); }, + requiresDebugger: true, }); suiteTeardown(async () => { diff --git a/test/integration-tests/WorkspaceContext.test.ts b/test/integration-tests/WorkspaceContext.test.ts index afd14f630..9f4827f0d 100644 --- a/test/integration-tests/WorkspaceContext.test.ts +++ b/test/integration-tests/WorkspaceContext.test.ts @@ -14,12 +14,13 @@ import * as vscode from "vscode"; import * as assert from "assert"; +import { afterEach } from "mocha"; import { testAssetUri } from "../fixtures"; import { FolderOperation, WorkspaceContext } from "../../src/WorkspaceContext"; import { createBuildAllTask } from "../../src/tasks/SwiftTaskProvider"; import { Version } from "../../src/utilities/version"; import { SwiftExecution } from "../../src/tasks/SwiftExecution"; -import { activateExtensionForSuite } from "./utilities/testutilities"; +import { activateExtensionForSuite, updateSettings } from "./utilities/testutilities"; import { FolderContext } from "../../src/FolderContext"; import { assertContains } from "./testexplorer/utilities"; @@ -82,31 +83,32 @@ suite("WorkspaceContext Test Suite", () => { }).timeout(60000 * 2); }); - suite("Tasks", async function () { + suite("Tasks", function () { activateExtensionForSuite({ async setup(ctx) { workspaceContext = ctx; }, }); + let resetSettings: (() => Promise) | undefined; + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + // Was hitting a timeout in suiteSetup during CI build once in a while this.timeout(5000); - const swiftConfig = vscode.workspace.getConfiguration("swift"); - - suiteTeardown(async () => { - await swiftConfig.update("buildArguments", undefined); - await swiftConfig.update("packageArguments", undefined); - await swiftConfig.update("path", undefined); - await swiftConfig.update("diagnosticsStyle", undefined); - }); - test("Default Task values", async () => { const folder = workspaceContext.folders.find( f => f.folder.fsPath === packageFolder.fsPath ); assert(folder); - await swiftConfig.update("diagnosticsStyle", undefined); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + }); const buildAllTask = createBuildAllTask(folder); const execution = buildAllTask.execution; assert.strictEqual(buildAllTask.definition.type, "swift"); @@ -123,7 +125,9 @@ suite("WorkspaceContext Test Suite", () => { f => f.folder.fsPath === packageFolder.fsPath ); assert(folder); - await swiftConfig.update("diagnosticsStyle", "default"); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "default", + }); const buildAllTask = createBuildAllTask(folder); const execution = buildAllTask.execution; assert.strictEqual(buildAllTask.definition.type, "swift"); @@ -139,7 +143,9 @@ suite("WorkspaceContext Test Suite", () => { f => f.folder.fsPath === packageFolder.fsPath ); assert(folder); - await swiftConfig.update("diagnosticsStyle", "swift"); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "swift", + }); const buildAllTask = createBuildAllTask(folder); const execution = buildAllTask.execution; assert.strictEqual(buildAllTask.definition.type, "swift"); @@ -156,12 +162,13 @@ suite("WorkspaceContext Test Suite", () => { f => f.folder.fsPath === packageFolder.fsPath ); assert(folder); - await swiftConfig.update("diagnosticsStyle", undefined); - await swiftConfig.update("buildArguments", ["--sanitize=thread"]); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + "swift.buildArguments": ["--sanitize=thread"], + }); const buildAllTask = createBuildAllTask(folder); const execution = buildAllTask.execution as SwiftExecution; assertContainsArg(execution, "--sanitize=thread"); - await swiftConfig.update("buildArguments", []); }); test("Package Arguments Settings", async () => { @@ -169,12 +176,13 @@ suite("WorkspaceContext Test Suite", () => { f => f.folder.fsPath === packageFolder.fsPath ); assert(folder); - await swiftConfig.update("diagnosticsStyle", undefined); - await swiftConfig.update("packageArguments", ["--replace-scm-with-registry"]); + resetSettings = await updateSettings({ + "swift.diagnosticsStyle": "", + "swift.packageArguments": ["--replace-scm-with-registry"], + }); const buildAllTask = createBuildAllTask(folder); const execution = buildAllTask.execution as SwiftExecution; assertContainsArg(execution, "--replace-scm-with-registry"); - await swiftConfig.update("packageArguments", []); }); test("Swift Path", async () => { diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index 59d6b1201..e3109e0d3 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -28,7 +28,7 @@ import { Version } from "../../../src/utilities/version"; suite("Build Commands @slow", function () { // Default timeout is a bit too short, give it a little bit more time - this.timeout(2 * 60 * 1000); + this.timeout(3 * 60 * 1000); let folderContext: FolderContext; let workspaceContext: WorkspaceContext; @@ -56,6 +56,7 @@ suite("Build Commands @slow", function () { async teardown() { await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); }, + requiresDebugger: true, }); test("Swift: Run Build", async () => { diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts index d393ac5e8..579bb0afb 100644 --- a/test/integration-tests/commands/dependency.test.ts +++ b/test/integration-tests/commands/dependency.test.ts @@ -14,10 +14,7 @@ import { expect } from "chai"; import * as vscode from "vscode"; -import { - PackageDependenciesProvider, - PackageNode, -} from "../../../src/ui/PackageDependencyProvider"; +import { PackageNode, ProjectPanelProvider } from "../../../src/ui/ProjectPanelProvider"; import { testAssetUri } from "../../fixtures"; import { FolderContext } from "../../../src/FolderContext"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; @@ -28,8 +25,8 @@ import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider suite("Dependency Commmands Test Suite", function () { // full workflow's interaction with spm is longer than the default timeout - // 60 seconds for each test should be more than enough - this.timeout(60 * 1000); + // 3 minutes for each test should be more than enough + this.timeout(3 * 60 * 1000); let defaultContext: FolderContext; let depsContext: FolderContext; @@ -57,12 +54,12 @@ suite("Dependency Commmands Test Suite", function () { }); suite("Swift: Use Local Dependency", function () { - let treeProvider: PackageDependenciesProvider; + let treeProvider: ProjectPanelProvider; setup(async () => { await workspaceContext.focusFolder(depsContext); await executeTaskAndWaitForResult((await getBuildAllTask(depsContext)) as SwiftTask); - treeProvider = new PackageDependenciesProvider(workspaceContext); + treeProvider = new ProjectPanelProvider(workspaceContext); }); teardown(() => { @@ -70,8 +67,25 @@ suite("Dependency Commmands Test Suite", function () { }); async function getDependency() { - const items = await treeProvider.getChildren(); - return items.find(n => n.name === "swift-markdown") as PackageNode; + const headers = await treeProvider.getChildren(); + const header = headers.find(n => n.name === "Dependencies") as PackageNode; + expect(header).to.not.be.undefined; + const children = await header.getChildren(); + return children.find(n => n.name === "swift-markdown") as PackageNode; + } + + // Wait for the dependency to switch to the expected state. + // This doesn't happen immediately after the USE_LOCAL_DEPENDENCY + // and RESET_PACKAGE commands because the file watcher on + // workspace-state.json needs to trigger. + async function getDependencyInState(state: "remote" | "editing") { + for (let i = 0; i < 10; i++) { + const dep = await getDependency(); + if (dep.type === state) { + return dep; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } } async function useLocalDependencyTest() { @@ -85,15 +99,14 @@ suite("Dependency Commmands Test Suite", function () { ); expect(result).to.be.true; + const dep = await getDependencyInState("editing"); + expect(dep).to.not.be.undefined; // Make sure using local - expect((await getDependency()).type).to.equal("editing"); - } - - async function assertUsingRemote() { - expect((await getDependency()).type).to.equal("remote"); + expect(dep?.type).to.equal("editing"); } test("Swift: Reset Package Dependencies", async function () { + this.skip(); // https://github.com/swiftlang/vscode-swift/issues/1316 // spm reset after using local dependency is broken on windows if (process.platform === "win32") { this.skip(); @@ -104,10 +117,13 @@ suite("Dependency Commmands Test Suite", function () { const result = await vscode.commands.executeCommand(Commands.RESET_PACKAGE); expect(result).to.be.true; - await assertUsingRemote(); + const dep = await getDependencyInState("remote"); + expect(dep).to.not.be.undefined; + expect(dep?.type).to.equal("remote"); }); - test("Swift: Revert To Original Version", async () => { + test("Swift: Revert To Original Version", async function () { + this.skip(); // https://github.com/swiftlang/vscode-swift/issues/1316 await useLocalDependencyTest(); const result = await vscode.commands.executeCommand( @@ -116,7 +132,9 @@ suite("Dependency Commmands Test Suite", function () { ); expect(result).to.be.true; - await assertUsingRemote(); + const dep = await getDependencyInState("remote"); + expect(dep).to.not.be.undefined; + expect(dep?.type).to.equal("remote"); }); }); }); diff --git a/test/integration-tests/configuration.test.ts b/test/integration-tests/configuration.test.ts new file mode 100644 index 000000000..6fa093d67 --- /dev/null +++ b/test/integration-tests/configuration.test.ts @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as path from "path"; +import { activateExtensionForSuite, updateSettings } from "./utilities/testutilities"; +import { expect } from "chai"; +import { afterEach } from "mocha"; +import configuration from "../../src/configuration"; +import { createBuildAllTask } from "../../src/tasks/SwiftTaskProvider"; +import { WorkspaceContext } from "../../src/WorkspaceContext"; + +suite("Configuration Test Suite", function () { + let workspaceContext: WorkspaceContext; + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + }, + }); + + let resetSettings: (() => Promise) | undefined; + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + } + }); + + test("Should substitute variables in build task", async function () { + resetSettings = await updateSettings({ + "swift.buildPath": "${workspaceFolder}/somepath", + }); + + const task = createBuildAllTask(workspaceContext.folders[0], false); + expect(task).to.not.be.undefined; + expect(task.definition.args).to.not.be.undefined; + const index = task.definition.args.indexOf("--scratch-path"); + expect(task.definition.args[index + 1]).to.equal( + vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath + "/somepath" + ); + }); + + test("Should substitute variables in configuration", async function () { + resetSettings = await updateSettings({ + "swift.buildPath": "${workspaceFolder}${pathSeparator}${workspaceFolderBasename}", + }); + + const basePath = vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath; + const baseName = path.basename(basePath ?? ""); + const sep = path.sep; + expect(configuration.buildPath).to.equal(`${basePath}${sep}${baseName}`); + }); +}); diff --git a/test/integration-tests/debugger/lldb.test.ts b/test/integration-tests/debugger/lldb.test.ts index aa070721c..0f747bf91 100644 --- a/test/integration-tests/debugger/lldb.test.ts +++ b/test/integration-tests/debugger/lldb.test.ts @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; -import { getLLDBLibPath, getLldbProcess } from "../../../src/debugger/lldb"; +import { getLLDBLibPath } from "../../../src/debugger/lldb"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { activateExtensionForTest } from "../utilities/testutilities"; import { Version } from "../../../src/utilities/version"; @@ -34,19 +34,7 @@ suite("lldb contract test suite", () => { } workspaceContext = ctx; }, - }); - - test("getLldbProcess Contract Test, make sure the command returns", async () => { - const result = await getLldbProcess(workspaceContext); - - // Assumption: machine will always return some process - expect(result).to.be.an("array"); - - // If result is an array, assert that each element has a pid and label - result?.forEach(item => { - expect(item).to.have.property("pid").that.is.a("number"); - expect(item).to.have.property("label").that.is.a("string"); - }); + requiresDebugger: true, }); test("getLLDBLibPath Contract Test, make sure we can find lib LLDB", async () => { diff --git a/test/integration-tests/extension.test.ts b/test/integration-tests/extension.test.ts index b9dad4080..346b34510 100644 --- a/test/integration-tests/extension.test.ts +++ b/test/integration-tests/extension.test.ts @@ -17,8 +17,10 @@ import { WorkspaceContext } from "../../src/WorkspaceContext"; import { getBuildAllTask } from "../../src/tasks/SwiftTaskProvider"; import { SwiftExecution } from "../../src/tasks/SwiftExecution"; import { activateExtensionForTest } from "./utilities/testutilities"; +import { expect } from "chai"; -suite("Extension Test Suite", () => { +suite("Extension Test Suite", function () { + this.timeout(60000); let workspaceContext: WorkspaceContext; activateExtensionForTest({ @@ -41,18 +43,19 @@ suite("Extension Test Suite", () => { }).timeout(5000);*/ }); - suite("Workspace", () => { + suite("Workspace", function () { + this.timeout(60000); /** Verify tasks.json is being loaded */ test("Tasks.json", async () => { const folder = workspaceContext.folders.find(f => f.name === "test/defaultPackage"); assert(folder); const buildAllTask = await getBuildAllTask(folder); const execution = buildAllTask.execution as SwiftExecution; - assert.strictEqual(buildAllTask.definition.type, "swift"); - assert.strictEqual(buildAllTask.name, "swift: Build All (defaultPackage)"); + expect(buildAllTask.definition.type).to.equal("swift"); + expect(buildAllTask.name).to.include("Build All (defaultPackage)"); for (const arg of ["build", "--build-tests", "--verbose"]) { assert(execution?.args.find(item => item === arg)); } - }); + }).timeout(60000); }); -}).timeout(15000); +}); diff --git a/test/integration-tests/language/LanguageClientIntegration.test.ts b/test/integration-tests/language/LanguageClientIntegration.test.ts index 3caab8c8d..883cc49a1 100644 --- a/test/integration-tests/language/LanguageClientIntegration.test.ts +++ b/test/integration-tests/language/LanguageClientIntegration.test.ts @@ -33,7 +33,7 @@ async function buildProject(ctx: WorkspaceContext, name: string) { } suite("Language Client Integration Suite @slow", function () { - this.timeout(2 * 60 * 1000); + this.timeout(3 * 60 * 1000); let clientManager: LanguageClientManager; let workspaceContext: WorkspaceContext; diff --git a/test/integration-tests/process-list/processList.test.ts b/test/integration-tests/process-list/processList.test.ts new file mode 100644 index 000000000..3fae5648e --- /dev/null +++ b/test/integration-tests/process-list/processList.test.ts @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as path from "path"; +import { expect } from "chai"; +import { createProcessList, Process } from "../../../src/process-list"; + +suite("ProcessList Tests", () => { + function expectProcessName(processes: Process[], command: string) { + expect( + processes.findIndex(proc => path.basename(proc.command) === command), + `Expected the list of processes to include '${command}':\n ${processes.map(proc => `${proc.id} - ${path.basename(proc.command)}`).join("\n")}\n\n` + ).to.be.greaterThanOrEqual(0); + } + + test("retreives the list of available processes", async function () { + // We can guarantee that certain VS Code processes will be present during tests + const processes = await createProcessList().listAllProcesses(); + let processNameDarwin: string = "Code"; + let processNameWin32: string = "Code"; + let processNameLinux: string = "code"; + if (process.env["VSCODE_VERSION"] === "insiders") { + processNameDarwin = "Code - Insiders"; + processNameWin32 = "Code - Insiders"; + processNameLinux = "code-insiders"; + } + switch (process.platform) { + case "darwin": + expectProcessName(processes, `${processNameDarwin} Helper`); + expectProcessName(processes, `${processNameDarwin} Helper (GPU)`); + expectProcessName(processes, `${processNameDarwin} Helper (Plugin)`); + expectProcessName(processes, `${processNameDarwin} Helper (Renderer)`); + break; + case "win32": + expectProcessName(processes, `${processNameWin32}.exe`); + break; + case "linux": + expectProcessName(processes, `${processNameLinux}`); + break; + default: + this.skip(); + } + }); +}); diff --git a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts index 47e1bc5e9..9b0159ee6 100644 --- a/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/integration-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import * as assert from "assert"; +import { beforeEach, afterEach } from "mocha"; import { expect } from "chai"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { SwiftPluginTaskProvider } from "../../../src/tasks/SwiftPluginTaskProvider"; @@ -30,59 +31,170 @@ import { } from "../../utilities/tasks"; import { mutable } from "../../utilities/types"; import { SwiftExecution } from "../../../src/tasks/SwiftExecution"; +import { SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; +import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; -suite("SwiftPluginTaskProvider Test Suite", () => { +suite("SwiftPluginTaskProvider Test Suite", function () { let workspaceContext: WorkspaceContext; let folderContext: FolderContext; - suite("settings plugin arguments", () => { - activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); - await folderContext.loadSwiftPlugins(); - expect(workspaceContext.folders).to.not.have.lengthOf(0); - return await updateSettings({ - "swift.pluginPermissions": { - "command-plugin:command_plugin": { - disableSandbox: true, - allowWritingToPackageDirectory: true, - allowWritingToDirectory: ["/foo", "/bar"], - allowNetworkConnections: "all", - }, + this.timeout(120000); // Mostly only when running suite with .only + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + const outputChannel = new SwiftOutputChannel("SwiftPluginTaskProvider.tests"); + folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); + await folderContext.loadSwiftPlugins(outputChannel); + expect(outputChannel.logs.length).to.equal(0, `Expected no output channel logs`); + expect(workspaceContext.folders).to.not.have.lengthOf(0); + }, + }); + + const expectedPluginPermissions = [ + "--disable-sandbox", + "--allow-writing-to-package-directory", + "--allow-writing-to-directory", + "/foo", + "/bar", + "--allow-network-connections", + "all", + ]; + + [ + { + name: "global plugin permissions", + settings: { + "swift.pluginPermissions": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "plugin scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", }, - }); + }, }, - }); + expected: expectedPluginPermissions, + }, + { + name: "command scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "command-plugin:command_plugin": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "wildcard scoped plugin permissions", + settings: { + "swift.pluginPermissions": { + "*": { + disableSandbox: true, + allowWritingToPackageDirectory: true, + allowWritingToDirectory: ["/foo", "/bar"], + allowNetworkConnections: "all", + }, + }, + }, + expected: expectedPluginPermissions, + }, + { + name: "global plugin arguments", + settings: { + "swift.pluginArguments": ["-c", "release"], + }, + expected: ["-c", "release"], + }, + { + name: "plugin scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "command scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "command-plugin:command_plugin": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "wildcard scoped plugin arguments", + settings: { + "swift.pluginArguments": { + "*": ["-c", "release"], + }, + }, + expected: ["-c", "release"], + }, + { + name: "overlays settings", + settings: { + "swift.pluginArguments": { + "*": ["-a"], + "command-plugin": ["-b"], + "command-plugin:command_plugin": ["-c"], + }, + }, + expected: ["-a", "-b", "-c"], + }, + ].forEach(({ name, settings, expected }) => { + suite(name, () => { + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings(settings); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); - test("provides a task with permissions set via settings", async () => { - const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); - const task = tasks.find(t => t.name === "command-plugin"); - const swiftExecution = task?.execution as SwiftExecution; - assert.deepEqual(swiftExecution.args, [ - "package", - "--disable-sandbox", - "--allow-writing-to-package-directory", - "--allow-writing-to-directory", - "/foo", - "/bar", - "--allow-network-connections", - "all", - "command_plugin", - ]); + test("sets arguments", async () => { + const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); + const task = tasks.find(t => t.name === "command-plugin"); + expect(task).to.not.be.undefined; + + const swiftExecution = task?.execution as SwiftExecution; + expect(swiftExecution).to.not.be.undefined; + assert.deepEqual( + swiftExecution.args, + workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + ...expected, + "command_plugin", + ]) + ); + }); }); }); suite("execution", () => { - activateExtensionForSuite({ - async setup(ctx) { - workspaceContext = ctx; - folderContext = await folderInRootWorkspace("command-plugin", workspaceContext); - await folderContext.loadSwiftPlugins(); - expect(workspaceContext.folders).to.not.have.lengthOf(0); - }, - }); - suite("createSwiftPluginTask", () => { let taskProvider: SwiftPluginTaskProvider; @@ -101,7 +213,7 @@ suite("SwiftPluginTaskProvider Test Suite", () => { const { exitCode, output } = await executeTaskAndWaitForResult(task); expect(exitCode).to.equal(0); expect(cleanOutput(output)).to.include("Hello, World!"); - }).timeout(60000); + }); test("Exit code on failure", async () => { const task = taskProvider.createSwiftPluginTask( @@ -118,20 +230,25 @@ suite("SwiftPluginTaskProvider Test Suite", () => { mutable(task.execution).command = "/definitely/not/swift"; const { exitCode, output } = await executeTaskAndWaitForResult(task); expect(exitCode, `${output}`).to.not.equal(0); - }).timeout(10000); + }); }); suite("provideTasks", () => { suite("includes command plugin provided by the extension", async () => { - let task: vscode.Task | undefined; + let task: SwiftTask | undefined; setup(async () => { const tasks = await vscode.tasks.fetchTasks({ type: "swift-plugin" }); - task = tasks.find(t => t.name === "command-plugin"); + task = tasks.find(t => t.name === "command-plugin") as SwiftTask; }); test("provides", () => { - expect(task?.detail).to.equal("swift package command_plugin"); + expect(task?.execution.args).to.deep.equal( + workspaceContext.toolchain.buildFlags.withAdditionalFlags([ + "package", + "command_plugin", + ]) + ); }); test("executes", async () => { @@ -140,7 +257,7 @@ suite("SwiftPluginTaskProvider Test Suite", () => { await vscode.tasks.executeTask(task); const exitCode = await exitPromise; expect(exitCode).to.equal(0); - }).timeout(30000); // 30 seconds to run + }); }); suite("includes command plugin provided by tasks.json", async () => { @@ -152,7 +269,7 @@ suite("SwiftPluginTaskProvider Test Suite", () => { }); test("provides", () => { - expect(task?.detail).to.equal("swift package command_plugin --foo"); + expect(task?.detail).to.include("swift package command_plugin --foo"); }); test("executes", async () => { @@ -161,7 +278,7 @@ suite("SwiftPluginTaskProvider Test Suite", () => { await vscode.tasks.executeTask(task); const exitCode = await exitPromise; expect(exitCode).to.equal(0); - }).timeout(30000); // 30 seconds to run + }); }); }); }); diff --git a/test/integration-tests/tasks/TaskQueue.test.ts b/test/integration-tests/tasks/TaskQueue.test.ts index 113d3ce7a..a72330f16 100644 --- a/test/integration-tests/tasks/TaskQueue.test.ts +++ b/test/integration-tests/tasks/TaskQueue.test.ts @@ -156,7 +156,7 @@ suite("TaskQueue Test Suite", () => { taskQueue.queueOperation(new TaskOperation(task2)).then(rt => results.push(rt)), ]); assert.notStrictEqual(results, [1, 2]); - }).timeout(8000); + }).timeout(15000); // check queuing task will return expected value test("swift exec", async () => { diff --git a/test/integration-tests/testexplorer/MockTestRunState.ts b/test/integration-tests/testexplorer/MockTestRunState.ts index d9f7478ed..91335235e 100644 --- a/test/integration-tests/testexplorer/MockTestRunState.ts +++ b/test/integration-tests/testexplorer/MockTestRunState.ts @@ -24,8 +24,8 @@ export enum TestStatus { skipped = "skipped", } -/** TestItem */ -interface TestItem { +/** TestRunTestItem */ +export interface TestRunTestItem { name: string; status: TestStatus; issues?: { @@ -40,20 +40,30 @@ interface TestItem { interface ITestItemFinder { getIndex(id: string): number; - tests: TestItem[]; + tests: TestRunTestItem[]; } export class DarwinTestItemFinder implements ITestItemFinder { - constructor(public tests: TestItem[]) {} + tests: TestRunTestItem[] = []; getIndex(id: string): number { - return this.tests.findIndex(item => item.name === id); + const index = this.tests.findIndex(item => item.name === id); + if (index === -1) { + this.tests.push({ name: id, status: TestStatus.enqueued, output: [] }); + return this.tests.length - 1; + } + return index; } } export class NonDarwinTestItemFinder implements ITestItemFinder { - constructor(public tests: TestItem[]) {} + tests: TestRunTestItem[] = []; getIndex(id: string): number { - return this.tests.findIndex(item => item.name.endsWith(id)); + const index = this.tests.findIndex(item => item.name.endsWith(id)); + if (index === -1) { + this.tests.push({ name: id, status: TestStatus.enqueued, output: [] }); + return this.tests.length - 1; + } + return index; } } @@ -71,18 +81,15 @@ export class TestRunState implements ITestRunState { public testItemFinder: ITestItemFinder; - get tests(): TestItem[] { + get tests(): TestRunTestItem[] { return this.testItemFinder.tests; } - constructor(testNames: string[], darwin: boolean) { - const tests = testNames.map(name => { - return { name: name, status: TestStatus.enqueued, output: [] }; - }); + constructor(darwin: boolean) { if (darwin) { - this.testItemFinder = new DarwinTestItemFinder(tests); + this.testItemFinder = new DarwinTestItemFinder(); } else { - this.testItemFinder = new NonDarwinTestItemFinder(tests); + this.testItemFinder = new NonDarwinTestItemFinder(); } } diff --git a/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts b/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts index 7b84475f4..df56401e7 100644 --- a/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts +++ b/test/integration-tests/testexplorer/SwiftTestingOutputParser.test.ts @@ -41,6 +41,7 @@ class TestEventStream { suite("SwiftTestingOutputParser Suite", () => { let outputParser: SwiftTestingOutputParser; + let testRunState: TestRunState; beforeEach(() => { outputParser = new SwiftTestingOutputParser( @@ -48,6 +49,7 @@ suite("SwiftTestingOutputParser Suite", () => { () => {}, () => {} ); + testRunState = new TestRunState(true); }); type ExtractPayload = T extends { payload: infer E } ? E : never; @@ -76,7 +78,6 @@ suite("SwiftTestingOutputParser Suite", () => { } test("Passed test", async () => { - const testRunState = new TestRunState(["MyTests.MyTests/testPass()"], true); const events = new TestEventStream([ testEvent("runStarted"), testEvent("testCaseStarted", "MyTests.MyTests/testPass()"), @@ -97,7 +98,6 @@ suite("SwiftTestingOutputParser Suite", () => { }); test("Skipped test", async () => { - const testRunState = new TestRunState(["MyTests.MyTests/testSkip()"], true); const events = new TestEventStream([ testEvent("runStarted"), testEvent("testSkipped", "MyTests.MyTests/testSkip()"), @@ -116,7 +116,6 @@ suite("SwiftTestingOutputParser Suite", () => { }); async function performTestFailure(messages: EventMessage[]) { - const testRunState = new TestRunState(["MyTests.MyTests/testFail()"], true); const issueLocation = { _filePath: "file:///some/file.swift", line: 1, @@ -174,7 +173,6 @@ suite("SwiftTestingOutputParser Suite", () => { }); test("Parameterized test", async () => { - const testRunState = new TestRunState(["MyTests.MyTests/testParameterized()"], true); const events = new TestEventStream([ { kind: "test", @@ -270,10 +268,6 @@ suite("SwiftTestingOutputParser Suite", () => { }); test("Output is captured", async () => { - const testRunState = new TestRunState( - ["MyTests.MyTests/testOutput()", "MyTests.MyTests/testOutput2()"], - true - ); const symbol = TestSymbol.pass; const makeEvent = (kind: ExtractPayload["kind"], testId?: string) => testEvent(kind, testId, [{ text: kind, symbol }]); diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts index 374044db0..b4aeb49c6 100644 --- a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -20,6 +20,7 @@ import { beforeEach, afterEach } from "mocha"; import { TestExplorer } from "../../../src/TestExplorer/TestExplorer"; import { assertContains, + assertContainsTrimmed, assertTestControllerHierarchy, assertTestResults, eventPromise, @@ -46,7 +47,8 @@ import { updateSettings, } from "../utilities/testutilities"; import { Commands } from "../../../src/commands"; -import { SwiftToolchain } from "../../../src/toolchain/toolchain"; +import { executeTaskAndWaitForResult } from "../../utilities/tasks"; +import { createBuildAllTask } from "../../../src/tasks/SwiftTaskProvider"; suite("Test Explorer Suite", function () { const MAX_TEST_RUN_TIME_MINUTES = 5; @@ -67,10 +69,14 @@ suite("Test Explorer Suite", function () { testExplorer = targetFolder.addTestExplorer(); + await executeTaskAndWaitForResult(createBuildAllTask(targetFolder)); + // Set up the listener before bringing the text explorer in to focus, // which starts searching the workspace for tests. await waitForTestExplorerReady(testExplorer); }, + requiresLSP: true, + requiresDebugger: true, }); suite("Debugging", function () { @@ -117,6 +123,7 @@ suite("Test Explorer Suite", function () { afterEach(async () => { if (resetSettings) { await resetSettings(); + resetSettings = undefined; } }); @@ -127,17 +134,6 @@ suite("Test Explorer Suite", function () { }); suite("CodeLLDB", () => { - async function getLLDBDebugAdapterPath() { - switch (process.platform) { - case "linux": - return "/usr/lib/liblldb.so"; - case "win32": - return await (await SwiftToolchain.create()).getLLDBDebugAdapter(); - default: - throw new Error("Please provide the path to lldb for this platform"); - } - } - let resetSettings: (() => Promise) | undefined; beforeEach(async function () { // CodeLLDB on windows doesn't print output and so cannot be parsed @@ -145,20 +141,15 @@ suite("Test Explorer Suite", function () { this.skip(); } - const lldbPath = - process.env["CI"] === "1" - ? { "lldb.library": await getLLDBDebugAdapterPath() } - : {}; - resetSettings = await updateSettings({ - "swift.debugger.useDebugAdapterFromToolchain": false, - ...lldbPath, + "swift.debugger.debugAdapter": "CodeLLDB", }); }); afterEach(async () => { if (resetSettings) { await resetSettings(); + resetSettings = undefined; } }); @@ -206,12 +197,12 @@ suite("Test Explorer Suite", function () { ["testPassing()", "testFailing()", "testDisabled()"], "testWithKnownIssue()", "testWithKnownIssueAndUnknownIssue()", - "testAttachment()", + "testLotsOfOutput()", "DuplicateSuffixTests", ["testPassing()", "testPassingSuffix()"], ], ]); - } else if (workspaceContext.swiftVersion.isLessThanOrEqual(new Version(5, 10, 0))) { + } else if (workspaceContext.swiftVersion.isLessThan(new Version(6, 0, 0))) { // 5.10 uses `swift test list` which returns test alphabetically, without the round brackets. // Does not include swift-testing tests. assertTestControllerHierarchy(testExplorer.controller, [ @@ -246,7 +237,31 @@ suite("Test Explorer Suite", function () { } }); - test("attachments", async function () { + test("captures lots of output", async () => { + const testRun = await runTest( + testExplorer, + TestKind.standard, + "PackageTests.testLotsOfOutput()" + ); + + assertTestResults(testRun, { + passed: ["PackageTests.testLotsOfOutput()"], + }); + + // Right now the swift-testing "test run complete" text is being emitted + // in the middle of the print, so the last line is actually end end of our + // huge string. If they fix this in future this `find` ensures the test wont break. + const needle = "100000"; + const lastTenLines = testRun.runState.output.slice(-10).join("\n"); + assertContainsTrimmed( + testRun.runState.output, + needle, + `Expected all test output to be captured, but it was truncated. Last 10 lines of output were: ${lastTenLines}` + ); + }); + + // Disabled until Attachments are formalized and released. + test.skip("attachments", async function () { // Attachments were introduced in 6.1 if (workspaceContext.swiftVersion.isLessThan(new Version(6, 1, 0))) { this.skip(); @@ -294,6 +309,15 @@ suite("Test Explorer Suite", function () { assertTestResults(testRun, { skipped: ["PackageTests.testWithKnownIssue()"], }); + + const testItem = testRun.testItems.find( + ({ id }) => id === "PackageTests.testWithKnownIssue()" + ); + assert.ok(testItem, "Unable to find test item for testWithKnownIssue"); + assert.ok( + testItem.tags.find(tag => tag.id === "skipped"), + "skipped tag was not found on test item" + ); }); test("testWithKnownIssueAndUnknownIssue", async () => { @@ -551,16 +575,18 @@ suite("Test Explorer Suite", function () { "PackageTests.topLevelTestPassing()" ); - assertContains( + // Use assertContainsTrimmed to ignore the line ending differences + // across platforms (windows vs linux/darwin) + assertContainsTrimmed( testRun.runState.output, - "A print statement in a test.\r\r\n" + "A print statement in a test." ); assertTestResults(testRun, { passed: ["PackageTests.topLevelTestPassing()"], }); }); - test(`Runs failing test (${runProfile})`, async function () { + test(`swift-testing Runs failing test (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -582,7 +608,7 @@ suite("Test Explorer Suite", function () { }); }); - test(`Runs Suite (${runProfile})`, async function () { + test(`swift-testing Runs Suite (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -607,15 +633,30 @@ suite("Test Explorer Suite", function () { }); }); - test(`Runs parameterized test (${runProfile})`, async function () { + test(`swift-testing Runs parameterized test (${runProfile})`, async function () { const testId = "PackageTests.parameterizedTest(_:)"; const testRun = await runTest(testExplorer, runProfile, testId); + let passed: string[]; + let failedId: string; + if ( + workspaceContext.swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0)) + ) { + passed = [ + `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [49])], discriminator: 0, isStable: true`, + `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [51])], discriminator: 0, isStable: true`, + ]; + failedId = `${testId}/PackageTests.swift:59:2/Parameterized test case ID: argumentIDs: [Testing.Test.Case.Argument.ID(bytes: [50])], discriminator: 0, isStable: true`; + } else { + passed = [ + `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])`, + `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [51])])`, + ]; + failedId = `${testId}/PackageTests.swift:59:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])`; + } + assertTestResults(testRun, { - passed: [ - `${testId}/PackageTests.swift:63:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [49])])`, - `${testId}/PackageTests.swift:63:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [51])])`, - ], + passed, failed: [ { issues: [ @@ -624,7 +665,7 @@ suite("Test Explorer Suite", function () { text: "Expectation failed: (arg → 2) != 2", })}`, ], - test: `${testId}/PackageTests.swift:63:2/argumentIDs: Optional([Testing.Test.Case.Argument.ID(bytes: [50])])`, + test: failedId, }, { issues: [], @@ -657,7 +698,7 @@ suite("Test Explorer Suite", function () { assert.deepEqual(unrunnableChildren, [true, true, true]); }); - test(`Runs Suite (${runProfile})`, async function () { + test(`swift-testing Runs Suite (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -682,7 +723,7 @@ suite("Test Explorer Suite", function () { }); }); - test(`Runs All (${runProfile})`, async function () { + test(`swift-testing Runs All (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -721,7 +762,7 @@ suite("Test Explorer Suite", function () { }); suite(`XCTests (${runProfile})`, () => { - test("Runs passing test", async function () { + test(`XCTest Runs passing test (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -736,7 +777,7 @@ suite("Test Explorer Suite", function () { }); }); - test("Runs failing test", async function () { + test(`XCTest Runs failing test (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, @@ -757,7 +798,7 @@ suite("Test Explorer Suite", function () { }); }); - test("Runs Suite", async function () { + test(`XCTest Runs Suite (${runProfile})`, async function () { const testRun = await runTest( testExplorer, runProfile, diff --git a/test/integration-tests/testexplorer/TestRunArguments.test.ts b/test/integration-tests/testexplorer/TestRunArguments.test.ts index 6fc75aa8f..f435d2774 100644 --- a/test/integration-tests/testexplorer/TestRunArguments.test.ts +++ b/test/integration-tests/testexplorer/TestRunArguments.test.ts @@ -240,7 +240,7 @@ suite("TestRunArguments Suite", () => { const testArgs = new TestRunArguments(runRequestByIds([anotherSwiftTestId]), false); assertRunArguments(testArgs, { xcTestArgs: [], - swiftTestArgs: [`${anotherSwiftTestId}/`], + swiftTestArgs: [anotherSwiftTestId], testItems: [swiftTestSuiteId, testTargetId, anotherSwiftTestId], }); }); diff --git a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts index 6ad6bac66..2b2342f4c 100644 --- a/test/integration-tests/testexplorer/XCTestOutputParser.test.ts +++ b/test/integration-tests/testexplorer/XCTestOutputParser.test.ts @@ -13,559 +13,607 @@ //===----------------------------------------------------------------------===// import * as assert from "assert"; +import { beforeEach } from "mocha"; import { darwinTestRegex, nonDarwinTestRegex, XCTestOutputParser, } from "../../../src/TestExplorer/TestParsers/XCTestOutputParser"; -import { TestRunState, TestStatus } from "./MockTestRunState"; +import { TestRunState, TestRunTestItem, TestStatus } from "./MockTestRunState"; import { sourceLocationToVSCodeLocation } from "../../../src/utilities/utilities"; +import { TestXUnitParser } from "../../../src/TestExplorer/TestXUnitParser"; +import { activateExtensionForSuite } from "../utilities/testutilities"; +import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; + +enum ParserTestKind { + Regular = "Regular Test Run", + Parallel = "Parallel Test Run", +} suite("XCTestOutputParser Suite", () => { - const inputToTestOutput = (input: string) => - input + function inputToTestOutput(input: string) { + return input .split("\n") .slice(0, -1) .map(line => `${line}\r\n`); + } + + function expectedStateToXML(tests: TestRunTestItem[]) { + const extractClassName = (name: string) => { + const parts = name.split("/"); + return parts[0]; + }; + const extractName = (name: string) => { + const parts = name.split("/"); + return parts[parts.length - 1]; + }; + const extractTiming = (test: TestRunTestItem) => { + if (test.timing) { + const time = + "duration" in test.timing ? test.timing.duration : test.timing.timestamp; + return `time="${time}"`; + } + return ""; + }; + const extractFailures = (test: TestRunTestItem[]) => { + return test.reduce((acc, t) => acc + ((t.issues?.length ?? 0) > 0 ? 1 : 0), 0); + }; + return ` + + +${tests.map( + t => + `${(t.issues ?? []).map(() => `\n`).join("\n")}` +)} + + +`; + } + + let hasMultiLineParallelTestOutput: boolean; + activateExtensionForSuite({ + async setup(ctx) { + hasMultiLineParallelTestOutput = ctx.toolchain.hasMultiLineParallelTestOutput; + }, + }); - suite("Darwin", () => { - const outputParser = new XCTestOutputParser(darwinTestRegex); + [ParserTestKind.Regular, ParserTestKind.Parallel].forEach(parserTestKind => { + function assertTestRunState(testRunState: TestRunState, expected: TestRunTestItem[]) { + // When parsing the results of a parallel test run the TestXUnitParser runs on the + // generated XML results after the test run is completed to fill out the state with + // anything that couldn't be captured off the output stream. + if (parserTestKind === ParserTestKind.Parallel) { + const xmlResults = expectedStateToXML(expected); + const xmlParser = new TestXUnitParser(hasMultiLineParallelTestOutput); + xmlParser.parse(xmlResults, testRunState, new SwiftOutputChannel("test")); + } + + assert.deepEqual(testRunState.tests, expected); + } + + suite(`${parserTestKind}`, () => { + suite("Darwin", () => { + let outputParser: XCTestOutputParser; + let testRunState: TestRunState; + beforeEach(() => { + outputParser = new XCTestOutputParser(darwinTestRegex); + testRunState = new TestRunState(true); + }); - test("Passed Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testPass"], true); - const input = `Test Case '-[MyTests.MyTests testPass]' started. + test("Passed Test", () => { + const input = `Test Case '-[MyTests.MyTests testPass]' started. Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). `; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(testRunState.tests, [ - { - name: "MyTests.MyTests/testPass", - status: TestStatus.passed, - timing: { duration: 0.001 }, - output: inputToTestOutput(input), - }, - ]); - }); + outputParser.parseResult(input, testRunState); - test("Multiple Passed Tests", () => { - const testRunState = new TestRunState( - ["MyTests.MyTests/testPass", "MyTests.MyTests/testPass2"], - true - ); - const test1Input = `Test Case '-[MyTests.MyTests testPass]' started. + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(input), + }, + ]); + }); + + test("Multiple Passed Tests", () => { + const test1Input = `Test Case '-[MyTests.MyTests testPass]' started. Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). `; - const test2Input = `Test Case '-[MyTests.MyTests testPass2]' started. + const test2Input = `Test Case '-[MyTests.MyTests testPass2]' started. Test Case '-[MyTests.MyTests testPass2]' passed (0.001 seconds). `; - const input = `${test1Input}${test2Input}`; - - outputParser.parseResult(input, testRunState); - - assert.deepEqual(testRunState.tests, [ - { - name: "MyTests.MyTests/testPass", - status: TestStatus.passed, - timing: { duration: 0.001 }, - output: inputToTestOutput(test1Input), - }, - { - name: "MyTests.MyTests/testPass2", - status: TestStatus.passed, - timing: { duration: 0.001 }, - output: inputToTestOutput(test2Input), - }, - ]); - }); + const input = `${test1Input}${test2Input}`; + + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(test1Input), + }, + { + name: "MyTests.MyTests/testPass2", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(test2Input), + }, + ]); + }); - test("Failed Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], true); - const input = `Test Case '-[MyTests.MyTests testFail]' started. + test("Failed Test", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : XCTAssertEqual failed: ("1") is not equal to ("2") Test Case '-[MyTests.MyTests testFail]' failed (0.106 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.106 }, - issues: [ - { - message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: { - expected: '"1"', - actual: '"2"', + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: { + actual: '"1"', + expected: '"2"', + }, + }, + ], + output: inputToTestOutput(input), }, - }, - ], - output: inputToTestOutput(input), - }); - }); + ]); + }); - test("Skipped Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testSkip"], true); - const input = `Test Case '-[MyTests.MyTests testSkip]' started. + test("Skipped Test", () => { + const input = `Test Case '-[MyTests.MyTests testSkip]' started. /Users/user/Developer/MyTests/MyTests.swift:90: -[MyTests.MyTests testSkip] : Test skipped Test Case '-[MyTests.MyTests testSkip]' skipped (0.002 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); + outputParser.parseResult(input, testRunState); - assert.deepEqual(runState, { - name: "MyTests.MyTests/testSkip", - status: TestStatus.skipped, - output: inputToTestOutput(input), - }); - }); + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testSkip", + status: TestStatus.skipped, + output: inputToTestOutput(input), + }, + ]); + }); - test("Multi-line Fail", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], true); - const input = `Test Case '-[MyTests.MyTests testFail]' started. + test("Multi-line Fail", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Multiline fail message Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.571 }, - issues: [ - { - message: `failed - Multiline + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Multiline fail message`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: undefined, - }, - ], - output: inputToTestOutput(input), - }); - }); + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); - test("Multi-line Fail followed by another error", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], true); - const input = `Test Case '-[MyTests.MyTests testFail]' started. + test("Multi-line Fail followed by another error", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Multiline fail message /Users/user/Developer/MyTests/MyTests.swift:61: error: -[MyTests.MyTests testFail] : failed - Again Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.571 }, - issues: [ - { - message: `failed - Multiline + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Multiline fail message`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: undefined, - }, - { - message: `failed - Again`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 61, - 0 - ), - isKnown: false, - diff: undefined, - }, - ], - output: inputToTestOutput(input), - }); - }); + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + { + message: `failed - Again`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 61, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); - test("Single-line Fail followed by another error", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], true); - const input = `Test Case '-[MyTests.MyTests testFail]' started. + test("Single-line Fail followed by another error", () => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : failed - Message /Users/user/Developer/MyTests/MyTests.swift:61: error: -[MyTests.MyTests testFail] : failed - Again Test Case '-[MyTests.MyTests testFail]' failed (0.571 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.571 }, - issues: [ - { - message: `failed - Message`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: undefined, - }, - { - message: `failed - Again`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 61, - 0 - ), - isKnown: false, - diff: undefined, - }, - ], - output: inputToTestOutput(input), - }); - }); + outputParser.parseResult(input, testRunState); - test("Split line", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testPass"], true); - const input1 = `Test Case '-[MyTests.MyTests testPass]' started. + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.571 }, + issues: [ + { + message: `failed - Message`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: undefined, + }, + { + message: `failed - Again`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 61, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }); + + test("Split line", () => { + const input1 = `Test Case '-[MyTests.MyTests testPass]' started. Test Case '-[MyTests.MyTests`; - const input2 = ` testPass]' passed (0.006 seconds). + const input2 = ` testPass]' passed (0.006 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input1, testRunState); - outputParser.parseResult(input2, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testPass", - status: TestStatus.passed, - timing: { duration: 0.006 }, - output: inputToTestOutput(input1 + input2), - }); - }); + outputParser.parseResult(input1, testRunState); + outputParser.parseResult(input2, testRunState); - test("Suite", () => { - const testRunState = new TestRunState( - ["MyTests.MyTests", "MyTests.MyTests/testPass"], - true - ); - const input = `Test Suite 'MyTests' started at 2024-08-26 13:19:25.325. + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.006 }, + output: inputToTestOutput(input1 + input2), + }, + ]); + }); + + test("Suite", () => { + const input = `Test Suite 'MyTests' started at 2024-08-26 13:19:25.325. Test Case '-[MyTests.MyTests testPass]' started. Test Case '-[MyTests.MyTests testPass]' passed (0.001 seconds). Test Suite 'MyTests' passed at 2024-08-26 13:19:25.328. - Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds + Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.001) seconds `; - outputParser.parseResult(input, testRunState); - - const testOutput = inputToTestOutput(input); - assert.deepEqual(testRunState.tests, [ - { - name: "MyTests.MyTests", - output: [testOutput[0], testOutput[3]], - status: TestStatus.passed, - }, - { - name: "MyTests.MyTests/testPass", - status: TestStatus.passed, - timing: { duration: 0.001 }, - output: [testOutput[1], testOutput[2]], - }, - ]); - assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); - }); + outputParser.parseResult(input, testRunState); - test("Empty Suite", () => { - const testRunState = new TestRunState([], true); - const input = `Test Suite 'Selected tests' started at 2024-10-19 15:23:29.594. + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests", + output: [testOutput[0], testOutput[3]], + status: TestStatus.passed, + }, + { + name: "MyTests.MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: [testOutput[1], testOutput[2]], + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); + + test("Empty Suite", () => { + const input = `Test Suite 'Selected tests' started at 2024-10-19 15:23:29.594. Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-19 15:23:29.595. Test Suite 'EmptyAppPackageTests.xctest' passed at 2024-10-19 15:23:29.595. - Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds + Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds Test Suite 'Selected tests' passed at 2024-10-19 15:23:29.596. - Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds + Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds warning: No matching test cases were run`; - outputParser.parseResult(input, testRunState); + outputParser.parseResult(input, testRunState); - assert.deepEqual(testRunState.tests, []); - assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); - }); + assertTestRunState(testRunState, []); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); - test("Multiple Suites", () => { - const testRunState = new TestRunState( - [ - "MyTests.TestSuite1", - "MyTests.TestSuite1/testFirst", - "MyTests.TestSuite2", - "MyTests.TestSuite2/testSecond", - ], - true - ); - const input = `Test Suite 'All tests' started at 2024-10-20 21:54:32.568. + test("Multiple Suites", () => { + const input = `Test Suite 'All tests' started at 2024-10-20 21:54:32.568. Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-20 21:54:32.570. Test Suite 'TestSuite1' started at 2024-10-20 21:54:32.570. Test Case '-[MyTests.TestSuite1 testFirst]' started. Test Case '-[MyTests.TestSuite1 testFirst]' passed (0.000 seconds). Test Suite 'TestSuite1' passed at 2024-10-20 21:54:32.570. - Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.001) seconds + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.001) seconds Test Suite 'TestSuite2' started at 2024-10-20 21:54:32.570. Test Case '-[MyTests.TestSuite2 testSecond]' started. Test Case '-[MyTests.TestSuite2 testSecond]' passed (0.000 seconds). Test Suite 'TestSuite2' passed at 2024-10-20 21:54:32.571. - Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds Test Suite 'EmptyAppPackageTests.xctest' passed at 2024-10-20 21:54:32.571. - Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds + Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds Test Suite 'All tests' passed at 2024-10-20 21:54:32.571. - Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds`; - - outputParser.parseResult(input, testRunState); - - const testOutput = inputToTestOutput(input); - assert.deepEqual(testRunState.tests, [ - { - name: "MyTests.TestSuite1", - output: [testOutput[2], testOutput[5]], - status: "passed", - }, - { - name: "MyTests.TestSuite1/testFirst", - output: [testOutput[3], testOutput[4]], - status: "passed", - timing: { - duration: 0, - }, - }, - { - name: "MyTests.TestSuite2", - output: [testOutput[7], testOutput[10]], - status: "passed", - }, - { - name: "MyTests.TestSuite2/testSecond", - output: [testOutput[8], testOutput[9]], - status: "passed", - timing: { - duration: 0, - }, - }, - ]); - assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); - }); + Executed 2 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds`; + + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ + { + name: "MyTests.TestSuite1", + output: [testOutput[2], testOutput[5]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite1/testFirst", + output: [testOutput[3], testOutput[4]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + { + name: "MyTests.TestSuite2", + output: [testOutput[7], testOutput[10]], + status: TestStatus.passed, + }, + { + name: "MyTests.TestSuite2/testSecond", + output: [testOutput[8], testOutput[9]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); - test("Multiple Suites with Failed Test", () => { - const testRunState = new TestRunState( - [ - "MyTests.TestSuite1", - "MyTests.TestSuite1/testFirst", - "MyTests.TestSuite2", - "MyTests.TestSuite2/testSecond", - ], - true - ); - const input = `Test Suite 'Selected tests' started at 2024-10-20 22:01:46.206. + test("Multiple Suites with Failed Test", () => { + const input = `Test Suite 'Selected tests' started at 2024-10-20 22:01:46.206. Test Suite 'EmptyAppPackageTests.xctest' started at 2024-10-20 22:01:46.207. Test Suite 'TestSuite1' started at 2024-10-20 22:01:46.207. Test Case '-[MyTests.TestSuite1 testFirst]' started. Test Case '-[MyTests.TestSuite1 testFirst]' passed (0.000 seconds). Test Suite 'TestSuite1' passed at 2024-10-20 22:01:46.208. - Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds + Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds Test Suite 'TestSuite2' started at 2024-10-20 22:01:46.208. Test Case '-[MyTests.TestSuite2 testSecond]' started. /Users/user/Developer/MyTests/MyTests.swift:13: error: -[MyTests.TestSuite2 testSecond] : failed Test Case '-[MyTests.TestSuite2 testSecond]' failed (0.000 seconds). Test Suite 'TestSuite2' failed at 2024-10-20 22:01:46.306. - Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds + Executed 1 test, with 1 failure (0 unexpected) in 0.000 (0.000) seconds Test Suite 'EmptyAppPackageTests.xctest' failed at 2024-10-20 22:01:46.306. - Executed 2 tests, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + Executed 2 tests, with 1 failure (0 unexpected) in 0.001 (0.001) seconds Test Suite 'Selected tests' failed at 2024-10-20 22:01:46.306. - Executed 2 tests, with 1 failure (0 unexpected) in 0.002 (0.002) seconds`; - outputParser.parseResult(input, testRunState); - - const testOutput = inputToTestOutput(input); - assert.deepEqual(testRunState.tests, [ - { - name: "MyTests.TestSuite1", - output: [testOutput[2], testOutput[5]], - status: "passed", - }, - { - name: "MyTests.TestSuite1/testFirst", - output: [testOutput[3], testOutput[4]], - status: "passed", - timing: { - duration: 0, - }, - }, - { - name: "MyTests.TestSuite2", - output: [testOutput[7], testOutput[11]], - status: "failed", - }, - { - name: "MyTests.TestSuite2/testSecond", - output: [testOutput[8], testOutput[9], testOutput[10]], - status: "failed", - timing: { - duration: 0, - }, - issues: [ + Executed 2 tests, with 1 failure (0 unexpected) in 0.002 (0.002) seconds`; + outputParser.parseResult(input, testRunState); + + const testOutput = inputToTestOutput(input); + assertTestRunState(testRunState, [ { - message: "failed", - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 13, - 0 - ), - isKnown: false, - diff: undefined, + name: "MyTests.TestSuite1", + output: [testOutput[2], testOutput[5]], + status: TestStatus.passed, }, - ], - }, - ]); - assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); - }); + { + name: "MyTests.TestSuite1/testFirst", + output: [testOutput[3], testOutput[4]], + status: TestStatus.passed, + timing: { + duration: 0, + }, + }, + { + name: "MyTests.TestSuite2", + output: [testOutput[7], testOutput[11]], + status: TestStatus.failed, + }, + { + name: "MyTests.TestSuite2/testSecond", + output: [testOutput[8], testOutput[9], testOutput[10]], + status: TestStatus.failed, + timing: { + duration: 0, + }, + issues: [ + { + message: "failed", + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 13, + 0 + ), + isKnown: false, + diff: undefined, + }, + ], + }, + ]); + assert.deepEqual(inputToTestOutput(input), testRunState.allOutput); + }); - suite("Diffs", () => { - const testRun = (message: string, expected?: string, actual?: string) => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], true); - const input = `Test Case '-[MyTests.MyTests testFail]' started. + suite("Diffs", () => { + const testRun = (message: string, actual?: string, expected?: string) => { + const input = `Test Case '-[MyTests.MyTests testFail]' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: -[MyTests.MyTests testFail] : ${message} Test Case '-[MyTests.MyTests testFail]' failed (0.106 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.106 }, - issues: [ - { - message, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: - expected && actual - ? { - expected, - actual, - } - : undefined, - }, - ], - output: inputToTestOutput(input), + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests.MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: + expected && actual + ? { + expected, + actual, + } + : undefined, + }, + ], + output: inputToTestOutput(input), + }, + ]); + }; + + test("XCTAssertEqual", () => { + testRun(`XCTAssertEqual failed: ("1") is not equal to ("2")`, '"1"', '"2"'); + }); + test("XCTAssertEqualMultiline", () => { + testRun( + `XCTAssertEqual failed: ("foo\nbar") is not equal to ("foo\nbaz")`, + '"foo\nbar"', + '"foo\nbaz"' + ); + }); + test("XCTAssertIdentical", () => { + testRun( + `XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 2")`, + '"V: 1"', + '"V: 2"' + ); + }); + test("XCTAssertIdentical with Identical Strings", () => { + testRun(`XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 1")`); + }); }); - }; - - test("XCTAssertEqual", () => { - testRun(`XCTAssertEqual failed: ("1") is not equal to ("2")`, '"1"', '"2"'); - }); - test("XCTAssertEqualMultiline", () => { - testRun( - `XCTAssertEqual failed: ("foo\nbar") is not equal to ("foo\nbaz")`, - '"foo\nbar"', - '"foo\nbaz"' - ); - }); - test("XCTAssertIdentical", () => { - testRun( - `XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 2")`, - '"V: 1"', - '"V: 2"' - ); }); - test("XCTAssertIdentical with Identical Strings", () => { - testRun(`XCTAssertIdentical failed: ("V: 1") is not identical to ("V: 1")`); - }); - }); - }); - suite("Linux", () => { - const outputParser = new XCTestOutputParser(nonDarwinTestRegex); + suite("Linux", () => { + let outputParser: XCTestOutputParser; + let testRunState: TestRunState; + beforeEach(() => { + outputParser = new XCTestOutputParser(nonDarwinTestRegex); + testRunState = new TestRunState(false); + }); - test("Passed Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testPass"], false); - const input = `Test Case 'MyTests.testPass' started. + test("Passed Test", () => { + const input = `Test Case 'MyTests.testPass' started. Test Case 'MyTests.testPass' passed (0.001 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testPass", - status: TestStatus.passed, - timing: { duration: 0.001 }, - output: inputToTestOutput(input), - }); - }); + outputParser.parseResult(input, testRunState); - test("Failed Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testFail"], false); - const input = `Test Case 'MyTests.testFail' started. + assertTestRunState(testRunState, [ + { + name: "MyTests/testPass", + status: TestStatus.passed, + timing: { duration: 0.001 }, + output: inputToTestOutput(input), + }, + ]); + }); + + test("Failed Test", () => { + const input = `Test Case 'MyTests.testFail' started. /Users/user/Developer/MyTests/MyTests.swift:59: error: MyTests.testFail : XCTAssertEqual failed: ("1") is not equal to ("2") Test Case 'MyTests.testFail' failed (0.106 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); - - assert.deepEqual(runState, { - name: "MyTests.MyTests/testFail", - status: TestStatus.failed, - timing: { duration: 0.106 }, - issues: [ - { - message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, - location: sourceLocationToVSCodeLocation( - "/Users/user/Developer/MyTests/MyTests.swift", - 59, - 0 - ), - isKnown: false, - diff: { - expected: '"1"', - actual: '"2"', + outputParser.parseResult(input, testRunState); + + assertTestRunState(testRunState, [ + { + name: "MyTests/testFail", + status: TestStatus.failed, + timing: { duration: 0.106 }, + issues: [ + { + message: `XCTAssertEqual failed: ("1") is not equal to ("2")`, + location: sourceLocationToVSCodeLocation( + "/Users/user/Developer/MyTests/MyTests.swift", + 59, + 0 + ), + isKnown: false, + diff: { + actual: '"1"', + expected: '"2"', + }, + }, + ], + output: inputToTestOutput(input), }, - }, - ], - output: inputToTestOutput(input), - }); - }); + ]); + }); - test("Skipped Test", () => { - const testRunState = new TestRunState(["MyTests.MyTests/testSkip"], false); - const input = `Test Case 'MyTests.testSkip' started. + test("Skipped Test", () => { + const input = `Test Case 'MyTests.testSkip' started. /Users/user/Developer/MyTests/MyTests.swift:90: MyTests.testSkip : Test skipped Test Case 'MyTests.testSkip' skipped (0.002 seconds). `; - const runState = testRunState.tests[0]; - outputParser.parseResult(input, testRunState); + outputParser.parseResult(input, testRunState); - assert.deepEqual(runState, { - name: "MyTests.MyTests/testSkip", - status: TestStatus.skipped, - output: inputToTestOutput(input), + assertTestRunState(testRunState, [ + { + name: "MyTests/testSkip", + status: TestStatus.skipped, + output: inputToTestOutput(input), + }, + ]); + }); }); }); }); diff --git a/test/integration-tests/testexplorer/utilities.ts b/test/integration-tests/testexplorer/utilities.ts index 20662258c..b049f74c7 100644 --- a/test/integration-tests/testexplorer/utilities.ts +++ b/test/integration-tests/testexplorer/utilities.ts @@ -110,9 +110,23 @@ export function assertTestControllerHierarchy( * * @param array The array to check. * @param value The value to check for. + * @param message An optional message to display if the assertion fails. */ -export function assertContains(array: T[], value: T) { - assert.ok(array.includes(value), `${value} is not in ${array}`); +export function assertContains(array: T[], value: T, message?: string) { + assert.ok(array.includes(value), message ?? `${value} is not in ${array}`); +} + +/** + * Asserts that an array of strings contains the value ignoring + * leading/trailing whitespace. + * + * @param array The array to check. + * @param value The value to check for. + * @param message An optional message to display if the assertion fails. + */ +export function assertContainsTrimmed(array: string[], value: string, message?: string) { + const found = array.find(row => row.trim() === value); + assert.ok(found, message ?? `${value} is not in ${array}`); } /** @@ -160,7 +174,11 @@ export function assertTestResults( skipped: (state.skipped ?? []).sort(), errored: (state.errored ?? []).sort(), unknown: 0, - } + }, + ` + Build Output: + ${testRun.runState.output.join("\n")} + ` ); } @@ -280,6 +298,9 @@ export async function runTest( const testItems = await gatherTests(testExplorer.controller, ...tests); const request = new vscode.TestRunRequest(testItems); + // The first promise is the return value, the second promise builds and runs + // the tests, populating the TestRunProxy with results and blocking the return + // of that TestRunProxy until the test run is complete. return ( await Promise.all([ eventPromise(testExplorer.onCreateTestRun), diff --git a/test/integration-tests/ui/PackageDependencyProvider.test.ts b/test/integration-tests/ui/PackageDependencyProvider.test.ts deleted file mode 100644 index 75f6ea6a7..000000000 --- a/test/integration-tests/ui/PackageDependencyProvider.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2024 the VS Code Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VS Code Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import { expect } from "chai"; -import * as vscode from "vscode"; -import * as path from "path"; -import { - PackageDependenciesProvider, - PackageNode, -} from "../../../src/ui/PackageDependencyProvider"; -import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; -import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; -import { testAssetPath } from "../../fixtures"; -import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; - -suite("PackageDependencyProvider Test Suite", function () { - let treeProvider: PackageDependenciesProvider; - this.timeout(2 * 60 * 1000); // Allow up to 2 minutes to build - - activateExtensionForSuite({ - async setup(ctx) { - const workspaceContext = ctx; - await waitForNoRunningTasks(); - await folderInRootWorkspace("defaultPackage", workspaceContext); - const folderContext = await folderInRootWorkspace("dependencies", workspaceContext); - await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); - treeProvider = new PackageDependenciesProvider(workspaceContext); - await workspaceContext.focusFolder(folderContext); - }, - async teardown() { - treeProvider.dispose(); - }, - testAssets: ["dependencies"], - }); - - test("Includes remote dependency", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; - expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); - assertPathsEqual( - dep?.path, - path.join(testAssetPath("dependencies"), ".build/checkouts/swift-markdown") - ); - }); - - test("Includes local dependency", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect( - dep, - `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` - ).to.not.be.undefined; - assertPathsEqual(dep?.location, testAssetPath("defaultPackage")); - assertPathsEqual(dep?.path, testAssetPath("defaultPackage")); - }); - - test("Lists local dependency file structure", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect( - dep, - `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` - ).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources")); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "PackageExe"); - expect(childFolder).to.not.be.undefined; - - assertPathsEqual( - childFolder?.path, - path.join(testAssetPath("defaultPackage"), "Sources/PackageExe") - ); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "main.swift"); - expect(file).to.not.be.undefined; - - assertPathsEqual( - file?.path, - path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift") - ); - }); - - test("Lists remote dependency file structure", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - const depPath = path.join(testAssetPath("dependencies"), ".build/checkouts/swift-markdown"); - assertPathsEqual(folder?.path, path.join(depPath, "Sources")); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "CAtomic"); - expect(childFolder).to.not.be.undefined; - - assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic")); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "CAtomic.c"); - expect(file).to.not.be.undefined; - - assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c")); - }); - - function assertPathsEqual(path1: string | undefined, path2: string | undefined) { - expect(path1).to.not.be.undefined; - expect(path2).to.not.be.undefined; - // Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows. - expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath); - } -}); diff --git a/test/integration-tests/ui/ProjectPanelProvider.test.ts b/test/integration-tests/ui/ProjectPanelProvider.test.ts new file mode 100644 index 000000000..10a786178 --- /dev/null +++ b/test/integration-tests/ui/ProjectPanelProvider.test.ts @@ -0,0 +1,393 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { expect } from "chai"; +import { beforeEach, afterEach } from "mocha"; +import * as vscode from "vscode"; +import * as path from "path"; +import { + ProjectPanelProvider, + PackageNode, + FileNode, + TreeNode, +} from "../../../src/ui/ProjectPanelProvider"; +import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities/tasks"; +import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; +import { testAssetPath } from "../../fixtures"; +import { + activateExtensionForSuite, + folderInRootWorkspace, + updateSettings, +} from "../utilities/testutilities"; +import contextKeys from "../../../src/contextKeys"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { Version } from "../../../src/utilities/version"; +import { wait } from "../../../src/utilities/utilities"; +import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; + +suite("ProjectPanelProvider Test Suite", function () { + let workspaceContext: WorkspaceContext; + let treeProvider: ProjectPanelProvider; + this.timeout(5 * 60 * 1000); // Allow up to 5 minutes to build + + activateExtensionForSuite({ + async setup(ctx) { + workspaceContext = ctx; + await waitForNoRunningTasks(); + const folderContext = await folderInRootWorkspace("targets", workspaceContext); + await vscode.workspace.openTextDocument( + path.join(folderContext.folder.fsPath, "Package.swift") + ); + const buildAllTask = await getBuildAllTask(folderContext); + buildAllTask.definition.dontTriggerTestDiscovery = true; + await executeTaskAndWaitForResult(buildAllTask as SwiftTask); + const outputChannel = new SwiftOutputChannel("ProjectPanelProvider.tests"); + await folderContext.loadSwiftPlugins(outputChannel); + expect(outputChannel.logs.length).to.equal(0, `Expected no output channel logs`); + treeProvider = new ProjectPanelProvider(workspaceContext); + await workspaceContext.focusFolder(folderContext); + }, + async teardown() { + contextKeys.flatDependenciesList = false; + treeProvider.dispose(); + }, + testAssets: ["targets"], + }); + + let resetSettings: (() => Promise) | undefined; + beforeEach(async function () { + resetSettings = await updateSettings({ + "swift.debugger.debugAdapter": "CodeLLDB", + }); + }); + + afterEach(async () => { + if (resetSettings) { + await resetSettings(); + resetSettings = undefined; + } + }); + + test("Includes top level nodes", async () => { + await waitForChildren( + () => treeProvider.getChildren(), + commands => { + const commandNames = commands.map(n => n.name); + expect(commandNames).to.deep.equal([ + "Dependencies", + "Targets", + "Tasks", + "Snippets", + "Commands", + ]); + } + ); + }); + + suite("Targets", () => { + test("Includes targets", async () => { + await waitForChildren( + () => getHeaderChildren("Targets"), + targets => { + const targetNames = targets.map(target => target.name); + expect( + targetNames, + `Expected to find dependencies target, but instead items were ${targetNames}` + ).to.deep.equal([ + "ExecutableTarget", + "LibraryTarget", + "PluginTarget", + "AnotherTests", + "TargetsTests", + ]); + } + ); + }); + }); + + suite("Tasks", () => { + beforeEach(async () => { + await waitForNoRunningTasks(); + }); + + async function getBuildAllTask() { + // In Swift 5.10 and below the build tasks are disabled while other tasks that could modify .build are running. + // Typically because the extension has just started up in tests its `swift test list` that runs to gather tests + // for the test explorer. If we're running 5.10 or below, poll for the build all task for up to 60 seconds. + if (workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) { + const startTime = Date.now(); + let task: PackageNode | undefined; + while (!task && Date.now() - startTime < 45 * 1000) { + const tasks = await getHeaderChildren("Tasks"); + task = tasks.find(n => n.name === "Build All (targets)") as PackageNode; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + return task; + } else { + const tasks = await getHeaderChildren("Tasks"); + return tasks.find(n => n.name === "Build All (targets)") as PackageNode; + } + } + + test("Includes tasks", async () => { + const dep = await getBuildAllTask(); + expect(dep).to.not.be.undefined; + }); + + test("Executes a task", async () => { + const task = await getBuildAllTask(); + expect(task).to.not.be.undefined; + const treeItem = task?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Snippets", () => { + test("Includes snippets", async () => { + await waitForChildren( + () => getHeaderChildren("Snippets"), + snippets => { + const snippetNames = snippets.map(n => n.name); + expect(snippetNames).to.deep.equal(["AnotherSnippet", "Snippet"]); + } + ); + }); + + test("Executes a snippet", async function () { + if ( + process.platform === "win32" && + workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(5, 9, 0)) + ) { + this.skip(); + } + + const snippet = await waitForChildren( + () => getHeaderChildren("Snippets"), + snippets => { + const snippet = snippets.find(n => n.name === "Snippet"); + expect(snippet).to.not.be.undefined; + return snippet; + } + ); + const result = await vscode.commands.executeCommand("swift.runSnippet", snippet?.name); + expect(result).to.be.true; + }); + }); + + suite("Commands", () => { + test("Includes commands", async function () { + if ( + process.platform === "win32" && + workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0)) + ) { + this.skip(); + } + + await waitForChildren( + () => getHeaderChildren("Commands"), + commands => { + const commandNames = commands.map(n => n.name); + expect(commandNames).to.deep.equal(["PluginTarget"]); + } + ); + }); + + test("Executes a command", async function () { + if ( + process.platform === "win32" && + workspaceContext.toolchain.swiftVersion.isLessThanOrEqual(new Version(6, 0, 0)) + ) { + this.skip(); + } + + const command = await waitForChildren( + () => getHeaderChildren("Commands"), + commands => { + const command = commands.find(n => n.name === "PluginTarget"); + expect(command).to.not.be.undefined; + return command; + } + ); + const treeItem = command?.toTreeItem(); + expect(treeItem?.command).to.not.be.undefined; + expect(treeItem?.command?.arguments).to.not.be.undefined; + if (treeItem && treeItem.command && treeItem.command.arguments) { + const command = treeItem.command.command; + const args = treeItem.command.arguments; + const result = await vscode.commands.executeCommand(command, ...args); + expect(result).to.be.true; + } + }); + }); + + suite("Dependencies", () => { + test("Includes remote dependency", async () => { + contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(items, null, 2)}`).to.not.be.undefined; + expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); + assertPathsEqual( + dep?.path, + path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown") + ); + }); + + test("Includes local dependency", async () => { + const items = await getHeaderChildren("Dependencies"); + const dep = items.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${items.map(n => n.name)}` + ).to.not.be.undefined; + assertPathsEqual(dep?.location, testAssetPath("defaultPackage")); + assertPathsEqual(dep?.path, testAssetPath("defaultPackage")); + }); + + test("Lists local dependency file structure", async () => { + contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "defaultpackage") as PackageNode; + expect( + dep, + `Expected to find defaultPackage, but instead items were ${children.map(n => n.name)}` + ).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + assertPathsEqual(folder?.path, path.join(testAssetPath("defaultPackage"), "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "PackageExe") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual( + childFolder?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe") + ); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "main.swift") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual( + file?.path, + path.join(testAssetPath("defaultPackage"), "Sources/PackageExe/main.swift") + ); + }); + + test("Lists remote dependency file structure", async () => { + contextKeys.flatDependenciesList = false; + const children = await getHeaderChildren("Dependencies"); + const dep = children.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep, `${JSON.stringify(children, null, 2)}`).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources") as FileNode; + expect(folder).to.not.be.undefined; + + const depPath = path.join(testAssetPath("targets"), ".build/checkouts/swift-markdown"); + assertPathsEqual(folder?.path, path.join(depPath, "Sources")); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "CAtomic") as FileNode; + expect(childFolder).to.not.be.undefined; + + assertPathsEqual(childFolder?.path, path.join(depPath, "Sources/CAtomic")); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "CAtomic.c") as FileNode; + expect(file).to.not.be.undefined; + + assertPathsEqual(file?.path, path.join(depPath, "Sources/CAtomic/CAtomic.c")); + }); + + test("Shows a flat dependency list", async () => { + contextKeys.flatDependenciesList = true; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(3); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "swift-cmark")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + + test("Shows a nested dependency list", async () => { + contextKeys.flatDependenciesList = false; + const items = await getHeaderChildren("Dependencies"); + expect(items.length).to.equal(2); + expect(items.find(n => n.name === "swift-markdown")).to.not.be.undefined; + expect(items.find(n => n.name === "defaultpackage")).to.not.be.undefined; + }); + + test("Shows an error node when there is a problem compiling Package.swift", async () => { + workspaceContext.folders[0].hasResolveErrors = true; + workspaceContext.currentFolder = workspaceContext.folders[0]; + const treeProvider = new ProjectPanelProvider(workspaceContext); + const children = await treeProvider.getChildren(); + const errorNode = children.find(n => n.name === "Error Parsing Package.swift"); + expect(errorNode).to.not.be.undefined; + }); + }); + + async function getHeaderChildren(headerName: string) { + const headers = await treeProvider.getChildren(); + const header = headers.find(n => n.name === headerName) as PackageNode; + expect(header).to.not.be.undefined; + return await header.getChildren(); + } + + async function waitForChildren( + getChildren: () => Promise, + predicate: (children: TreeNode[]) => T + ) { + let counter = 0; + let error: unknown; + // Check the predicate once a second for 30 seconds. + while (counter < 30) { + const children = await getChildren(); + try { + return predicate(children); + } catch (err) { + error = err; + counter += 1; + } + + if (!error) { + break; + } + + await wait(1000); + } + + if (error) { + throw error; + } + } + + function assertPathsEqual(path1: string | undefined, path2: string | undefined) { + expect(path1).to.not.be.undefined; + expect(path2).to.not.be.undefined; + // Convert to vscode.Uri to normalize paths, including drive letter capitalization on Windows. + expect(vscode.Uri.file(path1!).fsPath).to.equal(vscode.Uri.file(path2!).fsPath); + } +}); diff --git a/test/integration-tests/ui/SwiftOutputChannel.test.ts b/test/integration-tests/ui/SwiftOutputChannel.test.ts index 57b113965..8321b9319 100644 --- a/test/integration-tests/ui/SwiftOutputChannel.test.ts +++ b/test/integration-tests/ui/SwiftOutputChannel.test.ts @@ -20,7 +20,7 @@ suite("SwiftOutputChannel", function () { const channels: SwiftOutputChannel[] = []; setup(function () { const channelName = `SwiftOutputChannel Tests ${this.currentTest?.id ?? ""}`; - channel = new SwiftOutputChannel(channelName, false, 3); + channel = new SwiftOutputChannel(channelName, 3); channels.push(channel); }); diff --git a/test/integration-tests/utilities/lsputilities.ts b/test/integration-tests/utilities/lsputilities.ts index 066b791ce..30173287d 100644 --- a/test/integration-tests/utilities/lsputilities.ts +++ b/test/integration-tests/utilities/lsputilities.ts @@ -15,6 +15,7 @@ import * as vscode from "vscode"; import * as langclient from "vscode-languageclient/node"; import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClientManager"; +import { Version } from "../../../src/utilities/version"; export async function waitForClient( languageClientManager: LanguageClientManager, @@ -41,9 +42,25 @@ export namespace PollIndexRequest { export const type = new langclient.RequestType(method); } +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace WorkspaceSynchronizeRequest { + export const method = "workspace/synchronize" as const; + export const messageDirection: langclient.MessageDirection = + langclient.MessageDirection.clientToServer; + export const type = new langclient.RequestType(method); +} export async function waitForIndex(languageClientManager: LanguageClientManager): Promise { + const swiftVersion = languageClientManager.workspaceContext.swiftVersion; + const requestType = swiftVersion.isGreaterThanOrEqual(new Version(6, 2, 0)) + ? WorkspaceSynchronizeRequest.type + : PollIndexRequest.type; + await languageClientManager.useLanguageClient(async (client, token) => - client.sendRequest(PollIndexRequest.type, {}, token) + client.sendRequest( + requestType, + requestType === WorkspaceSynchronizeRequest.type ? { index: true } : {}, + token + ) ); } diff --git a/test/integration-tests/utilities/testutilities.ts b/test/integration-tests/utilities/testutilities.ts index 806d30bb6..7d3692ae4 100644 --- a/test/integration-tests/utilities/testutilities.ts +++ b/test/integration-tests/utilities/testutilities.ts @@ -22,6 +22,9 @@ import { FolderContext } from "../../../src/FolderContext"; import { waitForNoRunningTasks } from "../../utilities/tasks"; import { closeAllEditors } from "../../utilities/commands"; import { isDeepStrictEqual } from "util"; +import { Version } from "../../../src/utilities/version"; +import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; +import configuration from "../../../src/configuration"; function getRootWorkspaceFolder(): vscode.WorkspaceFolder { const result = vscode.workspace.workspaceFolders?.at(0); @@ -29,32 +32,18 @@ function getRootWorkspaceFolder(): vscode.WorkspaceFolder { return result; } +function printLogs(outputChannel: SwiftOutputChannel, message: string) { + console.error(`${message}, captured logs are:`); + outputChannel.logs.map(log => console.log(log)); + console.log("======== END OF LOGS ========\n\n"); +} + const extensionBootstrapper = (() => { let activator: (() => Promise) | undefined = undefined; let activatedAPI: Api | undefined = undefined; let lastTestName: string | undefined = undefined; - let lastTestLogs: string[] = []; const testTitle = (currentTest: Mocha.Test) => currentTest.titlePath().join(" → "); - mocha.afterEach(function () { - if (this.currentTest && this.currentTest.isFailed()) { - console.log(`Captured logs during ${testTitle(this.currentTest)}:`); - if (lastTestLogs.length === 0) { - console.log("No logs captured."); - } - for (const log of lastTestLogs) { - console.log(log); - } - } - }); - - mocha.beforeEach(function () { - if (this.currentTest && activatedAPI && process.env["VSCODE_TEST"]) { - activatedAPI.outputChannel.clear(); - activatedAPI.outputChannel.appendLine(`Starting test: ${testTitle(this.currentTest)}`); - } - }); - function testRunnerSetup( before: Mocha.HookFunction, setup: @@ -65,17 +54,45 @@ const extensionBootstrapper = (() => { | undefined, after: Mocha.HookFunction, teardown: ((this: Mocha.Context) => Promise) | undefined, - testAssets?: string[] + testAssets?: string[], + requiresLSP: boolean = false, + requiresDebugger: boolean = false ) { let workspaceContext: WorkspaceContext | undefined; let autoTeardown: void | (() => Promise); + let restoreSettings: (() => Promise) | undefined; before(async function () { + // Make sure that CodeLLDB is installed for debugging related tests + if (!vscode.extensions.getExtension("vadimcn.vscode-lldb")) { + await vscode.commands.executeCommand( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ); + } // Always activate the extension. If no test assets are provided, // default to adding `defaultPackage` to the workspace. workspaceContext = await extensionBootstrapper.activateExtension( this.currentTest, testAssets ?? ["defaultPackage"] ); + // Need the `disableSandbox` configuration which is only in 6.1 + // https://github.com/swiftlang/sourcekit-lsp/commit/7e2d12a7a0d184cc820ae6af5ddbb8aa18b1501c + if ( + process.platform === "darwin" && + workspaceContext.toolchain.swiftVersion.isLessThan(new Version(6, 1, 0)) && + requiresLSP + ) { + this.skip(); + } + if (requiresDebugger && configuration.debugger.disable) { + this.skip(); + } + // CodeLLDB does not work with libllbd in Swift toolchains prior to 5.10 + if (workspaceContext.swiftVersion.isLessThan(new Version(5, 10, 0))) { + restoreSettings = await updateSettings({ + "swift.debugger.setupCodeLLDB": "never", + }); + } if (!setup) { return; } @@ -87,16 +104,34 @@ const extensionBootstrapper = (() => { } catch (error: any) { // Mocha will throw an error to break out of a test if `.skip` is used. if (error.message?.indexOf("sync skip;") === -1) { - console.error(`Error during test/suite setup: ${JSON.stringify(error)}`); - console.error("Captured logs are:"); + console.error(`Error during test/suite setup, captured logs are:`); workspaceContext.outputChannel.logs.map(log => console.error(log)); - console.error("================ end test logs ================"); + console.log("======== END OF LOGS ========\n\n"); } throw error; } }); + mocha.beforeEach(function () { + if (this.currentTest && activatedAPI) { + activatedAPI.outputChannel.clear(); + activatedAPI.outputChannel.appendLine( + `Starting test: ${testTitle(this.currentTest)}` + ); + } + }); + + mocha.afterEach(function () { + if (this.currentTest && activatedAPI && this.currentTest.isFailed()) { + printLogs( + activatedAPI.outputChannel, + `Test failed: ${testTitle(this.currentTest)}` + ); + } + }); + after(async function () { + let userTeardownError: unknown | undefined; try { // First run the users supplied teardown, then await the autoTeardown if it exists. if (teardown) { @@ -107,13 +142,25 @@ const extensionBootstrapper = (() => { } } catch (error) { if (workspaceContext) { - console.error(`Error during test/suite teardown, captured logs are:`); - workspaceContext.outputChannel.logs.map(log => console.log(log)); + printLogs(workspaceContext.outputChannel, "Error during test/suite teardown"); } - throw error; + // We always want to restore settings and deactivate the extension even if the + // user supplied teardown fails. That way we have the best chance at not causing + // issues with the next test. + // + // Store the error and re-throw it after extension deactivation. + userTeardownError = error; } + if (restoreSettings) { + await restoreSettings(); + } await extensionBootstrapper.deactivateExtension(); + + // Re-throw the user supplied teardown error + if (userTeardownError) { + throw userTeardownError; + } }); } @@ -154,6 +201,10 @@ const extensionBootstrapper = (() => { } if (!workspaceContext) { + printLogs( + activatedAPI.outputChannel, + "Error during test/suite setup, workspace context could not be created" + ); throw new Error("Extension did not activate. Workspace context is not available."); } @@ -170,7 +221,6 @@ const extensionBootstrapper = (() => { if (!activatedAPI) { throw new Error("Extension is not activated. Call activateExtension() first."); } - lastTestLogs = activatedAPI.outputChannel.logs; // Wait for up to 10 seconds for all tasks to complete before deactivating. // Long running tasks should be avoided in tests, but this is a safety net. @@ -192,13 +242,17 @@ const extensionBootstrapper = (() => { ) => Promise<(() => Promise) | void>; teardown?: (this: Mocha.Context) => Promise; testAssets?: string[]; + requiresLSP?: boolean; + requiresDebugger?: boolean; }) { testRunnerSetup( mocha.before, config?.setup, mocha.after, config?.teardown, - config?.testAssets + config?.testAssets, + config?.requiresLSP, + config?.requiresDebugger ); }, @@ -209,13 +263,17 @@ const extensionBootstrapper = (() => { ) => Promise<(() => Promise) | void>; teardown?: (this: Mocha.Context) => Promise; testAssets?: string[]; + requiresLSP?: boolean; + requiresDebugger?: boolean; }) { testRunnerSetup( mocha.beforeEach, config?.setup, mocha.afterEach, config?.teardown, - config?.testAssets + config?.testAssets, + config?.requiresLSP, + config?.requiresDebugger ); }, }; @@ -279,7 +337,7 @@ export type SettingsMap = { [key: string]: unknown }; export async function updateSettings(settings: SettingsMap): Promise<() => Promise> { const applySettings = async (settings: SettingsMap) => { const savedOriginalSettings: SettingsMap = {}; - Object.keys(settings).forEach(async setting => { + for (const setting of Object.keys(settings)) { const { section, name } = decomposeSettingName(setting); const config = vscode.workspace.getConfiguration(section, { languageId: "swift" }); savedOriginalSettings[setting] = config.get(name); @@ -288,7 +346,7 @@ export async function updateSettings(settings: SettingsMap): Promise<() => Promi settings[setting] === "" ? undefined : settings[setting], vscode.ConfigurationTarget.Workspace ); - }); + } // There is actually a delay between when the config.update promise resolves and when // the setting is actually written. If we exit this function right away the test might @@ -296,10 +354,17 @@ export async function updateSettings(settings: SettingsMap): Promise<() => Promi // to their new value before continuing. for (const setting of Object.keys(settings)) { const { section, name } = decomposeSettingName(setting); + // If the setting is being unset then its possible the setting will evaluate to the + // default value, and so we should be checking to see if its switched to that instead. + const expected = !settings[setting] + ? (vscode.workspace.getConfiguration(section, { languageId: "swift" }).inspect(name) + ?.defaultValue ?? settings[setting]) + : settings[setting]; + while ( isDeepStrictEqual( vscode.workspace.getConfiguration(section, { languageId: "swift" }).get(name), - settings[setting] + expected ) === false ) { // Not yet, wait a bit and try again. diff --git a/test/unit-tests/commands/switchPlatform.test.ts b/test/unit-tests/commands/switchPlatform.test.ts new file mode 100644 index 000000000..f2c54f105 --- /dev/null +++ b/test/unit-tests/commands/switchPlatform.test.ts @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { expect } from "chai"; +import * as vscode from "vscode"; +import { + mockObject, + mockGlobalObject, + mockGlobalModule, + MockedObject, + mockFn, + instance, +} from "../../MockUtils"; +import { + DarwinCompatibleTarget, + SwiftToolchain, + getDarwinTargetTriple, +} from "../../../src/toolchain/toolchain"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { switchPlatform } from "../../../src/commands/switchPlatform"; +import { StatusItem } from "../../../src/ui/StatusItem"; +import configuration from "../../../src/configuration"; + +suite("Switch Target Platform Unit Tests", () => { + const mockedConfiguration = mockGlobalModule(configuration); + const windowMock = mockGlobalObject(vscode, "window"); + const mockSwiftToolchain = mockGlobalModule(SwiftToolchain); + let mockContext: MockedObject; + let mockedStatusItem: MockedObject; + + setup(() => { + mockedStatusItem = mockObject({ + start: mockFn(), + end: mockFn(), + }); + mockContext = mockObject({ + statusItem: instance(mockedStatusItem), + }); + }); + + test("Call Switch Platform and switch to iOS", async () => { + const selectedItem = { value: DarwinCompatibleTarget.iOS, label: "iOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.have.been.calledOnceWithExactly( + "Selecting the iOS target platform will provide code editing support, but compiling with a iOS SDK will have undefined results." + ); + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal( + getDarwinTargetTriple(DarwinCompatibleTarget.iOS) + ); + }); + + test("Call Switch Platform and switch to macOS", async () => { + const selectedItem = { value: undefined, label: "macOS" }; + windowMock.showQuickPick.resolves(selectedItem); + mockSwiftToolchain.getSDKForTarget.resolves(""); + expect(mockedConfiguration.swiftSDK).to.equal(""); + + await switchPlatform(instance(mockContext)); + + expect(windowMock.showQuickPick).to.have.been.calledOnce; + expect(windowMock.showWarningMessage).to.not.have.been.called; + expect(mockedStatusItem.start).to.have.been.called; + expect(mockedStatusItem.end).to.have.been.called; + expect(mockedConfiguration.swiftSDK).to.equal(""); + }); +}); diff --git a/test/unit-tests/debugger/attachDebugger.test.ts b/test/unit-tests/debugger/attachDebugger.test.ts deleted file mode 100644 index 937132a0d..000000000 --- a/test/unit-tests/debugger/attachDebugger.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the VS Code Swift open source project -// -// Copyright (c) 2021-2024 the VS Code Swift project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of VS Code Swift project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import { expect } from "chai"; -import * as vscode from "vscode"; -import * as lldb from "../../../src/debugger/lldb"; -import { attachDebugger } from "../../../src/commands/attachDebugger"; -import { - mockObject, - mockGlobalObject, - mockGlobalModule, - MockedObject, - instance, -} from "../../MockUtils"; -import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { WorkspaceContext } from "../../../src/WorkspaceContext"; -import { registerDebugger } from "../../../src/debugger/debugAdapterFactory"; -import { Version } from "../../../src/utilities/version"; - -suite("attachDebugger Unit Test Suite", () => { - const lldbMock = mockGlobalModule(lldb); - const windowMock = mockGlobalObject(vscode, "window"); - const debugMock = mockGlobalObject(vscode, "debug"); - - let mockContext: MockedObject; - let mockToolchain: MockedObject; - - setup(() => { - mockToolchain = mockObject({ - swiftVersion: new Version(6, 0, 0), - }); - mockContext = mockObject({ - toolchain: instance(mockToolchain), - }); - }); - - test("should call startDebugging with correct debugConfig", async () => { - // Setup fake debug adapter - registerDebugger(instance(mockContext)); - - // Mock the list of processes returned by getLldbProcess - const processPickItems = [ - { pid: 1234, label: "1234: Process1" }, - { pid: 2345, label: "2345: Process2" }, - ]; - lldbMock.getLldbProcess.resolves(processPickItems); - windowMock.showQuickPick.callsFake(async items => (await items)[0]); - - // Call attachDebugger - await attachDebugger(instance(mockContext)); - - // Verify startDebugging was called with the right pid. - // NB(separate itest): actual config return a fulfilled promise. - expect(debugMock.startDebugging).to.have.been.calledOnce; - expect(debugMock.startDebugging.args[0][1]).to.containSubset({ pid: 1234 }); - }); -}); diff --git a/test/unit-tests/debugger/debugAdapter.test.ts b/test/unit-tests/debugger/debugAdapter.test.ts index 56971c14d..a136db764 100644 --- a/test/unit-tests/debugger/debugAdapter.test.ts +++ b/test/unit-tests/debugger/debugAdapter.test.ts @@ -12,52 +12,27 @@ // //===----------------------------------------------------------------------===// -import * as vscode from "vscode"; import { expect } from "chai"; import * as mockFS from "mock-fs"; -import { - mockGlobalObject, - MockedObject, - mockObject, - instance, - mockGlobalModule, - mockFn, -} from "../../MockUtils"; +import { MockedObject, mockObject, instance, mockGlobalModule } from "../../MockUtils"; import configuration from "../../../src/configuration"; import { DebugAdapter, LaunchConfigType } from "../../../src/debugger/debugAdapter"; -import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; import { Version } from "../../../src/utilities/version"; -import contextKeys from "../../../src/contextKeys"; suite("DebugAdapter Unit Test Suite", () => { const mockConfiguration = mockGlobalModule(configuration); - const mockedContextKeys = mockGlobalModule(contextKeys); - const mockedWindow = mockGlobalObject(vscode, "window"); let mockDebugConfig: MockedObject<(typeof configuration)["debugger"]>; - let mockToolchain: MockedObject; - let mockOutputChannel: MockedObject; setup(() => { // Mock VS Code settings mockDebugConfig = mockObject<(typeof configuration)["debugger"]>({ - useDebugAdapterFromToolchain: false, + debugAdapter: "auto", customDebugAdapterPath: "", }); mockConfiguration.debugger = instance(mockDebugConfig); // Mock the file system mockFS({}); - // Mock the WorkspaceContext and related dependencies - const toolchainPath = "/toolchains/swift"; - mockToolchain = mockObject({ - swiftVersion: new Version(6, 0, 0), - getLLDBDebugAdapter: mockFn(s => s.callsFake(() => toolchainPath + "/lldb-dap")), - getLLDB: mockFn(s => s.callsFake(() => toolchainPath + "/lldb")), - }); - mockOutputChannel = mockObject({ - log: mockFn(), - }); }); teardown(() => { @@ -65,238 +40,43 @@ suite("DebugAdapter Unit Test Suite", () => { }); suite("getLaunchConfigType()", () => { - test("returns SWIFT_EXTENSION when Swift version >=6.0.0 and swift.debugger.useDebugAdapterFromToolchain is true", () => { - mockDebugConfig.useDebugAdapterFromToolchain = true; + test("returns LLDB_DAP when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to lldb-dap", () => { + mockDebugConfig.debugAdapter = "lldb-dap"; + expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( + LaunchConfigType.LLDB_DAP + ); + }); + + test("returns LLDB_DAP when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to auto", () => { + mockDebugConfig.debugAdapter = "auto"; expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( - LaunchConfigType.SWIFT_EXTENSION + LaunchConfigType.LLDB_DAP ); }); - test("returns CODE_LLDB when Swift version >=6.0.0 and swift.debugger.useDebugAdapterFromToolchain is false", () => { - mockDebugConfig.useDebugAdapterFromToolchain = false; + test("returns CODE_LLDB when Swift version >=6.0.0 and swift.debugger.debugAdapter is set to CODE_LLDB", () => { + mockDebugConfig.debugAdapter = "CodeLLDB"; expect(DebugAdapter.getLaunchConfigType(new Version(6, 0, 1))).to.equal( LaunchConfigType.CODE_LLDB ); }); test("returns CODE_LLDB when Swift version is older than 6.0.0 regardless of setting", () => { - // Try with the setting false - mockDebugConfig.useDebugAdapterFromToolchain = false; + // Try with the setting set to auto + mockDebugConfig.debugAdapter = "auto"; expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( LaunchConfigType.CODE_LLDB ); - // Try with the setting true - mockDebugConfig.useDebugAdapterFromToolchain = true; + // Try with the setting set to CodeLLDB + mockDebugConfig.debugAdapter = "CodeLLDB"; + expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( + LaunchConfigType.CODE_LLDB + ); + // Try with the setting set to lldb-dap + mockDebugConfig.debugAdapter = "lldb-dap"; expect(DebugAdapter.getLaunchConfigType(new Version(5, 10, 0))).to.equal( LaunchConfigType.CODE_LLDB ); }); }); - - suite("verifyDebugAdapterExists()", () => { - suite("Using lldb-dap", () => { - setup(() => { - mockToolchain.swiftVersion = new Version(6, 0, 0); - mockDebugConfig.useDebugAdapterFromToolchain = true; - // Should be using lldb-dap in this case - mockFS({ - "/toolchains/swift/lldb-dap": mockFS.file({ content: "", mode: 0o770 }), - }); - }); - - createCommonTests(); - - test("returns false when the toolchain throws an error trying to find lldb-dap", async () => { - mockToolchain.getLLDBDebugAdapter.rejects(new Error("Uh oh!")); - - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ) - ).to.eventually.be.false; - }); - - test("shows an error message to the user when the toolchain throws an error trying to find lldb-dap", async () => { - mockToolchain.getLLDBDebugAdapter.rejects(new Error("Uh oh!")); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ); - expect(mockedWindow.showErrorMessage).to.have.been.calledOnce; - }); - - test("disables the swift.lldbVSCodeAvailable context key if the toolchain throws an error trying to find lldb-dap", async () => { - mockToolchain.getLLDBDebugAdapter.rejects(new Error("Uh oh!")); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ); - expect(mockedContextKeys.lldbVSCodeAvailable).to.be.false; - }); - }); - - suite("Using lldb-dap with custom debug adapter path", () => { - setup(() => { - mockToolchain.swiftVersion = new Version(6, 0, 0); - mockDebugConfig.useDebugAdapterFromToolchain = true; - mockDebugConfig.customDebugAdapterPath = "/path/to/custom/lldb-dap"; - // Should be using a custom lldb-dap in this case - mockFS({ - "/path/to/custom/lldb-dap": mockFS.file({ content: "", mode: 0o770 }), - }); - }); - - createCommonTests(); - }); - - suite("Using CodeLLDB", () => { - setup(() => { - mockToolchain.swiftVersion = new Version(6, 0, 0); - mockDebugConfig.useDebugAdapterFromToolchain = false; - // Should be using CodeLLDB in this case - mockFS({ - "/toolchains/swift/lldb": mockFS.file({ content: "", mode: 0o770 }), - }); - }); - - createCommonTests(); - - test("returns false when the toolchain throws an error trying to find lldb", async () => { - mockToolchain.getLLDB.rejects(new Error("Uh oh!")); - - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ) - ).to.eventually.be.false; - }); - - test("shows an error message to the user when the toolchain throws an error trying to find lldb", async () => { - mockToolchain.getLLDB.rejects(new Error("Uh oh!")); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ); - expect(mockedWindow.showErrorMessage).to.have.been.calledOnce; - }); - - test("disables the swift.lldbVSCodeAvailable context key if the toolchain throws an error trying to find lldb", async () => { - mockToolchain.getLLDB.rejects(new Error("Uh oh!")); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ); - expect(mockedContextKeys.lldbVSCodeAvailable).to.be.false; - }); - }); - - function createCommonTests() { - test("returns true when debug adapter exists regardless of quiet setting", async () => { - // Test with quiet = true - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - true - ) - ).to.eventually.be.true; - - // Test with quiet = false - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ) - ).to.eventually.be.true; - }); - - test("returns false when debug adapter doesn't exist regardless of quiet setting", async () => { - // Reset the file system to empty - mockFS({}); - - // Test with quiet = true - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - true - ) - ).to.eventually.be.false; - - // Test with quiet = false - await expect( - DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ) - ).to.eventually.be.false; - }); - - test("shows an error message to the user when the debug adapter doesn't exist and quiet is false", async () => { - // Reset the file system to empty - mockFS({}); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - false - ); - expect(mockedWindow.showErrorMessage).to.have.been.called; - }); - - test("doesn't show an error message to the user when the debug adapter doesn't exist and quiet is false", async () => { - // Reset the file system to empty - mockFS({}); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - true - ); - expect(mockedWindow.showErrorMessage).to.not.have.been.called; - }); - - test("doesn't show an error message to the user when the debug adapter exists", async () => { - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel), - true - ); - expect(mockedWindow.showErrorMessage).to.not.have.been.called; - }); - - test("enables the swift.lldbVSCodeAvailable context key if the debugger exists", async () => { - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel) - ); - expect(mockedContextKeys.lldbVSCodeAvailable).to.be.true; - }); - - test("disables the swift.lldbVSCodeAvailable context key if the debugger doesn't exist", async () => { - // Reset the file system to empty - mockFS({}); - - await DebugAdapter.verifyDebugAdapterExists( - instance(mockToolchain), - instance(mockOutputChannel) - ); - expect(mockedContextKeys.lldbVSCodeAvailable).to.be.false; - }); - } - }); }); diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index 852d4e10c..ebc48f7e1 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -14,10 +14,7 @@ import * as vscode from "vscode"; import { expect } from "chai"; -import { - LLDBDebugAdapterExecutableFactory, - LLDBDebugConfigurationProvider, -} from "../../../src/debugger/debugAdapterFactory"; +import { LLDBDebugConfigurationProvider } from "../../../src/debugger/debugAdapterFactory"; import { Version } from "../../../src/utilities/version"; import { mockGlobalObject, @@ -27,257 +24,413 @@ import { mockGlobalModule, mockFn, } from "../../MockUtils"; -import configuration from "../../../src/configuration"; -import { DebugAdapter, LaunchConfigType } from "../../../src/debugger/debugAdapter"; +import * as mockFS from "mock-fs"; +import { LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "../../../src/debugger/debugAdapter"; +import * as lldb from "../../../src/debugger/lldb"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; +import * as debugAdapter from "../../../src/debugger/debugAdapter"; +import { Result } from "../../../src/utilities/result"; +import configuration from "../../../src/configuration"; -suite("Debug Adapter Factory Test Suite", () => { - const swift6 = new Version(6, 0, 0); - const swift510 = new Version(5, 10, 1); - const mockDebugConfig = mockGlobalObject(configuration, "debugger"); - - suite("LLDBDebugConfigurationProvider Test Suite", () => { - setup(() => { - mockDebugConfig.useDebugAdapterFromToolchain = true; - }); - - test("uses lldb-dap for swift versions >=6.0.0", async () => { - const configProvider = new LLDBDebugConfigurationProvider("darwin", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ type: LaunchConfigType.SWIFT_EXTENSION }); - }); - - test("delegates to CodeLLDB for swift versions <6.0.0", async () => { - const configProvider = new LLDBDebugConfigurationProvider("darwin", swift510); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ - type: LaunchConfigType.CODE_LLDB, - sourceLanguages: ["swift"], - }); - }); - - test("delegates to CodeLLDB on Swift 6.0.0 if setting swift.debugger.useDebugAdapterFromToolchain is explicitly disabled", async () => { - mockDebugConfig.useDebugAdapterFromToolchain = false; - const configProvider = new LLDBDebugConfigurationProvider("darwin", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ - type: LaunchConfigType.CODE_LLDB, - sourceLanguages: ["swift"], - }); - }); - - test("modifies program to add file extension on Windows", async () => { - const configProvider = new LLDBDebugConfigurationProvider("win32", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ - program: "${workspaceFolder}/.build/debug/executable.exe", - }); - }); - - test("does not modify program on Windows if file extension is already present", async () => { - const configProvider = new LLDBDebugConfigurationProvider("win32", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable.exe", - }); - expect(launchConfig).to.containSubset({ - program: "${workspaceFolder}/.build/debug/executable.exe", - }); - }); - - test("does not modify program on macOS", async () => { - const configProvider = new LLDBDebugConfigurationProvider("darwin", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ - program: "${workspaceFolder}/.build/debug/executable", - }); - }); - - test("does not modify program on Linux", async () => { - const configProvider = new LLDBDebugConfigurationProvider("linux", swift6); - const launchConfig = await configProvider.resolveDebugConfiguration(undefined, { - name: "Test Launch Config", - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - program: "${workspaceFolder}/.build/debug/executable", - }); - expect(launchConfig).to.containSubset({ - program: "${workspaceFolder}/.build/debug/executable", - }); - }); - }); -}); - -suite("debugAdapterFactory Tests", () => { - const mockAdapter = mockGlobalModule(DebugAdapter); +suite("LLDBDebugConfigurationProvider Tests", () => { let mockToolchain: MockedObject; let mockOutputChannel: MockedObject; + const mockDebugAdapter = mockGlobalObject(debugAdapter, "DebugAdapter"); + const mockWindow = mockGlobalObject(vscode, "window"); setup(() => { - mockToolchain = mockObject({}); + mockToolchain = mockObject({ swiftVersion: new Version(6, 0, 0) }); mockOutputChannel = mockObject({ log: mockFn(), }); }); - test("should return DebugAdapterExecutable when path and verification succeed", async () => { - const toolchainPath = "/path/to/debug/adapter"; - - mockAdapter.debugAdapterPath.resolves(toolchainPath); - mockAdapter.verifyDebugAdapterExists.resolves(true); - - const factory = new LLDBDebugAdapterExecutableFactory( + test("allows specifying a 'pid' in the launch configuration", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", instance(mockToolchain), instance(mockOutputChannel) ); - const result = await factory.createDebugAdapterDescriptor(); - - expect(result).to.be.instanceOf(vscode.DebugAdapterExecutable); - expect((result as vscode.DebugAdapterExecutable).command).to.equal(toolchainPath); - - expect(mockAdapter.debugAdapterPath).to.have.been.calledOnce; - expect(mockAdapter.verifyDebugAdapterExists).to.have.been.calledOnce; + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: 41038, + } + ); + expect(launchConfig).to.containSubset({ pid: 41038 }); }); - test("should throw error if debugAdapterPath fails", async () => { - const errorMessage = "Failed to get debug adapter path"; + test("converts 'pid' property from a string to a number", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: "41038", + } + ); + expect(launchConfig).to.containSubset({ pid: 41038 }); + }); - mockAdapter.debugAdapterPath.rejects(new Error(errorMessage)); + test("shows an error when the 'pid' property is a string that isn't a number", async () => { + // Simulate the user clicking the "Configure" button + mockWindow.showErrorMessage.resolves("Configure" as any); - const factory = new LLDBDebugAdapterExecutableFactory( + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", instance(mockToolchain), instance(mockOutputChannel) ); - - await expect(factory.createDebugAdapterDescriptor()).to.eventually.be.rejectedWith( - Error, - errorMessage + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: "not-a-number", + } ); - - expect(mockAdapter.debugAdapterPath).to.have.been.calledOnce; - expect(mockAdapter.verifyDebugAdapterExists).to.not.have.been.called; + expect(launchConfig).to.be.null; }); - test("should throw error if verifyDebugAdapterExists fails", async () => { - const toolchainPath = "/path/to/debug/adapter"; - const errorMessage = "Failed to verify debug adapter exists"; + test("shows an error when the 'pid' property isn't a number or string", async () => { + // Simulate the user clicking the "Configure" button + mockWindow.showErrorMessage.resolves("Configure" as any); - mockAdapter.debugAdapterPath.resolves(toolchainPath); - mockAdapter.verifyDebugAdapterExists.rejects(new Error(errorMessage)); - - const factory = new LLDBDebugAdapterExecutableFactory( + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", instance(mockToolchain), instance(mockOutputChannel) ); - - await expect(factory.createDebugAdapterDescriptor()).to.eventually.be.rejectedWith( - Error, - errorMessage + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + undefined, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "attach", + pid: {}, + } ); - - expect(mockAdapter.debugAdapterPath).to.have.been.calledOnce; - expect(mockAdapter.verifyDebugAdapterExists).to.have.been.calledOnce; + expect(launchConfig).to.be.null; }); - suite("LLDBDebugConfigurationProvider Tests", () => { - let provider: LLDBDebugConfigurationProvider; - const swift6 = new Version(6, 0, 0); + suite("CodeLLDB selected in settings", () => { + let mockLldbConfiguration: MockedObject; + const mockLLDB = mockGlobalModule(lldb); + const mockDebuggerConfig = mockGlobalObject(configuration, "debugger"); + const mockWorkspace = mockGlobalObject(vscode, "workspace"); + const mockExtensions = mockGlobalObject(vscode, "extensions"); + const mockCommands = mockGlobalObject(vscode, "commands"); setup(() => { - provider = new LLDBDebugConfigurationProvider("darwin", swift6); + mockExtensions.getExtension.returns(mockObject>({})); + mockLldbConfiguration = mockObject({ + get: mockFn(s => { + s.withArgs("library").returns("/path/to/liblldb.dyLib"); + s.withArgs("launch.expressions").returns("native"); + }), + update: mockFn(), + }); + mockWorkspace.getConfiguration.returns(instance(mockLldbConfiguration)); + mockLLDB.getLLDBLibPath.resolves(Result.makeSuccess("/path/to/liblldb.dyLib")); + mockDebuggerConfig.setupCodeLLDB = "prompt"; + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.CODE_LLDB); }); - test("should convert environment variables to string[] format", () => { - const env = { - VAR1: "value1", - VAR2: "value2", - }; + test("returns a launch configuration that uses CodeLLDB as the debug adapter", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ type: LaunchConfigType.CODE_LLDB }); + }); + + test("prompts the user to install CodeLLDB if it isn't found", async () => { + mockExtensions.getExtension.returns(undefined); + mockWindow.showErrorMessage.resolves("Install CodeLLDB" as any); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.not.be.undefined; + expect(mockCommands.executeCommand).to.have.been.calledWith( + "workbench.extensions.installExtension", + "vadimcn.vscode-lldb" + ); + }); + + test("prompts the user to update CodeLLDB settings if they aren't configured yet", async () => { + mockLldbConfiguration.get.withArgs("library").returns(undefined); + mockWindow.showInformationMessage.resolves("Global" as any); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.not.be.undefined; + expect(mockWindow.showInformationMessage).to.have.been.calledOnce; + expect(mockLldbConfiguration.update).to.have.been.calledWith( + "library", + "/path/to/liblldb.dyLib" + ); + }); + + test("avoids prompting the user about CodeLLDB if requested in settings", async () => { + mockDebuggerConfig.setupCodeLLDB = "alwaysUpdateGlobal"; + mockLldbConfiguration.get.withArgs("library").returns(undefined); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.be.an("object"); + expect(mockWindow.showInformationMessage).to.not.have.been.called; + expect(mockLldbConfiguration.update).to.have.been.calledWith( + "library", + "/path/to/liblldb.dyLib" + ); + }); + }); + + suite("lldb-dap selected in settings", () => { + setup(() => { + mockDebugAdapter.getLaunchConfigType.returns(LaunchConfigType.LLDB_DAP); + mockDebugAdapter.getLLDBDebugAdapterPath.resolves("/path/to/lldb-dap"); + mockFS({ + "/path/to/lldb-dap": mockFS.file({ content: "", mode: 0o770 }), + }); + }); - const result = provider.convertEnvironmentVariables(env); + teardown(() => { + mockFS.restore(); + }); - expect(result).to.deep.equal(["VAR1=value1", "VAR2=value2"]); + test("returns a launch configuration that uses lldb-dap as the debug adapter", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + type: LaunchConfigType.LLDB_DAP, + debugAdapterExecutable: "/path/to/lldb-dap", + }); }); - test("should return undefined when environment variables are undefined", () => { - const result = provider.convertEnvironmentVariables(undefined); - expect(result).to.deep.equal(undefined); + test("fails if the path to lldb-dap could not be found", async () => { + mockFS({}); // Reset mockFS so that no files exist + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + await expect( + configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }) + ).to.eventually.be.undefined; + expect(mockWindow.showErrorMessage).to.have.been.calledOnce; }); - test("should resolve debug configuration with converted environment variables", async () => { - const launchConfig: vscode.DebugConfiguration = { - type: LaunchConfigType.SWIFT_EXTENSION, - request: "launch", - name: "Test Launch", - env: { - VAR1: "value1", - VAR2: "value2", - }, - }; + test("modifies program to add file extension on Windows", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "win32", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + }); - const resolvedConfig = await provider.resolveDebugConfiguration( - undefined, - launchConfig + test("does not modify program on Windows if file extension is already present", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "win32", + instance(mockToolchain), + instance(mockOutputChannel) ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable.exe", + }); + }); - expect(resolvedConfig.env).to.deep.equal(["VAR1=value1", "VAR2=value2"]); + test("does not modify program on macOS", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable", + }); }); - test("should handle one environment variable", () => { - const env = { - VAR1: "value1", - }; - const result = provider.convertEnvironmentVariables(env); + test("does not modify program on Linux", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "linux", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.containSubset({ + program: "${workspaceFolder}/.build/debug/executable", + }); + }); - expect(result).to.deep.equal(["VAR1=value1"]); + test("should convert environment variables to string[] format when using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + env: { + VAR1: "value1", + VAR2: "value2", + }, + }); + expect(launchConfig) + .to.have.property("env") + .that.deep.equals(["VAR1=value1", "VAR2=value2"]); }); - test("should handle empty environment variables", () => { - const env = {}; - const result = provider.convertEnvironmentVariables(env); + test("should leave env undefined when environment variables are undefined and using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + program: "${workspaceFolder}/.build/debug/executable", + }); + expect(launchConfig).to.not.have.property("env"); + }); - expect(result).to.deep.equal([]); + test("should convert empty environment variables when using lldb-dap", async () => { + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + name: "Test Launch", + env: {}, + }); + + expect(launchConfig).to.have.property("env").that.deep.equals([]); }); - test("should handle a large number of environment variables", () => { + test("should handle a large number of environment variables when using lldb-dap", async () => { // Create 1000 environment variables const env: { [key: string]: string } = {}; for (let i = 0; i < 1000; i++) { env[`VAR${i}`] = `value${i}`; } - - const result = provider.convertEnvironmentVariables(env); + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockToolchain), + instance(mockOutputChannel) + ); + const launchConfig = + await configProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + name: "Test Launch", + env, + }); // Verify that all 1000 environment variables are properly converted - const expected = Array.from({ length: 1000 }, (_, i) => `VAR${i}=value${i}`); - expect(result).to.deep.equal(expected); + const expectedEnv = Array.from({ length: 1000 }, (_, i) => `VAR${i}=value${i}`); + expect(launchConfig).to.have.property("env").that.deep.equals(expectedEnv); }); }); }); diff --git a/test/unit-tests/debugger/lldb.test.ts b/test/unit-tests/debugger/lldb.test.ts index 0bc5c1d6d..c3bd582df 100644 --- a/test/unit-tests/debugger/lldb.test.ts +++ b/test/unit-tests/debugger/lldb.test.ts @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import * as vscode from "vscode"; import * as util from "../../../src/utilities/utilities"; import * as lldb from "../../../src/debugger/lldb"; import * as fs from "fs/promises"; @@ -23,14 +22,11 @@ import { MockedObject, mockFn, mockGlobalModule, - mockGlobalObject, mockObject, MockedFunction, mockGlobalValue, } from "../../MockUtils"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; -import { WorkspaceContext } from "../../../src/WorkspaceContext"; -import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; suite("debugger.lldb Tests", () => { suite("getLLDBLibPath Tests", () => { @@ -136,81 +132,4 @@ suite("debugger.lldb Tests", () => { expect(result).to.be.null; }); }); - - suite("getLldbProcess Unit Test Suite", () => { - const utilMock = mockGlobalModule(util); - const windowMock = mockGlobalObject(vscode, "window"); - - let mockContext: MockedObject; - let mockToolchain: MockedObject; - - setup(() => { - windowMock.createOutputChannel.returns({ - appendLine() {}, - } as unknown as vscode.LogOutputChannel); - - mockToolchain = mockObject({ - getLLDB: mockFn(s => s.resolves("/path/to/lldb")), - }); - mockContext = mockObject({ - toolchain: instance(mockToolchain), - outputChannel: new SwiftOutputChannel("mockChannel", false), - }); - }); - - test("should return an empty list when no processes are found", async () => { - utilMock.execFile.resolves({ stdout: "", stderr: "" }); - - const result = await lldb.getLldbProcess(instance(mockContext)); - - expect(result).to.be.an("array").that.is.empty; - }); - - test("should return a list with one process", async () => { - utilMock.execFile.resolves({ - stdout: `1234 5678 user1 group1 SingleProcess\n`, - stderr: "", - }); - - const result = await lldb.getLldbProcess(instance(mockContext)); - - expect(result).to.deep.equal([{ pid: 1234, label: "1234: SingleProcess" }]); - }); - - test("should return a list with many processes", async () => { - const manyProcessesOutput = Array(1000) - .fill(0) - .map((_, i) => { - return `${1000 + i} 2000 user${i} group${i} Process${i}`; - }) - .join("\n"); - utilMock.execFile.resolves({ - stdout: manyProcessesOutput, - stderr: "", - }); - - const result = await lldb.getLldbProcess(instance(mockContext)); - - // Assert that the result is an array with 1000 processes - const expected = Array(1000) - .fill(0) - .map((_, i) => ({ - pid: 1000 + i, - label: `${1000 + i}: Process${i}`, - })); - expect(result).to.deep.equal(expected); - }); - - test("should handle errors correctly", async () => { - utilMock.execFile.rejects(new Error("LLDB Error")); - utilMock.getErrorDescription.returns("LLDB Error"); - - const result = await lldb.getLldbProcess(instance(mockContext)); - - expect(result).to.equal(undefined); - expect(windowMock.showErrorMessage).to.have.been.calledWith( - "Failed to run LLDB: LLDB Error" - ); - }); - }); }); diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index fd9009c02..421fa0499 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; +import * as path from "path"; import { expect } from "chai"; import { match } from "sinon"; import { FolderEvent, FolderOperation, WorkspaceContext } from "../../../src/WorkspaceContext"; @@ -43,6 +44,11 @@ import { LanguageClientManager } from "../../../src/sourcekit-lsp/LanguageClient import configuration from "../../../src/configuration"; import { FolderContext } from "../../../src/FolderContext"; import { LanguageClientFactory } from "../../../src/sourcekit-lsp/LanguageClientFactory"; +import { LSPActiveDocumentManager } from "../../../src/sourcekit-lsp/didChangeActiveDocument"; +import { + DidChangeActiveDocumentNotification, + DidChangeActiveDocumentParams, +} from "../../../src/sourcekit-lsp/extensions/DidChangeActiveDocumentRequest"; suite("LanguageClientManager Suite", () => { let languageClientFactoryMock: MockedObject; @@ -92,7 +98,7 @@ suite("LanguageClientManager Suite", () => { }); mockedToolchain = mockObject({ swiftVersion: new Version(6, 0, 0), - buildFlags: mockedBuildFlags, + buildFlags: mockedBuildFlags as unknown as BuildFlags, getToolchainExecutable: mockFn(s => s.withArgs("sourcekit-lsp").returns("/path/to/toolchain/bin/sourcekit-lsp") ), @@ -112,6 +118,7 @@ suite("LanguageClientManager Suite", () => { }); mockedConverter = mockObject({ asUri: mockFn(s => s.callsFake(uri => uri.fsPath)), + asTextDocumentIdentifier: mockFn(s => s.callsFake(doc => ({ uri: doc.uri.fsPath }))), }); changeStateEmitter = new AsyncEventEmitter(); languageClientMock = mockObject({ @@ -123,6 +130,15 @@ suite("LanguageClientManager Suite", () => { dispose: mockFn(), }) ), + initializeResult: { + capabilities: { + experimental: { + "window/didChangeActiveDocument": { + version: 1, + }, + }, + }, + }, start: mockFn(s => s.callsFake(async () => { const oldState = languageClientMock.state; @@ -286,7 +302,7 @@ suite("LanguageClientManager Suite", () => { DidChangeWorkspaceFoldersNotification.type, { event: { - added: [{ name: "folder1", uri: "/folder1" }], + added: [{ name: "folder1", uri: path.normalize("/folder1") }], removed: [], }, } as DidChangeWorkspaceFoldersParams @@ -305,7 +321,7 @@ suite("LanguageClientManager Suite", () => { DidChangeWorkspaceFoldersNotification.type, { event: { - added: [{ name: "folder2", uri: "/folder2" }], + added: [{ name: "folder2", uri: path.normalize("/folder2") }], removed: [], }, } as DidChangeWorkspaceFoldersParams @@ -325,7 +341,7 @@ suite("LanguageClientManager Suite", () => { { event: { added: [], - removed: [{ name: "folder1", uri: "/folder1" }], + removed: [{ name: "folder1", uri: path.normalize("/folder1") }], }, } as DidChangeWorkspaceFoldersParams ); @@ -422,6 +438,77 @@ suite("LanguageClientManager Suite", () => { ]); }); + suite("active document changes", () => { + const mockWindow = mockGlobalObject(vscode, "window"); + + setup(() => { + mockedWorkspace.swiftVersion = new Version(6, 1, 0); + }); + + test("Notifies when the active document changes", async () => { + const document: vscode.TextDocument = instance( + mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }) + ); + + let _listener: ((e: vscode.TextEditor | undefined) => any) | undefined; + mockWindow.onDidChangeActiveTextEditor.callsFake((listener, _2, _1) => { + _listener = listener; + return { dispose: () => {} }; + }); + + new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + await waitForReturnedPromises(languageClientMock.start); + + const activeDocumentManager = new LSPActiveDocumentManager(); + activeDocumentManager.activateDidChangeActiveDocument(instance(languageClientMock)); + activeDocumentManager.didOpen(document, async () => {}); + + if (_listener) { + _listener(instance(mockObject({ document }))); + } + + expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + DidChangeActiveDocumentNotification.method, + { + textDocument: { + uri: path.normalize("/folder1/file.swift"), + }, + } as DidChangeActiveDocumentParams + ); + }); + + test("Notifies on startup with the active document", async () => { + const document: vscode.TextDocument = instance( + mockObject({ + uri: vscode.Uri.file("/folder1/file.swift"), + }) + ); + mockWindow.activeTextEditor = instance( + mockObject({ + document, + }) + ); + new LanguageClientManager(instance(mockedWorkspace), languageClientFactoryMock); + await waitForReturnedPromises(languageClientMock.start); + + const activeDocumentManager = new LSPActiveDocumentManager(); + activeDocumentManager.didOpen(document, async () => {}); + + activeDocumentManager.activateDidChangeActiveDocument(instance(languageClientMock)); + + expect(languageClientMock.sendNotification).to.have.been.calledOnceWith( + DidChangeActiveDocumentNotification.method, + { + textDocument: { + uri: path.normalize("/folder1/file.swift"), + }, + } as DidChangeActiveDocumentParams + ); + }); + }); + suite("SourceKit-LSP version doesn't support workspace folders", () => { let folder1: MockedObject; let folder2: MockedObject; diff --git a/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts index 28be1b94a..c7d54d27c 100644 --- a/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts +++ b/test/unit-tests/tasks/SwiftPluginTaskProvider.test.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import * as assert from "assert"; +import * as path from "path"; import { match } from "sinon"; import { WorkspaceContext } from "../../../src/WorkspaceContext"; import { SwiftPluginTaskProvider } from "../../../src/tasks/SwiftPluginTaskProvider"; @@ -31,7 +32,7 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => { setup(async () => { buildFlags = mockObject({ - withSwiftSDKFlags: mockFn(s => s.callsFake(args => args)), + withAdditionalFlags: mockFn(s => s.callsFake(args => args)), }); toolchain = mockObject({ swiftVersion: new Version(6, 0, 0), @@ -148,7 +149,10 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => { new vscode.CancellationTokenSource().token ); const swiftExecution = resolvedTask.execution as SwiftExecution; - assert.equal(swiftExecution.options.cwd, `${workspaceFolder.uri.fsPath}/myCWD`); + assert.equal( + swiftExecution.options.cwd, + path.normalize(`${workspaceFolder.uri.fsPath}/myCWD`) + ); }); test("includes fallback cwd", async () => { @@ -192,7 +196,7 @@ suite("SwiftPluginTaskProvider Unit Test Suite", () => { }); test("includes sdk flags", async () => { - buildFlags.withSwiftSDKFlags + buildFlags.withAdditionalFlags .withArgs(match(["package", "my-plugin"])) .returns(["package", "my-plugin", "--sdk", "/path/to/sdk"]); const taskProvider = new SwiftPluginTaskProvider(instance(workspaceContext)); diff --git a/test/unit-tests/tasks/SwiftTaskProvider.test.ts b/test/unit-tests/tasks/SwiftTaskProvider.test.ts index f5a4198a3..f5523e79f 100644 --- a/test/unit-tests/tasks/SwiftTaskProvider.test.ts +++ b/test/unit-tests/tasks/SwiftTaskProvider.test.ts @@ -49,8 +49,7 @@ suite("SwiftTaskProvider Unit Test Suite", () => { setup(async () => { buildFlags = mockObject({ - withSwiftSDKFlags: mockFn(s => s.returns([])), - withSwiftPackageFlags: mockFn(s => s.returns(s.args)), + withAdditionalFlags: mockFn(s => s.callsFake(arr => arr)), }); toolchain = mockObject({ swiftVersion: new Version(6, 0, 0), @@ -184,11 +183,8 @@ suite("SwiftTaskProvider Unit Test Suite", () => { }); test("include sdk flags", () => { - buildFlags.withSwiftSDKFlags + buildFlags.withAdditionalFlags .withArgs(match(["build"])) - .returns(["build", "--sdk", "/path/to/sdk"]); - buildFlags.withSwiftPackageFlags - .withArgs(match(["build", "--sdk", "/path/to/sdk"])) .returns(["build", "--sdk", "/path/to/sdk", "--replace-scm-with-registry"]); const task = createSwiftTask( ["build"], diff --git a/test/unit-tests/toolchain/BuildFlags.test.ts b/test/unit-tests/toolchain/BuildFlags.test.ts index aba7b7f40..628c9e455 100644 --- a/test/unit-tests/toolchain/BuildFlags.test.ts +++ b/test/unit-tests/toolchain/BuildFlags.test.ts @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import * as path from "path"; import { expect } from "chai"; import { DarwinCompatibleTarget, SwiftToolchain } from "../../../src/toolchain/toolchain"; import { ArgumentFilter, BuildFlags } from "../../../src/toolchain/BuildFlags"; @@ -24,6 +25,8 @@ suite("BuildFlags Test Suite", () => { let mockedToolchain: MockedObject; let buildFlags: BuildFlags; + const sandboxConfig = mockGlobalValue(configuration, "disableSandbox"); + suiteSetup(async () => { mockedToolchain = mockObject({ swiftVersion: new Version(6, 0, 0), @@ -33,6 +36,7 @@ suite("BuildFlags Test Suite", () => { setup(() => { mockedPlatform.setValue("darwin"); + sandboxConfig.setValue(false); }); suite("getDarwinTarget", () => { @@ -201,51 +205,57 @@ suite("BuildFlags Test Suite", () => { expect( BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false) - ).to.equal(".build"); + ).to.equal(path.normalize(".build")); expect( BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true) - ).to.equal("/some/full/workspace/test/path/.build"); + ).to.equal(path.normalize("/some/full/workspace/test/path/.build")); }); test("absolute configuration provided", () => { - buildPathConfig.setValue("/some/other/full/test/path"); + buildPathConfig.setValue(path.normalize("/some/other/full/test/path")); expect( - BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false) - ).to.equal("/some/other/full/test/path"); + BuildFlags.buildDirectoryFromWorkspacePath( + path.normalize("/some/full/workspace/test/path"), + false + ) + ).to.equal(path.normalize("/some/other/full/test/path")); expect( - BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true) - ).to.equal("/some/other/full/test/path"); + BuildFlags.buildDirectoryFromWorkspacePath( + path.normalize("/some/full/workspace/test/path"), + true + ) + ).to.equal(path.normalize("/some/other/full/test/path")); }); test("relative configuration provided", () => { - buildPathConfig.setValue("some/relative/test/path"); + buildPathConfig.setValue(path.normalize("some/relative/test/path")); expect( BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", false) - ).to.equal("some/relative/test/path"); + ).to.equal(path.normalize("some/relative/test/path")); expect( BuildFlags.buildDirectoryFromWorkspacePath("/some/full/workspace/test/path", true) - ).to.equal("/some/full/workspace/test/path/some/relative/test/path"); + ).to.equal(path.normalize("/some/full/workspace/test/path/some/relative/test/path")); }); }); - suite("withSwiftSDKFlags", () => { + suite("withAdditionalFlags", () => { const sdkConfig = mockGlobalValue(configuration, "sdk"); test("package", () => { for (const sub of ["dump-symbol-graph", "diagnose-api-breaking-changes", "resolve"]) { sdkConfig.setValue(""); expect( - buildFlags.withSwiftSDKFlags(["package", sub, "--disable-sandbox"]) + buildFlags.withAdditionalFlags(["package", sub, "--disable-sandbox"]) ).to.deep.equal(["package", sub, "--disable-sandbox"]); sdkConfig.setValue("/some/full/path/to/sdk"); expect( - buildFlags.withSwiftSDKFlags(["package", sub, "--disable-sandbox"]) + buildFlags.withAdditionalFlags(["package", sub, "--disable-sandbox"]) ).to.deep.equal([ "package", sub, @@ -256,25 +266,36 @@ suite("BuildFlags Test Suite", () => { } sdkConfig.setValue(""); - expect( - buildFlags.withSwiftSDKFlags(["package", "init", "--disable-sandbox"]) - ).to.deep.equal(["package", "init", "--disable-sandbox"]); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "init", + ]); sdkConfig.setValue("/some/full/path/to/sdk"); - expect( - buildFlags.withSwiftSDKFlags(["package", "init", "--disable-sandbox"]) - ).to.deep.equal(["package", "init", "--disable-sandbox"]); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "init", + ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["package", "init"])).to.deep.equal([ + "package", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + "init", + ]); }); test("build", () => { sdkConfig.setValue(""); expect( - buildFlags.withSwiftSDKFlags(["build", "--target", "MyExecutable"]) + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) ).to.deep.equal(["build", "--target", "MyExecutable"]); sdkConfig.setValue("/some/full/path/to/sdk"); expect( - buildFlags.withSwiftSDKFlags(["build", "--target", "MyExecutable"]) + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) ).to.deep.equal([ "build", "--sdk", @@ -282,17 +303,31 @@ suite("BuildFlags Test Suite", () => { "--target", "MyExecutable", ]); + + sandboxConfig.setValue(true); + expect( + buildFlags.withAdditionalFlags(["build", "--target", "MyExecutable"]) + ).to.deep.equal([ + "build", + "--sdk", + "/some/full/path/to/sdk", + "--target", + "MyExecutable", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); }); test("run", () => { sdkConfig.setValue(""); expect( - buildFlags.withSwiftSDKFlags(["run", "--product", "MyExecutable"]) + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) ).to.deep.equal(["run", "--product", "MyExecutable"]); sdkConfig.setValue("/some/full/path/to/sdk"); expect( - buildFlags.withSwiftSDKFlags(["run", "--product", "MyExecutable"]) + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) ).to.deep.equal([ "run", "--sdk", @@ -300,32 +335,70 @@ suite("BuildFlags Test Suite", () => { "--product", "MyExecutable", ]); + + sandboxConfig.setValue(true); + expect( + buildFlags.withAdditionalFlags(["run", "--product", "MyExecutable"]) + ).to.deep.equal([ + "run", + "--sdk", + "/some/full/path/to/sdk", + "--product", + "MyExecutable", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); }); test("test", () => { sdkConfig.setValue(""); - expect(buildFlags.withSwiftSDKFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ "test", "--filter", "MyTests", ]); sdkConfig.setValue("/some/full/path/to/sdk"); - expect(buildFlags.withSwiftSDKFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ "test", "--sdk", "/some/full/path/to/sdk", "--filter", "MyTests", ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["test", "--filter", "MyTests"])).to.deep.equal([ + "test", + "--sdk", + "/some/full/path/to/sdk", + "--filter", + "MyTests", + "--disable-sandbox", + "-Xswiftc", + "-disable-sandbox", + ]); }); test("other commands", () => { sdkConfig.setValue(""); - expect(buildFlags.withSwiftSDKFlags(["help", "repl"])).to.deep.equal(["help", "repl"]); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); sdkConfig.setValue("/some/full/path/to/sdk"); - expect(buildFlags.withSwiftSDKFlags(["help", "repl"])).to.deep.equal(["help", "repl"]); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); + + sandboxConfig.setValue(true); + expect(buildFlags.withAdditionalFlags(["help", "repl"])).to.deep.equal([ + "help", + "repl", + ]); }); }); diff --git a/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts index c87d77a4c..490bca449 100644 --- a/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts +++ b/test/unit-tests/toolchain/SelectedXcodeWatcher.test.ts @@ -16,11 +16,20 @@ import * as vscode from "vscode"; import { expect } from "chai"; import { SelectedXcodeWatcher } from "../../../src/toolchain/SelectedXcodeWatcher"; import { SwiftOutputChannel } from "../../../src/ui/SwiftOutputChannel"; -import { instance, MockedObject, mockFn, mockGlobalObject, mockObject } from "../../MockUtils"; +import { + instance, + MockedObject, + mockFn, + mockGlobalObject, + mockGlobalValue, + mockObject, +} from "../../MockUtils"; +import configuration from "../../../src/configuration"; suite("Selected Xcode Watcher", () => { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); let mockOutputChannel: MockedObject; + const pathConfig = mockGlobalValue(configuration, "path"); setup(function () { // Xcode only exists on macOS, so the SelectedXcodeWatcher is macOS-only. @@ -31,6 +40,8 @@ suite("Selected Xcode Watcher", () => { mockOutputChannel = mockObject({ appendLine: mockFn(), }); + + pathConfig.setValue(""); }); async function run(symLinksOnCallback: (string | undefined)[]) { @@ -72,4 +83,12 @@ suite("Selected Xcode Watcher", () => { "Reload Extensions" ); }); + + test("Ignores when path is explicitly set", async () => { + pathConfig.setValue("/path/to/swift/bin"); + + await run(["/foo", "/bar"]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.not.been.called; + }); }); diff --git a/test/unit-tests/toolchain/ToolchainVersion.test.ts b/test/unit-tests/toolchain/ToolchainVersion.test.ts new file mode 100644 index 000000000..288159788 --- /dev/null +++ b/test/unit-tests/toolchain/ToolchainVersion.test.ts @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2025 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import { expect } from "chai"; +import { ToolchainVersion } from "../../../src/toolchain/ToolchainVersion"; + +suite("ToolchainVersion Unit Test Suite", () => { + test("Parses snapshot", () => { + const version = ToolchainVersion.parse("main-snapshot-2025-03-28"); + expect(version.identifier).to.equal("swift-DEVELOPMENT-SNAPSHOT-2025-03-28-a"); + }); + + test("Parses release snapshot", () => { + const version = ToolchainVersion.parse("6.0-snapshot-2025-03-28"); + expect(version.identifier).to.equal("swift-6.0-DEVELOPMENT-SNAPSHOT-2025-03-28-a"); + }); + + test("Parses stable", () => { + const version = ToolchainVersion.parse("6.0.3"); + expect(version.identifier).to.equal("swift-6.0.3-RELEASE"); + }); +}); diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts index cc491fc7f..5b1af995a 100644 --- a/test/unit-tests/toolchain/toolchain.test.ts +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; +import * as path from "path"; import * as mockFS from "mock-fs"; import * as utilities from "../../../src/utilities/utilities"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; @@ -78,7 +79,9 @@ suite("SwiftToolchain Unit Test Suite", () => { }); await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( - "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin/lldb-dap" + path.normalize( + "/Library/Developer/Toolchains/swift-6.0.1-RELEASE.xctoolchain/usr/bin/lldb-dap" + ) ); }); @@ -174,7 +177,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }); await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( - "/toolchains/swift-6.0.0/usr/bin/lldb-dap" + path.normalize("/toolchains/swift-6.0.0/usr/bin/lldb-dap") ); }); @@ -213,7 +216,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }); await expect(sut.getLLDBDebugAdapter()).to.eventually.equal( - "/toolchains/swift-6.0.0/usr/bin/lldb-dap.exe" + path.normalize("/toolchains/swift-6.0.0/usr/bin/lldb-dap.exe") ); }); diff --git a/test/unit-tests/ui/PackageDependencyProvider.test.ts b/test/unit-tests/ui/PackageDependencyProvider.test.ts index aa5871abc..585e31045 100644 --- a/test/unit-tests/ui/PackageDependencyProvider.test.ts +++ b/test/unit-tests/ui/PackageDependencyProvider.test.ts @@ -13,8 +13,11 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; +import * as path from "path"; import * as vscode from "vscode"; -import { FileNode, PackageNode } from "../../../src/ui/PackageDependencyProvider"; +import * as fs from "fs/promises"; +import { FileNode, PackageNode } from "../../../src/ui/ProjectPanelProvider"; +import { mockGlobalModule } from "../../MockUtils"; suite("PackageDependencyProvider Unit Test Suite", function () { suite("FileNode", () => { @@ -46,11 +49,15 @@ suite("PackageDependencyProvider Unit Test Suite", function () { suite("PackageNode", () => { test("can create a VSCode TreeItem that represents a Swift package", () => { const node = new PackageNode( - "SwiftMarkdown", - "/path/to/.build/swift-markdown", - "https://github.com/swiftlang/swift-markdown.git", - "1.2.3", - "remote" + { + identity: "SwiftMarkdown", + path: "/path/to/.build/swift-markdown", + location: "https://github.com/swiftlang/swift-markdown.git", + dependencies: [], + version: "1.2.3", + type: "remote", + }, + () => [] ); const item = node.toTreeItem(); @@ -58,5 +65,53 @@ suite("PackageDependencyProvider Unit Test Suite", function () { expect(item.description).to.deep.equal("1.2.3"); expect(item.command).to.be.undefined; }); + + const fsMock = mockGlobalModule(fs); + + test("enumerates child dependencies and files", async () => { + fsMock.readdir.resolves(["file1", "file2"] as any); + fsMock.stat.resolves({ isFile: () => true, isDirectory: () => false } as any); + + const node = new PackageNode( + { + identity: "SwiftMarkdown", + path: "/path/to/.build/swift-markdown", + location: "https://github.com/swiftlang/swift-markdown.git", + dependencies: [], + version: "1.2.3", + type: "remote", + }, + () => [ + { + identity: "SomeChildDependency", + path: "/path/to/.build/child-dependency", + location: "https://github.com/swiftlang/some-child-dependency.git", + dependencies: [], + version: "1.2.4", + type: "remote", + }, + ] + ); + + const children = await node.getChildren(); + + expect(children).to.have.lengthOf(3); + const [childDep, ...childFiles] = children; + expect(childDep.name).to.equal("SomeChildDependency"); + expect(childFiles).to.deep.equal([ + new FileNode( + "file1", + path.normalize("/path/to/.build/swift-markdown/file1"), + false, + "SwiftMarkdown-1.2.3" + ), + new FileNode( + "file2", + path.normalize("/path/to/.build/swift-markdown/file2"), + false, + "SwiftMarkdown-1.2.3" + ), + ]); + }); }); }); diff --git a/test/unit-tests/ui/ReloadExtension.test.ts b/test/unit-tests/ui/ReloadExtension.test.ts index 422bfce23..8c539b062 100644 --- a/test/unit-tests/ui/ReloadExtension.test.ts +++ b/test/unit-tests/ui/ReloadExtension.test.ts @@ -11,15 +11,24 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import { beforeEach } from "mocha"; import { expect } from "chai"; import { mockGlobalObject } from "../../MockUtils"; import * as vscode from "vscode"; -import { showReloadExtensionNotification } from "../../../src/ui/ReloadExtension"; +import { showReloadExtensionNotificationInstance } from "../../../src/ui/ReloadExtension"; import { Workbench } from "../../../src/utilities/commands"; suite("showReloadExtensionNotification()", async function () { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); const mockedVSCodeCommands = mockGlobalObject(vscode, "commands"); + let showReloadExtensionNotification: ( + message: string, + ...items: string[] + ) => Promise; + + beforeEach(() => { + showReloadExtensionNotification = showReloadExtensionNotificationInstance(); + }); test("displays a warning message asking the user if they would like to reload the window", async () => { mockedVSCodeWindow.showWarningMessage.resolves(undefined); @@ -57,4 +66,18 @@ suite("showReloadExtensionNotification()", async function () { ); expect(mockedVSCodeCommands.executeCommand).to.not.have.been.called; }); + + test("only shows one dialog at a time", async () => { + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + + await Promise.all([ + showReloadExtensionNotification("Want to reload?"), + showReloadExtensionNotification("Want to reload?"), + ]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions" + ); + }); }); diff --git a/test/unit-tests/utilities/filesystem.test.ts b/test/unit-tests/utilities/filesystem.test.ts index 477b1abf1..4bd084372 100644 --- a/test/unit-tests/utilities/filesystem.test.ts +++ b/test/unit-tests/utilities/filesystem.test.ts @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import * as path from "path"; import { isPathInsidePath, expandFilePathTilde } from "../../../src/utilities/filesystem"; import { expect } from "chai"; @@ -30,7 +31,7 @@ suite("File System Utilities Unit Test Suite", () => { suite("expandFilePathTilde", () => { test("expands tilde", () => { expect(expandFilePathTilde("~/Test", "/Users/John", "darwin")).to.equal( - "/Users/John/Test" + path.normalize("/Users/John/Test") ); }); diff --git a/test/unit-tests/utilities/workspace.test.ts b/test/unit-tests/utilities/workspace.test.ts new file mode 100644 index 000000000..7ca500fc0 --- /dev/null +++ b/test/unit-tests/utilities/workspace.test.ts @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import { searchForPackages } from "../../../src/utilities/workspace"; +import { testAssetUri } from "../../fixtures"; +import { expect } from "chai"; + +suite("Workspace Utilities Unit Test Suite", () => { + suite("searchForPackages", () => { + const packageFolder = testAssetUri("ModularPackage"); + const firstModuleFolder = vscode.Uri.joinPath(packageFolder, "Module1"); + const secondModuleFolder = vscode.Uri.joinPath(packageFolder, "Module2"); + + test("returns only root package when search for subpackages disabled", async () => { + const folders = await searchForPackages(packageFolder, false, false); + + expect(folders.map(folder => folder.fsPath)).eql([packageFolder.fsPath]); + }); + + test("returns subpackages when search for subpackages enabled", async () => { + const folders = await searchForPackages(packageFolder, false, true); + + expect(folders.map(folder => folder.fsPath).sort()).deep.equal([ + packageFolder.fsPath, + firstModuleFolder.fsPath, + secondModuleFolder.fsPath, + ]); + }); + }); +}); diff --git a/test/utilities/tasks.ts b/test/utilities/tasks.ts index dcd72f31a..688c6636c 100644 --- a/test/utilities/tasks.ts +++ b/test/utilities/tasks.ts @@ -109,7 +109,9 @@ export function waitForNoRunningTasks(options?: { timeout: number }): Promise e.task.name); reject( - `Timed out waiting for tasks to complete. The following ${runningTasks.length} tasks are still running: ${runningTasks}.` + new Error( + `Timed out waiting for tasks to complete. The following ${runningTasks.length} tasks are still running: ${runningTasks}.` + ) ); }, options.timeout); } diff --git a/userdocs/userdocs.docc/automatic-task-creation.md b/userdocs/userdocs.docc/automatic-task-creation.md new file mode 100644 index 000000000..1df207c9f --- /dev/null +++ b/userdocs/userdocs.docc/automatic-task-creation.md @@ -0,0 +1,13 @@ +# Automatic Task Creation + +vscode-swift automatically adds tasks for common operations with your Package. + +> Tip: Tasks use workflows common to all VSCode extensions. For more information see https://code.visualstudio.com/docs/editor/tasks + +For workspaces that contain a **Package.swift** file, this extension will add the following tasks: + +- **Build All**: Build all targets in the Package +- **Build Debug **: Each executable in a Package.swift get a task for building a debug build +- **Build Release **: Each executable in a Package.swift get a task for building a release build + +These tasks are available via **Terminal ▸ Run Task...** and **Terminal ▸ Run Build Task...**. \ No newline at end of file diff --git a/userdocs/userdocs.docc/commands.md b/userdocs/userdocs.docc/commands.md new file mode 100644 index 000000000..0c5e936d1 --- /dev/null +++ b/userdocs/userdocs.docc/commands.md @@ -0,0 +1,56 @@ +# Commands + +vscode-swift adds various commands to Visual Studio Code. + +The extension adds the following commands, available via the command palette. + +#### Configuration + +- **Create New Project...**: Create a new Swift project using a template. This opens a dialog to guide you through creating a new project structure. +- **Create New Swift File...**: Create a new `.swift` file in the current workspace. +- **Select Toolchain**: Select the locally installed Swift toolchain (including Xcode toolchains on macOS) that you want to use Swift tools from. + +The following command is only available on macOS: + +- **Select Target Platform**: This is an experimental command that offers code completion for iOS and tvOS projects. + +#### Building and Debugging + +- **Run Build**: Run `swift build` for the package associated with the open file. +- **Debug Build**: Run `swift build` with debugging enabled for the package associated with the open file, launching the binary and attaching the debugger. +- **Attach to Process...**: Attach the debugger to an already running process for debugging. +- **Clean Build Folder**: Clean the build folder for the package associated with the open file, removing all previously built products. + +#### Dependency Management + +- **Resolve Package Dependencies**: Run `swift package resolve` on packages associated with the open file. +- **Update Package Dependencies**: Run `swift package update` on packages associated with the open file. +- **Reset Package Dependencies**: Run `swift package reset` on packages associated with the open file. +- **Add to Workspace**: Add the current package to the active workspace in VS Code. +- **Clean Build**: Run `swift package clean` on packages associated with the open file. +- **Open Package.swift**: Open `Package.swift` for the package associated with the open file. +- **Use Local Version**: Switch the package dependency to use a local version of the package instead of the remote repository version. +- **Edit Locally**: Make the package dependency editable locally, allowing changes to the dependency to be reflected immediately. +- **Revert To Original Version**: Revert the package dependency to its original, unedited state after local changes have been made. +- **View Repository**: Open the external repository of the selected Swift package in a browser. + +#### Testing + +- **Test: Run All Tests**: Run all the tests across all test targes in the open project. +- **Test: Rerun Last Run**: Perform the last test run again. +- **Test: Open Coverage**: Open the last generated coverage report, if one exists. +- **Test: Run All Tests in Parallel**: Run all tests in parallel. This action only affects XCTests. Swift-testing tests are parallel by default, and their parallelism [is controlled in code](https://developer.apple.com/documentation/testing/parallelization). + +#### Snippets and Scripts + +- **Insert Function Comment**: Insert a standard comment block for documenting a Swift function in the current file. +- **Run Swift Script**: Run the currently open file, as a Swift script. The file must not be part of a build target. If the file has not been saved it will save it to a temporary file so it can be run. +- **Run Swift Snippet**: If the currently open file is a Swift snippet then run it. +- **Debug Swift Snippet**: If the currently open file is a Swift snippet then debug it. + +#### Diagnostics + +- **Capture VS Code Swift Diagnostic Bundle**: Capture a diagnostic bundle from VS Code, containing logs and information to aid in troubleshooting Swift-related issues. +- **Clear Diagnostics Collection**: Clear all collected diagnostics in the current workspace to start fresh. +- **Restart LSP Server**: Restart the Swift Language Server Protocol (LSP) server for the current workspace. +- **Re-Index Project**: Force a re-index of the project to refresh code completion and symbol navigation support. \ No newline at end of file diff --git a/docs/images/coverage-render.png b/userdocs/userdocs.docc/coverage-render.png similarity index 100% rename from docs/images/coverage-render.png rename to userdocs/userdocs.docc/coverage-render.png diff --git a/docs/images/coverage-report.png b/userdocs/userdocs.docc/coverage-report.png similarity index 100% rename from docs/images/coverage-report.png rename to userdocs/userdocs.docc/coverage-report.png diff --git a/docs/images/coverage-run.png b/userdocs/userdocs.docc/coverage-run.png similarity index 100% rename from docs/images/coverage-run.png rename to userdocs/userdocs.docc/coverage-run.png diff --git a/userdocs/userdocs.docc/debugging.md b/userdocs/userdocs.docc/debugging.md new file mode 100644 index 000000000..fd8dd77e1 --- /dev/null +++ b/userdocs/userdocs.docc/debugging.md @@ -0,0 +1,11 @@ +# Debugging + +vscode-swift allows you to debug your Swift packages. + +> Tip: Debugging works best when using a version of the Swift toolchain 6.0 or higher + +When you open a Swift package (a directory containing a `Package.swift` file), the extension automatically generates build tasks and launch configurations for each executable within the package. Additionally, if the package includes tests, the extension creates a configuration specifically designed to run those tests. These configurations all leverage the CodeLLDB extension as the debugger of choice. + +Use the **Run > Start Debugging** menu item to run an executable and start debugging. If you have multiple launch configurations you can choose which launch configuration to use in the debugger view. + +Debugging uses workflows common to all VSCode extensions. For more information see https://code.visualstudio.com/docs/editor/debugging \ No newline at end of file diff --git a/images/install-extension.png b/userdocs/userdocs.docc/install-extension.png similarity index 100% rename from images/install-extension.png rename to userdocs/userdocs.docc/install-extension.png diff --git a/userdocs/userdocs.docc/install-pre-release.png b/userdocs/userdocs.docc/install-pre-release.png new file mode 100644 index 000000000..d15c36905 Binary files /dev/null and b/userdocs/userdocs.docc/install-pre-release.png differ diff --git a/userdocs/userdocs.docc/installation.md b/userdocs/userdocs.docc/installation.md new file mode 100644 index 000000000..d45dec611 --- /dev/null +++ b/userdocs/userdocs.docc/installation.md @@ -0,0 +1,7 @@ +# Installation + +vscode-code Swift is installed through the extension marketplace. + +The Swift extension is supported on macOS, Linux, and Windows. + +To install, firstly ensure you have [Swift installed on your system](https://www.swift.org/install/). Then [install the Swift extension](https://marketplace.visualstudio.com/items?itemName=swiftlang.swift-vscode). Once your machine is ready, you can get started with the **Swift: Create New Project...** command. diff --git a/userdocs/userdocs.docc/language-features.md b/userdocs/userdocs.docc/language-features.md new file mode 100644 index 000000000..f36d2a172 --- /dev/null +++ b/userdocs/userdocs.docc/language-features.md @@ -0,0 +1,5 @@ +# Language Features + +vscode-swift provides various language features to help you write Swift code. + +The extension provides language features such as code completion and jump to definition via [SourceKit-LSP](https://github.com/apple/sourcekit-lsp). To ensure the extension functions correctly, it’s important to first build the project so that SourceKit-LSP has access to all the symbol data. Whenever you add a new dependency to your project, make sure to rebuild it so that SourceKit-LSP can update its information. \ No newline at end of file diff --git a/images/package-dependencies.png b/userdocs/userdocs.docc/package-dependencies.png similarity index 100% rename from images/package-dependencies.png rename to userdocs/userdocs.docc/package-dependencies.png diff --git a/userdocs/userdocs.docc/project-view.md b/userdocs/userdocs.docc/project-view.md new file mode 100644 index 000000000..2d4b98e07 --- /dev/null +++ b/userdocs/userdocs.docc/project-view.md @@ -0,0 +1,9 @@ +# Project View + +vscode-swift provides project view + +If your workspace contains a package, this extension will add a **Swift Project** view to the Explorer: + +![](package-dependencies.png) + +Additionally, the extension will monitor `Package.swift` and `Package.resolved` for changes, resolve any changes to the dependencies, and update the view as needed. diff --git a/docs/remote-dev.md b/userdocs/userdocs.docc/remote-dev.md similarity index 98% rename from docs/remote-dev.md rename to userdocs/userdocs.docc/remote-dev.md index 195926a3e..8964062a1 100644 --- a/docs/remote-dev.md +++ b/userdocs/userdocs.docc/remote-dev.md @@ -1,5 +1,7 @@ # Visual Studio Code Dev Containers +Dev containers can be used as an easy way to develop when building for other platforms. + [VS Code Dev Containers](https://code.visualstudio.com/docs/remote/containers) allows you to run your code and environment in a container. This is especially useful for Swift when developing on macOS and deploying to Linux. You can ensure there are no compatibility issues in Foundation when running your code. The extension also works with [GitHub Codespaces](https://github.com/features/codespaces) to allow you to write your code on the web. ## Requirements diff --git a/docs/settings.md b/userdocs/userdocs.docc/settings.md similarity index 58% rename from docs/settings.md rename to userdocs/userdocs.docc/settings.md index 60b67aa92..8f60ba69b 100644 --- a/docs/settings.md +++ b/userdocs/userdocs.docc/settings.md @@ -1,14 +1,16 @@ # Extension Settings +vscode-swift provides various settings to configure its behaviour. + The Visual Studio Code Swift extension comes with a number of settings you can use to control how it works. Detailed descriptions of each setting is provided in the extension settings page. This document outlines useful configuration options not covered by the settings descriptions in the extension settings page. ## Command Plugins -Swift packages can define [command plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) that can perform arbitrary tasks. For example, the [swift-format](https://github.com/swiftlang/swift-format) package exposes a `format-source-code` command which will use swift-format to format source code in a folder. These plugin commands can be invoked from VS Code using `> Swift: Run Command Plugin`. +Swift packages can define [command plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) that can perform arbitrary tasks. For example, the [swift-format](https://github.com/swiftlang/swift-format) package exposes a `format-source-code` command which will use swift-format to format source code in a folder. These plugin commands can be invoked from VS Code using `> Swift: Run Command Plugin`. -A plugin may require permissions to perform tasks like writing to the file system or using the network. If a plugin command requires one of these permissions, you will be prompted in the integrated terminal to accept them. If you trust the command and wish to apply permissions on every command execution, you can configure a setting in your `settings.json`. +A plugin may require permissions to perform tasks like writing to the file system or using the network. If a plugin command requires one of these permissions, you will be prompted in the integrated terminal to accept them. If you trust the command and wish to apply permissions on every command execution, you can configure the [`swift.pluginPermissions`](vscode://settings/swift.pluginPermissions) setting in your `settings.json`. ```json { @@ -23,7 +25,7 @@ A plugin may require permissions to perform tasks like writing to the file syste } ``` -A key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin. +A key of `PluginName:command` will set permissions for a specific command. A key of `PluginName` will set permissions for all commands in the plugin. If you'd like the same permissions to be applied to all plugins use `*` as the plugin name. Precedence order is determined by specificity, where more specific names take priority. The name `*` is the least specific and `PluginName:command` is the most specific. Alternatively, you can define a task in your tasks.json and define permissions directly on the task. This will create a new entry in the list shown by `> Swift: Run Command Plugin`. @@ -43,6 +45,24 @@ Alternatively, you can define a task in your tasks.json and define permissions d } ``` +If you'd like to provide specific arguments to your plugin command invocation you can use the `swift.pluginArguments` setting. Defining an array for this setting applies the same arguments to all plugin command invocations. + +```json +{ + "swift.pluginArguments": ["-c", "release"] +} +``` + +Alternatively you can specfiy which specific command the arguments should apply to using `PluginName:command`. A key of `PluginName` will use the arguments for all commands in the plugin. If you'd like the same arguments to be used for all plugins use `*` as the plugin name. + +```json +{ + "swift.pluginArguments": { + "PluginName:command": ["-c", "release"] + } +} +``` + ## SourceKit-LSP [SourceKit-LSP](https://github.com/apple/sourcekit-lsp) is the language server used by the the Swift extension to provide symbol completion, jump to definition etc. It is developed by Apple to provide Swift and C language support for any editor that supports the Language Server Protocol. @@ -51,17 +71,25 @@ Alternatively, you can define a task in your tasks.json and define permissions d If you're using a nightly (`main`) or recent `6.0` toolchain you can enable support for background indexing in Sourcekit-LSP. This removes the need to do a build before getting code completion and diagnostics. -To enable support, set the `swift.sourcekit-lsp.backgroundIndexing` setting to `true`. +To enable support, set the [`swift.sourcekit-lsp.backgroundIndexing`](vscode://settings/swift.sourcekit-lsp.backgroundIndexing) setting to `true`. ### Support for 'Expand Macro' If you are using a nightly (`main`) toolchain you can enable support for the "Peek Macro" Quick Action, accessible through the light bulb icon when the cursor is on a macro. -To enable support, set the following Sourcekit-LSP server arguments in your settings.json, or add two new entries to the `Sourcekit-lsp: Server Arguments` entry in the extension settings page. +To enable support, set the following Sourcekit-LSP server arguments in your settings.json, or add two new entries to the [`swift.sourcekit-lsp.serverArguments`](vscode://settings/swift.sourcekit-lsp.serverArguments) setting. -``` +```json "swift.sourcekit-lsp.serverArguments": [ "--experimental-feature", "show-macro-expansions" ] ``` + +## Windows Development + +### Specifying a Visual Studio installation + +Swift depends on a number of developer tools when running on Windows, including the C++ toolchain and the Windows SDK. Typically these are installed with [Visual Studio](https://visualstudio.microsoft.com/). + +If you have multiple versions of Visual Studio installed you can specify the path to the desired version by setting a `VCToolsInstallDir` environment variable using the [`swift.swiftEnvironmentVariables`](vscode://settings/swift.swiftEnvironmentVariables) setting. diff --git a/userdocs/userdocs.docc/supported-toolchains.md b/userdocs/userdocs.docc/supported-toolchains.md new file mode 100644 index 000000000..b2eb13cfb --- /dev/null +++ b/userdocs/userdocs.docc/supported-toolchains.md @@ -0,0 +1,18 @@ +# Supported Toolchains + +vscode-swift supports several versions of the Swift toolchain. + +vscode-swift supports the following Swift toolchains: + * 5.9 + * 5.10 + * 6.0 + * 6.1 + +The extension also strives to work with the latest nightly toolchains built from the main branch. + +Certain features of vscode-swift will only work with newer versions of the toolchains. We recommend using the latest version of the Swift toolchain to get the most benefit of the extension. The following features only work with certain toolchains as listed: + +Feature | Minimum Toolchain Required +------------------------ | ------------------------------------- +lldb-dap debugging | 6.0 + diff --git a/docs/test-coverage.md b/userdocs/userdocs.docc/test-coverage.md similarity index 89% rename from docs/test-coverage.md rename to userdocs/userdocs.docc/test-coverage.md index 167f1f043..5620a36c7 100644 --- a/docs/test-coverage.md +++ b/userdocs/userdocs.docc/test-coverage.md @@ -1,15 +1,17 @@ # Test Coverage +vscode-swift provides mechanisms to see coverage of your tests. + Test coverage is a measurement of how much of your code is tested by your tests. It defines how many lines of code were actually run when you ran your tests and how many were not. When a line of code is not run by your tests it will not have been tested and perhaps you need to extend your tests. The Swift extension integrates with VS Code's Code Coverage APIs to record what code has been hit or missed by your tests. -![](images/coverage-run.png) +![](coverage-run.png) Once you've performed a code coverage run a coverage report will be displayed in a section of the primary side bar. This report lists all the source files in your project and what percentage of lines were hit by tests. You can click on each file to open that file in the code editor. If you close the report you can always get it back by running the command `Test: Open Coverage`. -![](images/coverage-report.png) +![](coverage-report.png) After generating code coverage lines numbers in covered files will be coloured red or green depending on if they ran during the test run. Hovering over the line numbers shows how many times each line was run. Hitting the "Toggle Inline Coverage" link that appears when hovering over the line numbers will keep this information visible. -![](images/coverage-render.png) +![](coverage-render.png) diff --git a/userdocs/userdocs.docc/test-explorer.md b/userdocs/userdocs.docc/test-explorer.md new file mode 100644 index 000000000..8d585dbcf --- /dev/null +++ b/userdocs/userdocs.docc/test-explorer.md @@ -0,0 +1,9 @@ +# Test Explorer + +vscode-swift shows test results in the test explorer. + +If your package contains tests then they can be viewed, run and debugged in the Test Explorer. + +![](test-explorer.png) + +Once your project is built, the Test Explorer will list all your tests. These tests are grouped by package, then test target, and finally, by XCTestCase class. From the Test Explorer, you can initiate a test run, debug a test run, and if a file has already been opened, you can jump to the source code for a test. diff --git a/images/test-explorer.png b/userdocs/userdocs.docc/test-explorer.png similarity index 100% rename from images/test-explorer.png rename to userdocs/userdocs.docc/test-explorer.png diff --git a/userdocs/userdocs.docc/userdocs.md b/userdocs/userdocs.docc/userdocs.md new file mode 100644 index 000000000..983fa99dc --- /dev/null +++ b/userdocs/userdocs.docc/userdocs.md @@ -0,0 +1,40 @@ +# vscode-swift + +@Metadata { + @TechnologyRoot +} + +Language support for Swift in Visual Studio Code. + +This extension adds language support for Swift to Visual Studio Code, providing a seamless experience for developing Swift applications on platforms such as macOS, Linux and Windows. It supports: + +* Code completion +* Jump to definition, peek definition, find all references, symbol search +* Error annotations and apply suggestions from errors +* Automatic generation of launch configurations for debugging +* Automatic task creation +* Package dependency view +* Test Explorer view + +## Topics + +- +- + +### Features + +- +- +- +- +- +- + +### Advanced + +- + +### Reference + +- +- \ No newline at end of file