Repository: mason-org/mason.nvim Branch: main Commit: 44d1e90e1f66 Files: 225 Total size: 850.9 KB Directory structure: gitextract_j3t903i2/ ├── .cbfmt.toml ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── feature_request.yaml │ │ ├── general_issue.yaml │ │ ├── new_package_request.yaml │ │ ├── package_installation_form.yaml │ │ └── package_issue.yaml │ └── workflows/ │ ├── cbfmt.yml │ ├── release.yml │ ├── selene.yml │ ├── stylua.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PACKAGES.md ├── README.md ├── SECURITY.md ├── doc/ │ ├── .gitignore │ └── mason.txt ├── lua/ │ ├── mason/ │ │ ├── api/ │ │ │ └── command.lua │ │ ├── health.lua │ │ ├── init.lua │ │ ├── providers/ │ │ │ ├── client/ │ │ │ │ ├── gh.lua │ │ │ │ ├── golang.lua │ │ │ │ ├── init.lua │ │ │ │ ├── npm.lua │ │ │ │ ├── openvsx.lua │ │ │ │ ├── pypi.lua │ │ │ │ └── rubygems.lua │ │ │ └── registry-api/ │ │ │ └── init.lua │ │ ├── settings.lua │ │ ├── ui/ │ │ │ ├── colors.lua │ │ │ ├── components/ │ │ │ │ ├── header.lua │ │ │ │ ├── help/ │ │ │ │ │ ├── dap.lua │ │ │ │ │ ├── formatter.lua │ │ │ │ │ ├── init.lua │ │ │ │ │ ├── linter.lua │ │ │ │ │ └── lsp.lua │ │ │ │ ├── json-schema.lua │ │ │ │ ├── language-filter.lua │ │ │ │ ├── main/ │ │ │ │ │ ├── init.lua │ │ │ │ │ └── package_list.lua │ │ │ │ └── tabs.lua │ │ │ ├── init.lua │ │ │ ├── instance.lua │ │ │ └── palette.lua │ │ └── version.lua │ ├── mason-core/ │ │ ├── EventEmitter.lua │ │ ├── async/ │ │ │ ├── control.lua │ │ │ ├── init.lua │ │ │ └── uv.lua │ │ ├── fetch.lua │ │ ├── fs.lua │ │ ├── functional/ │ │ │ ├── data.lua │ │ │ ├── function.lua │ │ │ ├── init.lua │ │ │ ├── list.lua │ │ │ ├── logic.lua │ │ │ ├── number.lua │ │ │ ├── relation.lua │ │ │ ├── string.lua │ │ │ ├── table.lua │ │ │ └── type.lua │ │ ├── installer/ │ │ │ ├── InstallHandle.lua │ │ │ ├── InstallLocation.lua │ │ │ ├── InstallRunner.lua │ │ │ ├── UninstallRunner.lua │ │ │ ├── compiler/ │ │ │ │ ├── compilers/ │ │ │ │ │ ├── cargo.lua │ │ │ │ │ ├── composer.lua │ │ │ │ │ ├── gem.lua │ │ │ │ │ ├── generic/ │ │ │ │ │ │ ├── build.lua │ │ │ │ │ │ ├── download.lua │ │ │ │ │ │ └── init.lua │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── build.lua │ │ │ │ │ │ ├── init.lua │ │ │ │ │ │ └── release.lua │ │ │ │ │ ├── golang.lua │ │ │ │ │ ├── luarocks.lua │ │ │ │ │ ├── mason.lua │ │ │ │ │ ├── npm.lua │ │ │ │ │ ├── nuget.lua │ │ │ │ │ ├── opam.lua │ │ │ │ │ ├── openvsx.lua │ │ │ │ │ └── pypi.lua │ │ │ │ ├── expr.lua │ │ │ │ ├── init.lua │ │ │ │ ├── link.lua │ │ │ │ ├── schemas.lua │ │ │ │ └── util.lua │ │ │ ├── context/ │ │ │ │ ├── InstallContextCwd.lua │ │ │ │ ├── InstallContextFs.lua │ │ │ │ ├── InstallContextSpawn.lua │ │ │ │ └── init.lua │ │ │ ├── init.lua │ │ │ ├── linker.lua │ │ │ └── managers/ │ │ │ ├── cargo.lua │ │ │ ├── common.lua │ │ │ ├── composer.lua │ │ │ ├── gem.lua │ │ │ ├── golang.lua │ │ │ ├── luarocks.lua │ │ │ ├── npm.lua │ │ │ ├── nuget.lua │ │ │ ├── opam.lua │ │ │ ├── powershell.lua │ │ │ ├── pypi.lua │ │ │ └── std.lua │ │ ├── log.lua │ │ ├── notify.lua │ │ ├── optional.lua │ │ ├── package/ │ │ │ ├── AbstractPackage.lua │ │ │ └── init.lua │ │ ├── path.lua │ │ ├── pep440/ │ │ │ └── init.lua │ │ ├── platform.lua │ │ ├── process.lua │ │ ├── providers.lua │ │ ├── purl.lua │ │ ├── receipt.lua │ │ ├── result.lua │ │ ├── semver.lua │ │ ├── spawn.lua │ │ ├── terminator.lua │ │ └── ui/ │ │ ├── display.lua │ │ ├── init.lua │ │ └── state.lua │ ├── mason-registry/ │ │ ├── api.lua │ │ ├── index/ │ │ │ └── init.lua │ │ ├── init.lua │ │ ├── installer.lua │ │ └── sources/ │ │ ├── file.lua │ │ ├── github.lua │ │ ├── init.lua │ │ ├── lua.lua │ │ ├── synthesized.lua │ │ └── util.lua │ ├── mason-test/ │ │ └── helpers.lua │ └── mason-vendor/ │ ├── semver.lua │ └── zzlib/ │ ├── inflate-bit32.lua │ ├── inflate-bwo.lua │ └── init.lua ├── selene.toml ├── stylua.toml ├── tests/ │ ├── fixtures/ │ │ ├── purl-test-suite-data.json │ │ └── receipts/ │ │ ├── 1.0.json │ │ ├── 1.1.json │ │ └── 2.0.json │ ├── helpers/ │ │ └── lua/ │ │ ├── dummy-registry/ │ │ │ ├── dummy.lua │ │ │ ├── dummy2.lua │ │ │ ├── index.lua │ │ │ └── registry.lua │ │ └── luassertx.lua │ ├── mason/ │ │ ├── api/ │ │ │ └── command_spec.lua │ │ └── setup_spec.lua │ ├── mason-core/ │ │ ├── EventEmitter_spec.lua │ │ ├── async/ │ │ │ └── async_spec.lua │ │ ├── fetch_spec.lua │ │ ├── fs_spec.lua │ │ ├── functional/ │ │ │ ├── data_spec.lua │ │ │ ├── function_spec.lua │ │ │ ├── list_spec.lua │ │ │ ├── logic_spec.lua │ │ │ ├── number_spec.lua │ │ │ ├── relation_spec.lua │ │ │ ├── string_spec.lua │ │ │ ├── table_spec.lua │ │ │ └── type_spec.lua │ │ ├── installer/ │ │ │ ├── InstallHandle_spec.lua │ │ │ ├── InstallRunner_spec.lua │ │ │ ├── compiler/ │ │ │ │ ├── compiler_spec.lua │ │ │ │ ├── compilers/ │ │ │ │ │ ├── cargo_spec.lua │ │ │ │ │ ├── composer_spec.lua │ │ │ │ │ ├── gem_spec.lua │ │ │ │ │ ├── generic/ │ │ │ │ │ │ ├── build_spec.lua │ │ │ │ │ │ └── download_spec.lua │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── build_spec.lua │ │ │ │ │ │ └── release_spec.lua │ │ │ │ │ ├── golang_spec.lua │ │ │ │ │ ├── luarocks_spec.lua │ │ │ │ │ ├── npm_spec.lua │ │ │ │ │ ├── nuget_spec.lua │ │ │ │ │ ├── opam_spec.lua │ │ │ │ │ ├── openvsx_spec.lua │ │ │ │ │ └── pypi_spec.lua │ │ │ │ ├── expr_spec.lua │ │ │ │ ├── link_spec.lua │ │ │ │ └── util_spec.lua │ │ │ ├── context_spec.lua │ │ │ ├── linker_spec.lua │ │ │ └── managers/ │ │ │ ├── cargo_spec.lua │ │ │ ├── common_spec.lua │ │ │ ├── composer_spec.lua │ │ │ ├── gem_spec.lua │ │ │ ├── golang_spec.lua │ │ │ ├── luarocks_spec.lua │ │ │ ├── npm_spec.lua │ │ │ ├── nuget_spec.lua │ │ │ ├── opam_spec.lua │ │ │ ├── powershell_spec.lua │ │ │ ├── pypi_spec.lua │ │ │ └── std_spec.lua │ │ ├── optional_spec.lua │ │ ├── package/ │ │ │ └── package_spec.lua │ │ ├── path_spec.lua │ │ ├── pep440_spec.lua │ │ ├── platform_spec.lua │ │ ├── process_spec.lua │ │ ├── providers/ │ │ │ └── provider_spec.lua │ │ ├── purl_spec.lua │ │ ├── receipt_spec.lua │ │ ├── result_spec.lua │ │ ├── spawn_spec.lua │ │ ├── terminator_spec.lua │ │ └── ui_spec.lua │ ├── mason-registry/ │ │ ├── api_spec.lua │ │ ├── registry_spec.lua │ │ └── sources/ │ │ ├── collection_spec.lua │ │ └── lua_spec.lua │ └── minimal_init.vim └── vim.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cbfmt.toml ================================================ [languages] lua = ["stylua -s -"] sh = ["shellharden --transform "] ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace=true max_line_length = 120 charset = utf-8 [*.md] trim_trailing_whitespace=false ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [williamboman] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ --- contact_links: - name: Ask a question about mason.nvim or get support url: https://github.com/mason-org/mason.nvim/discussions/new?category=q-a about: Ask a question or request support for using mason.nvim ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ --- name: Feature request description: Suggest an idea for this project labels: - enhancement body: - type: checkboxes attributes: label: I've searched open issues for similar requests description: If possible, please contribute to any [open issues](https://github.com/mason-org/mason.nvim/issues) instead of opening a new one. options: - label: "Yes" - type: textarea attributes: label: Is your feature request related to a problem? Please describe. validations: required: true - type: textarea attributes: label: Describe the solution you'd like validations: required: true - type: textarea attributes: label: Describe potential alternatives you've considered - type: textarea attributes: label: Additional context ================================================ FILE: .github/ISSUE_TEMPLATE/general_issue.yaml ================================================ --- name: Non-package-related issue description: Report an issue not related to installation or usage of packages body: - type: markdown attributes: value: | # Issue reporting guidelines 1. This is not a general support board for package usage questions (e.g. "How do I do X?"). For questions, please refer to [the discussion board](https://github.com/mason-org/mason.nvim/discussions/categories/q-a) first! :) 1. Before reporting an issue, make sure that you meet the minimum requirements mentioned in the README. Also review `:checkhealth mason` for potential problems. --- - type: checkboxes attributes: label: I've searched open issues for similar requests description: If possible, please contribute to any [open issues](https://github.com/mason-org/mason.nvim/issues?q=is%3Aissue) instead of opening a new one. options: - label: "Yes" - type: checkboxes attributes: label: I've recently downloaded the latest plugin version of mason.nvim options: - label: "Yes" - type: textarea attributes: label: Problem description description: A clear and short description of 1) what the issue is, and 2) why you think it's an issue with mason.nvim. validations: required: true - type: textarea attributes: label: Expected behavior description: A short description of the behavior you expected. validations: required: true - type: textarea attributes: label: Steps to reproduce placeholder: | 1. ... 2. ... validations: required: true - type: textarea attributes: label: "Neovim version (>= 0.10.0)" description: "Output of `nvim --version`" placeholder: | NVIM v0.10.0-dev Build type: Release LuaJIT 2.1.0-beta3 validations: required: true - type: input attributes: label: "Operating system/version" description: "On Linux and Mac systems: `$ uname -a`" validations: required: true - type: textarea attributes: label: Healthcheck output placeholder: ":checkhealth mason" render: shell validations: required: true - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem ================================================ FILE: .github/ISSUE_TEMPLATE/new_package_request.yaml ================================================ --- name: New package request description: Request a new package not currently available title: "[New package]: " labels: - new-package-request body: - type: checkboxes attributes: label: I've searched open & closed issues for similar requests description: If possible, please contribute to [existing issues](https://github.com/mason-org/mason.nvim/issues?q=is%3Aissue+label%3Anew-package-request) instead of opening a new one. options: - label: "Yes" - type: input attributes: label: Package name description: Which package would you like to request to be added? validations: required: true - type: input attributes: label: Package homepage description: e.g., a GitHub page validations: required: true - type: input attributes: label: Languages description: Which languages does this package target? placeholder: typescript, javascript - type: textarea attributes: label: How is this package distributed? description: Is the package distributed through a standardized channel (such as GitHub release files, npm, pip, etc.)? Leave empty if you don't know. ================================================ FILE: .github/ISSUE_TEMPLATE/package_installation_form.yaml ================================================ --- name: Package installation issue description: Report an issue that occurs during the installation of a package labels: - installation-issue body: - type: markdown attributes: value: | # Issue reporting guidelines 1. Before reporting an issue, make sure that you meet the minimum requirements mentioned in the README. Also review `:checkhealth mason` for potential problems. 1. Please try to review the error yourself first and ensure it's not a problem that is local to your system only. --- - type: checkboxes attributes: label: I've searched open issues for similar requests description: If possible, please contribute to any [open issues](https://github.com/mason-org/mason.nvim/issues?q=is%3Aissue+label%3Ainstallation-issue) instead of opening a new one. options: - label: "Yes" - type: checkboxes attributes: label: I've recently downloaded the latest plugin version of mason.nvim options: - label: "Yes" - type: textarea attributes: label: Problem description description: A clear and short description of 1) what the issue is, and 2) why you think it's an issue with mason.nvim. validations: required: true - type: textarea attributes: label: Expected behavior description: A short description of the behavior you expected. validations: required: true - type: input attributes: label: Affected packages description: If this issue is specific to one or more packages, list them here. If not, write 'All'. validations: required: true - type: textarea attributes: label: Mason output description: Please provide the **installation output** available in the `:Mason` window, if possible. placeholder: "Please only provide the output of the package installation." render: Text - type: textarea attributes: label: Installation log description: "`:MasonLog`. Refer to `:help mason-debugging`" placeholder: "The default log level is not helpful for debugging purposes! Make sure you set the log level to DEBUG before installing the package (:h mason-debugging)." render: Text validations: required: true - type: textarea attributes: label: "Neovim version (>= 0.10.0)" description: "Output of `nvim --version`" placeholder: | NVIM v0.7.0-dev Build type: Release LuaJIT 2.1.0-beta3 validations: required: true - type: input attributes: label: "Operating system/version" description: "On Linux and Mac systems: `$ uname -a`" validations: required: true - type: textarea attributes: label: Healthcheck placeholder: ":checkhealth mason" render: Text validations: required: true - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem ================================================ FILE: .github/ISSUE_TEMPLATE/package_issue.yaml ================================================ --- name: Package issue description: Report an issue with using a package installed via mason.nvim labels: - package-issue body: - type: markdown attributes: value: | # Issue reporting guidelines 1. This is not a general support board for package usage questions (e.g. "How do I do X?"). For questions, please refer to [the discussion board](https://github.com/mason-org/mason.nvim/discussions/categories/q-a) first! :) 1. Before reporting an issue, make sure that you meet the minimum requirements mentioned in the README. Also review `:checkhealth mason` for potential problems. 1. General usage issues with packages should not be reported here. Please only report issues that you believe are a result of something that mason.nvim does. 1. Please try to review errors yourself first and ensure it's not a problem that is local to your system only. --- - type: checkboxes attributes: label: I've searched open issues for similar requests description: If possible, please contribute to any open issues instead of opening a new one. options: - label: "Yes" - type: checkboxes attributes: label: I've manually reviewed logs to find potential errors description: Logs such as `:MasonLog`, `:LspLog`, etc. (don't paste logs without reviewing them yourself first) options: - label: "Yes" - type: checkboxes attributes: label: I've recently downloaded the latest plugin version of mason.nvim options: - label: "Yes" - type: textarea attributes: label: Problem description description: A clear and short description of 1) what the issue is, and 2) why you think it's an issue with mason.nvim. validations: required: true - type: textarea attributes: label: Expected behavior description: A description of the behavior you expected. validations: required: true - type: textarea attributes: label: Steps to reproduce placeholder: | 1. ... 2. ... validations: required: true - type: input attributes: label: Affected packages description: If this issue is specific to one or more packages, list them here. If not, write 'All'. validations: required: true - type: textarea attributes: label: "Neovim version (>= 0.10.0)" description: "Output of `nvim --version`" placeholder: | NVIM v0.10.0-dev Build type: Release LuaJIT 2.1.0-beta3 validations: required: true - type: input attributes: label: "Operating system/version" description: "On Linux and Mac systems: `$ uname -a`" validations: required: true - type: textarea attributes: label: Healthcheck placeholder: ":checkhealth mason" render: Text validations: required: true - type: textarea id: screenshots attributes: label: Screenshots or recordings description: If applicable, add screenshots or recordings to help explain your problem. ================================================ FILE: .github/workflows/cbfmt.yml ================================================ name: cbfmt check on: push: branches: - "main" pull_request: jobs: cbfmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download cbfmt run: | mkdir /tmp/cbfmt && cd $_ curl -fsSL -o cbfmt.tar.gz "https://github.com/lukas-reineke/cbfmt/releases/download/v0.2.0/cbfmt_linux-x86_64_v0.2.0.tar.gz" tar --strip-components 1 -xvf cbfmt.tar.gz mv cbfmt /usr/local/bin/ - name: Download Stylua run: | mkdir /tmp/stylua && cd $_ curl -fsSL -o stylua.zip "https://github.com/JohnnyMorganz/StyLua/releases/download/v0.20.0/stylua-linux.zip" unzip -d /usr/local/bin stylua.zip - name: Download Shellharden run: | mkdir /tmp/shellharden && cd $_ curl -fsSL -o shellharden.tar.gz https://github.com/anordal/shellharden/releases/download/v4.3.1/shellharden-x86_64-unknown-linux-gnu.tar.gz tar -xvf shellharden.tar.gz mv shellharden /usr/local/bin/ - name: Run cbfmt check # Lua examples in README.md doesn't conform to Stylua rules, on purpose. run: find . -name '*.md' -not -path './dependencies/*' -not -path './README.md' -not -path './CHANGELOG.md' | xargs cbfmt --check ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - "main" permissions: contents: write pull-requests: write jobs: release: name: release runs-on: ubuntu-latest steps: - uses: google-github-actions/release-please-action@v3 id: release with: token: ${{ secrets.PAT }} release-type: simple package-name: mason.nvim extra-files: | README.md lua/mason/version.lua - uses: actions/checkout@v4 - uses: rickstaa/action-create-tag@v1 if: ${{ steps.release.outputs.release_created }} with: tag: stable message: "Current stable release: ${{ steps.release.outputs.tag_name }}" force_push_tag: true ================================================ FILE: .github/workflows/selene.yml ================================================ name: Selene check on: push: branches: - "main" pull_request: jobs: selene: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Selene check uses: NTBBloodbath/selene-action@v1.0.0 with: # token is needed because the action allegedly downloads binary from github releases token: ${{ secrets.GITHUB_TOKEN }} args: lua/ tests/ version: 0.27.1 ================================================ FILE: .github/workflows/stylua.yml ================================================ name: Stylua check on: push: branches: - "main" pull_request: jobs: stylua: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Stylua check uses: JohnnyMorganz/stylua-action@v1.1.1 with: # token is needed because the action allegedly downloads binary from github releases token: ${{ secrets.GITHUB_TOKEN }} # CLI arguments args: --check . version: 0.20.0 ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: - "main" pull_request: jobs: tests: strategy: fail-fast: false matrix: nvim_version: - v0.10.0 - v0.10.1 - v0.10.2 - v0.10.3 - v0.10.4 - v0.11.0 - v0.11.1 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: rhysd/action-setup-vim@v1 with: neovim: true version: ${{ matrix.nvim_version }} - name: Run tests run: | set -e make test nvim -u NONE -E -R --headless +'helptags doc' +q ================================================ FILE: .gitignore ================================================ .luarc.json /dependencies /tests/fixtures/mason ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [2.2.1](https://github.com/mason-org/mason.nvim/compare/v2.2.0...v2.2.1) (2026-01-07) ### Bug Fixes * **registry:** exclude synthesized registry when updating/installing registries ([#2054](https://github.com/mason-org/mason.nvim/issues/2054)) ([3fce8bd](https://github.com/mason-org/mason.nvim/commit/3fce8bd25e773bae4267c9e8f2cfbfda22aeb017)) ## [2.2.0](https://github.com/mason-org/mason.nvim/compare/v2.1.0...v2.2.0) (2026-01-07) ### Features * add support for removal of packages from a registry ([#2052](https://github.com/mason-org/mason.nvim/issues/2052)) ([69862d6](https://github.com/mason-org/mason.nvim/commit/69862d6c8dbe215489c3e48e624ff25f44437e55)) ### Bug Fixes * **installer:** attempt to recover from known fs error while finalizing installation on some file systems ([#1933](https://github.com/mason-org/mason.nvim/issues/1933)) ([198f075](https://github.com/mason-org/mason.nvim/commit/198f07572c0014774fb87371946e0f03b4908bce)) * **installer:** update cwd after uv_fs_rename() was successful ([#2033](https://github.com/mason-org/mason.nvim/issues/2033)) ([57e5a8a](https://github.com/mason-org/mason.nvim/commit/57e5a8addb8c71fb063ee4acda466c7cf6ad2800)) ## [2.1.0](https://github.com/mason-org/mason.nvim/compare/v2.0.1...v2.1.0) (2025-09-30) ### Features * **compiler:** make `supported_platforms` a universal source field ([#2002](https://github.com/mason-org/mason.nvim/issues/2002)) ([7dc4fac](https://github.com/mason-org/mason.nvim/commit/7dc4facca9702f95353d5a1f87daf23d78e31c2a)) ### Bug Fixes * **process:** close check handles ([#1995](https://github.com/mason-org/mason.nvim/issues/1995)) ([a1fbecc](https://github.com/mason-org/mason.nvim/commit/a1fbecc0fd76300e8fe84879fb1531f35cf7b018)) * **pypi:** add support for "compatible release" (~=) PEP440 expressions ([#2000](https://github.com/mason-org/mason.nvim/issues/2000)) ([9e25c98](https://github.com/mason-org/mason.nvim/commit/9e25c98d4826998460926f8c5c2284848d80ae89)) * **spawn:** always expand executable path on Windows ([#2021](https://github.com/mason-org/mason.nvim/issues/2021)) ([a83eabd](https://github.com/mason-org/mason.nvim/commit/a83eabdc8c49c0c93bf5bb162fa3b57404a9d095)) * **ui:** only set border to none if `'winborder'` doesn't exist ([#1984](https://github.com/mason-org/mason.nvim/issues/1984)) ([3671ab0](https://github.com/mason-org/mason.nvim/commit/3671ab0d40aa5bd24b1686562bd0a23391ecf76a)) ## [2.0.1](https://github.com/mason-org/mason.nvim/compare/v2.0.0...v2.0.1) (2025-07-25) ### Bug Fixes * **fetch:** add busybox wget support ([#1829](https://github.com/mason-org/mason.nvim/issues/1829)) ([8024d64](https://github.com/mason-org/mason.nvim/commit/8024d64e1330b86044fed4c8494ef3dcd483a67c)) * **pypi:** pass --no-user flag ([#1958](https://github.com/mason-org/mason.nvim/issues/1958)) ([1aceba8](https://github.com/mason-org/mason.nvim/commit/1aceba8bc158b5aaf90649077cad06744bc23ac4)) * **registry:** ensure there's no duplicate registry entries ([#1957](https://github.com/mason-org/mason.nvim/issues/1957)) ([3501b0f](https://github.com/mason-org/mason.nvim/commit/3501b0f96d9f2f878b1947cf3614bc02d053a0c0)) * **spawn:** fix calling vim.fn when inside fast event loop on Windows ([#1950](https://github.com/mason-org/mason.nvim/issues/1950)) ([888d6ee](https://github.com/mason-org/mason.nvim/commit/888d6ee499d8089a3a4be4309d239d6be1c1e6c0)) * **spawn:** fix locating exepath on Windows systems using a Unix `'shell'` ([#1991](https://github.com/mason-org/mason.nvim/issues/1991)) ([edd8f7b](https://github.com/mason-org/mason.nvim/commit/edd8f7bce8f86465349b24e235718eb3ea52878d)) ## [2.0.0](https://github.com/mason-org/mason.nvim/compare/v1.11.0...v2.0.0) (2025-05-06) This release has been an ongoing effort for quite some time now and is now ready for release. Most users should not experience any breaking changes. If you use any of the Lua APIs that Mason provides you'll find an outline of the changes below, breaking changes are marked with `Breaking Change`. ### Repository has been moved The repository has been transferred to the [`mason-org`](https://github.com/mason-org) organization. The new URL is https://github.com/mason-org/mason.nvim. The previous URL will continue to function as a redirect to the new URL but users are recommended to update to the new location. ### Addition of new maintainers ❤️ - [@mehalter](https://github.com/mehalter) - [@Conarius](https://github.com/Conarius) - [@chrisgrieser](https://github.com/chrisgrieser) ### Features - Symlinks now uses relative paths instead of absolute paths. - Uninstalled packages now display their available version in the `:Mason` UI. - Packages in the `:Mason` UI now display the source [`purl`](https://github.com/package-url/purl-spec). - Official support for [custom registries](https://github.com/mason-org/registry-examples). - Make registry installations run concurrently. - Add support for `'winborder'`. - Display current `mason.nvim` version in the `:Mason` UI header. ### Bug Fixes - Only attempt unlinking package if the receipt is found. - Expand executable paths on Windows before passing to uv_spawn. - Fix initializing UI state when using multiple registries. - Fix the display of outdated packages in the Mason UI under certain conditions. ### Misc - `Breaking Change` Minimum Neovim requirement changed from 0.7.0 to 0.10.0. - `Breaking Change` APIs related to custom packages written in Lua has been removed. - All `require("mason-core.installer.managers")` modules have been removed. - The package structure of Lua packages has changed, refer to [custom registries](https://github.com/mason-org/registry-examples) for information on how to continue using custom packages in Lua. ### Event changes #### Package - `Breaking Change` `install:success` now provides the receipt as payload argument. - `Breaking Change` `install:failed` now provides the error as payload argument. - `Breaking Change` `uninstall:success` now provides the receipt of the uninstalled package as payload argument. - `uninstall:failed` is now emitted when package uninstallation fails. #### Registry - `Breaking Change` `package:install:success` now provides the receipt as payload argument. - `Breaking Change` `package:install:failed` now provides the error as payload argument. - `Breaking Change` `package:uninstall:success` now provides the receipt of the uninstalled package as payload argument. - `package:uninstall:failed` is now emitted when package uninstallation fails. - `Breaking Change` `update` is no longer emitted when registry is updated. It's replaced by the following events: - `update:start` when the registry starts updating - `update:success` when the registry is successfully updated - `update:failed` when the registry failed to update - `update:progress` is emitted when the registry update process makes progress when multiple registries are used ### Package API changes #### `Package:get_install_path()` has been removed. `Breaking Change` This method has been removed to prepare for future changes. If you're using this method to access an executable, please consider simply using the canonical name of the executable as Mason adds these to your `PATH` by default. If you're using the method to access other files inside the package, please consider accessing the `$MASON/share` directory instead. Example: _Clarification: The `$MASON` environment variable has been available since v1.0.0._ ```lua -- 1a. There's no need to reach into the package directory via Package:get_install_path() to access the executable print(vim.fn.exepath("kotlin-debug-adapter")) -- /Users/william/.local/share/nvim/mason/bin/kotlin-debug-adapter -- 1b. Alternatively if you've configured Mason to not modify PATH print(vim.fn.expand("$MASON/bin/kotlin-debug-adapter")) -- /Users/william/.local/share/nvim/mason/bin/kotlin-debug-adapter -- 2. To access other files inside the package directory, consider accessing them via the share/ directory vim.print(vim.fn.globpath("$MASON/share/java-debug-adapter", "*.jar", true, true)) -- { "/Users/william/.local/share/nvim/mason/share/java-debug-adapter/com.microsoft.java.debug.plugin-0.53.1.jar", "/Users/william/.local/share/nvim/mason/share/java-debug-adapter/com.microsoft.java.debug.plugin.jar" } -- 3. If you absolutely need to access the package directory (please consider raising an issue/PR in the registry if possible) print(vim.fn.expand("$MASON/packages/kotlin-debug-adapter/adapter/bin/kotlin-debug-adapter")) -- /Users/william/.local/share/nvim/mason/packages/kotlin-debug-adapter/adapter/bin/kotlin-debug-adapter ``` > [!NOTE] > Why was this method removed? The contents of the package directory is not a stable interface and its structure may > change without prior notice, for example to host multiple versions of a package. The only stable interfaces on the > file system are files available in `bin/`, `share/` and `opt/` - these directories are only subject to breaking > changes done by the underlying package itself. --- #### `Package:uninstall(opts, callback)` is now asynchronous. `Breaking Change` This method now provides an asynchronous interface and accepts two new optional arguments `opts` and `callback`. `opts` currently doesn't have any valid values other than an empty Lua table `{}`. `callback` is called when the package is uninstalled, successfully or not. While the uninstall mechanism under the hood remains synchronous for the time being it is not a guarantee going forward and users are recommended to always use the asynchronous version. Example: ```lua local registry = require("mason-registry") local pkg = registry.get_package("lua-language-server") pkg:uninstall({}, function (success, result) if success then -- Do something on success. else -- Do something on error. end end) ``` --- #### `Package:check_new_version()` has been removed. `Breaking Change` `Package:check_new_version()` is replaced by `Package:get_latest_version()`. `Package:get_latest_version()` is a synchronous API. > [!NOTE] > Similarly to before, this function returns the package version provided by the currently installed registry version. Example: ```lua local registry = require("mason-registry") local pkg = registry.get_package("lua-language-server") local latest_version = pkg:get_latest_version() ``` --- #### `Package:get_installed_version()` is now synchronous. `Breaking Change` This function no longer accepts a callback. Example: ```lua local registry = require("mason-registry") local pkg = registry.get_package("lua-language-server") if pkg:is_installed() then local installed_version = pkg:get_installed_version() end ``` --- #### `Package:install()` will now error if the package is currently being installed. `Breaking Change` Use the new `Package:is_installing()` method to check whether an installation is already running. --- #### `Package:uninstall()` will now error if the package is not already installed. `Breaking Change` Use the new `Package:is_installed()` method to check whether the package is installed. --- #### `Package:install(opts, callback)` now accepts a callback. This optional callback is called by Mason when package installation finishes, successfully or not. Example: ```lua local registry = require("mason-registry") local pkg = registry.get_package("lua-language-server") pkg:install({}, function (success, result) if success then -- Do something on success. else -- Do something on error. end end) ``` ### Custom registries v2.0.0 introduces official support for custom registries. Currently supported registry protocols are `github:`, `file:`, and `lua:`. Lua-based registries have been reworked, please see https://github.com/mason-org/registry-examples for examples. Thanks to all sponsors who continue to help finance monthly costs and all 181 contributors of mason.nvim and 246 contributors of the core registry! ## [1.11.0](https://github.com/williamboman/mason.nvim/compare/v1.10.0...v1.11.0) (2025-02-15) ### Features * **pypi:** improve resolving suitable python version ([#1725](https://github.com/williamboman/mason.nvim/issues/1725)) ([0950b15](https://github.com/williamboman/mason.nvim/commit/0950b15060067f752fde13a779a994f59516ce3d)) * **ui:** add backdrop ([#1759](https://github.com/williamboman/mason.nvim/issues/1759)) ([0a3a85f](https://github.com/williamboman/mason.nvim/commit/0a3a85fa1a59e0bb0811c87556dee51f027b3358)) ### Bug Fixes * avoid calling vim.fn in fast event ([#1878](https://github.com/williamboman/mason.nvim/issues/1878)) ([3a444cb](https://github.com/williamboman/mason.nvim/commit/3a444cb7b0cee6b1e2ed31b7e76f37509075dc46)) * avoid calling vim.fn.has inside fast event ([#1705](https://github.com/williamboman/mason.nvim/issues/1705)) ([1b3d604](https://github.com/williamboman/mason.nvim/commit/1b3d60405d1d720b2c4927f19672e9479703b00f)) * fix usage of deprecated Neovim APIs ([#1703](https://github.com/williamboman/mason.nvim/issues/1703)) ([0f1cb65](https://github.com/williamboman/mason.nvim/commit/0f1cb65f436b769733d18b41572f617a1fb41f62)) * **fs:** fall back to `fs_stat` if entry type is not returned by `fs_readdir` ([#1783](https://github.com/williamboman/mason.nvim/issues/1783)) ([1114b23](https://github.com/williamboman/mason.nvim/commit/1114b2336e917d883c30f89cd63ba94050001b2d)) * **health:** support multidigit luarocks version numbers ([#1648](https://github.com/williamboman/mason.nvim/issues/1648)) ([751b1fc](https://github.com/williamboman/mason.nvim/commit/751b1fcbf3d3b783fcf8d48865264a9bcd8f9b10)) * **pypi:** allow access to system site packages by default ([#1584](https://github.com/williamboman/mason.nvim/issues/1584)) ([2be2600](https://github.com/williamboman/mason.nvim/commit/2be2600f9b5a61b0c6109a3fb161b3abe75e5195)) * **pypi:** exclude python3.12 from candidate list ([#1722](https://github.com/williamboman/mason.nvim/issues/1722)) ([f8ce876](https://github.com/williamboman/mason.nvim/commit/f8ce8768f296717c72b3910eee7bd5ac5223cdb9)) * **pypi:** prefer stock python3 if it satisfies version requirement ([#1736](https://github.com/williamboman/mason.nvim/issues/1736)) ([f96a318](https://github.com/williamboman/mason.nvim/commit/f96a31855fa8aea55599cea412fe611b85a874ed)) * **registry:** exhaust streaming parser when loading "file:" registries ([#1708](https://github.com/williamboman/mason.nvim/issues/1708)) ([49ff59a](https://github.com/williamboman/mason.nvim/commit/49ff59aded1047a773670651cfa40e76e63c6377)) * replace deprecated calls to vim.validate ([#1876](https://github.com/williamboman/mason.nvim/issues/1876)) ([5664dd5](https://github.com/williamboman/mason.nvim/commit/5664dd5deb3ac9527da90691543eb28df51c1ef8)) * **ui:** fix rendering JSON schemas ([#1757](https://github.com/williamboman/mason.nvim/issues/1757)) ([e2f7f90](https://github.com/williamboman/mason.nvim/commit/e2f7f9044ec30067bc11800a9e266664b88cda22)) * **ui:** reposition window if border is different than "none" ([#1859](https://github.com/williamboman/mason.nvim/issues/1859)) ([f9f3b46](https://github.com/williamboman/mason.nvim/commit/f9f3b464dda319288b8ce592e53f0d9cf9ca8b4e)) ### Performance Improvements * **registry:** significantly improve the "file:" protocol performance ([#1702](https://github.com/williamboman/mason.nvim/issues/1702)) ([098a56c](https://github.com/williamboman/mason.nvim/commit/098a56c385ca3a1a0d4682d129203dda35421b8e)) ## [1.10.0](https://github.com/williamboman/mason.nvim/compare/v1.9.0...v1.10.0) (2024-01-29) ### Features * don't use vim.g.python3_host_prog as a candidate for python ([#1606](https://github.com/williamboman/mason.nvim/issues/1606)) ([bce96d2](https://github.com/williamboman/mason.nvim/commit/bce96d2fd483e71826728c6f9ac721fc9dd7d2cf)) * **pypi:** attempt more python3 candidates ([#1608](https://github.com/williamboman/mason.nvim/issues/1608)) ([dcd0ea3](https://github.com/williamboman/mason.nvim/commit/dcd0ea30ccfc7d47e879878d1270d6847a519181)) ### Bug Fixes * **golang:** fix fetching package versions for packages containing subpath specifier ([#1607](https://github.com/williamboman/mason.nvim/issues/1607)) ([9c94168](https://github.com/williamboman/mason.nvim/commit/9c9416817c9f4e6f333c749327a1ed5355cfab61)) * **pypi:** fix variable shadowing ([#1610](https://github.com/williamboman/mason.nvim/issues/1610)) ([aa550fb](https://github.com/williamboman/mason.nvim/commit/aa550fb0649643eee89d5e64c67f81916e88a736)) * **ui:** don't indent empty lines ([#1597](https://github.com/williamboman/mason.nvim/issues/1597)) ([c7e6705](https://github.com/williamboman/mason.nvim/commit/c7e67059bb8ce7e126263471645c531d961b5e1d)) ## [1.9.0](https://github.com/williamboman/mason.nvim/compare/v1.8.3...v1.9.0) (2024-01-06) ### Features * add support for openvsx sources ([#1589](https://github.com/williamboman/mason.nvim/issues/1589)) ([6c68547](https://github.com/williamboman/mason.nvim/commit/6c685476df4f202e371bdd3d726729d6f3f8b9f0)) ### Bug Fixes * **cargo:** don't attempt to fetch versions when version targets commit SHA ([#1585](https://github.com/williamboman/mason.nvim/issues/1585)) ([a09da6a](https://github.com/williamboman/mason.nvim/commit/a09da6ac634926a299dd439da08bdb547a8ca011)) ## [1.8.3](https://github.com/williamboman/mason.nvim/compare/v1.8.2...v1.8.3) (2023-11-08) ### Bug Fixes * **pypi:** support MSYS2 virtual environments on Windows ([#1547](https://github.com/williamboman/mason.nvim/issues/1547)) ([3e2432a](https://github.com/williamboman/mason.nvim/commit/3e2432ad0bca01fc3356389b341aa3e5e2da9fd8)) ## [1.8.2](https://github.com/williamboman/mason.nvim/compare/v1.8.1...v1.8.2) (2023-10-31) ### Bug Fixes * **registry:** fix parsing registry identifiers that contain ":" ([#1542](https://github.com/williamboman/mason.nvim/issues/1542)) ([87eb3ac](https://github.com/williamboman/mason.nvim/commit/87eb3ac2ab4fcbf5326d8bde6842b073a3be65a7)) ## [1.8.1](https://github.com/williamboman/mason.nvim/compare/v1.8.0...v1.8.1) (2023-10-10) ### Bug Fixes * **health:** schedule vim.fn call ([#1514](https://github.com/williamboman/mason.nvim/issues/1514)) ([3ba3b79](https://github.com/williamboman/mason.nvim/commit/3ba3b79f73d5411e72c7df5445150f4e9278d4d7)) ## [1.8.0](https://github.com/williamboman/mason.nvim/compare/v1.7.0...v1.8.0) (2023-09-04) ### Features * **ui:** add setting to toggle help view ([#1468](https://github.com/williamboman/mason.nvim/issues/1468)) ([e1602c8](https://github.com/williamboman/mason.nvim/commit/e1602c868f938877057cb6f45e50859cb55cad96)) ### Bug Fixes * **registry:** reset registries state when setting registries ([#1474](https://github.com/williamboman/mason.nvim/issues/1474)) ([c811fbf](https://github.com/williamboman/mason.nvim/commit/c811fbf09c7642eebb37d6694f1a016a043f6ed3)) * **registry:** schedule vim.fn calls in FileRegistrySource ([#1471](https://github.com/williamboman/mason.nvim/issues/1471)) ([1c77412](https://github.com/williamboman/mason.nvim/commit/1c77412d7ff73e453cdc5366c8d7cd98d2242802)) ## [1.7.0](https://github.com/williamboman/mason.nvim/compare/v1.6.2...v1.7.0) (2023-08-25) ### Features * **cargo:** support fetching versions for git crates hosted on github ([#1459](https://github.com/williamboman/mason.nvim/issues/1459)) ([e9eb004](https://github.com/williamboman/mason.nvim/commit/e9eb0048cecc577a1eec534485d3e010487b46a7)) * **registry:** add file: source protocol ([#1457](https://github.com/williamboman/mason.nvim/issues/1457)) ([8544039](https://github.com/williamboman/mason.nvim/commit/85440397264a31208721e4501c93b23a4940b27e)) ### Bug Fixes * **std:** use gtar if available ([#1433](https://github.com/williamboman/mason.nvim/issues/1433)) ([a51c2d0](https://github.com/williamboman/mason.nvim/commit/a51c2d063c5377ee9e58c5f9cda7c7436787be72)) * **ui:** properly reset new package version state ([#1454](https://github.com/williamboman/mason.nvim/issues/1454)) ([68e6a15](https://github.com/williamboman/mason.nvim/commit/68e6a153d7cd1251eb85ebb48d2e351e9ab940b8)) ## [1.6.2](https://github.com/williamboman/mason.nvim/compare/v1.6.1...v1.6.2) (2023-08-09) ### Bug Fixes * **ui:** don't disable search mode if empty pattern and last-pattern is set ([#1445](https://github.com/williamboman/mason.nvim/issues/1445)) ([be6f680](https://github.com/williamboman/mason.nvim/commit/be6f680774a75a06ceede3bd7159df2388f49b04)) ## [1.6.1](https://github.com/williamboman/mason.nvim/compare/v1.6.0...v1.6.1) (2023-07-21) ### Bug Fixes * **installer:** retain unmapped source fields ([#1399](https://github.com/williamboman/mason.nvim/issues/1399)) ([0579574](https://github.com/williamboman/mason.nvim/commit/05795741895ee16062eabeb0d89bff7cbcd693fa)) ## [1.6.0](https://github.com/williamboman/mason.nvim/compare/v1.5.1...v1.6.0) (2023-07-04) ### Features * **ui:** display package deprecation message ([#1391](https://github.com/williamboman/mason.nvim/issues/1391)) ([b728115](https://github.com/williamboman/mason.nvim/commit/b7281153cd9167d2b1a5d8cbda1ba8d4ad9fa8c2)) * **ui:** don't use diagnostic messages for displaying deprecated, uninstalled, packages ([#1393](https://github.com/williamboman/mason.nvim/issues/1393)) ([c290d0e](https://github.com/williamboman/mason.nvim/commit/c290d0e4ab6da9cac1e26684e53fba0b615862ed)) ## [1.5.1](https://github.com/williamboman/mason.nvim/compare/v1.5.0...v1.5.1) (2023-06-28) ### Bug Fixes * **linker:** ensure exec wrapper target is executable ([#1380](https://github.com/williamboman/mason.nvim/issues/1380)) ([10da1a3](https://github.com/williamboman/mason.nvim/commit/10da1a33b4ac24ad4d76a9af91871720ac6b65e4)) * **purl:** percent-encoding is case insensitive ([#1382](https://github.com/williamboman/mason.nvim/issues/1382)) ([b68d3be](https://github.com/williamboman/mason.nvim/commit/b68d3be4b664671002221d43c82e74a0f1006b26)) ## [1.5.0](https://github.com/williamboman/mason.nvim/compare/v1.4.0...v1.5.0) (2023-06-28) ### Features * **command:** add completion for option flags for :MasonInstall ([#1379](https://github.com/williamboman/mason.nvim/issues/1379)) ([e507af7](https://github.com/williamboman/mason.nvim/commit/e507af7b996dae90404345abb2bc88540f931589)) * **installer:** write more installation output to stdout ([#1376](https://github.com/williamboman/mason.nvim/issues/1376)) ([758ac5b](https://github.com/williamboman/mason.nvim/commit/758ac5b35e823eee74a90f855b2a66afc51ec92d)) ### Bug Fixes * **installer:** timeout schema download after 5s ([#1374](https://github.com/williamboman/mason.nvim/issues/1374)) ([d114376](https://github.com/williamboman/mason.nvim/commit/d11437645af60449ff252b2c9abda103c5610520)) ## [1.4.0](https://github.com/williamboman/mason.nvim/compare/v1.3.0...v1.4.0) (2023-06-21) ### Features * **fetch:** add explicit default timeout to requests ([#1364](https://github.com/williamboman/mason.nvim/issues/1364)) ([82cae55](https://github.com/williamboman/mason.nvim/commit/82cae550c87466b1163b216bdb9c71cb71dd8f67)) * **fetch:** include mason.nvim version in User-Agent ([#1362](https://github.com/williamboman/mason.nvim/issues/1362)) ([e706d30](https://github.com/williamboman/mason.nvim/commit/e706d305fbcc8701bd30e31dd727aee2853b9db9)) ## [1.3.0](https://github.com/williamboman/mason.nvim/compare/v1.2.1...v1.3.0) (2023-06-18) ### Features * **health:** add advice for Debian/Ubuntu regarding python3 venv ([#1358](https://github.com/williamboman/mason.nvim/issues/1358)) ([6f3853e](https://github.com/williamboman/mason.nvim/commit/6f3853e5ae8c200e29d2e394e479d9c3f8e018f5)) ## [1.2.1](https://github.com/williamboman/mason.nvim/compare/v1.2.0...v1.2.1) (2023-06-13) ### Bug Fixes * **providers:** fix some client providers and add some more ([#1354](https://github.com/williamboman/mason.nvim/issues/1354)) ([6f44955](https://github.com/williamboman/mason.nvim/commit/6f4495590a0f9e121b483c9b1236fbabbd80da7a)) ## [1.2.0](https://github.com/williamboman/mason.nvim/compare/v1.1.1...v1.2.0) (2023-06-13) ### Features * **command:** improve completion for :MasonInstall ([#1353](https://github.com/williamboman/mason.nvim/issues/1353)) ([13e26c8](https://github.com/williamboman/mason.nvim/commit/13e26c81ff5074ee8f095a791cd37fc1cec37377)) ### Bug Fixes * **async:** always check channel state ([#1351](https://github.com/williamboman/mason.nvim/issues/1351)) ([f503346](https://github.com/williamboman/mason.nvim/commit/f5033463bb911a136e577fc6f339328f162e2b4a)) * **command:** run :MasonUpdate synchronously in headless mode ([#1347](https://github.com/williamboman/mason.nvim/issues/1347)) ([0276793](https://github.com/williamboman/mason.nvim/commit/02767937fc2e1b214c854a8fdde26ae1d3529dd6)) * **functional:** strip_prefix and strip_suffix should not use patterns ([#1352](https://github.com/williamboman/mason.nvim/issues/1352)) ([f99b702](https://github.com/williamboman/mason.nvim/commit/f99b70233e49db2229350bb82d9ddc6e2f4131c0)) ## [1.1.1](https://github.com/williamboman/mason.nvim/compare/v1.1.0...v1.1.1) (2023-05-29) ### Bug Fixes * **ui:** improve search mode UI and remove redundant whitespaces ([#1332](https://github.com/williamboman/mason.nvim/issues/1332)) ([a18c031](https://github.com/williamboman/mason.nvim/commit/a18c031c72a3c7576ba5dc60ee30de8290c8757c)) ## [1.1.0](https://github.com/williamboman/mason.nvim/compare/v1.0.1...v1.1.0) (2023-05-18) ### Features * **installer:** lock package installation ([#1290](https://github.com/williamboman/mason.nvim/issues/1290)) ([227f8a9](https://github.com/williamboman/mason.nvim/commit/227f8a9aaae495f481c768f8346edfceaf6d2951)) * **ui:** add keymap setting for toggling package installation log ([#1268](https://github.com/williamboman/mason.nvim/issues/1268)) ([48bb1cc](https://github.com/williamboman/mason.nvim/commit/48bb1cc33a1fefe94f5ce4972446a1c6ad849f15)) * **ui:** add search mode ([#1306](https://github.com/williamboman/mason.nvim/issues/1306)) ([3b59f25](https://github.com/williamboman/mason.nvim/commit/3b59f25d435fb1b8d36c4cc26410c3569f0bd795)) * **ui:** display "update all" hint ([#1296](https://github.com/williamboman/mason.nvim/issues/1296)) ([e634134](https://github.com/williamboman/mason.nvim/commit/e634134312bb936f472468a401c9cae6485ab54b)) ### Bug Fixes * **sources:** don't skip installation if fixed version is not currently installed ([#1297](https://github.com/williamboman/mason.nvim/issues/1297)) ([9c5edf1](https://github.com/williamboman/mason.nvim/commit/9c5edf13c2e6bd5223eebfeb4557ccc841acaa0e)) * **ui:** use vim.cmd("") for nvim-0.7.0 compatibility ([#1307](https://github.com/williamboman/mason.nvim/issues/1307)) ([e60b855](https://github.com/williamboman/mason.nvim/commit/e60b855bfa8c7d34387200daa6e54a5e22d3da05)) ## [1.0.1](https://github.com/williamboman/mason.nvim/compare/v1.0.0...v1.0.1) (2023-04-26) ### Bug Fixes * **pypi:** also provide install_extra_args to pypi.install ([#1263](https://github.com/williamboman/mason.nvim/issues/1263)) ([646ef07](https://github.com/williamboman/mason.nvim/commit/646ef07907e0960987c13c0b13f69eb808cc66ad)) ================================================ FILE: CONTRIBUTING.md ================================================ - [Contribution policy](#contribution-policy) - [Adding a new package](#adding-a-new-package) - [Code style](#code-style) - [Generated code](#generated-code) - [Tests](#tests) - [Adding or changing a feature](#adding-or-changing-a-feature) - [Commit style](#commit-style) - [Pull requests](#pull-requests) # Contribution policy The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14][bcp14], [RFC2119][rfc2119], and [RFC8174][rfc8174] when, and only when, they appear in all capitals, as shown here. [bcp14]: https://tools.ietf.org/html/bcp14 [rfc2119]: https://tools.ietf.org/html/rfc2119 [rfc8174]: https://tools.ietf.org/html/rfc8174 # Adding a new package Core `mason.nvim` package definitions reside within the [`github:mason-org/mason-registry` registry](https://github.com/mason-org/mason-registry/). Contributions to add new packages MUST be done there (refer to the README and existing package definitions). # Code style This project adheres to Editorconfig, Selene, and Stylua code style & formatting rules. New patches MUST adhere to these coding styles. # Generated code Some changes such as adding or changing a package definition will require generating some new code. The changes to generated code MAY be included in a pull request. If it's not included in a pull request, it will automatically be generated and pushed to your branch before merge. Generating code can be done on Unix systems like so: ```sh make generate ``` # Tests [Tests](https://github.com/mason-org/mason.nvim/tree/main/tests) MAY be added or modified to reflect any new changes. Tests can be executed on Unix systems like so: ```sh make test FILE=tests/mason-core/managers/luarocks_spec.lua make test ``` # Adding or changing a feature Adding or changing a feature MUST be preceded with an issue where scope and acceptance criteria are agreed upon with project maintainers before implementation. # Commit style Commits SHOULD follow the [conventional commits guidelines](https://www.conventionalcommits.org/en/v1.0.0/). # Pull requests Once a pull request is marked as ready for review (i.e. not in draft mode), new changes SHOULD NOT be force-pushed to the branch. Merge commits SHOULD be preferred over rebases. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ================================================ FILE: Makefile ================================================ INSTALL_ROOT_DIR:=$(shell pwd)/tests/fixtures/mason NVIM_HEADLESS:=nvim --headless --noplugin -u tests/minimal_init.vim dependencies: git clone --depth 1 https://github.com/nvim-lua/plenary.nvim dependencies/pack/vendor/start/plenary.nvim git clone --depth 1 https://github.com/nvim-neotest/neotest dependencies/pack/vendor/start/neotest .PHONY: clean_dependencies clean_dependencies: rm -rf dependencies .PHONY: clean_fixtures clean_fixtures: rm -rf "${INSTALL_ROOT_DIR}" .PHONY: clean clean: clean_fixtures clean_dependencies .PHONY: test test: clean_fixtures dependencies INSTALL_ROOT_DIR=${INSTALL_ROOT_DIR} $(NVIM_HEADLESS) -c "call RunTests()" # vim:noexpandtab ================================================ FILE: PACKAGES.md ================================================ Moved to https://mason-registry.dev/registry/list ================================================ FILE: README.md ================================================ ![Linux](https://img.shields.io/badge/Linux-%23.svg?logo=linux&color=FCC624&logoColor=black) ![macOS](https://img.shields.io/badge/macOS-%23.svg?logo=apple&color=000000&logoColor=white) ![Windows](https://img.shields.io/badge/Windows-%23.svg?logo=windows&color=0078D6&logoColor=white) [![GitHub CI](https://github.com/mason-org/mason.nvim/workflows/Tests/badge.svg)](https://github.com/mason-org/mason.nvim/actions?query=workflow%3ATests+branch%3Amain+event%3Apush) [![Sponsors](https://img.shields.io/github/sponsors/williamboman)](https://github.com/sponsors/williamboman)

mason.nvim

Portable package manager for Neovim that runs everywhere Neovim runs.
Easily install and manage LSP servers, DAP servers, linters, and formatters.

:help mason.nvim

Latest version: v2.2.1

## Table of Contents - [Introduction](#introduction) - [Installation & Usage](#installation--usage) - [Recommended setup for `lazy.nvim`](#recommended-setup-for-lazynvim) - [Requirements](#requirements) - [Commands](#commands) - [Registries](#registries) - [Screenshots](#screenshots) - [Configuration](#configuration) ## Introduction > [`:h mason-introduction`][help-mason-introduction] `mason.nvim` is a Neovim plugin that allows you to easily manage external editor tooling such as LSP servers, DAP servers, linters, and formatters through a single interface. It runs everywhere Neovim runs (across Linux, macOS, Windows, etc.), with only a small set of [external requirements](#requirements) needed. Packages are installed in Neovim's data directory ([`:h standard-path`][help-standard-path]) by default. Executables are linked to a single `bin/` directory, which `mason.nvim` will add to Neovim's PATH during setup, allowing seamless access from Neovim builtins (LSP client, shell, terminal, etc.) as well as other 3rd party plugins. For a list of all available packages, see . ## Installation & Usage > [`:h mason-quickstart`][help-mason-quickstart] Install using your plugin manager of choice. **Setup is required**: ```lua require("mason").setup() ``` `mason.nvim` is optimized to load as little as possible during setup. Lazy-loading the plugin, or somehow deferring the setup, is not recommended. Refer to the [Configuration](#configuration) section for information about which settings are available. ### Recommended setup for `lazy.nvim` The following is the recommended setup when using `lazy.nvim`. It will set up the plugin for you, meaning **you don't have to call `require("mason").setup()` yourself**. ```lua { "mason-org/mason.nvim", opts = {} } ``` ## Requirements > [`:h mason-requirements`][help-mason-requirements] `mason.nvim` relaxes the minimum requirements by attempting multiple different utilities (for example, `wget`, `curl`, and `Invoke-WebRequest` are all perfect substitutes). The _minimum_ recommended requirements are: - neovim `>= 0.10.0` - For Unix systems: - `git(1)` - `curl(1)` or `GNU wget(1)` - `unzip(1)` - GNU tar (`tar(1)` or `gtar(1)` depending on platform) - `gzip(1)` - For Windows systems: - pwsh or powershell - git - GNU tar - One of the following: - [7zip][7zip] - [peazip][peazip] - [archiver][archiver] - [winzip][winzip] - [WinRAR][winrar] Note that `mason.nvim` will regularly shell out to external package managers, such as `cargo` and `npm`. Depending on your personal usage, some of these will also need to be installed. Refer to `:checkhealth mason` for a full list. [7zip]: https://www.7-zip.org/ [archiver]: https://github.com/mholt/archiver [peazip]: https://peazip.github.io/ [winzip]: https://www.winzip.com/ [winrar]: https://www.win-rar.com/ ## Commands > [`:h mason-commands`][help-mason-commands] - `:Mason` - opens a graphical status window - `:MasonUpdate` - updates all managed registries - `:MasonInstall ...` - installs/re-installs the provided packages - `:MasonUninstall ...` - uninstalls the provided packages - `:MasonUninstallAll` - uninstalls all packages - `:MasonLog` - opens the `mason.nvim` log file in a new tab window ## Registries Mason's core package registry is located at [mason-org/mason-registry](https://github.com/mason-org/mason-registry). Before any packages can be used, the registry needs to be downloaded. This is done automatically for you when using the different Mason commands (e.g. `:MasonInstall`), but can also be done manually by using the `:MasonUpdate` command. If you're utilizing Mason's Lua APIs to access packages, it's recommended to use the [`:h mason-registry.refresh()`][help-mason-registry-refresh] or [`:h mason-registry.update()`][help-mason-registry-update] functions to ensure you have the latest package information before retrieving packages. ## Screenshots | | | | | :----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------: | | Main window | Language search | Language filter | | Package information | New package versions | Help window | ## Configuration > [`:h mason-settings`][help-mason-settings] You may optionally configure certain behavior of `mason.nvim` when calling the `.setup()` function. Refer to the [default configuration](#default-configuration) for a list of all available settings. Example: ```lua require("mason").setup({ ui = { icons = { package_installed = "✓", package_pending = "➜", package_uninstalled = "✗" } } }) ``` ### Configuration using `lazy.nvim` ```lua { "mason-org/mason.nvim", opts = { ui = { icons = { package_installed = "✓", package_pending = "➜", package_uninstalled = "✗" } } } } ``` ### Default configuration ```lua ---@class MasonSettings local DEFAULT_SETTINGS = { ---@since 1.0.0 -- The directory in which to install packages. install_root_dir = path.concat { vim.fn.stdpath "data", "mason" }, ---@since 1.0.0 -- Where Mason should put its bin location in your PATH. Can be one of: -- - "prepend" (default, Mason's bin location is put first in PATH) -- - "append" (Mason's bin location is put at the end of PATH) -- - "skip" (doesn't modify PATH) ---@type '"prepend"' | '"append"' | '"skip"' PATH = "prepend", ---@since 1.0.0 -- Controls to which degree logs are written to the log file. It's useful to set this to vim.log.levels.DEBUG when -- debugging issues with package installations. log_level = vim.log.levels.INFO, ---@since 1.0.0 -- Limit for the maximum amount of packages to be installed at the same time. Once this limit is reached, any further -- packages that are requested to be installed will be put in a queue. max_concurrent_installers = 4, ---@since 1.0.0 -- [Advanced setting] -- The registries to source packages from. Accepts multiple entries. Should a package with the same name exist in -- multiple registries, the registry listed first will be used. registries = { "github:mason-org/mason-registry", }, ---@since 1.0.0 -- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions). -- Accepts multiple entries, where later entries will be used as fallback should prior providers fail. -- Builtin providers are: -- - mason.providers.registry-api - uses the https://api.mason-registry.dev API -- - mason.providers.client - uses only client-side tooling to resolve metadata providers = { "mason.providers.registry-api", "mason.providers.client", }, github = { ---@since 1.0.0 -- The template URL to use when downloading assets from GitHub. -- The placeholders are the following (in order): -- 1. The repository (e.g. "rust-lang/rust-analyzer") -- 2. The release version (e.g. "v0.3.0") -- 3. The asset name (e.g. "rust-analyzer-v0.3.0-x86_64-unknown-linux-gnu.tar.gz") download_url_template = "https://github.com/%s/releases/download/%s/%s", }, pip = { ---@since 1.0.0 -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. -- -- Example: { "--proxy", "https://proxyserver" } install_args = {}, }, ui = { ---@since 1.0.0 -- Whether to automatically check for new versions when opening the :Mason window. check_outdated_packages_on_open = true, ---@since 1.0.0 -- The border to use for the UI window. Accepts same border values as |nvim_open_win()|. -- Defaults to `:h 'winborder'` if nil. border = nil, ---@since 1.11.0 -- The backdrop opacity. 0 is fully opaque, 100 is fully transparent. backdrop = 60, ---@since 1.0.0 -- Width of the window. Accepts: -- - Integer greater than 1 for fixed width. -- - Float in the range of 0-1 for a percentage of screen width. width = 0.8, ---@since 1.0.0 -- Height of the window. Accepts: -- - Integer greater than 1 for fixed height. -- - Float in the range of 0-1 for a percentage of screen height. height = 0.9, icons = { ---@since 1.0.0 -- The list icon to use for installed packages. package_installed = "◍", ---@since 1.0.0 -- The list icon to use for packages that are installing, or queued for installation. package_pending = "◍", ---@since 1.0.0 -- The list icon to use for packages that are not installed. package_uninstalled = "◍", }, keymaps = { ---@since 1.0.0 -- Keymap to expand a package toggle_package_expand = "", ---@since 1.0.0 -- Keymap to install the package under the current cursor position install_package = "i", ---@since 1.0.0 -- Keymap to reinstall/update the package under the current cursor position update_package = "u", ---@since 1.0.0 -- Keymap to check for new version for the package under the current cursor position check_package_version = "c", ---@since 1.0.0 -- Keymap to update all installed packages update_all_packages = "U", ---@since 1.0.0 -- Keymap to check which installed packages are outdated check_outdated_packages = "C", ---@since 1.0.0 -- Keymap to uninstall a package uninstall_package = "X", ---@since 1.0.0 -- Keymap to cancel a package installation cancel_installation = "", ---@since 1.0.0 -- Keymap to apply language filter apply_language_filter = "", ---@since 1.1.0 -- Keymap to toggle viewing package installation log toggle_package_install_log = "", ---@since 1.8.0 -- Keymap to toggle the help view toggle_help = "g?", }, }, } ``` --- 👋 didn't find what you were looking for? Try looking in the help docs :help mason.nvim! [help-mason-commands]: ./doc/mason.txt#L140 [help-mason-introduction]: ./doc/mason.txt#L11 [help-mason-quickstart]: ./doc/mason.txt#L42 [help-mason-registry-refresh]: ./doc/mason.txt#L520 [help-mason-registry-update]: ./doc/mason.txt#L513 [help-mason-requirements]: ./doc/mason.txt#L25 [help-mason-settings]: ./doc/mason.txt#L200 [help-standard-path]: https://neovim.io/doc/user/starting.html#standard-path ================================================ FILE: SECURITY.md ================================================ # Security policy ## Reporting a Vulnerability Please report any suspected security vulnerabilities [here][new-advisory]. If the issue is confirmed, we will release a patch as soon as possible depending on complexity. Please follow [responsible disclosure practices](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). Thanks! [new-advisory]: https://github.com/mason-org/mason.nvim/security/advisories/new ================================================ FILE: doc/.gitignore ================================================ tags ================================================ FILE: doc/mason.txt ================================================ *mason.nvim* Minimum version of neovim: 0.10.0 Author: William Boman *mason-help-guide* Type |gO| to see the table of contents. Press |K| on a helptag to jump to it: |mason-help-guide| ============================================================================== INTRODUCTION *mason-introduction* `mason.nvim` is a Neovim plugin that allows you to easily manage external editor tooling such as LSP servers, DAP servers, linters, and formatters through a single interface. It runs everywhere Neovim runs (across Linux, macOS, Windows, etc.), with only a small set of external requirements needed. Packages are installed in Neovim's data directory (`:h standard-path`) by default. Executables are linked to a single `bin/` directory, which `mason.nvim` will add to Neovim's PATH during setup, allowing seamless access from Neovim builtins (shell, terminal, etc.) as well as other 3rd party plugins. ============================================================================== REQUIREMENTS *mason-requirements* `mason.nvim` relaxes the minimum requirements by attempting multiple different utilities (for example, `wget`, `curl`, and `Invoke-WebRequest` are all perfect substitutes). The _minimum_ recommended requirements are: - neovim `>= 0.10.0` - For Unix systems: `git(1)`, `curl(1)` or `wget(1)`, `unzip(1)`, `tar(1)`, `gzip(1)` - For Windows systems: pwsh or powershell, git, tar, and 7zip or peazip or archiver or winzip or WinRAR Note that Mason will regularly shell out to external package managers, such as `cargo` and `npm`. Depending on your personal usage, some of these will also need to be installed. Refer to `:checkhealth mason` for a full list. ============================================================================== QUICK START *mason-quickstart* ----------------- SETTING UP MASON.NVIM First you'll need to set up Mason. This is done by calling the `setup()` function: >lua require("mason").setup() < Mason will do the following during setup: 1) add Mason's `bin/` directory to the Neovim session's PATH 2) register commands (|mason-commands|) Refer to |mason-settings| for all available settings. ----------------- INSTALLING PACKAGES Install a package via |:MasonInstall|, for example: >vim :MasonInstall stylua < You may also install multiple packages at a time: >vim :MasonInstall stylua lua-language-server < To install a specific version of a package, you may provide it as part of the package name, like so: >vim :MasonInstall rust-analyzer@nightly < Please refer to each package's own release pages to find which versions are available. You may also install packages in headless mode. This will run the command in blocking mode and the command won't yield back until all packages have finished installing: >sh $ nvim --headless -c "MasonInstall lua-language-server rust-analyzer" -c qall < Note: ~ You may also use Mason's Lua API to programmatically manage package installations. Through this interface you will also gain access to more features to allow further customization. ----------------- THE MASON WINDOW To view the UI for mason, run: >vim :Mason < Through this UI you may explore which packages that are available, see which installed packages have new versions available, install, uninstall, or update packages, expand package information, and more. The UI comes with a set of keybinds which you may find in the help view by pressing `g?` when the Mason window is open. ============================================================================== REGISTRIES *mason-registries* `mason.nvim` sources package definitions from the registries it has been configured with (see |mason-settings|). `mason.nvim` uses the core registry, governed by `mason.nvim`, by default. This may be extended, or even entirely overridden, through additional registries, like so: >lua require("mason").setup { registries = { "lua:my-registry", "github:mason-org/mason-registry", }, } < Packages are loaded from registries in the order they've been configured, with registries appearing first in the list having precedence. ============================================================================== HOW TO INSTALL PACKAGES *mason-how-to-install-packages* You may install packages either via the command interface or via Mason's Lua APIs. See |:MasonInstall| for more details. ============================================================================== HOW TO USE PACKAGES *mason-how-to-use-packages* Although many packages are perfectly usable out of the box through Neovim builtins, it is recommended to use other 3rd party plugins to further integrate these. See also ~ Execute external commands: |:!cmd|. Launch an embedded terminal: |terminal|. Launch background jobs: |jobstart| & |uv.spawn()| (via |vim.loop|) ============================================================================== COMMANDS *mason-commands* ------------------------------------------------------------------------------ OPEN THE MASON WINDOW *:Mason* :Mason Opens the graphical status window. Through this UI you may explore which packages that are available, see which installed packages have new versions available, install, uninstall, or update packages, expand package information, and more. The UI comes with a set of keybinds which you may find in the help view by pressing `g?` when the Mason window is open. ------------------------------------------------------------------------------ UPDATE REGISTRIES *:MasonUpdate* >vim :MasonUpdate < Updates all managed registries. ------------------------------------------------------------------------------ INSTALLING PACKAGES *:MasonInstall* >vim :MasonInstall ... < Installs the provided packages. Packages may include a version specifier, like so: >vim :MasonInstall lua-language-server@v3.0.0 < Runs in blocking fashion if there are no UIs attached (i.e. running in headless mode): >sh $ nvim --headless -c "MasonInstall stylua" -c "qall" < ------------------------------------------------------------------------------ UNINSTALLING PACKAGES *:MasonUninstall* >vim :MasonUninstall ... < Uninstalls the provided packages. ------------------------------------------------------------------------------ UNINSTALLING ALL PACKAGES *:MasonUninstallAll* >vim :MasonUninstallAll < Uninstalls all installed packages. ------------------------------------------------------------------------------ VIEW THE MASON LOG *:MasonLog* >vim :MasonLog < Opens the log file in a new tab window. ============================================================================== SETTINGS *mason-settings* You can configure certain behavior of mason when calling the `.setup()` function. Refer to the |mason-default-settings| for all available settings. Example: >lua require("mason").setup({ ui = { icons = { package_installed = "✓", package_pending = "➜", package_uninstalled = "✗" } } }) < *mason-default-settings* >lua ---@class MasonSettings local DEFAULT_SETTINGS = { ---@since 1.0.0 -- The directory in which to install packages. install_root_dir = path.concat { vim.fn.stdpath "data", "mason" }, ---@since 1.0.0 -- Where Mason should put its bin location in your PATH. Can be one of: -- - "prepend" (default, Mason's bin location is put first in PATH) -- - "append" (Mason's bin location is put at the end of PATH) -- - "skip" (doesn't modify PATH) ---@type '"prepend"' | '"append"' | '"skip"' PATH = "prepend", ---@since 1.0.0 -- Controls to which degree logs are written to the log file. It's useful to set this to vim.log.levels.DEBUG when -- debugging issues with package installations. log_level = vim.log.levels.INFO, ---@since 1.0.0 -- Limit for the maximum amount of packages to be installed at the same time. Once this limit is reached, any further -- packages that are requested to be installed will be put in a queue. max_concurrent_installers = 4, ---@since 1.0.0 -- [Advanced setting] -- The registries to source packages from. Accepts multiple entries. Should a package with the same name exist in -- multiple registries, the registry listed first will be used. registries = { "github:mason-org/mason-registry", }, ---@since 1.0.0 -- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions). -- Accepts multiple entries, where later entries will be used as fallback should prior providers fail. -- Builtin providers are: -- - mason.providers.registry-api - uses the https://api.mason-registry.dev API -- - mason.providers.client - uses only client-side tooling to resolve metadata providers = { "mason.providers.registry-api", "mason.providers.client", }, github = { ---@since 1.0.0 -- The template URL to use when downloading assets from GitHub. -- The placeholders are the following (in order): -- 1. The repository (e.g. "rust-lang/rust-analyzer") -- 2. The release version (e.g. "v0.3.0") -- 3. The asset name (e.g. "rust-analyzer-v0.3.0-x86_64-unknown-linux-gnu.tar.gz") download_url_template = "https://github.com/%s/releases/download/%s/%s", }, pip = { ---@since 1.0.0 -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. -- -- Example: { "--proxy", "https://proxyserver" } install_args = {}, }, ui = { ---@since 1.0.0 -- Whether to automatically check for new versions when opening the :Mason window. check_outdated_packages_on_open = true, ---@since 1.0.0 -- The border to use for the UI window. Accepts same border values as |nvim_open_win()|. -- Defaults to `:h 'winborder'` if nil. border = nil, ---@since 1.11.0 -- The backdrop opacity. 0 is fully opaque, 100 is fully transparent. backdrop = 60, ---@since 1.0.0 -- Width of the window. Accepts: -- - Integer greater than 1 for fixed width. -- - Float in the range of 0-1 for a percentage of screen width. width = 0.8, ---@since 1.0.0 -- Height of the window. Accepts: -- - Integer greater than 1 for fixed height. -- - Float in the range of 0-1 for a percentage of screen height. height = 0.9, icons = { ---@since 1.0.0 -- The list icon to use for installed packages. package_installed = "◍", ---@since 1.0.0 -- The list icon to use for packages that are installing, or queued for installation. package_pending = "◍", ---@since 1.0.0 -- The list icon to use for packages that are not installed. package_uninstalled = "◍", }, keymaps = { ---@since 1.0.0 -- Keymap to expand a package toggle_package_expand = "", ---@since 1.0.0 -- Keymap to install the package under the current cursor position install_package = "i", ---@since 1.0.0 -- Keymap to reinstall/update the package under the current cursor position update_package = "u", ---@since 1.0.0 -- Keymap to check for new version for the package under the current cursor position check_package_version = "c", ---@since 1.0.0 -- Keymap to update all installed packages update_all_packages = "U", ---@since 1.0.0 -- Keymap to check which installed packages are outdated check_outdated_packages = "C", ---@since 1.0.0 -- Keymap to uninstall a package uninstall_package = "X", ---@since 1.0.0 -- Keymap to cancel a package installation cancel_installation = "", ---@since 1.0.0 -- Keymap to apply language filter apply_language_filter = "", ---@since 1.1.0 -- Keymap to toggle viewing package installation log toggle_package_install_log = "", ---@since 1.8.0 -- Keymap to toggle the help view toggle_help = "g?", }, }, } < ============================================================================== DOWNLOAD MIRRORS *mason-download-mirrors* ------------------------------------------------------------------------------ GITHUB MIRROR *mason-download-mirror-github* It's possible to customize the download URL used when downloading assets from GitHub releases by setting the `github.download_url_template` settings during setup, like so: >lua require("mason").setup { github = { -- The template URL to use when downloading assets from GitHub. -- The placeholders are the following (in order): -- 1. The repository (e.g. "rust-lang/rust-analyzer") -- 2. The release version (e.g. "v0.3.0") -- 3. The asset name (e.g. "rust-analyzer-v0.3.0-x86_64-unknown-linux-gnu.tar.gz") download_url_template = "https://my.mirror.com/%s/releases/download/%s/%s", }, } < ============================================================================== INSTALLATION ERRORS *mason-errors* *mason-provider-errors* By default, Mason uses the api.mason-registry.dev API to resolve package metadata. Calling this service may result in network errors on some networks (e.g., SSL issues on corporate VPNs). If resolving the SSL error is not an option, you will have to change the provider implementation. Mason provides a client provider which calls underlying 3rd party service APIs directly, which you can enable like so: >lua require("mason").setup { providers = { "mason.providers.client", "mason.providers.registry-api", } } < Note: ~ The client provider have less overall coverage and may come with additional performance penalties (spawning slow commands, network & parsing overheads, etc.). ============================================================================== DEBUGGING *mason-debugging* To help with debugging issues with installing/uninstalling packages, please make sure to set mason's log level to DEBUG or TRACE, like so: >lua require("mason").setup { log_level = vim.log.levels.DEBUG } < You may find the logs by entering the command `:MasonLog`. Providing the contents of this file when reporting an issue will help tremendously. Remember to redo whatever is failing after changing the log level in order to capture new log entries. ============================================================================== Lua module: "mason" >lua require("mason") < *mason.setup()* setup({config}) Sets up mason with the provided {config} (see |mason-settings|). ============================================================================== Lua module: "mason-registry" >lua require("mason-registry") < *mason-registry.is_installed()* is_installed({package_name}) Checks whether the provided package name is installed. In many situations, this is a more efficient option than the Package:is_installed() method due to a smaller amount of modules required to load. Parameters: {package_name} - string Returns: boolean *mason-registry.get_package()* get_package({package_name}) Returns an instance of the Package class if the provided package name exists. This function errors if a package cannot be found. Parameters: {package_name} - string Returns: Package *mason-registry.has_package()* has_package({package_name}) Returns true if the provided package_name can be found in the registry. Parameters: {package_name} - string Returns: boolean *mason-registry.get_installed_packages()* get_installed_packages() Returns all installed package instances. This is a slower function that loads more modules. Returns: Package[] *mason-registry.get_installed_package_names()* get_installed_package_names() Returns all installed package names. This is a fast function that doesn't load any extra modules. Returns: string[] *mason-registry.get_all_packages()* get_all_packages() Returns all package instances. This is a slower function that loads more modules. Returns: Package[] *mason-registry.get_all_package_names()* get_all_package_names() Returns all package names. This is a faster function than |mason-registry.get_all_packages()| because it loads fewer modules. Returns: string[] *mason-registry.get_all_package_specs()* get_all_package_specs() Returns all package specifications. This is a faster function than |mason-registry.get_all_packages()| because it loads fewer modules. Returns: RegistryPackageSpec[] *mason-registry.update()* update({callback}) Updates all managed registries. Parameters: {callback} - Callback of the signature `fun(success: boolean, updated_registries: RegistrySource[])` *mason-registry.refresh()* refresh({callback?}) Refreshes all registries if needed. This is a convenience wrapper around |mason-registry.update()| that only updates registries if: 1) registries haven't been updated in a while 2) or, one or more registries are not installed Runs in a blocking fashion if no {callback} is provided. Note that when running in blocking fashion the entire editor is frozen, so prefer the asynchronous variant unless absolutely needed. Parameters: {callback?} (optional) - Invoked when the registry has been refreshed. Example: >lua local registry = require("mason-registry") -- 1. synchronous registry.refresh() local packages = registry.get_all_packages() ... -- 2. asynchronous registry.refresh(function () local packages = registry.get_all_packages() ... end) < vim:tw=78:ft=help:norl:expandtab:sw=4 ================================================ FILE: lua/mason/api/command.lua ================================================ local _ = require "mason-core.functional" local platform = require "mason-core.platform" local function Mason() require("mason.ui").open() end vim.api.nvim_create_user_command("Mason", Mason, { desc = "Opens mason's UI window.", nargs = 0, }) local get_valid_packages = _.filter_map(function(pkg_specifier) local Optional = require "mason-core.optional" local notify = require "mason-core.notify" local Package = require "mason-core.package" local registry = require "mason-registry" local package_name, version = Package.Parse(pkg_specifier) local ok, pkg = pcall(registry.get_package, package_name) if ok and pkg then return Optional.of { pkg = pkg, version = version } else notify(("%q is not a valid package."):format(pkg_specifier), vim.log.levels.ERROR) return Optional.empty() end end) ---@param package_specifiers string[] ---@param opts? table local function MasonInstall(package_specifiers, opts) opts = opts or {} local a = require "mason-core.async" local registry = require "mason-registry" local Optional = require "mason-core.optional" local install_packages = _.filter_map(function(target) if target.pkg:is_installing() then return Optional.empty() else return Optional.of(target.pkg:install { version = target.version, debug = opts.debug, force = opts.force, strict = opts.strict, target = opts.target, }) end end) if platform.is_headless then registry.refresh() local valid_packages = get_valid_packages(package_specifiers) if #valid_packages ~= #package_specifiers then -- When executing in headless mode we don't allow any of the provided packages to be invalid. -- This is to avoid things like scripts silently not erroring even if they've provided one or more invalid packages. return vim.cmd [[1cq]] end a.run_blocking(function() local results = { a.wait_all(_.map( ---@param target { pkg: Package, version: string? } function(target) return function() if target.pkg:is_installing() then return end return a.wait(function(resolve) local handle = target.pkg:install({ version = target.version, debug = opts.debug, force = opts.force, strict = opts.strict, target = opts.target, }, function(success, err) resolve { success, target.pkg, err } end) if not opts.quiet then handle :on("stdout", vim.schedule_wrap(vim.api.nvim_out_write)) :on("stderr", vim.schedule_wrap(vim.api.nvim_err_write)) end end) end end, valid_packages )), } a.scheduler() local is_failure = _.compose(_.equals(false), _.head) if _.any(is_failure, results) then local failures = _.filter(is_failure, results) local failed_packages = _.map(_.nth(2), failures) for _, failure in ipairs(failures) do local _, pkg, error = unpack(failure) vim.api.nvim_err_writeln(("Package %s failed with the following error:"):format(pkg.name)) vim.api.nvim_err_writeln(tostring(error)) end vim.cmd [[1cq]] end end) else local ui = require "mason.ui" ui.open() -- Important: We start installation of packages _after_ opening the UI. This gives the UI components a chance to -- register the necessary event handlers in time, avoiding desynced state. registry.refresh(function() local valid_packages = get_valid_packages(package_specifiers) install_packages(valid_packages) vim.schedule(function() ui.set_sticky_cursor "installing-section" end) end) end end local parse_opts = _.compose( _.from_pairs, _.map(_.compose(function(arg) if #arg == 2 then return arg else return { arg[1], true } end end, _.split "=", _.gsub("^%-%-", ""))) ) ---@param args string[] ---@return table opts, string[] args local function parse_args(args) local opts_list, args = unpack(_.partition(_.starts_with "--", args)) local opts = parse_opts(opts_list) return opts, args end vim.api.nvim_create_user_command("MasonInstall", function(opts) local command_opts, packages = parse_args(opts.fargs) MasonInstall(packages, command_opts) end, { desc = "Install one or more packages.", nargs = "+", ---@param arg_lead string complete = function(arg_lead) local registry = require "mason-registry" registry.refresh() if _.starts_with("--", arg_lead) then return _.filter(_.starts_with(arg_lead), { "--debug", "--force", "--strict", "--target=", }) elseif _.matches("^.+@", arg_lead) then local pkg_name, version = unpack(_.match("^(.+)@(.*)", arg_lead)) local ok, pkg = pcall(registry.get_package, pkg_name) if not ok or not pkg then return {} end local a = require "mason-core.async" return a.run_blocking(function() return a.wait_first { function() return pkg:get_all_versions() :map( _.compose( _.map(_.concat(arg_lead)), _.map(_.strip_prefix(version)), _.filter(_.starts_with(version)) ) ) :get_or_else {} end, function() a.sleep(4000) return {} end, } end) end local all_pkg_names = registry.get_all_package_names() return _.sort_by(_.identity, _.filter(_.starts_with(arg_lead), all_pkg_names)) end, }) ---@param package_names string[] local function MasonUninstall(package_names) local valid_packages = get_valid_packages(package_names) if #valid_packages > 0 then _.each(function(target) target.pkg:uninstall() end, valid_packages) require("mason.ui").open() end end vim.api.nvim_create_user_command("MasonUninstall", function(opts) MasonUninstall(opts.fargs) end, { desc = "Uninstall one or more packages.", nargs = "+", ---@param arg_lead string complete = function(arg_lead) local registry = require "mason-registry" return _.sort_by(_.identity, _.filter(_.starts_with(arg_lead), registry.get_installed_package_names())) end, }) local function MasonUninstallAll() local registry = require "mason-registry" require("mason.ui").open() for _, pkg in ipairs(registry.get_installed_packages()) do pkg:uninstall() end end vim.api.nvim_create_user_command("MasonUninstallAll", MasonUninstallAll, { desc = "Uninstall all packages.", }) local function MasonUpdate() local notify = require "mason-core.notify" local registry = require "mason-registry" notify "Updating registries…" ---@param success boolean ---@param updated_registries RegistrySource[] local function handle_result(success, updated_registries) if success then local count = #updated_registries notify(("Successfully updated %d %s."):format(count, count == 1 and "registry" or "registries")) else notify(("Failed to update registries: %s"):format(updated_registries), vim.log.levels.ERROR) end end if platform.is_headless then local a = require "mason-core.async" a.run_blocking(function() local success, updated_registries = a.wait(registry.update) a.scheduler() handle_result(success, updated_registries) end) else registry.update(_.scheduler_wrap(handle_result)) end end vim.api.nvim_create_user_command("MasonUpdate", MasonUpdate, { desc = "Update Mason registries.", }) local function MasonLog() local log = require "mason-core.log" vim.cmd(([[tabnew %s]]):format(log.outfile)) end vim.api.nvim_create_user_command("MasonLog", MasonLog, { desc = "Opens the mason.nvim log.", }) return { Mason = Mason, MasonInstall = MasonInstall, MasonUninstall = MasonUninstall, MasonUninstallAll = MasonUninstallAll, MasonUpdate = MasonUpdate, MasonLog = MasonLog, } ================================================ FILE: lua/mason/health.lua ================================================ local health = vim.health or require "health" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local control = require "mason-core.async.control" local platform = require "mason-core.platform" local providers = require "mason-core.providers" local registry = require "mason-registry" local settings = require "mason.settings" local spawn = require "mason-core.spawn" local version = require "mason.version" local Semaphore = control.Semaphore local M = {} local report_start = _.scheduler_wrap(health.start or health.report_start) local report_ok = _.scheduler_wrap(health.ok or health.report_ok) local report_warn = _.scheduler_wrap(health.warn or health.report_warn) local report_error = _.scheduler_wrap(health.error or health.report_error) local sem = Semaphore:new(5) ---@async ---@param opts {cmd:string, args:string[], name: string, use_stderr: boolean?, version_check: (fun(version: string): string?), relaxed: boolean?, advice: string[]} local function check(opts) local get_first_non_empty_line = _.compose(_.head, _.filter(_.complement(_.matches "^%s*$")), _.split "\n") local permit = sem:acquire() Result.try(function(try) local result = try(spawn[opts.cmd] { opts.args, on_spawn = function(_, stdio) local stdin = stdio[1] -- some processes (`sh` for example) will endlessly read from stdin, so we close it immediately if not stdin:is_closing() then stdin:close() end end, }) ---@type string? local version = get_first_non_empty_line(opts.use_stderr and result.stderr or result.stdout) if opts.version_check then local ok, version_mismatch = pcall(opts.version_check, version) if ok and version_mismatch then local report = opts.relaxed and report_warn or report_error report(("%s: unsupported version `%s`"):format(opts.name, version), { version_mismatch }) return elseif not ok then local report = opts.relaxed and report_warn or report_error report(("%s: failed to parse version"):format(opts.name), { ("Error: %s"):format(version_mismatch) }) return end end report_ok(("%s: `%s`"):format(opts.name, version or "Ok")) end):on_failure(function(err) local report = opts.relaxed and report_warn or report_error report(("%s: not available"):format(opts.name), opts.advice or { tostring(err) }) end) permit:forget() end local function check_registries() report_start "mason.nvim [Registries]" a.wait(registry.refresh) for source in registry.sources:iterate { include_uninstalled = true } do if source:is_installed() then report_ok(("Registry `%s` is installed."):format(source:get_display_name())) else report_error( ("Registry `%s` is not installed."):format(source:get_display_name()), { "Run :MasonUpdate to install." } ) end end end local function check_neovim() if vim.fn.has "nvim-0.10.0" == 1 then report_ok "neovim version >= 0.10.0" else report_error("neovim version < 0.10.0", { "Upgrade Neovim." }) end end ---@async local function check_core_utils() report_start "mason.nvim [Core utils]" check { name = "unzip", cmd = "unzip", args = { "-v" }, relaxed = true } -- wget is used interchangeably with curl, but with lower priority, so we mark wget as relaxed check { cmd = "wget", args = { "--help" }, name = "wget", relaxed = true } check { cmd = "curl", args = { "--version" }, name = "curl" } check { cmd = "gzip", args = { "--version" }, name = "gzip", use_stderr = platform.is.mac, -- Apple gzip prints version string to stderr relaxed = platform.is.win, } a.scheduler() local tar = vim.fn.executable "gtar" == 1 and "gtar" or "tar" check { cmd = tar, args = { "--version" }, name = tar } if platform.is.unix then check { cmd = "bash", args = { "--version" }, name = "bash" } end if platform.is.win then check { cmd = "pwsh", args = { "-NoProfile", "-Command", [[$PSVersionTable.PSVersion, $PSVersionTable.OS, $PSVersionTable.Platform -join " "]], }, name = "pwsh", } check { cmd = "7z", args = { "--help" }, name = "7z", relaxed = true } end end local function check_thunk(opts) return function() check(opts) end end ---@async local function check_languages() report_start "mason.nvim [Languages]" a.wait_all { check_thunk { cmd = "go", args = { "version" }, name = "Go", relaxed = true, version_check = function(version) -- Parses output such as "go version go1.17.3 darwin/arm64" into major, minor, patch components local _, _, major, minor = version:find "go(%d+)%.(%d+)" -- Due to https://go.dev/doc/go-get-install-deprecation if not (tonumber(major) >= 1 and tonumber(minor) >= 17) then return "Go version must be >= 1.17." end end, }, check_thunk { cmd = "cargo", args = { "--version" }, name = "cargo", relaxed = true, version_check = function(version) local _, _, major, minor = version:find "(%d+)%.(%d+)%.(%d+)" if (tonumber(major) <= 1) and (tonumber(minor) < 60) then return "Some cargo installations require Rust >= 1.60.0." end end, }, check_thunk { cmd = "luarocks", args = { "--version" }, name = "luarocks", relaxed = true, version_check = function(version) local _, _, major = version:find "(%d+)%.(%d+)%.(%d+)" if not (tonumber(major) >= 3) then -- Because of usage of "--dev" flag return "Luarocks version must be >= 3.0.0." end end, }, check_thunk { cmd = "ruby", args = { "--version" }, name = "Ruby", relaxed = true }, check_thunk { cmd = "gem", args = { "--version" }, name = "RubyGem", relaxed = true }, check_thunk { cmd = "composer", args = { "--version" }, name = "Composer", relaxed = true }, check_thunk { cmd = "php", args = { "--version" }, name = "PHP", relaxed = true }, check_thunk { cmd = "npm", args = { "--version" }, name = "npm", relaxed = true, version_check = function(version) -- Parses output such as "8.1.2" into major, minor, patch components local _, _, major = version:find "(%d+)%.(%d+)%.(%d+)" -- Based off of general observations of feature parity. -- In npm v7, peerDependencies are now automatically installed. if tonumber(major) < 7 then return "npm version must be >= 7" end end, }, check_thunk { cmd = "node", args = { "--version" }, name = "node", relaxed = true, version_check = function(version) -- Parses output such as "v16.3.1" into major, minor, patch local _, _, major = version:find "v(%d+)%.(%d+)%.(%d+)" if tonumber(major) < 14 then return "Node version must be >= 14" end end, }, check_thunk { cmd = "javac", args = { "-version" }, name = "javac", relaxed = true }, check_thunk { cmd = "java", args = { "-version" }, name = "java", use_stderr = true, relaxed = true }, check_thunk { cmd = "julia", args = { "--version" }, name = "julia", relaxed = true }, function() local python = platform.is.win and "python" or "python3" check { cmd = python, args = { "--version" }, name = "python", relaxed = true } check { cmd = python, args = { "-m", "pip", "--version" }, name = "pip", relaxed = true } check { cmd = python, args = { "-c", "import venv" }, name = "python venv", relaxed = true, advice = { [[On Debian/Ubuntu systems, you need to install the python3-venv package using the following command: apt-get install python3-venv]], }, } end, function() a.scheduler() if vim.env.JAVA_HOME then check { cmd = ("%s/bin/java"):format(vim.env.JAVA_HOME), args = { "-version" }, name = "JAVA_HOME", use_stderr = true, relaxed = true, } end end, } end ---@async local function check_mason() providers.github .get_latest_release("mason-org/mason.nvim") :on_success( ---@param latest_release GitHubRelease function(latest_release) a.scheduler() if latest_release.tag_name ~= version.VERSION then report_warn(("mason.nvim version %s"):format(version.VERSION), { ("The latest version of mason.nvim is: %s"):format(latest_release.tag_name), }) else report_ok(("mason.nvim version %s"):format(version.VERSION)) end end ) :on_failure(function() a.scheduler() report_ok(("mason.nvim version %s"):format(version.VERSION)) end) report_ok(("PATH: %s"):format(settings.current.PATH)) report_ok(("Providers: \n %s"):format(_.join("\n ", settings.current.providers))) end function M.check() report_start "mason.nvim" a.run_blocking(function() check_mason() check_neovim() check_registries() check_core_utils() check_languages() a.wait(vim.schedule) end) end return M ================================================ FILE: lua/mason/init.lua ================================================ local InstallLocation = require "mason-core.installer.InstallLocation" local Registry = require "mason-registry" local settings = require "mason.settings" local M = {} local function setup_autocmds() vim.api.nvim_create_autocmd("VimLeavePre", { callback = function() require("mason-core.terminator").terminate(5000) end, once = true, }) end M.has_setup = false ---@param config MasonSettings? function M.setup(config) if config then settings.set(config) end local global_location = InstallLocation.global() global_location:set_env { PATH = settings.current.PATH } for _, registry in ipairs(settings.current.registries) do Registry.sources:append(registry) end require "mason.api.command" setup_autocmds() M.has_setup = true end return M ================================================ FILE: lua/mason/providers/client/gh.lua ================================================ local _ = require "mason-core.functional" local fetch = require "mason-core.fetch" local spawn = require "mason-core.spawn" local stringify_params = _.compose(_.join "&", _.map(_.join "="), _.sort_by(_.head), _.to_pairs) ---@param path string ---@param opts { params: table? }? ---@return Result # JSON decoded response. local function gh_api_call(path, opts) if opts and opts.params then local params = stringify_params(opts.params) path = ("%s?%s"):format(path, params) end return spawn .gh({ "api", path, env = { CLICOLOR_FORCE = 0 } }) :map(_.prop "stdout") :or_else(function() return fetch(("https://api.github.com/%s"):format(path), { headers = { Accept = "application/vnd.github.v3+json; q=1.0, application/json; q=0.8", }, }) end) :map_catching(vim.json.decode) end ---@type GitHubProvider return { get_latest_release = function(repo) local path = ("repos/%s/releases/latest"):format(repo) return gh_api_call(path) end, get_all_release_versions = function(repo) local path = ("repos/%s/releases"):format(repo) return gh_api_call(path):map(_.map(_.prop "tag_name")) end, get_all_tags = function(repo) local path = ("repos/%s/git/matching-refs/tags"):format(repo) return gh_api_call(path):map(_.map(_.compose(_.gsub("^refs/tags/", ""), _.prop "ref"))) end, } ================================================ FILE: lua/mason/providers/client/golang.lua ================================================ local _ = require "mason-core.functional" local spawn = require "mason-core.spawn" ---@type GolangProvider return { get_all_versions = function(pkg) return spawn .go({ "list", "-json", "-m", "-versions", pkg, }) :map(_.prop "stdout") :map_catching(vim.json.decode) :map(_.prop "Versions") :map(_.reverse) end, } ================================================ FILE: lua/mason/providers/client/init.lua ================================================ ---@type Provider return { github = require "mason.providers.client.gh", npm = require "mason.providers.client.npm", pypi = require "mason.providers.client.pypi", rubygems = require "mason.providers.client.rubygems", golang = require "mason.providers.client.golang", openvsx = require "mason.providers.client.openvsx", } ================================================ FILE: lua/mason/providers/client/npm.lua ================================================ local _ = require "mason-core.functional" local spawn = require "mason-core.spawn" ---@type NpmProvider return { get_latest_version = function(pkg) return spawn .npm({ "view", "--json", pkg .. "@latest" }) :map(_.prop "stdout") :map_catching(vim.json.decode) :map(_.pick { "name", "version" }) end, get_all_versions = function(pkg) return spawn .npm({ "view", "--json", pkg, "versions" }) :map(_.prop "stdout") :map_catching(vim.json.decode) :map(_.reverse) end, } ================================================ FILE: lua/mason/providers/client/openvsx.lua ================================================ local _ = require "mason-core.functional" local fetch = require "mason-core.fetch" local semver = require "mason-core.semver" ---@param path string local function api_url(path) return ("https://open-vsx.org/api/%s"):format(path) end ---@param version string local function maybe_semver_sort(version) return semver.parse(version):get_or_else(version) end ---@type OpenVSXProvider return { get_latest_version = function(namespace, extension) return fetch(api_url("%s/%s"):format(namespace, extension)):map_catching(vim.json.decode):map(_.prop "version") end, get_all_versions = function(namespace, extension) return fetch(api_url("%s/%s/versions"):format(namespace, extension)) :map_catching(vim.json.decode) :map(_.compose(_.keys, _.prop "versions")) :map(_.compose(_.reverse, _.sort_by(maybe_semver_sort))) end, } ================================================ FILE: lua/mason/providers/client/pypi.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local fetch = require "mason-core.fetch" local fs = require "mason-core.fs" local platform = require "mason-core.platform" local spawn = require "mason-core.spawn" ---@param args SpawnArgs local function python(args) a.scheduler() local py_exec = platform.is.win and "python" or "python3" -- run in tmpdir in case pip inadvertently produces some output args.cwd = vim.fn.tempname() fs.async.mkdir(args.cwd) return spawn[py_exec](args) end ---@async ---@param pkg string local function get_all_versions(pkg) -- https://stackoverflow.com/a/26664162 return python({ "-m", "pip", "install", "--disable-pip-version-check", "--use-deprecated=legacy-resolver", -- for pip >= 20.3 ("%s=="):format(pkg), -- invalid version specifier to trigger the wanted error message }) :recover(_.prop "stderr") :map(_.compose(_.split ", ", _.head, _.match "%(from versions: (.+)%)")) :map(_.reverse) end ---@param pkg string local function synthesize_pkg(pkg) ---@param version Optional return function(version) return version :map(function(v) return { name = pkg, version = v } end) :ok_or "Unable to find latest version." end end ---@type PyPiProvider return { get_latest_version = function(pkg) return get_all_versions(pkg):map(_.compose(Optional.of_nilable, _.last)):and_then(synthesize_pkg(pkg)) end, get_all_versions = get_all_versions, get_supported_python_versions = function(pkg, version) return fetch(("https://pypi.org/pypi/%s/%s/json"):format(pkg, version)) :map_catching(vim.json.decode) :map(_.path { "info", "requires_python" }) :and_then(function(requires_python) if type(requires_python) ~= "string" or requires_python == "" then return Result.failure "Package does not specify supported Python versions." else return Result.success(requires_python) end end) end, } ================================================ FILE: lua/mason/providers/client/rubygems.lua ================================================ local Optional = require "mason-core.optional" local _ = require "mason-core.functional" local spawn = require "mason-core.spawn" ---@param gem string ---@param output string "$ gem list" output local parse_gem_versions = _.curryN(function(gem, output) local lines = _.split("\n", output) return Optional.of_nilable(_.find_first(_.starts_with(gem), lines)) :map(_.compose(_.head, _.match "%((.+)%)$")) :map(_.split ", ") :ok_or "Failed to parse gem list output." end, 2) ---@async ---@param gem string local function get_all_versions(gem) return spawn.gem({ "list", gem, "--remote", "--all" }):map(_.prop "stdout"):and_then(parse_gem_versions(gem)) end ---@type RubyGemsProvider return { get_latest_version = function(gem) return get_all_versions(gem):map(_.head) end, get_all_versions = function(gem) return get_all_versions(gem) end, } ================================================ FILE: lua/mason/providers/registry-api/init.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local api = require "mason-registry.api" ---@type Provider return { github = { get_latest_release = function(repo) return api.github.releases.latest { repo = repo } end, get_all_release_versions = function(repo) return api.github.releases.all { repo = repo } end, get_latest_tag = function(repo) return api.github.tags.latest { repo = repo } end, get_all_tags = function(repo) return api.github.tags.all { repo = repo } end, }, npm = { get_latest_version = function(pkg) return api.npm.versions.latest { package = pkg } end, get_all_versions = function(pkg) return api.npm.versions.all { package = pkg } end, }, pypi = { get_latest_version = function(pkg) return api.pypi.versions.latest { package = pkg } end, get_all_versions = function(pkg) return api.pypi.versions.all { package = pkg } end, get_supported_python_versions = function(pkg, version) return api.pypi.versions .get({ package = pkg, version = version }) :map(_.prop "requires_python") :and_then(function(requires_python) if type(requires_python) ~= "string" or requires_python == "" then return Result.failure "Package does not specify supported Python versions." else return Result.success(requires_python) end end) end, }, rubygems = { get_latest_version = function(gem) return api.rubygems.versions.latest { gem = gem } end, get_all_versions = function(gem) return api.rubygems.versions.all { gem = gem } end, }, packagist = { get_latest_version = function(pkg) return api.packagist.versions.latest { pkg = pkg } end, get_all_versions = function(pkg) return api.packagist.versions.all { pkg = pkg } end, }, crates = { get_latest_version = function(crate) return api.crate.versions.latest { crate = crate } end, get_all_versions = function(crate) return api.crate.versions.all { crate = crate } end, }, golang = { get_all_versions = function(pkg) return api.golang.versions.all { pkg = api.encode_uri_component(pkg) } end, }, openvsx = { get_latest_version = function(namespace, extension) return api.openvsx.versions.latest { namespace = namespace, extension = extension } end, get_all_versions = function(namespace, extension) return api.openvsx.versions.all { namespace = namespace, extension = extension } end, }, } ================================================ FILE: lua/mason/settings.lua ================================================ local path = require "mason-core.path" local M = {} ---@class MasonSettings local DEFAULT_SETTINGS = { ---@since 1.0.0 -- The directory in which to install packages. install_root_dir = path.concat { vim.fn.stdpath "data", "mason" }, ---@since 1.0.0 -- Where Mason should put its bin location in your PATH. Can be one of: -- - "prepend" (default, Mason's bin location is put first in PATH) -- - "append" (Mason's bin location is put at the end of PATH) -- - "skip" (doesn't modify PATH) ---@type '"prepend"' | '"append"' | '"skip"' PATH = "prepend", ---@since 1.0.0 -- Controls to which degree logs are written to the log file. It's useful to set this to vim.log.levels.DEBUG when -- debugging issues with package installations. log_level = vim.log.levels.INFO, ---@since 1.0.0 -- Limit for the maximum amount of packages to be installed at the same time. Once this limit is reached, any further -- packages that are requested to be installed will be put in a queue. max_concurrent_installers = 4, ---@since 1.0.0 -- [Advanced setting] -- The registries to source packages from. Accepts multiple entries. Should a package with the same name exist in -- multiple registries, the registry listed first will be used. registries = { "github:mason-org/mason-registry", }, ---@since 1.0.0 -- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions). -- Accepts multiple entries, where later entries will be used as fallback should prior providers fail. -- Builtin providers are: -- - mason.providers.registry-api - uses the https://api.mason-registry.dev API -- - mason.providers.client - uses only client-side tooling to resolve metadata providers = { "mason.providers.registry-api", "mason.providers.client", }, github = { ---@since 1.0.0 -- The template URL to use when downloading assets from GitHub. -- The placeholders are the following (in order): -- 1. The repository (e.g. "rust-lang/rust-analyzer") -- 2. The release version (e.g. "v0.3.0") -- 3. The asset name (e.g. "rust-analyzer-v0.3.0-x86_64-unknown-linux-gnu.tar.gz") download_url_template = "https://github.com/%s/releases/download/%s/%s", }, pip = { ---@since 1.0.0 -- Whether to upgrade pip to the latest version in the virtual environment before installing packages. upgrade_pip = false, ---@since 1.0.0 -- These args will be added to `pip install` calls. Note that setting extra args might impact intended behavior -- and is not recommended. -- -- Example: { "--proxy", "https://proxyserver" } install_args = {}, }, ui = { ---@since 1.0.0 -- Whether to automatically check for new versions when opening the :Mason window. check_outdated_packages_on_open = true, ---@since 1.0.0 -- The border to use for the UI window. Accepts same border values as |nvim_open_win()|. -- Defaults to `:h 'winborder'` if nil. border = nil, ---@since 1.11.0 -- The backdrop opacity. 0 is fully opaque, 100 is fully transparent. backdrop = 60, ---@since 1.0.0 -- Width of the window. Accepts: -- - Integer greater than 1 for fixed width. -- - Float in the range of 0-1 for a percentage of screen width. width = 0.8, ---@since 1.0.0 -- Height of the window. Accepts: -- - Integer greater than 1 for fixed height. -- - Float in the range of 0-1 for a percentage of screen height. height = 0.9, icons = { ---@since 1.0.0 -- The list icon to use for installed packages. package_installed = "◍", ---@since 1.0.0 -- The list icon to use for packages that are installing, or queued for installation. package_pending = "◍", ---@since 1.0.0 -- The list icon to use for packages that are not installed. package_uninstalled = "◍", }, keymaps = { ---@since 1.0.0 -- Keymap to expand a package toggle_package_expand = "", ---@since 1.0.0 -- Keymap to install the package under the current cursor position install_package = "i", ---@since 1.0.0 -- Keymap to reinstall/update the package under the current cursor position update_package = "u", ---@since 1.0.0 -- Keymap to check for new version for the package under the current cursor position check_package_version = "c", ---@since 1.0.0 -- Keymap to update all installed packages update_all_packages = "U", ---@since 1.0.0 -- Keymap to check which installed packages are outdated check_outdated_packages = "C", ---@since 1.0.0 -- Keymap to uninstall a package uninstall_package = "X", ---@since 1.0.0 -- Keymap to cancel a package installation cancel_installation = "", ---@since 1.0.0 -- Keymap to apply language filter apply_language_filter = "", ---@since 1.1.0 -- Keymap to toggle viewing package installation log toggle_package_install_log = "", ---@since 1.8.0 -- Keymap to toggle the help view toggle_help = "g?", }, }, } M._DEFAULT_SETTINGS = DEFAULT_SETTINGS M.current = M._DEFAULT_SETTINGS ---@param opts MasonSettings function M.set(opts) M.current = vim.tbl_deep_extend("force", vim.deepcopy(M.current), opts) end return M ================================================ FILE: lua/mason/ui/colors.lua ================================================ local hl_groups = { MasonBackdrop = { bg = "#000000", default = true }, MasonNormal = { link = "NormalFloat", default = true }, MasonHeader = { bold = true, fg = "#222222", bg = "#DCA561", default = true }, MasonHeaderSecondary = { bold = true, fg = "#222222", bg = "#56B6C2", default = true }, MasonHighlight = { fg = "#56B6C2", default = true }, MasonHighlightBlock = { bg = "#56B6C2", fg = "#222222", default = true }, MasonHighlightBlockBold = { bg = "#56B6C2", fg = "#222222", bold = true, default = true }, MasonHighlightSecondary = { fg = "#DCA561", default = true }, MasonHighlightBlockSecondary = { bg = "#DCA561", fg = "#222222", default = true }, MasonHighlightBlockBoldSecondary = { bg = "#DCA561", fg = "#222222", bold = true, default = true }, MasonLink = { link = "MasonHighlight", default = true }, MasonMuted = { fg = "#888888", default = true }, MasonMutedBlock = { bg = "#888888", fg = "#222222", default = true }, MasonMutedBlockBold = { bg = "#888888", fg = "#222222", bold = true, default = true }, MasonError = { link = "ErrorMsg", default = true }, MasonWarning = { link = "WarningMsg", default = true }, MasonHeading = { bold = true, default = true }, } for name, hl in pairs(hl_groups) do vim.api.nvim_set_hl(0, name, hl) end ================================================ FILE: lua/mason/ui/components/header.lua ================================================ local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local p = require "mason.ui.palette" local settings = require "mason.settings" local version = require "mason.version" ---@param state InstallerUiState return function(state) local uninstalled_registries = _.filter(_.prop_eq("is_installed", false), state.info.registries) return Ui.Node { Ui.CascadingStyleNode({ "CENTERED" }, { Ui.HlTextNode { Ui.When(state.view.is_showing_help, { p.header_secondary(" " .. state.header.title_prefix .. " mason.nvim "), p.header_secondary(version.VERSION .. " "), p.none((" "):rep(#state.header.title_prefix + 1)), }, { p.header " mason.nvim ", p.header(version.VERSION .. " "), state.view.is_searching and p.Comment " (search mode, press to clear)" or p.none "", }), Ui.When(state.view.is_showing_help, { p.none " press ", p.highlight_secondary(settings.current.ui.keymaps.toggle_help), p.none " for package list", }, { p.none "press ", p.highlight(settings.current.ui.keymaps.toggle_help), p.none " for help", }), { p.Comment "https://github.com/mason-org/mason.nvim" }, }, }), Ui.When(not state.info.registry_update.in_progress and #uninstalled_registries > 0, function() return Ui.CascadingStyleNode({ "INDENT" }, { Ui.EmptyLine(), Ui.HlTextNode { { p.warning "Uninstalled registries", }, { p.Comment "Packages from the following registries are unavailable. Press ", p.highlight(settings.current.ui.keymaps.check_outdated_packages), p.Comment " to install.", }, unpack(_.map(function(registry) return { p.none(" - " .. registry.name) } end, uninstalled_registries)), }, Ui.EmptyLine(), }) end), Ui.When( not state.info.registry_update.in_progress and state.info.registry_update.error, Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode { { p.error "Registry installation failed with the following error:", }, { p.none " ", p.Comment(state.info.registry_update.error), }, }, Ui.EmptyLine(), }) ), } end ================================================ FILE: lua/mason/ui/components/help/dap.lua ================================================ local Ui = require "mason-core.ui" local p = require "mason.ui.palette" ---@param state InstallerUiState return function(state) return Ui.HlTextNode { { p.Bold "What is DAP?", }, { p.none "The ", p.highlight_secondary "D", p.none "ebugger ", p.highlight_secondary "A", p.none "dapter ", p.highlight_secondary "P", p.none "rotocol defines the abstract protocol used", }, { p.none "between a development tool (e.g. IDE or editor) and a debugger.", }, { p.none "This provides editors with a standardized interface for enabling debugging", }, { p.none "capabilities - such as pausing execution, stepping through statements,", }, { p.none "and inspecting variables." }, {}, { p.none "For more information, see:" }, { p.none " - https://microsoft.github.io/debug-adapter-protocol/" }, } end ================================================ FILE: lua/mason/ui/components/help/formatter.lua ================================================ local Ui = require "mason-core.ui" local p = require "mason.ui.palette" ---@param state InstallerUiState return function(state) return Ui.HlTextNode { { p.Bold "What is a formatter?", }, { p.none "A code formatter is a tool that reformats code to fit a certain" }, { p.none "formatting convention. This usually entails things like adjusting" }, { p.none "indentation, breaking long lines into smaller lines, adding or" }, { p.none "removing whitespaces. Formatting rules are often included as a" }, { p.none "separate configuration file within the project." }, } end ================================================ FILE: lua/mason/ui/components/help/init.lua ================================================ local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local log = require "mason-core.log" local p = require "mason.ui.palette" local settings = require "mason.settings" local DAPHelp = require "mason.ui.components.help.dap" local FormatterHelp = require "mason.ui.components.help.formatter" local LSPHelp = require "mason.ui.components.help.lsp" local LinterHelp = require "mason.ui.components.help.linter" ---@param state InstallerUiState local function Ship(state) local ship_indent = { (" "):rep(state.view.ship_indentation), "" } -- stylua: ignore start local ship = { { ship_indent, p.muted "/^v^\\", p.none " | | |" }, { ship_indent, p.none " )_) )_) )_) ", p.muted "/^v^\\" }, { ship_indent, p.muted " ", p.muted "/^v^\\", p.none " )___))___))___)\\ ", p.highlight_secondary(state.view.ship_exclamation) }, { ship_indent, p.none " )____)____)_____)\\\\" }, { ship_indent, p.none " _____|____|____|____\\\\\\__" }, { ship_indent, p.muted " ", p.none "\\ /" }, } -- stylua: ignore end local water = { { p.highlight " ^^^^^ ^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^ <>< " }, { p.highlight " ^^^^ ^^ ^^^ ^ ^^^ ^^^ <>< ^^^^ " }, { p.highlight " ><> ^^^ ^^ ><> ^^ ^^ ^ " }, } if state.view.ship_indentation < 0 then for _, shipline in ipairs(ship) do local removed_chars = 0 for _, span in ipairs(shipline) do local span_length = #span[1] local chars_to_remove = (math.abs(state.view.ship_indentation) - removed_chars) span[1] = string.sub(span[1], chars_to_remove + 1) removed_chars = removed_chars + (span_length - #span[1]) end end end return Ui.Node { Ui.HlTextNode(ship), Ui.HlTextNode(water), } end ---@param state InstallerUiState local function GenericHelp(state) local keymap_tuples = { { "Toggle help", settings.current.ui.keymaps.toggle_help }, { "Toggle package info", settings.current.ui.keymaps.toggle_package_expand }, { "Toggle package installation log", settings.current.ui.keymaps.toggle_package_install_log }, { "Apply language filter", settings.current.ui.keymaps.apply_language_filter }, { "Install package", settings.current.ui.keymaps.install_package }, { "Uninstall package", settings.current.ui.keymaps.uninstall_package }, { "Update package", settings.current.ui.keymaps.update_package }, { "Update all outdated packages", settings.current.ui.keymaps.update_all_packages }, { "Check for new package version", settings.current.ui.keymaps.check_package_version }, { "Check for new versions (all packages)", settings.current.ui.keymaps.check_outdated_packages }, { "Cancel installation of package", settings.current.ui.keymaps.cancel_installation }, { "Close window", "q" }, { "Close window", "" }, } local is_current_settings_expanded = state.view.is_current_settings_expanded return Ui.Node { Ui.HlTextNode { { p.muted "Mason log: ", p.none(log.outfile) }, }, Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Registries", }, { p.muted "Packages are sourced from the following registries:", }, unpack(_.map(function(registry) return { p.none(" - " .. registry.name) } end, state.info.registries)), }, Ui.EmptyLine(), Ui.Table { { p.Bold "Keyboard shortcuts", }, unpack(_.map(function(keymap_tuple) return { p.muted(keymap_tuple[1]), p.highlight(keymap_tuple[2]) } end, keymap_tuples)), }, Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Problems installing packages" }, { p.muted "Make sure you meet the minimum requirements to install packages. For debugging, refer to:", }, }, Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode { { p.highlight ":help mason-debugging", }, { p.highlight ":checkhealth mason", }, }, }), Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Problems with package functionality" }, { p.muted "Please refer to each package's own homepage for further assistance." }, }, Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "How do I use installed packages?" }, { p.muted "Mason only makes packages available for use. It does not automatically integrate" }, { p.muted "these into Neovim. You have multiple different options for using any given" }, { p.muted "package, and you are free to pick and choose as you see fit." }, { p.muted "See ", p.highlight ":help mason-how-to-use-packages", p.muted " for a recommendation.", }, }, Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Missing a package?" }, { p.muted "Please consider contributing to mason.nvim:" }, }, Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode { { p.none "- ", p.highlight "https://github.com/mason-org/mason.nvim/blob/main/CONTRIBUTING.md", }, { p.none "- ", p.highlight "https://github.com/mason-org/mason.nvim/blob/main/doc/reference.md", }, }, }), Ui.EmptyLine(), Ui.HlTextNode { { p.Bold(("%s Current settings"):format(is_current_settings_expanded and "↓" or "→")), p.highlight " :help mason-settings", }, }, Ui.Keybind(settings.current.ui.keymaps.toggle_package_expand, "TOGGLE_EXPAND_CURRENT_SETTINGS", nil), Ui.When(is_current_settings_expanded, function() local settings_split_by_newline = vim.split(vim.inspect(settings.current), "\n") local current_settings = _.map(function(line) return { p.muted(line) } end, settings_split_by_newline) return Ui.HlTextNode(current_settings) end), } end ---@param state InstallerUiState return function(state) ---@type INode local heading = Ui.Node {} if state.view.current == "LSP" then heading = Ui.Node { LSPHelp(state), Ui.EmptyLine(), } elseif state.view.current == "DAP" then heading = Ui.Node { DAPHelp(state), Ui.EmptyLine(), } elseif state.view.current == "Linter" then heading = Ui.Node { LinterHelp(state), Ui.EmptyLine(), } elseif state.view.current == "Formatter" then heading = Ui.Node { FormatterHelp(state), Ui.EmptyLine(), } end return Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode(state.view.has_changed and p.none "" or p.Comment "(change view by pressing its number)"), heading, GenericHelp(state), Ui.EmptyLine(), Ship(state), Ui.EmptyLine(), }) end ================================================ FILE: lua/mason/ui/components/help/linter.lua ================================================ local Ui = require "mason-core.ui" local p = require "mason.ui.palette" ---@param state InstallerUiState return function(state) return Ui.HlTextNode { { p.Bold "What is a linter?", }, { p.none "A linter is a static code analysis tool used to provide diagnostics around" }, { p.none "programming errors, bugs, stylistic errors and suspicious constructs." }, { p.none "Linters can be executed as a standalone program in a terminal, where it" }, { p.none "usually expects one or more input files to lint. There are also Neovim plugins" }, { p.none "that integrate these diagnostics inside the editor." }, } end ================================================ FILE: lua/mason/ui/components/help/lsp.lua ================================================ local Ui = require "mason-core.ui" local p = require "mason.ui.palette" ---@param state InstallerUiState return function(state) return Ui.HlTextNode { { p.Bold "What is LSP?", }, { p.none "The ", p.highlight_secondary "L", p.none "anguage ", p.highlight_secondary "S", p.none "erver ", p.highlight_secondary "P", p.none "rotocol defines the protocol used between an", }, { p.none "editor or IDE and a language server that provides language features", }, { p.none "like auto complete, go to definition, find all references etc.", }, {}, { p.none "The term ", p.highlight_secondary "LSP", p.none " is often used to reference a server implementation of", }, { p.none "the LSP protocol." }, {}, { p.none "For more information, see:" }, { p.none " - https://microsoft.github.io/language-server-protocol/" }, { p.none " - ", p.highlight ":help lsp" }, } end ================================================ FILE: lua/mason/ui/components/json-schema.lua ================================================ -- Here be dragons local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local settings = require "mason.settings" local property_type_highlights = { ["string"] = "String", ["string[]"] = "String", ["boolean"] = "Boolean", ["number"] = "Number", ["number[]"] = "Number", ["integer"] = "Number", ["integer[]"] = "Number", } local function resolve_type(property_schema) if _.is_list(property_schema.type) then return table.concat(property_schema.type, " | ") elseif property_schema.type == "array" then if property_schema.items then return ("%s[]"):format(property_schema.items.type) else return property_schema.type end end return property_schema.type or "N/A" end local function Indent(indentation, children) -- create a list table with as many "INDENT" entries as the numeric indentation variable local indent = {} for _ = 1, indentation do table.insert(indent, "INDENT") end return Ui.CascadingStyleNode(indent, children) end ---@param pkg Package ---@param schema_id string ---@param state UiPackageState ---@param schema table ---@param key string? ---@param level number? ---@param key_width number? The width the key should occupate in the UI to produce an even column. ---@param compound_key string? local function JsonSchema(pkg, schema_id, state, schema, key, level, key_width, compound_key) level = level or 0 compound_key = ("%s%s"):format(compound_key or "", key or "") local toggle_expand_keybind = Ui.Keybind( settings.current.ui.keymaps.toggle_package_expand, "TOGGLE_JSON_SCHEMA_KEY", { package = pkg, schema_id = schema_id, key = compound_key } ) local node_is_expanded = state.expanded_json_schema_keys[schema_id][compound_key] local key_prefix = node_is_expanded and "↓ " or "→ " if (schema.type == "object" or schema.type == nil) and schema.properties then local nodes = {} if key then -- This node belongs to some parent object - render a heading for it. -- It'll act as the anchor for its children. local heading = Ui.HlTextNode { key_prefix .. key, node_is_expanded and "Bold" or "", } nodes[#nodes + 1] = heading nodes[#nodes + 1] = toggle_expand_keybind end -- All level 0 nodes are expanded by default - otherwise we'd not render anything at all if level == 0 or node_is_expanded then local max_property_length = 0 local sorted_properties = {} for property in pairs(schema.properties) do max_property_length = math.max(max_property_length, vim.api.nvim_strwidth(property)) sorted_properties[#sorted_properties + 1] = property end table.sort(sorted_properties) for _, property in ipairs(sorted_properties) do nodes[#nodes + 1] = Indent(level, { JsonSchema( pkg, schema_id, state, schema.properties[property], property, level + 1, max_property_length, compound_key ), }) end end return Ui.Node(nodes) elseif schema.oneOf then local nodes = {} for i, alternative_schema in ipairs(schema.oneOf) do nodes[#nodes + 1] = JsonSchema( pkg, schema_id, state, alternative_schema, ("%s (alt. %d)"):format(key, i), level, key_width, compound_key ) end return Ui.Node(nodes) elseif _.is_list(schema) then return Ui.Node(_.map(function(sub_schema) return JsonSchema(pkg, schema_id, state, sub_schema) end, schema)) elseif level > 0 then -- Leaf nodes cannot occupy the root level. -- Leaf node (aka any type that isn't an object). local type = resolve_type(schema) local heading local label = (key_prefix .. key .. (" "):rep(key_width or 0)):sub(1, key_width + 5) -- + 5 to account for key_prefix plus some extra whitespace if schema.default ~= nil then heading = Ui.HlTextNode { { { label, node_is_expanded and "Bold" or "", }, { " default: ", "Comment", }, { vim.json.encode(schema.default), property_type_highlights[type] or "MasonMuted", }, }, } else heading = Ui.HlTextNode { label, node_is_expanded and "Bold" or "", } end return Ui.Node { heading, toggle_expand_keybind, Ui.When(node_is_expanded, function() local description = _.map(function(line) return { { line, "Comment" } } end, vim.split(schema.description or "No description available.", "\n")) local type_highlight = property_type_highlights[type] or "MasonMuted" local table_rows = { { { "type", "MasonMuted" }, { type, type_highlight } }, } if _.is_list(schema.enum) then for idx, enum in ipairs(schema.enum) do local enum_description = "" if schema.enumDescriptions and schema.enumDescriptions[idx] then enum_description = "- " .. schema.enumDescriptions[idx] end table_rows[#table_rows + 1] = { { idx == 1 and "possible values" or "", "MasonMuted" }, { vim.json.encode(enum), type_highlight }, { enum_description, "Comment" }, } end end return Indent(level, { Ui.HlTextNode(description), Ui.Table(table_rows), }) end), } else return Ui.Node {} end end return JsonSchema ================================================ FILE: lua/mason/ui/components/language-filter.lua ================================================ local Ui = require "mason-core.ui" local p = require "mason.ui.palette" local settings = require "mason.settings" ---@param state InstallerUiState return function(state) return Ui.CascadingStyleNode({ "INDENT" }, { Ui.When(state.view.language_filter, function() return Ui.Node { Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Language Filter: ", p.highlight(state.view.language_filter), p.Comment " press to clear", }, }, } end), Ui.When(not state.view.language_filter, function() return Ui.Node { Ui.EmptyLine(), Ui.HlTextNode { { p.Bold "Language Filter:", p.Comment( (" press %s to apply filter"):format(settings.current.ui.keymaps.apply_language_filter) ), }, }, } end), }) end ================================================ FILE: lua/mason/ui/components/main/init.lua ================================================ local Ui = require "mason-core.ui" local PackageList = require "mason.ui.components.main.package_list" ---@param state InstallerUiState return function(state) return Ui.Node { Ui.EmptyLine(), PackageList(state), } end ================================================ FILE: lua/mason/ui/components/main/package_list.lua ================================================ local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local p = require "mason.ui.palette" local settings = require "mason.settings" local JsonSchema = require "mason.ui.components.json-schema" ---@param props { state: InstallerUiState, heading: INode, packages: Package[], list_item_renderer: (fun(package: Package, state: InstallerUiState): INode), hide_when_empty: boolean } local function PackageListContainer(props) local items = {} for i = 1, #props.packages do local pkg = props.packages[i] if props.state.packages.visible[pkg.name] then items[#items + 1] = props.list_item_renderer(pkg, props.state) end end if props.hide_when_empty and #items == 0 then return Ui.Node {} end return Ui.Node { props.heading, Ui.VirtualTextNode { p.Comment(("(%d)"):format(#items)) }, Ui.CascadingStyleNode({ "INDENT" }, items), Ui.When( #items == 0, Ui.CascadingStyleNode({ "CENTERED" }, { Ui.HlTextNode(p.Comment "No packages."), }) ), Ui.EmptyLine(), } end ---@param executables table? local function ExecutablesTable(executables) if not executables or _.size(executables) == 0 then return Ui.Node {} end local rows = {} for executable in pairs(executables) do table.insert(rows, { p.none "", p.Bold(executable) }) end rows[1][1] = p.muted "executables" return rows end ---@param state InstallerUiState ---@param pkg Package ---@param is_installed boolean local function ExpandedPackageInfo(state, pkg, is_installed) local pkg_state = state.packages.states[pkg.name] return Ui.CascadingStyleNode({ "INDENT" }, { Ui.When(not is_installed and pkg.spec.deprecation, function() return Ui.HlTextNode(p.warning(("Deprecation message: %s"):format(pkg.spec.deprecation.message))) end), Ui.HlTextNode(_.map(function(line) return { p.Comment(line) } end, _.split("\n", pkg.spec.description))), Ui.EmptyLine(), Ui.Table(_.concat( _.filter(_.identity, { is_installed and { p.muted "installed version", pkg_state.version and p.Bold(pkg_state.version) or p.muted "-", } or { p.muted "version", p.Bold(pkg:get_latest_version()), }, pkg_state.new_version and { p.muted "latest version", p.muted(pkg_state.new_version), }, pkg_state.installed_purl and { p.muted "installed purl", p.highlight(pkg_state.installed_purl), } or { p.muted "purl", p.highlight(pkg.spec.source.id), }, { p.muted "homepage", pkg.spec.homepage and p.highlight(pkg.spec.homepage) or p.muted "-", }, { p.muted "languages", #pkg.spec.languages > 0 and p.Bold(table.concat(pkg.spec.languages, ", ")) or p.muted "-", }, { p.muted "categories", #pkg.spec.categories > 0 and p.Bold(table.concat(pkg.spec.categories, ", ")) or p.muted "-", }, }), Ui.When(is_installed, function() return ExecutablesTable(pkg_state.linked_executables) end) )), Ui.When(pkg_state.lsp_settings_schema ~= nil, function() local has_expanded = pkg_state.expanded_json_schemas["lsp"] return Ui.Node { Ui.EmptyLine(), Ui.HlTextNode { { p.Bold(("%s LSP server configuration schema"):format(has_expanded and "↓" or "→")), p.Comment((" (press enter to %s)"):format(has_expanded and "collapse" or "expand")), }, }, Ui.Keybind( settings.current.ui.keymaps.toggle_package_expand, "TOGGLE_JSON_SCHEMA", { package = pkg, schema_id = "lsp" } ), Ui.When(has_expanded, function() return Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode( p.muted "This is a read-only overview of the settings this server accepts. Note that some settings might not apply to neovim." ), Ui.EmptyLine(), JsonSchema(pkg, "lsp", pkg_state, pkg_state.lsp_settings_schema), }) end), } end), Ui.EmptyLine(), }) end local get_package_search_keywords = _.compose(_.join ", ", _.map(_.to_lower), _.path { "spec", "languages" }) ---@param state InstallerUiState ---@param pkg Package ---@param opts { keybinds: KeybindHandlerNode[], icon: string[], is_installed: boolean, sticky: StickyCursorNode? } local function PackageComponent(state, pkg, opts) local pkg_state = state.packages.states[pkg.name] local is_expanded = state.packages.expanded == pkg.name local label = (is_expanded or pkg_state.has_transitioned) and p.Bold(" " .. pkg.name) or p.none(" " .. pkg.name) local package_line = { opts.icon, label, } local pkg_aliases = pkg:get_aliases() if #pkg_aliases > 0 then package_line[#package_line + 1] = p.Comment(" " .. table.concat(pkg:get_aliases(), ", ")) end if state.view.is_searching then package_line[#package_line + 1] = p.Comment((" (keywords: %s)"):format(get_package_search_keywords(pkg))) end if not opts.is_installed and pkg.spec.deprecation ~= nil then package_line[#package_line + 1] = p.warning " deprecated" end return Ui.Node { Ui.HlTextNode { package_line }, opts.sticky or Ui.Node {}, Ui.When(opts.is_installed and pkg.spec.deprecation ~= nil, function() return Ui.DiagnosticsNode { message = ("deprecated: %s"):format(pkg.spec.deprecation.message), severity = vim.diagnostic.severity.WARN, source = ("Deprecated since version %s"):format(pkg.spec.deprecation.since), } end), Ui.Keybind(settings.current.ui.keymaps.check_package_version, "CHECK_NEW_PACKAGE_VERSION", pkg), Ui.When(pkg_state.new_version ~= nil, function() return Ui.DiagnosticsNode { message = ("new version available: %s -> %s"):format(pkg_state.version or "-", pkg_state.new_version), severity = vim.diagnostic.severity.INFO, } end), Ui.Node(opts.keybinds), Ui.When(is_expanded, function() return ExpandedPackageInfo(state, pkg, opts.is_installed) end), } end local get_outdated_packages_preview = _.if_else( _.compose(_.lte(4), _.size), _.compose(_.join ", ", _.map(_.prop "name")), _.compose( _.join ", ", _.converge(_.concat, { _.compose(_.map(_.prop "name"), _.take(3)), function(pkgs) return { ("and %d more…"):format(#pkgs - 3) } end, }) ) ) ---@param state InstallerUiState local function Installed(state) return Ui.Node { Ui.Keybind(settings.current.ui.keymaps.check_outdated_packages, "UPDATE_REGISTRY", nil, true), PackageListContainer { state = state, heading = Ui.Node { Ui.HlTextNode(p.heading "Installed"), Ui.When(state.info.registry_update.in_progress, function() local styling = state.info.registry_update.percentage_complete == 1 and p.highlight_block or p.muted_block local is_all_registries_installed = _.all(_.prop "is_installed", state.info.registries) local registry_count = #state.info.registries local text if registry_count > 1 then text = p.Comment( is_all_registries_installed and ("updating %d registries "):format(registry_count) or ("installing %d registries "):format(registry_count) ) else text = p.Comment(is_all_registries_installed and "updating registry " or "installing registry ") end return Ui.VirtualTextNode { text, styling( ("%-4s"):format(math.floor(state.info.registry_update.percentage_complete * 100) .. "%") ), styling((" "):rep(state.info.registry_update.percentage_complete * 15)), } end), Ui.When( not state.info.registry_update.in_progress and #state.packages.outdated_packages > 0, function() return Ui.VirtualTextNode { p.muted "Press ", p.highlight(settings.current.ui.keymaps.update_all_packages), p.muted " to update ", p.highlight(tostring(#state.packages.outdated_packages)), p.muted(#state.packages.outdated_packages > 1 and " packages " or " package "), p.Comment(("(%s)"):format(get_outdated_packages_preview(state.packages.outdated_packages))), } end ), }, packages = state.packages.installed, ---@param pkg Package list_item_renderer = function(pkg) return PackageComponent(state, pkg, { is_installed = true, icon = p.highlight(settings.current.ui.icons.package_installed), keybinds = { Ui.Keybind(settings.current.ui.keymaps.update_package, "INSTALL_PACKAGE", pkg), Ui.Keybind(settings.current.ui.keymaps.uninstall_package, "UNINSTALL_PACKAGE", pkg), Ui.Keybind(settings.current.ui.keymaps.toggle_package_expand, "TOGGLE_EXPAND_PACKAGE", pkg), }, sticky = Ui.StickyCursor { id = ("%s-installed"):format(pkg.name) }, }) end, }, } end ---@param pkg Package ---@param state InstallerUiState local function InstallingPackageComponent(pkg, state) ---@type UiPackageState local pkg_state = state.packages.states[pkg.name] local current_state = pkg_state.is_terminated and p.Comment " (cancelling)" or p.none "" local tail = pkg_state.short_tailed_output and ("▶ # [%d/%d] %s"):format( #pkg_state.tailed_output, #pkg_state.tailed_output, pkg_state.short_tailed_output ) or "" return Ui.Node { Ui.HlTextNode { { pkg_state.has_failed and p.error(settings.current.ui.icons.package_uninstalled) or p.highlight(settings.current.ui.icons.package_pending), p.none(" " .. pkg.name), current_state, pkg_state.latest_spawn and p.Comment((" $ %s"):format(pkg_state.latest_spawn)) or p.none "", }, }, Ui.StickyCursor { id = ("%s-installing"):format(pkg.name) }, Ui.Keybind(settings.current.ui.keymaps.cancel_installation, "TERMINATE_PACKAGE_HANDLE", pkg), Ui.Keybind(settings.current.ui.keymaps.install_package, "INSTALL_PACKAGE", pkg), Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode(pkg_state.is_log_expanded and p.Bold "▼ Displaying full log" or p.muted(tail)), Ui.Keybind(settings.current.ui.keymaps.toggle_package_install_log, "TOGGLE_INSTALL_LOG", pkg), Ui.StickyCursor { id = ("%s-toggle-install-log"):format(pkg.name) }, }), Ui.When(pkg_state.is_log_expanded, function() return Ui.CascadingStyleNode({ "INDENT", "INDENT" }, { Ui.HlTextNode(_.map(function(line) return { p.muted(line) } end, pkg_state.tailed_output)), }) end), } end ---@param state InstallerUiState local function Installing(state) local packages = state.packages.installing return PackageListContainer { state = state, heading = Ui.Node { Ui.HlTextNode(p.heading "Installing"), Ui.StickyCursor { id = "installing-section" }, Ui.Keybind(settings.current.ui.keymaps.cancel_installation, "TERMINATE_PACKAGE_HANDLES", packages), }, hide_when_empty = true, packages = packages, ---@param pkg Package list_item_renderer = InstallingPackageComponent, } end ---@param state InstallerUiState local function Queued(state) local packages = state.packages.queued return PackageListContainer { state = state, heading = Ui.Node { Ui.HlTextNode(p.heading "Queued"), Ui.StickyCursor { id = "queued-section" }, Ui.Keybind(settings.current.ui.keymaps.cancel_installation, "TERMINATE_PACKAGE_HANDLES", packages), }, packages = packages, hide_when_empty = true, ---@param pkg Package list_item_renderer = function(pkg) return Ui.Node { Ui.HlTextNode { { p.highlight(settings.current.ui.icons.package_pending), p.none(" " .. pkg.name) }, }, Ui.StickyCursor { id = ("%s-installing"):format(pkg.spec.name) }, Ui.Keybind(settings.current.ui.keymaps.cancel_installation, "TERMINATE_PACKAGE_HANDLE", pkg), } end, } end ---@param state InstallerUiState local function Failed(state) local packages = state.packages.failed if #packages == 0 then return Ui.Node {} end return PackageListContainer { state = state, heading = Ui.HlTextNode(p.heading "Failed"), packages = packages, list_item_renderer = InstallingPackageComponent, } end ---@param state InstallerUiState local function Uninstalled(state) return PackageListContainer { state = state, heading = Ui.HlTextNode(p.heading "Available"), packages = state.packages.uninstalled, ---@param pkg Package list_item_renderer = function(pkg) return PackageComponent(state, pkg, { icon = p.muted(settings.current.ui.icons.package_uninstalled), keybinds = { Ui.Keybind(settings.current.ui.keymaps.install_package, "INSTALL_PACKAGE", pkg), Ui.Keybind(settings.current.ui.keymaps.toggle_package_expand, "TOGGLE_EXPAND_PACKAGE", pkg), }, sticky = Ui.StickyCursor { id = ("%s-uninstalled"):format(pkg.name) }, }) end, } end ---@param state InstallerUiState return function(state) return Ui.CascadingStyleNode({ "INDENT" }, { Failed(state), Installing(state), Queued(state), Installed(state), Uninstalled(state), }) end ================================================ FILE: lua/mason/ui/components/tabs.lua ================================================ local Package = require "mason-core.package" local Ui = require "mason-core.ui" local p = require "mason.ui.palette" ---@param text string ---@param index integer ---@param is_active boolean ---@param use_secondary_highlight boolean local function create_tab_span(text, index, is_active, use_secondary_highlight) local highlight_block = use_secondary_highlight and p.highlight_block_bold_secondary or p.highlight_block_bold if is_active then return { highlight_block " ", highlight_block("(" .. index .. ")"), highlight_block(" " .. text .. " "), p.none " ", } else return { p.muted_block " ", p.muted_block("(" .. index .. ")"), p.muted_block(" " .. text .. " "), p.none " ", } end end ---@param state InstallerUiState return function(state) local tabs = {} for i, text in ipairs { "All", Package.Cat.LSP, Package.Cat.DAP, Package.Cat.Linter, Package.Cat.Formatter } do vim.list_extend(tabs, create_tab_span(text, i, state.view.current == text, state.view.is_showing_help)) end return Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode { tabs }, Ui.StickyCursor { id = "tabs" }, }) end ================================================ FILE: lua/mason/ui/init.lua ================================================ local M = {} function M.close() local api = require "mason.ui.instance" api.close() end function M.open() local api = require "mason.ui.instance" api.window.open() end ---@param view string function M.set_view(view) local api = require "mason.ui.instance" api.set_view(view) end ---@param tag any function M.set_sticky_cursor(tag) local api = require "mason.ui.instance" api.set_sticky_cursor(tag) end return M ================================================ FILE: lua/mason/ui/instance.lua ================================================ -- !!! -- in dire need of rework, proceed with caution -- !!! local Package = require "mason-core.package" local Ui = require "mason-core.ui" local _ = require "mason-core.functional" local a = require "mason-core.async" local display = require "mason-core.ui.display" local notify = require "mason-core.notify" local registry = require "mason-registry" local settings = require "mason.settings" local Header = require "mason.ui.components.header" local Help = require "mason.ui.components.help" local LanguageFilter = require "mason.ui.components.language-filter" local Main = require "mason.ui.components.main" local Tabs = require "mason.ui.components.tabs" require "mason.ui.colors" ---@param state InstallerUiState local function GlobalKeybinds(state) return Ui.Node { Ui.Keybind(settings.current.ui.keymaps.toggle_help, "TOGGLE_HELP", nil, true), Ui.Keybind("q", "CLOSE_WINDOW", nil, true), Ui.When(not state.view.language_filter, Ui.Keybind("", "CLOSE_WINDOW", nil, true)), Ui.When(state.view.language_filter, Ui.Keybind("", "CLEAR_LANGUAGE_FILTER", nil, true)), Ui.When(state.view.is_searching, Ui.Keybind("", "CLEAR_SEARCH_MODE", nil, true)), Ui.Keybind(settings.current.ui.keymaps.apply_language_filter, "LANGUAGE_FILTER", nil, true), Ui.Keybind(settings.current.ui.keymaps.update_all_packages, "UPDATE_ALL_PACKAGES", nil, true), Ui.Keybind("1", "SET_VIEW", "All", true), Ui.Keybind("2", "SET_VIEW", "LSP", true), Ui.Keybind("3", "SET_VIEW", "DAP", true), Ui.Keybind("4", "SET_VIEW", "Linter", true), Ui.Keybind("5", "SET_VIEW", "Formatter", true), } end ---@class UiPackageState ---@field expanded_json_schema_keys table> ---@field expanded_json_schemas table ---@field has_expanded_before boolean ---@field has_transitioned boolean ---@field is_terminated boolean ---@field is_log_expanded boolean ---@field has_failed boolean ---@field latest_spawn string? ---@field linked_executables table? ---@field installed_purl string? ---@field lsp_settings_schema table? ---@field new_version string? ---@field short_tailed_output string? ---@field tailed_output string[] ---@field version string? ---@class InstallerUiState local INITIAL_STATE = { info = { ---@type string | nil used_disk_space = nil, ---@type { name: string, is_installed: boolean }[] registries = {}, registry_update = { ---@type string? error = nil, in_progress = false, percentage_complete = 0, }, }, view = { is_searching = false, is_showing_help = false, is_current_settings_expanded = false, language_filter = nil, current = "All", has_changed = false, ship_indentation = 0, ship_exclamation = "", }, header = { title_prefix = "", -- for animation }, packages = { ---@type Package[] outdated_packages = {}, ---@type Package[] all = {}, ---@type table visible = {}, ---@type string|nil expanded = nil, ---@type table states = {}, ---@type Package[] installed = {}, ---@type Package[] installing = {}, ---@type Package[] failed = {}, ---@type Package[] queued = {}, ---@type Package[] uninstalled = {}, }, } ---@generic T ---@param list T[] ---@param item T ---@return T local function remove(list, item) for i, v in ipairs(list) do if v == item then table.remove(list, i) return list end end return list end local window = display.new_view_only_win("mason.nvim", "mason") window.view( ---@param state InstallerUiState function(state) return Ui.Node { GlobalKeybinds(state), Header(state), Tabs(state), Ui.When(state.view.is_showing_help, function() return Help(state) end), Ui.When(not state.view.is_showing_help, function() return Ui.Node { LanguageFilter(state), Main(state), } end), } end ) local mutate_state, get_state = window.state(INITIAL_STATE) window.events:on("search:enter", function() mutate_state(function(state) state.view.is_searching = true end) vim.schedule(function() vim.cmd "redraw" end) end) window.events:on("search:leave", function(search) if search == "" and vim.fn.getreg "/" == "" then mutate_state(function(state) state.view.is_searching = false end) end end) ---@param pkg Package ---@param group string ---@param tail boolean? Whether to insert at the end. local function mutate_package_grouping(pkg, group, tail) mutate_state(function(state) remove(state.packages.installing, pkg) remove(state.packages.queued, pkg) remove(state.packages.uninstalled, pkg) remove(state.packages.installed, pkg) remove(state.packages.failed, pkg) if tail then table.insert(state.packages[group], pkg) else table.insert(state.packages[group], 1, pkg) end state.packages.states[pkg.name].has_transitioned = true end) end ---@param mutate_fn fun(state: InstallerUiState) local function mutate_package_visibility(mutate_fn) mutate_state(function(state) mutate_fn(state) local view_predicate = { ["All"] = _.T, ["LSP"] = _.prop_satisfies(_.any(_.equals(Package.Cat.LSP)), "categories"), ["DAP"] = _.prop_satisfies(_.any(_.equals(Package.Cat.DAP)), "categories"), ["Linter"] = _.prop_satisfies(_.any(_.equals(Package.Cat.Linter)), "categories"), ["Formatter"] = _.prop_satisfies(_.any(_.equals(Package.Cat.Formatter)), "categories"), } local language_predicate = _.if_else( _.always(state.view.language_filter), _.prop_satisfies(_.any(_.equals(state.view.language_filter)), "languages"), _.T ) for __, pkg in ipairs(state.packages.all) do state.packages.visible[pkg.name] = _.all_pass({ view_predicate[state.view.current], language_predicate }, pkg.spec) end end) end ---@return UiPackageState local function create_initial_package_state() return { expanded_json_schema_keys = {}, expanded_json_schemas = {}, has_expanded_before = false, has_transitioned = false, is_terminated = false, is_log_expanded = false, has_failed = false, latest_spawn = nil, linked_executables = nil, installed_purl = nil, lsp_settings_schema = nil, new_version = nil, short_tailed_output = nil, tailed_output = {}, version = nil, } end ---@param handle InstallHandle local function setup_handle(handle) local function handle_state_change() if handle.state == "QUEUED" then mutate_package_grouping(handle.package, "queued", true) elseif handle.state == "ACTIVE" then mutate_package_grouping(handle.package, "installing", true) elseif handle.state == "CLOSED" then mutate_state(function(state) state.packages.states[handle.package.name].is_terminated = false end) end end local function handle_spawnhandle_change() mutate_state(function(state) state.packages.states[handle.package.name].latest_spawn = handle:peek_spawn_handle():map(tostring):map(_.gsub("\n", "\\n ")):or_else(nil) end) end ---@param chunk string local function handle_output(chunk) mutate_state(function(state) local pkg_state = state.packages.states[handle.package.name] local lines = vim.split(chunk, "\n") for i = 1, #lines do local line = lines[i] if i == 1 and pkg_state.tailed_output[#pkg_state.tailed_output] then pkg_state.tailed_output[#pkg_state.tailed_output] = pkg_state.tailed_output[#pkg_state.tailed_output] .. line else pkg_state.tailed_output[#pkg_state.tailed_output + 1] = line end if not line:match "^%s*$" then pkg_state.short_tailed_output = line:gsub("^%s+", "") end end end) end local function handle_terminate() mutate_state(function(state) state.packages.states[handle.package.name].is_terminated = handle.is_terminated if handle:is_queued() then -- This is really already handled by the "install:failed" handler, but for UX reasons we handle -- terminated, queued, handlers here. The reason for this is that a queued handler, which is -- aborted, will not fail its installation until it acquires a semaphore permit, leading to a weird -- UX that may be perceived as non-functional. mutate_package_grouping(handle.package, handle.package:is_installed() and "installed" or "uninstalled") end end) end handle:on("terminate", handle_terminate) handle:on("state:change", handle_state_change) handle:on("spawn_handles:change", handle_spawnhandle_change) handle:on("stdout", handle_output) handle:on("stderr", handle_output) -- hydrate initial state handle_state_change() handle_spawnhandle_change() mutate_state(function(state) state.packages.states[handle.package.name] = create_initial_package_state() end) end ---@param pkg Package local function hydrate_detailed_package_state(pkg) mutate_state(function(state) -- initialize expanded keys table state.packages.states[pkg.name].expanded_json_schema_keys["lsp"] = state.packages.states[pkg.name].expanded_json_schema_keys["lsp"] or {} state.packages.states[pkg.name].lsp_settings_schema = pkg:get_lsp_settings_schema():or_else(nil) state.packages.states[pkg.name].version = pkg:get_installed_version() end) pkg:get_receipt():if_present( ---@param receipt InstallReceipt function(receipt) mutate_state(function(state) state.packages.states[pkg.name].linked_executables = receipt:get_links().bin state.packages.states[pkg.name].installed_purl = receipt:get_installed_purl() end) end ) end local help_animation do local help_command = ":help" local help_command_len = #help_command help_animation = Ui.animation { function(tick) mutate_state(function(state) state.header.title_prefix = help_command:sub(help_command_len - tick, help_command_len) end) end, range = { 0, help_command_len }, delay_ms = 80, } end local ship_animation = Ui.animation { function(tick) mutate_state(function(state) state.view.ship_indentation = tick if tick > -5 then state.view.ship_exclamation = "https://github.com/sponsors/williamboman" elseif tick > -27 then state.view.ship_exclamation = "Sponsor mason.nvim development!" else state.view.ship_exclamation = "" end end) end, range = { -35, 5 }, delay_ms = 250, } local function toggle_help() mutate_state(function(state) state.view.is_showing_help = not state.view.is_showing_help if state.view.is_showing_help then help_animation() ship_animation() end end) end local function set_view(event) local view = event.payload mutate_package_visibility(function(state) state.view.current = view state.view.has_changed = true end) if window.is_open() then local cursor_line = window.get_cursor()[1] if cursor_line > (window.get_win_config().height * 0.75) then window.set_sticky_cursor "tabs" end end end local function terminate_package_handle(event) ---@type Package local pkg = event.payload pkg:get_install_handle():if_present( ---@param handle InstallHandle function(handle) if not handle:is_closed() then vim.schedule_wrap(notify)(("Cancelling installation of %q."):format(pkg.name)) handle:terminate() end end ) pkg:get_uninstall_handle():if_present( ---@param handle InstallHandle function(handle) if not handle:is_closed() then vim.schedule_wrap(notify)(("Cancelling uninstallation of %q."):format(pkg.name)) handle:terminate() end end ) end local function terminate_all_package_handles(event) ---@type Package[] local pkgs = _.list_copy(event.payload) -- we copy because list is mutated while iterating it for _, pkg in ipairs(pkgs) do pkg:get_install_handle():if_present( ---@param handle InstallHandle function(handle) if not handle:is_closed() then handle:terminate() end end ) end end local function install_package(event) ---@type AbstractPackage local pkg = event.payload if not pkg:is_installing() then pkg:install() end mutate_state(function(state) state.packages.outdated_packages = _.filter(_.complement(_.equals(pkg)), state.packages.outdated_packages) end) end local function uninstall_package(event) ---@type AbstractPackage local pkg = event.payload if not pkg:is_uninstalling() then pkg:uninstall() end end local function toggle_expand_package(event) ---@type Package local pkg = event.payload mutate_state(function(state) if state.packages.expanded == pkg.name then state.packages.expanded = nil else if not state.packages.states[pkg.name].has_expanded_before then hydrate_detailed_package_state(pkg) state.packages.states[pkg.name].has_expanded_before = true end state.packages.expanded = pkg.name end end) end ---@param pkg Package local function check_new_package_version(pkg) local installed_version = pkg:get_installed_version() mutate_state(function(state) state.packages.states[pkg.name].version = installed_version end) local latest_version = pkg:get_latest_version() if latest_version ~= installed_version and pkg:is_installable { version = latest_version } then mutate_state(function(state) state.packages.states[pkg.name].new_version = latest_version end) return true else mutate_state(function(state) state.packages.states[pkg.name].new_version = nil end) return false end end local function check_new_package_versions() mutate_state(function(state) local outdated_packages = {} for _, pkg in ipairs(state.packages.installed) do local current_version = pkg:get_installed_version() local latest_version = pkg:get_latest_version() if current_version ~= latest_version then state.packages.states[pkg.name].version = current_version state.packages.states[pkg.name].new_version = latest_version table.insert(outdated_packages, pkg) else state.packages.states[pkg.name].new_version = nil end end state.packages.outdated_packages = outdated_packages end) end local function toggle_json_schema(event) local package, schema_id = event.payload.package, event.payload.schema_id mutate_state(function(state) state.packages.states[package.name].expanded_json_schemas[schema_id] = not state.packages.states[package.name].expanded_json_schemas[schema_id] end) end local function toggle_json_schema_keys(event) local package, schema_id, key = event.payload.package, event.payload.schema_id, event.payload.key mutate_state(function(state) state.packages.states[package.name].expanded_json_schema_keys[schema_id][key] = not state.packages.states[package.name].expanded_json_schema_keys[schema_id][key] end) end local function filter() vim.ui.select(_.sort_by(_.identity, _.keys(Package.Lang)), { prompt = "Select language:", kind = "mason.ui.language-filter", }, function(choice) if not choice or choice == "" then return end mutate_package_visibility(function(state) state.view.language_filter = choice end) end) end local function clear_filter() mutate_package_visibility(function(state) state.view.language_filter = nil end) end local function clear_search_mode() mutate_state(function(state) state.view.is_searching = false end) end local function toggle_expand_current_settings() mutate_state(function(state) state.view.is_current_settings_expanded = not state.view.is_current_settings_expanded end) end local function update_all_packages() local state = get_state() _.each(function(pkg) pkg:install() end, state.packages.outdated_packages) mutate_state(function(state) state.packages.outdated_packages = {} end) end local function toggle_install_log(event) ---@type Package local pkg = event.payload mutate_state(function(state) state.packages.states[pkg.name].is_log_expanded = not state.packages.states[pkg.name].is_log_expanded end) end local effects = { ["CHECK_NEW_PACKAGE_VERSION"] = a.scope(_.compose(_.partial(pcall, check_new_package_version), _.prop "payload")), ["UPDATE_REGISTRY"] = function() registry.update() end, ["CLEAR_LANGUAGE_FILTER"] = clear_filter, ["CLEAR_SEARCH_MODE"] = clear_search_mode, ["CLOSE_WINDOW"] = window.close, ["INSTALL_PACKAGE"] = install_package, ["LANGUAGE_FILTER"] = filter, ["SET_VIEW"] = set_view, ["TERMINATE_PACKAGE_HANDLE"] = terminate_package_handle, ["TERMINATE_PACKAGE_HANDLES"] = terminate_all_package_handles, ["TOGGLE_EXPAND_CURRENT_SETTINGS"] = toggle_expand_current_settings, ["TOGGLE_EXPAND_PACKAGE"] = toggle_expand_package, ["TOGGLE_HELP"] = toggle_help, ["TOGGLE_INSTALL_LOG"] = toggle_install_log, ["TOGGLE_JSON_SCHEMA"] = toggle_json_schema, ["TOGGLE_JSON_SCHEMA_KEY"] = toggle_json_schema_keys, ["UNINSTALL_PACKAGE"] = uninstall_package, ["UPDATE_ALL_PACKAGES"] = update_all_packages, } local registered_packages = {} ---@param pkg Package local function setup_package(pkg) if registered_packages[pkg] then return end mutate_state(function(state) for _, group in ipairs { state.packages.installed, state.packages.uninstalled, state.packages.failed, state.packages.outdated_packages, } do for i, existing_pkg in ipairs(group) do if existing_pkg.name == pkg.name and pkg ~= existing_pkg then -- New package instance (i.e. from a new, updated, registry source). -- Release the old package instance. table.remove(group, i) end end end end) -- hydrate initial state mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() state.packages.visible[pkg.name] = true table.insert(state.packages[pkg:is_installed() and "installed" or "uninstalled"], pkg) end) pkg:get_install_handle():if_present(setup_handle) pkg:on("install:handle", setup_handle) pkg:on("install:success", function() vim.schedule(function() notify(("%s was successfully installed."):format(pkg.name)) end) mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() if state.packages.expanded == pkg.name then hydrate_detailed_package_state(pkg) end end) mutate_package_grouping(pkg, "installed") end) pkg:on( "install:failed", ---@param handle InstallHandle function(handle) if handle.is_terminated then -- If installation was explicitly terminated - restore to "pristine" state mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() end) mutate_package_grouping(pkg, pkg:is_installed() and "installed" or "uninstalled") else vim.schedule(function() notify(("%s failed to install."):format(pkg.name), vim.log.levels.ERROR) end) mutate_package_grouping(pkg, "failed") mutate_state(function(state) state.packages.states[pkg.name].has_failed = true end) end end ) pkg:on("uninstall:success", function() if pkg:is_installing() then -- We don't care about uninstallations that occur during installation because it's expected behaviour and -- not constructive to surface to users. return end vim.schedule(function() notify(("%s was successfully uninstalled."):format(pkg.name)) end) mutate_state(function(state) state.packages.states[pkg.name] = create_initial_package_state() state.packages.outdated_packages = _.filter(_.complement(_.equals(pkg)), state.packages.outdated_packages) end) mutate_package_grouping(pkg, "uninstalled") end) registered_packages[pkg] = true end local function update_registry_info() local registries = {} for source in registry.sources:iterate { include_uninstalled = true, include_synthesized = false } do table.insert(registries, { name = source:get_display_name(), is_installed = source:is_installed(), }) end mutate_state(function(state) state.info.registries = registries end) end ---@param packages Package[] local function setup_packages(packages) for _, pkg in ipairs(_.sort_by(_.prop "name", packages)) do setup_package(pkg) end mutate_state(function(state) state.packages.all = packages end) end registry:on("update:failed", function(errors) mutate_state(function(state) state.info.registry_update.percentage_complete = 0 state.info.registry_update.in_progress = false state.info.registry_update.error = table.concat(errors, " - ") end) end) registry:on("update:success", function() setup_packages(registry.get_all_packages()) update_registry_info() check_new_package_versions() -- Wait with resetting the state in order to keep displaying the update message vim.defer_fn(function() mutate_state(function(state) if state.info.registry_update.percentage_complete ~= 1 then -- New update was started already, don't reset state return end state.info.registry_update.in_progress = false state.info.registry_update.percentage_complete = 0 end) end, 1000) end) registry:on("update:start", function() mutate_state(function(state) state.packages.outdated_packages = {} state.info.registry_update.error = nil state.info.registry_update.in_progress = true state.info.registry_update.percentage_complete = 0 end) end) registry:on("update:progress", function(finished, all) mutate_state(function(state) state.info.registry_update.percentage_complete = #finished / #all end) end) update_registry_info() if registry.sources:is_all_installed() then setup_packages(registry.get_all_packages()) end if settings.current.ui.check_outdated_packages_on_open then registry.update() else registry.refresh(function(success, updated_registries) if success and #updated_registries == 0 then setup_packages(registry.get_all_packages()) update_registry_info() end end) end local border = settings.current.ui.border if border == nil and vim.fn.exists "&winborder" == 0 then border = "none" end window.init { effects = effects, border = border, winhighlight = { "NormalFloat:MasonNormal", }, } return { window = window, set_view = function(view) set_view { payload = view } end, set_sticky_cursor = function(tag) window.set_sticky_cursor(tag) end, } ================================================ FILE: lua/mason/ui/palette.lua ================================================ local M = {} local function hl(highlight) return function(text) return { text, highlight } end end -- aliases M.none = hl "" M.header = hl "MasonHeader" M.header_secondary = hl "MasonHeaderSecondary" M.muted = hl "MasonMuted" M.muted_block = hl "MasonMutedBlock" M.muted_block_bold = hl "MasonMutedBlockBold" M.highlight = hl "MasonHighlight" M.highlight_block = hl "MasonHighlightBlock" M.highlight_block_bold = hl "MasonHighlightBlockBold" M.highlight_block_secondary = hl "MasonHighlightBlockSecondary" M.highlight_block_bold_secondary = hl "MasonHighlightBlockBoldSecondary" M.highlight_secondary = hl "MasonHighlightSecondary" M.error = hl "MasonError" M.warning = hl "MasonWarning" M.heading = hl "MasonHeading" setmetatable(M, { __index = function(self, key) self[key] = hl(key) return self[key] end, }) return M ================================================ FILE: lua/mason/version.lua ================================================ local M = {} M.VERSION = "v2.2.1" -- x-release-please-version M.MAJOR_VERSION = 2 -- x-release-please-major M.MINOR_VERSION = 2 -- x-release-please-minor M.PATCH_VERSION = 1 -- x-release-please-patch return M ================================================ FILE: lua/mason-core/EventEmitter.lua ================================================ local log = require "mason-core.log" ---@class EventEmitter ---@field private __event_handlers table> ---@field private __event_handlers_once table> local EventEmitter = {} EventEmitter.__index = EventEmitter function EventEmitter:new() local instance = {} setmetatable(instance, self) instance.__event_handlers = {} instance.__event_handlers_once = {} return instance end ---@generic T ---@param obj T ---@return T function EventEmitter.init(obj) obj.__event_handlers = {} obj.__event_handlers_once = {} return obj end ---@param event any ---@param handler fun(...): any local function call_handler(event, handler, ...) local ok, err = pcall(handler, ...) if not ok then log.fmt_warn("EventEmitter handler failed for event %s with error %s", event, err) end end ---@param event any function EventEmitter:emit(event, ...) if self.__event_handlers[event] then for handler in pairs(self.__event_handlers[event]) do call_handler(event, handler, ...) end end if self.__event_handlers_once[event] then for handler in pairs(self.__event_handlers_once[event]) do call_handler(event, handler, ...) self.__event_handlers_once[event][handler] = nil end end return self end ---@param event any ---@param handler fun(payload: any) function EventEmitter:on(event, handler) if not self.__event_handlers[event] then self.__event_handlers[event] = {} end self.__event_handlers[event][handler] = handler return self end ---@param event any ---@param handler fun(payload: any) function EventEmitter:once(event, handler) if not self.__event_handlers_once[event] then self.__event_handlers_once[event] = {} end self.__event_handlers_once[event][handler] = handler return self end ---@param event any ---@param handler fun(payload: any) function EventEmitter:off(event, handler) if self.__event_handlers[event] then self.__event_handlers[event][handler] = nil end if self.__event_handlers_once[event] then self.__event_handlers_once[event][handler] = nil end return self end ---@private function EventEmitter:__clear_event_handlers() self.__event_handlers = {} self.__event_handlers_once = {} end return EventEmitter ================================================ FILE: lua/mason-core/async/control.lua ================================================ local a = require "mason-core.async" ---@class Condvar local Condvar = {} Condvar.__index = Condvar function Condvar:new() ---@type Condvar local instance = {} setmetatable(instance, self) instance.handles = {} return instance end ---@async function Condvar:wait() a.wait(function(resolve) self.handles[#self.handles + 1] = resolve end) end function Condvar:notify() local handle = table.remove(self.handles) pcall(handle) end function Condvar:notify_all() while #self.handles > 0 do self:notify() end self.handles = {} end ---@class Permit local Permit = {} Permit.__index = Permit function Permit:new(semaphore) ---@type Permit local instance = {} setmetatable(instance, self) instance.semaphore = semaphore return instance end function Permit:forget() local semaphore = self.semaphore semaphore.permits = semaphore.permits + 1 if semaphore.permits > 0 and #semaphore.handles > 0 then semaphore.permits = semaphore.permits - 1 local release = table.remove(semaphore.handles, 1) release() end end ---@class Semaphore local Semaphore = {} Semaphore.__index = Semaphore ---@param permits integer function Semaphore:new(permits) ---@type Semaphore local instance = {} setmetatable(instance, self) instance.permits = permits instance.handles = {} return instance end ---@async function Semaphore:acquire() if self.permits > 0 then self.permits = self.permits - 1 else a.wait(function(resolve) table.insert(self.handles, resolve) end) end return Permit:new(self) end ---@class OneShotChannel ---@field has_sent boolean ---@field value any ---@field condvar Condvar local OneShotChannel = {} OneShotChannel.__index = OneShotChannel function OneShotChannel:new() ---@type OneShotChannel local instance = {} setmetatable(instance, self) instance.has_sent = false instance.value = nil instance.condvar = Condvar:new() return instance end function OneShotChannel:is_closed() return self.has_sent end function OneShotChannel:send(...) assert(not self.has_sent, "Oneshot channel can only send once.") self.has_sent = true self.value = { ... } self.condvar:notify_all() self.condvar = nil end function OneShotChannel:receive() if not self.has_sent then self.condvar:wait() end return unpack(self.value) end ---@class Channel ---@field private condvar Condvar ---@field private buffer any? ---@field is_closed boolean local Channel = {} Channel.__index = Channel function Channel:new() ---@type Channel local instance = {} setmetatable(instance, self) instance.condvar = Condvar:new() instance.buffer = nil instance.is_closed = false return instance end function Channel:close() self.is_closed = true end ---@async function Channel:send(value) assert(not self.is_closed, "Channel is closed.") while self.buffer ~= nil do self.condvar:wait() end self.buffer = value self.condvar:notify() while self.buffer ~= nil do self.condvar:wait() end end ---@async function Channel:receive() assert(not self.is_closed, "Channel is closed.") while self.buffer == nil do self.condvar:wait() end local value = self.buffer self.buffer = nil self.condvar:notify() return value end ---@async function Channel:iter() return function() while not self.is_closed do return self:receive() end end end return { Condvar = Condvar, Semaphore = Semaphore, OneShotChannel = OneShotChannel, Channel = Channel, } ================================================ FILE: lua/mason-core/async/init.lua ================================================ local _ = require "mason-core.functional" local co = coroutine local exports = {} local Promise = {} Promise.__index = Promise function Promise:new(resolver) local instance = {} setmetatable(instance, self) instance.resolver = resolver instance.has_resolved = false return instance end ---@param success boolean ---@param cb fun(success: boolean, value: table) function Promise:_wrap_resolver_cb(success, cb) return function(...) if self.has_resolved then return end self.has_resolved = true cb(success, { ... }) end end function Promise:__call(callback) self.resolver(self:_wrap_resolver_cb(true, callback), self:_wrap_resolver_cb(false, callback)) end local function await(resolver) local ok, value = co.yield(Promise:new(resolver)) if not ok then error(value[1], 0) end return unpack(value) end local function table_pack(...) return { n = select("#", ...), ... } end ---@generic T ---@param async_fn T ---@param should_reject_err boolean? Whether the provided async_fn takes a callback with the signature `fun(err, result)` ---@return T local function promisify(async_fn, should_reject_err) return function(...) local args = table_pack(...) return await(function(resolve, reject) if should_reject_err then args[args.n + 1] = function(err, result) if err then reject(err) else resolve(result) end end else args[args.n + 1] = resolve end local ok, err = pcall(async_fn, unpack(args, 1, args.n + 1)) if not ok then reject(err) end end) end end local function new_execution_context(suspend_fn, callback, ...) ---@type thread? local thread = co.create(suspend_fn) local cancelled = false local step step = function(...) if cancelled or not thread then return end local ok, promise_or_result = co.resume(thread, ...) if cancelled or not thread then return end if ok then if co.status(thread) == "suspended" then if getmetatable(promise_or_result) == Promise then promise_or_result(step) else -- yield to parent coroutine step(coroutine.yield(promise_or_result)) end else callback(true, promise_or_result) thread = nil end else callback(false, promise_or_result) thread = nil end end step(...) return function() cancelled = true thread = nil end end exports.run = function(suspend_fn, callback, ...) return new_execution_context(suspend_fn, callback, ...) end ---@generic T ---@param suspend_fn T exports.scope = function(suspend_fn) return function(...) return new_execution_context(suspend_fn, function(success, err) if not success then error(err, 0) end end, ...) end end exports.run_blocking = function(suspend_fn, ...) local resolved, ok, result local cancel_coroutine = new_execution_context(suspend_fn, function(a, b) resolved = true ok = a result = b end, ...) if resolved or vim.wait(0x7FFFFFFF, function() return resolved == true end, 50) then if not ok then error(result, 2) end return result else cancel_coroutine() error("async function failed to resolve in time.", 2) end end exports.wait = await exports.promisify = promisify exports.sleep = function(ms) await(function(resolve) vim.defer_fn(resolve, ms) end) end exports.scheduler = function() if vim.in_fast_event() then await(vim.schedule) end end ---@async ---@param suspend_fns async fun()[] ---@param mode '"first"' | '"all"' local function wait(suspend_fns, mode) local channel = require("mason-core.async.control").OneShotChannel:new() if #suspend_fns == 0 then return end do local results = {} local thread_cancellations = {} local count = #suspend_fns local completed = 0 local function cancel() for _, cancel_thread in ipairs(thread_cancellations) do cancel_thread() end end for i, suspend_fn in ipairs(suspend_fns) do thread_cancellations[i] = exports.run(suspend_fn, function(success, result) completed = completed + 1 if channel:is_closed() then return end if not success then cancel() channel:send(false, result) results = nil thread_cancellations = {} else results[i] = result if mode == "first" or completed >= count then cancel() channel:send(true, mode == "first" and { result } or results) results = nil thread_cancellations = {} end end end) end end local ok, results = channel:receive() if not ok then error(results, 2) end return unpack(results) end ---@async ---@param suspend_fns async fun()[] function exports.wait_all(suspend_fns) return wait(suspend_fns, "all") end ---@async ---@param suspend_fns async fun()[] function exports.wait_first(suspend_fns) return wait(suspend_fns, "first") end function exports.blocking(suspend_fn) return _.partial(exports.run_blocking, suspend_fn) end return exports ================================================ FILE: lua/mason-core/async/uv.lua ================================================ local a = require "mason-core.async" ---@type table local M = setmetatable({}, { __index = function(cache, method) cache[method] = a.promisify(vim.loop[method], true) return cache[method] end, }) return M ---@alias UvMethod ---| '"write"' ---| '"shutdown"' ---| '"close"' ---| '"fs_close"' ---| '"fs_open"' ---| '"fs_read"' ---| '"fs_unlink"' ---| '"fs_write"' ---| '"fs_mkdir"' ---| '"fs_mkdtemp"' ---| '"fs_mkstemp"' ---| '"fs_rmdir"' ---| '"fs_scandir"' ---| '"fs_stat"' ---| '"fs_fstat"' ---| '"fs_lstat"' ---| '"fs_rename"' ---| '"fs_fsync"' ---| '"fs_fdatasync"' ---| '"fs_ftruncate"' ---| '"fs_sendfile"' ---| '"fs_access"' ---| '"fs_chmod"' ---| '"fs_fchmod"' ---| '"fs_utime"' ---| '"fs_futime"' ---| '"fs_lutime"' ---| '"fs_link"' ---| '"fs_symlink"' ---| '"fs_readlink"' ---| '"fs_realpath"' ---| '"fs_chown"' ---| '"fs_fchown"' ---| '"fs_lchown"' ---| '"fs_copyfile"' ---| '"fs_opendir"' ---| '"fs_readdir"' ---| '"fs_closedir"' ---| '"fs_statfs"' ================================================ FILE: lua/mason-core/fetch.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local async_uv = require "mason-core.async.uv" local log = require "mason-core.log" local platform = require "mason-core.platform" local powershell = require "mason-core.installer.managers.powershell" local spawn = require "mason-core.spawn" local version = require "mason.version" local USER_AGENT = ("mason.nvim %s (+https://github.com/mason-org/mason.nvim)"):format(version.VERSION) local TIMEOUT_SECONDS = 30 ---@alias FetchMethod ---| '"GET"' ---| '"POST"' ---| '"PUT"' ---| '"PATCH"' ---| '"DELETE"' ---@alias FetchOpts {out_file: string?, method: FetchMethod?, headers: table?, data: string?} ---@async ---@param url string The url to fetch. ---@param opts FetchOpts? ---@return Result # Result local function fetch(url, opts) opts = opts or {} if not opts.headers then opts.headers = {} end if not opts.method then opts.method = "GET" end opts.headers["User-Agent"] = USER_AGENT log.fmt_debug("Fetching URL %s", url) local platform_specific = Result.failure if platform.is.win then local header_entries = _.join( "; ", _.map(function(pair) return ("%q=%q"):format(pair[1], pair[2]) end, _.to_pairs(opts.headers)) ) local headers = ("-Headers @{%s}"):format(header_entries) if opts.out_file then platform_specific = function() return powershell.command( ([[iwr %s -TimeoutSec %s -UseBasicParsing -Method %q -Uri %q %s -OutFile %q;]]):format( headers, TIMEOUT_SECONDS, opts.method, url, opts.data and ("-Body %s"):format(opts.data) or "", opts.out_file ) ) end else platform_specific = function() return powershell.command( ([[Write-Output (iwr %s -TimeoutSec %s -Method %q -UseBasicParsing %s -Uri %q).Content;]]):format( headers, TIMEOUT_SECONDS, opts.method, opts.data and ("-Body %s"):format(opts.data) or "", url ) ) end end end local function wget() local headers = _.sort_by( _.nth(2), _.map( _.compose(function(header) return { "--header", header } end, _.join ": "), _.to_pairs(opts.headers) ) ) if opts.data and opts.method ~= "POST" then return Result.failure(("fetch: data provided but method is not POST (was %s)"):format(opts.method or "-")) end if not _.any(_.equals(opts.method), { "GET", "POST" }) then -- Note: --spider can be used for HEAD support, if ever needed return Result.failure(("fetch: wget doesn't support HTTP method %s"):format(opts.method)) end return spawn.wget { headers, "-o", "/dev/null", "-O", opts.out_file or "-", "-T", TIMEOUT_SECONDS, opts.data and opts.method == "POST" and { "--post-data", opts.data, } or vim.NIL, url, } end local function curl() local headers = _.sort_by( _.nth(2), _.map( _.compose(function(header) return { "-H", header } end, _.join ": "), _.to_pairs(opts.headers) ) ) return spawn.curl { headers, "-fsSL", { "-X", opts.method, }, opts.data and { "-d", "@-" } or vim.NIL, opts.out_file and { "-o", opts.out_file } or vim.NIL, { "--connect-timeout", TIMEOUT_SECONDS }, url, on_spawn = a.scope(function(_, stdio) local stdin = stdio[1] if opts.data then log.trace("Writing stdin to curl", opts.data) async_uv.write(stdin, opts.data) end async_uv.shutdown(stdin) async_uv.close(stdin) end), } end return curl():or_else(wget):or_else(platform_specific):map(function(result) if opts.out_file then return result else return result.stdout end end) end return fetch ================================================ FILE: lua/mason-core/fs.lua ================================================ local Path = require "mason-core.path" local a = require "mason-core.async" local log = require "mason-core.log" local settings = require "mason.settings" local function make_module(uv) local M = {} ---@param path string function M.fstat(path) log.trace("fs: fstat", path) local fd = uv.fs_open(path, "r", 438) local fstat = uv.fs_fstat(fd) uv.fs_close(fd) return fstat end ---@param path string function M.file_exists(path) log.trace("fs: file_exists", path) local ok, fstat = pcall(M.fstat, path) if not ok then return false end return fstat.type == "file" end ---@param path string function M.dir_exists(path) log.trace("fs: dir_exists", path) local ok, fstat = pcall(M.fstat, path) if not ok then return false end return fstat.type == "directory" end ---@param path string function M.rmrf(path) assert( Path.is_subdirectory(settings.current.install_root_dir, path), ("Refusing to rmrf %q which is outside of the allowed boundary %q. Please report this error at https://github.com/mason-org/mason.nvim/issues/new"):format( path, settings.current.install_root_dir ) ) log.debug("fs: rmrf", path) if vim.in_fast_event() then a.scheduler() end if vim.fn.delete(path, "rf") ~= 0 then log.debug "fs: rmrf failed" error(("rmrf: Could not remove directory %q."):format(path)) end end ---@param path string function M.unlink(path) log.debug("fs: unlink", path) uv.fs_unlink(path) end ---@param path string function M.mkdir(path) log.debug("fs: mkdir", path) uv.fs_mkdir(path, 493) -- 493(10) == 755(8) end ---@param path string function M.mkdirp(path) log.debug("fs: mkdirp", path) if vim.in_fast_event() then a.scheduler() end if vim.fn.mkdir(path, "p") ~= 1 then log.debug "fs: mkdirp failed" error(("mkdirp: Could not create directory %q."):format(path)) end end ---@param path string function M.rmdir(path) log.debug("fs: rmdir", path) uv.fs_rmdir(path) end ---@param path string ---@param new_path string function M.rename(path, new_path) log.debug("fs: rename", path, new_path) uv.fs_rename(path, new_path) end ---@param path string ---@param new_path string ---@param flags table? { excl?: boolean, ficlone?: boolean, ficlone_force?: boolean } function M.copy_file(path, new_path, flags) log.debug("fs: copy_file", path, new_path, flags) uv.fs_copyfile(path, new_path, flags) end ---@param path string ---@param contents string ---@param flags string? Defaults to "w". function M.write_file(path, contents, flags) log.trace("fs: write_file", path) local fd = uv.fs_open(path, flags or "w", 438) uv.fs_write(fd, contents, -1) uv.fs_close(fd) end ---@param path string ---@param contents string function M.append_file(path, contents) M.write_file(path, contents, "a") end ---@param path string function M.read_file(path) log.trace("fs: read_file", path) local fd = uv.fs_open(path, "r", 438) local fstat = uv.fs_fstat(fd) local contents = uv.fs_read(fd, fstat.size, 0) uv.fs_close(fd) return contents end ---@alias ReaddirEntry {name: string, type: string} ---@param path string: The full path to the directory to read. ---@return ReaddirEntry[] function M.readdir(path) log.trace("fs: fs_opendir", path) local dir = assert(vim.loop.fs_opendir(path, nil, 25)) local all_entries = {} local exhausted = false repeat local entries = uv.fs_readdir(dir) log.trace("fs: fs_readdir", path, entries) if entries and #entries > 0 then for i = 1, #entries do if entries[i].name and not entries[i].type then -- See https://github.com/luvit/luv/issues/660 local full_path = Path.concat { path, entries[i].name } log.trace("fs: fs_readdir falling back to fs_stat to find type", full_path) local stat = uv.fs_stat(full_path) entries[i].type = stat.type end all_entries[#all_entries + 1] = entries[i] end else log.trace("fs: fs_readdir exhausted scan", path) exhausted = true end until exhausted uv.fs_closedir(dir) return all_entries end ---@param path string ---@param new_path string function M.symlink(path, new_path) log.trace("fs: symlink", path, new_path) uv.fs_symlink(path, new_path) end ---@param path string ---@param mode integer function M.chmod(path, mode) log.trace("fs: chmod", path, mode) uv.fs_chmod(path, mode) end return M end return { async = make_module(require "mason-core.async.uv"), sync = make_module(vim.loop), } ================================================ FILE: lua/mason-core/functional/data.lua ================================================ local _ = {} _.table_pack = function(...) return { n = select("#", ...), ... } end ---@generic T : string ---@param values T[] ---@return table _.enum = function(values) local result = {} for i = 1, #values do local v = values[i] result[v] = v end return result end ---@generic T ---@param list T[] ---@return table _.set_of = function(list) local set = {} for i = 1, #list do set[list[i]] = true end return set end return _ ================================================ FILE: lua/mason-core/functional/function.lua ================================================ local data = require "mason-core.functional.data" local _ = {} ---@generic T : fun(...) ---@param fn T ---@param arity integer ---@return T _.curryN = function(fn, arity) return function(...) local args = data.table_pack(...) if args.n >= arity then return fn(unpack(args, 1, arity)) else return _.curryN(_.partial(fn, unpack(args, 1, args.n)), arity - args.n) end end end _.compose = function(...) local functions = data.table_pack(...) assert(functions.n > 0, "compose requires at least one function") return function(...) local result = data.table_pack(...) for i = functions.n, 1, -1 do result = data.table_pack(functions[i](unpack(result, 1, result.n))) end return unpack(result, 1, result.n) end end ---@generic T ---@param fn fun(...): T ---@return fun(...): T _.partial = function(fn, ...) local bound_args = data.table_pack(...) return function(...) local args = data.table_pack(...) local merged_args = {} for i = 1, bound_args.n do merged_args[i] = bound_args[i] end for i = 1, args.n do merged_args[bound_args.n + i] = args[i] end return fn(unpack(merged_args, 1, bound_args.n + args.n)) end end ---@generic T ---@param value T ---@return T _.identity = function(value) return value end _.always = function(a) return function() return a end end _.T = _.always(true) _.F = _.always(false) ---@generic T : fun(...) ---@param fn T ---@param cache_key_generator (fun(...): any)? ---@return T _.memoize = function(fn, cache_key_generator) cache_key_generator = cache_key_generator or _.identity local cache = {} return function(...) local key = cache_key_generator(...) if not cache[key] then cache[key] = data.table_pack(fn(...)) end return unpack(cache[key], 1, cache[key].n) end end ---@generic T ---@param fn fun(): T ---@return fun(): T _.lazy = function(fn) local memoized = _.memoize(fn, _.always "lazyval") return function() return memoized() end end _.tap = _.curryN(function(fn, value) fn(value) return value end, 2) ---@generic T, U ---@param value T ---@param fn fun(value: T): U ---@return U _.apply_to = _.curryN(function(value, fn) return fn(value) end, 2) ---@generic T, R, V ---@param fn fun (args...: V[]): R ---@param args V[] ---@return R _.apply = _.curryN(function(fn, args) return fn(unpack(args)) end, 2) ---@generic T, V ---@param fn fun(...): T ---@param fns (fun(value: V))[] ---@param val V ---@return T _.converge = _.curryN(function(fn, fns, val) return fn(unpack(vim.tbl_map(_.apply_to(val), fns))) end, 3) ---@param spec table ---@param value any ---@return table _.apply_spec = _.curryN(function(spec, value) spec = vim.deepcopy(spec) local function transform(item) if type(item) == "table" then for k, v in pairs(item) do item[k] = transform(v) end return item else return item(value) end end return transform(spec) end, 2) return _ ================================================ FILE: lua/mason-core/functional/init.lua ================================================ local _ = {} local function lazy_require(module) return setmetatable({}, { __index = function(m, k) return require(module)[k] end, }) end _.lazy_require = lazy_require ---@module "mason-core.functional.data" local data = lazy_require "mason-core.functional.data" _.table_pack = data.table_pack _.enum = data.enum _.set_of = data.set_of ---@module "mason-core.functional.function" local fun = lazy_require "mason-core.functional.function" _.curryN = fun.curryN _.compose = fun.compose _.partial = fun.partial _.identity = fun.identity _.always = fun.always _.T = fun.T _.F = fun.F _.memoize = fun.memoize _.lazy = fun.lazy _.tap = fun.tap _.apply_to = fun.apply_to _.apply = fun.apply _.converge = fun.converge _.apply_spec = fun.apply_spec ---@module "mason-core.functional.list" local list = lazy_require "mason-core.functional.list" _.reverse = list.reverse _.list_not_nil = list.list_not_nil _.list_copy = list.list_copy _.find_first = list.find_first _.any = list.any _.all = list.all _.filter = list.filter _.map = list.map _.filter_map = list.filter_map _.each = list.each _.concat = list.concat _.append = list.append _.prepend = list.prepend _.zip_table = list.zip_table _.nth = list.nth _.head = list.head _.last = list.last _.length = list.length _.flatten = list.flatten _.sort_by = list.sort_by _.uniq_by = list.uniq_by _.join = list.join _.partition = list.partition _.take = list.take _.drop = list.drop _.drop_last = list.drop_last _.reduce = list.reduce _.split_every = list.split_every _.index_by = list.index_by ---@module "mason-core.functional.relation" local relation = lazy_require "mason-core.functional.relation" _.equals = relation.equals _.not_equals = relation.not_equals _.prop_eq = relation.prop_eq _.prop_satisfies = relation.prop_satisfies _.path_satisfies = relation.path_satisfies _.min = relation.min _.add = relation.add ---@module "mason-core.functional.logic" local logic = lazy_require "mason-core.functional.logic" _.all_pass = logic.all_pass _.any_pass = logic.any_pass _.if_else = logic.if_else _.is_not = logic.is_not _.complement = logic.complement _.cond = logic.cond _.default_to = logic.default_to ---@module "mason-core.functional.number" local number = lazy_require "mason-core.functional.number" _.negate = number.negate _.gt = number.gt _.gte = number.gte _.lt = number.lt _.lte = number.lte _.inc = number.inc _.dec = number.dec ---@module "mason-core.functional.string" local string = lazy_require "mason-core.functional.string" _.matches = string.matches _.match = string.match _.format = string.format _.split = string.split _.gsub = string.gsub _.trim = string.trim _.trim_start_matches = string.trim_start_matches _.trim_end_matches = string.trim_end_matches _.strip_prefix = string.strip_prefix _.strip_suffix = string.strip_suffix _.dedent = string.dedent _.starts_with = string.starts_with _.to_upper = string.to_upper _.to_lower = string.to_lower ---@module "mason-core.functional.table" local tbl = lazy_require "mason-core.functional.table" _.prop = tbl.prop _.path = tbl.path _.pick = tbl.pick _.keys = tbl.keys _.size = tbl.size _.to_pairs = tbl.to_pairs _.from_pairs = tbl.from_pairs _.invert = tbl.invert _.evolve = tbl.evolve _.merge_left = tbl.merge_left _.dissoc = tbl.dissoc _.assoc = tbl.assoc ---@module "mason-core.functional.type" local typ = lazy_require "mason-core.functional.type" _.is_nil = typ.is_nil _.is = typ.is _.is_list = typ.is_list -- TODO do something else with these _.coalesce = function(...) local args = _.table_pack(...) for i = 1, args.n do local variable = args[i] if variable ~= nil then return variable end end end _.when = function(condition, value) return condition and value or nil end _.lazy_when = function(condition, value) return condition and value() or nil end ---@param fn fun() _.scheduler = function(fn) if vim.in_fast_event() then vim.schedule(fn) else fn() end end ---@generic T : fun(...) ---@param fn T ---@return T _.scheduler_wrap = function(fn) return function(...) local args = _.table_pack(...) _.scheduler(function() fn(unpack(args, 1, args.n + 1)) end) end end return _ ================================================ FILE: lua/mason-core/functional/list.lua ================================================ local data = require "mason-core.functional.data" local fun = require "mason-core.functional.function" local _ = {} ---@generic T ---@param list T[] ---@return T[] _.reverse = function(list) local result = {} for i = #list, 1, -1 do result[#result + 1] = list[i] end return result end _.list_not_nil = function(...) local result = {} local args = data.table_pack(...) for i = 1, args.n do if args[i] ~= nil then result[#result + 1] = args[i] end end return result end ---@generic T ---@param predicate fun(item: T): boolean ---@param list T[] ---@return T | nil _.find_first = fun.curryN(function(predicate, list) local result for i = 1, #list do local entry = list[i] if predicate(entry) then return entry end end return result end, 2) ---@generic T ---@param predicate fun(item: T): boolean ---@param list T[] ---@return boolean _.any = fun.curryN(function(predicate, list) for i = 1, #list do if predicate(list[i]) then return true end end return false end, 2) ---@generic T ---@param predicate fun(item: T): boolean ---@param list T[] ---@return boolean _.all = fun.curryN(function(predicate, list) for i = 1, #list do if not predicate(list[i]) then return false end end return true end, 2) ---@generic T ---@type fun(filter_fn: (fun(item: T): boolean), items: T[]): T[] _.filter = fun.curryN(vim.tbl_filter, 2) ---@generic T, U ---@type fun(map_fn: (fun(item: T): U), items: T[]): U[] _.map = fun.curryN(vim.tbl_map, 2) ---@param tbl table ---@return table _.flatten = function(tbl) local result = {} --- @param _tbl table local function _tbl_flatten(_tbl) local n = #_tbl for i = 1, n do local v = _tbl[i] if type(v) == "table" then _tbl_flatten(v) elseif v then table.insert(result, v) end end end _tbl_flatten(tbl) return result end ---@generic T ---@param map_fn fun(item: T): Optional ---@param list T[] ---@return any[] _.filter_map = fun.curryN(function(map_fn, list) local ret = {} for i = 1, #list do map_fn(list[i]):if_present(function(value) ret[#ret + 1] = value end) end return ret end, 2) ---@generic T ---@param fn fun(item: T, index: integer) ---@param list T[] _.each = fun.curryN(function(fn, list) for k, v in pairs(list) do fn(v, k) end end, 2) ---@generic T ---@type fun(list: T[]): T[] _.list_copy = _.map(fun.identity) _.concat = fun.curryN(function(a, b) if type(a) == "table" then assert(type(b) == "table", "concat: expected table") return vim.list_extend(_.list_copy(a), b) elseif type(a) == "string" then assert(type(b) == "string", "concat: expected string") return a .. b end end, 2) ---@generic T ---@param value T ---@param list T[] ---@return T[] _.append = fun.curryN(function(value, list) local list_copy = _.list_copy(list) list_copy[#list_copy + 1] = value return list_copy end, 2) ---@generic T ---@param value T ---@param list T[] ---@return T[] _.prepend = fun.curryN(function(value, list) local list_copy = _.list_copy(list) table.insert(list_copy, 1, value) return list_copy end, 2) ---@generic T ---@generic U ---@param keys T[] ---@param values U[] ---@return table _.zip_table = fun.curryN(function(keys, values) local res = {} for i, key in ipairs(keys) do res[key] = values[i] end return res end, 2) ---@generic T ---@param offset number ---@param value T[]|string ---@return T|string|nil _.nth = fun.curryN(function(offset, value) local index = offset < 0 and (#value + (offset + 1)) or offset if type(value) == "string" then return string.sub(value, index, index) else return value[index] end end, 2) _.head = _.nth(1) ---@generic T ---@param list T[] ---@return T? _.last = function(list) return list[#list] end ---@param value string|any[] _.length = function(value) return #value end ---@generic T ---@param comp fun(item: T): any ---@param list T[] ---@return T[] _.sort_by = fun.curryN(function(comp, list) local copied_list = _.list_copy(list) table.sort(copied_list, function(a, b) return comp(a) < comp(b) end) return copied_list end, 2) ---@param sep string ---@param list any[] _.join = fun.curryN(function(sep, list) return table.concat(list, sep) end, 2) ---@generic T ---@param id fun(item: T): any ---@param list T[] ---@return T[] _.uniq_by = fun.curryN(function(id, list) local set = {} local result = {} for i = 1, #list do local item = list[i] local uniq_key = id(item) if not set[uniq_key] then set[uniq_key] = true table.insert(result, item) end end return result end, 2) ---@generic T ---@param predicate fun(item: T): boolean ---@param list T[] ---@return T[][] # [T[], T[]] _.partition = fun.curryN(function(predicate, list) local partitions = { {}, {} } for _, item in ipairs(list) do table.insert(partitions[predicate(item) and 1 or 2], item) end return partitions end, 2) ---@generic T ---@param n integer ---@param list T[] ---@return T[] _.take = fun.curryN(function(n, list) local result = {} for i = 1, math.min(n, #list) do result[#result + 1] = list[i] end return result end, 2) ---@generic T ---@param n integer ---@param list T[] ---@return T[] _.drop = fun.curryN(function(n, list) local result = {} for i = n + 1, #list do result[#result + 1] = list[i] end return result end, 2) ---@generic T ---@param n integer ---@param list T[] ---@return T[] _.drop_last = fun.curryN(function(n, list) local result = {} for i = 1, #list - n do result[#result + 1] = list[i] end return result end, 2) ---@generic T, U ---@param fn fun(acc: U, item: T): U ---@param acc U ---@param list T[] ---@return U _.reduce = fun.curryN(function(fn, acc, list) for i = 1, #list do acc = fn(acc, list[i]) end return acc end, 3) ---@generic T ---@param n integer ---@param list T[] ---@return T[][] _.split_every = fun.curryN(function(n, list) assert(n > 0, "n needs to be greater than 0.") local res = {} local idx = 1 while idx <= #list do table.insert(res, { unpack(list, idx, idx + n - 1) }) idx = idx + n end return res end, 2) ---@generic T, U ---@param index fun(item: T): U ---@param list T[] ---@return table _.index_by = fun.curryN(function(index, list) local res = {} for _, item in ipairs(list) do res[index(item)] = item end return res end, 2) return _ ================================================ FILE: lua/mason-core/functional/logic.lua ================================================ local fun = require "mason-core.functional.function" local _ = {} ---@generic T ---@param predicates (fun(item: T): boolean)[] ---@param item T ---@return boolean _.all_pass = fun.curryN(function(predicates, item) for i = 1, #predicates do if not predicates[i](item) then return false end end return true end, 2) ---@generic T ---@param predicates (fun(item: T): boolean)[] ---@param item T ---@return boolean _.any_pass = fun.curryN(function(predicates, item) for i = 1, #predicates do if predicates[i](item) then return true end end return false end, 2) ---@generic T, U ---@param predicate fun(item: T): boolean ---@param on_true fun(item: T): U ---@param on_false fun(item: T): U ---@param value T ---@return U _.if_else = fun.curryN(function(predicate, on_true, on_false, value) if predicate(value) then return on_true(value) else return on_false(value) end end, 4) ---@param value boolean ---@return boolean _.is_not = function(value) return not value end ---@generic T ---@param predicate fun(value: T): boolean ---@param value T ---@return boolean _.complement = fun.curryN(function(predicate, value) return not predicate(value) end, 2) ---@generic T, U ---@param predicate_transformer_pairs {[1]: (fun(value: T): boolean), [2]: (fun(value: T): U)}[] ---@param value T ---@return U? _.cond = fun.curryN(function(predicate_transformer_pairs, value) for _, pair in ipairs(predicate_transformer_pairs) do local predicate, transformer = pair[1], pair[2] if predicate(value) then return transformer(value) end end end, 2) ---@generic T ---@param default_val T ---@param val T? ---@return T _.default_to = fun.curryN(function(default_val, val) if val ~= nil then return val else return default_val end end, 2) return _ ================================================ FILE: lua/mason-core/functional/number.lua ================================================ local fun = require "mason-core.functional.function" local _ = {} ---@param number number _.negate = function(number) return -number end _.gt = fun.curryN(function(number, value) return value > number end, 2) _.gte = fun.curryN(function(number, value) return value >= number end, 2) _.lt = fun.curryN(function(number, value) return value < number end, 2) _.lte = fun.curryN(function(number, value) return value <= number end, 2) _.inc = fun.curryN(function(increment, value) return value + increment end, 2) _.dec = fun.curryN(function(decrement, value) return value - decrement end, 2) return _ ================================================ FILE: lua/mason-core/functional/relation.lua ================================================ local fun = require "mason-core.functional.function" local _ = {} _.equals = fun.curryN(function(expected, value) return value == expected end, 2) _.not_equals = fun.curryN(function(expected, value) return value ~= expected end, 2) _.prop_eq = fun.curryN(function(property, value, tbl) return tbl[property] == value end, 3) _.prop_satisfies = fun.curryN(function(predicate, property, tbl) return predicate(tbl[property]) end, 3) ---@param predicate fun(value: any): boolean ---@param path any[] ---@param tbl table _.path_satisfies = fun.curryN(function(predicate, path, tbl) -- see https://github.com/neovim/neovim/pull/21426 local value = vim.tbl_get(tbl, unpack(path)) return predicate(value) end, 3) ---@param a number ---@param b number ---@return number _.min = fun.curryN(function(a, b) return b - a end, 2) ---@param a number ---@param b number ---@return number _.add = fun.curryN(function(a, b) return b + a end, 2) return _ ================================================ FILE: lua/mason-core/functional/string.lua ================================================ local fun = require "mason-core.functional.function" local _ = {} ---@param pattern string ---@param str string _.matches = fun.curryN(function(pattern, str) return str:match(pattern) ~= nil end, 2) _.match = fun.curryN(function(pattern, str) return { str:match(pattern) } end, 2) ---@param template string ---@param str string _.format = fun.curryN(function(template, str) return template:format(str) end, 2) ---@param sep string ---@param str string _.split = fun.curryN(function(sep, str) return vim.split(str, sep) end, 2) ---@param pattern string ---@param repl string|function|table ---@param str string _.gsub = fun.curryN(function(pattern, repl, str) return string.gsub(str, pattern, repl) end, 3) _.trim = fun.curryN(function(str) return vim.trim(str) end, 1) ---https://github.com/nvim-lua/nvim-package-specification/blob/93475e47545b579fd20b6c5ce13c4163e7956046/lua/packspec/schema.lua#L8-L37 ---@param str string ---@return string _.dedent = fun.curryN(function(str) local lines = {} local indent = nil for line in str:gmatch "[^\n]*\n?" do if indent == nil then if not line:match "^%s*$" then -- save pattern for indentation from the first non-empty line indent, line = line:match "^(%s*)(.*)$" indent = "^" .. indent .. "(.*)$" table.insert(lines, line) end else if line:match "^%s*$" then -- replace empty lines with a single newline character. -- empty lines are handled separately to allow the -- closing "]]" to be one indentation level lower. table.insert(lines, "\n") else -- strip indentation on non-empty lines line = assert(line:match(indent), "inconsistent indentation") table.insert(lines, line) end end end lines = table.concat(lines) -- trim trailing whitespace return lines:match "^(.-)%s*$" end, 1) ---@param prefix string ---@str string _.starts_with = fun.curryN(function(prefix, str) return vim.startswith(str, prefix) end, 2) ---@param str string _.to_upper = function(str) return str:upper() end ---@param str string _.to_lower = function(str) return str:lower() end ---@param pattern string ---@param str string _.trim_start_matches = fun.curryN(function(pattern, str) for i = 1, #str do if not str:sub(i, i):match(pattern) then return str:sub(i) end end return str end, 2) ---@param pattern string ---@param str string _.trim_end_matches = fun.curryN(function(pattern, str) for i = #str, 1, -1 do if not str:sub(i, i):match(pattern) then return str:sub(1, i) end end return str end, 2) _.strip_prefix = fun.curryN(function(prefix_pattern, str) if #prefix_pattern > #str then return str end for i = 1, #prefix_pattern do if str:sub(i, i) ~= prefix_pattern:sub(i, i) then return str end end return str:sub(#prefix_pattern + 1) end, 2) _.strip_suffix = fun.curryN(function(suffix_pattern, str) if #suffix_pattern > #str then return str end for i = 1, #suffix_pattern do if str:sub(-i, -i) ~= suffix_pattern:sub(-i, -i) then return str end end return str:sub(1, -#suffix_pattern - 1) end, 2) return _ ================================================ FILE: lua/mason-core/functional/table.lua ================================================ local fun = require "mason-core.functional.function" local _ = {} ---@generic T : table ---@param tbl T ---@return T local function shallow_clone(tbl) local res = {} for k, v in pairs(tbl) do res[k] = v end return res end ---@generic T, U ---@param index T ---@param tbl table ---@return U? _.prop = fun.curryN(function(index, tbl) return tbl[index] end, 2) ---@param path any[] ---@param tbl table _.path = fun.curryN(function(path, tbl) -- see https://github.com/neovim/neovim/pull/21426 local value = vim.tbl_get(tbl, unpack(path)) return value end, 2) ---@generic T, U ---@param keys T[] ---@param tbl table ---@return table _.pick = fun.curryN(function(keys, tbl) local ret = {} for _, key in ipairs(keys) do ret[key] = tbl[key] end return ret end, 2) _.keys = fun.curryN(vim.tbl_keys, 1) _.size = fun.curryN(vim.tbl_count, 1) ---@generic K, V ---@param tbl table ---@return { [1]: K, [2]: V }[] _.to_pairs = fun.curryN(function(tbl) local result = {} for k, v in pairs(tbl) do result[#result + 1] = { k, v } end return result end, 1) ---@generic K, V ---@param pairs { [1]: K, [2]: V }[] ---@return table _.from_pairs = fun.curryN(function(pairs) local result = {} for _, pair in ipairs(pairs) do result[pair[1]] = pair[2] end return result end, 1) ---@generic K, V ---@param tbl table ---@return table _.invert = fun.curryN(function(tbl) local result = {} for k, v in pairs(tbl) do result[v] = k end return result end, 1) ---@generic K, V ---@param transforms table ---@param tbl table ---@return table _.evolve = fun.curryN(function(transforms, tbl) local result = shallow_clone(tbl) for key, value in pairs(tbl) do if transforms[key] then result[key] = transforms[key](value) end end return result end, 2) ---@generic T : table ---@param left T ---@param right T ---@return T _.merge_left = fun.curryN(function(left, right) return vim.tbl_extend("force", right, left) end, 2) ---@generic K, V ---@param key K ---@param value V ---@param tbl table ---@return table _.assoc = fun.curryN(function(key, value, tbl) local res = shallow_clone(tbl) res[key] = value return res end, 3) ---@generic K, V ---@param key K ---@param tbl table ---@return table _.dissoc = fun.curryN(function(key, tbl) local res = shallow_clone(tbl) res[key] = nil return res end, 2) return _ ================================================ FILE: lua/mason-core/functional/type.lua ================================================ local fun = require "mason-core.functional.function" local rel = require "mason-core.functional.relation" local _ = {} _.is_nil = rel.equals(nil) ---@param typ type ---@param value any _.is = fun.curryN(function(typ, value) return type(value) == typ end, 2) _.is_list = vim.islist or vim.tbl_islist return _ ================================================ FILE: lua/mason-core/installer/InstallHandle.lua ================================================ local EventEmitter = require "mason-core.EventEmitter" local Optional = require "mason-core.optional" local _ = require "mason-core.functional" local a = require "mason-core.async" local log = require "mason-core.log" local platform = require "mason-core.platform" local process = require "mason-core.process" local spawn = require "mason-core.spawn" local uv = vim.loop ---@alias InstallHandleState --- | '"IDLE"' --- | '"QUEUED"' --- | '"ACTIVE"' --- | '"CLOSED"' ---@class InstallHandleSpawnHandle ---@field uv_handle luv_handle ---@field pid integer ---@field cmd string ---@field args string[] local InstallHandleSpawnHandle = {} InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle ---@param luv_handle luv_handle ---@param pid integer ---@param cmd string ---@param args string[] function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) ---@type InstallHandleSpawnHandle local instance = {} setmetatable(instance, InstallHandleSpawnHandle) instance.uv_handle = luv_handle instance.pid = pid instance.cmd = cmd instance.args = args return instance end function InstallHandleSpawnHandle:__tostring() return ("%s %s"):format(self.cmd, table.concat(self.args, " ")) end ---@class InstallHandle : EventEmitter ---@field public package AbstractPackage ---@field state InstallHandleState ---@field stdio_sink BufferedSink ---@field is_terminated boolean ---@field location InstallLocation ---@field private spawn_handles InstallHandleSpawnHandle[] local InstallHandle = {} InstallHandle.__index = InstallHandle setmetatable(InstallHandle, { __index = EventEmitter }) ---@param pkg AbstractPackage ---@param location InstallLocation function InstallHandle:new(pkg, location) ---@type InstallHandle local instance = EventEmitter.new(self) local sink = process.BufferedSink:new() sink:connect_events(instance) instance.state = "IDLE" instance.package = pkg instance.spawn_handles = {} instance.stdio_sink = sink instance.is_terminated = false instance.location = location return instance end ---@param luv_handle luv_handle ---@param pid integer ---@param cmd string ---@param args string[] function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args) local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args) log.fmt_trace("Pushing spawn_handles stack for %s: %s (pid: %s)", self, spawn_handles, pid) self.spawn_handles[#self.spawn_handles + 1] = spawn_handles self:emit "spawn_handles:change" end ---@param luv_handle luv_handle function InstallHandle:deregister_spawn_handle(luv_handle) for i = #self.spawn_handles, 1, -1 do if self.spawn_handles[i].uv_handle == luv_handle then log.fmt_trace("Popping spawn_handles stack for %s: %s", self, self.spawn_handles[i]) table.remove(self.spawn_handles, i) self:emit "spawn_handles:change" return true end end return false end ---@return Optional # Optional function InstallHandle:peek_spawn_handle() return Optional.of_nilable(self.spawn_handles[#self.spawn_handles]) end function InstallHandle:is_idle() return self.state == "IDLE" end function InstallHandle:is_queued() return self.state == "QUEUED" end function InstallHandle:is_active() return self.state == "ACTIVE" end function InstallHandle:is_closed() return self.state == "CLOSED" end function InstallHandle:is_closing() return self:is_closed() or self.is_terminated end ---@param new_state InstallHandleState function InstallHandle:set_state(new_state) local old_state = self.state self.state = new_state log.fmt_trace("Changing %s state from %s to %s", self, old_state, new_state) self:emit("state:change", new_state, old_state) end ---@param signal integer function InstallHandle:kill(signal) assert(not self:is_closed(), "Cannot kill closed handle.") log.fmt_trace("Sending signal %s to luv handles in %s", signal, self) for _, spawn_handles in pairs(self.spawn_handles) do process.kill(spawn_handles.uv_handle, signal) end self:emit("kill", signal) end ---@param pid integer local win_taskkill = a.scope(function(pid) spawn.taskkill { "/f", "/t", "/pid", pid, } end) function InstallHandle:terminate() assert(not self:is_closed(), "Cannot terminate closed handle.") if self.is_terminated then log.fmt_trace("Handle is already terminated %s", self) return end log.fmt_trace("Terminating %s", self) -- https://github.com/libuv/libuv/issues/1133 if platform.is.win then for _, spawn_handles in ipairs(self.spawn_handles) do win_taskkill(spawn_handles.pid) end else self:kill(15) -- SIGTERM end self.is_terminated = true self:emit "terminate" local check = uv.new_check() check:start(function() for _, spawn_handles in ipairs(self.spawn_handles) do local luv_handle = spawn_handles.uv_handle local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) if ok and not is_closing then return end end check:stop() check:close() if not self:is_closed() then self:close() end end) end function InstallHandle:queued() assert(self:is_idle(), "Can only queue idle handles.") self:set_state "QUEUED" end function InstallHandle:active() assert(self:is_idle() or self:is_queued(), "Can only activate idle or queued handles.") self:set_state "ACTIVE" end function InstallHandle:close() log.fmt_trace("Closing %s", self) assert(not self:is_closed(), "Handle is already closed.") for _, spawn_handles in ipairs(self.spawn_handles) do local luv_handle = spawn_handles.uv_handle local ok, is_closing = pcall(luv_handle.is_closing, luv_handle) if ok then assert(is_closing, "There are open libuv handles.") end end self.spawn_handles = {} self:set_state "CLOSED" self:emit "closed" self:__clear_event_handlers() end function InstallHandle:__tostring() return ("InstallHandle(package=%s, state=%s)"):format(self.package, self.state) end return InstallHandle ================================================ FILE: lua/mason-core/installer/InstallLocation.lua ================================================ local Path = require "mason-core.path" local platform = require "mason-core.platform" local settings = require "mason.settings" ---@class InstallLocation ---@field private dir string local InstallLocation = {} InstallLocation.__index = InstallLocation ---@param dir string function InstallLocation:new(dir) ---@type InstallLocation local instance = {} setmetatable(instance, self) instance.dir = dir return instance end function InstallLocation.global() return InstallLocation:new(settings.current.install_root_dir) end function InstallLocation:get_dir() return self.dir end function InstallLocation:initialize() local Result = require "mason-core.result" local fs = require "mason-core.fs" return Result.try(function(try) for _, p in ipairs { self.dir, self:bin(), self:share(), self:package(), self:staging(), } do if not fs.sync.dir_exists(p) then try(Result.pcall(fs.sync.mkdirp, p)) end end end) end ---@param path string? function InstallLocation:bin(path) return Path.concat { self.dir, "bin", path } end ---@param path string? function InstallLocation:share(path) return Path.concat { self.dir, "share", path } end ---@param path string? function InstallLocation:opt(path) return Path.concat { self.dir, "opt", path } end ---@param pkg string? function InstallLocation:package(pkg) return Path.concat { self.dir, "packages", pkg } end ---@param path string? function InstallLocation:staging(path) return Path.concat { self.dir, "staging", path } end ---@param name string function InstallLocation:lockfile(name) return self:staging(("%s.lock"):format(name)) end ---@param path string function InstallLocation:registry(path) return Path.concat { self.dir, "registries", path } end ---@param pkg string function InstallLocation:receipt(pkg) return Path.concat { self:package(pkg), "mason-receipt.json" } end ---@param opts { PATH: '"append"' | '"prepend"' | '"skip"' } function InstallLocation:set_env(opts) vim.env.MASON = self.dir if opts.PATH == "prepend" then vim.env.PATH = self:bin() .. platform.path_sep .. vim.env.PATH elseif opts.PATH == "append" then vim.env.PATH = vim.env.PATH .. platform.path_sep .. self:bin() end end return InstallLocation ================================================ FILE: lua/mason-core/installer/InstallRunner.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local compiler = require "mason-core.installer.compiler" local control = require "mason-core.async.control" local fs = require "mason-core.fs" local linker = require "mason-core.installer.linker" local log = require "mason-core.log" local registry = require "mason-registry" local OneShotChannel = control.OneShotChannel local InstallContext = require "mason-core.installer.context" ---@class InstallRunner ---@field handle InstallHandle ---@field global_semaphore Semaphore ---@field global_permit Permit? ---@field package_permit Permit? local InstallRunner = {} InstallRunner.__index = InstallRunner ---@param handle InstallHandle ---@param semaphore Semaphore function InstallRunner:new(handle, semaphore) ---@type InstallRunner local instance = {} setmetatable(instance, self) instance.global_semaphore = semaphore instance.handle = handle return instance end ---@alias InstallRunnerCallback fun(success: true, receipt: InstallReceipt) | fun(success: false, error: any) ---@param opts PackageInstallOpts ---@param callback? InstallRunnerCallback function InstallRunner:execute(opts, callback) local handle = self.handle log.fmt_info("Executing installer for %s %s", handle.package, opts) local context = InstallContext:new(handle, opts) local tailed_output = {} if opts.debug then local function append_log(chunk) tailed_output[#tailed_output + 1] = chunk end handle:on("stdout", append_log) handle:on("stderr", append_log) end ---@async local function finalize_logs(success, result) if not success then context.stdio_sink:stderr(tostring(result)) context.stdio_sink:stderr "\n" end if opts.debug then context.fs:write_file("mason-debug.log", table.concat(tailed_output, "")) context.stdio_sink:stdout(("[debug] Installation directory retained at %q.\n"):format(context.cwd:get())) end end ---@async local finalize = a.scope(function(success, result) finalize_logs(success, result) if not opts.debug and not success then -- clean up installation dir pcall(function() fs.async.rmrf(context.cwd:get()) end) end if not handle:is_closing() then handle:close() end self:release_lock() self:release_permit() if success then log.fmt_info("Installation succeeded for %s", handle.package) if callback then callback(true, result.receipt) end handle.package:emit("install:success", result.receipt) registry:emit("package:install:success", handle.package, result.receipt) else log.fmt_error("Installation failed for %s error=%s", handle.package, result) if callback then callback(false, result) end handle.package:emit("install:failed", result) registry:emit("package:install:failed", handle.package, result) end end) local cancel_execution = a.run(function() return Result.try(function(try) try(self.handle.location:initialize()) try(self:acquire_permit()):receive() try(self:acquire_lock(opts.force)) context.receipt:with_start_time(vim.loop.gettimeofday()) context.receipt:with_registry(context.package.registry:serialize()) -- 1. initialize working directory try(context.cwd:initialize()) -- 2. run installer ---@type async fun(ctx: InstallContext): Result local installer = try(compiler.compile_installer(handle.package.spec, opts)) try(context:execute(installer)) -- 3. promote temporary installation dir try(Result.pcall(function() context:promote_cwd() end)) -- 4. link package & write receipt try(linker.link(context):on_failure(function() -- unlink any links that were made before failure context:build_receipt():on_success( ---@param receipt InstallReceipt function(receipt) linker.unlink(handle.package, receipt, self.handle.location):on_failure(function(err) log.error("Failed to unlink failed installation.", err) end) end ) end)) ---@type InstallReceipt local receipt = try(context:build_receipt()) try(Result.pcall(fs.sync.write_file, handle.location:receipt(handle.package.name), receipt:to_json())) return { receipt = receipt, } end):get_or_throw() end, finalize) handle:once("terminate", function() cancel_execution() local function on_close() finalize(false, "Installation was aborted.") end if handle:is_closed() then on_close() else handle:once("closed", on_close) end end) end ---@async ---@private function InstallRunner:release_lock() pcall(fs.async.unlink, self.handle.location:lockfile(self.handle.package.name)) end ---@async ---@param force boolean? ---@private function InstallRunner:acquire_lock(force) local pkg = self.handle.package log.debug("Attempting to lock package", pkg) local lockfile = self.handle.location:lockfile(pkg.name) if force ~= true and fs.async.file_exists(lockfile) then log.error("Lockfile already exists.", pkg) return Result.failure( ("Lockfile exists, installation is already running in another process (pid: %s). Run with :MasonInstall --force to bypass."):format( fs.async.read_file(lockfile) ) ) end a.scheduler() fs.async.write_file(lockfile, vim.fn.getpid()) log.debug("Wrote lockfile", pkg) return Result.success(lockfile) end ---@private function InstallRunner:acquire_permit() local channel = OneShotChannel:new() log.fmt_debug("Acquiring permit for %s", self.handle.package) local handle = self.handle if handle:is_active() or handle:is_closing() then log.fmt_debug("Received active or closing handle %s", handle) return Result.failure "Invalid handle state." end handle:queued() a.run(function() self.global_permit = self.global_semaphore:acquire() self.package_permit = handle.package:acquire_permit() end, function(success, err) if not success or handle:is_closing() then if not success then log.error("Acquiring permits failed", err) end self:release_permit() else log.fmt_debug("Activating handle %s", handle) handle:active() channel:send() end end) return Result.success(channel) end ---@private function InstallRunner:release_permit() if self.global_permit then self.global_permit:forget() self.global_permit = nil end if self.package_permit then self.package_permit:forget() self.package_permit = nil end end return InstallRunner ================================================ FILE: lua/mason-core/installer/UninstallRunner.lua ================================================ local InstallContext = require "mason-core.installer.context" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local compiler = require "mason-core.installer.compiler" local control = require "mason-core.async.control" local fs = require "mason-core.fs" local log = require "mason-core.log" local registry = require "mason-registry" local OneShotChannel = control.OneShotChannel ---@class UninstallRunner ---@field handle InstallHandle ---@field global_semaphore Semaphore ---@field package_permit Permit? ---@field global_permit Permit? local UninstallRunner = {} UninstallRunner.__index = UninstallRunner ---@param handle InstallHandle ---@param global_semaphore Semaphore ---@return UninstallRunner function UninstallRunner:new(handle, global_semaphore) local instance = {} setmetatable(instance, self) instance.handle = handle instance.global_semaphore = global_semaphore return instance end ---@param opts PackageUninstallOpts ---@param callback? InstallRunnerCallback function UninstallRunner:execute(opts, callback) local pkg = self.handle.package local location = self.handle.location log.fmt_info("Executing uninstaller for %s %s", pkg, opts) a.run(function() Result.try(function(try) if not opts.bypass_permit then try(self:acquire_permit()):receive() end ---@type InstallReceipt? local receipt = pkg:get_receipt(location):or_else(nil) if receipt == nil then log.fmt_warn("Receipt not found when uninstalling %s", pkg) else try(pkg:unlink(location)) end fs.sync.rmrf(location:package(pkg.name)) return receipt end):get_or_throw() end, function(success, result) if not self.handle:is_closing() then self.handle:close() end self:release_permit() if success then local receipt = result log.fmt_info("Uninstallation succeeded for %s", pkg) if callback then callback(true, receipt) end pkg:emit("uninstall:success", receipt) registry:emit("package:uninstall:success", pkg, receipt) else log.fmt_error("Uninstallation failed for %s error=%s", pkg, result) if callback then callback(false, result) end pkg:emit("uninstall:failed", result) registry:emit("package:uninstall:failed", pkg, result) end end) end ---@private function UninstallRunner:acquire_permit() local channel = OneShotChannel:new() log.fmt_debug("Acquiring permit for %s", self.handle.package) local handle = self.handle if handle:is_active() or handle:is_closing() then log.fmt_debug("Received active or closing handle %s", handle) return Result.failure "Invalid handle state." end handle:queued() a.run(function() self.global_permit = self.global_semaphore:acquire() self.package_permit = handle.package:acquire_permit() end, function(success, err) if not success or handle:is_closing() then if not success then log.error("Acquiring permits failed", err) end self:release_permit() else log.fmt_debug("Activating handle %s", handle) handle:active() channel:send() end end) return Result.success(channel) end ---@private function UninstallRunner:release_permit() if self.global_permit then self.global_permit:forget() self.global_permit = nil end if self.package_permit then self.package_permit:forget() self.package_permit = nil end end return UninstallRunner ================================================ FILE: lua/mason-core/installer/compiler/compilers/cargo.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" local M = {} ---@class CargoSource : RegistryPackageSource ---@param source CargoSource ---@param purl Purl function M.parse(source, purl) local repository_url = _.path({ "qualifiers", "repository_url" }, purl) local git if repository_url then git = { url = repository_url, rev = _.path({ "qualifiers", "rev" }, purl) == "true", } end ---@type string? local features = _.path({ "qualifiers", "features" }, purl) local locked = _.path({ "qualifiers", "locked" }, purl) ---@class ParsedCargoSource : ParsedPackageSource local parsed_source = { crate = purl.name, version = purl.version, features = features, locked = locked ~= "false", git = git, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedCargoSource function M.install(ctx, source) local cargo = require "mason-core.installer.managers.cargo" return cargo.install(source.crate, source.version, { git = source.git, features = source.features, locked = source.locked, }) end ---@async ---@param purl Purl function M.get_versions(purl) ---@type string? local repository_url = _.path({ "qualifiers", "repository_url" }, purl) local rev = _.path({ "qualifiers", "rev" }, purl) if repository_url then if rev == "true" then -- When ?rev=true we're targeting a commit SHA. It's not feasible to retrieve all commit SHAs for a -- repository so we fail instead. return Result.failure "Unable to retrieve commit SHAs." end ---@type Result? local git_tags = _.cond { { _.matches "github.com/(.+)", _.compose(providers.github.get_all_tags, _.head, _.match "github.com/(.+)"), }, }(repository_url) if git_tags then return git_tags end end return providers.crates.get_all_versions(purl.name) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/composer.lua ================================================ local Result = require "mason-core.result" local providers = require "mason-core.providers" local util = require "mason-core.installer.compiler.util" local M = {} ---@param source RegistryPackageSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedComposerSource : ParsedPackageSource local parsed_source = { package = ("%s/%s"):format(purl.namespace, purl.name), version = purl.version, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedComposerSource function M.install(ctx, source) local composer = require "mason-core.installer.managers.composer" return composer.install(source.package, source.version) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.packagist.get_all_versions(("%s/%s"):format(purl.namespace, purl.name)) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/gem.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" local M = {} ---@class GemSource : RegistryPackageSource ---@field extra_packages? string[] ---@param source GemSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedGemSource : ParsedPackageSource local parsed_source = { package = purl.name, version = purl.version, extra_packages = source.extra_packages, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedGemSource function M.install(ctx, source) local gem = require "mason-core.installer.managers.gem" return gem.install(source.package, source.version, { extra_packages = source.extra_packages, }) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.rubygems.get_all_versions(purl.name) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/generic/build.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local common = require "mason-core.installer.managers.common" local expr = require "mason-core.installer.compiler.expr" local util = require "mason-core.installer.compiler.util" local M = {} ---@class GenericBuildSource : RegistryPackageSource ---@field build BuildInstruction | BuildInstruction[] ---@param source GenericBuildSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) return Result.try(function(try) ---@type BuildInstruction local build_instruction = try(util.coalesce_by_target(source.build, opts)) if build_instruction.env then local expr_ctx = { version = purl.version, target = build_instruction.target } build_instruction.env = try(expr.tbl_interpolate(build_instruction.env, expr_ctx)) end ---@class ParsedGenericBuildSource : ParsedPackageSource local parsed_source = { build = build_instruction, } return parsed_source end) end ---@async ---@param ctx InstallContext ---@param source ParsedGenericBuildSource function M.install(ctx, source) return common.run_build_instruction(source.build) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/generic/download.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local common = require "mason-core.installer.managers.common" local expr = require "mason-core.installer.compiler.expr" local util = require "mason-core.installer.compiler.util" local M = {} ---@class GenericDownload ---@field target (Platform | Platform[])? ---@field files table ---@class GenericDownloadSource : RegistryPackageSource ---@field download GenericDownload | GenericDownload[] ---@param source GenericDownloadSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) return Result.try(function(try) local download = try(util.coalesce_by_target(source.download, opts)) local expr_ctx = { version = purl.version } ---@type { files: table } local interpolated_download = try(expr.tbl_interpolate(download, expr_ctx)) ---@type DownloadItem[] local downloads = _.map(function(pair) ---@type DownloadItem return { out_file = pair[1], download_url = pair[2], } end, _.to_pairs(interpolated_download.files)) ---@class ParsedGenericDownloadSource : ParsedPackageSource local parsed_source = { download = interpolated_download, downloads = downloads, } return parsed_source end) end ---@async ---@param ctx InstallContext ---@param source ParsedGenericDownloadSource function M.install(ctx, source) return common.download_files(ctx, source.downloads) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/generic/init.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local M = {} ---@param source GenericDownloadSource | GenericBuildSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) if source.download then source = source --[[@as GenericDownloadSource]] return require("mason-core.installer.compiler.compilers.generic.download").parse(source, purl, opts) elseif source.build then source = source --[[@as GenericBuildSource]] return require("mason-core.installer.compiler.compilers.generic.build").parse(source, purl, opts) else return Result.failure "Unknown source type." end end ---@async ---@param ctx InstallContext ---@param source ParsedGenericDownloadSource | ParsedGenericBuildSource function M.install(ctx, source) if source.download then source = source --[[@as ParsedGenericDownloadSource]] return require("mason-core.installer.compiler.compilers.generic.download").install(ctx, source) elseif source.build then source = source --[[@as ParsedGenericBuildSource]] return require("mason-core.installer.compiler.compilers.generic.build").install(ctx, source) else return Result.failure "Unknown source type." end end ---@async ---@param purl Purl function M.get_versions(purl) return Result.failure "Unimplemented." end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/github/build.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local common = require "mason-core.installer.managers.common" local expr = require "mason-core.installer.compiler.expr" local util = require "mason-core.installer.compiler.util" local M = {} ---@class GitHubBuildSource : RegistryPackageSource ---@field build BuildInstruction | BuildInstruction[] ---@param source GitHubBuildSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) return Result.try(function(try) ---@type BuildInstruction local build_instruction = try(util.coalesce_by_target(source.build, opts)) local expr_ctx = { version = purl.version } build_instruction.env = try(expr.tbl_interpolate(build_instruction.env or {}, expr_ctx)) ---@class ParsedGitHubBuildSource : ParsedPackageSource local parsed_source = { build = build_instruction, repo = ("https://github.com/%s/%s.git"):format(purl.namespace, purl.name), rev = purl.version, } return parsed_source end) end ---@async ---@param ctx InstallContext ---@param source ParsedGitHubBuildSource function M.install(ctx, source) local std = require "mason-core.installer.managers.std" return Result.try(function(try) try(std.clone(source.repo, { rev = source.rev })) try(common.run_build_instruction(source.build)) end) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/github/init.lua ================================================ local Result = require "mason-core.result" local M = {} ---@param source GitHubReleaseSource | GitHubBuildSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) if source.asset then source = source --[[@as GitHubReleaseSource]] return require("mason-core.installer.compiler.compilers.github.release").parse(source, purl, opts) elseif source.build then source = source --[[@as GitHubBuildSource]] return require("mason-core.installer.compiler.compilers.github.build").parse(source, purl, opts) else return Result.failure "Unknown source type." end end ---@async ---@param ctx InstallContext ---@param source ParsedGitHubReleaseSource | ParsedGitHubBuildSource function M.install(ctx, source) if source.asset then source = source--[[@as ParsedGitHubReleaseSource]] return require("mason-core.installer.compiler.compilers.github.release").install(ctx, source) elseif source.build then source = source--[[@as ParsedGitHubBuildSource]] return require("mason-core.installer.compiler.compilers.github.build").install(ctx, source) else return Result.failure "Unknown source type." end end ---@async ---@param purl Purl ---@param source GitHubReleaseSource | GitHubBuildSource function M.get_versions(purl, source) if source.asset then return require("mason-core.installer.compiler.compilers.github.release").get_versions(purl) elseif source.build then -- We can't yet reliably determine the true source (release, tag, commit, etc.) for "build" sources. return Result.failure "Unimplemented." else return Result.failure "Unknown source type." end end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/github/release.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local common = require "mason-core.installer.managers.common" local expr = require "mason-core.installer.compiler.expr" local providers = require "mason-core.providers" local settings = require "mason.settings" local util = require "mason-core.installer.compiler.util" ---@class GitHubReleaseSourceAsset : FileDownloadSpec ---@field target? Platform | Platform[] ---@class GitHubReleaseSource : RegistryPackageSource ---@field asset GitHubReleaseSourceAsset | GitHubReleaseSourceAsset[] local M = {} ---@param source GitHubReleaseSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) return Result.try(function(try) local expr_ctx = { version = purl.version } ---@type GitHubReleaseSourceAsset local asset = try(util.coalesce_by_target(try(expr.tbl_interpolate(source.asset, expr_ctx)), opts)) local downloads = common.parse_downloads(asset, function(file) return settings.current.github.download_url_template:format( ("%s/%s"):format(purl.namespace, purl.name), purl.version, file ) end) ---@class ParsedGitHubReleaseSource : ParsedPackageSource local parsed_source = { repo = ("%s/%s"):format(purl.namespace, purl.name), asset = common.normalize_files(asset), downloads = downloads, } return parsed_source end) end ---@async ---@param ctx InstallContext ---@param source ParsedGitHubReleaseSource function M.install(ctx, source) return common.download_files(ctx, source.downloads) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.github.get_all_release_versions(("%s/%s"):format(purl.namespace, purl.name)) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/golang.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" local util = require "mason-core.installer.compiler.util" local M = {} ---@param purl Purl local function get_package_name(purl) if purl.subpath then return ("%s/%s/%s"):format(purl.namespace, purl.name, purl.subpath) else return ("%s/%s"):format(purl.namespace, purl.name) end end ---@class GolangSource : RegistryPackageSource ---@field extra_packages? string[] ---@param source GolangSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedGolangSource : ParsedPackageSource local parsed_source = { package = get_package_name(purl), version = purl.version, extra_packages = source.extra_packages, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedGolangSource function M.install(ctx, source) local golang = require "mason-core.installer.managers.golang" return golang.install(source.package, source.version, { extra_packages = source.extra_packages, }) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.golang.get_all_versions(("%s/%s"):format(purl.namespace, purl.name)) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/luarocks.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local M = {} ---@param purl Purl local function parse_package_name(purl) if purl.namespace then return ("%s/%s"):format(purl.namespace, purl.name) else return purl.name end end local parse_server = _.path { "qualifiers", "repository_url" } local parse_dev = _.compose(_.equals "true", _.path { "qualifiers", "dev" }) ---@param source RegistryPackageSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedLuaRocksSource : ParsedPackageSource local parsed_source = { package = parse_package_name(purl), version = purl.version, ---@type string? server = parse_server(purl), ---@type boolean? dev = parse_dev(purl), } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedLuaRocksSource function M.install(ctx, source) local luarocks = require "mason-core.installer.managers.luarocks" return luarocks.install(source.package, source.version, { server = source.server, dev = source.dev, }) end ---@async ---@param purl Purl function M.get_versions(purl) return Result.failure "Unimplemented." end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/mason.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local M = {} ---@param source RegistryPackageSource ---@param purl Purl function M.parse(source, purl) if type(source.install) ~= "function" and type((getmetatable(source.install) or {}).__call) ~= "function" then return Result.failure "source.install is not a function." end ---@class ParsedMasonSource : ParsedPackageSource local parsed_source = { purl = purl, ---@type async fun(ctx: InstallContext, purl: Purl) install = source.install, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedMasonSource function M.install(ctx, source) ctx.spawn.strict_mode = true return Result.pcall(source.install, ctx, source.purl) :on_success(function() ctx.spawn.strict_mode = false end) :on_failure(function() ctx.spawn.strict_mode = false end) end ---@async ---@param purl Purl function M.get_versions(purl) return Result.failure "Unimplemented." end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/npm.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" ---@param purl Purl local function purl_to_npm(purl) if purl.namespace then return ("%s/%s"):format(purl.namespace, purl.name) else return purl.name end end local M = {} ---@class NpmSource : RegistryPackageSource ---@field extra_packages? string[] ---@param source NpmSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedNpmSource : ParsedPackageSource local parsed_source = { package = purl_to_npm(purl), version = purl.version, extra_packages = source.extra_packages, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedNpmSource function M.install(ctx, source) local npm = require "mason-core.installer.managers.npm" return Result.try(function(try) try(npm.init()) try(npm.install(source.package, source.version, { extra_packages = source.extra_packages, })) end) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.npm.get_all_versions(purl_to_npm(purl)) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/nuget.lua ================================================ local Result = require "mason-core.result" local M = {} ---@param source RegistryPackageSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedNugetSource : ParsedPackageSource local parsed_source = { package = purl.name, version = purl.version, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedNugetSource function M.install(ctx, source) local nuget = require "mason-core.installer.managers.nuget" return nuget.install(source.package, source.version) end ---@async ---@param purl Purl function M.get_versions(purl) return Result.failure "Unimplemented." end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/opam.lua ================================================ local Result = require "mason-core.result" local M = {} ---@param source RegistryPackageSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedOpamSource : ParsedPackageSource local parsed_source = { package = purl.name, version = purl.version, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedOpamSource function M.install(ctx, source) local opam = require "mason-core.installer.managers.opam" return opam.install(source.package, source.version) end ---@async ---@param purl Purl function M.get_versions(purl) return Result.failure "Unimplemented." end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/openvsx.lua ================================================ local Result = require "mason-core.result" local common = require "mason-core.installer.managers.common" local expr = require "mason-core.installer.compiler.expr" local providers = require "mason-core.providers" local util = require "mason-core.installer.compiler.util" local M = {} ---@class OpenVSXSourceDownload : FileDownloadSpec ---@field target? Platform | Platform[] ---@field target_platform? string ---@class OpenVSXSource : RegistryPackageSource ---@field download OpenVSXSourceDownload | OpenVSXSourceDownload[] ---@param source OpenVSXSource ---@param purl Purl ---@param opts PackageInstallOpts function M.parse(source, purl, opts) return Result.try(function(try) local expr_ctx = { version = purl.version } ---@type OpenVSXSourceDownload local download = try(util.coalesce_by_target(try(expr.tbl_interpolate(source.download, expr_ctx)), opts)) local downloads = common.parse_downloads(download, function(file) if download.target_platform then return ("https://open-vsx.org/api/%s/%s/%s/%s/file/%s"):format( purl.namespace, purl.name, download.target_platform, purl.version, file ) else return ("https://open-vsx.org/api/%s/%s/%s/file/%s"):format( purl.namespace, purl.name, purl.version, file ) end end) ---@class ParsedOpenVSXSource : ParsedPackageSource local parsed_source = { download = common.normalize_files(download), downloads = downloads, } return parsed_source end) end ---@param ctx InstallContext ---@param source ParsedOpenVSXSource function M.install(ctx, source) return common.download_files(ctx, source.downloads) end ---@param purl Purl function M.get_versions(purl) return providers.openvsx.get_all_versions(purl.namespace, purl.name) end return M ================================================ FILE: lua/mason-core/installer/compiler/compilers/pypi.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local providers = require "mason-core.providers" local settings = require "mason.settings" local M = {} ---@class PypiSource : RegistryPackageSource ---@field extra_packages? string[] ---@param source PypiSource ---@param purl Purl function M.parse(source, purl) ---@class ParsedPypiSource : ParsedPackageSource local parsed_source = { package = purl.name, version = purl.version --[[ @as string ]], extra = _.path({ "qualifiers", "extra" }, purl), extra_packages = source.extra_packages, pip = { upgrade = settings.current.pip.upgrade_pip, extra_args = settings.current.pip.install_args, }, } return Result.success(parsed_source) end ---@async ---@param ctx InstallContext ---@param source ParsedPypiSource function M.install(ctx, source) local pypi = require "mason-core.installer.managers.pypi" return Result.try(function(try) try(pypi.init { package = { name = source.package, version = source.version, }, upgrade_pip = source.pip.upgrade, install_extra_args = source.pip.extra_args, }) try(pypi.install(source.package, source.version, { extra = source.extra, extra_packages = source.extra_packages, install_extra_args = source.pip.extra_args, })) end) end ---@async ---@param purl Purl function M.get_versions(purl) return providers.pypi.get_all_versions(purl.name) end return M ================================================ FILE: lua/mason-core/installer/compiler/expr.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local platform = require "mason-core.platform" local M = {} local parse_expr = _.compose( _.apply_spec { value_expr = _.head, filters = _.drop(1), }, _.filter(_.complement(_.equals "")), _.map(_.trim), _.split "|" ) ---@param predicate (fun(value: string): boolean) | boolean ---@param value string local take_if = _.curryN(function(predicate, value) if type(predicate) == "boolean" then predicate = _.always(predicate) end return predicate(value) and value or nil end, 2) ---@param predicate (fun(value: string): boolean) | boolean ---@param value string local take_if_not = _.curryN(function(predicate, value) if type(predicate) == "boolean" then predicate = _.always(predicate) end return (not predicate(value)) and value or nil end, 2) local FILTERS = { equals = _.equals, not_equals = _.not_equals, strip_prefix = _.strip_prefix, strip_suffix = _.strip_suffix, take_if = take_if, take_if_not = take_if_not, to_lower = _.to_lower, to_upper = _.to_upper, is_platform = function(target) return platform.is[target] end, } ---@generic T : table ---@param tbl T ---@return T local function shallow_clone(tbl) local res = {} for k, v in pairs(tbl) do res[k] = v end return res end ---@param expr string ---@param ctx table local function eval(expr, ctx) return setfenv(assert(loadstring("return " .. expr), ("Failed to parse expression: %q"):format(expr)), ctx)() end ---@param str string ---@param ctx table function M.interpolate(str, ctx) ctx = shallow_clone(ctx) setmetatable(ctx, { __index = FILTERS }) return Result.pcall(function() return _.gsub("{{([^}]+)}}", function(expr) local components = parse_expr(expr) local value = eval(components.value_expr, ctx) local filters = _.map(function(filter_expr) local filter = eval(filter_expr, ctx) assert(type(filter) == "function", ("Invalid filter expression: %q"):format(filter_expr)) return filter end, components.filters) local reduced_value = _.reduce(_.apply_to, value, filters) return reduced_value ~= nil and tostring(reduced_value) or "" end, str) end) end ---@generic T : table ---@param tbl T ---@param ctx table ---@return Result # Result function M.tbl_interpolate(tbl, ctx) return Result.try(function(try) local interpolated = {} for k, v in pairs(tbl) do if type(k) == "string" then k = try(M.interpolate(k, ctx)) end if type(v) == "string" then interpolated[k] = try(M.interpolate(v, ctx)) elseif type(v) == "table" then interpolated[k] = try(M.tbl_interpolate(v, ctx)) else interpolated[k] = v end end return interpolated end) end return M ================================================ FILE: lua/mason-core/installer/compiler/init.lua ================================================ local Optional = require "mason-core.optional" local Purl = require "mason-core.purl" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local link = require "mason-core.installer.compiler.link" local log = require "mason-core.log" local schemas = require "mason-core.installer.compiler.schemas" local util = require "mason-core.installer.compiler.util" local M = {} ---@type table M.SCHEMA_CAP = _.set_of { "registry+v1", } ---@type table local COMPILERS = {} ---@param id string ---@param compiler InstallerCompiler function M.register_compiler(id, compiler) COMPILERS[id] = compiler end M.register_compiler("cargo", _.lazy_require "mason-core.installer.compiler.compilers.cargo") M.register_compiler("composer", _.lazy_require "mason-core.installer.compiler.compilers.composer") M.register_compiler("gem", _.lazy_require "mason-core.installer.compiler.compilers.gem") M.register_compiler("generic", _.lazy_require "mason-core.installer.compiler.compilers.generic") M.register_compiler("github", _.lazy_require "mason-core.installer.compiler.compilers.github") M.register_compiler("golang", _.lazy_require "mason-core.installer.compiler.compilers.golang") M.register_compiler("luarocks", _.lazy_require "mason-core.installer.compiler.compilers.luarocks") M.register_compiler("mason", _.lazy_require "mason-core.installer.compiler.compilers.mason") M.register_compiler("npm", _.lazy_require "mason-core.installer.compiler.compilers.npm") M.register_compiler("nuget", _.lazy_require "mason-core.installer.compiler.compilers.nuget") M.register_compiler("opam", _.lazy_require "mason-core.installer.compiler.compilers.opam") M.register_compiler("openvsx", _.lazy_require "mason-core.installer.compiler.compilers.openvsx") M.register_compiler("pypi", _.lazy_require "mason-core.installer.compiler.compilers.pypi") ---@param purl Purl ---@return Result # Result function M.get_compiler(purl) return Optional.of_nilable(COMPILERS[purl.type]) :ok_or(("Current version of mason.nvim is not capable of parsing package type %q."):format(purl.type)) end ---@class InstallerCompiler ---@field parse fun(source: RegistryPackageSource, purl: Purl, opts: PackageInstallOpts): Result ---@field install async fun(ctx: InstallContext, source: ParsedPackageSource, purl: Purl): Result ---@field get_versions async fun(purl: Purl, source: RegistryPackageSource): Result # Result ---@class ParsedPackageSource ---Upserts {dst} with contents of {src}. List table values will be merged, with contents of {src} prepended. ---@param dst table ---@param src table local function upsert(dst, src) for k, v in pairs(src) do if type(v) == "table" then if _.is_list(v) then dst[k] = _.concat(v, dst[k] or {}) else dst[k] = upsert(dst[k] or {}, src[k]) end else dst[k] = v end end return dst end ---@param source RegistryPackageSource ---@param version string? ---@return RegistryPackageSource local function coalesce_source(source, version) if version and source.version_overrides then for i = #source.version_overrides, 1, -1 do local version_override = source.version_overrides[i] local version_type, constraint = unpack(_.split(":", version_override.constraint)) if version_type == "semver" then local semver = require "mason-core.semver" local version_match = Result.try(function(try) local requested_version = try(semver.parse(version)) if _.starts_with("<=", constraint) then local rule_version = try(semver.parse(_.strip_prefix("<=", constraint))) return requested_version <= rule_version elseif _.starts_with(">=", constraint) then local rule_version = try(semver.parse(_.strip_prefix(">=", constraint))) return requested_version >= rule_version else local rule_version = try(semver.parse(constraint)) return requested_version == rule_version end end):get_or_else(false) if version_match then return _.dissoc("constraint", version_override) end end end end return _.dissoc("version_overrides", source) end ---@param spec RegistryPackageSpec ---@param opts PackageInstallOpts function M.parse(spec, opts) log.trace("Parsing spec", spec.name, opts) return Result.try(function(try) if not M.SCHEMA_CAP[spec.schema] then return Result.failure( ("Current version of mason.nvim is not capable of parsing package schema version %q."):format( spec.schema ) ) end local source = coalesce_source(spec.source, opts.version) if source.supported_platforms then try(util.ensure_valid_platform(source.supported_platforms)) end ---@type Purl local purl = try(Purl.parse(source.id)) log.trace("Parsed purl.", source.id, purl) if opts.version then purl.version = opts.version end ---@type InstallerCompiler local compiler = try(M.get_compiler(purl)) log.trace("Found compiler for purl.", source.id) local parsed_source = try(compiler.parse(source, purl, opts)) log.trace("Parsed source for purl.", source.id, parsed_source) return { compiler = compiler, source = vim.tbl_extend("keep", parsed_source, source), raw_source = source, purl = purl, } end):on_failure(function(err) log.debug("Failed to parse spec spec", spec.name, err) end) end ---@async ---@param spec RegistryPackageSpec ---@param opts PackageInstallOpts function M.compile_installer(spec, opts) log.debug("Compiling installer.", spec.name, opts) return Result.try(function(try) -- Parsers run synchronously and may access API functions, so we schedule before-hand. a.scheduler() local map_parse_err = _.cond { { _.equals "PLATFORM_UNSUPPORTED", function() if opts.target then return ("Platform %q is unsupported."):format(opts.target) else return "The current platform is unsupported." end end, }, { _.T, _.identity }, } ---@type { purl: Purl, compiler: InstallerCompiler, source: ParsedPackageSource, raw_source: RegistryPackageSource } local parsed = try(M.parse(spec, opts):map_err(map_parse_err)) ---@async ---@param ctx InstallContext return function(ctx) return Result.try(function(try) if ctx.opts.version then try(util.ensure_valid_version(function() return parsed.compiler.get_versions(parsed.purl, parsed.raw_source) end)) end -- Run installer a.scheduler() try(parsed.compiler.install(ctx, parsed.source, parsed.purl)) if spec.schemas then local result = schemas.download(ctx, spec, parsed.purl, parsed.source):on_failure(function(err) log.error("Failed to download schemas", ctx.package, err) end) if opts.strict then -- schema download sources are not considered stable nor a critical feature, so we only fail in strict mode try(result) end end -- Expand & register links if spec.bin then try(link.bin(ctx, spec, parsed.purl, parsed.source)) end if spec.share then try(link.share(ctx, spec, parsed.purl, parsed.source)) end if spec.opt then try(link.opt(ctx, spec, parsed.purl, parsed.source)) end ctx.receipt:with_source { type = ctx.package.spec.schema, id = Purl.compile(parsed.purl), -- Exclude the "install" field from "mason" sources because this is a Lua function. raw = parsed.purl.type == "mason" and _.dissoc("install", parsed.raw_source) or parsed.raw_source, } ctx.receipt:with_install_options(opts) end) end end) end return M ================================================ FILE: lua/mason-core/installer/compiler/link.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local expr = require "mason-core.installer.compiler.expr" local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} local filter_empty_values = _.compose( _.from_pairs, _.filter(function(pair) return pair[2] ~= "" end), _.to_pairs ) local bin_delegates = { ["luarocks"] = function(target) return require("mason-core.installer.managers.luarocks").bin_path(target) end, ["composer"] = function(target) return require("mason-core.installer.managers.composer").bin_path(target) end, ["opam"] = function(target) return require("mason-core.installer.managers.opam").bin_path(target) end, ["python"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() if not ctx.fs:file_exists(target) then return Result.failure(("Cannot write python wrapper for path %q as it doesn't exist."):format(target)) end return Result.pcall(function() local python = platform.is.win and "python" or "python3" return ctx:write_shell_exec_wrapper( bin, ("%s %q"):format(python, path.concat { ctx:get_install_path(), target }) ) end) end, ["php"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() return Result.pcall(function() return ctx:write_php_exec_wrapper(bin, target) end) end, ["pyvenv"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() return Result.pcall(function() return ctx:write_pyvenv_exec_wrapper(bin, target) end) end, ["dotnet"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() if not ctx.fs:file_exists(target) then return Result.failure(("Cannot write dotnet wrapper for path %q as it doesn't exist."):format(target)) end return Result.pcall(function() return ctx:write_shell_exec_wrapper( bin, ("dotnet %q"):format(path.concat { ctx:get_install_path(), target, }) ) end) end, ["node"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() return Result.pcall(function() return ctx:write_node_exec_wrapper(bin, target) end) end, ["ruby"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() return Result.pcall(function() return ctx:write_ruby_exec_wrapper(bin, target) end) end, ["exec"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() return Result.pcall(function() return ctx:write_exec_wrapper(bin, target) end) end, ["java-jar"] = function(target, bin) local installer = require "mason-core.installer" local ctx = installer.context() if not ctx.fs:file_exists(target) then return Result.failure(("Cannot write Java JAR wrapper for path %q as it doesn't exist."):format(target)) end return Result.pcall(function() return ctx:write_shell_exec_wrapper( bin, ("java -jar %q"):format(path.concat { ctx:get_install_path(), target, }) ) end) end, ["nuget"] = function(target) return require("mason-core.installer.managers.nuget").bin_path(target) end, ["npm"] = function(target) return require("mason-core.installer.managers.npm").bin_path(target) end, ["gem"] = function(target) return require("mason-core.installer.managers.gem").create_bin_wrapper(target) end, ["cargo"] = function(target) return require("mason-core.installer.managers.cargo").bin_path(target) end, ["pypi"] = function(target) return require("mason-core.installer.managers.pypi").bin_path(target) end, ["golang"] = function(target) return require("mason-core.installer.managers.golang").bin_path(target) end, } ---Expands bin specification from spec and registers bins to be linked. ---@async ---@param ctx InstallContext ---@param spec RegistryPackageSpec ---@param purl Purl ---@param source ParsedPackageSource local function expand_bin(ctx, spec, purl, source) log.debug("Registering bin links", ctx.package, spec.bin) return Result.try(function(try) local expr_ctx = { version = purl.version, source = source, } local bin_table = spec.bin if not bin_table then log.fmt_debug("%s spec provides no bin.", ctx.package) return end local interpolated_bins = filter_empty_values(try(expr.tbl_interpolate(bin_table, expr_ctx))) local expanded_bin_table = {} for bin, target in pairs(interpolated_bins) do -- Expand "npm:typescript-language-server"-like expressions local delegated_bin = _.match("^(.+):(.+)$", target) if #delegated_bin > 0 then local bin_type, executable = unpack(delegated_bin) log.fmt_trace("Transforming managed executable=%s via %s", executable, bin_type) local delegate = try(Optional.of_nilable(bin_delegates[bin_type]):ok_or(("Unknown bin type: %s"):format(bin_type))) target = try(delegate(executable, bin)) end log.fmt_debug("Expanded bin link %s -> %s", bin, target) if not ctx.fs:file_exists(target) then return Result.failure(("Tried to link bin %q to non-existent target %q."):format(bin, target)) end if platform.is.unix then ctx.fs:chmod_exec(target) end expanded_bin_table[bin] = target end return expanded_bin_table end) end local is_dir_path = _.matches "/$" ---Expands symlink path specifications from spec and returns symlink file table. ---@async ---@param ctx InstallContext ---@param purl Purl ---@param source ParsedPackageSource ---@param file_spec_table table local function expand_file_spec(ctx, purl, source, file_spec_table) log.debug("Registering symlinks", ctx.package, file_spec_table) return Result.try(function(try) local expr_ctx = { version = purl.version, source = source } ---@type table local interpolated_paths = filter_empty_values(try(expr.tbl_interpolate(file_spec_table, expr_ctx))) ---@type table local expanded_links = {} for dest, source_path in pairs(interpolated_paths) do local cwd = ctx.cwd:get() if is_dir_path(dest) then -- linking dir -> dir if not is_dir_path(source_path) then return Result.failure(("Cannot link file %q to dir %q."):format(source_path, dest)) end a.scheduler() local glob = path.concat { cwd, source_path } .. "**/*" log.fmt_trace("Symlink glob for %s: %s", ctx.package, glob) ---@type string[] local files = _.filter_map(function(abs_path) if not fs.sync.file_exists(abs_path) then -- only link actual files (e.g. exclude directory entries from glob) return Optional.empty() end -- turn into relative paths return Optional.of(abs_path:sub(#cwd + 2)) -- + 2 to remove leading path separator (/) end, vim.fn.glob(glob, false, true)) log.fmt_trace("Expanded glob %s: %s", glob, files) for __, file in ipairs(files) do -- File destination should be relative to the source directory. For example, should the source_path -- be "gh_2.22.1_macOS_amd64/share/man/" and dest be "man/", it should link source files to the -- following destinations: -- -- gh_2.22.1_macOS_amd64/share/man/ man/ -- ------------------------------------------------------------------------- -- gh_2.22.1_macOS_amd64/share/man/man1/gh.1 man/man1/gh.1 -- gh_2.22.1_macOS_amd64/share/man/man1/gh-run.1 man/man1/gh-run.1 -- gh_2.22.1_macOS_amd64/share/man/man1/gh-ssh-key.1 man/man1/gh-run.1 -- local file_dest = path.concat { _.trim_end_matches("/", dest), file:sub(#source_path + 1), } expanded_links[file_dest] = file end else -- linking file -> file if is_dir_path(source_path) then return Result.failure(("Cannot link dir %q to file %q."):format(source_path, dest)) end expanded_links[dest] = source_path end end return expanded_links end) end ---@async ---@param ctx InstallContext ---@param spec RegistryPackageSpec ---@param purl Purl ---@param source ParsedPackageSource ---@nodiscard M.bin = function(ctx, spec, purl, source) return expand_bin(ctx, spec, purl, source):on_success(function(links) ctx.links.bin = vim.tbl_extend("force", ctx.links.bin, links) end) end ---@async ---@param ctx InstallContext ---@param spec RegistryPackageSpec ---@param purl Purl ---@param source ParsedPackageSource ---@nodiscard M.share = function(ctx, spec, purl, source) return expand_file_spec(ctx, purl, source, spec.share):on_success(function(links) ctx.links.share = vim.tbl_extend("force", ctx.links.share, links) end) end ---@async ---@param ctx InstallContext ---@param spec RegistryPackageSpec ---@param purl Purl ---@param source ParsedPackageSource ---@nodiscard M.opt = function(ctx, spec, purl, source) return expand_file_spec(ctx, purl, source, spec.opt):on_success(function(links) ctx.links.opt = vim.tbl_extend("force", ctx.links.opt, links) end) end return M ================================================ FILE: lua/mason-core/installer/compiler/schemas.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local expr = require "mason-core.installer.compiler.expr" local fetch = require "mason-core.fetch" local log = require "mason-core.log" local path = require "mason-core.path" local std = require "mason-core.installer.managers.std" local M = {} ---@async ---@param ctx InstallContext ---@param url string local function download_lsp_schema(ctx, url) return Result.try(function(try) local is_vscode_schema = _.starts_with("vscode:", url) local out_file = path.concat { "mason-schemas", "lsp.json" } local share_file = path.concat { "mason-schemas", "lsp", ("%s.json"):format(ctx.package.name) } if is_vscode_schema then local url = unpack(_.match("^vscode:(.+)$", url)) ctx.stdio_sink:stdout(("Downloading LSP configuration schema from %q…\n"):format(url)) local json = try(fetch(url)) ---@type { contributes?: { configuration?: table } } local schema = try(Result.pcall(vim.json.decode, json)) local configuration = schema.contributes and schema.contributes.configuration if configuration then ctx.fs:write_file(out_file, vim.json.encode(configuration) --[[@as string]]) ctx.links.share[share_file] = out_file else return Result.failure "Unable to find LSP entry in VSCode schema." end else ctx.stdio_sink:stdout(("Downloading LSP configuration schema from %q…\n"):format(url)) try(std.download_file(url, out_file)) ctx.links.share[share_file] = out_file end end) end ---@async ---@param ctx InstallContext ---@param spec RegistryPackageSpec ---@param purl Purl ---@param source ParsedPackageSource ---@nodiscard function M.download(ctx, spec, purl, source) return Result.try(function(try) log.debug("schemas: download", ctx.package, spec.schemas) local schemas = spec.schemas if not schemas then return end ---@type RegistryPackageSchemas local interpolated_schemas = try(expr.tbl_interpolate(schemas, { version = purl.version, source = source })) ctx.fs:mkdir "mason-schemas" if interpolated_schemas.lsp then try(a.wait_first { function() return download_lsp_schema(ctx, interpolated_schemas.lsp) end, function() a.sleep(5000) return Result.failure "Schema download timed out." end, }) end end) end return M ================================================ FILE: lua/mason-core/installer/compiler/util.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local platform = require "mason-core.platform" local M = {} ---@generic T : { target: Platform | Platform[] } ---@param candidates T[] | T ---@param opts PackageInstallOpts ---@return Result # Result function M.coalesce_by_target(candidates, opts) if not _.is_list(candidates) then return Result.success(candidates) end return Optional.of_nilable(_.find_first(function(asset) if opts.target then -- Matching against a provided target rather than the current platform is an escape hatch primarily meant -- for automated testing purposes. if type(asset.target) == "table" then return _.any(_.equals(opts.target), asset.target) else return asset.target == opts.target end else if type(asset.target) == "table" then return _.any(function(target) return platform.is[target] end, asset.target) else return platform.is[asset.target] end end end, candidates)):ok_or "PLATFORM_UNSUPPORTED" end ---Checks whether a custom version of a package installation corresponds to a valid version. ---@async ---@param versions_thunk async fun(): Result Result function M.ensure_valid_version(versions_thunk) local ctx = installer.context() local version = ctx.opts.version if version and not ctx.opts.force then ctx.stdio_sink:stdout "Fetching available versions…\n" local all_versions = versions_thunk() if all_versions:is_failure() then log.warn("Failed to fetch versions for package", ctx.package) -- Gracefully fail (i.e. optimistically continue package installation) return Result.success() end all_versions = all_versions:get_or_else {} if not _.any(_.equals(version), all_versions) then ctx.stdio_sink:stderr(("Tried to install invalid version %q. Available versions:\n"):format(version)) ctx.stdio_sink:stderr(_.compose(_.join "\n", _.map(_.join ", "), _.split_every(15))(all_versions)) ctx.stdio_sink:stderr "\n\n" ctx.stdio_sink:stderr( ("Run with --force flag to bypass version validation:\n :MasonInstall --force %s@%s\n\n"):format( ctx.package.name, version ) ) return Result.failure(("Version %q is not available."):format(version)) end end return Result.success() end ---@param platforms string[] function M.ensure_valid_platform(platforms) if not _.any(function(target) return platform.is[target] end, platforms) then return Result.failure "PLATFORM_UNSUPPORTED" end return Result.success() end return M ================================================ FILE: lua/mason-core/installer/context/InstallContextCwd.lua ================================================ local Result = require "mason-core.result" local fs = require "mason-core.fs" local path = require "mason-core.path" ---@class InstallContextCwd ---@field private handle InstallHandle ---@field private cwd string? local InstallContextCwd = {} InstallContextCwd.__index = InstallContextCwd ---@param handle InstallHandle function InstallContextCwd:new(handle) ---@type InstallContextCwd local instance = {} setmetatable(instance, self) instance.handle = handle instance.cwd = nil return instance end function InstallContextCwd:initialize() return Result.try(function(try) local staging_dir = self.handle.location:staging(self.handle.package.name) if fs.sync.dir_exists(staging_dir) then try(Result.pcall(fs.sync.rmrf, staging_dir)) end try(Result.pcall(fs.sync.mkdirp, staging_dir)) self:set(staging_dir) end) end function InstallContextCwd:get() assert(self.cwd ~= nil, "Tried to access cwd before it was set.") return self.cwd end ---@param new_abs_cwd string function InstallContextCwd:set(new_abs_cwd) assert(type(new_abs_cwd) == "string", "new_cwd is not a string") assert( path.is_subdirectory(self.handle.location:get_dir(), new_abs_cwd), ("%q is not a subdirectory of %q"):format(new_abs_cwd, self.handle.location) ) self.cwd = new_abs_cwd return self end return InstallContextCwd ================================================ FILE: lua/mason-core/installer/context/InstallContextFs.lua ================================================ local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" ---@class InstallContextFs ---@field private cwd InstallContextCwd local InstallContextFs = {} InstallContextFs.__index = InstallContextFs ---@param cwd InstallContextCwd function InstallContextFs:new(cwd) ---@type InstallContextFs local instance = {} setmetatable(instance, InstallContextFs) instance.cwd = cwd return instance end ---@async ---@param rel_path string The relative path from the current working directory to the file to append. ---@param contents string function InstallContextFs:append_file(rel_path, contents) return fs.async.append_file(path.concat { self.cwd:get(), rel_path }, contents) end ---@async ---@param rel_path string The relative path from the current working directory to the file to write. ---@param contents string function InstallContextFs:write_file(rel_path, contents) return fs.async.write_file(path.concat { self.cwd:get(), rel_path }, contents) end ---@async ---@param rel_path string The relative path from the current working directory to the file to read. function InstallContextFs:read_file(rel_path) return fs.async.read_file(path.concat { self.cwd:get(), rel_path }) end ---@async ---@param rel_path string The relative path from the current working directory. function InstallContextFs:file_exists(rel_path) return fs.async.file_exists(path.concat { self.cwd:get(), rel_path }) end ---@async ---@param rel_path string The relative path from the current working directory. function InstallContextFs:dir_exists(rel_path) return fs.async.dir_exists(path.concat { self.cwd:get(), rel_path }) end ---@async ---@param rel_path string The relative path from the current working directory. function InstallContextFs:rmrf(rel_path) return fs.async.rmrf(path.concat { self.cwd:get(), rel_path }) end ---@async ---@param rel_path string The relative path from the current working directory. function InstallContextFs:unlink(rel_path) return fs.async.unlink(path.concat { self.cwd:get(), rel_path }) end ---@async ---@param old_path string ---@param new_path string function InstallContextFs:rename(old_path, new_path) return fs.async.rename(path.concat { self.cwd:get(), old_path }, path.concat { self.cwd:get(), new_path }) end ---@async ---@param dir_path string function InstallContextFs:mkdir(dir_path) return fs.async.mkdir(path.concat { self.cwd:get(), dir_path }) end ---@async ---@param dir_path string function InstallContextFs:mkdirp(dir_path) return fs.async.mkdirp(path.concat { self.cwd:get(), dir_path }) end ---@async ---@param file_path string function InstallContextFs:chmod_exec(file_path) local bit = require "bit" -- see chmod(2) local USR_EXEC = 0x40 local GRP_EXEC = 0x8 local ALL_EXEC = 0x1 local EXEC = bit.bor(USR_EXEC, GRP_EXEC, ALL_EXEC) local fstat = self:fstat(file_path) if bit.band(fstat.mode, EXEC) ~= EXEC then local plus_exec = bit.bor(fstat.mode, EXEC) log.fmt_debug("Setting exec flags on file %s %o -> %o", file_path, fstat.mode, plus_exec) self:chmod(file_path, plus_exec) -- chmod +x end end ---@async ---@param file_path string ---@param mode integer function InstallContextFs:chmod(file_path, mode) return fs.async.chmod(path.concat { self.cwd:get(), file_path }, mode) end ---@async ---@param file_path string function InstallContextFs:fstat(file_path) return fs.async.fstat(path.concat { self.cwd:get(), file_path }) end return InstallContextFs ================================================ FILE: lua/mason-core/installer/context/InstallContextSpawn.lua ================================================ local spawn = require "mason-core.spawn" ---@class InstallContextSpawn ---@field strict_mode boolean Whether spawn failures should raise an exception rather then return a Result. ---@field private cwd InstallContextCwd ---@field private handle InstallHandle ---@field [string] async fun(opts: SpawnArgs): Result local InstallContextSpawn = {} ---@param handle InstallHandle ---@param cwd InstallContextCwd ---@param strict_mode boolean function InstallContextSpawn:new(handle, cwd, strict_mode) ---@type InstallContextSpawn local instance = {} setmetatable(instance, self) instance.cwd = cwd instance.handle = handle instance.strict_mode = strict_mode return instance end ---@param cmd string function InstallContextSpawn:__index(cmd) ---@param args JobSpawnOpts return function(args) args.cwd = args.cwd or self.cwd:get() args.stdio_sink = args.stdio_sink or self.handle.stdio_sink local on_spawn = args.on_spawn local captured_handle args.on_spawn = function(handle, stdio, pid, ...) captured_handle = handle self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args)) if on_spawn then on_spawn(handle, stdio, pid, ...) end end local function pop_spawn_stack() if captured_handle then self.handle:deregister_spawn_handle(captured_handle) end end local result = spawn[cmd](args):on_success(pop_spawn_stack):on_failure(pop_spawn_stack) if self.strict_mode then return result:get_or_throw() else return result end end end return InstallContextSpawn ================================================ FILE: lua/mason-core/installer/context/init.lua ================================================ local InstallContextCwd = require "mason-core.installer.context.InstallContextCwd" local InstallContextFs = require "mason-core.installer.context.InstallContextFs" local InstallContextSpawn = require "mason-core.installer.context.InstallContextSpawn" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local fetch = require "mason-core.fetch" local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local receipt = require "mason-core.receipt" ---@class InstallContext ---@field receipt InstallReceiptBuilder ---@field fs InstallContextFs ---@field location InstallLocation ---@field spawn InstallContextSpawn ---@field handle InstallHandle ---@field public package AbstractPackage ---@field cwd InstallContextCwd ---@field opts PackageInstallOpts ---@field stdio_sink StdioSink ---@field links { bin: table, share: table, opt: table } local InstallContext = {} InstallContext.__index = InstallContext ---@param handle InstallHandle ---@param opts PackageInstallOpts function InstallContext:new(handle, opts) local cwd = InstallContextCwd:new(handle) local spawn = InstallContextSpawn:new(handle, cwd, false) local fs = InstallContextFs:new(cwd) return setmetatable({ cwd = cwd, spawn = spawn, handle = handle, location = handle.location, -- for convenience package = handle.package, -- for convenience fs = fs, receipt = receipt.InstallReceiptBuilder:new(), stdio_sink = handle.stdio_sink, links = { bin = {}, share = {}, opt = {}, }, opts = opts, }, InstallContext) end ---@async ---@param url string ---@param opts? FetchOpts function InstallContext:fetch(url, opts) opts = opts or {} if opts.out_file then opts.out_file = path.concat { self.cwd:get(), opts.out_file } end return fetch(url, opts):get_or_throw() end ---@async function InstallContext:promote_cwd() local cwd = self.cwd:get() local install_path = self:get_install_path() if install_path == cwd then log.fmt_debug("cwd %s is already promoted", cwd) return end log.fmt_debug("Promoting cwd %s to %s", cwd, install_path) -- 1. Uninstall any existing installation if self.handle.package:is_installed() then a.wait(function(resolve, reject) self.handle.package:uninstall({ bypass_permit = true }, function(success, result) if not success then reject(result) else resolve() end end) end) end -- 2. Prepare for renaming cwd to destination if platform.is.unix then -- Some Unix systems will raise an error when renaming a directory to a destination that does not already exist. fs.sync.mkdir(install_path) end -- 3. Move the cwd to the final installation directory local rename_success, rename_err = pcall(fs.sync.rename, cwd, install_path) if not rename_success then -- On some file systems, we cannot create the directory before renaming. Therefore, remove it and then rename. log.trace("Call to uv_fs_rename() while promoting cwd failed.", rename_err) fs.sync.rmdir(install_path) assert(fs.sync.dir_exists(cwd), "Current working directory no longer exists after retrying uv_fs_rename().") fs.sync.rename(cwd, install_path) end -- 4. Update cwd self.cwd:set(install_path) end ---@param rel_path string The relative path from the current working directory to change cwd to. Will only restore to the initial cwd after execution of fn (if provided). ---@param fn async (fun(): any)? The function to run in the context of the given path. function InstallContext:chdir(rel_path, fn) local old_cwd = self.cwd:get() self.cwd:set(path.concat { old_cwd, rel_path }) if fn then local ok, result = pcall(fn) self.cwd:set(old_cwd) if not ok then error(result, 0) end return result end end ---@async ---@param fn fun(resolve: fun(result: any), reject: fun(error: any)) function InstallContext:await(fn) return a.wait(fn) end ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param script_rel_path string Relative path to the Node.js script. function InstallContext:write_node_exec_wrapper(new_executable_rel_path, script_rel_path) if not self.fs:file_exists(script_rel_path) then error(("Cannot write Node exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0) end return self:write_shell_exec_wrapper( new_executable_rel_path, ("node %q"):format(path.concat { self:get_install_path(), script_rel_path, }) ) end ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param script_rel_path string Relative path to the Node.js script. function InstallContext:write_ruby_exec_wrapper(new_executable_rel_path, script_rel_path) if not self.fs:file_exists(script_rel_path) then error(("Cannot write Ruby exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0) end return self:write_shell_exec_wrapper( new_executable_rel_path, ("ruby %q"):format(path.concat { self:get_install_path(), script_rel_path, }) ) end ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param script_rel_path string Relative path to the PHP script. function InstallContext:write_php_exec_wrapper(new_executable_rel_path, script_rel_path) if not self.fs:file_exists(script_rel_path) then error(("Cannot write PHP exec wrapper for path %q as it doesn't exist."):format(script_rel_path), 0) end return self:write_shell_exec_wrapper( new_executable_rel_path, ("php %q"):format(path.concat { self:get_install_path(), script_rel_path, }) ) end ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param module string The python module to call. function InstallContext:write_pyvenv_exec_wrapper(new_executable_rel_path, module) local pypi = require "mason-core.installer.managers.pypi" local module_exists, module_err = pcall(function() local result = self.spawn.python { "-c", ("import %s"):format(module), with_paths = { pypi.venv_path(self.cwd:get()) } } if not self.spawn.strict_mode then result:get_or_throw() end end) if not module_exists then log.fmt_error("Failed to find module %q for package %q. %s", module, self.package, module_err) error(("Cannot write Python exec wrapper for module %q as it doesn't exist."):format(module), 0) end return self:write_shell_exec_wrapper( new_executable_rel_path, ("%q -m %s"):format( path.concat { pypi.venv_path(self:get_install_path()), "python", }, module ) ) end ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param target_executable_rel_path string function InstallContext:write_exec_wrapper(new_executable_rel_path, target_executable_rel_path) if not self.fs:file_exists(target_executable_rel_path) then error(("Cannot write exec wrapper for path %q as it doesn't exist."):format(target_executable_rel_path), 0) end if platform.is.unix then self.fs:chmod_exec(target_executable_rel_path) end return self:write_shell_exec_wrapper( new_executable_rel_path, ("%q"):format(path.concat { self:get_install_path(), target_executable_rel_path, }) ) end local BASH_TEMPLATE = _.dedent [[ #!/usr/bin/env bash %s exec %s "$@" ]] local BATCH_TEMPLATE = _.dedent [[ @ECHO off %s %s %%* ]] ---@param new_executable_rel_path string Relative path to the executable file to create. ---@param command string The shell command to run. ---@param env table? ---@return string # The created executable filename. function InstallContext:write_shell_exec_wrapper(new_executable_rel_path, command, env) if self.fs:file_exists(new_executable_rel_path) or self.fs:dir_exists(new_executable_rel_path) then error(("Cannot write exec wrapper to %q because the file already exists."):format(new_executable_rel_path), 0) end return platform.when { unix = function() local formatted_envs = _.map(function(pair) local var, value = pair[1], pair[2] return ("export %s=%q"):format(var, value) end, _.to_pairs(env or {})) self.fs:write_file(new_executable_rel_path, BASH_TEMPLATE:format(_.join("\n", formatted_envs), command)) self.fs:chmod_exec(new_executable_rel_path) return new_executable_rel_path end, win = function() local executable_file = ("%s.cmd"):format(new_executable_rel_path) local formatted_envs = _.map(function(pair) local var, value = pair[1], pair[2] return ("SET %s=%s"):format(var, value) end, _.to_pairs(env or {})) self.fs:write_file(executable_file, BATCH_TEMPLATE:format(_.join("\n", formatted_envs), command)) return executable_file end, } end ---@param executable string ---@param rel_path string function InstallContext:link_bin(executable, rel_path) self.links.bin[executable] = rel_path return self end InstallContext.CONTEXT_REQUEST = {} ---@generic T ---@param fn fun(context: InstallContext): T ---@return T function InstallContext:execute(fn) local thread = coroutine.create(function(...) -- We wrap the function to allow it to be a spy instance (in which case it's not actually a function, but a -- callable metatable - coroutine.create strictly expects functions only) return fn(...) end) local step local ret_val step = function(...) local ok, result = coroutine.resume(thread, ...) if not ok then error(result, 0) elseif result == InstallContext.CONTEXT_REQUEST then step(self) elseif coroutine.status(thread) == "suspended" then -- yield to parent coroutine step(coroutine.yield(result)) else ret_val = result end end step(self) return ret_val end ---@async function InstallContext:build_receipt() log.fmt_debug("Building receipt for %s", self.package) return Result.pcall(function() return self.receipt:with_name(self.package.name):with_completion_time(vim.loop.gettimeofday()):build() end) end function InstallContext:get_install_path() return self.location:package(self.package.name) end return InstallContext ================================================ FILE: lua/mason-core/installer/init.lua ================================================ local InstallContext = require "mason-core.installer.context" local M = {} ---@return InstallContext function M.context() return coroutine.yield(InstallContext.CONTEXT_REQUEST) end return M ================================================ FILE: lua/mason-core/installer/linker.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@alias LinkContext { type: '"bin"' | '"opt"' | '"share"', prefix: fun(path: string, location: InstallLocation): string } ---@type table<'"BIN"' | '"OPT"' | '"SHARE"', LinkContext> local LinkContext = { BIN = { type = "bin", ---@param path string ---@param location InstallLocation prefix = function(path, location) return location:bin(path) end, }, OPT = { type = "opt", ---@param path string ---@param location InstallLocation prefix = function(path, location) return location:opt(path) end, }, SHARE = { type = "share", ---@param path string ---@param location InstallLocation prefix = function(path, location) return location:share(path) end, }, } ---@param receipt InstallReceipt ---@param link_context LinkContext ---@param location InstallLocation local function unlink(receipt, link_context, location) return Result.pcall(function() local links = receipt:get_links()[link_context.type] if not links then return end for linked_file in pairs(links) do if receipt:get_schema_version() == "1.0" and link_context == LinkContext.BIN and platform.is.win then linked_file = linked_file .. ".cmd" end local share_path = link_context.prefix(linked_file, location) fs.sync.unlink(share_path) end end) end ---@param pkg AbstractPackage ---@param receipt InstallReceipt ---@param location InstallLocation ---@nodiscard function M.unlink(pkg, receipt, location) log.fmt_debug("Unlinking %s", pkg, receipt:get_links()) return Result.try(function(try) try(unlink(receipt, LinkContext.BIN, location)) try(unlink(receipt, LinkContext.SHARE, location)) try(unlink(receipt, LinkContext.OPT, location)) end) end ---@async ---@param context InstallContext ---@param link_context LinkContext ---@param link_fn async fun(new_abs_path: string, target_abs_path: string, target_rel_path: string): Result local function link(context, link_context, link_fn) log.trace("Linking", context.package, link_context.type, context.links[link_context.type]) return Result.try(function(try) for name, rel_path in pairs(context.links[link_context.type]) do if platform.is.win and link_context == LinkContext.BIN then name = ("%s.cmd"):format(name) end local new_abs_path = link_context.prefix(name, context.location) local target_abs_path = path.concat { context:get_install_path(), rel_path } local target_rel_path = path.relative(new_abs_path, target_abs_path) -- 1. Ensure destination directory exists a.scheduler() local dir = vim.fn.fnamemodify(new_abs_path, ":h") if not fs.async.dir_exists(dir) then try(Result.pcall(fs.async.mkdirp, dir)) end -- 2. Ensure source file exists and target doesn't yet exist OR if --force unlink target if it already -- exists. if context.opts.force then if fs.async.file_exists(new_abs_path) then try(Result.pcall(fs.async.unlink, new_abs_path)) end elseif fs.async.file_exists(new_abs_path) then return Result.failure(("%q is already linked."):format(new_abs_path, name)) end if not fs.async.file_exists(target_abs_path) then return Result.failure(("Link target %q does not exist."):format(target_abs_path)) end -- 3. Execute link. try(link_fn(new_abs_path, target_abs_path, target_rel_path)) context.receipt:with_link(link_context.type, name, rel_path) end end) end ---@param context InstallContext ---@param link_context LinkContext local function symlink(context, link_context) return link(context, link_context, function(new_abs_path, _, target_rel_path) return Result.pcall(fs.async.symlink, target_rel_path, new_abs_path) end) end ---@param context InstallContext ---@param link_context LinkContext local function copyfile(context, link_context) return link(context, link_context, function(new_abs_path, target_abs_path) return Result.pcall(fs.async.copy_file, target_abs_path, new_abs_path, { excl = true }) end) end ---@param context InstallContext local function win_bin_wrapper(context) return link(context, LinkContext.BIN, function(new_abs_path, __, target_rel_path) local windows_target_rel_path = target_rel_path:gsub("/", "\\") return Result.pcall( fs.async.write_file, new_abs_path, _.dedent(([[ @ECHO off GOTO start :find_dp0 SET dp0=%%~dp0 EXIT /b :start SETLOCAL CALL :find_dp0 endLocal & goto #_undefined_# 2>NUL || title %%COMSPEC%% & "%%dp0%%\%s" %%* ]]):format(windows_target_rel_path)) ) end) end ---@async ---@param context InstallContext ---@nodiscard function M.link(context) log.fmt_debug("Linking %s", context.package) return Result.try(function(try) if platform.is.win then try(win_bin_wrapper(context)) try(copyfile(context, LinkContext.SHARE)) try(copyfile(context, LinkContext.OPT)) else try(symlink(context, LinkContext.BIN)) try(symlink(context, LinkContext.SHARE)) try(symlink(context, LinkContext.OPT)) end end) end return M ================================================ FILE: lua/mason-core/installer/managers/cargo.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@async ---@param crate string ---@param version string ---@param opts? { features?: string, locked?: boolean, git?: { url: string, rev?: boolean } } function M.install(crate, version, opts) opts = opts or {} log.fmt_debug("cargo: install %s %s %s", crate, version, opts) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing crate %s@%s…\n"):format(crate, version)) return ctx.spawn.cargo { "install", "--root", ".", opts.git and { "--git", opts.git.url, opts.git.rev and "--rev" or "--tag", version, } or { "--version", version }, opts.features and { "--features", opts.features } or vim.NIL, opts.locked and "--locked" or vim.NIL, crate, } end ---@param bin string function M.bin_path(bin) return Result.pcall(platform.when, { unix = function() return path.concat { "bin", bin } end, win = function() return path.concat { "bin", ("%s.exe"):format(bin) } end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/common.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local async_uv = require "mason-core.async.uv" local installer = require "mason-core.installer" local log = require "mason-core.log" local platform = require "mason-core.platform" local powershell = require "mason-core.installer.managers.powershell" local std = require "mason-core.installer.managers.std" local M = {} ---@class DownloadItem ---@field download_url string ---@field out_file string ---@class FileDownloadSpec ---@field file string | string[] local get_source_file = _.compose(_.head, _.split ":") local get_outfile = _.compose(_.last, _.split ":") ---Normalizes file paths from e.g. "file:out-dir/" to "out-dir/file". ---@param file string local function normalize_file_path(file) local source_file = get_source_file(file) local new_path = get_outfile(file) -- a dir expression (e.g. "libexec/") if _.matches("/$", new_path) then return new_path .. source_file end return new_path end ---@generic T : FileDownloadSpec ---@type fun(download: T): T M.normalize_files = _.evolve { file = _.cond { { _.is "string", normalize_file_path }, { _.T, _.map(normalize_file_path) }, }, } ---@param download FileDownloadSpec ---@param url_generator fun(file: string): string ---@return DownloadItem[] function M.parse_downloads(download, url_generator) local files = download.file if type(files) == "string" then files = { files } end return _.map(function(file) local source_file = get_source_file(file) local out_file = normalize_file_path(file) return { download_url = url_generator(source_file), out_file = out_file, } end, files) end ---@async ---@param ctx InstallContext ---@param downloads DownloadItem[] ---@nodiscard function M.download_files(ctx, downloads) return Result.try(function(try) for __, download in ipairs(downloads) do a.scheduler() local out_dir = vim.fn.fnamemodify(download.out_file, ":h") local out_file = vim.fn.fnamemodify(download.out_file, ":t") if out_dir ~= "." then try(Result.pcall(function() ctx.fs:mkdirp(out_dir) end)) end try(ctx:chdir(out_dir, function() return Result.try(function(try) try(std.download_file(download.download_url, out_file)) try(std.unpack(out_file)) end) end)) end end) end ---@class BuildInstruction ---@field target? Platform | Platform[] ---@field run string ---@field staged? boolean ---@field env? table ---@async ---@param build BuildInstruction ---@return Result ---@nodiscard function M.run_build_instruction(build) log.fmt_debug("build: run %s", build) local ctx = installer.context() if build.staged == false then ctx:promote_cwd() end return platform.when { unix = function() return ctx.spawn.bash { on_spawn = a.scope(function(_, stdio) local stdin = stdio[1] async_uv.write(stdin, "set -euxo pipefail;\n") async_uv.write(stdin, build.run) async_uv.shutdown(stdin) async_uv.close(stdin) end), env = build.env, } end, win = function() return powershell.command(build.run, { env = build.env, }, ctx.spawn) end, } end return M ================================================ FILE: lua/mason-core/installer/managers/composer.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@async ---@param package string ---@param version string ---@nodiscard function M.install(package, version) log.fmt_debug("composer: install %s %s", package, version) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing composer package %s@%s…\n"):format(package, version)) return Result.try(function(try) try(ctx.spawn.composer { "init", "--no-interaction", "--stability=stable", }) try(ctx.spawn.composer { "require", ("%s:%s"):format(package, version), }) end) end ---@param executable string function M.bin_path(executable) return Result.pcall(platform.when, { unix = function() return path.concat { "vendor", "bin", executable } end, win = function() return path.concat { "vendor", "bin", ("%s.bat"):format(executable) } end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/gem.lua ================================================ local Result = require "mason-core.result" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@async ---@param pkg string ---@param version string ---@param opts? { extra_packages?: string[] } ---@nodiscard function M.install(pkg, version, opts) opts = opts or {} log.fmt_debug("gem: install %s %s %s", pkg, version, opts) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing gem %s@%s…\n"):format(pkg, version)) return ctx.spawn.gem { "install", "--no-user-install", "--no-format-executable", "--install-dir=.", "--bindir=bin", "--no-document", ("%s:%s"):format(pkg, version), opts.extra_packages or vim.NIL, env = { GEM_HOME = ctx.cwd:get(), }, } end ---@async ---@param bin string ---@nodiscard function M.create_bin_wrapper(bin) local ctx = installer.context() local bin_path = platform.when { unix = function() return path.concat { "bin", bin } end, win = function() return path.concat { "bin", ("%s.bat"):format(bin) } end, } if not ctx.fs:file_exists(bin_path) then return Result.failure(("Cannot link Gem executable %q because it doesn't exist."):format(bin)) end return Result.pcall(ctx.write_shell_exec_wrapper, ctx, bin, path.concat { ctx:get_install_path(), bin_path }, { GEM_PATH = platform.when { unix = function() return ("%s:$GEM_PATH"):format(ctx:get_install_path()) end, win = function() return ("%s;%%GEM_PATH%%"):format(ctx:get_install_path()) end, }, }) end return M ================================================ FILE: lua/mason-core/installer/managers/golang.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local platform = require "mason-core.platform" local M = {} ---@async ---@param pkg string ---@param version string ---@param opts? { extra_packages?: string[] } function M.install(pkg, version, opts) return Result.try(function(try) opts = opts or {} log.fmt_debug("golang: install %s %s %s", pkg, version, opts) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing go package %s@%s…\n"):format(pkg, version)) local env = { GOBIN = ctx.cwd:get(), } try(ctx.spawn.go { "install", "-v", ("%s@%s"):format(pkg, version), env = env, }) if opts.extra_packages then for _, pkg in ipairs(opts.extra_packages) do try(ctx.spawn.go { "install", "-v", ("%s@latest"):format(pkg), env = env, }) end end end) end ---@param bin string function M.bin_path(bin) return Result.pcall(platform.when, { unix = function() return bin end, win = function() return ("%s.exe"):format(bin) end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/luarocks.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@async ---@param pkg string ---@param version string ---@param opts { server?: string, dev?: boolean } function M.install(pkg, version, opts) opts = opts or {} log.fmt_debug("luarocks: install %s %s %s", pkg, version, opts) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing luarocks package %s@%s…\n"):format(pkg, version)) ctx:promote_cwd() -- luarocks encodes absolute paths during installation return ctx.spawn.luarocks { "install", { "--tree", ctx.cwd:get() }, opts.dev and "--dev" or vim.NIL, opts.server and ("--server=%s"):format(opts.server) or vim.NIL, { pkg, version }, } end ---@param exec string function M.bin_path(exec) return Result.pcall(platform.when, { unix = function() return path.concat { "bin", exec } end, win = function() return path.concat { "bin", ("%s.bat"):format(exec) } end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/npm.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local semver = require "mason-core.semver" local spawn = require "mason-core.spawn" local M = {} ---@async ---@param predicate fun(npm_version: Semver): boolean ---@return boolean local function npm_version_satisfies(predicate) return Result.try(function(try) local npm_versions = try(spawn.npm { "version", "--json" }).stdout ---@type { npm: string } local versions = try(Result.pcall(vim.json.decode, npm_versions)) ---@type Semver local npm_version = try(semver.parse(versions.npm)) return predicate(npm_version) end):get_or_else(false) end ---@async function M.init() log.debug "npm: init" local ctx = installer.context() return Result.try(function(try) try(ctx.spawn.npm { "init", "--yes", "--scope=mason", }) -- Use shallow install-strategy. The reasons for this are: -- a) To avoid polluting the executables (aka bin-links) that npm creates. -- b) The installation is, after all, more similar to a "global" installation. We don't really gain -- any of the benefits of not using global style (e.g., deduping the dependency tree). -- -- We write to .npmrc manually instead of going through npm because managing a local .npmrc file -- is a bit unreliable across npm versions (especially <7), so we take extra measures to avoid -- inadvertently polluting global npm config. try(Result.pcall(function() if npm_version_satisfies(_.gte(semver.new "9.0.0")) then ctx.fs:append_file(".npmrc", "\ninstall-strategy=shallow") else ctx.fs:append_file(".npmrc", "\nglobal-style=true") end end)) ctx.stdio_sink:stdout "Initialized npm root.\n" end) end ---@async ---@param pkg string ---@param version string ---@param opts? { extra_packages?: string[] } function M.install(pkg, version, opts) opts = opts or {} log.fmt_debug("npm: install %s %s %s", pkg, version, opts) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing npm package %s@%s…\n"):format(pkg, version)) return ctx.spawn.npm { "install", ("%s@%s"):format(pkg, version), opts.extra_packages or vim.NIL, } end ---@async ---@param pkg string function M.uninstall(pkg) local ctx = installer.context() ctx.stdio_sink:stdout(("Uninstalling npm package %s…\n"):format(pkg)) return ctx.spawn.npm { "uninstall", pkg } end ---@param exec string function M.bin_path(exec) return Result.pcall(platform.when, { unix = function() return path.concat { "node_modules", ".bin", exec } end, win = function() return path.concat { "node_modules", ".bin", ("%s.cmd"):format(exec) } end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/nuget.lua ================================================ local Result = require "mason-core.result" local installer = require "mason-core.installer" local log = require "mason-core.log" local platform = require "mason-core.platform" local M = {} ---@async ---@param package string ---@param version string ---@nodiscard function M.install(package, version) log.fmt_debug("nuget: install %s %s", package, version) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing nuget package %s@%s…\n"):format(package, version)) return ctx.spawn.dotnet { "tool", "update", "--tool-path", ".", { "--version", version }, package, } end ---@param bin string function M.bin_path(bin) return Result.pcall(platform.when, { unix = function() return bin end, win = function() return ("%s.exe"):format(bin) end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/opam.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local M = {} ---@async ---@param package string ---@param version string ---@nodiscard function M.install(package, version) log.fmt_debug("opam: install %s %s", package, version) local ctx = installer.context() ctx.stdio_sink:stdout(("Installing opam package %s@%s…\n"):format(package, version)) return ctx.spawn.opam { "install", "--destdir=.", "--yes", "--verbose", ("%s.%s"):format(package, version), } end ---@param bin string function M.bin_path(bin) return Result.pcall(platform.when, { unix = function() return path.concat { "bin", bin } end, win = function() return path.concat { "bin", ("%s.exe"):format(bin) } end, }) end return M ================================================ FILE: lua/mason-core/installer/managers/powershell.lua ================================================ local _ = require "mason-core.functional" local a = require "mason-core.async" local process = require "mason-core.process" local spawn = require "mason-core.spawn" local M = {} local PWSHOPT = { progress_preference = [[ $ProgressPreference = 'SilentlyContinue'; ]], -- https://stackoverflow.com/a/63301751 security_protocol = [[ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ]], error_action_preference = [[ $ErrorActionPreference = "Stop"; ]], } local powershell = _.lazy(function() a.scheduler() if vim.fn.executable "pwsh" == 1 then return "pwsh" else return "powershell" end end) ---@async ---@param command string ---@param opts SpawnArgs? ---@param custom_spawn JobSpawn? function M.command(command, opts, custom_spawn) opts = opts or {} ---@type JobSpawn local spawner = custom_spawn or spawn return spawner[powershell()](vim.tbl_extend("keep", { "-NoProfile", "-NonInteractive", "-Command", PWSHOPT.error_action_preference .. PWSHOPT.progress_preference .. PWSHOPT.security_protocol .. command, env_raw = process.graft_env(opts.env or {}, { "PSMODULEPATH" }), on_spawn = function(_, stdio) local stdin = stdio[1] stdin:close() end, }, opts)) end return M ================================================ FILE: lua/mason-core/installer/managers/pypi.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local pep440 = require "mason-core.pep440" local platform = require "mason-core.platform" local providers = require "mason-core.providers" local semver = require "mason-core.semver" local spawn = require "mason-core.spawn" local M = {} local VENV_DIR = "venv" function M.venv_path(dir) return path.concat { dir, VENV_DIR, platform.is.win and "Scripts" or "bin", } end ---@async ---@param candidates string[] local function resolve_python3(candidates) local is_executable = _.compose(_.equals(1), vim.fn.executable) a.scheduler() local available_candidates = _.filter(is_executable, candidates) for __, candidate in ipairs(available_candidates) do ---@type string local version_output = spawn[candidate]({ "--version" }):map(_.prop "stdout"):get_or_else "" local ok, version = pcall(semver.new, version_output:match "Python (3%.%d+.%d+)") if ok then return { executable = candidate, version = version } end end return nil end ---@param version string ---@param specifiers string local function pep440_check_version(version, specifiers) -- The version check only implements a subset of the PEP440 specification and may error with certain inputs. local ok, result = pcall(pep440.check_version, version, specifiers) if not ok then log.fmt_warn( "Failed to check PEP440 version compatibility for version %s with specifiers %s: %s", version, specifiers, result ) return false end return result end ---@param supported_python_versions string local function get_versioned_candidates(supported_python_versions) return _.filter_map(function(pair) local version, executable = unpack(pair) if not pep440_check_version(tostring(version), supported_python_versions) then return Optional.empty() end return Optional.of(executable) end, { { semver.new "3.12.0", "python3.12" }, { semver.new "3.11.0", "python3.11" }, { semver.new "3.10.0", "python3.10" }, { semver.new "3.9.0", "python3.9" }, { semver.new "3.8.0", "python3.8" }, { semver.new "3.7.0", "python3.7" }, { semver.new "3.6.0", "python3.6" }, }) end ---@async ---@param pkg { name: string, version: string } local function create_venv(pkg) local ctx = installer.context() ---@type string? local supported_python_versions = providers.pypi.get_supported_python_versions(pkg.name, pkg.version):get_or_nil() -- 1. Resolve stock python3 installation. local stock_candidates = platform.is.win and { "python", "python3" } or { "python3", "python" } local stock_target = resolve_python3(stock_candidates) if stock_target then log.fmt_debug("Resolved stock python3 installation version %s", stock_target.version) end -- 2. Resolve suitable versioned python3 installation (python3.12, python3.11, etc.). local versioned_candidates = {} if supported_python_versions ~= nil then if stock_target and not pep440_check_version(tostring(stock_target.version), supported_python_versions) then log.fmt_debug("Finding versioned candidates for %s", supported_python_versions) versioned_candidates = get_versioned_candidates(supported_python_versions) end end local target = resolve_python3(versioned_candidates) or stock_target if not target then return Result.failure( ("Unable to find python3 installation in PATH. Tried the following candidates: %s."):format( _.join(", ", _.concat(stock_candidates, versioned_candidates)) ) ) end -- 3. If a versioned python3 installation was not found, warn the user if the stock python3 installation is outside -- the supported version range. if target == stock_target and supported_python_versions ~= nil and not pep440_check_version(tostring(target.version), supported_python_versions) then if ctx.opts.force then ctx.stdio_sink:stderr( ("Warning: The resolved python3 version %s is not compatible with the required Python versions: %s.\n"):format( target.version, supported_python_versions ) ) else ctx.stdio_sink:stderr "Run with :MasonInstall --force to bypass this version validation.\n" return Result.failure( ("Failed to find a python3 installation in PATH that meets the required versions (%s). Found version: %s."):format( supported_python_versions, target.version ) ) end end log.fmt_debug("Found python3 installation version=%s, executable=%s", target.version, target.executable) ctx.stdio_sink:stdout "Creating virtual environment…\n" return ctx.spawn[target.executable] { "-m", "venv", "--system-site-packages", VENV_DIR } end ---@param ctx InstallContext ---@param executable string local function find_venv_executable(ctx, executable) local candidates = _.filter(_.identity, { platform.is.unix and path.concat { VENV_DIR, "bin", executable }, -- MSYS2 platform.is.win and path.concat { VENV_DIR, "bin", ("%s.exe"):format(executable) }, -- Stock Windows platform.is.win and path.concat { VENV_DIR, "Scripts", ("%s.exe"):format(executable) }, }) for _, candidate in ipairs(candidates) do if ctx.fs:file_exists(candidate) then return Result.success(candidate) end end return Result.failure(("Failed to find executable %q in Python virtual environment."):format(executable)) end ---@async ---@param args SpawnArgs local function venv_python(args) local ctx = installer.context() return find_venv_executable(ctx, "python"):and_then(function(python_path) return ctx.spawn[path.concat { ctx.cwd:get(), python_path }](args) end) end ---@async ---@param pkgs string[] ---@param extra_args? string[] local function pip_install(pkgs, extra_args) return venv_python { "-m", "pip", "--disable-pip-version-check", "install", "--no-user", "--ignore-installed", extra_args or vim.NIL, pkgs, } end ---@async ---@param opts { package: { name: string, version: string }, upgrade_pip: boolean, install_extra_args?: string[] } function M.init(opts) return Result.try(function(try) log.fmt_debug("pypi: init", opts) local ctx = installer.context() -- pip3 will hardcode the full path to venv executables, so we need to promote cwd to make sure pip uses the final destination path. ctx:promote_cwd() try(create_venv(opts.package)) if opts.upgrade_pip then ctx.stdio_sink:stdout "Upgrading pip inside the virtual environment…\n" try(pip_install({ "pip" }, opts.install_extra_args)) end end) end ---@async ---@param pkg string ---@param version string ---@param opts? { extra?: string, extra_packages?: string[], install_extra_args?: string[] } function M.install(pkg, version, opts) opts = opts or {} log.fmt_debug("pypi: install %s %s %s", pkg, version, opts or "") local ctx = installer.context() ctx.stdio_sink:stdout(("Installing pip package %s@%s…\n"):format(pkg, version)) return pip_install({ opts.extra and ("%s[%s]==%s"):format(pkg, opts.extra, version) or ("%s==%s"):format(pkg, version), opts.extra_packages or vim.NIL, }, opts.install_extra_args) end ---@async ---@param pkg string function M.uninstall(pkg) log.fmt_debug("pypi: uninstall %s", pkg) return venv_python { "-m", "pip", "uninstall", "-y", pkg, } end ---@param executable string function M.bin_path(executable) local ctx = installer.context() return find_venv_executable(ctx, executable) end return M ================================================ FILE: lua/mason-core/installer/managers/std.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local fetch = require "mason-core.fetch" local installer = require "mason-core.installer" local log = require "mason-core.log" local path = require "mason-core.path" local platform = require "mason-core.platform" local powershell = require "mason-core.installer.managers.powershell" local M = {} ---@async ---@param rel_path string ---@nodiscard local function unpack_7z(rel_path) log.fmt_debug("std: unpack_7z %s", rel_path) local ctx = installer.context() return ctx.spawn["7z"] { "x", "-y", "-r", rel_path } end ---@async ---@param rel_path string ---@nodiscard local function unpack_peazip(rel_path) log.fmt_debug("std: unpack_peazip %s", rel_path) local ctx = installer.context() return ctx.spawn.peazip { "-ext2here", path.concat { ctx.cwd:get(), rel_path } } -- peazip requires absolute paths end ---@async ---@param rel_path string ---@nodiscard local function wzunzip(rel_path) log.fmt_debug("std: wzunzip %s", rel_path) local ctx = installer.context() return ctx.spawn.wzunzip { rel_path } end ---@async ---@param rel_path string ---@nodiscard local function unpack_winrar(rel_path) log.fmt_debug("std: unpack_winrar %s", rel_path) local ctx = installer.context() return ctx.spawn.winrar { "e", rel_path } end ---@async ---@param rel_path string ---@nodiscard local function gunzip_unix(rel_path) log.fmt_debug("std: gunzip_unix %s", rel_path) local ctx = installer.context() return ctx.spawn.gzip { "-d", rel_path } end ---@async ---@param rel_path string ---@nodiscard local function unpack_arc(rel_path) log.fmt_debug("std: unpack_arc %s", rel_path) local ctx = installer.context() return ctx.spawn.arc { "unarchive", rel_path } end ---@param rel_path string ---@return Result local function win_decompress(rel_path) local ctx = installer.context() return gunzip_unix(rel_path) :or_else(function() return unpack_7z(rel_path) end) :or_else(function() return unpack_peazip(rel_path) end) :or_else(function() return wzunzip(rel_path) end) :or_else(function() return unpack_winrar(rel_path) end) :on_success(function() pcall(function() ctx.fs:unlink(rel_path) end) end) end ---@async ---@param url string ---@param out_file string ---@nodiscard function M.download_file(url, out_file) log.fmt_debug("std: downloading file %s", url, out_file) local ctx = installer.context() ctx.stdio_sink:stdout(("Downloading file %q…\n"):format(url)) return fetch(url, { out_file = path.concat { ctx.cwd:get(), out_file }, }):map_err(function(err) return ("%s\nFailed to download file %q."):format(err, url) end) end ---@async ---@param rel_path string ---@nodiscard local function untar(rel_path) log.fmt_debug("std: untar %s", rel_path) local ctx = installer.context() a.scheduler() local tar = vim.fn.executable "gtar" == 1 and "gtar" or "tar" return ctx.spawn[tar]({ "--no-same-owner", "-xvf", rel_path }):on_success(function() pcall(function() ctx.fs:unlink(rel_path) end) end) end ---@async ---@param rel_path string ---@nodiscard local function unzip(rel_path) log.fmt_debug("std: unzip %s", rel_path) local ctx = installer.context() return platform.when { unix = function() return ctx.spawn.unzip({ "-d", ".", rel_path }):on_success(function() pcall(function() ctx.fs:unlink(rel_path) end) end) end, win = function() return Result.pcall(function() -- Expand-Archive seems to be hard-coded to only allow .zip extensions. Bit weird but ok. if not _.matches("%.zip$", rel_path) then local zip_file = ("%s.zip"):format(rel_path) ctx.fs:rename(rel_path, zip_file) return zip_file end return rel_path end):and_then(function(zip_file) return powershell .command( ("Microsoft.PowerShell.Archive\\Expand-Archive -Path %q -DestinationPath ."):format(zip_file), {}, ctx.spawn ) :on_success(function() pcall(function() ctx.fs:unlink(zip_file) end) end) end) end, } end ---@async ---@param rel_path string ---@nodiscard local function gunzip(rel_path) log.fmt_debug("std: gunzip %s", rel_path) return platform.when { unix = function() return gunzip_unix(rel_path) end, win = function() return win_decompress(rel_path) end, } end ---@async ---@param rel_path string ---@return Result ---@nodiscard local function untar_compressed(rel_path) log.fmt_debug("std: untar_compressed %s", rel_path) return platform.when { unix = function() return untar(rel_path) end, win = function() return win_decompress(rel_path) :and_then(function() return untar(_.gsub("%.tar%..*$", ".tar", rel_path)) end) :or_else(function() -- arc both decompresses and unpacks tar in one go return unpack_arc(rel_path) end) end, } end ---@async ---@param rel_path string ---@return Result ---@nodiscard local function untar_zst(rel_path) return platform.when { unix = function() return untar(rel_path) end, win = function() local ctx = installer.context() local uncompressed_tar = rel_path:gsub("%.zst$", "") ctx.spawn.zstd { "-dfo", uncompressed_tar, rel_path } ctx.fs:unlink(rel_path) return untar(uncompressed_tar) end, } end -- Order is important. local unpack_by_filename = _.cond { { _.matches "%.tar$", untar }, { _.matches "%.tar%.gz$", untar }, { _.matches "%.tar%.bz2$", untar }, { _.matches "%.tar%.xz$", untar_compressed }, { _.matches "%.tar%.zst$", untar_zst }, { _.matches "%.zip$", unzip }, { _.matches "%.vsix$", unzip }, { _.matches "%.gz$", gunzip }, { _.T, _.compose(Result.success, _.format "%q doesn't need unpacking.") }, } ---@async ---@param rel_path string The relative path to the file to unpack. ---@nodiscard function M.unpack(rel_path) log.fmt_debug("std: unpack %s", rel_path) local ctx = installer.context() ctx.stdio_sink:stdout((("Unpacking %q…\n"):format(rel_path))) return unpack_by_filename(rel_path) end ---@async ---@param git_url string ---@param opts? { rev?: string, recursive?: boolean } ---@nodiscard function M.clone(git_url, opts) opts = opts or {} log.fmt_debug("std: clone %s %s", git_url, opts) local ctx = installer.context() ctx.stdio_sink:stdout((("Cloning git repository %q…\n"):format(git_url))) return Result.try(function(try) try(ctx.spawn.git { "clone", "--depth", "1", opts.recursive and "--recursive" or vim.NIL, git_url, ".", }) if opts.rev then try(ctx.spawn.git { "fetch", "--depth", "1", "origin", opts.rev }) try(ctx.spawn.git { "checkout", "--quiet", "FETCH_HEAD" }) end end) end return M ================================================ FILE: lua/mason-core/log.lua ================================================ local _ = require "mason-core.functional" local path = require "mason-core.path" local settings = require "mason.settings" local config = { -- Name of the plugin. Prepended to log messages name = "mason", -- Should print the output to neovim while running -- values: 'sync','async',false use_console = vim.env.MASON_VERBOSE_LOGS == "1", -- Should highlighting be used in console (using echohl) highlights = true, -- Should write to a file use_file = true, -- Level configuration modes = { { name = "trace", hl = "Comment", level = vim.log.levels.TRACE }, { name = "debug", hl = "Comment", level = vim.log.levels.DEBUG }, { name = "info", hl = "None", level = vim.log.levels.INFO }, { name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN }, { name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR }, }, -- Can limit the number of decimals displayed for floats float_precision = 0.01, } local log = { outfile = path.concat { (vim.fn.has "nvim-0.8.0" == 1) and vim.fn.stdpath "log" or vim.fn.stdpath "cache", ("%s.log"):format(config.name), }, } -- selene: allow(incorrect_standard_library_use) local unpack = unpack or table.unpack do local round = function(x, increment) increment = increment or 1 x = x / increment return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment end local tbl_has_tostring = function(tbl) local mt = getmetatable(tbl) return mt and mt.__tostring ~= nil end local make_string = function(...) local t = {} for i = 1, select("#", ...) do local x = select(i, ...) if type(x) == "number" and config.float_precision then x = tostring(round(x, config.float_precision)) elseif type(x) == "table" and not tbl_has_tostring(x) then x = vim.inspect(x) else x = tostring(x) end t[#t + 1] = x end return table.concat(t, " ") end local log_at_level = function(level_config, message_maker, ...) -- Return early if we're below the current_log_level if level_config.level < settings.current.log_level then return end local nameupper = level_config.name:upper() local msg = message_maker(...) local info = debug.getinfo(config.info_level or 2, "Sl") local lineinfo = info.short_src .. ":" .. info.currentline -- Output to console if config.use_console then local log_to_console = function() local console_string = string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg) if config.highlights and level_config.hl then vim.cmd(string.format("echohl %s", level_config.hl)) end local split_console = vim.split(console_string, "\n") for _, v in ipairs(split_console) do local formatted_msg = string.format("[%s] %s", config.name, vim.fn.escape(v, [["\]])) local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg)) if not ok then vim.api.nvim_out_write(msg .. "\n") end end if config.highlights and level_config.hl then vim.cmd "echohl NONE" end end if config.use_console == "sync" and not vim.in_fast_event() then log_to_console() else vim.schedule(log_to_console) end end -- Output to log file if config.use_file then local fp = assert(io.open(log.outfile, "a")) local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) fp:write(str) fp:close() end end for __, x in ipairs(config.modes) do -- log.info("these", "are", "separated") log[x.name] = function(...) return log_at_level(x, make_string, ...) end -- log.fmt_info("These are %s strings", "formatted") log[("fmt_%s"):format(x.name)] = function(...) return log_at_level(x, function(...) local passed = { ... } local fmt = table.remove(passed, 1) local inspected = {} for _, v in ipairs(passed) do if type(v) == "table" and tbl_has_tostring(v) then table.insert(inspected, v) else table.insert(inspected, vim.inspect(v)) end end return string.format(fmt, unpack(inspected)) end, ...) end -- log.lazy_info(expensive_to_calculate) log[("lazy_%s"):format(x.name)] = function(f) return log_at_level(x, function() local passed = _.table_pack(f()) local fmt = table.remove(passed, 1) local inspected = {} for _, v in ipairs(passed) do if type(v) == "table" and tbl_has_tostring(v) then table.insert(inspected, v) else table.insert(inspected, vim.inspect(v)) end end return string.format(fmt, unpack(inspected)) end) end -- log.file_info("do not print") log[("file_%s"):format(x.name)] = function(vals, override) local original_console = config.use_console config.use_console = false config.info_level = override.info_level log_at_level(x, make_string, unpack(vals)) config.use_console = original_console config.info_level = nil end end end return log ================================================ FILE: lua/mason-core/notify.lua ================================================ local TITLE = "mason.nvim" return function(msg, level) level = level or vim.log.levels.INFO vim.notify(msg, level, { title = TITLE, }) end ================================================ FILE: lua/mason-core/optional.lua ================================================ ---@class Optional ---@field private _value unknown local Optional = {} Optional.__index = Optional ---@param value any function Optional:new(value) ---@type Optional local instance = {} setmetatable(instance, self) instance._value = value return instance end local EMPTY = Optional:new(nil) ---@param value any function Optional.of_nilable(value) if value == nil then return EMPTY else return Optional:new(value) end end function Optional.empty() return EMPTY end ---@param value any function Optional.of(value) return Optional:new(value) end ---@param mapper_fn fun(value: any): any function Optional:map(mapper_fn) if self:is_present() then return Optional.of_nilable(mapper_fn(self._value)) else return EMPTY end end function Optional:get() if not self:is_present() then error("No value present.", 2) end return self._value end ---@param value any function Optional:or_else(value) if self:is_present() then return self._value else return value end end ---@param supplier fun(): any function Optional:or_else_get(supplier) if self:is_present() then return self._value else return supplier() end end ---@param supplier fun(value: any): Optional ---@return Optional function Optional:and_then(supplier) if self:is_present() then return supplier(self._value) else return self end end ---@param supplier fun(): Optional ---@return Optional function Optional:or_(supplier) if self:is_present() then return self else return supplier() end end ---@param exception any? The exception to throw if the result is a failure. function Optional:or_else_throw(exception) if self:is_present() then return self._value else if exception then error(exception, 2) else error("No value present.", 2) end end end ---@param fn fun(value: any) function Optional:if_present(fn) if self:is_present() then fn(self._value) end return self end ---@param fn fun(value: any) function Optional:if_not_present(fn) if not self:is_present() then fn(self._value) end return self end function Optional:is_present() return self._value ~= nil end ---@param err (fun(): any)|string function Optional:ok_or(err) local Result = require "mason-core.result" if self:is_present() then return Result.success(self:get()) else if type(err) == "string" then return Result.failure(err) else return Result.failure(err()) end end end return Optional ================================================ FILE: lua/mason-core/package/AbstractPackage.lua ================================================ local EventEmitter = require "mason-core.EventEmitter" local InstallLocation = require "mason-core.installer.InstallLocation" local Optional = require "mason-core.optional" local Purl = require "mason-core.purl" local Result = require "mason-core.result" local _ = require "mason-core.functional" local fs = require "mason-core.fs" local log = require "mason-core.log" local settings = require "mason.settings" local Semaphore = require("mason-core.async.control").Semaphore ---@alias PackageInstallOpts { version?: string, debug?: boolean, target?: string, force?: boolean, strict?: boolean, location?: InstallLocation } ---@alias PackageUninstallOpts { bypass_permit?: boolean, location?: InstallLocation } ---@class AbstractPackage : EventEmitter ---@field name string ---@field spec RegistryPackageSpec ---@field registry RegistrySource ---@field private install_handle InstallHandle? The currently associated installation handle. ---@field private uninstall_handle InstallHandle? The currently associated uninstallation handle. local AbstractPackage = {} AbstractPackage.__index = AbstractPackage setmetatable(AbstractPackage, { __index = EventEmitter }) AbstractPackage.SEMAPHORE = Semaphore:new(settings.current.max_concurrent_installers) ---@type PackageInstallOpts AbstractPackage.DEFAULT_INSTALL_OPTS = { debug = false, force = false, strict = false, target = nil, version = nil, } ---@param spec RegistryPackageSpec ---@param reg RegistrySource function AbstractPackage:new(spec, reg) local instance = EventEmitter.new(self) instance.name = spec.name -- for convenient access instance.spec = spec instance.registry = reg return instance end ---@param spec RegistryPackageSpec ---@param reg RegistrySource function AbstractPackage:update(spec, reg) self.name = spec.name -- shouldn't be necessary but might as well self.spec = spec self.registry = reg return self end ---@return boolean function AbstractPackage:is_installing() return self:get_install_handle() :map( ---@param handle InstallHandle function(handle) return not handle:is_closed() end ) :or_else(false) end ---@return boolean function AbstractPackage:is_uninstalling() return self:get_uninstall_handle() :map( ---@param handle InstallHandle function(handle) return not handle:is_closed() end ) :or_else(false) end function AbstractPackage:get_install_handle() return Optional.of_nilable(self.install_handle) end function AbstractPackage:get_uninstall_handle() return Optional.of_nilable(self.uninstall_handle) end ---@param location InstallLocation function AbstractPackage:new_handle(location) assert(location, "Cannot create new handle without a location.") local InstallHandle = require "mason-core.installer.InstallHandle" local handle = InstallHandle:new(self, location) -- Ideally we'd decouple this and leverage Mason's event system, but to allow loading as little as possible during -- setup (i.e. not load modules related to Mason's event system) of the mason.nvim plugin we explicitly call into -- terminator here. require("mason-core.terminator").register(handle) return handle end ---@param location? InstallLocation function AbstractPackage:new_install_handle(location) location = location or InstallLocation.global() log.fmt_trace("Creating new installation handle for %s", self) self:get_install_handle():if_present(function(handle) assert(handle:is_closed(), "Cannot create new install handle because existing handle is not closed.") end) self.install_handle = self:new_handle(location) self:emit("install:handle", self.install_handle) return self.install_handle end ---@param location? InstallLocation function AbstractPackage:new_uninstall_handle(location) location = location or InstallLocation.global() log.fmt_trace("Creating new uninstallation handle for %s", self) self:get_uninstall_handle():if_present(function(handle) assert(handle:is_closed(), "Cannot create new uninstall handle because existing handle is not closed.") end) self.uninstall_handle = self:new_handle(location) self:emit("uninstall:handle", self.uninstall_handle) return self.uninstall_handle end ---@param opts? PackageInstallOpts function AbstractPackage:is_installable(opts) return require("mason-core.installer.compiler").parse(self.spec, opts or {}):is_success() end ---@param location? InstallLocation ---@return Optional # Optional function AbstractPackage:get_receipt(location) location = location or InstallLocation.global() local receipt_path = location:receipt(self.name) if fs.sync.file_exists(receipt_path) then local receipt = require "mason-core.receipt" return Optional.of(receipt.InstallReceipt.from_json(vim.json.decode(fs.sync.read_file(receipt_path)))) end return Optional.empty() end ---@param location? InstallLocation ---@return boolean function AbstractPackage:is_installed(location) error "Unimplemented." end ---@return Result # Result function AbstractPackage:get_all_versions() local compiler = require "mason-core.installer.compiler" return Result.try(function(try) ---@type Purl local purl = try(Purl.parse(self.spec.source.id)) ---@type InstallerCompiler local compiler = try(compiler.get_compiler(purl)) return compiler.get_versions(purl, self.spec.source) end) end ---@return string function AbstractPackage:get_latest_version() return Purl.parse(self.spec.source.id) :map(_.prop "version") :get_or_throw(("Unable to retrieve version from malformed purl: %s."):format(self.spec.source.id)) end ---@param location? InstallLocation ---@return string? function AbstractPackage:get_installed_version(location) return self:get_receipt(location) :map( ---@param receipt InstallReceipt function(receipt) return receipt:get_installed_package_version() end ) :or_else(nil) end ---@param opts? PackageInstallOpts ---@param callback? InstallRunnerCallback ---@return InstallHandle function AbstractPackage:install(opts, callback) error "Unimplemented." end ---@param opts? PackageUninstallOpts ---@param callback? InstallRunnerCallback ---@return InstallHandle function AbstractPackage:uninstall(opts, callback) error "Unimplemented." end ---@private ---@param location? InstallLocation function AbstractPackage:unlink(location) location = location or InstallLocation.global() log.fmt_trace("Unlinking", self, location) local linker = require "mason-core.installer.linker" return self:get_receipt(location):ok_or("Unable to find receipt."):and_then(function(receipt) return linker.unlink(self, receipt, location) end) end ---@async ---@private ---@return Permit function AbstractPackage:acquire_permit() error "Unimplemented." end return AbstractPackage ================================================ FILE: lua/mason-core/package/init.lua ================================================ local AbstractPackage = require "mason-core.package.AbstractPackage" local InstallLocation = require "mason-core.installer.InstallLocation" local InstallRunner = require "mason-core.installer.InstallRunner" local Optional = require "mason-core.optional" local Result = require "mason-core.result" local UninstallRunner = require "mason-core.installer.UninstallRunner" local _ = require "mason-core.functional" local fs = require "mason-core.fs" local path = require "mason-core.path" local platform = require "mason-core.platform" local registry = require "mason-registry" local Semaphore = require("mason-core.async.control").Semaphore ---@class Package : AbstractPackage ---@field spec RegistryPackageSpec ---@field local_semaphore Semaphore local Package = {} Package.__index = Package setmetatable(Package, { __index = AbstractPackage }) ---@param package_identifier string ---@return string, string? Package.Parse = function(package_identifier) local name, version = unpack(vim.split(package_identifier, "@")) return name, version end ---@alias PackageLanguage string ---@type table Package.Lang = setmetatable({}, { __index = function(s, lang) s[lang] = lang return s[lang] end, }) ---@enum PackageCategory Package.Cat = { Compiler = "Compiler", Runtime = "Runtime", DAP = "DAP", LSP = "LSP", Linter = "Linter", Formatter = "Formatter", } ---@alias PackageLicense string ---@type table Package.License = setmetatable({}, { __index = function(s, license) s[license] = license return s[license] end, }) ---@class RegistryPackageSourceVersionOverride : RegistryPackageSource ---@field constraint string ---@class RegistryPackageSource ---@field id string PURL-compliant identifier. ---@field supported_platforms? Platform[] ---@field version_overrides? RegistryPackageSourceVersionOverride[] ---@class RegistryPackageSchemas ---@field lsp string? ---@class RegistryPackageDeprecation ---@field since string ---@field message string ---@alias RegistryPackageSpecSchema --- | '"registry+v1"' ---@class RegistryPackageSpec ---@field schema RegistryPackageSpecSchema ---@field name string ---@field description string ---@field homepage string ---@field licenses string[] ---@field languages string[] ---@field categories string[] ---@field deprecation RegistryPackageDeprecation? ---@field source RegistryPackageSource ---@field schemas RegistryPackageSchemas? ---@field bin table? ---@field share table? ---@field opt table? ---@param spec RegistryPackageSpec local function validate_spec(spec) if platform.cached_features["nvim-0.11"] ~= 1 then return end vim.validate("schema", spec.schema, _.equals "registry+v1", "registry+v1") vim.validate("name", spec.name, "string") vim.validate("description", spec.description, "string") vim.validate("homepage", spec.homepage, "string") vim.validate("licenses", spec.licenses, "table") vim.validate("categories", spec.categories, "table") vim.validate("languages", spec.languages, "table") vim.validate("source", spec.source, "table") vim.validate("bin", spec.bin, { "table", "nil" }) vim.validate("share", spec.share, { "table", "nil" }) end ---@param spec RegistryPackageSpec ---@param reg RegistrySource function Package:new(spec, reg) validate_spec(spec) ---@type Package local instance = AbstractPackage.new(self, spec, reg) instance.local_semaphore = Semaphore:new(1) return instance end ---@param opts? PackageInstallOpts ---@param callback? InstallRunnerCallback ---@return InstallHandle function Package:install(opts, callback) opts = opts or {} assert(not self:is_installing(), "Package is already installing.") assert(not self:is_uninstalling(), "Package is uninstalling.") opts = vim.tbl_extend("force", self.DEFAULT_INSTALL_OPTS, opts or {}) local handle = self:new_install_handle(opts.location) registry:emit("package:install:handle", handle) local runner = InstallRunner:new(handle, AbstractPackage.SEMAPHORE) runner:execute(opts, callback) return handle end ---@param opts? PackageUninstallOpts ---@param callback? fun(success: boolean, error: any) function Package:uninstall(opts, callback) opts = opts or {} assert(self:is_installed(opts.location), "Package is not installed.") assert(not self:is_uninstalling(), "Package is already uninstalling.") local handle = self:new_uninstall_handle(opts.location) registry:emit("package:uninstall:handle", handle) local runner = UninstallRunner:new(handle, AbstractPackage.SEMAPHORE) runner:execute(opts, callback) return handle end ---@param location? InstallLocation function Package:is_installed(location) location = location or InstallLocation.global() local ok, stat = pcall(vim.loop.fs_stat, location:package(self.name)) if not ok or not stat then return false end return stat.type == "directory" end function Package:get_lsp_settings_schema() local schema_file = InstallLocation.global() :share(path.concat { "mason-schemas", "lsp", ("%s.json"):format(self.name) }) if fs.sync.file_exists(schema_file) then return Result.pcall(vim.json.decode, fs.sync.read_file(schema_file), { luanil = { object = true, array = true }, }):ok() end return Optional.empty() end function Package:get_aliases() return require("mason-registry").get_package_aliases(self.name) end ---@async ---@private function Package:acquire_permit() return self.local_semaphore:acquire() end function Package:__tostring() return ("Package(name=%s)"):format(self.name) end return Package ================================================ FILE: lua/mason-core/path.lua ================================================ local M = {} ---@param path_components string[] ---@return string function M.concat(path_components) return vim.fs.normalize(table.concat(path_components, "/")) end ---@path root_path string ---@path path string function M.is_subdirectory(root_path, path) local root_path_normalized = vim.fs.normalize(root_path) local path_normalized = vim.fs.normalize(path) if path_normalized == root_path_normalized then return true end for dir in vim.fs.parents(path_normalized) do if root_path_normalized == dir then return true end end return false end local function find_closest_common_parent(from, to) local distance = 0 for parent in vim.fs.parents(from) do if to:find(parent, 1, true) then return parent, distance else distance = distance + 1 end end return "/", distance end function M.relative(from, to) local from_normalized = vim.fs.normalize(from) local to_normalized = vim.fs.normalize(to) local common_parent, distance = find_closest_common_parent(from_normalized, to_normalized) local relative_path_component = distance == 0 and "." or (".."):rep(distance, "/") return M.concat { relative_path_component, to_normalized:sub(#common_parent + 1) } end return M ================================================ FILE: lua/mason-core/pep440/init.lua ================================================ local function split_version(version) local parts = {} for part in version:gmatch "[^.]+" do table.insert(parts, tonumber(part) or part) end return parts end local function compare_versions(version1, version2) local v1_parts = split_version(version1) local v2_parts = split_version(version2) local len = math.max(#v1_parts, #v2_parts) for i = 1, len do local v1_part = v1_parts[i] or 0 local v2_part = v2_parts[i] or 0 if v1_part < v2_part then return -1 elseif v1_part > v2_part then return 1 end end return 0 end local function check_single_specifier(version, specifier) local operator, spec_version = specifier:match "^([<>=!~]+)%s*(.+)$" local comp_result = compare_versions(version, spec_version) if operator == "==" then return comp_result == 0 elseif operator == "!=" then return comp_result ~= 0 elseif operator == "<=" then return comp_result <= 0 elseif operator == "<" then return comp_result < 0 elseif operator == ">=" then return comp_result >= 0 elseif operator == ">" then return comp_result > 0 elseif operator == "~=" then if comp_result < 0 then return false end local spec_version_components = split_version(spec_version) local version_components = split_version(version) for i = 1, #spec_version_components - 1 do if spec_version_components[i] ~= version_components[i] then return false end end return true else error("Unknown operator in version specifier: " .. operator) end end local function check_version(version, specifiers) for specifier in specifiers:gmatch "[^,]+" do if not check_single_specifier(version, specifier:match "^%s*(.-)%s*$") then return false end end return true end return { check_version = check_version, } ================================================ FILE: lua/mason-core/platform.lua ================================================ local _ = require "mason-core.functional" local M = {} local uname = vim.loop.os_uname() ---@alias Platform ---| '"darwin_arm64"' ---| '"darwin_x64"' ---| '"linux_arm"' ---| '"linux_arm64"' ---| '"linux_arm64_gnu"' ---| '"linux_arm64_openbsd"' ---| '"linux_arm_gnu"' ---| '"linux_x64"' ---| '"linux_x64_gnu"' ---| '"linux_x64_openbsd"' ---| '"linux_x86"' ---| '"linux_x86_gnu"' ---| '"win_arm"' ---| '"win_arm64"' ---| '"win_x64"' ---| '"win_x86"' local arch_aliases = { ["x86_64"] = "x64", ["i386"] = "x86", ["i686"] = "x86", -- x86 compat ["aarch64"] = "arm64", ["aarch64_be"] = "arm64", ["armv8b"] = "arm64", -- arm64 compat ["armv8l"] = "arm64", -- arm64 compat } M.arch = arch_aliases[uname.machine] or uname.machine M.sysname = uname.sysname M.is_headless = #vim.api.nvim_list_uis() == 0 local function system(args) if vim.fn.executable(args[1]) == 1 then local ok, output = pcall(vim.fn.system, args) if ok and (vim.v.shell_error == 0 or vim.v.shell_error == 1) then return true, output end return false, output end return false, args[1] .. " is not executable" end ---@type fun(): ('"glibc"' | '"musl"')? local get_libc = _.lazy(function() local getconf_ok, getconf_output = system { "getconf", "GNU_LIBC_VERSION" } if getconf_ok and getconf_output:find "glibc" then return "glibc" end local ldd_ok, ldd_output = system { "ldd", "--version" } if ldd_ok then if ldd_output:find "musl" then return "musl" elseif ldd_output:find "GLIBC" or ldd_output:find "glibc" or ldd_output:find "GNU" then return "glibc" end end end) -- Most of the code that calls into these functions executes outside of the main event loop, where API/fn functions are -- disabled. We evaluate these immediately here to avoid issues with main loop synchronization. M.cached_features = { ["win"] = vim.fn.has "win32", ["win32"] = vim.fn.has "win32", ["win64"] = vim.fn.has "win64", ["mac"] = vim.fn.has "mac", ["darwin"] = vim.fn.has "mac", ["unix"] = vim.fn.has "unix", ["linux"] = vim.fn.has "linux", ["nvim-0.11"] = vim.fn.has "nvim-0.11", } ---@type fun(env: string): boolean local check_env = _.memoize(_.cond { { _.equals "musl", function() return get_libc() == "musl" end, }, { _.equals "gnu", function() return get_libc() == "glibc" end, }, { _.equals "openbsd", _.always(uname.sysname == "OpenBSD") }, { _.T, _.F }, }) ---Table that allows for checking whether the provided targets apply to the current system. ---Each key is a target tuple consisting of at most 3 targets, in the following order: --- 1) OS (e.g. linux, unix, darwin, win) - Mandatory --- 2) Architecture (e.g. arm64, x64) - Optional --- 3) Environment (e.g. gnu, musl, openbsd) - Optional ---Each target is separated by a "_" character, like so: "linux_x64_musl". ---@type table M.is = setmetatable({}, { __index = function(__, key) local os, arch, env = unpack(vim.split(key, "_", { plain = true })) if not M.cached_features[os] or M.cached_features[os] ~= 1 then return false end if arch and arch ~= M.arch then return false end if env and not check_env(env) then return false end return true end, }) ---@generic T ---@param platform_table table ---@return T local function get_by_platform(platform_table) if M.is.darwin then return platform_table.darwin or platform_table.mac or platform_table.unix elseif M.is.linux then return platform_table.linux or platform_table.unix elseif M.is.unix then return platform_table.unix elseif M.is.win then return platform_table.win else return nil end end function M.when(cases) local case = get_by_platform(cases) if case then return case() else error "Current platform is not supported." end end ---@type async fun(): table M.os_distribution = _.lazy(function() local parse_os_release = _.compose(_.from_pairs, _.map(_.split "="), _.split "\n") ---@param entries table local function parse_ubuntu(entries) -- Parses the Ubuntu OS VERSION_ID into their version components, e.g. "18.04" -> {major=18, minor=04} local version_id = entries.VERSION_ID:gsub([["]], "") local version_parts = vim.split(version_id, "%.") local major = tonumber(version_parts[1]) local minor = tonumber(version_parts[2]) return { id = "ubuntu", version_id = version_id, version = { major = major, minor = minor }, } end ---@param entries table local function parse_centos(entries) -- Parses the CentOS VERSION_ID into a major version (the only thing available). local version_id = entries.VERSION_ID:gsub([["]], "") local major = tonumber(version_id) return { id = "centos", version_id = version_id, version = { major = major }, } end ---Parses the provided contents of an /etc/*-release file and identifies the Linux distribution. local parse_linux_dist = _.cond { { _.prop_eq("ID", "ubuntu"), parse_ubuntu }, { _.prop_eq("ID", [["centos"]]), parse_centos }, { _.T, _.always { id = "linux-generic", version = {} } }, } return M.when { linux = function() local spawn = require "mason-core.spawn" return spawn .bash({ "-c", "cat /etc/*-release" }) :map_catching(_.compose(parse_linux_dist, parse_os_release, _.prop "stdout")) :recover(function() return { id = "linux-generic", version = {} } end) :get_or_throw() end, darwin = function() return { id = "macOS", version = {} } end, win = function() return { id = "windows", version = {} } end, } end) ---@type async fun(): Result M.get_homebrew_prefix = _.lazy(function() assert(M.is.darwin, "Can only locate Homebrew installation on Mac systems.") local spawn = require "mason-core.spawn" return spawn .brew({ "--prefix" }) :map_catching(function(result) return vim.trim(result.stdout) end) :map_err(function() return "Failed to locate Homebrew installation." end) end) ---@async function M.get_node_version() local spawn = require "mason-core.spawn" return spawn.node({ "--version" }):map(function(result) -- Parses output such as "v16.3.1" into major, minor, patch local _, _, major, minor, patch = _.head(_.split("\n", result.stdout)):find "v(%d+)%.(%d+)%.(%d+)" return { major = tonumber(major), minor = tonumber(minor), patch = tonumber(patch) } end) end -- PATH separator M.path_sep = M.is.win and ";" or ":" return M ================================================ FILE: lua/mason-core/process.lua ================================================ local _ = require "mason-core.functional" local log = require "mason-core.log" local platform = require "mason-core.platform" local uv = vim.loop ---@alias luv_pipe any ---@alias luv_handle any ---@class IStdioSink local IStdioSink = {} ---@param chunk string function IStdioSink:stdout(chunk) end ---@param chunk string function IStdioSink:stderr(chunk) end ---@class StdioSink : IStdioSink ---@field stdout_sink? fun(chunk: string) ---@field stderr_sink? fun(chunk: string) local StdioSink = {} StdioSink.__index = StdioSink ---@param opts { stdout?: fun(chunk: string), stderr?: fun(chunk: string) } function StdioSink:new(opts) ---@type StdioSink local instance = {} setmetatable(instance, self) instance.stdout_sink = opts.stdout instance.stderr_sink = opts.stderr return instance end ---@param chunk string function StdioSink:stdout(chunk) if self.stdout_sink then self.stdout_sink(chunk) end end ---@param chunk string function StdioSink:stderr(chunk) if self.stderr_sink then self.stderr_sink(chunk) end end ---@class BufferedSink : IStdioSink ---@field buffers { stdout: string[], stderr: string[] } ---@field events? EventEmitter local BufferedSink = {} BufferedSink.__index = BufferedSink function BufferedSink:new() ---@type BufferedSink local instance = {} setmetatable(instance, self) instance.buffers = { stdout = {}, stderr = {}, } return instance end ---@param events EventEmitter function BufferedSink:connect_events(events) self.events = events end ---@param chunk string function BufferedSink:stdout(chunk) local stdout = self.buffers.stdout stdout[#stdout + 1] = chunk if self.events then self.events:emit("stdout", chunk) end end ---@param chunk string function BufferedSink:stderr(chunk) local stderr = self.buffers.stderr stderr[#stderr + 1] = chunk if self.events then self.events:emit("stderr", chunk) end end local M = {} ---@param pipe luv_pipe ---@param sink fun(chunk: string) local function connect_sink(pipe, sink) ---@param err string | nil ---@param data string | nil return function(err, data) if err then log.error("Unexpected error when reading pipe.", err) end if data ~= nil then sink(data) else pipe:read_stop() pipe:close() end end end -- We gather the root env immediately, primarily because of E5560. -- Also, there's no particular reason we need to refresh the environment (yet). local initial_environ = vim.fn.environ() ---@param new_paths string[] A list of paths to prepend the existing PATH with. function M.extend_path(new_paths) local new_path_str = table.concat(new_paths, platform.path_sep) return ("%s%s%s"):format(new_path_str, platform.path_sep, initial_environ.PATH or "") end ---Merges the provided env param with the user's full environment. Provided env has precedence. ---@param env table ---@param excluded_var_names string[]|nil ---@return string[] function M.graft_env(env, excluded_var_names) local excluded_var_names_set = excluded_var_names and _.set_of(excluded_var_names) or {} local merged_env = {} for key, val in pairs(initial_environ) do if not excluded_var_names_set[key] and env[key] == nil then merged_env[#merged_env + 1] = key .. "=" .. val end end for key, val in pairs(env) do if not excluded_var_names_set[key] then merged_env[#merged_env + 1] = key .. "=" .. val end end return merged_env end ---@param env_list string[] local function sanitize_env_list(env_list) local sanitized_list = {} for __, env in ipairs(env_list) do local safe_envs = { "GO111MODULE", "GOBIN", "GOPATH", "PATH", "GEM_HOME", "GEM_PATH", } local is_safe_env = _.any(function(safe_env) return env:find(safe_env .. "=") == 1 end, safe_envs) if is_safe_env then sanitized_list[#sanitized_list + 1] = env else local idx = env:find "=" sanitized_list[#sanitized_list + 1] = env:sub(1, idx) .. "" end end return sanitized_list end ---@alias JobSpawnCallback fun(success: boolean, exit_code: integer?, signal: integer?) ---@class JobSpawnOpts ---@field env string[]? List of "key=value" string. ---@field args string[] ---@field cwd string ---@field stdio_sink IStdioSink ---@param cmd string The command/executable. ---@param opts JobSpawnOpts ---@param callback JobSpawnCallback ---@return luv_handle?,luv_pipe[]?,integer? # Returns the job handle and the stdio array on success, otherwise returns nil. function M.spawn(cmd, opts, callback) local stdin = uv.new_pipe(false) local stdout = uv.new_pipe(false) local stderr = uv.new_pipe(false) local stdio = { stdin, stdout, stderr } local spawn_opts = { env = opts.env, stdio = stdio, args = opts.args, cwd = opts.cwd, detached = false, hide = true, } log.lazy_debug(function() local sanitized_env = opts.env and sanitize_env_list(opts.env) or nil return "Spawning cmd=%s, spawn_opts=%s", cmd, { args = opts.args, cwd = opts.cwd, env = sanitized_env, } end) local handle, pid_or_err handle, pid_or_err = uv.spawn(cmd, spawn_opts, function(exit_code, signal) local successful = exit_code == 0 and signal == 0 handle:close() if not stdin:is_closing() then stdin:close() end -- ensure all pipes are closed, for I am a qualified plumber local check = uv.new_check() check:start(function() for i = 1, #stdio do local pipe = stdio[i] if not pipe:is_closing() then return end end check:stop() check:close() callback(successful, exit_code, signal) end) log.fmt_debug("Job pid=%s exited with exit_code=%s, signal=%s", pid_or_err, exit_code, signal) end) if handle == nil then log.fmt_error("Failed to spawn process. cmd=%s, err=%s", cmd, pid_or_err) if type(pid_or_err) == "string" and pid_or_err:find "ENOENT" == 1 then opts.stdio_sink:stderr(("Could not find executable %q in PATH.\n"):format(cmd)) else opts.stdio_sink:stderr(("Failed to spawn process cmd=%s err=%s\n"):format(cmd, pid_or_err)) end callback(false) return nil, nil, nil end log.debug("Spawned with pid", pid_or_err) stdout:read_start(connect_sink(stdout, function(...) opts.stdio_sink:stdout(...) end)) stderr:read_start(connect_sink(stderr, function(...) opts.stdio_sink:stderr(...) end)) return handle, stdio, pid_or_err end ---@param luv_handle luv_handle ---@param signal integer function M.kill(luv_handle, signal) assert(type(signal) == "number", "signal is not a number") assert(signal > 0 and signal < 32, "signal must be between 1-31") log.fmt_trace("Sending signal %s to handle %s", signal, luv_handle) local ok, is_active = pcall(uv.is_active, luv_handle) if not ok or not is_active then log.fmt_trace("Tried to send signal %s to inactive uv handle.", signal) return end uv.process_kill(luv_handle, signal) end M.StdioSink = StdioSink M.BufferedSink = BufferedSink return M ================================================ FILE: lua/mason-core/providers.lua ================================================ local Result = require "mason-core.result" local log = require "mason-core.log" local settings = require "mason.settings" ---@alias GitHubRelease { tag_name: string, prerelease: boolean, draft: boolean, assets: table[] } ---@alias GitHubTag { name: string } ---@class GitHubProvider ---@field get_latest_release? async fun(repo: string): Result # Result ---@field get_all_release_versions? async fun(repo: string): Result # Result ---@field get_latest_tag? async fun(repo: string): Result # Result ---@field get_all_tags? async fun(repo: string): Result # Result ---@alias NpmPackage { name: string, version: string } ---@class NpmProvider ---@field get_latest_version? async fun(pkg: string): Result # Result ---@field get_all_versions? async fun(pkg: string): Result # Result ---@alias PyPiPackage { name: string, version: string } ---@class PyPiProvider ---@field get_latest_version? async fun(pkg: string): Result # Result ---@field get_all_versions? async fun(pkg: string): Result # Result # Sorting should not be relied upon due to "proprietary" sorting algo in pip that is difficult to replicate in mason-registry-api. ---@field get_supported_python_versions? async fun(pkg: string, version: string): Result # Result # Returns a version specifier as provided by the PyPI API (see PEP440). ---@alias RubyGem { name: string, version: string } ---@class RubyGemsProvider ---@field get_latest_version? async fun(gem: string): Result # Result ---@field get_all_versions? async fun(gem: string): Result # Result ---@alias PackagistPackage { name: string, version: string } ---@class PackagistProvider ---@field get_latest_version? async fun(pkg: string): Result # Result ---@field get_all_versions? async fun(pkg: string): Result # Result ---@alias Crate { name: string, version: string } ---@class CratesProvider ---@field get_latest_version? async fun(crate: string): Result # Result ---@field get_all_versions? async fun(crate: string): Result # Result ---@class GolangProvider ---@field get_all_versions? async fun(pkg: string): Result # Result ---@class OpenVSXProvider ---@field get_latest_version? async fun(namespace: string, extension: string): Result # Result ---@field get_all_versions? async fun(namespace: string, extension: string): Result # Result ---@class Provider ---@field github? GitHubProvider ---@field npm? NpmProvider ---@field pypi? PyPiProvider ---@field rubygems? RubyGemsProvider ---@field packagist? PackagistProvider ---@field crates? CratesProvider ---@field golang? GolangProvider ---@field openvsx? OpenVSXProvider local function service_mt(service) return setmetatable({}, { __index = function(_, method) return function(...) if #settings.current.providers < 1 then log.error "No providers configured." return Result.failure "1 or more providers are required." end for _, provider_module in ipairs(settings.current.providers) do local ok, provider = pcall(require, provider_module) if ok and provider then local impl = provider[service] and provider[service][method] if impl then ---@type boolean, Result local ok, result = pcall(impl, ...) if ok and result:is_success() then return result else if getmetatable(result) == Result then log.fmt_error("Provider %s %s failed: %s", service, method, result:err_or_nil()) else log.fmt_error("Provider %s %s errored: %s", service, method, result) end end end else log.fmt_error("Unable to find provider %s is not registered. %s", provider_module, provider) end end local err = ("No provider implementation succeeded for %s.%s"):format(service, method) log.error(err) return Result.failure(err) end end, }) end ---@type Provider local providers = setmetatable({}, { __index = function(tbl, service) tbl[service] = service_mt(service) return tbl[service] end, }) return providers ================================================ FILE: lua/mason-core/purl.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local _ = require "mason-core.functional" local M = {} -- Fully spec-compliant parser for purls (https://github.com/package-url/purl-spec) ---@param str string local function parse_hex(str) return tonumber(str, 16) end ---@param char string local function percent_encode(char) return ("%%%x"):format(string.byte(char, 1, 1)) end local decode_percent_encoding = _.gsub("%%([A-Fa-f0-9][A-Fa-f0-9])", _.compose(string.char, parse_hex)) local encode_percent_encoding = _.gsub("[!#$&'%(%)%*%+;=%?@%[%] ]", percent_encode) local function validate_conan(purl) if purl.namespace and not _.path({ "qualifiers", "channel" }, purl) then return Result.failure "Missing channel qualifier." elseif not purl.namespace and _.path({ "qualifiers", "channel" }, purl) then return Result.failure "Missing namespace." end return Result.success(purl) end local function validate_cran(purl) if not purl.version then return Result.failure "Missing version." end return Result.success(purl) end local function validate_swift(purl) if not purl.namespace then return Result.failure "Missing namespace." end if not purl.version then return Result.failure "Missing version." end return Result.success(purl) end ---@class Purl ---@field scheme '"pkg"' ---@field type string ---@field namespace string? ---@field name string ---@field version string? ---@field qualifiers table? ---@field subpath string? ---@param str string local function split_once_right(str, char) for i = #str, 1, -1 do if str:sub(i, i) == char then local segment = str:sub(i + 1, #str) return str:sub(1, i - 1), segment end end return str end ---@param str string local function split_once_left(str, char) for i = 1, #str do if str:sub(i, i) == char then local segment = str:sub(1, i - 1) return segment, str:sub(i + 1) end end return str end local function left_trim(char, str) for i = 1, #str do if str:sub(i, i) ~= char then return i end end return #str + 1 end local function right_trim(char, str) for i = #str, 1, -1 do if str:sub(i, i) ~= char then return i end end return #str + 1 end ---@param char string ---@param str string local function trim(char, str) return str:sub(left_trim(char, str), right_trim(char, str)) end local parse_subpath = _.compose( _.join "/", _.filter_map(function(segment) if segment == "." or segment == ".." or segment == "" then return Optional.empty() end return Optional.of(decode_percent_encoding(segment)) end), _.split "/", _.partial(trim, "/") ) local parse_qualifiers = _.compose( _.evolve { checksum = _.split ",", }, _.from_pairs, _.filter_map(function(pair) local key, value = split_once_left(pair, "=") if value ~= nil and value ~= "" then return Optional.of { _.to_lower(key), decode_percent_encoding(value) } else return Optional.empty() end end), _.split "&" ) local parse_namespace = _.compose( _.join "/", _.filter_map(function(segment) if segment == "" then return Optional.empty() end return Optional.of(decode_percent_encoding(segment)) end), _.split "/" ) local pypi = _.evolve { name = _.compose(_.to_lower, _.gsub("_", "-")), } local huggingface = _.evolve { version = _.to_lower, } local azuredatabricks = _.evolve { name = _.to_lower, namespace = _.to_lower, } local bitbucket = _.evolve { name = _.to_lower, namespace = _.to_lower, } local github = _.evolve { name = _.to_lower, namespace = _.to_lower, } local composer = _.evolve { name = _.to_lower, namespace = _.to_lower, } local is_mlflow_azuredatabricks = _.all_pass { _.prop_eq("type", "mlflow"), _.path_satisfies(_.matches "^https?://.*azuredatabricks%.net", { "qualifiers", "repository_url" }), } local type_validations = _.cond { { _.prop_eq("type", "conan"), validate_conan }, { _.prop_eq("type", "cran"), validate_cran }, { _.prop_eq("type", "swift"), validate_swift }, { _.T, Result.success }, } local type_transforms = _.cond { { _.prop_eq("type", "bitbucket"), bitbucket }, { _.prop_eq("type", "composer"), composer }, { _.prop_eq("type", "github"), github }, { _.prop_eq("type", "pypi"), pypi }, { _.prop_eq("type", "huggingface"), huggingface }, { is_mlflow_azuredatabricks, azuredatabricks }, { _.T, _.identity }, } local type_specific_transforms = _.compose(type_validations, type_transforms) ---@param raw_purl string ---@return Result # Result function M.parse(raw_purl) -- Implementation of recommended parsing algo -- https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#how-to-parse-a-purl-string-in-its-components local remainder, subpath = split_once_right(raw_purl, "#") if subpath then subpath = parse_subpath(subpath) end local remainder, qualifiers = split_once_right(remainder, "?") if qualifiers then qualifiers = parse_qualifiers(qualifiers) if not _.all(_.matches "^[a-zA-Z%-_%.][0-9a-zA-Z%-_%.]*$", _.keys(qualifiers)) then return Result.failure "Malformed purl (invalid qualifier names)." end end local scheme, remainder = split_once_left(remainder, ":") if not remainder then return Result.failure "Malformed purl (missing type, namespace, name, version components)." end if scheme ~= "pkg" then return Result.failure "Malformed purl (invalid scheme)." end remainder = trim("/", remainder) local type, remainder = split_once_left(remainder, "/") if not remainder then return Result.failure "Malformed purl (missing namespace, name, version components)" end type = _.to_lower(type) local remainder, version = split_once_right(remainder, "@") if version then version = decode_percent_encoding(version) end local remainder, name = split_once_right(remainder, "/") if not name then name = remainder remainder = nil end if name == "" then return Result.failure "Malformed purl (missing name)." end name = decode_percent_encoding(name) local namespace = remainder if namespace then namespace = parse_namespace(namespace) end return type_specific_transforms { scheme = scheme, type = type, namespace = namespace, name = name, version = version, qualifiers = qualifiers, subpath = subpath, } end local stringify_qualifiers = _.compose( _.join "&", _.sort_by(_.identity), _.map(_.compose(_.join "=", _.evolve { _.identity, encode_percent_encoding })), _.to_pairs, _.evolve { checksum = _.join ",", } ) ---@param purl Purl ---@return string function M.compile(purl) local str = "pkg:" str = str .. purl.type .. "/" if purl.namespace then str = str .. encode_percent_encoding(purl.namespace) .. "/" end str = str .. purl.name if purl.version then str = str .. "@" .. encode_percent_encoding(purl.version) end if purl.qualifiers then str = str .. "?" .. stringify_qualifiers(purl.qualifiers) end if purl.subpath then str = str .. "#" .. purl.subpath end return str end return M ================================================ FILE: lua/mason-core/receipt.lua ================================================ local Optional = require "mason-core.optional" local Purl = require "mason-core.purl" local _ = require "mason-core.functional" local M = {} ---@alias InstallReceiptSchemaVersion ---| '"1.0"' ---| '"1.1"' ---| '"2.0"' ---@alias InstallReceiptSource {type: RegistryPackageSpecSchema, id: string, raw: RegistryPackageSource} ---@class InstallReceiptLinks ---@field bin? table ---@field share? table ---@field opt? table ---@alias InstallReceiptRegistry { proto: '"github"' | '"lua"' | '"file"' } ---@class InstallReceipt ---@field name string ---@field schema_version InstallReceiptSchemaVersion ---@field metrics {start_time:integer, completion_time:integer} ---@field source InstallReceiptSource ---@field links InstallReceiptLinks ---@field install_options PackageInstallOpts ---@field registry InstallReceiptRegistry local InstallReceipt = {} InstallReceipt.__index = InstallReceipt function InstallReceipt:new(data) return setmetatable(data, self) end function InstallReceipt.from_json(json) return InstallReceipt:new(json) end function InstallReceipt:__tostring() return ("InstallReceipt(name=%s, purl=%s)"):format(self.name, self:get_source().id or "N/A") end function InstallReceipt:get_name() return self.name end ---@return string? function InstallReceipt:get_installed_package_version() local source = self:get_source() if source.id then return Purl.parse(source.id):map(_.prop "version"):get_or_nil() end end function InstallReceipt:get_schema_version() return self.schema_version end ---@param version string function InstallReceipt:is_schema_min(version) local semver = require "mason-vendor.semver" return semver(self.schema_version) >= semver(version) end ---@return InstallReceiptSource function InstallReceipt:get_source() if self:is_schema_min "2.0" then return self.source end return self.primary_source --[[@as InstallReceiptSource]] end ---@return string? function InstallReceipt:get_installed_purl() local source = self:get_source() return source.id end function InstallReceipt:get_raw_source() if self:is_schema_min "2.0" then return self.source.raw else return nil end end function InstallReceipt:get_registry() return self.registry end function InstallReceipt:get_install_options() return self.install_options end function InstallReceipt:get_links() return self.links end function InstallReceipt:to_json() return vim.json.encode(self) end ---@class InstallReceiptBuilder ---@field links InstallReceiptLinks local InstallReceiptBuilder = {} InstallReceiptBuilder.__index = InstallReceiptBuilder function InstallReceiptBuilder:new() ---@type InstallReceiptBuilder local instance = {} setmetatable(instance, self) instance.links = { bin = vim.empty_dict(), share = vim.empty_dict(), opt = vim.empty_dict(), } return instance end ---@param name string function InstallReceiptBuilder:with_name(name) self.name = name return self end ---@param source InstallReceiptSource function InstallReceiptBuilder:with_source(source) self.source = source return self end ---@param install_options PackageInstallOpts function InstallReceiptBuilder:with_install_options(install_options) self.install_options = install_options return self end ---@param typ '"bin"' | '"share"' | '"opt"' ---@param name string ---@param rel_path string function InstallReceiptBuilder:with_link(typ, name, rel_path) assert(not self.links[typ][name], ("%s/%s has already been linked."):format(typ, name)) self.links[typ][name] = rel_path return self end ---@param seconds integer ---@param microseconds integer local function to_ms(seconds, microseconds) return (seconds * 1000) + math.floor(microseconds / 1000) end ---vim.loop.gettimeofday() ---@param seconds integer ---@param microseconds integer function InstallReceiptBuilder:with_completion_time(seconds, microseconds) self.completion_time = to_ms(seconds, microseconds) return self end ---vim.loop.gettimeofday() ---@param seconds integer ---@param microseconds integer function InstallReceiptBuilder:with_start_time(seconds, microseconds) self.start_time = to_ms(seconds, microseconds) return self end ---@param registry InstallReceiptRegistry function InstallReceiptBuilder:with_registry(registry) self.registry = registry return self end function InstallReceiptBuilder:build() assert(self.name, "name is required") assert(self.start_time, "start_time is required") assert(self.completion_time, "completion_time is required") assert(self.source, "source is required") assert(self.install_options, "install_options is required") assert(self.registry, "registry is required") return InstallReceipt:new { name = self.name, schema_version = "2.0", metrics = { start_time = self.start_time, completion_time = self.completion_time, }, install_options = self.install_options, source = self.source, registry = self.registry, links = self.links, } end M.InstallReceiptBuilder = InstallReceiptBuilder M.InstallReceipt = InstallReceipt return M ================================================ FILE: lua/mason-core/result.lua ================================================ ---@class Failure ---@field error any local Failure = {} Failure.__index = Failure function Failure:new(error) ---@type Failure local instance = {} setmetatable(instance, self) instance.error = error return instance end ---@class Result ---@field value any local Result = {} Result.__index = Result function Result:new(value) ---@type Result local instance = {} setmetatable(instance, self) instance.value = value return instance end function Result.success(value) return Result:new(value) end function Result.failure(error) return Result:new(Failure:new(error)) end function Result:get_or_nil() if self:is_success() then return self.value end end function Result:get_or_else(value) if self:is_success() then return self.value else return value end end ---@param exception any? The exception to throw if the result is a failure. function Result:get_or_throw(exception) if self:is_success() then return self.value else if exception ~= nil then error(exception, 2) else error(self.value.error, 2) end end end function Result:err_or_nil() if self:is_failure() then return self.value.error end end function Result:is_failure() return getmetatable(self.value) == Failure end function Result:is_success() return getmetatable(self.value) ~= Failure end ---@param mapper_fn fun(value: any): any function Result:map(mapper_fn) if self:is_success() then return Result.success(mapper_fn(self.value)) else return self end end ---@param mapper_fn fun(value: any): any function Result:map_err(mapper_fn) if self:is_failure() then return Result.failure(mapper_fn(self.value.error)) else return self end end ---@param mapper_fn fun(value: any): any function Result:map_catching(mapper_fn) if self:is_success() then local ok, result = pcall(mapper_fn, self.value) if ok then return Result.success(result) else return Result.failure(result) end else return self end end ---@param recover_fn fun(value: any): any function Result:recover(recover_fn) if self:is_failure() then return Result.success(recover_fn(self:err_or_nil())) else return self end end ---@param recover_fn fun(value: any): any function Result:recover_catching(recover_fn) if self:is_failure() then local ok, value = pcall(recover_fn, self:err_or_nil()) if ok then return Result.success(value) else return Result.failure(value) end else return self end end ---@param fn fun(value: any): any function Result:on_failure(fn) if self:is_failure() then fn(self.value.error) end return self end ---@param fn fun(value: any): any function Result:on_success(fn) if self:is_success() then fn(self.value) end return self end function Result:ok() local Optional = require "mason-core.optional" if self:is_success() then return Optional.of(self.value) else return Optional.empty() end end ---@param fn fun(value: any): Result function Result:and_then(fn) if self:is_success() then return fn(self.value) else return self end end ---@param fn fun(err: any): Result function Result:or_else(fn) if self:is_failure() then return fn(self:err_or_nil()) else return self end end ---@param fn fun(): any ---@return Result function Result.run_catching(fn) local ok, result = pcall(fn) if ok then return Result.success(result) else return Result.failure(result) end end ---@generic T ---@param fn fun(try: fun(result: Result)): T? ---@return Result # Result function Result.try(fn) local thread = coroutine.create(fn) local step step = function(...) local ok, result = coroutine.resume(thread, ...) if not ok then return Result.failure(result) end if coroutine.status(thread) == "dead" then if getmetatable(result) == Result then return result else return Result.success(result) end elseif getmetatable(result) == Result then if result:is_failure() then return result else return step(result:get_or_nil()) end else -- yield to parent coroutine return step(coroutine.yield(result)) end end return step(coroutine.yield) end function Result.pcall(fn, ...) local ok, res = pcall(fn, ...) if ok then return Result.success(res) else return Result.failure(res) end end return Result ================================================ FILE: lua/mason-core/semver.lua ================================================ local Result = require "mason-core.result" local semver = require "mason-vendor.semver" local M = {} ---@param version string ---@return Semver function M.new(version) version = version:gsub("^v", "") return semver(version) end ---@param version string ---@return Result # Result function M.parse(version) return Result.pcall(M.new, version) end return M ================================================ FILE: lua/mason-core/spawn.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local log = require "mason-core.log" local platform = require "mason-core.platform" local process = require "mason-core.process" local is_not_nil = _.complement(_.equals(vim.NIL)) ---@alias JobSpawn table ---@type JobSpawn local spawn = { _flatten_cmd_args = _.compose(_.filter(is_not_nil), _.flatten), } ---@param cmd string ---@param path? string local function exepath(cmd, path) local function get_exepath(cmd) if path then local old_path = vim.env.PATH vim.env.PATH = path local expanded_cmd = vim.fn.exepath(cmd) vim.env.PATH = old_path return expanded_cmd else return vim.fn.exepath(cmd) end end if platform.is.win then -- On Windows, exepath() assumes the system is capable of executing "Unix-like" executables if the shell is a Unix -- shell. We temporarily override it to a Windows shell ("powershell") to avoid that behaviour. local old_shell = vim.o.shell vim.o.shell = "powershell" local expanded_cmd = get_exepath(cmd) vim.o.shell = old_shell return expanded_cmd else return get_exepath(cmd) end end local function Failure(err, cmd) return Result.failure(setmetatable(err, { __tostring = function() return ("spawn: %s failed with exit code %s and signal %s. %s"):format( cmd, err.exit_code or "-", err.signal or "-", err.stderr or "" ) end, })) end local get_path_from_env_list = _.compose(_.strip_prefix "PATH=", _.find_first(_.starts_with "PATH=")) ---@class SpawnArgs ---@field with_paths string[]? Paths to add to the PATH environment variable. ---@field env table? Example { SOME_ENV = "value", SOME_OTHER_ENV = "some_value" } ---@field env_raw string[]? Example: { "SOME_ENV=value", "SOME_OTHER_ENV=some_value" } ---@field stdio_sink StdioSink? If provided, will be used to write to stdout and stderr. ---@field cwd string? ---@field on_spawn (fun(handle: luv_handle, stdio: luv_pipe[], pid: integer))? Will be called when the process successfully spawns. setmetatable(spawn, { ---@param canonical_cmd string __index = function(self, canonical_cmd) ---@param args SpawnArgs return function(args) local cmd_args = self._flatten_cmd_args(args) local env = args.env if args.with_paths then env = env or {} env.PATH = process.extend_path(args.with_paths) end ---@type JobSpawnOpts local spawn_args = { stdio_sink = args.stdio_sink, cwd = args.cwd, env = env and process.graft_env(env) or args.env_raw, args = cmd_args, } if not spawn_args.stdio_sink then spawn_args.stdio_sink = process.BufferedSink:new() end local cmd = canonical_cmd -- Find the executable path via vim.fn.exepath on Windows because libuv fails to resolve certain executables -- in PATH. if platform.is.win then a.scheduler() local expanded_cmd = exepath(canonical_cmd, spawn_args.env and get_path_from_env_list(spawn_args.env)) if expanded_cmd ~= "" then cmd = expanded_cmd end end local _, exit_code, signal = a.wait(function(resolve) local handle, stdio, pid = process.spawn(cmd, spawn_args, resolve) if args.on_spawn and handle and stdio and pid then args.on_spawn(handle, stdio, pid) end end) if exit_code == 0 and signal == 0 then if getmetatable(spawn_args.stdio_sink) == process.BufferedSink then local sink = spawn_args.stdio_sink --[[@as BufferedSink]] return Result.success { stdout = table.concat(sink.buffers.stdout, "") or nil, stderr = table.concat(sink.buffers.stderr, "") or nil, } else return Result.success() end else if getmetatable(spawn_args.stdio_sink) == process.BufferedSink then local sink = spawn_args.stdio_sink --[[@as BufferedSink]] return Failure({ exit_code = exit_code, signal = signal, stdout = table.concat(sink.buffers.stdout, "") or nil, stderr = table.concat(sink.buffers.stderr, "") or nil, }, canonical_cmd) else return Failure({ exit_code = exit_code, signal = signal, }, canonical_cmd) end end end end, }) return spawn ================================================ FILE: lua/mason-core/terminator.lua ================================================ local a = require "mason-core.async" -- Hasta la vista, baby. -- ______ -- <((((((\\\ -- / . }\ -- ;--..--._|} -- (\ '--/\--' ) -- \\ | '-' :'| -- \\ . -==- .-| -- \\ \.__.' \--._ -- [\\ __.--| // _/'--. -- \ \\ .'-._ ('-----'/ __/ \ -- \ \\ / __>| | '--. | -- \ \\ | \ | / / / -- \ '\ / \ | | _/ / -- \ \ \ | | / / -- snd \ \ \ / local M = {} ---@async ---@param handles InstallHandle[] ---@param grace_ms integer local function terminate_handles(handles, grace_ms) a.wait_all(vim.tbl_map( ---@param handle InstallHandle function(handle) return function() local timer if not handle:is_closed() then handle:terminate() timer = vim.defer_fn(function() if not handle:is_closed() then handle:kill(9) -- SIGKILL end end, grace_ms) end a.wait(function(resolve) if handle:is_closed() then resolve() else handle:once("closed", resolve) end end) if timer then timer:stop() end end end, handles )) end local active_handles = {} ---@param handle InstallHandle function M.register(handle) if handle:is_closed() then return end active_handles[handle] = true handle:once("closed", function() active_handles[handle] = nil end) end ---@param grace_ms integer function M.terminate(grace_ms) local handles = vim.tbl_keys(active_handles) if #handles > 0 then local package_names = vim.tbl_map(function(h) return h.package.name end, handles) table.sort(package_names) -- 1. Print warning message. vim.api.nvim_echo({ { "[mason.nvim] Neovim is exiting while packages are still installing. Terminating all installations…", "WarningMsg", }, }, true, {}) vim.cmd "redraw" -- 2. Synchronously terminate all installation handles. a.run_blocking(function() terminate_handles(handles, grace_ms) end) -- 3. Schedule error message to be displayed so that Neovim prints it to the tty. -- XXX: does this need to be conditional on which UIs are attached? vim.schedule(function() vim.api.nvim_err_writeln( ("[mason.nvim] Neovim exited while the following packages were installing. Installation was aborted.\n- %s"):format( table.concat(package_names, #package_names > 5 and ", " or "\n- ") ) ) end) end end return M ================================================ FILE: lua/mason-core/ui/display.lua ================================================ local EventEmitter = require "mason-core.EventEmitter" local log = require "mason-core.log" local settings = require "mason.settings" local state = require "mason-core.ui.state" local M = {} ---@generic T ---@param debounced_fn fun(arg1: T) ---@return fun(arg1: T) local function debounced(debounced_fn) local queued = false local last_arg = nil return function(a) last_arg = a if queued then return end queued = true vim.schedule(function() debounced_fn(last_arg) queued = false last_arg = nil end) end end ---@param line string ---@param render_context RenderContext local function get_styles(line, render_context) local indentation = 0 for i = 1, #render_context.applied_block_styles do local styles = render_context.applied_block_styles[i] for j = 1, #styles do local style = styles[j] if style == "INDENT" then indentation = indentation + 2 elseif style == "CENTERED" then local padding = math.floor((render_context.viewport_context.win_width - #line) / 2) indentation = math.max(0, padding) -- CENTERED overrides any already applied indentation end end end return { indentation = indentation, } end ---@param viewport_context ViewportContext ---@param node INode ---@param _render_context RenderContext? ---@param _output RenderOutput? local function render_node(viewport_context, node, _render_context, _output) ---@class RenderContext ---@field viewport_context ViewportContext ---@field applied_block_styles CascadingStyle[] local render_context = _render_context or { viewport_context = viewport_context, applied_block_styles = {}, } ---@class RenderHighlight ---@field hl_group string ---@field line number ---@field col_start number ---@field col_end number ---@class RenderKeybind ---@field line number ---@field key string ---@field effect string ---@field payload any ---@class RenderDiagnostic ---@field line number ---@field diagnostic {message: string, severity: integer, source: string|nil} ---@class RenderOutput ---@field lines string[]: The buffer lines. ---@field virt_texts {line: integer, content: table}[]: List of tuples. ---@field highlights RenderHighlight[] ---@field keybinds RenderKeybind[] ---@field diagnostics RenderDiagnostic[] ---@field sticky_cursors { line_map: table, id_map: table } local output = _output or { lines = {}, virt_texts = {}, highlights = {}, keybinds = {}, diagnostics = {}, sticky_cursors = { line_map = {}, id_map = {} }, } if node.type == "VIRTUAL_TEXT" then output.virt_texts[#output.virt_texts + 1] = { line = #output.lines - 1, content = node.virt_text, } elseif node.type == "HL_TEXT" then for i = 1, #node.lines do local line = node.lines[i] local line_highlights = {} local full_line = "" for j = 1, #line do local span = line[j] local content, hl_group = span[1], span[2] local col_start = #full_line full_line = full_line .. content if hl_group ~= "" then line_highlights[#line_highlights + 1] = { hl_group = hl_group, line = #output.lines, col_start = col_start, col_end = col_start + #content, } end end -- only apply cascading styles to non-empty lines if string.len(full_line) > 0 then local active_styles = get_styles(full_line, render_context) full_line = (" "):rep(active_styles.indentation) .. full_line for j = 1, #line_highlights do local highlight = line_highlights[j] highlight.col_start = highlight.col_start + active_styles.indentation highlight.col_end = highlight.col_end + active_styles.indentation output.highlights[#output.highlights + 1] = highlight end end output.lines[#output.lines + 1] = full_line end elseif node.type == "NODE" or node.type == "CASCADING_STYLE" then if node.type == "CASCADING_STYLE" then render_context.applied_block_styles[#render_context.applied_block_styles + 1] = node.styles end for i = 1, #node.children do render_node(viewport_context, node.children[i], render_context, output) end if node.type == "CASCADING_STYLE" then render_context.applied_block_styles[#render_context.applied_block_styles] = nil end elseif node.type == "KEYBIND_HANDLER" then output.keybinds[#output.keybinds + 1] = { line = node.is_global and -1 or #output.lines, key = node.key, effect = node.effect, payload = node.payload, } elseif node.type == "DIAGNOSTICS" then output.diagnostics[#output.diagnostics + 1] = { line = #output.lines, message = node.diagnostic.message, severity = node.diagnostic.severity, source = node.diagnostic.source, } elseif node.type == "STICKY_CURSOR" then output.sticky_cursors.id_map[node.id] = #output.lines output.sticky_cursors.line_map[#output.lines] = node.id end return output end -- exported for tests M._render_node = render_node ---@alias WindowOpts { effects?: table, winhighlight?: string[], border?: string|table } ---@param size number ---@param viewport integer local function calc_size(size, viewport) return size > 1 and math.min(size, viewport) or math.floor(size * viewport) end ---@param opts WindowOpts ---@param sizes_only boolean Whether to only return properties that control the window size. local function create_popup_window_opts(opts, sizes_only) local lines = vim.o.lines - vim.o.cmdheight local columns = vim.o.columns local height = calc_size(settings.current.ui.height, lines) local width = calc_size(settings.current.ui.width, columns) local row = math.floor((lines - height) / 2) local col = math.floor((columns - width) / 2) if opts.border ~= "none" and opts.border ~= "" then row = math.max(row - 1, 0) col = math.max(col - 1, 0) end local popup_layout = { height = height, width = width, row = row, col = col, relative = "editor", style = "minimal", zindex = 45, } if not sizes_only then popup_layout.border = opts.border end return popup_layout end local function create_backdrop_window_opts() return { relative = "editor", width = vim.o.columns, height = vim.o.lines, row = 0, col = 0, style = "minimal", focusable = false, border = "none", zindex = 44, } end ---@param name string Human readable identifier. ---@param filetype string function M.new_view_only_win(name, filetype) local namespace = vim.api.nvim_create_namespace(("installer_%s"):format(name)) local bufnr, backdrop_bufnr, renderer, mutate_state, get_state, unsubscribe, win_id, backdrop_win_id, window_mgmt_augroup, autoclose_augroup, registered_keymaps, registered_keybinds, registered_effect_handlers, sticky_cursor local has_initiated = false ---@type WindowOpts local window_opts = {} local events = EventEmitter:new() vim.diagnostic.config({ virtual_text = { severity = { min = vim.diagnostic.severity.HINT, max = vim.diagnostic.severity.ERROR }, }, right_align = false, underline = false, signs = false, virtual_lines = false, }, namespace) local function close_window() -- We queue the win_buf to be deleted in a schedule call, otherwise when used with folke/which-key (and -- set timeoutlen=0) we run into a weird segfault. -- It should probably be unnecessary once https://github.com/neovim/neovim/issues/15548 is resolved vim.schedule(function() if win_id and vim.api.nvim_win_is_valid(win_id) then log.trace "Deleting window" vim.api.nvim_win_close(win_id, true) end end) end ---@param line number ---@param key string local function call_effect_handler(line, key) local line_keybinds = registered_keybinds[line] if line_keybinds then local keybind = line_keybinds[key] if keybind then local effect_handler = registered_effect_handlers[keybind.effect] if effect_handler then log.fmt_trace("Calling handler for effect %s on line %d for key %s", keybind.effect, line, key) effect_handler { payload = keybind.payload } return true end end end return false end local function dispatch_effect(key) local line = vim.api.nvim_win_get_cursor(0)[1] log.fmt_trace("Dispatching effect on line %d, key %s, bufnr %s", line, key, bufnr) call_effect_handler(line, key) -- line keybinds call_effect_handler(-1, key) -- global keybinds end local output local draw = function(view) local win_valid = win_id ~= nil and vim.api.nvim_win_is_valid(win_id) local buf_valid = bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr) if not win_valid or not buf_valid then -- the window has been closed or the buffer is somehow no longer valid unsubscribe(true) log.trace("Buffer or window is no longer valid", win_id, bufnr) return end local win_width = vim.api.nvim_win_get_width(win_id) ---@class ViewportContext local viewport_context = { win_width = win_width, } local cursor_pos_pre_render = vim.api.nvim_win_get_cursor(win_id) if output then sticky_cursor = output.sticky_cursors.line_map[cursor_pos_pre_render[1]] end output = render_node(viewport_context, view) local lines, virt_texts, highlights, keybinds, diagnostics = output.lines, output.virt_texts, output.highlights, output.keybinds, output.diagnostics -- set line contents vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) vim.api.nvim_buf_set_option(bufnr, "modifiable", true) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.api.nvim_buf_set_option(bufnr, "modifiable", false) -- restore sticky cursor position if sticky_cursor then local new_sticky_cursor_line = output.sticky_cursors.id_map[sticky_cursor] if new_sticky_cursor_line and new_sticky_cursor_line ~= cursor_pos_pre_render then vim.api.nvim_win_set_cursor(win_id, { new_sticky_cursor_line, cursor_pos_pre_render[2] }) end end -- set virtual texts for i = 1, #virt_texts do local virt_text = virt_texts[i] vim.api.nvim_buf_set_extmark(bufnr, namespace, virt_text.line, 0, { virt_text = virt_text.content, }) end -- set diagnostics vim.diagnostic.set( namespace, bufnr, vim.tbl_map(function(diagnostic) return { lnum = diagnostic.line - 1, col = 0, message = diagnostic.message, severity = diagnostic.severity, source = diagnostic.source, } end, diagnostics), { signs = false, } ) -- set highlights for i = 1, #highlights do local highlight = highlights[i] vim.api.nvim_buf_add_highlight( bufnr, namespace, highlight.hl_group, highlight.line, highlight.col_start, highlight.col_end ) end -- set keybinds registered_keybinds = {} for i = 1, #keybinds do local keybind = keybinds[i] if not registered_keybinds[keybind.line] then registered_keybinds[keybind.line] = {} end registered_keybinds[keybind.line][keybind.key] = keybind if not registered_keymaps[keybind.key] then registered_keymaps[keybind.key] = true vim.keymap.set("n", keybind.key, function() dispatch_effect(keybind.key) end, { buffer = bufnr, nowait = true, silent = true, }) end end end local function open() bufnr = vim.api.nvim_create_buf(false, true) win_id = vim.api.nvim_open_win(bufnr, true, create_popup_window_opts(window_opts, false)) local normal_hl = vim.api.nvim_get_hl and vim.api.nvim_get_hl(0, { name = "Normal" }) local is_nvim_transparent = normal_hl and normal_hl.bg == nil if settings.current.ui.backdrop ~= 100 and vim.o.termguicolors and not is_nvim_transparent then backdrop_bufnr = vim.api.nvim_create_buf(false, true) backdrop_win_id = vim.api.nvim_open_win(backdrop_bufnr, false, create_backdrop_window_opts()) vim.wo[backdrop_win_id].winhighlight = "Normal:MasonBackdrop" vim.wo[backdrop_win_id].winblend = settings.current.ui.backdrop vim.bo[backdrop_bufnr].buftype = "nofile" -- https://github.com/folke/lazy.nvim/issues/1399 vim.bo[backdrop_bufnr].filetype = "mason_backdrop" vim.bo[backdrop_bufnr].bufhidden = "wipe" end vim.api.nvim_create_autocmd("CmdLineEnter", { buffer = bufnr, callback = function() if vim.v.event.cmdtype == "/" or vim.v.event.cmdtype == "?" then events:emit "search:enter" end end, }) vim.api.nvim_create_autocmd("CmdLineLeave", { buffer = bufnr, callback = function(args) if vim.v.event.cmdtype == "/" or vim.v.event.cmdtype == "?" then events:emit("search:leave", vim.fn.getcmdline()) end end, }) registered_effect_handlers = window_opts.effects registered_keybinds = {} registered_keymaps = {} local buf_opts = { modifiable = false, swapfile = false, textwidth = 0, buftype = "nofile", bufhidden = "wipe", buflisted = false, filetype = filetype, undolevels = -1, } local win_opts = { number = false, relativenumber = false, wrap = false, spell = false, foldenable = false, signcolumn = "no", colorcolumn = "", cursorline = true, } -- window options for key, value in pairs(win_opts) do vim.api.nvim_win_set_option(win_id, key, value) end if window_opts.winhighlight then vim.api.nvim_win_set_option(win_id, "winhighlight", table.concat(window_opts.winhighlight, ",")) end -- buffer options for key, value in pairs(buf_opts) do vim.api.nvim_buf_set_option(bufnr, key, value) end vim.cmd [[ syntax clear ]] window_mgmt_augroup = vim.api.nvim_create_augroup("MasonWindowMgmt", {}) autoclose_augroup = vim.api.nvim_create_augroup("MasonWindow", {}) vim.api.nvim_create_autocmd({ "VimResized" }, { group = window_mgmt_augroup, buffer = bufnr, callback = function() if win_id and vim.api.nvim_win_is_valid(win_id) then draw(renderer(get_state())) vim.api.nvim_win_set_config(win_id, create_popup_window_opts(window_opts, true)) end if backdrop_win_id and vim.api.nvim_win_is_valid(backdrop_win_id) then vim.api.nvim_win_set_config(backdrop_win_id, create_backdrop_window_opts()) end end, }) vim.api.nvim_create_autocmd({ "WinClosed" }, { once = true, pattern = tostring(win_id), callback = function() if backdrop_win_id and vim.api.nvim_win_is_valid(backdrop_win_id) then vim.api.nvim_win_close(backdrop_win_id, true) end end, }) vim.api.nvim_create_autocmd({ "BufHidden", "BufUnload" }, { group = autoclose_augroup, buffer = bufnr, -- This is for instances where the window remains but the buffer is no longer visible, for example when -- loading another buffer into it (this is basically imitating 'winfixbuf', which was added in 0.10.0). callback = close_window, }) -- This autocmd is responsible for closing the Mason window(s) when the user focuses another window. It -- essentially behaves as WinLeave except it keeps the Mason window(s) open under certain circumstances. local win_enter_aucmd win_enter_aucmd = vim.api.nvim_create_autocmd({ "WinEnter" }, { group = autoclose_augroup, pattern = "*", callback = function() local buftype = vim.api.nvim_buf_get_option(0, "buftype") -- This allows us to keep the floating window open for things like diagnostic popups, UI inputs á la dressing.nvim, etc. if buftype ~= "prompt" and buftype ~= "nofile" then close_window() vim.api.nvim_del_autocmd(win_enter_aucmd) end end, }) return win_id end return { events = events, ---@param _renderer fun(state: table): table view = function(_renderer) renderer = _renderer end, ---@param _effects table effects = function(_effects) window_opts.effects = _effects end, ---@generic T : table ---@param initial_state T ---@return fun(mutate_fn: fun(current_state: T)), fun(): T state = function(initial_state) mutate_state, get_state, unsubscribe = state.create_state_container( initial_state, debounced(function(new_state) draw(renderer(new_state)) end) ) -- we don't need to subscribe to state changes until the window is actually opened unsubscribe(true) return mutate_state, get_state end, ---@param opts WindowOpts init = function(opts) assert(renderer ~= nil, "No view function has been registered. Call .view() before .init().") assert(unsubscribe ~= nil, "No state has been registered. Call .state() before .init().") window_opts = opts has_initiated = true end, open = vim.schedule_wrap(function() log.trace "Opening window" assert(has_initiated, "Display has not been initiated, cannot open.") if win_id and vim.api.nvim_win_is_valid(win_id) then -- window is already open return end unsubscribe(false) open() draw(renderer(get_state())) end), ---@type fun() close = vim.schedule_wrap(function() assert(has_initiated, "Display has not been initiated, cannot close.") unsubscribe(true) log.fmt_trace("Closing window win_id=%s, bufnr=%s", win_id, bufnr) close_window() vim.api.nvim_del_augroup_by_id(window_mgmt_augroup) vim.api.nvim_del_augroup_by_id(autoclose_augroup) end), ---@param pos number[]: (row, col) tuple set_cursor = function(pos) assert(win_id ~= nil, "Window has not been opened, cannot set cursor.") return vim.api.nvim_win_set_cursor(win_id, pos) end, ---@return number[]: (row, col) tuple get_cursor = function() assert(win_id ~= nil, "Window has not been opened, cannot get cursor.") return vim.api.nvim_win_get_cursor(win_id) end, is_open = function() return win_id ~= nil and vim.api.nvim_win_is_valid(win_id) end, ---@param tag any set_sticky_cursor = function(tag) if output then local new_sticky_cursor_line = output.sticky_cursors.id_map[tag] if new_sticky_cursor_line then sticky_cursor = tag local cursor = vim.api.nvim_win_get_cursor(win_id) vim.api.nvim_win_set_cursor(win_id, { new_sticky_cursor_line, cursor[2] }) end end end, get_win_config = function() assert(win_id ~= nil, "Window has not been opened, cannot get config.") return vim.api.nvim_win_get_config(win_id) end, } end return M ================================================ FILE: lua/mason-core/ui/init.lua ================================================ local _ = require "mason-core.functional" local M = {} ---@alias NodeType ---| '"NODE"' ---| '"CASCADING_STYLE"' ---| '"VIRTUAL_TEXT"' ---| '"DIAGNOSTICS"' ---| '"HL_TEXT"' ---| '"KEYBIND_HANDLER"' ---| '"STICKY_CURSOR"' ---@alias INode Node | HlTextNode | CascadingStyleNode | VirtualTextNode | KeybindHandlerNode | DiagnosticsNode | StickyCursorNode ---@param children INode[] function M.Node(children) ---@class Node local node = { type = "NODE", children = children, } return node end ---@param lines_with_span_tuples string[][]|string[] function M.HlTextNode(lines_with_span_tuples) if type(lines_with_span_tuples[1]) == "string" then -- this enables a convenience API for just rendering a single line (with just a single span) lines_with_span_tuples = { { lines_with_span_tuples } } end ---@class HlTextNode local node = { type = "HL_TEXT", lines = lines_with_span_tuples, } return node end local create_unhighlighted_lines = _.map(function(line) return { { line, "" } } end) ---@param lines string[] function M.Text(lines) return M.HlTextNode(create_unhighlighted_lines(lines)) end ---@alias CascadingStyle ---| '"INDENT"' ---| '"CENTERED"' ---@param styles CascadingStyle[] ---@param children INode[] function M.CascadingStyleNode(styles, children) ---@class CascadingStyleNode local node = { type = "CASCADING_STYLE", styles = styles, children = children, } return node end ---@param virt_text string[][] List of (text, highlight) tuples. function M.VirtualTextNode(virt_text) ---@class VirtualTextNode local node = { type = "VIRTUAL_TEXT", virt_text = virt_text, } return node end ---@param diagnostic {message: string, severity: integer, source: string?} function M.DiagnosticsNode(diagnostic) ---@class DiagnosticsNode local node = { type = "DIAGNOSTICS", diagnostic = diagnostic, } return node end ---@param condition boolean ---@param node INode | fun(): INode ---@param default_val any function M.When(condition, node, default_val) if condition then if type(node) == "function" then return node() else return node end end return default_val or M.Node {} end ---@param key string The keymap to register to. Example: "". ---@param effect string The effect to call when keymap is triggered by the user. ---@param payload any The payload to pass to the effect handler when triggered. ---@param is_global boolean? Whether to register the keybind to apply on all lines in the buffer. function M.Keybind(key, effect, payload, is_global) ---@class KeybindHandlerNode local node = { type = "KEYBIND_HANDLER", key = key, effect = effect, payload = payload, is_global = is_global or false, } return node end function M.EmptyLine() return M.Text { "" } end ---@param rows string[][][] A list of rows to include in the table. Each row consists of an array of (text, highlight) tuples (aka spans). function M.Table(rows) local col_maxwidth = {} for i = 1, #rows do local row = rows[i] for j = 1, #row do local col = row[j] local content = col[1] col_maxwidth[j] = math.max(vim.api.nvim_strwidth(content), col_maxwidth[j] or 0) end end for i = 1, #rows do local row = rows[i] for j = 1, #row do local col = row[j] local content = col[1] col[1] = content .. string.rep(" ", col_maxwidth[j] - vim.api.nvim_strwidth(content) + 1) -- +1 for default minimum padding end end return M.HlTextNode(rows) end ---@param opts { id: string } function M.StickyCursor(opts) ---@class StickyCursorNode local node = { type = "STICKY_CURSOR", id = opts.id, } return node end ---Makes it possible to create stateful animations by progressing from the start of a range to the end. ---This is done in "ticks", in accordance with the provided options. ---@param opts {range: integer[], delay_ms: integer, start_delay_ms: integer, iteration_delay_ms: integer} function M.animation(opts) local animation_fn = opts[1] local start_tick, end_tick = opts.range[1], opts.range[2] local is_animating = false local function start_animation() if is_animating then return end local tick, start tick = function(current_tick) animation_fn(current_tick) if current_tick < end_tick then vim.defer_fn(function() tick(current_tick + 1) end, opts.delay_ms) else is_animating = false if opts.iteration_delay_ms then start(opts.iteration_delay_ms) end end end start = function(delay_ms) is_animating = true if delay_ms then vim.defer_fn(function() tick(start_tick) end, delay_ms) else tick(start_tick) end end start(opts.start_delay_ms) local function cancel() is_animating = false end return cancel end return start_animation end return M ================================================ FILE: lua/mason-core/ui/state.lua ================================================ local M = {} ---@generic T : table ---@param initial_state T ---@param subscriber fun(state: T) function M.create_state_container(initial_state, subscriber) -- we do deepcopy to make sure instances of state containers doesn't mutate the initial state local state = vim.deepcopy(initial_state) local has_unsubscribed = false ---@param mutate_fn fun(current_state: table) return function(mutate_fn) mutate_fn(state) if not has_unsubscribed then subscriber(state) end end, function() return state end, function(val) has_unsubscribed = val end end return M ================================================ FILE: lua/mason-registry/api.lua ================================================ local _ = require "mason-core.functional" local fetch = require "mason-core.fetch" local api = {} -- https://github.com/mason-org/mason-registry-api local BASE_URL = "https://api.mason-registry.dev" local stringify_params = _.compose(_.join "&", _.map(_.join "="), _.sort_by(_.head), _.to_pairs) ---@alias ApiFetchOpts { params: table? } ---@async ---@param path string ---@param opts ApiFetchOpts? ---@return Result # JSON decoded response. function api.get(path, opts) if opts and opts.params then local params = stringify_params(opts.params) path = ("%s?%s"):format(path, params) end return fetch(("%s%s"):format(BASE_URL, path), { headers = { Accept = "application/vnd.mason-registry.v1+json; q=1.0, application/json; q=0.8", }, }):map_catching(vim.json.decode) end ---@alias ApiSignature async fun(path_params: T, opts?: ApiFetchOpts): Result ---@param char string local function percent_encode(char) return ("%%%x"):format(string.byte(char, 1, 1)) end api.encode_uri_component = _.gsub("[!#%$&'%(%)%*%+,/:;=%?@%[%]]", percent_encode) ---@param path_template string local function get(path_template) ---@async ---@param path_params table ---@param opts ApiFetchOpts? return function(path_params, opts) local path = path_template:gsub("{([%w_%.0-9]+)}", function(prop) return path_params[prop] end) -- This is done so that test stubs trigger as expected (you have to explicitly match against nil arguments) if opts then return api.get(path, opts) else return api.get(path) end end end api.github = { releases = { ---@type ApiSignature<{ repo: string }> latest = get "/api/github/{repo}/releases/latest", ---@type ApiSignature<{ repo: string }> all = get "/api/github/{repo}/releases/all", }, tags = { ---@type ApiSignature<{ repo: string }> latest = get "/api/github/{repo}/tags/latest", ---@type ApiSignature<{ repo: string }> all = get "/api/github/{repo}/tags/all", }, } api.npm = { versions = { ---@type ApiSignature<{ package: string }> latest = get "/api/npm/{package}/versions/latest", ---@type ApiSignature<{ package: string }> all = get "/api/npm/{package}/versions/all", }, } api.pypi = { versions = { ---@type ApiSignature<{ package: string }> latest = get "/api/pypi/{package}/versions/latest", ---@type ApiSignature<{ package: string }> all = get "/api/pypi/{package}/versions/all", ---@type ApiSignature<{ package: string, version: string }> get = get "/api/pypi/{package}/versions/{version}", }, } api.rubygems = { versions = { ---@type ApiSignature<{ gem: string }> latest = get "/api/rubygems/{gem}/versions/latest", ---@type ApiSignature<{ gem: string }> all = get "/api/rubygems/{gem}/versions/all", }, } api.packagist = { versions = { ---@type ApiSignature<{ pkg: string }> latest = get "/api/packagist/{pkg}/versions/latest", ---@type ApiSignature<{ pkg: string }> all = get "/api/packagist/{pkg}/versions/all", }, } api.crate = { versions = { ---@type ApiSignature<{ crate: string }> latest = get "/api/crate/{crate}/versions/latest", ---@type ApiSignature<{ crate: string }> all = get "/api/crate/{crate}/versions/all", }, } api.golang = { versions = { ---@type ApiSignature<{ pkg: string }> all = get "/api/golang/{pkg}/versions/all", }, } api.openvsx = { versions = { ---@type ApiSignature<{ namespace: string, extension: string }> latest = get "/api/openvsx/{namespace}/{extension}/versions/latest", ---@type ApiSignature<{ namespace: string, extension: string }> all = get "/api/openvsx/{namespace}/{extension}/versions/all", }, } return api ================================================ FILE: lua/mason-registry/index/init.lua ================================================ return {} ================================================ FILE: lua/mason-registry/init.lua ================================================ local EventEmitter = require "mason-core.EventEmitter" local InstallLocation = require "mason-core.installer.InstallLocation" local log = require "mason-core.log" local uv = vim.loop local LazySourceCollection = require "mason-registry.sources" -- singleton local Registry = EventEmitter:new() Registry.sources = LazySourceCollection:new() ---@type table Registry.aliases = {} ---@param pkg_name string function Registry.is_installed(pkg_name) local stat = uv.fs_stat(InstallLocation.global():package(pkg_name)) return stat ~= nil and stat.type == "directory" end ---Returns an instance of the Package class if the provided package name exists. This function errors if a package ---cannot be found. ---@param pkg_name string ---@return Package function Registry.get_package(pkg_name) for source in Registry.sources:iterate() do local pkg = source:get_package(pkg_name) if pkg then return pkg end end log.fmt_error("Cannot find package %q.", pkg_name) error(("Cannot find package %q."):format(pkg_name)) end function Registry.has_package(pkg_name) local ok = pcall(Registry.get_package, pkg_name) return ok end function Registry.get_installed_package_names() local fs = require "mason-core.fs" if not fs.sync.dir_exists(InstallLocation.global():package()) then return {} end local entries = fs.sync.readdir(InstallLocation:global():package()) local directories = {} for _, entry in ipairs(entries) do if entry.type == "directory" then directories[#directories + 1] = entry.name end end return directories end function Registry.get_installed_packages() return vim.tbl_map(Registry.get_package, Registry.get_installed_package_names()) end function Registry.get_all_package_names() local pkgs = {} for source in Registry.sources:iterate() do for _, name in ipairs(source:get_all_package_names()) do pkgs[name] = true end end return vim.tbl_keys(pkgs) end function Registry.get_all_packages() local _ = require "mason-core.functional" local packages = _.uniq_by(_.identity, _.concat(Registry.get_all_package_names(), Registry.get_installed_package_names())) return vim.tbl_map(Registry.get_package, packages) end function Registry.get_all_package_specs() local specs = {} for source in Registry.sources:iterate() do for _, spec in ipairs(source:get_all_package_specs()) do if not specs[spec.name] then specs[spec.name] = spec end end end return vim.tbl_values(specs) end ---Register aliases for the specified packages ---@param new_aliases table function Registry.register_package_aliases(new_aliases) for pkg_name, pkg_aliases in pairs(new_aliases) do Registry.aliases[pkg_name] = Registry.aliases[pkg_name] or {} for _, alias in pairs(pkg_aliases) do if alias ~= pkg_name then table.insert(Registry.aliases[pkg_name], alias) end end end end ---@param name string function Registry.get_package_aliases(name) return Registry.aliases[name] or {} end ---@param callback? fun(success: boolean, updated_registries: RegistrySource[]) function Registry.update(callback) local a = require "mason-core.async" local installer = require "mason-registry.installer" local noop = function() end a.run(function() if installer.channel then log.debug "Registry update already in progress." return installer.channel:receive():get_or_throw() else log.debug "Updating the registry." Registry:emit("update:start", Registry.sources) return installer .install(Registry.sources, function(finished, all) Registry:emit("update:progress", finished, all) end) :on_success(function(updated_registries) log.fmt_debug("Successfully updated %d registries.", #updated_registries) Registry:emit("update:success", updated_registries) end) :on_failure(function(errors) log.error("Failed to update registries.", errors) Registry:emit("update:failed", errors) end) :get_or_throw() end end, callback or noop) end local REGISTRY_STORE_TTL = 86400 -- 24 hrs ---@param callback? fun(success: boolean, updated_registries: RegistrySource[]) function Registry.refresh(callback) log.debug "Refreshing the registry." local a = require "mason-core.async" local installer = require "mason-registry.installer" local state = installer.get_registry_state() if state and Registry.sources:is_all_installed() then local registry_age = os.time() - state.timestamp if registry_age <= REGISTRY_STORE_TTL and state.checksum == Registry.sources:checksum() then log.fmt_debug( "Registry refresh is not necessary yet. Registry age=%d, checksum=%s", registry_age, state.checksum ) if callback then callback(true, {}) end return end end local function async_update() return a.wait(function(resolve, reject) Registry.update(function(success, result) if success then resolve(result) else reject(result) end end) end) end if not callback then -- We don't want to error in the synchronous version because of how this function is recommended to be used in -- 3rd party code. If accessing the update result is required, users are recommended to pass a callback. pcall(a.run_blocking, async_update) else a.run(async_update, callback) end end return Registry ================================================ FILE: lua/mason-registry/installer.lua ================================================ local a = require "mason-core.async" local log = require "mason-core.log" local OneShotChannel = require("mason-core.async.control").OneShotChannel local Result = require "mason-core.result" local _ = require "mason-core.functional" local fs = require "mason-core.fs" local path = require "mason-core.path" local M = {} local STATE_FILE = path.concat { vim.fn.stdpath "cache", "mason-registry-update" } ---@param sources LazySourceCollection ---@param time integer local function update_registry_state(sources, time) log.trace("Updating registry state", sources, time) local dir = vim.fn.fnamemodify(STATE_FILE, ":h") if not fs.sync.dir_exists(dir) then fs.sync.mkdirp(dir) end fs.sync.write_file(STATE_FILE, _.join("\n", { sources:checksum(), tostring(time) })) end ---@return { checksum: string, timestamp: integer }? function M.get_registry_state() if fs.sync.file_exists(STATE_FILE) then local parse_state_file = _.compose(_.evolve { timestamp = tonumber }, _.zip_table { "checksum", "timestamp" }, _.split "\n") return parse_state_file(fs.sync.read_file(STATE_FILE)) end end ---@async ---@param sources LazySourceCollection ---@param on_progress fun(finished: RegistrySource[], all: RegistrySource[]) ---@return Result # Result function M.install(sources, on_progress) log.debug("Installing registries.", sources) assert(not M.channel, "Cannot install when channel is active.") M.channel = OneShotChannel:new() local finished_registries = {} local registries = sources:to_list { include_uninstalled = true, include_synthesized = false } local results = { a.wait_all(_.map( ---@param source RegistrySource function(source) return function() log.trace("Installing registry.", source) return source :install() :map(_.always(source)) :map_err(function(err) return ("%s failed to install: %s"):format(source, err) end) :on_success(function() table.insert(finished_registries, source) on_progress(finished_registries, registries) end) end end, registries )), } local any_failed = _.any(Result.is_failure, results) if any_failed then local unwrap_failures = _.compose(_.map(Result.err_or_nil), _.filter(Result.is_failure)) local result = Result.failure(unwrap_failures(results)) M.channel:send(result) M.channel = nil return result else local result = Result.success(_.map(Result.get_or_nil, results)) a.scheduler() update_registry_state(sources, os.time()) M.channel:send(result) M.channel = nil return result end end return M ================================================ FILE: lua/mason-registry/sources/file.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local a = require "mason-core.async" local async_uv = require "mason-core.async.uv" local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local process = require "mason-core.process" local spawn = require "mason-core.spawn" local util = require "mason-registry.sources.util" ---@class FileRegistrySourceSpec ---@field id string ---@field path string ---@class FileRegistrySource : RegistrySource ---@field spec FileRegistrySourceSpec ---@field root_dir string ---@field buffer { specs: RegistryPackageSpec[], instances: table }? local FileRegistrySource = {} FileRegistrySource.__index = FileRegistrySource ---@param spec FileRegistrySourceSpec function FileRegistrySource:new(spec) ---@type FileRegistrySource local instance = {} setmetatable(instance, self) instance.id = spec.id instance.spec = spec return instance end function FileRegistrySource:is_installed() return self.buffer ~= nil end ---@return RegistryPackageSpec[] function FileRegistrySource:get_all_package_specs() return _.filter_map(util.map_registry_spec, self:get_buffer().specs) end ---@param specs RegistryPackageSpec[] function FileRegistrySource:reload(specs) self.buffer = _.assoc("specs", specs, self.buffer or {}) self.buffer.instances = _.compose( _.index_by(_.prop "name"), _.map(util.hydrate_package(self, self.buffer.instances or {})) )(self:get_all_package_specs()) return self.buffer end function FileRegistrySource:get_buffer() return self.buffer or { specs = {}, instances = {}, } end ---@param pkg_name string ---@return Package? function FileRegistrySource:get_package(pkg_name) return self:get_buffer().instances[pkg_name] end function FileRegistrySource:get_all_package_names() return _.map(_.prop "name", self:get_all_package_specs()) end ---@async function FileRegistrySource:install() return Result.try(function(try) a.scheduler() if vim.fn.executable "yq" ~= 1 then return Result.failure "yq is not installed." end local yq = vim.fn.exepath "yq" local registry_dir = vim.fn.expand(self.spec.path) --[[@as string]] local packages_dir = path.concat { registry_dir, "packages" } if not fs.async.dir_exists(registry_dir) then return Result.failure(("Directory %s does not exist."):format(registry_dir)) end if not fs.async.dir_exists(packages_dir) then return Result.failure "packages/ directory is missing." end ---@type ReaddirEntry[] local entries = _.filter(_.prop_eq("type", "directory"), fs.async.readdir(packages_dir)) local streaming_parser = coroutine.wrap(function() local buffer = "" while true do local delim = buffer:find("\n", 1, true) if delim then local content = buffer:sub(1, delim - 1) buffer = buffer:sub(delim + 1) local chunk = coroutine.yield(content) buffer = buffer .. chunk else local chunk = coroutine.yield() buffer = buffer .. chunk end end end) -- Initialize parser coroutine. streaming_parser() local specs = {} local stderr_buffer = {} local parse_failures = 0 ---@param raw_spec string local function handle_spec(raw_spec) local ok, result = pcall(vim.json.decode, raw_spec) if ok then specs[#specs + 1] = result else log.fmt_error("Failed to parse JSON, err=%s, json=%s", result, raw_spec) parse_failures = parse_failures + 1 end end try(spawn [yq]({ "-I0", -- output one document per line { "-o", "json" }, stdio_sink = process.StdioSink:new { stdout = function(chunk) local raw_spec = streaming_parser(chunk) if raw_spec then handle_spec(raw_spec) end end, stderr = function(chunk) stderr_buffer[#stderr_buffer + 1] = chunk end, }, on_spawn = a.scope(function(_, stdio) local stdin = stdio[1] for _, entry in ipairs(entries) do local contents = fs.async.read_file(path.concat { packages_dir, entry.name, "package.yaml" }) async_uv.write(stdin, contents) end async_uv.shutdown(stdin) async_uv.close(stdin) end), }) :map_err(function() return ("Failed to parse package YAML: %s"):format(table.concat(stderr_buffer, "")) end)) -- Exhaust parser coroutine. for raw_spec in function() return streaming_parser "" end do handle_spec(raw_spec) end if parse_failures > 0 then return Result.failure(("Failed to parse %d packages."):format(parse_failures)) end return specs end) :on_success(function(specs) self:reload(specs) end) :on_failure(function(err) log.fmt_error("Failed to install registry %s. %s", self, err) end) end function FileRegistrySource:get_display_name() if self:is_installed() then return ("local: %s"):format(self.spec.path) else return ("local: %s [uninstalled]"):format(self.spec.path) end end function FileRegistrySource:serialize() return { proto = "file", path = self.id, } end ---@param other FileRegistrySource function FileRegistrySource:is_same_location(other) return vim.fn.expand(self.spec.path) == vim.fn.expand(other.spec.path) end function FileRegistrySource:__tostring() return ("FileRegistrySource(path=%s)"):format(self.spec.path) end return FileRegistrySource ================================================ FILE: lua/mason-registry/sources/github.lua ================================================ local InstallLocation = require "mason-core.installer.InstallLocation" local Result = require "mason-core.result" local _ = require "mason-core.functional" local fetch = require "mason-core.fetch" local fs = require "mason-core.fs" local log = require "mason-core.log" local path = require "mason-core.path" local providers = require "mason-core.providers" local settings = require "mason.settings" local util = require "mason-registry.sources.util" -- Parse sha256sum text output to a table structure local parse_checksums = _.compose(_.from_pairs, _.map(_.compose(_.reverse, _.split " ")), _.split "\n", _.trim) ---@class GitHubRegistrySourceSpec ---@field id string ---@field namespace string ---@field name string ---@field version string? ---@class GitHubRegistrySource : RegistrySource ---@field spec GitHubRegistrySourceSpec ---@field repo string ---@field root_dir string ---@field private data_file string ---@field private info_file string ---@field buffer table? local GitHubRegistrySource = {} GitHubRegistrySource.__index = GitHubRegistrySource ---@param spec GitHubRegistrySourceSpec function GitHubRegistrySource:new(spec) ---@type GitHubRegistrySource local instance = {} setmetatable(instance, GitHubRegistrySource) local root_dir = InstallLocation.global():registry(path.concat { "github", spec.namespace, spec.name }) instance.id = spec.id instance.spec = spec instance.repo = ("%s/%s"):format(spec.namespace, spec.name) instance.root_dir = root_dir instance.data_file = path.concat { root_dir, "registry.json" } instance.info_file = path.concat { root_dir, "info.json" } return instance end function GitHubRegistrySource:is_installed() return fs.sync.file_exists(self.data_file) and fs.sync.file_exists(self.info_file) end ---@return RegistryPackageSpec[] function GitHubRegistrySource:get_all_package_specs() if not self:is_installed() then return {} end local data = vim.json.decode(fs.sync.read_file(self.data_file)) --[[@as RegistryPackageSpec[] ]] return _.filter_map(util.map_registry_spec, data) end function GitHubRegistrySource:reload() if not self:is_installed() then return end self.buffer = _.compose(_.index_by(_.prop "name"), _.map(util.hydrate_package(self, self.buffer or {})))( self:get_all_package_specs() ) return self.buffer end function GitHubRegistrySource:get_buffer() return self.buffer or self:reload() or {} end ---@param pkg string ---@return Package? function GitHubRegistrySource:get_package(pkg) return self:get_buffer()[pkg] end function GitHubRegistrySource:get_all_package_names() return _.map(_.prop "name", self:get_all_package_specs()) end ---@async function GitHubRegistrySource:install() local zzlib = require "mason-vendor.zzlib" return Result.try(function(try) local version = self.spec.version if self:is_installed() and self:get_info().version == version then -- Fixed version is already installed - nothing to update return end if not fs.async.dir_exists(self.root_dir) then log.debug("Creating registry directory", self) try(Result.pcall(fs.async.mkdirp, self.root_dir)) end if version == nil then log.trace("Resolving latest version for registry", self) ---@type GitHubRelease local release = try( providers.github .get_latest_release(self.repo) :map_err(_.always "Failed to fetch latest registry version from GitHub API.") ) version = release.tag_name log.trace("Resolved latest registry version", self, version) end local zip_file = path.concat { self.root_dir, "registry.json.zip" } try(fetch(settings.current.github.download_url_template:format(self.repo, version, "registry.json.zip"), { out_file = zip_file, }):map_err(_.always "Failed to download registry archive.")) local zip_buffer = fs.async.read_file(zip_file) local registry_contents = try( Result.pcall(zzlib.unzip, zip_buffer, "registry.json") :on_failure(_.partial(log.error, "Failed to unpack registry archive.")) :map_err(_.always "Failed to unpack registry archive.") ) pcall(fs.async.unlink, zip_file) local checksums = try( fetch(settings.current.github.download_url_template:format(self.repo, version, "checksums.txt")):map_err( _.always "Failed to download checksums.txt." ) ) try(Result.pcall(fs.async.write_file, self.data_file, registry_contents)) try(Result.pcall( fs.async.write_file, self.info_file, vim.json.encode { checksums = parse_checksums(checksums), version = version, download_timestamp = os.time(), } )) end) :on_success(function() self:reload() end) :on_failure(function(err) log.fmt_error("Failed to install registry %s. %s", self, err) end) end ---@return { checksums: table, version: string, download_timestamp: integer } function GitHubRegistrySource:get_info() return vim.json.decode(fs.sync.read_file(self.info_file)) end function GitHubRegistrySource:get_display_name() if self:is_installed() then local info = self:get_info() return ("github.com/%s version: %s"):format(self.repo, info.version) else return ("github.com/%s [uninstalled]"):format(self.repo) end end function GitHubRegistrySource:serialize() local info = self:get_info() return { proto = "github", namespace = self.spec.namespace, name = self.spec.name, version = info.version, checksums = info.checksums, } end ---@param other GitHubRegistrySource function GitHubRegistrySource:is_same_location(other) return self.spec.namespace == other.spec.namespace and self.spec.name == other.spec.name end function GitHubRegistrySource:__tostring() if self.spec.version then return ("GitHubRegistrySource(repo=%s, version=%s)"):format(self.repo, self.spec.version) else return ("GitHubRegistrySource(repo=%s)"):format(self.repo) end end return GitHubRegistrySource ================================================ FILE: lua/mason-registry/sources/init.lua ================================================ local log = require "mason-core.log" ---@class RegistrySource ---@field id string ---@field get_package fun(self: RegistrySource, pkg_name: string): Package? ---@field get_all_package_names fun(self: RegistrySource): string[] ---@field get_all_package_specs fun(self: RegistrySource): RegistryPackageSpec[] ---@field get_display_name fun(self: RegistrySource): string ---@field is_installed fun(self: RegistrySource): boolean ---@field install fun(self: RegistrySource): Result ---@field serialize fun(self: RegistrySource): InstallReceiptRegistry ---@field is_same_location fun(self: RegistrySource, other: RegistrySource): boolean ---@alias RegistrySourceType '"github"' | '"lua"' | '"file"' | '"synthesized"' ---@class LazySource ---@field type RegistrySourceType ---@field id string ---@field init fun(id: string): RegistrySource local LazySource = {} LazySource.__index = LazySource ---@param id string function LazySource.GitHub(id) local namespace, name = id:match "^(.+)/(.+)$" if not namespace or not name then error(("Failed to parse repository from GitHub registry: %q"):format(id), 0) end local name, version = unpack(vim.split(name, "@")) local GitHubRegistrySource = require "mason-registry.sources.github" return GitHubRegistrySource:new { id = id, namespace = namespace, name = name, version = version, } end ---@param id string function LazySource.Lua(id) local LuaRegistrySource = require "mason-registry.sources.lua" return LuaRegistrySource:new { id = id, mod = id, } end ---@param id string function LazySource.File(id) local FileRegistrySource = require "mason-registry.sources.file" return FileRegistrySource:new { id = id, path = id, } end function LazySource.Synthesized() local SynthesizedSource = require "mason-registry.sources.synthesized" return SynthesizedSource:new() end ---@param type RegistrySourceType ---@param id string ---@param init fun(id: string): RegistrySource function LazySource:new(type, id, init) local instance = setmetatable({}, self) instance.type = type instance.id = id instance.init = init return instance end function LazySource:get() if not self.instance then self.instance = self.init(self.id) end return self.instance end ---@param other LazySource function LazySource:is_same_location(other) if self.type == other.type then return self:get():is_same_location(other:get()) end return false end function LazySource:get_full_id() return ("%s:%s"):format(self.type, self.id) end function LazySource:__tostring() return ("LazySource(type=%s, id=%s)"):format(self.type, self.id) end ---@param str string local function split_once_left(str, char) for i = 1, #str do if str:sub(i, i) == char then local segment = str:sub(1, i - 1) return segment, str:sub(i + 1) end end return str end ---@param registry_id string local function parse(registry_id) local type, id = split_once_left(registry_id, ":") assert(id, ("Malformed registry %q"):format(registry_id)) if type == "github" then return LazySource:new(type, id, LazySource.GitHub) elseif type == "lua" then return LazySource:new(type, id, LazySource.Lua) elseif type == "file" then return LazySource:new(type, id, LazySource.File) end error(("Unknown registry type: %s"):format(type)) end ---@class LazySourceCollection ---@field list LazySource[] ---@field synthesized LazySource local LazySourceCollection = {} LazySourceCollection.__index = LazySourceCollection ---@return LazySourceCollection function LazySourceCollection:new() local instance = {} setmetatable(instance, self) instance.list = {} instance.synthesized = LazySource:new("synthesized", "synthesized", LazySource.Synthesized) return instance end ---@param registry string function LazySourceCollection:append(registry) self:unique_insert(parse(registry)) return self end ---@param registry string function LazySourceCollection:prepend(registry) self:unique_insert(parse(registry), 1) return self end ---@param source LazySource ---@param idx? integer function LazySourceCollection:unique_insert(source, idx) idx = idx or #self.list + 1 if idx > 1 then for i = 1, math.min(idx, #self.list) do if self.list[i]:is_same_location(source) then log.fmt_warn( "Ignoring duplicate registry entry %s (duplicate of %s)", source:get_full_id(), self.list[i]:get_full_id() ) return end end end for i = #self.list, idx, -1 do if self.list[i]:is_same_location(source) then table.remove(self.list, i) end end table.insert(self.list, idx, source) end function LazySourceCollection:is_all_installed() for source in self:iterate { include_uninstalled = true } do if not source:is_installed() then return false end end return true end function LazySourceCollection:checksum() ---@type string[] local registry_ids = vim.tbl_map( ---@param source LazySource function(source) return source.id end, self.list ) table.sort(registry_ids) return vim.fn.sha256(table.concat(registry_ids, "")) end ---@alias LazySourceCollectionIterate { include_uninstalled?: boolean, include_synthesized?: boolean } ---@param opts? LazySourceCollectionIterate function LazySourceCollection:iterate(opts) opts = opts or {} local idx = 1 return function() while idx <= #self.list do local source = self.list[idx]:get() idx = idx + 1 if opts.include_uninstalled or source:is_installed() then return source end end -- We've exhausted the true registry sources, fall back to the synthesized registry source. if idx == #self.list + 1 and opts.include_synthesized ~= false then idx = idx + 1 return self.synthesized:get() end end end ---@param opts? LazySourceCollectionIterate function LazySourceCollection:to_list(opts) opts = opts or {} local list = {} for source in self:iterate(opts) do table.insert(list, source) end return list end ---@param idx integer function LazySourceCollection:get(idx) return self.list[idx] end function LazySourceCollection:size() return #self.list end function LazySourceCollection:__tostring() return ("LazySourceCollection(list={%s})"):format(table.concat(vim.tbl_map(tostring, self.list), ", ")) end return LazySourceCollection ================================================ FILE: lua/mason-registry/sources/lua.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local util = require "mason-registry.sources.util" ---@class LuaRegistrySourceSpec ---@field id string ---@field mod string ---@class LuaRegistrySource : RegistrySource ---@field private spec LuaRegistrySourceSpec ---@field buffer { specs: RegistryPackageSpec[], instances: table }? local LuaRegistrySource = {} LuaRegistrySource.__index = LuaRegistrySource ---@param spec LuaRegistrySourceSpec function LuaRegistrySource:new(spec) ---@type LuaRegistrySource local instance = {} setmetatable(instance, LuaRegistrySource) instance.id = spec.id instance.spec = spec return instance end ---@param pkg_name string ---@return Package? function LuaRegistrySource:get_package(pkg_name) return self:get_buffer().instances[pkg_name] end ---@param specs RegistryPackageSpec[] function LuaRegistrySource:reload(specs) self.buffer = _.assoc("specs", specs, self.buffer or {}) self.buffer.instances = _.compose( _.index_by(_.prop "name"), _.map(util.hydrate_package(self, self.buffer.instances or {})) )(self:get_all_package_specs()) return self.buffer end function LuaRegistrySource:install() return Result.try(function(try) local index = try(Result.pcall(require, self.spec.mod)) ---@type RegistryPackageSpec[] local specs = {} for _, mod in pairs(index) do table.insert(specs, try(Result.pcall(require, mod))) end try(Result.pcall(self.reload, self, specs)) end) end ---@return string[] function LuaRegistrySource:get_all_package_names() return _.map(_.prop "name", self:get_all_package_specs()) end ---@return RegistryPackageSpec[] function LuaRegistrySource:get_all_package_specs() return _.filter_map(util.map_registry_spec, self:get_buffer().specs) end function LuaRegistrySource:get_buffer() return self.buffer or { specs = {}, instances = {}, } end function LuaRegistrySource:is_installed() return self.buffer ~= nil end function LuaRegistrySource:get_display_name() if self:is_installed() then return ("require(%q)"):format(self.spec.mod) else return ("require(%q) [uninstalled]"):format(self.spec.mod) end end function LuaRegistrySource:serialize() return { proto = "lua", mod = self.id, } end ---@param other LuaRegistrySource function LuaRegistrySource:is_same_location(other) return self.id == other.id end function LuaRegistrySource:__tostring() return ("LuaRegistrySource(mod=%s)"):format(self.spec.mod) end return LuaRegistrySource ================================================ FILE: lua/mason-registry/sources/synthesized.lua ================================================ local Package = require "mason-core.package" local Result = require "mason-core.result" local _ = require "mason-core.functional" local InstallReceipt = require("mason-core.receipt").InstallReceipt local InstallLocation = require "mason-core.installer.InstallLocation" local fs = require "mason-core.fs" local log = require "mason-core.log" ---@class SynthesizedRegistrySource : RegistrySource ---@field buffer table local SynthesizedRegistrySource = {} SynthesizedRegistrySource.__index = SynthesizedRegistrySource function SynthesizedRegistrySource:new() ---@type SynthesizedRegistrySource local instance = {} setmetatable(instance, self) instance.buffer = {} return instance end function SynthesizedRegistrySource:is_installed() return true end ---@return RegistryPackageSpec[] function SynthesizedRegistrySource:get_all_package_specs() return {} end ---@param pkg_name string ---@param receipt InstallReceipt ---@return Package function SynthesizedRegistrySource:load_package(pkg_name, receipt) local installed_version = receipt:get_installed_package_version() local source = { id = ("pkg:mason/%s@%s"):format(pkg_name, installed_version or "N%2FA"), -- N%2FA = N/A install = function() error("This package can no longer be installed because it has been removed from the registry.", 0) end, } ---@type RegistryPackageSpec local spec = { schema = "registry+v1", name = pkg_name, description = "", categories = {}, languages = {}, homepage = "", licenses = {}, deprecation = { since = installed_version or "N/A", message = "This package has been removed from the registry.", }, source = source, } local existing_pkg = self.buffer[pkg_name] if existing_pkg then existing_pkg:update(spec, self) return existing_pkg else local pkg = Package:new(spec, self) self.buffer[pkg_name] = pkg return pkg end end ---@param pkg_name string ---@return Package? function SynthesizedRegistrySource:get_package(pkg_name) local receipt_path = InstallLocation.global():receipt(pkg_name) if fs.sync.file_exists(receipt_path) then local ok, receipt_json = pcall(vim.json.decode, fs.sync.read_file(receipt_path)) if ok then local receipt = InstallReceipt.from_json(receipt_json) return self:load_package(pkg_name, receipt) else log.error("Failed to decode package receipt", pkg_name, receipt_json) end end end function SynthesizedRegistrySource:get_all_package_names() return vim.tbl_keys(self.buffer) end ---@async function SynthesizedRegistrySource:install() return Result.success() end function SynthesizedRegistrySource:get_display_name() return "SynthesizedRegistrySource" end function SynthesizedRegistrySource:serialize() return {} end ---@param other SynthesizedRegistrySource function SynthesizedRegistrySource:is_same_location(other) return true end function SynthesizedRegistrySource:__tostring() return "SynthesizedRegistrySource" end return SynthesizedRegistrySource ================================================ FILE: lua/mason-registry/sources/util.lua ================================================ local Optional = require "mason-core.optional" local Package = require "mason-core.package" local _ = require "mason-core.functional" local compiler = require "mason-core.installer.compiler" local log = require "mason-core.log" local M = {} ---@param spec RegistryPackageSpec function M.map_registry_spec(spec) spec.schema = spec.schema or "registry+v1" if not compiler.SCHEMA_CAP[spec.schema] then log.fmt_debug("Excluding package=%s with unsupported schema_version=%s", spec.name, spec.schema) return Optional.empty() end return Optional.of(spec) end ---@param registry RegistrySource ---@param buffer table ---@param spec RegistryPackageSpec M.hydrate_package = _.curryN(function(registry, buffer, spec) -- hydrate Pkg.Lang/License index _.each(function(lang) local _ = Package.Lang[lang] end, spec.languages) _.each(function(lang) local _ = Package.License[lang] end, spec.licenses) local existing_instance = buffer[spec.name] if existing_instance then -- Apply spec to the existing Package instances. This is important as to not have lingering package instances. existing_instance:update(spec, registry) return existing_instance end local new_instance = Package:new(spec, registry) return new_instance end, 3) return M ================================================ FILE: lua/mason-test/helpers.lua ================================================ local InstallContext = require "mason-core.installer.context" local InstallHandle = require "mason-core.installer.InstallHandle" local InstallLocation = require "mason-core.installer.InstallLocation" local Result = require "mason-core.result" local a = require "mason-core.async" local registry = require "mason-registry" local spy = require "luassert.spy" local M = {} ---@param opts? { install_opts?: PackageInstallOpts, package?: string } function M.create_context(opts) local pkg = registry.get_package(opts and opts.package or "dummy") local handle = InstallHandle:new(pkg, InstallLocation.global()) local context = InstallContext:new(handle, opts and opts.install_opts or {}) context.spawn = setmetatable({}, { __index = function(s, cmd) s[cmd] = spy.new(function() return Result.success { stdout = nil, stderr = nil } end) return s[cmd] end, }) context.cwd:initialize():get_or_throw() return context end ---@param pkg AbstractPackage ---@param opts? PackageInstallOpts function M.sync_install(pkg, opts) return a.run_blocking(function() return a.wait(function(resolve, reject) pkg:install(opts, function(success, result) (success and resolve or reject)(result) end) end) end) end ---@param pkg AbstractPackage ---@param opts? PackageUninstallOpts function M.sync_uninstall(pkg, opts) return a.run_blocking(function() return a.wait(function(resolve, reject) pkg:uninstall(opts, function(success, result) (success and resolve or reject)(result) end) end) end) end ---@param runner InstallRunner ---@param opts PackageInstallOpts function M.sync_runner_execute(runner, opts) local callback = spy.new() runner:execute(opts, callback) assert.wait(function() assert.spy(callback).was_called() end) return callback end return M ================================================ FILE: lua/mason-vendor/semver.lua ================================================ -- stylua: ignore start local semver = { _VERSION = '1.2.1', _DESCRIPTION = 'semver for Lua', _URL = 'https://github.com/kikito/semver.lua', _LICENSE = [[ MIT LICENSE Copyright (c) 2015 Enrique García Cota Permission is hereby granted, free of charge, to any person obtaining a copy of tother software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and tother permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] } local function checkPositiveInteger(number, name) assert(number >= 0, name .. ' must be a valid positive number') assert(math.floor(number) == number, name .. ' must be an integer') end local function present(value) return value and value ~= '' end -- splitByDot("a.bbc.d") == {"a", "bbc", "d"} local function splitByDot(str) str = str or "" local t, count = {}, 0 str:gsub("([^%.]+)", function(c) count = count + 1 t[count] = c end) return t end local function parsePrereleaseAndBuildWithSign(str) local prereleaseWithSign, buildWithSign = str:match("^(-[^+]+)(+.+)$") if not (prereleaseWithSign and buildWithSign) then prereleaseWithSign = str:match("^(-.+)$") buildWithSign = str:match("^(+.+)$") end assert(prereleaseWithSign or buildWithSign, ("The parameter %q must begin with + or - to denote a prerelease or a build"):format(str)) return prereleaseWithSign, buildWithSign end local function parsePrerelease(prereleaseWithSign) if prereleaseWithSign then local prerelease = prereleaseWithSign:match("^-(%w[%.%w-]*)$") assert(prerelease, ("The prerelease %q is not a slash followed by alphanumerics, dots and slashes"):format(prereleaseWithSign)) return prerelease end end local function parseBuild(buildWithSign) if buildWithSign then local build = buildWithSign:match("^%+(%w[%.%w-]*)$") assert(build, ("The build %q is not a + sign followed by alphanumerics, dots and slashes"):format(buildWithSign)) return build end end local function parsePrereleaseAndBuild(str) if not present(str) then return nil, nil end local prereleaseWithSign, buildWithSign = parsePrereleaseAndBuildWithSign(str) local prerelease = parsePrerelease(prereleaseWithSign) local build = parseBuild(buildWithSign) return prerelease, build end local function parseVersion(str) local sMajor, sMinor, sPatch, sPrereleaseAndBuild = str:match("^(%d+)%.?(%d*)%.?(%d*)(.-)$") assert(type(sMajor) == 'string', ("Could not extract version number(s) from %q"):format(str)) local major, minor, patch = tonumber(sMajor), tonumber(sMinor), tonumber(sPatch) local prerelease, build = parsePrereleaseAndBuild(sPrereleaseAndBuild) return major, minor, patch, prerelease, build end -- return 0 if a == b, -1 if a < b, and 1 if a > b local function compare(a,b) return a == b and 0 or a < b and -1 or 1 end local function compareIds(myId, otherId) if myId == otherId then return 0 elseif not myId then return -1 elseif not otherId then return 1 end local selfNumber, otherNumber = tonumber(myId), tonumber(otherId) if selfNumber and otherNumber then -- numerical comparison return compare(selfNumber, otherNumber) -- numericals are always smaller than alphanums elseif selfNumber then return -1 elseif otherNumber then return 1 else return compare(myId, otherId) -- alphanumerical comparison end end local function smallerIdList(myIds, otherIds) local myLength = #myIds local comparison for i=1, myLength do comparison = compareIds(myIds[i], otherIds[i]) if comparison ~= 0 then return comparison == -1 end -- if comparison == 0, continue loop end return myLength < #otherIds end local function smallerPrerelease(mine, other) if mine == other or not mine then return false elseif not other then return true end return smallerIdList(splitByDot(mine), splitByDot(other)) end ---@class ISemver local methods = {} ---@return Semver function methods:nextMajor() return semver(self.major + 1, 0, 0) end ---@return Semver function methods:nextMinor() return semver(self.major, self.minor + 1, 0) end ---@return Semver function methods:nextPatch() return semver(self.major, self.minor, self.patch + 1) end ---@class Semver : ISemver ---@field major integer ---@field minor integer ---@field patch integer ---@field prerelease? string ---@field build? string local mt = { __index = methods } function mt:__eq(other) return self.major == other.major and self.minor == other.minor and self.patch == other.patch and self.prerelease == other.prerelease -- notice that build is ignored for precedence in semver 2.0.0 end function mt:__lt(other) if self.major ~= other.major then return self.major < other.major end if self.minor ~= other.minor then return self.minor < other.minor end if self.patch ~= other.patch then return self.patch < other.patch end return smallerPrerelease(self.prerelease, other.prerelease) -- notice that build is ignored for precedence in semver 2.0.0 end -- This works like the "pessimisstic operator" in Rubygems. -- if a and b are versions, a ^ b means "b is backwards-compatible with a" -- in other words, "it's safe to upgrade from a to b" function mt:__pow(other) if self.major == 0 then return self == other end return self.major == other.major and self.minor <= other.minor end function mt:__tostring() local buffer = { ("%d.%d.%d"):format(self.major, self.minor, self.patch) } if self.prerelease then table.insert(buffer, "-" .. self.prerelease) end if self.build then table.insert(buffer, "+" .. self.build) end return table.concat(buffer) end ---@return Semver local function new(major, minor, patch, prerelease, build) assert(major, "At least one parameter is needed") if type(major) == 'string' then major,minor,patch,prerelease,build = parseVersion(major) end patch = patch or 0 minor = minor or 0 checkPositiveInteger(major, "major") checkPositiveInteger(minor, "minor") checkPositiveInteger(patch, "patch") local result = {major=major, minor=minor, patch=patch, prerelease=prerelease, build=build} return setmetatable(result, mt) end setmetatable(semver, { __call = function(_, ...) return new(...) end }) semver._VERSION= semver(semver._VERSION) return semver ================================================ FILE: lua/mason-vendor/zzlib/inflate-bit32.lua ================================================ -- stylua: ignore start -- zzlib-bit32 - zlib decompression in Lua - version using bit/bit32 libraries -- Copyright (c) 2016-2023 Francois Galea -- This program is free software. It comes without any warranty, to -- the extent permitted by applicable law. You can redistribute it -- and/or modify it under the terms of the Do What The Fuck You Want -- To Public License, Version 2, as published by Sam Hocevar. See -- the COPYING file or http://www.wtfpl.net/ for more details. local inflate = {} local bit = bit32 or bit inflate.band = bit.band inflate.rshift = bit.rshift function inflate.bitstream_init(file) local bs = { file = file, -- the open file handle buf = nil, -- character buffer len = nil, -- length of character buffer pos = 1, -- position in char buffer, next to be read b = 0, -- bit buffer n = 0, -- number of bits in buffer } -- get rid of n first bits function bs:flushb(n) self.n = self.n - n self.b = bit.rshift(self.b,n) end -- returns the next byte from the stream, excluding any half-read bytes function bs:next_byte() if self.pos > self.len then self.buf = self.file:read(4096) self.len = self.buf:len() self.pos = 1 end local pos = self.pos self.pos = pos + 1 return self.buf:byte(pos) end -- peek a number of n bits from stream function bs:peekb(n) while self.n < n do self.b = self.b + bit.lshift(self:next_byte(),self.n) self.n = self.n + 8 end return bit.band(self.b,bit.lshift(1,n)-1) end -- get a number of n bits from stream function bs:getb(n) local ret = bs:peekb(n) self.n = self.n - n self.b = bit.rshift(self.b,n) return ret end -- get next variable-size of maximum size=n element from stream, according to Huffman table function bs:getv(hufftable,n) local e = hufftable[bs:peekb(n)] local len = bit.band(e,15) local ret = bit.rshift(e,4) self.n = self.n - len self.b = bit.rshift(self.b,len) return ret end function bs:close() if self.file then self.file:close() end end if type(file) == "string" then bs.file = nil bs.buf = file else bs.buf = file:read(4096) end bs.len = bs.buf:len() return bs end local function hufftable_create(depths) local nvalues = #depths local nbits = 1 local bl_count = {} local next_code = {} for i=1,nvalues do local d = depths[i] if d > nbits then nbits = d end bl_count[d] = (bl_count[d] or 0) + 1 end local table = {} local code = 0 bl_count[0] = 0 for i=1,nbits do code = (code + (bl_count[i-1] or 0)) * 2 next_code[i] = code end for i=1,nvalues do local len = depths[i] or 0 if len > 0 then local e = (i-1)*16 + len local code = next_code[len] local rcode = 0 for j=1,len do rcode = rcode + bit.lshift(bit.band(1,bit.rshift(code,j-1)),len-j) end for j=0,2^nbits-1,2^len do table[j+rcode] = e end next_code[len] = next_code[len] + 1 end end return table,nbits end local function block_loop(out,bs,nlit,ndist,littable,disttable) local lit repeat lit = bs:getv(littable,nlit) if lit < 256 then table.insert(out,lit) elseif lit > 256 then local nbits = 0 local size = 3 local dist = 1 if lit < 265 then size = size + lit - 257 elseif lit < 285 then nbits = bit.rshift(lit-261,2) size = size + bit.lshift(bit.band(lit-261,3)+4,nbits) else size = 258 end if nbits > 0 then size = size + bs:getb(nbits) end local v = bs:getv(disttable,ndist) if v < 4 then dist = dist + v else nbits = bit.rshift(v-2,1) dist = dist + bit.lshift(bit.band(v,1)+2,nbits) dist = dist + bs:getb(nbits) end local p = #out-dist+1 while size > 0 do table.insert(out,out[p]) p = p + 1 size = size - 1 end end until lit == 256 end local function block_dynamic(out,bs) local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 } local hlit = 257 + bs:getb(5) local hdist = 1 + bs:getb(5) local hclen = 4 + bs:getb(4) local depths = {} for i=1,hclen do local v = bs:getb(3) depths[order[i]] = v end for i=hclen+1,19 do depths[order[i]] = 0 end local lengthtable,nlen = hufftable_create(depths) local i=1 while i<=hlit+hdist do local v = bs:getv(lengthtable,nlen) if v < 16 then depths[i] = v i = i + 1 elseif v < 19 then local nbt = {2,3,7} local nb = nbt[v-15] local c = 0 local n = 3 + bs:getb(nb) if v == 16 then c = depths[i-1] elseif v == 18 then n = n + 8 end for j=1,n do depths[i] = c i = i + 1 end else error("wrong entry in depth table for literal/length alphabet: "..v); end end local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end local littable,nlit = hufftable_create(litdepths) local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end local disttable,ndist = hufftable_create(distdepths) block_loop(out,bs,nlit,ndist,littable,disttable) end local function block_static(out,bs) local cnt = { 144, 112, 24, 8 } local dpt = { 8, 9, 7, 8 } local depths = {} for i=1,4 do local d = dpt[i] for j=1,cnt[i] do table.insert(depths,d) end end local littable,nlit = hufftable_create(depths) depths = {} for i=1,32 do depths[i] = 5 end local disttable,ndist = hufftable_create(depths) block_loop(out,bs,nlit,ndist,littable,disttable) end local function block_uncompressed(out,bs) bs:flushb(bit.band(bs.n,7)) local len = bs:getb(16) if bs.n > 0 then error("Unexpected.. should be zero remaining bits in buffer.") end local nlen = bs:getb(16) if bit.bxor(len,nlen) ~= 65535 then error("LEN and NLEN don't match") end for i=1,len do table.insert(out,bs:next_byte()) end end function inflate.main(bs) local last,type local output = {} repeat local block last = bs:getb(1) type = bs:getb(2) if type == 0 then block_uncompressed(output,bs) elseif type == 1 then block_static(output,bs) elseif type == 2 then block_dynamic(output,bs) else error("unsupported block type") end until last == 1 bs:flushb(bit.band(bs.n,7)) return output end local crc32_table function inflate.crc32(s,crc) if not crc32_table then crc32_table = {} for i=0,255 do local r=i for j=1,8 do r = bit.bxor(bit.rshift(r,1),bit.band(0xedb88320,bit.bnot(bit.band(r,1)-1))) end crc32_table[i] = r end end crc = bit.bnot(crc or 0) for i=1,#s do local c = s:byte(i) crc = bit.bxor(crc32_table[bit.bxor(c,bit.band(crc,0xff))],bit.rshift(crc,8)) end crc = bit.bnot(crc) if crc<0 then -- in Lua < 5.2, sign extension was performed crc = crc + 4294967296 end return crc end return inflate ================================================ FILE: lua/mason-vendor/zzlib/inflate-bwo.lua ================================================ -- stylua: ignore start -- zzlib - zlib decompression in Lua - version using Lua 5.3 bitwise operators -- Copyright (c) 2016-2023 Francois Galea -- This program is free software. It comes without any warranty, to -- the extent permitted by applicable law. You can redistribute it -- and/or modify it under the terms of the Do What The Fuck You Want -- To Public License, Version 2, as published by Sam Hocevar. See -- the COPYING file or http://www.wtfpl.net/ for more details. local inflate = {} function inflate.band(x,y) return x & y end function inflate.rshift(x,y) return x >> y end function inflate.bitstream_init(file) local bs = { file = file, -- the open file handle buf = nil, -- character buffer len = nil, -- length of character buffer pos = 1, -- position in char buffer, next to be read b = 0, -- bit buffer n = 0, -- number of bits in buffer } -- get rid of n first bits function bs:flushb(n) self.n = self.n - n self.b = self.b >> n end -- returns the next byte from the stream, excluding any half-read bytes function bs:next_byte() if self.pos > self.len then self.buf = self.file:read(4096) self.len = self.buf:len() self.pos = 1 end local pos = self.pos self.pos = pos + 1 return self.buf:byte(pos) end -- peek a number of n bits from stream function bs:peekb(n) while self.n < n do self.b = self.b + (self:next_byte()<> n return ret end -- get next variable-size of maximum size=n element from stream, according to Huffman table function bs:getv(hufftable,n) local e = hufftable[bs:peekb(n)] local len = e & 15 local ret = e >> 4 self.n = self.n - len self.b = self.b >> len return ret end function bs:close() if self.file then self.file:close() end end if type(file) == "string" then bs.file = nil bs.buf = file else bs.buf = file:read(4096) end bs.len = bs.buf:len() return bs end local function hufftable_create(depths) local nvalues = #depths local nbits = 1 local bl_count = {} local next_code = {} for i=1,nvalues do local d = depths[i] if d > nbits then nbits = d end bl_count[d] = (bl_count[d] or 0) + 1 end local table = {} local code = 0 bl_count[0] = 0 for i=1,nbits do code = (code + (bl_count[i-1] or 0)) * 2 next_code[i] = code end for i=1,nvalues do local len = depths[i] or 0 if len > 0 then local e = (i-1)*16 + len local code = next_code[len] local rcode = 0 for j=1,len do rcode = rcode + ((1&(code>>(j-1))) << (len-j)) end for j=0,2^nbits-1,2^len do table[j+rcode] = e end next_code[len] = next_code[len] + 1 end end return table,nbits end local function block_loop(out,bs,nlit,ndist,littable,disttable) local lit repeat lit = bs:getv(littable,nlit) if lit < 256 then table.insert(out,lit) elseif lit > 256 then local nbits = 0 local size = 3 local dist = 1 if lit < 265 then size = size + lit - 257 elseif lit < 285 then nbits = (lit-261) >> 2 size = size + ((((lit-261)&3)+4) << nbits) else size = 258 end if nbits > 0 then size = size + bs:getb(nbits) end local v = bs:getv(disttable,ndist) if v < 4 then dist = dist + v else nbits = (v-2) >> 1 dist = dist + (((v&1)+2) << nbits) dist = dist + bs:getb(nbits) end local p = #out-dist+1 while size > 0 do table.insert(out,out[p]) p = p + 1 size = size - 1 end end until lit == 256 end local function block_dynamic(out,bs) local order = { 17, 18, 19, 1, 9, 8, 10, 7, 11, 6, 12, 5, 13, 4, 14, 3, 15, 2, 16 } local hlit = 257 + bs:getb(5) local hdist = 1 + bs:getb(5) local hclen = 4 + bs:getb(4) local depths = {} for i=1,hclen do local v = bs:getb(3) depths[order[i]] = v end for i=hclen+1,19 do depths[order[i]] = 0 end local lengthtable,nlen = hufftable_create(depths) local i=1 while i<=hlit+hdist do local v = bs:getv(lengthtable,nlen) if v < 16 then depths[i] = v i = i + 1 elseif v < 19 then local nbt = {2,3,7} local nb = nbt[v-15] local c = 0 local n = 3 + bs:getb(nb) if v == 16 then c = depths[i-1] elseif v == 18 then n = n + 8 end for j=1,n do depths[i] = c i = i + 1 end else error("wrong entry in depth table for literal/length alphabet: "..v); end end local litdepths = {} for i=1,hlit do table.insert(litdepths,depths[i]) end local littable,nlit = hufftable_create(litdepths) local distdepths = {} for i=hlit+1,#depths do table.insert(distdepths,depths[i]) end local disttable,ndist = hufftable_create(distdepths) block_loop(out,bs,nlit,ndist,littable,disttable) end local function block_static(out,bs) local cnt = { 144, 112, 24, 8 } local dpt = { 8, 9, 7, 8 } local depths = {} for i=1,4 do local d = dpt[i] for j=1,cnt[i] do table.insert(depths,d) end end local littable,nlit = hufftable_create(depths) depths = {} for i=1,32 do depths[i] = 5 end local disttable,ndist = hufftable_create(depths) block_loop(out,bs,nlit,ndist,littable,disttable) end local function block_uncompressed(out,bs) bs:flushb(bs.n&7) local len = bs:getb(16) if bs.n > 0 then error("Unexpected.. should be zero remaining bits in buffer.") end local nlen = bs:getb(16) if len~nlen ~= 65535 then error("LEN and NLEN don't match") end for i=1,len do table.insert(out,bs:next_byte()) end end function inflate.main(bs) local last,type local output = {} repeat local block last = bs:getb(1) type = bs:getb(2) if type == 0 then block_uncompressed(output,bs) elseif type == 1 then block_static(output,bs) elseif type == 2 then block_dynamic(output,bs) else error("unsupported block type") end until last == 1 bs:flushb(bs.n&7) return output end local crc32_table function inflate.crc32(s,crc) if not crc32_table then crc32_table = {} for i=0,255 do local r=i for j=1,8 do r = (r >> 1) ~ (0xedb88320 & ~((r & 1) - 1)) end crc32_table[i] = r end end crc = (crc or 0) ~ 0xffffffff for i=1,#s do local c = s:byte(i) crc = crc32_table[c ~ (crc & 0xff)] ~ (crc >> 8) end crc = (crc or 0) ~ 0xffffffff if crc<0 then -- in Lua < 5.2, sign extension was performed crc = crc + 4294967296 end return crc end return inflate ================================================ FILE: lua/mason-vendor/zzlib/init.lua ================================================ -- stylua: ignore start -- zzlib - zlib decompression in Lua - Implementation-independent code -- Copyright (c) 2016-2023 Francois Galea -- This program is free software. It comes without any warranty, to -- the extent permitted by applicable law. You can redistribute it -- and/or modify it under the terms of the Do What The Fuck You Want -- To Public License, Version 2, as published by Sam Hocevar. See -- the COPYING file or http://www.wtfpl.net/ for more details. local unpack = table.unpack or unpack local infl local lua_version = tonumber(_VERSION:match("^Lua (.*)")) if not lua_version or lua_version < 5.3 then -- older version of Lua or Luajit being used - use bit/bit32-based implementation infl = require("mason-vendor.zzlib.inflate-bit32") else -- From Lua 5.3, use implementation based on bitwise operators infl = require("mason-vendor.zzlib.inflate-bwo") end local zzlib = {} local function arraytostr(array) local tmp = {} local size = #array local pos = 1 local imax = 1 while size > 0 do local bsize = size>=2048 and 2048 or size local s = string.char(unpack(array,pos,pos+bsize-1)) pos = pos + bsize size = size - bsize local i = 1 while tmp[i] do s = tmp[i]..s tmp[i] = nil i = i + 1 end if i > imax then imax = i end tmp[i] = s end local str = "" for i=1,imax do if tmp[i] then str = tmp[i]..str end end return str end local function inflate_gzip(bs) local id1,id2,cm,flg = bs.buf:byte(1,4) if id1 ~= 31 or id2 ~= 139 then error("invalid gzip header") end if cm ~= 8 then error("only deflate format is supported") end bs.pos=11 if infl.band(flg,4) ~= 0 then local xl1,xl2 = bs.buf.byte(bs.pos,bs.pos+1) local xlen = xl2*256+xl1 bs.pos = bs.pos+xlen+2 end if infl.band(flg,8) ~= 0 then local pos = bs.buf:find("\0",bs.pos) bs.pos = pos+1 end if infl.band(flg,16) ~= 0 then local pos = bs.buf:find("\0",bs.pos) bs.pos = pos+1 end if infl.band(flg,2) ~= 0 then -- TODO: check header CRC16 bs.pos = bs.pos+2 end local result = arraytostr(infl.main(bs)) local crc = bs:getb(8)+256*(bs:getb(8)+256*(bs:getb(8)+256*bs:getb(8))) bs:close() if crc ~= infl.crc32(result) then error("checksum verification failed") end return result end -- compute Adler-32 checksum local function adler32(s) local s1 = 1 local s2 = 0 for i=1,#s do local c = s:byte(i) s1 = (s1+c)%65521 s2 = (s2+s1)%65521 end return s2*65536+s1 end local function inflate_zlib(bs) local cmf = bs.buf:byte(1) local flg = bs.buf:byte(2) if (cmf*256+flg)%31 ~= 0 then error("zlib header check bits are incorrect") end if infl.band(cmf,15) ~= 8 then error("only deflate format is supported") end if infl.rshift(cmf,4) ~= 7 then error("unsupported window size") end if infl.band(flg,32) ~= 0 then error("preset dictionary not implemented") end bs.pos=3 local result = arraytostr(infl.main(bs)) local adler = ((bs:getb(8)*256+bs:getb(8))*256+bs:getb(8))*256+bs:getb(8) bs:close() if adler ~= adler32(result) then error("checksum verification failed") end return result end local function inflate_raw(buf,offset,crc) local bs = infl.bitstream_init(buf) bs.pos = offset local result = arraytostr(infl.main(bs)) if crc and crc ~= infl.crc32(result) then error("checksum verification failed") end return result end function zzlib.gunzipf(filename) local file,err = io.open(filename,"rb") if not file then return nil,err end return inflate_gzip(infl.bitstream_init(file)) end function zzlib.gunzip(str) return inflate_gzip(infl.bitstream_init(str)) end function zzlib.inflate(str) return inflate_zlib(infl.bitstream_init(str)) end local function int2le(str,pos) local a,b = str:byte(pos,pos+1) return b*256+a end local function int4le(str,pos) local a,b,c,d = str:byte(pos,pos+3) return ((d*256+c)*256+b)*256+a end local function nextfile(buf,p) if int4le(buf,p) ~= 0x02014b50 then -- end of central directory list return end -- local flag = int2le(buf,p+8) local packed = int2le(buf,p+10)~=0 local crc = int4le(buf,p+16) local namelen = int2le(buf,p+28) local name = buf:sub(p+46,p+45+namelen) local offset = int4le(buf,p+42)+1 p = p+46+namelen+int2le(buf,p+30)+int2le(buf,p+32) if int4le(buf,offset) ~= 0x04034b50 then error("invalid local header signature") end local size = int4le(buf,offset+18) local extlen = int2le(buf,offset+28) offset = offset+30+namelen+extlen return p,name,offset,size,packed,crc end function zzlib.files(buf) local p = #buf-21 if int4le(buf,p) ~= 0x06054b50 then -- not sure there is a reliable way to locate the end of central directory record -- if it has a variable sized comment field error(".ZIP file comments not supported") end local cdoffset = int4le(buf,p+16)+1 return nextfile,buf,cdoffset end function zzlib.unzip(buf,arg1,arg2) if type(arg1) == "number" then -- mode 1: unpack data from specified position in zip file return inflate_raw(buf,arg1,arg2) end -- mode 2: search and unpack file from zip file local filename = arg1 for _,name,offset,size,packed,crc in zzlib.files(buf) do if name == filename then local result if not packed then -- no compression result = buf:sub(offset,offset+size-1) else -- DEFLATE compression result = inflate_raw(buf,offset,crc) end return result end end error("file '"..filename.."' not found in ZIP archive") end return zzlib ================================================ FILE: selene.toml ================================================ std="lua51+vim" exclude = ["lua/mason-vendor/*"] [rules] unused_variable = "allow" shadowing = "allow" mixed_table = "allow" ================================================ FILE: stylua.toml ================================================ indent_type = "Spaces" call_parentheses = "None" [sort_requires] enabled = true ================================================ FILE: tests/fixtures/purl-test-suite-data.json ================================================ [ { "description": "valid maven purl", "purl": "pkg:maven/org.apache.commons/io@1.3.4", "canonical_purl": "pkg:maven/org.apache.commons/io@1.3.4", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": "1.3.4", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "basic valid maven purl without version", "purl": "pkg:maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid go purl without version and with subpath", "purl": "pkg:GOLANG/google.golang.org/genproto#/googleapis/api/annotations/", "canonical_purl": "pkg:golang/google.golang.org/genproto#googleapis/api/annotations", "type": "golang", "namespace": "google.golang.org", "name": "genproto", "version": null, "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "valid go purl with version and subpath", "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/api/annotations/", "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations", "type": "golang", "namespace": "google.golang.org", "name": "genproto", "version": "abcdedf", "qualifiers": null, "subpath": "googleapis/api/annotations", "is_invalid": false }, { "description": "bitbucket namespace and name should be lowercased", "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c", "canonical_purl": "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c", "type": "bitbucket", "namespace": "birkenfeld", "name": "pygments-main", "version": "244fd47e07d1014f0aed9c", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "github namespace and name should be lowercased", "purl": "pkg:github/Package-url/purl-Spec@244fd47e07d1004f0aed9c", "canonical_purl": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c", "type": "github", "namespace": "package-url", "name": "purl-spec", "version": "244fd47e07d1004f0aed9c", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "debian can use qualifiers", "purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie", "canonical_purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie", "type": "deb", "namespace": "debian", "name": "curl", "version": "7.50.3-1", "qualifiers": {"arch": "i386", "distro": "jessie"}, "subpath": null, "is_invalid": false }, { "description": "docker uses qualifiers and hash image id as versions", "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", "canonical_purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io", "type": "docker", "namespace": "customer", "name": "dockerimage", "version": "sha256:244fd47e07d1004f0aed9c", "qualifiers": {"repository_url": "gcr.io"}, "subpath": null, "is_invalid": false }, { "description": "Java gem can use a qualifier", "purl": "pkg:gem/jruby-launcher@1.1.2?Platform=java", "canonical_purl": "pkg:gem/jruby-launcher@1.1.2?platform=java", "type": "gem", "namespace": null, "name": "jruby-launcher", "version": "1.1.2", "qualifiers": {"platform": "java"}, "subpath": null, "is_invalid": false }, { "description": "maven often uses qualifiers", "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repositorY_url=repo.spring.io/release", "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", "version": "1.9.1", "qualifiers": {"classifier": "sources", "repository_url": "repo.spring.io/release"}, "subpath": null, "is_invalid": false }, { "description": "maven pom reference", "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repositorY_url=repo.spring.io/release", "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io/release", "type": "maven", "namespace": "org.apache.xmlgraphics", "name": "batik-anim", "version": "1.9.1", "qualifiers": {"extension": "pom", "repository_url": "repo.spring.io/release"}, "subpath": null, "is_invalid": false }, { "description": "maven can come with a type qualifier", "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll", "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll", "type": "maven", "namespace": "net.sf.jacob-project", "name": "jacob", "version": "1.14.3", "qualifiers": {"classifier": "x86", "type": "dll"}, "subpath": null, "is_invalid": false }, { "description": "npm can be scoped", "purl": "pkg:npm/%40angular/animation@12.3.1", "canonical_purl": "pkg:npm/%40angular/animation@12.3.1", "type": "npm", "namespace": "@angular", "name": "animation", "version": "12.3.1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "nuget names are case sensitive", "purl": "pkg:Nuget/EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "pkg:nuget/EnterpriseLibrary.Common@6.0.1304", "type": "nuget", "namespace": null, "name": "EnterpriseLibrary.Common", "version": "6.0.1304", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "pypi names have special rules and not case sensitive", "purl": "pkg:PYPI/Django_package@1.11.1.dev1", "canonical_purl": "pkg:pypi/django-package@1.11.1.dev1", "type": "pypi", "namespace": null, "name": "django-package", "version": "1.11.1.dev1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "rpm often use qualifiers", "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25", "canonical_purl": "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25", "type": "rpm", "namespace": "fedora", "name": "curl", "version": "7.50.3-1.fc25", "qualifiers": {"arch": "i386", "distro": "fedora-25"}, "subpath": null, "is_invalid": false }, { "description": "a scheme is always required", "purl": "EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "EnterpriseLibrary.Common@6.0.1304", "type": null, "namespace": null, "name": "EnterpriseLibrary.Common", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "a type is always required", "purl": "pkg:EnterpriseLibrary.Common@6.0.1304", "canonical_purl": "pkg:EnterpriseLibrary.Common@6.0.1304", "type": null, "namespace": null, "name": "EnterpriseLibrary.Common", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "a name is required", "purl": "pkg:maven/@1.3.4", "canonical_purl": "pkg:maven/@1.3.4", "type": "maven", "namespace": null, "name": null, "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "slash / after scheme is not significant", "purl": "pkg:/maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "double slash // after scheme is not significant", "purl": "pkg://maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "slash /// after type is not significant", "purl": "pkg:///maven/org.apache.commons/io", "canonical_purl": "pkg:maven/org.apache.commons/io", "type": "maven", "namespace": "org.apache.commons", "name": "io", "version": null, "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid maven purl with case sensitive namespace and name", "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3", "type": "maven", "namespace": "HTTPClient", "name": "HTTPClient", "version": "0.3-3", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid maven purl containing a space in the version and qualifier", "purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value", "canonical_purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value", "type": "maven", "namespace": "mygroup", "name": "myartifact", "version": "1.0.0 Final", "qualifiers": {"mykey": "my value"}, "subpath": null, "is_invalid": false }, { "description": "checks for invalid qualifier keys", "purl": "pkg:npm/myartifact@1.0.0?in%20production=true", "canonical_purl": null, "type": "npm", "namespace": null, "name": "myartifact", "version": "1.0.0", "qualifiers": {"in production": "true"}, "subpath": null, "is_invalid": true }, { "description": "valid conan purl", "purl": "pkg:conan/cctz@2.3", "canonical_purl": "pkg:conan/cctz@2.3", "type": "conan", "namespace": null, "name": "cctz", "version": "2.3", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "valid conan purl with namespace and qualifier channel", "purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable", "canonical_purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable", "type": "conan", "namespace": "bincrafters", "name": "cctz", "version": "2.3", "qualifiers": {"channel": "stable"}, "subpath": null, "is_invalid": false }, { "description": "invalid conan purl only namespace", "purl": "pkg:conan/bincrafters/cctz@2.3", "canonical_purl": "pkg:conan/bincrafters/cctz@2.3", "type": "conan", "namespace": "bincrafters", "name": "cctz", "version": "2.3", "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "invalid conan purl only channel qualifier", "purl": "pkg:conan/cctz@2.3?channel=stable", "canonical_purl": "pkg:conan/cctz@2.3?channel=stable", "type": "conan", "namespace": null, "name": "cctz", "version": "2.3", "qualifiers": {"channel": "stable"}, "subpath": null, "is_invalid": true }, { "description": "valid conda purl with qualifiers", "purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2", "canonical_purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2", "type": "conda", "namespace": null, "name": "absl-py", "version": "0.4.1", "qualifiers": {"build": "py36h06a4308_0", "channel": "main", "subdir": "linux-64", "type": "tar.bz2"}, "subpath": null, "is_invalid": false }, { "description": "valid cran purl", "purl": "pkg:cran/A3@0.9.1", "canonical_purl": "pkg:cran/A3@0.9.1", "type": "cran", "namespace": null, "name": "A3", "version": "0.9.1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "invalid cran purl without name", "purl": "pkg:cran/@0.9.1", "canonical_purl": "pkg:cran/@0.9.1", "type": "cran", "namespace": null, "name": null, "version": "0.9.1", "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "invalid cran purl without version", "purl": "pkg:cran/A3", "canonical_purl": "pkg:cran/A3", "type": "cran", "namespace": null, "name": "A3", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "valid swift purl", "purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3", "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3", "type": "swift", "namespace": "github.com/Alamofire", "name": "Alamofire", "version": "5.4.3", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "invalid swift purl without namespace", "purl": "pkg:swift/Alamofire@5.4.3", "canonical_purl": "pkg:swift/Alamofire@5.4.3", "type": "swift", "namespace": null, "name": "Alamofire", "version": "5.4.3", "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "invalid swift purl without name", "purl": "pkg:swift/github.com/Alamofire/@5.4.3", "canonical_purl": "pkg:swift/github.com/Alamofire/@5.4.3", "type": "swift", "namespace": "github.com/Alamofire", "name": null, "version": "5.4.3", "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "invalid swift purl without version", "purl": "pkg:swift/github.com/Alamofire/Alamofire", "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire", "type": "swift", "namespace": "github.com/Alamofire", "name": "Alamofire", "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "valid hackage purl", "purl": "pkg:hackage/AC-HalfInteger@1.2.1", "canonical_purl": "pkg:hackage/AC-HalfInteger@1.2.1", "type": "hackage", "namespace": null, "name": "AC-HalfInteger", "version": "1.2.1", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "name and version are always required", "purl": "pkg:hackage", "canonical_purl": "pkg:hackage", "type": "hackage", "namespace": null, "name": null, "version": null, "qualifiers": null, "subpath": null, "is_invalid": true }, { "description": "minimal Hugging Face model", "purl": "pkg:huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027", "canonical_purl": "pkg:huggingface/distilbert-base-uncased@043235d6088ecd3dd5fb5ca3592b6913fd516027", "type": "huggingface", "namespace": null, "name": "distilbert-base-uncased", "version": "043235d6088ecd3dd5fb5ca3592b6913fd516027", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "Hugging Face model with staging endpoint", "purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co", "canonical_purl": "pkg:huggingface/microsoft/deberta-v3-base@559062ad13d311b87b2c455e67dcd5f1c8f65111?repository_url=https://hub-ci.huggingface.co", "type": "huggingface", "namespace": "microsoft", "name": "deberta-v3-base", "version": "559062ad13d311b87b2c455e67dcd5f1c8f65111", "qualifiers": {"repository_url": "https://hub-ci.huggingface.co"}, "subpath": null, "is_invalid": false }, { "description": "Hugging Face model with various cases", "purl": "pkg:huggingface/EleutherAI/gpt-neo-1.3B@797174552AE47F449AB70B684CABCB6603E5E85E", "canonical_purl": "pkg:huggingface/EleutherAI/gpt-neo-1.3B@797174552ae47f449ab70b684cabcb6603e5e85e", "type": "huggingface", "namespace": "EleutherAI", "name": "gpt-neo-1.3B", "version": "797174552ae47f449ab70b684cabcb6603e5e85e", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "MLflow model tracked in Azure Databricks (case insensitive)", "purl": "pkg:mlflow/CreditFraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", "canonical_purl": "pkg:mlflow/creditfraud@3?repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", "type": "mlflow", "namespace": null, "name": "creditfraud", "version": "3", "qualifiers": {"repository_url": "https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow"}, "subpath": null, "is_invalid": false }, { "description": "MLflow model tracked in Azure ML (case sensitive)", "purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace", "canonical_purl": "pkg:mlflow/CreditFraud@3?repository_url=https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace", "type": "mlflow", "namespace": null, "name": "CreditFraud", "version": "3", "qualifiers": {"repository_url": "https://westus2.api.azureml.ms/mlflow/v1.0/subscriptions/a50f2011-fab8-4164-af23-c62881ef8c95/resourceGroups/TestResourceGroup/providers/Microsoft.MachineLearningServices/workspaces/TestWorkspace"}, "subpath": null, "is_invalid": false }, { "description": "MLflow model with unique identifiers", "purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow", "canonical_purl": "pkg:mlflow/trafficsigns@10?model_uuid=36233173b22f4c89b451f1228d700d49&repository_url=https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow&run_id=410a3121-2709-4f88-98dd-dba0ef056b0a", "type": "mlflow", "namespace": null, "name": "trafficsigns", "version": "10", "qualifiers": {"model_uuid": "36233173b22f4c89b451f1228d700d49", "run_id": "410a3121-2709-4f88-98dd-dba0ef056b0a", "repository_url": "https://adb-5245952564735461.0.azuredatabricks.net/api/2.0/mlflow"}, "subpath": null, "is_invalid": false }, { "description": "composer names are not case sensitive", "purl": "pkg:composer/Laravel/Laravel@5.5.0", "canonical_purl": "pkg:composer/laravel/laravel@5.5.0", "type": "composer", "namespace": "laravel", "name": "laravel", "version": "5.5.0", "qualifiers": null, "subpath": null, "is_invalid": false }, { "description": "splits checksum qualifier", "purl": "pkg:github/rust-lang/rust-analyzer@2022-12-05?download_url=https://github.com/rust-lang/rust-analyzer/releases/download/2022-12-05/rust-analyzer-aarch64-apple-darwin.gz&checksum=sha1:256d83a0a59929099e7564169ef444c5e4088afc,sha256:28461b29ac0da9c653616e1d96092c85f86e24dd448d0fbe1973aa4c6d9b8b44", "canonical_purl": "pkg:github/rust-lang/rust-analyzer@2022-12-05?checksum=sha1:256d83a0a59929099e7564169ef444c5e4088afc,sha256:28461b29ac0da9c653616e1d96092c85f86e24dd448d0fbe1973aa4c6d9b8b44&download_url=https://github.com/rust-lang/rust-analyzer/releases/download/2022-12-05/rust-analyzer-aarch64-apple-darwin.gz", "type": "github", "namespace": "rust-lang", "name": "rust-analyzer", "version": "2022-12-05", "qualifiers": {"download_url": "https://github.com/rust-lang/rust-analyzer/releases/download/2022-12-05/rust-analyzer-aarch64-apple-darwin.gz", "checksum": ["sha1:256d83a0a59929099e7564169ef444c5e4088afc", "sha256:28461b29ac0da9c653616e1d96092c85f86e24dd448d0fbe1973aa4c6d9b8b44"]}, "subpath": null, "is_invalid": false } ] ================================================ FILE: tests/fixtures/receipts/1.0.json ================================================ { "schema_version": "1.0", "primary_source": { "type": "npm", "package": "@angular/language-server" }, "links": { "bin": { "ngserver": "node_modules/.bin/ngserver" } }, "metrics": { "start_time": 1694752057715, "completion_time": 1694752066467 }, "secondary_sources": [ { "type": "npm", "package": "typescript" } ], "name": "angular-language-server" } ================================================ FILE: tests/fixtures/receipts/1.1.json ================================================ { "schema_version": "1.1", "metrics": { "start_time": 1694752380220, "completion_time": 1694752386830 }, "links": { "share": {}, "opt": {}, "bin": { "ngserver": "node_modules/.bin/ngserver" } }, "name": "angular-language-server", "primary_source": { "type": "registry+v1", "id": "pkg:npm/%40angular/language-server@16.1.8", "source": { "extra_packages": [ "typescript@5.1.3" ], "version": "16.1.8", "package": "@angular/language-server" } }, "secondary_sources": [] } ================================================ FILE: tests/fixtures/receipts/2.0.json ================================================ { "links": { "bin": { "ngserver": "node_modules/.bin/ngserver" }, "share": {}, "opt": {} }, "name": "angular-language-server", "schema_version": "2.0", "metrics": { "start_time": 1739692587948, "completion_time": 1739692591360 }, "source": { "id": "pkg:npm/%40angular/language-server@19.1.0", "raw": { "id": "pkg:npm/%40angular/language-server@19.1.0", "extra_packages": [ "typescript@5.4.5" ] }, "type": "registry+v1" }, "install_options": { "debug": false, "strict": false, "force": false }, "registry": { "name": "mason-registry", "version": "2025-05-03-lawful-clave", "checksums": { "registry.json": "4ae083fe8e50d0bea5382be05c7ede8d2def55ff2b6b89dc129b153039d9f2a2", "registry.json.zip": "2116d5db7676afe7052de329db4dfbf656054d8c35ce12414eb9d58561b2fde9" }, "proto": "github", "namespace": "mason-org" } } ================================================ FILE: tests/helpers/lua/dummy-registry/dummy.lua ================================================ return { name = "dummy", description = [[This is a dummy package.]], homepage = "https://example.com", licenses = { "MIT" }, languages = { "DummyLang" }, categories = { "LSP" }, source = { id = "pkg:mason/dummy@1.0.0", ---@async ---@param ctx InstallContext install = function(ctx) end, }, } ================================================ FILE: tests/helpers/lua/dummy-registry/dummy2.lua ================================================ return { name = "dummy2", description = [[This is a dummy2 package.]], homepage = "https://example.com", licenses = { "MIT" }, languages = { "Dummy2Lang" }, categories = { "LSP" }, source = { id = "pkg:mason/dummy2@1.0.0", ---@async ---@param ctx InstallContext install = function(ctx) end, }, } ================================================ FILE: tests/helpers/lua/dummy-registry/index.lua ================================================ return { ["dummy"] = "dummy-registry.dummy", ["dummy2"] = "dummy-registry.dummy2", ["registry"] = "dummy-registry.registry", } ================================================ FILE: tests/helpers/lua/dummy-registry/registry.lua ================================================ return { name = "registry", description = [[This is a dummy package.]], homepage = "https://example.com", licenses = { "MIT" }, languages = { "DummyLang" }, categories = { "LSP" }, source = { id = "pkg:dummy/registry@1.0.0", }, } ================================================ FILE: tests/helpers/lua/luassertx.lua ================================================ local a = require "mason-core.async" local assert = require "luassert" local match = require "luassert.match" local function wait(_, arguments) ---@type (fun()) Function to execute until it does not error. local assertions_fn = arguments[1] ---@type number Timeout in milliseconds. Defaults to 5000. local timeout = arguments[2] or 5000 local err if not vim.wait(timeout, function() local ok, err_ = pcall(assertions_fn) err = err_ return ok end, math.min(timeout, 100)) then error(err) end return true end local function wait_for(_, arguments) ---@type (fun()) Function to execute until it does not error. local assertions_fn = arguments[1] ---@type number Timeout in milliseconds. Defaults to 5000. local timeout = arguments[2] timeout = timeout or 15000 local start = vim.loop.hrtime() local is_ok, err repeat is_ok, err = pcall(assertions_fn) if not is_ok then a.sleep(math.min(timeout, 100)) end until is_ok or ((vim.loop.hrtime() - start) / 1e6) > timeout if not is_ok then error(err) end return is_ok end local function tbl_containing(_, arguments, _) return function(value) local expected = arguments[1] for key, val in pairs(expected) do if match.is_matcher(val) then if not val(value[key]) then return false end elseif value[key] ~= val then return false end end return true end end local function list_containing(_, arguments, _) return function(value) local expected = arguments[1] for _, val in pairs(value) do if match.is_matcher(expected) then if expected(val) then return true end elseif expected == val then return true end end return false end end local function instanceof(_, arguments, _) return function(value) local expected_mt = arguments[1] return getmetatable(value) == expected_mt end end local function capture(_, arguments, _) return function(value) arguments[1](value) return true end end assert:register("matcher", "tbl_containing", tbl_containing) assert:register("matcher", "list_containing", list_containing) assert:register("matcher", "instanceof", instanceof) assert:register("matcher", "capture", capture) assert:register("assertion", "wait_for", wait_for) assert:register("assertion", "wait", wait) ================================================ FILE: tests/mason/api/command_spec.lua ================================================ local log = require "mason-core.log" local match = require "luassert.match" local spy = require "luassert.spy" local stub = require "luassert.stub" local Pkg = require "mason-core.package" local a = require "mason-core.async" local api = require "mason.api.command" local registry = require "mason-registry" describe(":Mason", function() it("should open the UI window", function() api.Mason() a.run_blocking(a.wait, vim.schedule) local win = vim.api.nvim_get_current_win() local buf = vim.api.nvim_win_get_buf(win) assert.equals("mason", vim.api.nvim_buf_get_option(buf, "filetype")) end) end) describe(":MasonInstall", function() it("should install the provided packages", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" spy.on(Pkg, "install") api.MasonInstall { "dummy@1.0.0", "dummy2" } assert.spy(Pkg.install).was_called(2) assert.spy(Pkg.install).was_called_with(match.is_ref(dummy), { version = "1.0.0" }, match.is_function()) assert .spy(Pkg.install) .was_called_with(match.is_ref(dummy2), match.tbl_containing { version = match.is_nil() }, match.is_function()) end) it("should install provided packages in debug mode", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" spy.on(Pkg, "install") vim.cmd [[MasonInstall --debug dummy dummy2]] assert.spy(Pkg.install).was_called(2) assert .spy(Pkg.install) .was_called_with(match.is_ref(dummy), { version = nil, debug = true }, match.is_function()) assert .spy(Pkg.install) .was_called_with(match.is_ref(dummy2), { version = nil, debug = true }, match.is_function()) end) it("should open the UI window", function() local dummy = registry.get_package "dummy" spy.on(dummy, "install") api.MasonInstall { "dummy" } local win = vim.api.nvim_get_current_win() local buf = vim.api.nvim_win_get_buf(win) assert.equals("mason", vim.api.nvim_buf_get_option(buf, "filetype")) end) end) describe(":MasonUninstall", function() it("should uninstall the provided packages", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy" spy.on(Pkg, "uninstall") api.MasonUninstall { "dummy", "dummy2" } assert.spy(Pkg.uninstall).was_called(2) assert.spy(Pkg.uninstall).was_called_with(match.is_ref(dummy)) assert.spy(Pkg.uninstall).was_called_with(match.is_ref(dummy2)) end) end) describe(":MasonLog", function() it("should open the log file", function() api.MasonLog() assert.equals(2, #vim.api.nvim_list_tabpages()) local win = vim.api.nvim_get_current_win() local buf = vim.api.nvim_win_get_buf(win) vim.api.nvim_buf_call(buf, function() assert.equals(log.outfile, vim.fn.expand "%") end) end) end) describe(":MasonUpdate", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should update registries", function() stub(registry, "update", function(cb) cb(true, { {} }) end) spy.on(vim, "notify") api.MasonUpdate() assert.spy(vim.notify).was_called(2) assert.spy(vim.notify).was_called_with("Updating registries…", vim.log.levels.INFO, { title = "mason.nvim", }) assert.spy(vim.notify).was_called_with("Successfully updated 1 registry.", vim.log.levels.INFO, { title = "mason.nvim", }) end) it("should notify errors", function() stub(registry, "update", function(cb) cb(false, "Some error.") end) spy.on(vim, "notify") api.MasonUpdate() assert.spy(vim.notify).was_called(2) assert.spy(vim.notify).was_called_with("Updating registries…", vim.log.levels.INFO, { title = "mason.nvim", }) assert.spy(vim.notify).was_called_with("Failed to update registries: Some error.", vim.log.levels.ERROR, { title = "mason.nvim", }) end) end) ================================================ FILE: tests/mason/setup_spec.lua ================================================ local InstallLocation = require "mason-core.installer.InstallLocation" local mason = require "mason" local match = require "luassert.match" local settings = require "mason.settings" describe("mason setup", function() before_each(function() vim.env.MASON = nil vim.env.PATH = "/usr/local/bin:/usr/bin" settings.set(settings._DEFAULT_SETTINGS) end) it("should modify the PATH environment", function() mason.setup() local global_location = InstallLocation.global() assert.equals(("%s:/usr/local/bin:/usr/bin"):format(global_location:bin()), vim.env.PATH) end) it("should prepend the PATH environment", function() mason.setup { PATH = "prepend" } local global_location = InstallLocation.global() assert.equals(("%s:/usr/local/bin:/usr/bin"):format(global_location:bin()), vim.env.PATH) end) it("should append PATH", function() mason.setup { PATH = "append" } local global_location = InstallLocation.global() assert.equals(("/usr/local/bin:/usr/bin:%s"):format(global_location:bin()), vim.env.PATH) end) it("shouldn't modify PATH", function() local PATH = vim.env.PATH local global_location = InstallLocation.global() mason.setup { PATH = "skip" } assert.equals(PATH, vim.env.PATH) end) it("should set MASON env", function() assert.is_nil(vim.env.MASON) local global_location = InstallLocation.global() mason.setup() assert.equals(vim.fn.expand "~/.local/share/nvim/mason", vim.env.MASON) end) it("should set up user commands", function() local global_location = InstallLocation.global() mason.setup() local user_commands = vim.api.nvim_get_commands {} assert.is_true(match.tbl_containing { bang = false, bar = false, nargs = "0", definition = "Opens mason's UI window.", }(user_commands["Mason"])) assert.is_true(match.tbl_containing { bang = false, bar = false, definition = "Install one or more packages.", nargs = "+", complete = "", }(user_commands["MasonInstall"])) assert.is_true(match.tbl_containing { bang = false, bar = false, definition = "Uninstall one or more packages.", nargs = "+", complete = "", }(user_commands["MasonUninstall"])) assert.is_true(match.tbl_containing { bang = false, bar = false, definition = "Uninstall all packages.", nargs = "0", }(user_commands["MasonUninstallAll"])) assert.is_true(match.tbl_containing { bang = false, bar = false, definition = "Opens the mason.nvim log.", nargs = "0", }(user_commands["MasonLog"])) end) it("should set the has_setup flag", function() package.loaded["mason"] = nil local mason = require "mason" assert.is_false(mason.has_setup) mason.setup() assert.is_true(mason.has_setup) end) end) ================================================ FILE: tests/mason-core/EventEmitter_spec.lua ================================================ local log = require "mason-core.log" local match = require "luassert.match" local spy = require "luassert.spy" local EventEmitter = require "mason-core.EventEmitter" local a = require "mason-core.async" describe("EventEmitter", function() it("should call registered event handlers", function() local emitter = EventEmitter.init(setmetatable({}, { __index = EventEmitter })) local my_event_handler = spy.new() emitter:on("my:event", my_event_handler --[[@as fun()]]) emitter:emit("my:event", { table = "value" }) emitter:emit("my:event", 1337, 42) assert.spy(my_event_handler).was_called(2) assert.spy(my_event_handler).was_called_with(match.same { table = "value" }) assert.spy(my_event_handler).was_called_with(1337, 42) end) it("should call registered event handlers only once", function() local emitter = EventEmitter.init(setmetatable({}, { __index = EventEmitter })) local my_event_handler = spy.new() emitter:once("my:event", my_event_handler --[[@as fun()]]) emitter:emit("my:event", { table = "value" }) emitter:emit("my:event", 1337, 42) assert.spy(my_event_handler).was_called(1) assert.spy(my_event_handler).was_called_with(match.same { table = "value" }) end) it("should remove registered event handlers", function() local emitter = EventEmitter.init(setmetatable({}, { __index = EventEmitter })) local my_event_handler = spy.new() --[[@as fun()]] emitter:on("my:event", my_event_handler) emitter:once("my:event", my_event_handler) emitter:off("my:event", my_event_handler) emitter:emit("my:event", { table = "value" }) assert.spy(my_event_handler).was_called(0) end) it("should log errors in handlers", function() spy.on(log, "fmt_warn") local emitter = EventEmitter.init(setmetatable({}, { __index = EventEmitter })) emitter:on("event", mockx.throws "My error.") emitter:emit "event" a.run_blocking(a.wait, vim.schedule) assert.spy(log.fmt_warn).was_called(1) assert .spy(log.fmt_warn) .was_called_with("EventEmitter handler failed for event %s with error %s", "event", "My error.") end) end) ================================================ FILE: tests/mason-core/async/async_spec.lua ================================================ local _ = require "mason-core.functional" local a = require "mason-core.async" local assert = require "luassert" local control = require "mason-core.async.control" local match = require "luassert.match" local process = require "mason-core.process" local spy = require "luassert.spy" local function timestamp() local seconds, microseconds = vim.loop.gettimeofday() return (seconds * 1000) + math.floor(microseconds / 1000) end describe("async", function() it("should run in blocking mode", function() local start = timestamp() a.run_blocking(function() a.sleep(100) end) local stop = timestamp() local grace_ms = 50 assert.is_true((stop - start) >= (100 - grace_ms)) end) it("should return values in blocking mode", function() local function slow_maths(arg1, arg2) a.sleep(10) return arg1 + arg2 - 42 end local value = a.run_blocking(slow_maths, 13, 37) assert.equals(8, value) end) it("should pass arguments to .run", function() local fn = spy.new() a.run(function(...) fn(...) end, spy.new(), 100, 200) assert.spy(fn).was_called(1) assert.spy(fn).was_called_with(100, 200) end) it("should wrap callback-style async functions via promisify", function() local async_spawn = _.compose(_.table_pack, a.promisify(process.spawn)) local stdio = process.BufferedSink:new() local success, exit_code = unpack(a.run_blocking(async_spawn, "env", { args = {}, env = { "FOO=BAR", "BAR=BAZ" }, stdio_sink = stdio, })) assert.is_true(success) assert.equals(0, exit_code) assert.equals("FOO=BAR\nBAR=BAZ\n", table.concat(stdio.buffers.stdout, "")) end) it("should propagate errors in callback-style functions via promisify", function() local err = assert.has_error(function() a.run_blocking(a.promisify(function(cb) cb "Error message." end, true)) end) assert.equals(err, "Error message.") end) it("should return all values from a.wait", function() a.run_blocking(function() local val1, val2, val3 = a.wait(function(resolve) resolve(1, 2, 3) end) assert.equals(1, val1) assert.equals(2, val2) assert.equals(3, val3) end) end) it("should cancel coroutine", function() local capture = spy.new() a.run_blocking(function() local cancel = a.scope(function() a.sleep(10) capture() end)() cancel() a.sleep(20) end) assert.spy(capture).was_not.called() end) it("should raise error if async function raises error", function() a.run_blocking(function() local err = assert.has.errors(a.promisify(function() error "something went wrong" end)) assert.is_true(match.has_match "something went wrong$"(err)) end) end) it("should raise error if async function rejects", function() a.run_blocking(function() local err = assert.has.errors(function() a.wait(function(_, reject) reject "This is an error" end) end) assert.equals("This is an error", err) end) end) it("should pass nil arguments to promisified functions", function() local fn = spy.new(function(_, _, _, _, _, _, _, cb) cb() end) a.run_blocking(function() a.promisify(fn)(nil, 2, nil, 4, nil, nil, 7) end) assert.spy(fn).was_called_with(nil, 2, nil, 4, nil, nil, 7, match.is_function()) end) it("should accept yielding non-promise values to parent coroutine context", function() local thread = coroutine.create(function(val) a.run_blocking(function() coroutine.yield(val) end) end) local ok, value = coroutine.resume(thread, 1337) assert.is_true(ok) assert.equals(1337, value) end) it("should run all suspending functions concurrently", function() local function sleep(ms, ret_val) return function() a.sleep(ms) return ret_val end end local start = timestamp() local one, two, three, four, five = unpack(a.run_blocking(function() return _.table_pack(a.wait_all { sleep(100, 1), sleep(100, "two"), sleep(100, "three"), sleep(100, 4), sleep(100, 5), }) end)) local grace = 50 local delta = timestamp() - start assert.is_true(delta <= (100 + grace)) assert.is_true(delta >= (100 - grace)) assert.equals(1, one) assert.equals("two", two) assert.equals("three", three) assert.equals(4, four) assert.equals(5, five) end) it("should run all suspending functions concurrently", function() local start = timestamp() local called = spy.new() local function sleep(ms, ret_val) return function() a.sleep(ms) called() return ret_val end end local first = a.run_blocking(a.wait_first, { sleep(150, 1), sleep(50, "first"), sleep(150, "three"), sleep(150, 4), sleep(150, 5), }) local grace = 20 local delta = timestamp() - start assert.is_true(delta <= (50 + grace)) assert.equals("first", first) end) it("should yield back immediately when not providing any functions", function() assert.is_nil(a.wait_first {}) assert.is_nil(a.wait_all {}) end) end) describe("async :: Condvar", function() local Condvar = control.Condvar it("should block execution until condvar is notified", function() local condvar = Condvar:new() local function wait() local start = timestamp() condvar:wait() local stop = timestamp() return stop - start end local start = timestamp() local condvar_waits = a.run_blocking(function() vim.defer_fn(function() condvar:notify_all() end, 110) return _.table_pack(a.wait_all { wait, wait, wait, wait, }) end) local stop = timestamp() for _, delay in ipairs(condvar_waits) do assert.is_True(delay >= 100) end assert.is_true((stop - start) >= 100) end) end) describe("async :: Semaphore", function() local Semaphore = control.Semaphore it("should limit the amount of permits", function() local sem = Semaphore:new(5) ---@type Permit[] local permits = {} local cancel_thread = a.run(function() while true do table.insert(permits, sem:acquire()) end end) cancel_thread() assert.equals(5, #permits) end) it("should lease new permits", function() local sem = Semaphore:new(2) ---@type Permit[] local permits = {} local cancel_thread = a.run(function() while true do table.insert(permits, sem:acquire()) end end) assert.equals(2, #permits) permits[1]:forget() permits[2]:forget() assert.equals(4, #permits) cancel_thread() end) end) describe("async :: OneShotChannel", function() local OneShotChannel = control.OneShotChannel it("should only allow sending once", function() local channel = OneShotChannel:new() assert.is_false(channel:is_closed()) channel:send "value" assert.is_true(channel:is_closed()) local err = assert.has_error(function() channel:send "value" end) assert.equals("Oneshot channel can only send once.", err) end) it("should wait until it can receive", function() local channel = OneShotChannel:new() local start = timestamp() local value = a.run_blocking(function() vim.defer_fn(function() channel:send(42) end, 110) return channel:receive() end) local stop = timestamp() assert.is_true((stop - start) >= 100) assert.equals(42, value) end) it("should receive immediately if value is already sent", function() local channel = OneShotChannel:new() channel:send(42) assert.equals(42, channel:receive()) end) end) describe("async :: Channel", function() local Channel = control.Channel it("should suspend send until buffer is received", function() local channel = Channel:new() spy.on(channel, "send") local guard = spy.new() a.run(function() channel:send "message" guard() channel:send "another message" end, function() end) assert.spy(channel.send).was_called(1) assert.spy(channel.send).was_called_with(match.is_ref(channel), "message") assert.spy(guard).was_not_called() end) it("should send subsequent messages after they're received", function() local channel = Channel:new() spy.on(channel, "send") a.run(function() channel:send "message" channel:send "another message" end, function() end) local value = channel:receive() assert.equals(value, "message") assert.spy(channel.send).was_called(2) assert.spy(channel.send).was_called_with(match.is_ref(channel), "message") assert.spy(channel.send).was_called_with(match.is_ref(channel), "another message") end) it("should suspend receive until message is sent", function() local channel = Channel:new() a.run(function() a.sleep(100) channel:send "hello world" end, function() end) local start = timestamp() local value = a.run_blocking(function() return channel:receive() end) local stop = timestamp() assert.is_true((stop - start) > 80) assert.equals(value, "hello world") end) end) ================================================ FILE: tests/mason-core/fetch_spec.lua ================================================ local Result = require "mason-core.result" local fetch = require "mason-core.fetch" local match = require "luassert.match" local spawn = require "mason-core.spawn" local stub = require "luassert.stub" local version = require "mason.version" describe("fetch", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should exhaust all candidates", function() stub(spawn, "wget") stub(spawn, "curl") spawn.wget.returns(Result.failure "wget failure") spawn.curl.returns(Result.failure "curl failure") local result = fetch("https://api.github.com", { headers = { ["X-Custom-Header"] = "here" }, }) assert.is_true(result:is_failure()) assert.spy(spawn.wget).was_called(1) assert.spy(spawn.curl).was_called(1) assert.spy(spawn.wget).was_called_with { { { "--header", ("User-Agent: mason.nvim %s (+https://github.com/mason-org/mason.nvim)"):format(version.VERSION), }, { "--header", "X-Custom-Header: here", }, }, "-o", "/dev/null", "-O", "-", "-T", 30, vim.NIL, -- body-data "https://api.github.com", } assert.spy(spawn.curl).was_called_with(match.tbl_containing { match.same { { "-H", ("User-Agent: mason.nvim %s (+https://github.com/mason-org/mason.nvim)"):format(version.VERSION), }, { "-H", "X-Custom-Header: here", }, }, "-fsSL", match.same { "-X", "GET" }, vim.NIL, -- data vim.NIL, -- out file match.same { "--connect-timeout", 30 }, "https://api.github.com", on_spawn = match.is_function(), }) end) it("should return stdout", function() stub(spawn, "curl") spawn.curl.returns(Result.success { stdout = [[{"data": "here"}]], }) local result = fetch "https://api.github.com/data" assert.is_true(result:is_success()) assert.equals([[{"data": "here"}]], result:get_or_throw()) end) it("should respect out_file opt", function() stub(spawn, "wget") stub(spawn, "curl") spawn.wget.returns(Result.failure "wget failure") spawn.curl.returns(Result.failure "curl failure") fetch("https://api.github.com/data", { out_file = "/test.json" }) assert.spy(spawn.wget).was_called_with { { { "--header", ("User-Agent: mason.nvim %s (+https://github.com/mason-org/mason.nvim)"):format(version.VERSION), }, }, "-o", "/dev/null", "-O", "/test.json", "-T", 30, vim.NIL, -- body-data "https://api.github.com/data", } assert.spy(spawn.curl).was_called_with(match.tbl_containing { match.same { { "-H", ("User-Agent: mason.nvim %s (+https://github.com/mason-org/mason.nvim)"):format(version.VERSION), }, }, "-fsSL", match.same { "-X", "GET" }, vim.NIL, -- data match.same { "-o", "/test.json" }, match.same { "--connect-timeout", 30 }, "https://api.github.com/data", on_spawn = match.is_function(), }) end) end) describe("fetch :: wget", function() it("should reject non-supported HTTP methods", function() stub(spawn, "wget") stub(spawn, "curl") spawn.wget.returns(Result.failure "wget failure") spawn.curl.returns(Result.failure "curl failure") local PATCH_ERR = assert.has_error(function() fetch("https://api.github.com/data", { method = "PATCH" }):get_or_throw() end) local DELETE_ERR = assert.has_error(function() fetch("https://api.github.com/data", { method = "DELETE" }):get_or_throw() end) local PUT_ERR = assert.has_error(function() fetch("https://api.github.com/data", { method = "PUT" }):get_or_throw() end) assert.equals("fetch: wget doesn't support HTTP method PATCH", PATCH_ERR) assert.equals("fetch: wget doesn't support HTTP method DELETE", DELETE_ERR) assert.equals("fetch: wget doesn't support HTTP method PUT", PUT_ERR) end) it("should reject requests with opts.data if not opts.method is not POST", function() stub(spawn, "wget") stub(spawn, "curl") spawn.wget.returns(Result.failure "wget failure") spawn.curl.returns(Result.failure "curl failure") local err = assert.has_error(function() fetch("https://api.github.com/data", { data = "data" }):get_or_throw() end) assert.equals("fetch: data provided but method is not POST (was GET)", err) end) end) ================================================ FILE: tests/mason-core/fs_spec.lua ================================================ local fs = require "mason-core.fs" local mason = require "mason" describe("fs", function() before_each(function() mason.setup { install_root_dir = "/foo", } end) it("refuses to rmrf paths outside of boundary", function() local e = assert.has_error(function() fs.sync.rmrf "/thisisa/path" end) assert.equals( [[Refusing to rmrf "/thisisa/path" which is outside of the allowed boundary "/foo". Please report this error at https://github.com/mason-org/mason.nvim/issues/new]], e ) end) end) ================================================ FILE: tests/mason-core/functional/data_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: data", function() it("creates enums", function() local colors = _.enum { "BLUE", "YELLOW", } assert.same({ ["BLUE"] = "BLUE", ["YELLOW"] = "YELLOW", }, colors) end) it("creates sets", function() local colors = _.set_of { "BLUE", "YELLOW", "BLUE", "RED", } assert.same({ ["BLUE"] = true, ["YELLOW"] = true, ["RED"] = true, }, colors) end) end) ================================================ FILE: tests/mason-core/functional/function_spec.lua ================================================ local _ = require "mason-core.functional" local match = require "luassert.match" local spy = require "luassert.spy" describe("functional: function", function() it("curries functions", function() local function sum(...) local res = 0 for i = 1, select("#", ...) do res = res + select(i, ...) end return res end local arity0 = _.curryN(sum, 0) local arity1 = _.curryN(sum, 1) local arity2 = _.curryN(sum, 2) local arity3 = _.curryN(sum, 3) assert.equals(0, arity0(42)) assert.equals(42, arity1(42)) assert.equals(3, arity2(1)(2)) assert.equals(3, arity2(1, 2)) assert.equals(6, arity3(1)(2)(3)) assert.equals(6, arity3(1, 2, 3)) -- should discard superfluous args assert.equals(0, arity1(0, 10, 20, 30)) end) it("coalesces first non-nil value", function() assert.equals("Hello World!", _.coalesce(nil, nil, "Hello World!", "")) end) it("should compose functions", function() local function add(x) return function(y) return y + x end end local function subtract(x) return function(y) return y - x end end local function multiply(x) return function(y) return y * x end end local big_maths = _.compose(add(1), subtract(3), multiply(5)) assert.equals(23, big_maths(5)) end) it("should not allow composing no functions", function() local e = assert.has_error(function() _.compose() end) assert.equals("compose requires at least one function", e) end) it("should partially apply functions", function() local funcy = spy.new() local partially_funcy = _.partial(funcy, "a", "b", "c") partially_funcy("d", "e", "f") assert.spy(funcy).was_called_with("a", "b", "c", "d", "e", "f") end) it("should partially apply functions with nil arguments", function() local funcy = spy.new() local partially_funcy = _.partial(funcy, "a", nil, "c") partially_funcy("d", nil, "f") assert.spy(funcy).was_called_with("a", nil, "c", "d", nil, "f") end) it("memoizes functions with default cache mechanism", function() local expensive_function = spy.new(function(s) return s end) local memoized_fn = _.memoize(expensive_function) assert.equals("key", memoized_fn "key") assert.equals("key", memoized_fn "key") assert.equals("new_key", memoized_fn "new_key") assert.spy(expensive_function).was_called(2) end) it("memoizes function with custom cache mechanism", function() local expensive_function = spy.new(function(arg1, arg2) return arg1 .. arg2 end) local memoized_fn = _.memoize(expensive_function, function(arg1, arg2) return arg1 .. arg2 end) assert.equals("key1key2", memoized_fn("key1", "key2")) assert.equals("key1key2", memoized_fn("key1", "key2")) assert.equals("key1key3", memoized_fn("key1", "key3")) assert.spy(expensive_function).was_called(2) end) it("should evaluate functions lazily", function() local impl = spy.new(function() return {}, {} end) local lazy_fn = _.lazy(impl) assert.spy(impl).was_called(0) local a, b = lazy_fn() assert.spy(impl).was_called(1) assert.is_true(match.is_table()(a)) assert.is_true(match.is_table()(b)) local new_a, new_b = lazy_fn() assert.spy(impl).was_called(1) assert.is_true(match.is_ref(a)(new_a)) assert.is_true(match.is_ref(b)(new_b)) end) it("should support nil return values in lazy functions", function() local lazy_fn = _.lazy(function() return nil, 2 end) local a, b = lazy_fn() assert.is_nil(a) assert.equals(2, b) end) it("should provide identity value", function() local obj = {} assert.equals(2, _.identity(2)) assert.equals(obj, _.identity(obj)) end) it("should always return bound value", function() local obj = {} assert.equals(2, _.always(2)()) assert.equals(obj, _.always(obj)()) end) it("true is true and false is false", function() assert.is_true(_.T()) assert.is_false(_.F()) end) it("should tap values", function() local fn = spy.new() assert.equals(42, _.tap(fn, 42)) assert.spy(fn).was_called() assert.spy(fn).was_called_with(42) end) it("should apply function", function() local max = spy.new(math.max) local max_fn = _.apply(max) assert.equals(42, max_fn { 1, 2, 3, 4, 5, 6, 7, 8, 9, 42, 10, 8, 4 }) assert.spy(max).was_called(1) assert.spy(max).was_called_with(1, 2, 3, 4, 5, 6, 7, 8, 9, 42, 10, 8, 4) end) it("should apply value to function", function() local agent = spy.new() _.apply_to("007", agent) assert.spy(agent).was_called(1) assert.spy(agent).was_called_with "007" end) it("should converge on function", function() local target = spy.new() _.converge(target, { _.head, _.last }, { "These", "Are", "Some", "Words", "Ain't", "That", "Pretty", "Nuts" }) assert.spy(target).was_called(1) assert.spy(target).was_called_with("These", "Nuts") end) it("should apply spec", function() local apply = _.apply_spec { sum = _.add(2), list = { _.add(2), _.add(6) }, nested = { sum = _.min(2), }, } assert.same({ sum = 4, list = { 4, 8 }, nested = { sum = 0, }, }, apply(2)) end) end) ================================================ FILE: tests/mason-core/functional/list_spec.lua ================================================ local Optional = require "mason-core.optional" local _ = require "mason-core.functional" local spy = require "luassert.spy" describe("functional: list", function() it("should produce list without nils", function() assert.same({ 1, 2, 3, 4 }, _.list_not_nil(nil, 1, 2, nil, 3, nil, 4, nil)) end) it("makes a shallow copy of a list", function() local list = { "BLUE", { nested = "TABLE" }, "RED" } local list_copy = _.list_copy(list) assert.same({ "BLUE", { nested = "TABLE" }, "RED" }, list_copy) assert.is_false(list == list_copy) assert.is_true(list[2] == list_copy[2]) end) it("reverses lists", function() local colors = { "BLUE", "YELLOW", "RED" } assert.same({ "RED", "YELLOW", "BLUE", }, _.reverse(colors)) -- should not modify in-place assert.same({ "BLUE", "YELLOW", "RED" }, colors) end) it("maps over list", function() local colors = { "BLUE", "YELLOW", "RED" } assert.same( { "LIGHT_BLUE", "LIGHT_YELLOW", "LIGHT_RED", }, _.map(function(color) return "LIGHT_" .. color end, colors) ) -- should not modify in-place assert.same({ "BLUE", "YELLOW", "RED" }, colors) end) it("filter_map over list", function() local colors = { "BROWN", "BLUE", "YELLOW", "GREEN", "CYAN" } assert.same( { "BROWN EYES", "BLUE EYES", "GREEN EYES", }, _.filter_map(function(color) if _.any_pass({ _.equals "BROWN", _.equals "BLUE", _.equals "GREEN" }, color) then return Optional.of(("%s EYES"):format(color)) else return Optional.empty() end end, colors) ) end) it("finds first item that fulfills predicate", function() local predicate = spy.new(function(item) return item == "Waldo" end) assert.equals( "Waldo", _.find_first(predicate, { "Where", "On Earth", "Is", "Waldo", "?", }) ) assert.spy(predicate).was.called(4) end) it("determines whether any item in the list fulfills predicate", function() local predicate = spy.new(function(item) return item == "On Earth" end) assert.is_true(_.any(predicate, { "Where", "On Earth", "Is", "Waldo", "?", })) assert.spy(predicate).was.called(2) end) it("should check that all items in list fulfills predicate", function() assert.is_true(_.all(_.is "string", { "Where", "On Earth", "Is", "Waldo", "?", })) local predicate = spy.new(_.is "string") assert.is_false(_.all(predicate, { "Five", "Plus", 42, "Equals", 47, })) assert.spy(predicate).was_called(3) end) it("should iterate list in .each", function() local list = { "BLUE", "YELLOW", "RED" } local iterate_fn = spy.new() _.each(iterate_fn, list) assert.spy(iterate_fn).was_called(3) assert.spy(iterate_fn).was_called_with("BLUE", 1) assert.spy(iterate_fn).was_called_with("YELLOW", 2) assert.spy(iterate_fn).was_called_with("RED", 3) end) it("should concat list tables", function() local list = { "monstera", "tulipa", "carnation" } assert.same({ "monstera", "tulipa", "carnation", "rose", "daisy" }, _.concat(list, { "rose", "daisy" })) assert.same({ "monstera", "tulipa", "carnation" }, list) -- does not mutate list end) it("should concat strings", function() assert.equals("FooBar", _.concat("Foo", "Bar")) end) it("should zip list into table", function() local fnkey = function() end assert.same({ a = "a", [fnkey] = 1, }, _.zip_table({ "a", fnkey }, { "a", 1 })) end) it("should get nth item", function() assert.equals("first", _.nth(1, { "first", "middle", "last" })) assert.equals("last", _.nth(-1, { "first", "middle", "last" })) assert.equals("middle", _.nth(-2, { "first", "middle", "last" })) assert.equals("a", _.nth(1, "abc")) assert.equals("c", _.nth(-1, "abc")) assert.equals("b", _.nth(-2, "abc")) assert.is_nil(_.nth(0, { "value" })) assert.equals("", _.nth(0, "abc")) end) it("should get length", function() assert.equals(0, _.length {}) assert.equals(0, _.length { nil }) assert.equals(0, _.length { obj = "doesnt count" }) assert.equals(0, _.length "") assert.equals(1, _.length { "" }) assert.equals(4, _.length "fire") end) it("should sort by comparator", function() local list = { { name = "William", }, { name = "Boman", }, } assert.same({ { name = "Boman", }, { name = "William", }, }, _.sort_by(_.prop "name", list)) -- Should not mutate original list assert.same({ { name = "William", }, { name = "Boman", }, }, list) end) it("should append to list", function() local list = { "Earth", "Wind" } assert.same({ "Earth", "Wind", { "Fire" } }, _.append({ "Fire" }, list)) -- Does not mutate original list assert.same({ "Earth", "Wind" }, list) end) it("should prepend to list", function() local list = { "Fire" } assert.same({ { "Earth", "Wind" }, "Fire" }, _.prepend({ "Earth", "Wind" }, list)) -- Does not mutate original list assert.same({ "Fire" }, list) end) it("joins lists", function() assert.equals("Hello, John", _.join(", ", { "Hello", "John" })) end) it("should uniq_by lists", function() local list = { "Person.", "Woman.", "Man.", "Person.", "Woman.", "Camera.", "TV." } assert.same({ "Person.", "Woman.", "Man.", "Camera.", "TV." }, _.uniq_by(_.identity, list)) end) it("should partition lists", function() local words = { "person", "Woman", "Man", "camera", "TV" } assert.same({ { "Woman", "Man", "TV" }, { "person", "camera" }, }, _.partition(_.matches "%u", words)) end) it("should return head", function() assert.equals("Head", _.head { "Head", "Tail", "Tail" }) end) it("should return last", function() assert.equals("Last", _.last { "Head", "List", "Last" }) end) it("should take n items", function() local list = { "First", "Second", "Third", "I", "Have", "Poor", "Imagination" } assert.same({ "First", "Second", "Third" }, _.take(3, list)) assert.same({}, _.take(0, list)) assert.same({ "First", "Second", "Third", "I", "Have", "Poor", "Imagination" }, _.take(10000, list)) end) it("should drop n items", function() local list = { "First", "Second", "Third", "I", "Have", "Poor", "Imagination" } assert.same({ "I", "Have", "Poor", "Imagination" }, _.drop(3, list)) assert.same({ "First", "Second", "Third", "I", "Have", "Poor", "Imagination" }, _.drop(0, list)) assert.same({}, _.drop(10000, list)) end) it("should drop last n items", function() local list = { "First", "Second", "Third", "I", "Have", "Poor", "Imagination" } assert.same({ "First", "Second", "Third" }, _.drop_last(4, list)) assert.same({ "First", "Second", "Third", "I", "Have", "Poor", "Imagination" }, _.drop_last(0, list)) assert.same({}, _.drop_last(10000, list)) end) it("should reduce lists", function() local add = spy.new(_.add) assert.equals(15, _.reduce(add, 0, { 1, 2, 3, 4, 5 })) assert.spy(add).was_called(5) assert.spy(add).was_called_with(0, 1) assert.spy(add).was_called_with(1, 2) assert.spy(add).was_called_with(3, 3) assert.spy(add).was_called_with(6, 4) assert.spy(add).was_called_with(10, 5) end) it("should split lists", function() assert.same({ { 1, 2, 3 }, { 4, 5, 6 }, { 7 }, }, _.split_every(3, { 1, 2, 3, 4, 5, 6, 7 })) assert.same({ { 1, 2, 3 } }, _.split_every(5, { 1, 2, 3 })) assert.same({ { 1 }, { 2 }, { 3 } }, _.split_every(1, { 1, 2, 3 })) assert.has_error(function() _.split_every(0, {}) end) end) it("should index_by lists", function() assert.same( { apple = { fruit = "apple", color = "red" }, banana = { fruit = "banana", color = "yellow" }, }, _.index_by(_.prop "fruit", { { fruit = "apple", color = "red" }, { fruit = "banana", color = "yellow" }, }) ) end) it("should flatten tables", function() assert.same({ 1, 2, 3 }, _.flatten { 1, 2, 3 }) assert.same({ 1, 2, 3, "a" }, _.flatten { 1, { 2 }, { 3 }, "a" }) assert.same({ 1, 2, 3, 4, 5 }, _.flatten { 1, { { 2, 3 }, { 4 } }, { 5 } }) end) -- Note: this is not necessarily a requirement, but it is expected to behave this way as of writing. it("should flatten keyed tables", function() assert.same( { "-xvf", "file", }, _.flatten { { "-xvf", { "file" } }, cmd = "tar", env = { LC_ALL = "latin", }, } ) end) end) describe("list immutability", function() it("should not mutate lists", function() local og_list = setmetatable({ "a", "b", "c" }, { __newindex = function() error "Tried to newindex" end, }) _.reverse(og_list) _.list_copy(og_list) _.filter(_.F, og_list) _.map(_.to_upper, og_list) _.filter_map(_.always(Optional.empty()), og_list) _.each(_.length, og_list) _.concat(og_list, { "d", "e" }) _.append("d", og_list) _.prepend("0", og_list) _.zip_table({ "first", "second", "third" }, og_list) _.nth(1, og_list) _.head(og_list) _.last(og_list) _.length(og_list) _.flatten(og_list) _.sort_by(_.identity, og_list) _.uniq_by(_.identity, og_list) _.join(".", og_list) _.partition(_.equals "a", og_list) _.take(2, og_list) _.drop(2, og_list) _.drop_last(2, og_list) _.reduce(_.concat, "", og_list) assert.same({ "a", "b", "c" }, og_list) end) end) ================================================ FILE: tests/mason-core/functional/logic_spec.lua ================================================ local _ = require "mason-core.functional" local spy = require "luassert.spy" describe("functional: logic", function() it("should check that all_pass checks that all predicates pass", function() local is_waldo = _.equals "waldo" assert.is_true(_.all_pass { _.T, _.T, is_waldo, _.T } "waldo") assert.is_false(_.all_pass { _.T, _.T, is_waldo, _.F } "waldo") assert.is_false(_.all_pass { _.T, _.T, is_waldo, _.T } "waldina") end) it("should check that any_pass checks that any predicates pass", function() local is_waldo = _.equals "waldo" local is_waldina = _.equals "waldina" local is_luigi = _.equals "luigi" assert.is_true(_.any_pass { is_waldo, is_waldina } "waldo") assert.is_false(_.any_pass { is_waldina, is_luigi } "waldo") assert.is_true(_.any_pass { is_waldina, is_luigi } "waldina") end) it("should branch if_else", function() local a = spy.new() local b = spy.new() _.if_else(_.T, a, b) "a" _.if_else(_.F, a, b) "b" assert.spy(a).was_called(1) assert.spy(a).was_called_with "a" assert.spy(b).was_called(1) assert.spy(b).was_called_with "b" end) it("should flip booleans", function() assert.is_true(_.is_not(false)) assert.is_false(_.is_not(true)) end) it("should resolve correct cond", function() local planetary_object = _.cond { { _.equals "Moon!", _.format "to the %s", }, { _.equals "World!", _.format "Hello %s", }, } assert.equals("Hello World!", planetary_object "World!") assert.equals("to the Moon!", planetary_object "Moon!") end) it("should give complements", function() assert.is_true(_.complement(_.is_nil, "not nil")) assert.is_false(_.complement(_.is_nil, nil)) end) it("should default to provided value", function() local fortytwo = _.default_to(42) assert.equals(42, fortytwo(nil)) assert.equals(1337, fortytwo(1337)) end) end) ================================================ FILE: tests/mason-core/functional/number_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: number", function() it("should negate numbers", function() assert.equals(-42, _.negate(42)) assert.equals(42, _.negate(-42)) end) it("should check numbers greater than value", function() local greater_than_life = _.gt(42) assert.is_false(greater_than_life(0)) assert.is_false(greater_than_life(42)) assert.is_true(greater_than_life(43)) end) it("should check numbers greater or equal than value", function() local greater_or_equal_to_life = _.gte(42) assert.is_false(greater_or_equal_to_life(0)) assert.is_true(greater_or_equal_to_life(42)) assert.is_true(greater_or_equal_to_life(43)) end) it("should check numbers lower than value", function() local lesser_than_life = _.lt(42) assert.is_true(lesser_than_life(0)) assert.is_false(lesser_than_life(42)) assert.is_false(lesser_than_life(43)) end) it("should check numbers lower or equal than value", function() local lesser_or_equal_to_life = _.lte(42) assert.is_true(lesser_or_equal_to_life(0)) assert.is_true(lesser_or_equal_to_life(42)) assert.is_false(lesser_or_equal_to_life(43)) end) it("should increment numbers", function() local add_5 = _.inc(5) assert.equals(0, add_5(-5)) assert.equals(5, add_5(0)) assert.equals(7, add_5(2)) end) it("should decrement numbers", function() local subtract_5 = _.dec(5) assert.equals(5, subtract_5(10)) assert.equals(-5, subtract_5(0)) assert.equals(-3, subtract_5(2)) end) end) ================================================ FILE: tests/mason-core/functional/relation_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: relation", function() it("should check equality", function() local tbl = {} local is_tbl = _.equals(tbl) local is_a = _.equals "a" local is_42 = _.equals(42) assert.is_true(is_tbl(tbl)) assert.is_true(is_a "a") assert.is_true(is_42(42)) assert.is_false(is_a "b") assert.is_false(is_42(32)) end) it("should check non-equality", function() local tbl = {} local is_not_tbl = _.not_equals(tbl) local is_not_a = _.not_equals "a" local is_not_42 = _.not_equals(42) assert.is_false(is_not_tbl(tbl)) assert.is_false(is_not_a "a") assert.is_false(is_not_42(42)) assert.is_true(is_not_a "b") assert.is_true(is_not_42(32)) end) it("should check property equality", function() local fn_key = function() end local tbl = { a = "a", b = "b", number = 42, [fn_key] = "fun" } assert.is_true(_.prop_eq("a", "a", tbl)) assert.is_true(_.prop_eq(fn_key, "fun", tbl)) assert.is_true(_.prop_eq(fn_key) "fun"(tbl)) end) it("should check whether property satisfies predicate", function() local obj = { low = 0, med = 10, high = 15, } assert.is_false(_.prop_satisfies(_.gt(10), "low", obj)) assert.is_false(_.prop_satisfies(_.gt(10), "med")(obj)) assert.is_true(_.prop_satisfies(_.gt(10)) "high"(obj)) end) it("should check whether nested property satisfies predicate", function() local obj = { low = { value = 0 }, med = { value = 10 }, high = { value = 15 }, } assert.is_false(_.path_satisfies(_.gt(10), { "low", "value" }, obj)) assert.is_false(_.path_satisfies(_.gt(10), { "med", "value" })(obj)) assert.is_true(_.path_satisfies(_.gt(10)) { "high", "value" }(obj)) end) it("should subtract numbers", function() assert.equals(42, _.min(42, 84)) assert.equals(-1, _.min(11, 10)) end) it("should add numbers", function() assert.equals(1337, _.add(1300, 37)) assert.equals(-10, _.add(90, -100)) end) end) ================================================ FILE: tests/mason-core/functional/string_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: string", function() it("matches string patterns", function() assert.is_true(_.matches("foo", "foo")) assert.is_true(_.matches("bar", "foobarbaz")) assert.is_true(_.matches("ba+r", "foobaaaaaaarbaz")) assert.is_false(_.matches("ba+r", "foobharbaz")) assert.is_false(_.matches("bar", "foobaz")) end) it("returns string pattern matches", function() assert.same({ "foo" }, _.match("foo", "foo")) assert.same({ "foo", "bar", "baz" }, _.match("(foo) (bar) (baz)", "foo bar baz")) end) it("should format strings", function() assert.equals("Hello World!", _.format("%s", "Hello World!")) assert.equals("special manouvers", _.format("%s manouvers", "special")) end) it("should split strings", function() assert.same({ "This", "is", "a", "sentence" }, _.split("%s", "This is a sentence")) assert.same({ "This", "is", "a", "sentence" }, _.split("|", "This|is|a|sentence")) end) it("should gsub strings", function() assert.same("predator", _.gsub("^apex%s*", "", "apex predator")) end) it("should dedent strings", function() assert.equals( [[Lorem Ipsum Dolor Sit Amet]], _.dedent [[ Lorem Ipsum Dolor Sit Amet ]] ) end) it("should transform casing", function() assert.equals("HELLO!", _.to_upper "Hello!") assert.equals("hello!", _.to_lower "Hello!") end) it("trim strings", function() assert.equals("HELLO!", _.trim " HELLO! ") end) it("should trim_start strings", function() assert.equals("HELLO! ", _.trim_start_matches("%s", " HELLO! ")) end) it("should trim_end strings", function() assert.equals(" HELLO!", _.trim_end_matches("%s", " HELLO! ")) end) it("should strip_prefix", function() assert.equals("withthewind", _.strip_prefix("gone", "gonewiththewind")) assert.equals("1.3.0", _.strip_prefix("v", "v1.3.0")) assert.equals("-30", _.strip_prefix("2023-05", "2023-05-30")) assert.equals("The same", _.strip_prefix("Not Equals", "The same")) assert.equals("long_pattern", _.strip_prefix("long_pattern_here", "long_pattern")) assert.equals("", _.strip_prefix("pattern_here", "pattern_here")) assert.equals("s", _.strip_prefix("pattern_here", "pattern_heres")) end) it("should strip_suffix", function() assert.equals("gone", _.strip_suffix("withtthewind", "gonewithtthewind")) assert.equals("name", _.strip_suffix(".tar.gz", "name.tar.gz")) assert.equals("2023", _.strip_suffix("-05-30", "2023-05-30")) assert.equals("The same", _.strip_suffix("Not Equals", "The same")) assert.equals("pattern_here", _.strip_suffix("long_pattern_here", "pattern_here")) assert.equals("", _.strip_suffix("pattern_here", "pattern_here")) assert.equals("s", _.strip_suffix("pattern_here", "spattern_here")) end) end) ================================================ FILE: tests/mason-core/functional/table_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: table", function() it("retrieves property of table", function() assert.equals("hello", _.prop("a", { a = "hello" })) end) it("retrieves nested property of table", function() assert.equals("hello", _.path({ "a", "greeting" }, { a = { greeting = "hello" } })) end) it("picks properties of table", function() local function fn() end assert.same( { ["key1"] = 1, [fn] = 2, }, _.pick({ "key1", fn }, { ["key1"] = 1, [fn] = 2, [3] = 3, }) ) end) it("converts table to pairs", function() assert.same( _.sort_by(_.nth(1), { { "skies", "cloudy", }, { "temperature", "20°", }, }), _.sort_by(_.nth(1), _.to_pairs { skies = "cloudy", temperature = "20°" }) ) end) it("converts pairs to table", function() assert.same( { skies = "cloudy", temperature = "20°" }, _.from_pairs { { "skies", "cloudy", }, { "temperature", "20°", }, } ) end) it("should invert tables", function() assert.same( { val1 = "key1", val2 = "key2", }, _.invert { key1 = "val1", key2 = "val2", } ) end) it("should evolve table", function() assert.same( { non_existent = nil, firstname = "JOHN", lastname = "DOE", age = 42, }, _.evolve({ non_existent = _.always "hello", firstname = _.to_upper, lastname = _.to_upper, age = _.add(2), }, { firstname = "John", lastname = "Doe", age = 40, }) ) end) it("should merge left", function() assert.same( { firstname = "John", lastname = "Doe", }, _.merge_left({ firstname = "John", }, { firstname = "Jane", lastname = "Doe", }) ) end) it("should dissoc keys", function() assert.same({ a = "a", c = "c", }, _.dissoc("b", { a = "a", b = "b", c = "c" })) end) it("should assoc keys", function() assert.same({ a = "a", b = "b", c = "c", }, _.assoc("b", "b", { a = "a", c = "c" })) end) end) describe("table immutability", function() it("should not mutate tables", function() local og_table = setmetatable({ key = "value", imagination = "poor", hotel = "trivago" }, { __newindex = function() error "Tried to newindex" end, }) _.prop("hotel", og_table) _.path({ "hotel" }, og_table) _.pick({ "hotel" }, og_table) _.keys(og_table) _.size(og_table) _.from_pairs(_.to_pairs(og_table)) _.invert(og_table) _.evolve({ hotel = _.to_upper }, og_table) _.merge_left(og_table, {}) _.assoc("new", "value", og_table) _.dissoc("hotel", og_table) assert.same({ key = "value", imagination = "poor", hotel = "trivago" }, og_table) end) end) ================================================ FILE: tests/mason-core/functional/type_spec.lua ================================================ local _ = require "mason-core.functional" describe("functional: type", function() it("should check nil value", function() assert.is_true(_.is_nil(nil)) assert.is_false(_.is_nil(1)) assert.is_false(_.is_nil {}) assert.is_false(_.is_nil(function() end)) end) it("should check types", function() local is_fun = _.is "function" local is_string = _.is "string" local is_number = _.is "number" local is_boolean = _.is "boolean" assert.is_true(is_fun(function() end)) assert.is_false(is_fun(1)) assert.is_true(is_string "") assert.is_false(is_string(1)) assert.is_true(is_number(1)) assert.is_false(is_number "") assert.is_true(is_boolean(true)) assert.is_false(is_boolean(1)) end) it("should check is_list", function() assert.is_true(_.is_list {}) assert.is_true(_.is_list { 1, 2, 3 }) assert.is_true(_.is_list { 1, "a" }) assert.is_false(_.is_list(vim.empty_dict())) assert.is_false(_.is_list { 1, 2, keyed = "value" }) if vim.fn.has "nvim-0.10.0" == 1 then -- meh assert.is_false(_.is_list { 1, 2, nil, 3 }) end end) end) ================================================ FILE: tests/mason-core/installer/InstallHandle_spec.lua ================================================ local InstallHandle = require "mason-core.installer.InstallHandle" local mock = require "luassert.mock" local spy = require "luassert.spy" local stub = require "luassert.stub" describe("InstallHandle ::", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should register spawn handle", function() local handle = InstallHandle:new(mock.new {}) local spawn_handle_change_handler = spy.new() local luv_handle = mock.new {} handle:once("spawn_handles:change", spawn_handle_change_handler) handle:register_spawn_handle(luv_handle, 1337, "tar", { "-xvf", "file" }) assert.same({ uv_handle = luv_handle, pid = 1337, cmd = "tar", args = { "-xvf", "file" }, }, handle:peek_spawn_handle():get()) assert.spy(spawn_handle_change_handler).was_called(1) end) it("should deregister spawn handle", function() local handle = InstallHandle:new(mock.new {}) local spawn_handle_change_handler = spy.new() local luv_handle1 = mock.new {} local luv_handle2 = mock.new {} handle:on("spawn_handles:change", spawn_handle_change_handler) handle:register_spawn_handle(luv_handle1, 42, "curl", { "someurl" }) handle:register_spawn_handle(luv_handle2, 1337, "tar", { "-xvf", "file" }) assert.is_true(handle:deregister_spawn_handle(luv_handle1)) assert.equals(1, #handle.spawn_handles) assert.same({ uv_handle = luv_handle2, pid = 1337, cmd = "tar", args = { "-xvf", "file" }, }, handle:peek_spawn_handle():get()) assert.spy(spawn_handle_change_handler).was_called(3) end) it("should change state", function() local handle = InstallHandle:new(mock.new {}) local state_change_handler = spy.new() handle:once("state:change", state_change_handler) handle:set_state "QUEUED" assert.equals("QUEUED", handle.state) assert.spy(state_change_handler).was_called(1) assert.spy(state_change_handler).was_called_with("QUEUED", "IDLE") end) it("should send signals to registered handles", function() local process = require "mason-core.process" stub(process, "kill") local uv_handle = {} local handle = InstallHandle:new(mock.new {}) local kill_handler = spy.new() handle:once("kill", kill_handler) handle.state = "ACTIVE" handle.spawn_handles = { { uv_handle = uv_handle } } handle:kill(9) assert.spy(process.kill).was_called(1) assert.spy(process.kill).was_called_with(uv_handle, 9) assert.spy(kill_handler).was_called(1) assert.spy(kill_handler).was_called_with(9) end) it("should terminate handle", function() local process = require "mason-core.process" stub(process, "kill") local uv_handle1 = {} local uv_handle2 = {} local handle = InstallHandle:new(mock.new {}) local kill_handler = spy.new() local terminate_handler = spy.new() local closed_handler = spy.new() handle:once("kill", kill_handler) handle:once("terminate", terminate_handler) handle:once("closed", closed_handler) handle.state = "ACTIVE" handle.spawn_handles = { { uv_handle = uv_handle2 }, { uv_handle = uv_handle2 } } handle:terminate() assert.spy(process.kill).was_called(2) assert.spy(process.kill).was_called_with(uv_handle1, 15) assert.spy(process.kill).was_called_with(uv_handle2, 15) assert.spy(kill_handler).was_called(1) assert.spy(kill_handler).was_called_with(15) assert.spy(terminate_handler).was_called(1) assert.is_true(handle.is_terminated) assert.wait(function() assert.is_true(handle:is_closed()) assert.spy(closed_handler).was_called(1) end) end) end) ================================================ FILE: tests/mason-core/installer/InstallRunner_spec.lua ================================================ local InstallHandle = require "mason-core.installer.InstallHandle" local InstallLocation = require "mason-core.installer.InstallLocation" local InstallRunner = require "mason-core.installer.InstallRunner" local fs = require "mason-core.fs" local match = require "luassert.match" local receipt = require "mason-core.receipt" local spy = require "luassert.spy" local stub = require "luassert.stub" local Semaphore = require("mason-core.async.control").Semaphore local a = require "mason-core.async" local registry = require "mason-registry" local test_helpers = require "mason-test.helpers" describe("InstallRunner ::", function() local dummy = registry.get_package "dummy" local dummy2 = registry.get_package "dummy2" local snapshot before_each(function() snapshot = assert.snapshot() if dummy:is_installed() then test_helpers.sync_uninstall(dummy) end if dummy2:is_installed() then test_helpers.sync_uninstall(dummy2) end end) after_each(function() snapshot:revert() end) describe("locking ::", function() it("should respect semaphore locks", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() local dummy_handle = InstallHandle:new(dummy, location) local runner_1 = InstallRunner:new(dummy_handle, semaphore) local runner_2 = InstallRunner:new(InstallHandle:new(dummy2, location), semaphore) stub(dummy.spec.source, "install", function(ctx) ctx:await(function() end) end) spy.on(dummy2.spec.source, "install", function() end) local callback1 = spy.new() local callback2 = spy.new() local run = a.scope(function() runner_1:execute({}, callback1) runner_2:execute({}, callback2) end) run() assert.wait(function() assert.spy(dummy.spec.source.install).was_called(1) assert.spy(dummy2.spec.source.install).was_not_called() end) dummy_handle:terminate() assert.wait(function() assert.spy(dummy2.spec.source.install).was_called(1) end) assert.wait(function() assert.spy(callback1).was_called() assert.spy(callback2).was_called() end) end) it("should write lockfile", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() local dummy_handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(dummy_handle, semaphore) spy.on(fs.async, "write_file") test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(fs.async.write_file).was_called_with(location:lockfile(dummy.name), vim.fn.getpid()) end) end) it("should abort installation if installation lock exists", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() local dummy_handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(dummy_handle, semaphore) stub(fs.async, "file_exists") stub(fs.async, "read_file") fs.async.file_exists.on_call_with(location:lockfile(dummy.name)).returns(true) fs.async.read_file.on_call_with(location:lockfile(dummy.name)).returns "1337" local callback = test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(callback).was_called() assert.spy(callback).was_called_with( false, "Lockfile exists, installation is already running in another process (pid: 1337). Run with :MasonInstall --force to bypass." ) end) end) it("should not abort installation if installation lock exists with force=true", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() local dummy_handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(dummy_handle, semaphore) stub(fs.async, "file_exists") stub(fs.async, "read_file") fs.async.file_exists.on_call_with(location:lockfile(dummy.name)).returns(true) fs.async.read_file.on_call_with(location:lockfile(dummy.name)).returns "1337" local callback = test_helpers.sync_runner_execute(runner, { force = true }) assert.wait(function() assert.spy(callback).was_called() assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) end) end) it("should release lock after successful installation", function() local semaphore = Semaphore:new(1) local location = InstallLocation.global() local dummy_handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(dummy_handle, semaphore) stub(dummy.spec.source, "install", function() a.sleep(1000) end) local callback = spy.new() runner:execute({}, callback) assert.wait(function() assert.is_true(fs.sync.file_exists(location:lockfile(dummy.name))) end) assert.wait(function() assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) end) assert.is_false(fs.sync.file_exists(location:lockfile(dummy.name))) end) end) it("should initialize install location", function() local location = InstallLocation.global() local runner = InstallRunner:new(InstallHandle:new(dummy, location), Semaphore:new(1)) spy.on(location, "initialize") test_helpers.sync_runner_execute(runner, {}) assert.wait(function() assert.spy(location.initialize).was_called(1) end) end) it("should emit failures", function() local registry_spy = spy.new() local package_spy = spy.new() registry:once("package:install:failed", registry_spy) dummy:once("install:failed", package_spy) local location = InstallLocation.global() local handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function() error("I've made a mistake.", 0) end) local callback = test_helpers.sync_runner_execute(runner, {}) assert.spy(registry_spy).was_called(1) assert.spy(registry_spy).was_called_with(match.is_ref(dummy), "I've made a mistake.") assert.spy(package_spy).was_called(1) assert.spy(package_spy).was_called_with "I've made a mistake." assert.spy(callback).was_called(1) assert.spy(callback).was_called_with(false, "I've made a mistake.") end) it("should terminate installation", function() local location = InstallLocation.global() local handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(handle, Semaphore:new(1)) local capture = spy.new() stub(dummy.spec.source, "install", function() capture(1) handle:terminate() a.sleep(0) capture(2) end) local callback = test_helpers.sync_runner_execute(runner, {}) assert.spy(callback).was_called_with(false, "Installation was aborted.") assert.spy(capture).was_called(1) assert.spy(capture).was_called_with(1) end) it("should write debug logs when debug=true", function() local location = InstallLocation.global() local handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink:stdout "Hello " ctx.stdio_sink:stderr "world!" end) local callback = test_helpers.sync_runner_execute(runner, { debug = true }) assert.spy(callback).was_called_with(true, match.instanceof(receipt.InstallReceipt)) assert.is_true(fs.sync.file_exists(location:package "dummy/mason-debug.log")) assert.equals("Hello world!", fs.sync.read_file(location:package "dummy/mason-debug.log")) end) it("should not retain installation directory on failure", function() local location = InstallLocation.global() local handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink:stderr "Something will go terribly wrong.\n" error("This went terribly wrong.", 0) end) local callback = test_helpers.sync_runner_execute(runner, {}) assert.spy(callback).was_called_with(false, "This went terribly wrong.") assert.is_false(fs.sync.dir_exists(location:staging "dummy")) assert.is_false(fs.sync.dir_exists(location:package "dummy")) end) it("should retain installation directory on failure and debug=true", function() local location = InstallLocation.global() local handle = InstallHandle:new(dummy, location) local runner = InstallRunner:new(handle, Semaphore:new(1)) stub(dummy.spec.source, "install", function(ctx) ctx.stdio_sink:stderr "Something will go terribly wrong.\n" error("This went terribly wrong.", 0) end) local callback = test_helpers.sync_runner_execute(runner, { debug = true }) assert.spy(callback).was_called_with(false, "This went terribly wrong.") assert.is_true(fs.sync.dir_exists(location:staging "dummy")) assert.equals( "Something will go terribly wrong.\nThis went terribly wrong.\n", fs.sync.read_file(location:staging "dummy/mason-debug.log") ) end) describe("receipt ::", function() it("should write receipt", function() local location = InstallLocation.global() local runner = InstallRunner:new(InstallHandle:new(dummy, location), Semaphore:new(1)) test_helpers.sync_runner_execute(runner, {}) local receipt_file = location:package "dummy/mason-receipt.json" assert.is_true(fs.sync.file_exists(receipt_file)) assert.is_true(match.tbl_containing { name = "dummy", schema_version = "2.0", install_options = match.same {}, metrics = match.tbl_containing { completion_time = match.is_number(), start_time = match.is_number(), }, registry = match.same { proto = "lua", mod = "dummy-registry.index", }, source = match.same { id = "pkg:mason/dummy@1.0.0", type = "registry+v1", raw = { id = "pkg:mason/dummy@1.0.0", }, }, links = match.same { bin = {}, opt = {}, share = {}, }, }(vim.json.decode(fs.sync.read_file(receipt_file)))) end) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compiler_spec.lua ================================================ local Result = require "mason-core.result" local compiler = require "mason-core.installer.compiler" local match = require "luassert.match" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" local util = require "mason-core.installer.compiler.util" ---@type InstallerCompiler local dummy_compiler = { ---@param source RegistryPackageSource ---@param purl Purl ---@param opts PackageInstallOpts parse = function(source, purl, opts) return Result.try(function(try) return { package = purl.name, extra_info = source.extra_info, should_fail = source.should_fail, } end) end, install = function(ctx, source) if source.should_fail then return Result.failure "This is a failure." else return Result.success() end end, get_versions = function() return Result.success { "v1.0.0", "v2.0.0" } end, } describe("registry compiler :: parsing", function() it("should parse valid package specs", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.parse({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", extra_info = "here", }, }, {}) local parsed = result:get_or_nil() assert.is_true(result:is_success()) assert.is_true(match.is_ref(dummy_compiler)(parsed.compiler)) assert.same({ name = "package-name", scheme = "pkg", type = "dummy", version = "v1.2.3", }, parsed.purl) assert.same({ id = "pkg:dummy/package-name@v1.2.3", package = "package-name", extra_info = "here", }, parsed.source) end) it("should keep unmapped fields", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.parse({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", bin = "node:server.js", }, }, {}) local parsed = result:get_or_nil() assert.is_true(result:is_success()) assert.same({ id = "pkg:dummy/package-name@v1.2.3", package = "package-name", bin = "node:server.js", }, parsed.source) end) it("should reject incompatible schema versions", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.parse({ schema = "registry+v1337", source = { id = "pkg:dummy/package-name@v1.2.3", }, }, {}) assert.same( Result.failure [[Current version of mason.nvim is not capable of parsing package schema version "registry+v1337".]], result ) end) it("should use requested version", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.parse({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", }, }, { version = "v2.0.0" }) assert.is_true(result:is_success()) local parsed = result:get_or_nil() assert.same({ name = "package-name", scheme = "pkg", type = "dummy", version = "v2.0.0", }, parsed.purl) end) it("should handle PLATFORM_UNSUPPORTED", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", supported_platforms = { "VIC64" }, }, }, { version = "v2.0.0" }) assert.same(Result.failure "The current platform is unsupported.", result) end) it("should error upon parsing failures", function() compiler.register_compiler("dummy", dummy_compiler) local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", supported_platforms = { "VIC64" }, }, }, { version = "v2.0.0" }) assert.same(Result.failure "The current platform is unsupported.", result) end) end) describe("registry compiler :: compiling", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should run compiled installer function successfully", function() compiler.register_compiler("dummy", dummy_compiler) spy.on(dummy_compiler, "get_versions") ---@type PackageInstallOpts local opts = {} local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", }, }, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_throw() local ctx = test_helpers.create_context() local installer_result = ctx:execute(installer_fn) assert.same(Result.success(), installer_result) assert.spy(dummy_compiler.get_versions).was_not_called() end) it("should ensure valid version", function() compiler.register_compiler("dummy", dummy_compiler) spy.on(dummy_compiler, "get_versions") ---@type PackageInstallOpts local opts = { version = "v2.0.0" } local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", }, }, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_throw() local ctx = test_helpers.create_context { install_opts = opts } local installer_result = ctx:execute(installer_fn) assert.same(Result.success(), installer_result) assert.spy(dummy_compiler.get_versions).was_called(1) assert.spy(dummy_compiler.get_versions).was_called_with({ name = "package-name", scheme = "pkg", type = "dummy", version = "v2.0.0", }, { id = "pkg:dummy/package-name@v1.2.3", }) end) it("should reject invalid version", function() compiler.register_compiler("dummy", dummy_compiler) spy.on(dummy_compiler, "get_versions") ---@type PackageInstallOpts local opts = { version = "v13.3.7" } local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", }, }, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_throw() local ctx = test_helpers.create_context { install_opts = opts } local err = assert.has_error(function() ctx:execute(installer_fn):get_or_throw() end) assert.equals([[Version "v13.3.7" is not available.]], err) assert.spy(dummy_compiler.get_versions).was_called(1) assert.spy(dummy_compiler.get_versions).was_called_with({ name = "package-name", scheme = "pkg", type = "dummy", version = "v13.3.7", }, { id = "pkg:dummy/package-name@v1.2.3", }) end) it("should raise errors upon installer failures", function() compiler.register_compiler("dummy", dummy_compiler) ---@type PackageInstallOpts local opts = {} local result = compiler.compile_installer({ schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", should_fail = true, }, }, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_nil() local ctx = test_helpers.create_context() local err = assert.has_error(function() ctx:execute(installer_fn):get_or_throw() end) assert.equals("This is a failure.", err) end) it("should register links", function() compiler.register_compiler("dummy", dummy_compiler) local link = require "mason-core.installer.compiler.link" stub(link, "bin", mockx.returns(Result.success())) stub(link, "share", mockx.returns(Result.success())) stub(link, "opt", mockx.returns(Result.success())) local spec = { schema = "registry+v1", source = { id = "pkg:dummy/package-name@v1.2.3", }, bin = { ["exec"] = "exec" }, opt = { ["opt/"] = "opt/" }, share = { ["share/"] = "share/" }, } ---@type PackageInstallOpts local opts = {} local result = compiler.compile_installer(spec, opts) assert.is_true(result:is_success()) local installer_fn = result:get_or_nil() local ctx = test_helpers.create_context() local installer_result = ctx:execute(installer_fn) assert.is_true(installer_result:is_success()) for _, spy in ipairs { link.bin, link.share, link.opt } do assert.spy(spy).was_called(1) assert.spy(spy).was_called_with(match.is_ref(ctx), spec, { scheme = "pkg", type = "dummy", name = "package-name", version = "v1.2.3", }, { id = "pkg:dummy/package-name@v1.2.3", package = "package-name", }) end end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/cargo_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local cargo = require "mason-core.installer.compiler.compilers.cargo" local providers = require "mason-core.providers" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:cargo/crate-name@1.4.3"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("cargo compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { crate = "crate-name", version = "1.4.3", features = nil, locked = true, git = nil, }, cargo.parse({}, purl()) ) end) it("should respect repository_url qualifier", function() assert.same( Result.success { crate = "crate-name", version = "1.4.3", features = nil, locked = true, git = { url = "https://github.com/crate-org/crate-name", rev = false }, }, cargo.parse({}, purl { qualifiers = { repository_url = "https://github.com/crate-org/crate-name" } }) ) end) it("should respect repository_url qualifier with rev=true qualifier", function() assert.same( Result.success { crate = "crate-name", version = "1.4.3", features = nil, locked = true, git = { url = "https://github.com/crate-org/crate-name", rev = true }, }, cargo.parse( {}, purl { qualifiers = { repository_url = "https://github.com/crate-org/crate-name", rev = "true" } } ) ) end) it("should respect features qualifier", function() assert.same( Result.success { crate = "crate-name", version = "1.4.3", features = "lsp,cli", locked = true, git = nil, }, cargo.parse({}, purl { qualifiers = { features = "lsp,cli" } }) ) end) it("should respect locked qualifier", function() assert.same( Result.success { crate = "crate-name", version = "1.4.3", features = nil, locked = false, git = nil, }, cargo.parse({}, purl { qualifiers = { locked = "false" } }) ) end) end) describe("cargo compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install cargo packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.cargo" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return cargo.install(ctx, { crate = "crate-name", version = "1.2.0", features = nil, locked = true, git = nil, }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("crate-name", "1.2.0", { git = nil, features = nil, locked = true, }) end) end) describe("cargo compiler :: versions", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should recognize github cargo source", function() stub(providers.github, "get_all_tags", function() return Result.success { "1.0.0", "2.0.0", "3.0.0" } end) local result = cargo.get_versions(purl { qualifiers = { repository_url = "https://github.com/rust-lang/rust-analyzer", }, }) assert.is_true(result:is_success()) assert.same({ "1.0.0", "2.0.0", "3.0.0" }, result:get_or_throw()) assert.spy(providers.github.get_all_tags).was_called(1) assert.spy(providers.github.get_all_tags).was_called_with "rust-lang/rust-analyzer" end) it("should not provide git commit SHAs", function() local result = cargo.get_versions(purl { qualifiers = { repository_url = "https://github.com/rust-lang/rust-analyzer", rev = "true", }, }) assert.is_false(result:is_success()) assert.equals("Unable to retrieve commit SHAs.", result:err_or_nil()) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/composer_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local composer = require "mason-core.installer.compiler.compilers.composer" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:composer/vendor/package@2.0.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("composer compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "vendor/package", version = "2.0.0", }, composer.parse({}, purl()) ) end) end) describe("composer compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install composer packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.composer" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return composer.install(ctx, { package = "vendor/package", version = "1.2.0", }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("vendor/package", "1.2.0") end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/gem_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local gem = require "mason-core.installer.compiler.compilers.gem" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:gem/package@1.2.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("gem compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "package", version = "1.2.0", extra_packages = { "extra" }, }, gem.parse({ extra_packages = { "extra" } }, purl()) ) end) end) describe("gem compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install gem packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.gem" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return gem.install(ctx, { package = "package", version = "5.2.0", extra_packages = { "extra" }, }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("package", "5.2.0", { extra_packages = { "extra" } }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/generic/build_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local generic = require "mason-core.installer.compiler.compilers.generic" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:generic/namespace/name@v1.2.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("generic compiler :: build :: parsing", function() it("should parse single build target", function() assert.same( Result.success { build = { run = "make build", env = { SOME_VALUE = "here", }, }, }, generic.parse({ build = { run = "make build", env = { SOME_VALUE = "here", }, }, }, purl()) ) end) it("should coalesce build target", function() assert.same( Result.success { build = { target = "linux_arm64", run = "make build", env = { LINUX = "yes", }, }, }, generic.parse({ build = { { target = "linux_arm64", run = "make build", env = { LINUX = "yes", }, }, { target = "win_arm64", run = "make build", env = { WINDOWS = "yes", }, }, }, }, purl(), { target = "linux_arm64" }) ) end) it("should interpolate environment", function() assert.same( Result.success { build = { run = "make build", env = { LINUX = "2023-04-18", }, }, }, generic.parse( { build = { run = "make build", env = { LINUX = "{{version}}", }, }, }, purl { version = "2023-04-18" }, { target = "linux_arm64", } ) ) end) it("should check supported platforms", function() assert.same( Result.failure "PLATFORM_UNSUPPORTED", generic.parse( { build = { { target = "win_arm64", run = "make build", env = { WINDOWS = "yes", }, }, }, }, purl(), { target = "linux_x64", } ) ) end) end) describe("generic compiler :: build :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install", function() local ctx = test_helpers.create_context() local common = require "mason-core.installer.managers.common" stub(common, "run_build_instruction", mockx.returns(Result.success())) local result = ctx:execute(function() return generic.install(ctx, { build = { run = "make", env = { VALUE = "here" }, }, }) end) assert.is_true(result:is_success()) assert.spy(common.run_build_instruction).was_called(1) assert.spy(common.run_build_instruction).was_called_with { run = "make", env = { VALUE = "here" }, } end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/generic/download_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local generic = require "mason-core.installer.compiler.compilers.generic" local match = require "luassert.match" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:generic/namespace/name@v1.2.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("generic compiler :: download :: parsing", function() it("should parse single download target", function() assert.same( Result.success { downloads = { { out_file = "name.tar.gz", download_url = "https://getpackage.org/downloads/1.2.0/name.tar.gz", }, }, download = { files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/1.2.0/name.tar.gz]], }, }, }, generic.parse({ download = { files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/{{version | strip_prefix "v"}}/name.tar.gz]], }, }, }, purl()) ) end) it("should coalesce download target", function() assert.same( Result.success { downloads = { { out_file = "name.tar.gz", download_url = "https://getpackage.org/downloads/linux-aarch64/1.2.0/name.tar.gz", }, }, download = { target = "linux_arm64", files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/linux-aarch64/1.2.0/name.tar.gz]], }, }, }, generic.parse({ download = { { target = "linux_arm64", files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/linux-aarch64/{{version | strip_prefix "v"}}/name.tar.gz]], }, }, { target = "win_arm64", files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/win-aarch64/{{version | strip_prefix "v"}}/name.tar.gz]], }, }, }, }, purl(), { target = "linux_arm64" }) ) end) it("should check supported platforms", function() assert.same( Result.failure "PLATFORM_UNSUPPORTED", generic.parse( { download = { { target = "win_arm64", files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/win-aarch64/{{version | strip_prefix "v"}}/name.tar.gz]], }, }, }, }, purl(), { target = "linux_arm64", } ) ) end) end) describe("generic compiler :: download :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install generic packages", function() local ctx = test_helpers.create_context() local common = require "mason-core.installer.managers.common" stub(common, "download_files", mockx.returns(Result.success())) local result = ctx:execute(function() return generic.install(ctx, { downloads = { { out_file = "name.tar.gz", download_url = "https://getpackage.org/downloads/linux-aarch64/1.2.0/name.tar.gz", }, }, download = { target = "linux_arm64", files = { ["name.tar.gz"] = [[https://getpackage.org/downloads/linux-aarch64/1.2.0/name.tar.gz]], }, }, }) end) assert.is_true(result:is_success()) assert.spy(common.download_files).was_called(1) assert.spy(common.download_files).was_called_with(match.is_ref(ctx), { { out_file = "name.tar.gz", download_url = "https://getpackage.org/downloads/linux-aarch64/1.2.0/name.tar.gz", }, }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/github/build_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local github = require "mason-core.installer.compiler.compilers.github" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:github/namespace/name@2023-03-09"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("github compiler :: build :: parsing", function() it("should parse build source", function() assert.same( Result.success { build = { run = [[npm install && npm run compile]], env = {}, }, repo = "https://github.com/namespace/name.git", rev = "2023-03-09", }, github.parse({ build = { run = [[npm install && npm run compile]], }, }, purl()) ) end) it("should parse build source with multiple targets", function() assert.same( Result.success { build = { target = "win_x64", run = [[npm install]], env = {}, }, repo = "https://github.com/namespace/name.git", rev = "2023-03-09", }, github.parse({ build = { { target = "linux_arm64", run = [[npm install && npm run compile]], }, { target = "win_x64", run = [[npm install]], }, }, }, purl(), { target = "win_x64" }) ) end) end) describe("github compiler :: build :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install github build sources", function() local ctx = test_helpers.create_context() local std = require "mason-core.installer.managers.std" local common = require "mason-core.installer.managers.common" stub(std, "clone", mockx.returns(Result.success())) stub(common, "run_build_instruction", mockx.returns(Result.success())) local result = ctx:execute(function() return github.install(ctx, { repo = "namespace/name", rev = "2023-03-09", build = { run = [[npm install && npm run compile]], env = { SOME_VALUE = "here", }, }, }, purl()) end) assert.is_true(result:is_success()) assert.spy(std.clone).was_called(1) assert.spy(std.clone).was_called_with("namespace/name", { rev = "2023-03-09" }) assert.spy(common.run_build_instruction).was_called(1) assert.spy(common.run_build_instruction).was_called_with { run = [[npm install && npm run compile]], env = { SOME_VALUE = "here", }, } end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/github/release_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local common = require "mason-core.installer.managers.common" local compiler = require "mason-core.installer.compiler" local github = require "mason-core.installer.compiler.compilers.github" local match = require "luassert.match" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:github/namespace/name@2023-03-09"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("github compiler :: release :: parsing", function() it("should parse release asset source", function() assert.same( Result.success { repo = "namespace/name", asset = { file = "file-2023-03-09.jar", }, downloads = { { out_file = "file-2023-03-09.jar", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/file-2023-03-09.jar", }, }, }, github.parse({ asset = { file = "file-{{version}}.jar", }, }, purl()) ) end) it("should parse release asset source with multiple targets", function() assert.same( Result.success { repo = "namespace/name", asset = { target = "linux_x64", file = "file-linux-amd64-2023-03-09.tar.gz", }, downloads = { { out_file = "file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/file-linux-amd64-2023-03-09.tar.gz", }, }, }, github.parse({ asset = { { target = "win_arm", file = "file-win-arm-{{version}}.zip", }, { target = "linux_x64", file = "file-linux-amd64-{{version}}.tar.gz", }, }, }, purl(), { target = "linux_x64" }) ) end) it("should parse release asset source with output to different directory", function() assert.same( Result.success { repo = "namespace/name", asset = { file = "out-dir/file-linux-amd64-2023-03-09.tar.gz", }, downloads = { { out_file = "out-dir/file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/file-linux-amd64-2023-03-09.tar.gz", }, }, }, github.parse({ asset = { file = "file-linux-amd64-{{version}}.tar.gz:out-dir/", }, }, purl(), { target = "linux_x64" }) ) end) it("should expand returned asset.file to point to out_file", function() assert.same( Result.success { repo = "namespace/name", asset = { file = { "out-dir/linux-amd64-2023-03-09.tar.gz", "LICENSE.txt", "README.md", }, }, downloads = { { out_file = "out-dir/linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/linux-amd64-2023-03-09.tar.gz", }, { out_file = "LICENSE.txt", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/license", }, { out_file = "README.md", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/README.md", }, }, }, github.parse({ asset = { file = { "linux-amd64-{{version}}.tar.gz:out-dir/", "license:LICENSE.txt", "README.md", }, }, }, purl(), { target = "linux_x64" }) ) end) it("should interpolate asset table", function() assert.same( Result.success { repo = "namespace/name", asset = { file = "linux-amd64-2023-03-09.tar.gz", bin = "linux-amd64-2023-03-09", }, downloads = { { out_file = "linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/linux-amd64-2023-03-09.tar.gz", }, }, }, github.parse({ asset = { file = "linux-amd64-{{version}}.tar.gz", bin = "linux-amd64-{{version}}", }, }, purl(), { target = "linux_x64" }) ) end) it("should parse build source", function() assert.same( Result.success { build = { run = [[npm install && npm run compile]], env = {}, }, repo = "https://github.com/namespace/name.git", rev = "2023-03-09", }, github.parse({ build = { run = [[npm install && npm run compile]], }, }, purl()) ) end) it("should parse build source with multiple targets", function() assert.same( Result.success { build = { target = "win_x64", run = [[npm install]], env = {}, }, repo = "https://github.com/namespace/name.git", rev = "2023-03-09", }, github.parse({ build = { { target = "linux_arm64", run = [[npm install && npm run compile]], }, { target = "win_x64", run = [[npm install]], }, }, }, purl(), { target = "win_x64" }) ) end) it("should upsert version overrides", function() local result = compiler.parse({ schema = "registry+v1", source = { id = "pkg:github/owner/repo@1.2.3", asset = { { target = "darwin_x64", file = "asset.tar.gz", }, }, version_overrides = { { constraint = "semver:<=1.0.0", id = "pkg:github/owner/repo@1.0.0", asset = { { target = "darwin_x64", file = "old-asset.tar.gz", }, }, }, }, }, }, { version = "1.0.0", target = "darwin_x64" }) local parsed = result:get_or_nil() assert.is_true(result:is_success()) assert.same({ id = "pkg:github/owner/repo@1.0.0", asset = { target = "darwin_x64", file = "old-asset.tar.gz", }, downloads = { { download_url = "https://github.com/owner/repo/releases/download/1.0.0/old-asset.tar.gz", out_file = "old-asset.tar.gz", }, }, repo = "owner/repo", }, parsed.source) end) it("should override source if version override provides its own purl id", function() local result = compiler.parse({ schema = "registry+v1", source = { id = "pkg:github/owner/repo@1.2.3", asset = { file = "asset.tar.gz", }, version_overrides = { { constraint = "semver:<=1.0.0", id = "pkg:npm/old-package", }, }, }, }, { version = "1.0.0", target = "darwin_x64" }) assert.is_true(result:is_success()) local parsed = result:get_or_throw() assert.same({ type = "npm", scheme = "pkg", name = "old-package", version = "1.0.0", }, parsed.purl) end) end) describe("github compiler :: release :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install github release assets", function() local ctx = test_helpers.create_context() local std = require "mason-core.installer.managers.std" stub(std, "download_file", mockx.returns(Result.success())) stub(std, "unpack", mockx.returns(Result.success())) stub(common, "download_files", mockx.returns(Result.success())) local result = ctx:execute(function() return github.install(ctx, { repo = "namespace/name", asset = { file = "file-linux-amd64-2023-03-09.tar.gz", }, downloads = { { out_file = "file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/file-linux-amd64-2023-03-09.tar.gz", }, { out_file = "another-file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/another-file-linux-amd64-2023-03-09.tar.gz", }, }, }) end) assert.is_true(result:is_success()) assert.spy(common.download_files).was_called(1) assert.spy(common.download_files).was_called_with(match.is_ref(ctx), { { out_file = "file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/file-linux-amd64-2023-03-09.tar.gz", }, { out_file = "another-file-linux-amd64-2023-03-09.tar.gz", download_url = "https://github.com/namespace/name/releases/download/2023-03-09/another-file-linux-amd64-2023-03-09.tar.gz", }, }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/golang_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local golang = require "mason-core.installer.compiler.compilers.golang" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:golang/namespace/package@v1.5.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("golang compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "namespace/package", version = "v1.5.0", extra_packages = { "extra" }, }, golang.parse({ extra_packages = { "extra" } }, purl()) ) end) end) describe("golang compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install golang packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.golang" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return golang.install(ctx, { package = "namespace/package", version = "v1.5.0", extra_packages = { "extra" }, }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("namespace/package", "v1.5.0", { extra_packages = { "extra" } }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/luarocks_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local luarocks = require "mason-core.installer.compiler.compilers.luarocks" local match = require "luassert.match" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:luarocks/namespace/name@1.0.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("luarocks compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "namespace/name", version = "1.0.0", server = nil, dev = false, }, luarocks.parse({}, purl()) ) end) it("should parse package dev flag", function() assert.same( Result.success { package = "namespace/name", version = "1.0.0", server = nil, dev = true, }, luarocks.parse({}, purl { qualifiers = { dev = "true" } }) ) end) it("should parse package server flag", function() assert.same( Result.success { package = "namespace/name", version = "1.0.0", server = "https://luarocks.org/dev", dev = false, }, luarocks.parse({}, purl { qualifiers = { repository_url = "https://luarocks.org/dev" } }) ) end) end) describe("luarocks compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install luarocks packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.luarocks" local ret_val = Result.success() stub(manager, "install", mockx.returns(ret_val)) local result = ctx:execute(function() return luarocks.install(ctx, { package = "namespace/name", version = "1.0.0", server = "https://luarocks.org/dev", dev = false, }) end) assert.is_true(match.is_ref(ret_val)(result)) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("namespace/name", "1.0.0", { dev = false, server = "https://luarocks.org/dev", }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/npm_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local npm = require "mason-core.installer.compiler.compilers.npm" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:npm/%40namespace/package@v1.5.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("npm compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "@namespace/package", version = "v1.5.0", extra_packages = { "extra" }, }, npm.parse({ extra_packages = { "extra" } }, purl()) ) end) end) describe("npm compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install npm packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.npm" stub(manager, "init", mockx.returns(Result.success())) stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return npm.install(ctx, { package = "@namespace/package", version = "v1.5.0", extra_packages = { "extra" }, }) end) assert.is_true(result:is_success()) assert.spy(manager.init).was_called(1) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("@namespace/package", "v1.5.0", { extra_packages = { "extra" } }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/nuget_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local nuget = require "mason-core.installer.compiler.compilers.nuget" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:nuget/package@2.2.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("nuget compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "package", version = "2.2.0", }, nuget.parse({}, purl()) ) end) end) describe("nuget compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install nuget packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.nuget" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return nuget.install(ctx, { package = "package", version = "1.5.0", }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("package", "1.5.0") end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/opam_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local opam = require "mason-core.installer.compiler.compilers.opam" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:opam/package@2.2.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("opam compiler :: parsing", function() it("should parse package", function() assert.same( Result.success { package = "package", version = "2.2.0", }, opam.parse({}, purl()) ) end) end) describe("opam compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install opam packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.opam" stub(manager, "install", mockx.returns(Result.success())) local result = ctx:execute(function() return opam.install(ctx, { package = "package", version = "1.5.0", }) end) assert.is_true(result:is_success()) assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with("package", "1.5.0") end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/openvsx_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local common = require "mason-core.installer.managers.common" local match = require "luassert.match" local openvsx = require "mason-core.installer.compiler.compilers.openvsx" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:openvsx/namespace/name@1.10.1"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("openvsx provider :: download :: parsing", function() it("should parse download source", function() assert.same( Result.success { download = { file = "file-1.10.1.jar", }, downloads = { { out_file = "file-1.10.1.jar", download_url = "https://open-vsx.org/api/namespace/name/1.10.1/file/file-1.10.1.jar", }, }, }, openvsx.parse({ download = { file = "file-{{version}}.jar", }, }, purl()) ) end) it("should parse download source with multiple targets", function() assert.same( Result.success { download = { target = "linux_x64", file = "file-linux-amd64-1.0.0.vsix", }, downloads = { { out_file = "file-linux-amd64-1.0.0.vsix", download_url = "https://open-vsx.org/api/namespace/name/1.0.0/file/file-linux-amd64-1.0.0.vsix", }, }, }, openvsx.parse({ download = { { target = "win_arm", file = "file-win-arm-{{version}}.vsix", }, { target = "linux_x64", file = "file-linux-amd64-{{version}}.vsix", }, }, }, purl { version = "1.0.0" }, { target = "linux_x64" }) ) end) it("should parse download source with output to different directory", function() assert.same( Result.success { download = { file = "out-dir/file-linux-amd64-1.10.1.vsix", }, downloads = { { out_file = "out-dir/file-linux-amd64-1.10.1.vsix", download_url = "https://open-vsx.org/api/namespace/name/1.10.1/file/file-linux-amd64-1.10.1.vsix", }, }, }, openvsx.parse({ download = { file = "file-linux-amd64-{{version}}.vsix:out-dir/", }, }, purl(), { target = "linux_x64" }) ) end) it("should recognize target_platform when available", function() assert.same( Result.success { download = { file = "file-linux-1.10.1@win32-arm64.vsix", target = "win_arm64", target_platform = "win32-arm64", }, downloads = { { out_file = "file-linux-1.10.1@win32-arm64.vsix", download_url = "https://open-vsx.org/api/namespace/name/win32-arm64/1.10.1/file/file-linux-1.10.1@win32-arm64.vsix", }, }, }, openvsx.parse({ download = { { target = "win_arm64", file = "file-linux-{{version}}@win32-arm64.vsix", target_platform = "win32-arm64", }, }, }, purl(), { target = "win_arm64" }) ) end) end) describe("openvsx provider :: download :: installing", function() it("should install openvsx assets", function() local ctx = test_helpers.create_context() stub(common, "download_files", mockx.returns(Result.success())) local result = ctx:execute(function() return openvsx.install(ctx, { download = { file = "file-1.10.1.jar", }, downloads = { { out_file = "file-1.10.1.jar", download_url = "https://open-vsx.org/api/namespace/name/1.10.1/file/file-1.10.1.jar", }, }, }) end) assert.is_true(result:is_success()) assert.spy(common.download_files).was_called(1) assert.spy(common.download_files).was_called_with(match.is_ref(ctx), { { out_file = "file-1.10.1.jar", download_url = "https://open-vsx.org/api/namespace/name/1.10.1/file/file-1.10.1.jar", }, }) end) end) ================================================ FILE: tests/mason-core/installer/compiler/compilers/pypi_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local pypi = require "mason-core.installer.compiler.compilers.pypi" local settings = require "mason.settings" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param overrides Purl local function purl(overrides) local purl = Purl.parse("pkg:pypi/package@5.5.0"):get_or_throw() if not overrides then return purl end return vim.tbl_deep_extend("force", purl, overrides) end describe("pypi compiler :: parsing", function() it("should parse package", function() settings.set { pip = { install_args = { "--proxy", "http://localghost" }, upgrade_pip = true, }, } assert.same( Result.success { package = "package", version = "5.5.0", extra_packages = { "extra" }, pip = { upgrade = true, extra_args = { "--proxy", "http://localghost" }, }, }, pypi.parse({ extra_packages = { "extra" } }, purl()) ) settings.set(settings._DEFAULT_SETTINGS) end) end) describe("pypi compiler :: installing", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install pypi packages", function() local ctx = test_helpers.create_context() local manager = require "mason-core.installer.managers.pypi" stub(manager, "init", mockx.returns(Result.success())) stub(manager, "install", mockx.returns(Result.success())) settings.set { pip = { install_args = { "--proxy", "http://localghost" }, upgrade_pip = true, }, } local result = ctx:execute(function() return pypi.install(ctx, { package = "package", extra = "lsp", version = "1.5.0", extra_packages = { "extra" }, pip = { upgrade = true, extra_args = { "--proxy", "http://localghost" }, }, }) end) assert.is_true(result:is_success()) assert.spy(manager.init).was_called(1) assert.spy(manager.init).was_called_with { package = { name = "package", version = "1.5.0" }, upgrade_pip = true, install_extra_args = { "--proxy", "http://localghost" }, } assert.spy(manager.install).was_called(1) assert.spy(manager.install).was_called_with( "package", "1.5.0", { extra = "lsp", extra_packages = { "extra" }, install_extra_args = { "--proxy", "http://localghost" } } ) settings.set(settings._DEFAULT_SETTINGS) end) end) ================================================ FILE: tests/mason-core/installer/compiler/expr_spec.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local expr = require "mason-core.installer.compiler.expr" local match = require "luassert.match" describe("registry expressions", function() it("should eval simple expressions", function() assert.same(Result.success "Hello, world!", expr.interpolate("Hello, world!", {})) assert.same( Result.success "Hello, John Doe!", expr.interpolate("Hello, {{firstname}} {{ lastname }}!", { firstname = "John", lastname = "Doe", }) ) end) it("should eval nested access", function() assert.same( Result.success "Hello, world!", expr.interpolate("Hello, {{greeting.name}}!", { greeting = { name = "world" } }) ) end) it("should eval benign expressions", function() assert.same( Result.success "Hello, JOHNDOE JR.!", expr.interpolate("Hello, {{greeting.firstname .. greeting.lastname .. tostring(tbl) | to_upper}}!", { greeting = { firstname = "John", lastname = "Doe" }, tostring = tostring, tbl = setmetatable({}, { __tostring = function() return " Jr." end, }), }) ) assert.same( Result.success "Gloves", expr.interpolate("G{{ 'Cloves' | strip_prefix(trim) }}", { trim = "C", }) ) end) it("should eval expressions with filters", function() assert.same( Result.success "Hello, MR. John!", expr.interpolate("Hello, {{prefix|to_upper}} {{ name | trim }}!", { prefix = "Mr.", trim = _.trim, name = " John ", }) ) assert.same( Result.success "Hello, Sir MR. John!", expr.interpolate("Hello, {{prefix|to_upper | format 'Sir %s'}} {{ name | trim }}!", { format = _.format, trim = _.trim, prefix = "Mr.", name = " John ", }) ) end) it("should not interpolate nil values", function() assert.same(Result.success "Hello, ", expr.interpolate("Hello, {{non_existent}}", {})) assert.same(Result.success "", expr.interpolate("{{non_existent}}", {})) end) it("should error if piping nil values to functions that require non-nil values", function() local err = assert.has_error(function() expr.interpolate("Hello, {{ non_existent | to_upper }}", {}):get_or_throw() end) assert.is_true(match.matches "attempt to index local 'str' %(a nil value%)$"(err)) end) it("should reject invalid filters", function() assert.is_true( match.matches [[^.*Invalid filter expression: "whut"]]( expr.interpolate("Hello, {{ value | whut }}", { value = "value" }):err_or_nil() ) ) assert.is_true( match.matches [[^.*Failed to parse expression: "wh%-!uut"]]( expr.interpolate("Hello, {{ value | wh-!uut }}", { value = "value" }):err_or_nil() ) ) end) end) describe("expr filters :: equals/not_equals", function() it("should equals", function() assert.same( Result.success "true", expr.interpolate("{{equals('Hello, world!', value)}}", { value = "Hello, world!", }) ) assert.same( Result.success "true", expr.interpolate("{{ value | equals('Hello, world!') }}", { value = "Hello, world!", }) ) assert.same( Result.success "false", expr.interpolate("{{ value | equals('Hello, John!') }}", { value = "Hello, world!", }) ) end) it("should not equals", function() assert.same( Result.success "true", expr.interpolate("{{not_equals('Hello, John!', value)}}", { value = "Hello, world!", }) ) assert.same( Result.success "true", expr.interpolate("{{ value | not_equals('Hello, John!') }}", { value = "Hello, world!", }) ) assert.same( Result.success "false", expr.interpolate("{{ value | not_equals('Hello, world!') }}", { value = "Hello, world!", }) ) end) end) describe("expr filters :: take_if{_not}", function() it("should take if value matches", function() assert.same( Result.success "Hello, world!", expr.interpolate("Hello, {{ take_if(equals('world!'), value) }}", { value = "world!", }) ) assert.same( Result.success "Hello, world!", expr.interpolate("Hello, {{ value | take_if(equals('world!')) }}", { value = "world!", }) ) assert.same( Result.success "", expr.interpolate("{{ take_if(equals('Hello John!'), greeting) }}", { greeting = "Hello World!", }) ) assert.same( Result.success "", expr.interpolate("{{ take_if(false, greeting) }}", { greeting = "Hello World!", }) ) assert.same( Result.success "Hello World!", expr.interpolate("{{ take_if(true, greeting) }}", { greeting = "Hello World!", }) ) end) it("should not take if value matches", function() assert.same( Result.success "Hello, world!", expr.interpolate("Hello, {{ take_if_not(equals('John!'), value) }}", { value = "world!", }) ) assert.same( Result.success "Hello, world!", expr.interpolate("Hello, {{ value | take_if_not(equals('john!')) }}", { value = "world!", }) ) assert.same( Result.success "", expr.interpolate("{{ take_if_not(equals('Hello World!'), greeting) }}", { greeting = "Hello World!", }) ) assert.same( Result.success "Hello World!", expr.interpolate("{{ take_if_not(false, greeting) }}", { greeting = "Hello World!", }) ) assert.same( Result.success "", expr.interpolate("{{ take_if_not(true, greeting) }}", { greeting = "Hello World!", }) ) end) end) describe("expr filters :: strip_{suffix,prefix}", function() it("should strip prefix", function() assert.same( Result.success "1.0.0", expr.interpolate([[{{value | strip_prefix("v") }}]], { value = "v1.0.0", }) ) end) it("should strip suffix", function() assert.same( Result.success "bin/file", expr.interpolate([[{{value | strip_suffix(".tar.gz") }}]], { value = "bin/file.tar.gz", }) ) end) end) describe("table interpolation", function() it("should interpolate nested values", function() assert.same( Result.success { some = { nested = { value = "here", }, }, }, expr.tbl_interpolate({ some = { nested = { value = "{{value}}", }, }, }, { value = "here" }) ) end) it("should only only interpolate string values", function() assert.same( Result.success { a = 1, b = { c = 2 }, d = "Hello!", }, expr.tbl_interpolate({ a = 1, b = { c = 2 }, d = "Hello!", }, {}) ) end) it("should interpolate string keys", function() assert.same( Result.success { ["a-1.2.3"] = "1.2.3", [12] = "12", }, expr.tbl_interpolate({ ["a-{{version}}"] = "{{version}}", [12] = "12", }, { version = "1.2.3" }) ) end) end) ================================================ FILE: tests/mason-core/installer/compiler/link_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" local fs = require "mason-core.fs" local link = require "mason-core.installer.compiler.link" local match = require "luassert.match" local path = require "mason-core.path" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("registry linker", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should expand bin table", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(ctx.fs, "chmod") stub(ctx.fs, "fstat") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "exec.sh").returns(true) ctx.fs.fstat.on_call_with(match.is_ref(ctx.fs), "exec.sh").returns { mode = 493, -- 0755 } local result = link.bin( ctx, { bin = { ["exec"] = "exec.sh", }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), { metadata = "value", } ) assert.same( Result.success { ["exec"] = "exec.sh", }, result ) assert.same({ ["exec"] = "exec.sh", }, ctx.links.bin) assert.spy(ctx.fs.chmod).was_not_called() end) it("should chmod executable if necessary", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(ctx.fs, "chmod") stub(ctx.fs, "fstat") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "exec.sh").returns(true) ctx.fs.fstat.on_call_with(match.is_ref(ctx.fs), "exec.sh").returns { mode = 420, -- 0644 } local result = link.bin( ctx, { bin = { ["exec"] = "exec.sh", }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), { metadata = "value", } ) assert.is_true(result:is_success()) assert.spy(ctx.fs.chmod).was_called(1) assert.spy(ctx.fs.chmod).was_called_with(match.is_ref(ctx.fs), "exec.sh", 493) end) it("should interpolate bin table", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(ctx.fs, "chmod") stub(ctx.fs, "fstat") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "v1.0.0-exec.sh").returns(true) ctx.fs.fstat.on_call_with(match.is_ref(ctx.fs), "v1.0.0-exec.sh").returns { mode = 493, -- 0755 } local result = link.bin( ctx, { bin = { ["exec"] = "{{version}}-{{source.script}}", }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), { script = "exec.sh", } ) assert.same( Result.success { ["exec"] = "v1.0.0-exec.sh", }, result ) end) it("should delegate bin paths", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(ctx.fs, "chmod") stub(ctx.fs, "fstat") local matrix = { ["cargo:executable"] = "bin/executable", ["composer:executable"] = "vendor/bin/executable", ["golang:executable"] = "executable", ["luarocks:executable"] = "bin/executable", ["npm:executable"] = "node_modules/.bin/executable", ["nuget:executable"] = "executable", ["opam:executable"] = "bin/executable", -- ["pypi:executable"] = "venv/bin/executable", } for bin, path in pairs(matrix) do ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), path).returns(true) ctx.fs.fstat.on_call_with(match.is_ref(ctx.fs), path).returns { mode = 493, -- 0755 } local result = link.bin(ctx, { bin = { ["executable"] = bin, }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), {}) assert.same( Result.success { ["executable"] = path, }, result ) end end) it("should register share links", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(fs.sync, "file_exists") stub(vim.fn, "glob") vim.fn.glob.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0/dir/" } .. "**/*", false, true).returns { path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file1" }, path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file2" }, path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file3" }, } fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file1" }).returns(true) fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file2" }).returns(true) fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file3" }).returns(true) local result = link.share( ctx, { share = { ["file"] = "{{version}}-{{source.file}}", ["dir/"] = "{{version}}/dir/", ["empty/"] = "{{source.empty}}", }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), { file = "file", } ) assert.same( Result.success { ["file"] = "v1.0.0-file", ["dir/file1"] = "v1.0.0/dir/file1", ["dir/file2"] = "v1.0.0/dir/file2", ["dir/file3"] = "v1.0.0/dir/file3", }, result ) assert.same({ ["file"] = "v1.0.0-file", ["dir/file1"] = "v1.0.0/dir/file1", ["dir/file2"] = "v1.0.0/dir/file2", ["dir/file3"] = "v1.0.0/dir/file3", }, ctx.links.share) end) it("should register opt links", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(fs.sync, "file_exists") stub(vim.fn, "glob") vim.fn.glob.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0/dir/" } .. "**/*", false, true).returns { path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file1" }, path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file2" }, path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file3" }, } fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file1" }).returns(true) fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file2" }).returns(true) fs.sync.file_exists.on_call_with(path.concat { ctx.cwd:get(), "v1.0.0", "dir", "file3" }).returns(true) local result = link.opt( ctx, { opt = { ["file"] = "{{version}}-{{source.file}}", ["dir/"] = "{{version}}/dir/", ["empty/"] = "{{source.empty}}", }, }, Purl.parse("pkg:dummy/package@v1.0.0"):get_or_throw(), { file = "file", } ) assert.same( Result.success { ["file"] = "v1.0.0-file", ["dir/file1"] = "v1.0.0/dir/file1", ["dir/file2"] = "v1.0.0/dir/file2", ["dir/file3"] = "v1.0.0/dir/file3", }, result ) assert.same({ ["file"] = "v1.0.0-file", ["dir/file1"] = "v1.0.0/dir/file1", ["dir/file2"] = "v1.0.0/dir/file2", ["dir/file3"] = "v1.0.0/dir/file3", }, ctx.links.opt) end) end) ================================================ FILE: tests/mason-core/installer/compiler/util_spec.lua ================================================ local Result = require "mason-core.result" local match = require "luassert.match" local platform = require "mason-core.platform" local test_helpers = require "mason-test.helpers" local util = require "mason-core.installer.compiler.util" describe("registry installer util", function() it("should coalesce single target", function() local source = { value = "here" } local coalesced = util.coalesce_by_target(source, {}):get_or_nil() assert.is_true(match.is_ref(source)(coalesced)) end) it("should coalesce multiple targets", function() local source = { target = "VIC64", value = "here" } local coalesced = util.coalesce_by_target({ { target = "linux_arm64", value = "here", }, source, }, { target = "VIC64" }):get_or_nil() assert.is_true(match.is_ref(source)(coalesced)) end) it("should accept valid platform", function() platform.is.VIC64 = true local result = util.ensure_valid_platform { "VIC64", "linux_arm64", } assert.is_true(result:is_success()) platform.is.VIC64 = nil end) it("should reject invalid platform", function() local result = util.ensure_valid_platform { "VIC64" } assert.same(Result.failure "PLATFORM_UNSUPPORTED", result) end) it("should accept valid version", function() local ctx = test_helpers.create_context { install_opts = { version = "1.0.0" } } local result = ctx:execute(function() return util.ensure_valid_version(function() return Result.success { "1.0.0", "2.0.0", "3.0.0" } end) end) assert.is_true(result:is_success()) end) it("should reject invalid version", function() local ctx = test_helpers.create_context { install_opts = { version = "13.3.7" } } local result = ctx:execute(function() return util.ensure_valid_version(function() return Result.success { "1.0.0", "2.0.0", "3.0.0" } end) end) assert.same(Result.failure [[Version "13.3.7" is not available.]], result) end) it("should gracefully accept version if unable to resolve available versions", function() local ctx = test_helpers.create_context { install_opts = { version = "13.3.7" } } local result = ctx:execute(function() return util.ensure_valid_version(function() return Result.failure() end) end) assert.is_true(result:is_success()) end) it("should accept version if in force mode", function() local ctx = test_helpers.create_context { install_opts = { version = "13.3.7", force = true } } local result = ctx:execute(function() return util.ensure_valid_version(function() return Result.success { "1.0.0" } end) end) assert.is_true(result:is_success()) end) end) ================================================ FILE: tests/mason-core/installer/context_spec.lua ================================================ local a = require "mason-core.async" local match = require "luassert.match" local path = require "mason-core.path" local pypi = require "mason-core.installer.managers.pypi" local registry = require "mason-registry" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("installer", function() ---@module "mason-core.platform" local platform local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) before_each(function() package.loaded["mason-core.installer.platform"] = nil package.loaded["mason-core.installer.context"] = nil platform = require "mason-core.platform" end) it("should write shell exec wrapper on Unix", function() local ctx = test_helpers.create_context() stub(ctx.fs, "write_file") stub(ctx.fs, "file_exists") stub(ctx.fs, "dir_exists") stub(ctx.fs, "chmod_exec") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "my-executable").returns(false) ctx.fs.dir_exists.on_call_with(match.is_ref(ctx.fs), "my-executable").returns(false) ctx:write_shell_exec_wrapper("my-executable", "bash -c 'echo $GREETING'", { GREETING = "Hello World!", }) assert.spy(ctx.fs.write_file).was_called(1) assert.spy(ctx.fs.write_file).was_called_with( match.is_ref(ctx.fs), "my-executable", [[#!/usr/bin/env bash export GREETING="Hello World!" exec bash -c 'echo $GREETING' "$@"]] ) end) it("should write shell exec wrapper on Windows", function() platform.is.darwin = false platform.is.mac = false platform.is.unix = false platform.is.linux = false platform.is.win = true local ctx = test_helpers.create_context() stub(ctx.fs, "write_file") stub(ctx.fs, "file_exists") stub(ctx.fs, "dir_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "my-executable").returns(false) ctx.fs.dir_exists.on_call_with(match.is_ref(ctx.fs), "my-executable").returns(false) ctx:write_shell_exec_wrapper("my-executable", "cmd.exe /C echo %GREETING%", { GREETING = "Hello World!", }) assert.spy(ctx.fs.write_file).was_called(1) assert.spy(ctx.fs.write_file).was_called_with( match.is_ref(ctx.fs), "my-executable.cmd", [[@ECHO off SET GREETING=Hello World! cmd.exe /C echo %GREETING% %*]] ) end) it("should not write shell exec wrapper if new executable path already exists", function() local exec_rel_path = path.concat { "obscure", "path", "to", "server" } local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") stub(ctx.fs, "dir_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), exec_rel_path).returns(true) ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), "my-wrapper-script").returns(true) ctx.fs.dir_exists.on_call_with(match.is_ref(ctx.fs), "my-wrapper-script").returns(true) local err = assert.has_error(function() ctx:write_shell_exec_wrapper("my-wrapper-script", "contents") end) assert.equals([[Cannot write exec wrapper to "my-wrapper-script" because the file already exists.]], err) end) it("should write Node exec wrapper", function() local js_rel_path = path.concat { "some", "obscure", "path", "server.js" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), js_rel_path).returns(true) ctx:write_node_exec_wrapper("my-wrapper-script", js_rel_path) assert.spy(ctx.write_shell_exec_wrapper).was_called(1) assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", ("node %q"):format(path.concat { ctx:get_install_path(), js_rel_path }) ) end) it("should write Ruby exec wrapper", function() local js_rel_path = path.concat { "some", "obscure", "path", "server.js" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), js_rel_path).returns(true) ctx:write_ruby_exec_wrapper("my-wrapper-script", js_rel_path) assert.spy(ctx.write_shell_exec_wrapper).was_called(1) assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", ("ruby %q"):format(path.concat { ctx:get_install_path(), js_rel_path }) ) end) it("should not write Node exec wrapper if the target script doesn't exist", function() local js_rel_path = path.concat { "some", "obscure", "path", "server.js" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), js_rel_path).returns(false) local err = assert.has_error(function() ctx:write_node_exec_wrapper("my-wrapper-script", js_rel_path) end) assert.equals( [[Cannot write Node exec wrapper for path "some/obscure/path/server.js" as it doesn't exist.]], err ) assert.spy(ctx.write_shell_exec_wrapper).was_called(0) end) it("should write Python exec wrapper", function() local ctx = test_helpers.create_context() stub(ctx.cwd, "get") ctx.cwd.get.returns "/tmp/placeholder" stub(ctx, "write_shell_exec_wrapper") ctx:write_pyvenv_exec_wrapper("my-wrapper-script", "my-module") assert.spy(ctx.write_shell_exec_wrapper).was_called(1) assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", ("%q -m my-module"):format(path.concat { pypi.venv_path(ctx:get_install_path()), "python" }) ) end) it("should not write Python exec wrapper if module cannot be found", function() local ctx = test_helpers.create_context() stub(ctx.cwd, "get") ctx.cwd.get.returns "/tmp/placeholder" stub(ctx, "write_shell_exec_wrapper") stub(ctx.spawn, "python") ctx.spawn.python.invokes(function() error "" end) local err = assert.has_error(function() ctx:write_pyvenv_exec_wrapper("my-wrapper-script", "my-module") end) assert.equals([[Cannot write Python exec wrapper for module "my-module" as it doesn't exist.]], err) assert.spy(ctx.write_shell_exec_wrapper).was_called(0) end) it("should write exec wrapper", function() local exec_rel_path = path.concat { "obscure", "path", "to", "server" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), exec_rel_path).returns(true) ctx:write_exec_wrapper("my-wrapper-script", exec_rel_path) assert.spy(ctx.write_shell_exec_wrapper).was_called(1) assert .spy(ctx.write_shell_exec_wrapper) .was_called_with( match.is_ref(ctx), "my-wrapper-script", ("%q"):format(path.concat { ctx:get_install_path(), exec_rel_path }) ) end) it("should not write exec wrapper if target executable doesn't exist", function() local exec_rel_path = path.concat { "obscure", "path", "to", "server" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), exec_rel_path).returns(false) local err = assert.has_error(function() ctx:write_exec_wrapper("my-wrapper-script", exec_rel_path) end) assert.equals([[Cannot write exec wrapper for path "obscure/path/to/server" as it doesn't exist.]], err) assert.spy(ctx.write_shell_exec_wrapper).was_called(0) end) it("should write PHP exec wrapper", function() local php_rel_path = path.concat { "some", "obscure", "path", "cli.php" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), php_rel_path).returns(true) ctx:write_php_exec_wrapper("my-wrapper-script", php_rel_path) assert.spy(ctx.write_shell_exec_wrapper).was_called(1) assert.spy(ctx.write_shell_exec_wrapper).was_called_with( match.is_ref(ctx), "my-wrapper-script", ("php %q"):format(path.concat { ctx:get_install_path(), php_rel_path }) ) end) it("should not write PHP exec wrapper if the target script doesn't exist", function() local php_rel_path = path.concat { "some", "obscure", "path", "cli.php" } local ctx = test_helpers.create_context() stub(ctx, "write_shell_exec_wrapper") stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.is_ref(ctx.fs), php_rel_path).returns(false) local err = assert.has_error(function() ctx:write_php_exec_wrapper("my-wrapper-script", php_rel_path) end) assert.equals([[Cannot write PHP exec wrapper for path "some/obscure/path/cli.php" as it doesn't exist.]], err) assert.spy(ctx.write_shell_exec_wrapper).was_called(0) end) it("should await callback-style async function", function() local value = a.run_blocking(function() local ctx = test_helpers.create_context() return ctx:execute(function() return ctx:await(function(resolve, reject) vim.defer_fn(function() resolve "Value!" end, 500) end) end) end) assert.equals("Value!", value) end) it("should propagate errors in callback-style async function", function() local guard = spy.new() local error = assert.has_error(function() a.run_blocking(function() local ctx = test_helpers.create_context() return ctx:execute(function() ctx:await(function(resolve, reject) vim.defer_fn(function() reject "Error!" end, 500) end) guard() end) end) end) assert.equals("Error!", error) assert.spy(guard).was_called(0) end) end) ================================================ FILE: tests/mason-core/installer/linker_spec.lua ================================================ local a = require "mason-core.async" local fs = require "mason-core.fs" local path = require "mason-core.path" local registry = require "mason-registry" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" local WIN_CMD_SCRIPT = [[@ECHO off GOTO start :find_dp0 SET dp0=%%~dp0 EXIT /b :start SETLOCAL CALL :find_dp0 endLocal & goto #_undefined_# 2>NUL || title %%COMSPEC%% & "%%dp0%%\%s" %%*]] describe("linker", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) ---@module "mason-core.installer.linker" local linker ---@module "mason-core.platform" local platform before_each(function() package.loaded["mason-core.platform"] = nil package.loaded["mason-core.installer.linker"] = nil platform = require "mason-core.platform" linker = require "mason-core.installer.linker" end) it("should symlink executable on Unix", function() local ctx = test_helpers.create_context() stub(fs.async, "file_exists") stub(fs.async, "symlink") stub(fs.async, "write_file") fs.async.file_exists.on_call_with(ctx.location:bin "my-executable").returns(false) fs.async.file_exists.on_call_with(ctx.location:bin "another-executable").returns(false) fs.async.file_exists .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "my-executable" }) .returns(true) fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "another-executable" }).returns(true) ctx:link_bin("my-executable", path.concat { "nested", "path", "my-executable" }) ctx:link_bin("another-executable", "another-executable") local result = a.run_blocking(linker.link, ctx) assert.is_true(result:is_success()) assert.spy(fs.async.write_file).was_called(0) assert.spy(fs.async.symlink).was_called(2) assert .spy(fs.async.symlink) .was_called_with("../packages/dummy/another-executable", ctx.location:bin "another-executable") assert .spy(fs.async.symlink) .was_called_with("../packages/dummy/nested/path/my-executable", ctx.location:bin "my-executable") end) it("should write executable wrapper on Windows", function() local ctx = test_helpers.create_context() platform.is.darwin = false platform.is.mac = false platform.is.linux = false platform.is.unix = false platform.is.win = true stub(fs.async, "file_exists") stub(fs.async, "symlink") stub(fs.async, "write_file") fs.async.file_exists.on_call_with(ctx.location:bin "my-executable").returns(false) fs.async.file_exists.on_call_with(ctx.location:bin "another-executable").returns(false) fs.async.file_exists .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "my-executable" }) .returns(true) fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "another-executable" }).returns(true) ctx:link_bin("my-executable", path.concat { "nested", "path", "my-executable" }) ctx:link_bin("another-executable", "another-executable") local result = a.run_blocking(linker.link, ctx) assert.is_true(result:is_success()) assert.spy(fs.async.symlink).was_called(0) assert.spy(fs.async.write_file).was_called(2) assert .spy(fs.async.write_file) .was_called_with(ctx.location:bin "another-executable.cmd", WIN_CMD_SCRIPT:format "..\\packages\\dummy\\another-executable") assert .spy(fs.async.write_file) .was_called_with( ctx.location:bin "my-executable.cmd", WIN_CMD_SCRIPT:format "..\\packages\\dummy\\nested\\path\\my-executable" ) end) it("should symlink share files", function() local ctx = test_helpers.create_context() stub(fs.async, "mkdirp") stub(fs.async, "dir_exists") stub(fs.async, "file_exists") stub(fs.async, "symlink") stub(fs.async, "write_file") -- mock non-existent dest files fs.async.file_exists.on_call_with(ctx.location:share "share-file").returns(false) fs.async.file_exists.on_call_with(ctx.location:share(path.concat { "nested", "share-file" })).returns(false) fs.async.dir_exists.on_call_with(ctx.location:share "nested/path").returns(false) -- mock existent source files fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "share-file" }).returns(true) fs.async.file_exists .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }) .returns(true) ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" } ctx.links.share["share-file"] = "share-file" local result = a.run_blocking(linker.link, ctx) assert.is_true(result:is_success()) assert.spy(fs.async.write_file).was_called(0) assert.spy(fs.async.symlink).was_called(2) assert.spy(fs.async.symlink).was_called_with("../packages/dummy/share-file", ctx.location:share "share-file") assert .spy(fs.async.symlink) .was_called_with("../../../packages/dummy/nested/path/to/share-file", ctx.location:share "nested/path/share-file") assert.spy(fs.async.mkdirp).was_called(2) assert.spy(fs.async.mkdirp).was_called_with(ctx.location:share "nested/path") end) it("should copy share files on Windows", function() local ctx = test_helpers.create_context() platform.is.darwin = false platform.is.mac = false platform.is.linux = false platform.is.unix = false platform.is.win = true stub(fs.async, "mkdirp") stub(fs.async, "dir_exists") stub(fs.async, "file_exists") stub(fs.async, "copy_file") -- mock non-existent dest files fs.async.file_exists.on_call_with(ctx.location:share "share-file").returns(false) fs.async.file_exists.on_call_with(ctx.location:share(path.concat { "nested", "share-file" })).returns(false) fs.async.dir_exists.on_call_with(ctx.location:share "nested/path").returns(false) -- mock existent source files fs.async.file_exists.on_call_with(path.concat { ctx:get_install_path(), "share-file" }).returns(true) fs.async.file_exists .on_call_with(path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }) .returns(true) ctx.links.share["nested/path/share-file"] = path.concat { "nested", "path", "to", "share-file" } ctx.links.share["share-file"] = "share-file" local result = linker.link(ctx) assert.is_true(result:is_success()) assert.spy(fs.async.copy_file).was_called(2) assert .spy(fs.async.copy_file) .was_called_with(path.concat { ctx:get_install_path(), "share-file" }, ctx.location:share "share-file", { excl = true }) assert.spy(fs.async.copy_file).was_called_with( path.concat { ctx:get_install_path(), "nested", "path", "to", "share-file" }, ctx.location:share "nested/path/share-file", { excl = true } ) assert.spy(fs.async.mkdirp).was_called(2) assert.spy(fs.async.mkdirp).was_called_with(ctx.location:share "nested/path") end) end) ================================================ FILE: tests/mason-core/installer/managers/cargo_spec.lua ================================================ local cargo = require "mason-core.installer.managers.cargo" local match = require "luassert.match" local spy = require "luassert.spy" local test_helpers = require "mason-test.helpers" describe("cargo manager", function() it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() cargo.install("my-crate", "1.0.0") end) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", { "--version", "1.0.0" }, vim.NIL, -- features vim.NIL, -- locked "my-crate", } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() cargo.install("my-crate", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing crate my-crate@1.0.0…\n") end) it("should install locked", function() local ctx = test_helpers.create_context() ctx:execute(function() cargo.install("my-crate", "1.0.0", { locked = true, }) end) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", { "--version", "1.0.0" }, vim.NIL, -- features "--locked", -- locked "my-crate", } end) it("should install provided features", function() local ctx = test_helpers.create_context() ctx:execute(function() cargo.install("my-crate", "1.0.0", { features = "lsp,cli", }) end) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", { "--version", "1.0.0" }, { "--features", "lsp,cli" }, -- features vim.NIL, -- locked "my-crate", } end) it("should install git tag source", function() local ctx = test_helpers.create_context() ctx:execute(function() cargo.install("my-crate", "1.0.0", { git = { url = "https://github.com/neovim/neovim", }, }) end) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", { "--git", "https://github.com/neovim/neovim", "--tag", "1.0.0" }, vim.NIL, -- features vim.NIL, -- locked "my-crate", } end) it("should install git rev source", function() local ctx = test_helpers.create_context() ctx:execute(function() cargo.install("my-crate", "16dfc89abd413c391e5b63ae5d132c22843ce9a7", { git = { url = "https://github.com/neovim/neovim", rev = true, }, }) end) assert.spy(ctx.spawn.cargo).was_called(1) assert.spy(ctx.spawn.cargo).was_called_with { "install", "--root", ".", { "--git", "https://github.com/neovim/neovim", "--rev", "16dfc89abd413c391e5b63ae5d132c22843ce9a7" }, vim.NIL, -- features vim.NIL, -- locked "my-crate", } end) end) ================================================ FILE: tests/mason-core/installer/managers/common_spec.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local common = require "mason-core.installer.managers.common" local installer = require "mason-core.installer" local match = require "luassert.match" local mock = require "luassert.mock" local spy = require "luassert.spy" local std = require "mason-core.installer.managers.std" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("common manager :: download", function() it("should parse download files from common structure", function() local url_generator = _.format "https://example.com/%s" assert.same( { { download_url = "https://example.com/abc.jar", out_file = "abc.jar", }, }, common.parse_downloads({ file = "abc.jar", }, url_generator) ) assert.same( { { download_url = "https://example.com/abc.jar", out_file = "lib/abc.jar", }, }, common.parse_downloads({ file = "abc.jar:lib/", }, url_generator) ) assert.same( { { download_url = "https://example.com/abc.jar", out_file = "lib/abc.jar", }, { download_url = "https://example.com/file.jar", out_file = "lib/nested/new-name.jar", }, }, common.parse_downloads({ file = { "abc.jar:lib/", "file.jar:lib/nested/new-name.jar" }, }, url_generator) ) end) it("should download files", function() local ctx = test_helpers.create_context() stub(std, "download_file", mockx.returns(Result.success())) stub(std, "unpack", mockx.returns(Result.success())) local result = ctx:execute(function() return common.download_files(ctx, { { out_file = "file.jar", download_url = "https://example.com/file.jar" }, { out_file = "LICENSE.md", download_url = "https://example.com/LICENSE" }, }) end) assert.is_true(result:is_success()) assert.spy(std.download_file).was_called(2) assert.spy(std.download_file).was_called_with("https://example.com/file.jar", "file.jar") assert.spy(std.download_file).was_called_with("https://example.com/LICENSE", "LICENSE.md") assert.spy(std.unpack).was_called(2) assert.spy(std.unpack).was_called_with "file.jar" assert.spy(std.unpack).was_called_with "LICENSE.md" end) it("should download files to specified directory", function() local ctx = test_helpers.create_context() stub(std, "download_file", mockx.returns(Result.success())) stub(std, "unpack", mockx.returns(Result.success())) stub(ctx.fs, "mkdirp") local result = ctx:execute(function() return common.download_files(ctx, { { out_file = "lib/file.jar", download_url = "https://example.com/file.jar" }, { out_file = "doc/LICENSE.md", download_url = "https://example.com/LICENSE" }, { out_file = "nested/path/to/file", download_url = "https://example.com/some-file" }, }) end) assert.is_true(result:is_success()) assert.spy(ctx.fs.mkdirp).was_called(3) assert.spy(ctx.fs.mkdirp).was_called_with(match.is_ref(ctx.fs), "lib") assert.spy(ctx.fs.mkdirp).was_called_with(match.is_ref(ctx.fs), "doc") assert.spy(ctx.fs.mkdirp).was_called_with(match.is_ref(ctx.fs), "nested/path/to") end) end) describe("common manager :: build", function() it("should run build instruction", function() local ctx = test_helpers.create_context() local uv = require "mason-core.async.uv" spy.on(ctx, "promote_cwd") stub(uv, "write") stub(uv, "shutdown") stub(uv, "close") local stdin = mock.new() stub( ctx.spawn, "bash", ---@param args SpawnArgs function(args) args.on_spawn(mock.new(), { stdin }) return Result.success() end ) local result = ctx:execute(function() return common.run_build_instruction { run = [[npm install && npm run compile]], env = { MASON_VERSION = "2023-03-09", SOME_VALUE = "here", }, } end) assert.is_true(result:is_success()) assert.spy(ctx.promote_cwd).was_called(0) assert.spy(ctx.spawn.bash).was_called(1) assert.spy(ctx.spawn.bash).was_called_with(match.tbl_containing { on_spawn = match.is_function(), env = match.same { MASON_VERSION = "2023-03-09", SOME_VALUE = "here", }, }) assert.spy(uv.write).was_called(2) assert.spy(uv.write).was_called_with(stdin, "set -euxo pipefail;\n") assert.spy(uv.write).was_called_with(stdin, "npm install && npm run compile") assert.spy(uv.shutdown).was_called_with(stdin) assert.spy(uv.close).was_called_with(stdin) end) it("should promote cwd if not staged", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") stub(ctx.spawn, "bash", mockx.returns(Result.success())) local result = ctx:execute(function() return common.run_build_instruction { run = "make", staged = false, } end) assert.is_true(result:is_success()) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.bash).was_called(1) end) end) ================================================ FILE: tests/mason-core/installer/managers/composer_spec.lua ================================================ local composer = require "mason-core.installer.managers.composer" local match = require "luassert.match" local spy = require "luassert.spy" local test_helpers = require "mason-test.helpers" describe("composer manager", function() it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() composer.install("my-package", "1.0.0") end) assert.spy(ctx.spawn.composer).was_called(2) assert.spy(ctx.spawn.composer).was_called_with { "init", "--no-interaction", "--stability=stable", } assert.spy(ctx.spawn.composer).was_called_with { "require", "my-package:1.0.0", } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() composer.install("my-package", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing composer package my-package@1.0.0…\n") end) end) ================================================ FILE: tests/mason-core/installer/managers/gem_spec.lua ================================================ local gem = require "mason-core.installer.managers.gem" local match = require "luassert.match" local spy = require "luassert.spy" local test_helper = require "mason-test.helpers" describe("gem manager", function() it("should install", function() local ctx = test_helper.create_context() local result = ctx:execute(function() return gem.install("my-gem", "1.0.0") end) assert.is_true(result:is_success()) assert.spy(ctx.spawn.gem).was_called(1) assert.spy(ctx.spawn.gem).was_called_with { "install", "--no-user-install", "--no-format-executable", "--install-dir=.", "--bindir=bin", "--no-document", "my-gem:1.0.0", vim.NIL, -- extra_packages env = { GEM_HOME = ctx.location:staging "dummy", }, } end) it("should write output", function() local ctx = test_helper.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() gem.install("my-gem", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing gem my-gem@1.0.0…\n") end) it("should install extra packages", function() local ctx = test_helper.create_context() ctx:execute(function() gem.install("my-gem", "1.0.0", { extra_packages = { "extra-gem" }, }) end) assert.spy(ctx.spawn.gem).was_called(1) assert.spy(ctx.spawn.gem).was_called_with { "install", "--no-user-install", "--no-format-executable", "--install-dir=.", "--bindir=bin", "--no-document", "my-gem:1.0.0", { "extra-gem" }, env = { GEM_HOME = ctx.cwd:get(), }, } end) end) ================================================ FILE: tests/mason-core/installer/managers/golang_spec.lua ================================================ local golang = require "mason-core.installer.managers.golang" local match = require "luassert.match" local spy = require "luassert.spy" local test_helpers = require "mason-test.helpers" describe("golang manager", function() it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() golang.install("my-golang", "1.0.0") end) assert.spy(ctx.spawn.go).was_called(1) assert.spy(ctx.spawn.go).was_called_with { "install", "-v", "my-golang@1.0.0", env = { GOBIN = ctx.cwd:get(), }, } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() golang.install("my-golang", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing go package my-golang@1.0.0…\n") end) it("should install extra packages", function() local ctx = test_helpers.create_context() ctx:execute(function() golang.install("my-golang", "1.0.0", { extra_packages = { "extra", "package" }, }) end) assert.spy(ctx.spawn.go).was_called(3) assert.spy(ctx.spawn.go).was_called_with { "install", "-v", "my-golang@1.0.0", env = { GOBIN = ctx.cwd:get(), }, } assert.spy(ctx.spawn.go).was_called_with { "install", "-v", "extra@latest", env = { GOBIN = ctx.cwd:get(), }, } assert.spy(ctx.spawn.go).was_called_with { "install", "-v", "package@latest", env = { GOBIN = ctx.cwd:get(), }, } end) end) ================================================ FILE: tests/mason-core/installer/managers/luarocks_spec.lua ================================================ local luarocks = require "mason-core.installer.managers.luarocks" local match = require "luassert.match" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("luarocks manager", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should install", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") ctx:execute(function() luarocks.install("my-rock", "1.0.0") end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.luarocks).was_called(1) assert.spy(ctx.spawn.luarocks).was_called_with { "install", { "--tree", ctx.cwd:get() }, vim.NIL, -- dev vim.NIL, -- server { "my-rock", "1.0.0" }, } end) it("should install dev mode", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") ctx:execute(function() luarocks.install("my-rock", "1.0.0", { dev = true, }) end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.luarocks).was_called(1) assert.spy(ctx.spawn.luarocks).was_called_with { "install", { "--tree", ctx.cwd:get() }, "--dev", vim.NIL, -- server { "my-rock", "1.0.0" }, } end) it("should install using provided server", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") ctx:execute(function() luarocks.install("my-rock", "1.0.0", { server = "https://luarocks.org/dev", }) end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.luarocks).was_called(1) assert.spy(ctx.spawn.luarocks).was_called_with { "install", { "--tree", ctx.cwd:get() }, vim.NIL, -- dev "--server=https://luarocks.org/dev", { "my-rock", "1.0.0" }, } end) it("should write output", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() luarocks.install("my-rock", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing luarocks package my-rock@1.0.0…\n") end) end) ================================================ FILE: tests/mason-core/installer/managers/npm_spec.lua ================================================ local Result = require "mason-core.result" local match = require "luassert.match" local npm = require "mason-core.installer.managers.npm" local spawn = require "mason-core.spawn" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("npm manager", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should init package.json", function() local ctx = test_helpers.create_context() stub(ctx.fs, "append_file") stub(spawn, "npm") spawn.npm.returns(Result.success {}) spawn.npm.on_call_with({ "version", "--json" }).returns(Result.success { stdout = [[ { "npm": "8.1.0" } ]], }) ctx:execute(function() npm.init() end) assert.spy(ctx.spawn.npm).was_called(1) assert.spy(ctx.spawn.npm).was_called_with { "init", "--yes", "--scope=mason", } assert.spy(ctx.fs.append_file).was_called(1) assert.spy(ctx.fs.append_file).was_called_with(match.is_ref(ctx.fs), ".npmrc", "\nglobal-style=true") end) it("should use install-strategy on npm >= 9", function() local ctx = test_helpers.create_context() stub(ctx.fs, "append_file") stub(spawn, "npm") spawn.npm.returns(Result.success {}) spawn.npm.on_call_with({ "version", "--json" }).returns(Result.success { stdout = [[ { "npm": "9.1.0" } ]], }) ctx:execute(function() npm.init() end) assert.spy(ctx.spawn.npm).was_called(1) assert.spy(ctx.spawn.npm).was_called_with { "init", "--yes", "--scope=mason", } assert.spy(ctx.fs.append_file).was_called_with(match.is_ref(ctx.fs), ".npmrc", "\ninstall-strategy=shallow") end) it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() npm.install("my-package", "1.0.0") end) assert.spy(ctx.spawn.npm).was_called(1) assert.spy(ctx.spawn.npm).was_called_with { "install", "my-package@1.0.0", vim.NIL, -- extra_packages } end) it("should install extra packages", function() local ctx = test_helpers.create_context() ctx:execute(function() npm.install("my-package", "1.0.0", { extra_packages = { "extra-package" }, }) end) assert.spy(ctx.spawn.npm).was_called(1) assert.spy(ctx.spawn.npm).was_called_with { "install", "my-package@1.0.0", { "extra-package" }, } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() npm.install("my-package", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing npm package my-package@1.0.0…\n") end) end) ================================================ FILE: tests/mason-core/installer/managers/nuget_spec.lua ================================================ local match = require "luassert.match" local nuget = require "mason-core.installer.managers.nuget" local spy = require "luassert.spy" local test_helpers = require "mason-test.helpers" describe("nuget manager", function() it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() nuget.install("nuget-package", "1.0.0") end) assert.spy(ctx.spawn.dotnet).was_called(1) assert.spy(ctx.spawn.dotnet).was_called_with { "tool", "update", "--tool-path", ".", { "--version", "1.0.0" }, "nuget-package", } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() nuget.install("nuget-package", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing nuget package nuget-package@1.0.0…\n") end) end) ================================================ FILE: tests/mason-core/installer/managers/opam_spec.lua ================================================ local match = require "luassert.match" local opam = require "mason-core.installer.managers.opam" local spy = require "luassert.spy" local test_helpers = require "mason-test.helpers" describe("opam manager", function() it("should install", function() local ctx = test_helpers.create_context() ctx:execute(function() opam.install("opam-package", "1.0.0") end) assert.spy(ctx.spawn.opam).was_called(1) assert.spy(ctx.spawn.opam).was_called_with { "install", "--destdir=.", "--yes", "--verbose", "opam-package.1.0.0", } end) it("should write output", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() opam.install("opam-package", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing opam package opam-package@1.0.0…\n") end) end) ================================================ FILE: tests/mason-core/installer/managers/powershell_spec.lua ================================================ local a = require "mason-core.async" local match = require "luassert.match" local mock = require "luassert.mock" local spawn = require "mason-core.spawn" local spy = require "luassert.spy" local stub = require "luassert.stub" describe("powershell manager", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) local function powershell() package.loaded["mason-core.installer.managers.powershell"] = nil return require "mason-core.installer.managers.powershell" end it("should use pwsh if available", function() stub(spawn, "pwsh", function() end) stub(spawn, "powershell", function() end) stub(vim.fn, "executable") vim.fn.executable.on_call_with("pwsh").returns(1) powershell().command "echo 'Is this bash?'" assert.spy(spawn.pwsh).was_called(1) assert.spy(spawn.powershell).was_called(0) end) it("should use powershell if pwsh is not available", function() stub(spawn, "pwsh", function() end) stub(spawn, "powershell", function() end) stub(vim.fn, "executable") vim.fn.executable.on_call_with("pwsh").returns(0) local powershell = powershell() a.run_blocking(powershell.command, "echo 'Is this bash?'") assert.spy(spawn.pwsh).was_called(0) assert.spy(spawn.powershell).was_called(1) end) it("should use the provided spawner for commands", function() spy.on(spawn, "pwsh") local custom_spawn = mock.new { pwsh = mockx.just_runs } stub(vim.fn, "executable") vim.fn.executable.on_call_with("pwsh").returns(1) powershell().command("echo 'Is this bash?'", {}, custom_spawn) assert.spy(spawn.pwsh).was_called(0) assert.spy(custom_spawn.pwsh).was_called(1) end) it("should provide default powershell options via command interface", function() stub(spawn, "pwsh", function() end) stub(vim.fn, "executable") vim.fn.executable.on_call_with("pwsh").returns(1) powershell().command "echo 'Is this bash?'" assert.spy(spawn.pwsh).was_called(1) assert.spy(spawn.pwsh).was_called_with(match.tbl_containing { "-NoProfile", "-NonInteractive", "-Command", [[ $ErrorActionPreference = "Stop"; $ProgressPreference = 'SilentlyContinue'; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; echo 'Is this bash?']], }) end) it("should close stdin", function() local stdin = { close = spy.new(), } stub( spawn, "pwsh", ---@param args SpawnArgs function(args) args.on_spawn(mock.new(), { stdin, }, 1) end ) stub(vim.fn, "executable") vim.fn.executable.on_call_with("pwsh").returns(1) powershell().command "Powershell-Command" assert.spy(spawn.pwsh).was_called(1) assert.spy(stdin.close).was_called(1) assert.spy(stdin.close).was_called_with(match.is_ref(stdin)) end) end) ================================================ FILE: tests/mason-core/installer/managers/pypi_spec.lua ================================================ local Result = require "mason-core.result" local match = require "luassert.match" local path = require "mason-core.path" local providers = require "mason-core.providers" local pypi = require "mason-core.installer.managers.pypi" local spawn = require "mason-core.spawn" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" ---@param ctx InstallContext local function venv_py(ctx) return path.concat { ctx.cwd:get(), "venv", "bin", "python", } end describe("pypi manager", function() local snapshot before_each(function() snapshot = assert.snapshot() stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.11.0" }) end) after_each(function() snapshot:revert() end) it("should init venv without upgrading pip", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.failure())) ctx:execute(function() pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = false } end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.python3).was_called(1) assert.spy(ctx.spawn.python3).was_called_with { "-m", "venv", "--system-site-packages", "venv", } end) it("should init venv and upgrade pip", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.failure())) ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) ctx:execute(function() pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = true, install_extra_args = { "--proxy", "http://localhost" }, } end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.python3).was_called(1) assert.spy(ctx.spawn.python3).was_called_with { "-m", "venv", "--system-site-packages", "venv", } assert.spy(ctx.spawn[venv_py(ctx)]).was_called(1) assert.spy(ctx.spawn[venv_py(ctx)]).was_called_with { "-m", "pip", "--disable-pip-version-check", "install", "--no-user", "--ignore-installed", { "--proxy", "http://localhost" }, { "pip" }, } end) it("should find versioned candidates during init", function() local ctx = test_helpers.create_context() stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.12")) stub(vim.fn, "executable") vim.fn.executable.on_call_with("python3.12").returns(1) stub(spawn, "python3.12") spawn["python3.12"].on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.12.0" }) ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) ctx:execute(function() pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = false, install_extra_args = {}, } end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn["python3.12"]).was_called(1) assert.spy(ctx.spawn["python3.12"]).was_called_with { "-m", "venv", "--system-site-packages", "venv", } end) it("should error if unable to find a suitable python3 version", function() local ctx = test_helpers.create_context() spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) stub(vim.fn, "executable") vim.fn.executable.on_call_with("python3.12").returns(0) vim.fn.executable.on_call_with("python3.11").returns(0) vim.fn.executable.on_call_with("python3.10").returns(0) vim.fn.executable.on_call_with("python3.9").returns(0) vim.fn.executable.on_call_with("python3.8").returns(0) stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.5.0" }) local result = ctx:execute(function() return pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = false, install_extra_args = {}, } end) assert.same( Result.failure "Failed to find a python3 installation in PATH that meets the required versions (>=3.8). Found version: 3.5.0.", result ) assert .spy(ctx.stdio_sink.stderr) .was_called_with(match.is_ref(ctx.stdio_sink), "Run with :MasonInstall --force to bypass this version validation.\n") end) it( "should default to stock version if unable to find suitable versioned candidate during init and when force=true", function() local ctx = test_helpers.create_context { install_opts = { force = true } } spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) stub(vim.fn, "executable") vim.fn.executable.on_call_with("python3.12").returns(0) vim.fn.executable.on_call_with("python3.11").returns(0) vim.fn.executable.on_call_with("python3.10").returns(0) vim.fn.executable.on_call_with("python3.9").returns(0) vim.fn.executable.on_call_with("python3.8").returns(0) stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.5.0" }) ctx:execute(function() pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = true, install_extra_args = { "--proxy", "http://localhost" }, } end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.python3).was_called(1) assert.spy(ctx.spawn.python3).was_called_with { "-m", "venv", "--system-site-packages", "venv", } assert.spy(ctx.stdio_sink.stderr).was_called_with( match.is_ref(ctx.stdio_sink), "Warning: The resolved python3 version 3.5.0 is not compatible with the required Python versions: >=3.8.\n" ) end ) it("should prioritize stock python", function() local ctx = test_helpers.create_context { install_opts = { force = true } } spy.on(ctx.stdio_sink, "stderr") stub(ctx, "promote_cwd") stub(ctx.fs, "file_exists") stub(providers.pypi, "get_supported_python_versions", mockx.returns(Result.success ">=3.8")) stub(vim.fn, "executable") vim.fn.executable.on_call_with("python3.12").returns(1) stub(spawn, "python3", mockx.returns(Result.success())) spawn.python3.on_call_with({ "--version" }).returns(Result.success { stdout = "Python 3.8.0" }) ctx:execute(function() pypi.init { package = { name = "cmake-language-server", version = "0.1.10" }, upgrade_pip = true, install_extra_args = { "--proxy", "http://localhost" }, } end) assert.spy(ctx.promote_cwd).was_called(1) assert.spy(ctx.spawn.python3).was_called(1) assert.spy(ctx.spawn["python3.12"]).was_called(0) assert.spy(ctx.spawn.python3).was_called_with { "-m", "venv", "--system-site-packages", "venv", } end) it("should install", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) ctx:execute(function() pypi.install("pypi-package", "1.0.0") end) assert.spy(ctx.spawn[venv_py(ctx)]).was_called(1) assert.spy(ctx.spawn[venv_py(ctx)]).was_called_with { "-m", "pip", "--disable-pip-version-check", "install", "--no-user", "--ignore-installed", vim.NIL, -- install_extra_args { "pypi-package==1.0.0", vim.NIL, -- extra_packages }, } end) it("should write output", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) spy.on(ctx.stdio_sink, "stdout") ctx:execute(function() pypi.install("pypi-package", "1.0.0") end) assert .spy(ctx.stdio_sink.stdout) .was_called_with(match.is_ref(ctx.stdio_sink), "Installing pip package pypi-package@1.0.0…\n") end) it("should install extra specifier", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) ctx:execute(function() pypi.install("pypi-package", "1.0.0", { extra = "lsp", }) end) assert.spy(ctx.spawn[venv_py(ctx)]).was_called(1) assert.spy(ctx.spawn[venv_py(ctx)]).was_called_with { "-m", "pip", "--disable-pip-version-check", "install", "--no-user", "--ignore-installed", vim.NIL, -- install_extra_args { "pypi-package[lsp]==1.0.0", vim.NIL, -- extra_packages }, } end) it("should install extra packages", function() local ctx = test_helpers.create_context() stub(ctx.fs, "file_exists") ctx.fs.file_exists.on_call_with(match.ref(ctx.fs), "venv/bin/python").returns(true) ctx:execute(function() pypi.install("pypi-package", "1.0.0", { extra_packages = { "extra-package" }, install_extra_args = { "--proxy", "http://localhost:9000" }, }) end) assert.spy(ctx.spawn[venv_py(ctx)]).was_called(1) assert.spy(ctx.spawn[venv_py(ctx)]).was_called_with { "-m", "pip", "--disable-pip-version-check", "install", "--no-user", "--ignore-installed", { "--proxy", "http://localhost:9000" }, { "pypi-package==1.0.0", { "extra-package" }, }, } end) end) ================================================ FILE: tests/mason-core/installer/managers/std_spec.lua ================================================ local match = require "luassert.match" local std = require "mason-core.installer.managers.std" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("std unpack [Unix]", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should unpack .gz", function() local ctx = test_helpers.create_context() ctx:execute(function() std.unpack "file.gz" end) assert.spy(ctx.spawn.gzip).was_called(1) assert.spy(ctx.spawn.gzip).was_called_with { "-d", "file.gz" } end) describe("tar", function() before_each(function() stub(vim.fn, "executable") vim.fn.executable.on_call_with("gtar").returns(0) end) it("should use gtar if available", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") stub(vim.fn, "executable") vim.fn.executable.on_call_with("gtar").returns(1) ctx:execute(function() std.unpack "file.tar.gz" end) assert.spy(ctx.spawn.gtar).was_called(1) assert.spy(ctx.spawn.gtar).was_called_with { "--no-same-owner", "-xvf", "file.tar.gz" } end) it("should unpack .tar", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.tar" end) assert.spy(ctx.spawn.tar).was_called(1) assert.spy(ctx.spawn.tar).was_called_with { "--no-same-owner", "-xvf", "file.tar" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.tar") end) it("should unpack .tar.bz2", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.tar.bz2" end) assert.spy(ctx.spawn.tar).was_called(1) assert.spy(ctx.spawn.tar).was_called_with { "--no-same-owner", "-xvf", "file.tar.bz2" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.tar.bz2") end) it("should unpack .tar.gz", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.tar.gz" end) assert.spy(ctx.spawn.tar).was_called(1) assert.spy(ctx.spawn.tar).was_called_with { "--no-same-owner", "-xvf", "file.tar.gz" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.tar.gz") end) it("should unpack .tar.xz", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.tar.xz" end) assert.spy(ctx.spawn.tar).was_called(1) assert.spy(ctx.spawn.tar).was_called_with { "--no-same-owner", "-xvf", "file.tar.xz" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.tar.xz") end) it("should unpack .tar.zst", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.tar.zst" end) assert.spy(ctx.spawn.tar).was_called(1) assert.spy(ctx.spawn.tar).was_called_with { "--no-same-owner", "-xvf", "file.tar.zst" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.tar.zst") end) end) it("should unpack .vsix", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.vsix" end) assert.spy(ctx.spawn.unzip).was_called(1) assert.spy(ctx.spawn.unzip).was_called_with { "-d", ".", "file.vsix" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.vsix") end) it("should unpack .zip", function() local ctx = test_helpers.create_context() stub(ctx.fs, "unlink") ctx:execute(function() std.unpack "file.zip" end) assert.spy(ctx.spawn.unzip).was_called(1) assert.spy(ctx.spawn.unzip).was_called_with { "-d", ".", "file.zip" } assert.spy(ctx.fs.unlink).was_called(1) assert.spy(ctx.fs.unlink).was_called_with(match.is_ref(ctx.fs), "file.zip") end) end) describe("std clone", function() it("should clone", function() local ctx = test_helpers.create_context() ctx:execute(function() std.clone "https://github.com/mason-org/mason.nvim" end) assert.spy(ctx.spawn.git).was_called(1) assert.spy(ctx.spawn.git).was_called_with { "clone", "--depth", "1", vim.NIL, -- recursive "https://github.com/mason-org/mason.nvim", ".", } end) it("should clone and checkout rev", function() local ctx = test_helpers.create_context() ctx:execute(function() std.clone("https://github.com/mason-org/mason.nvim", { rev = "e1fd03b1856cb5ad8425f49e18353dc524b02f91", recursive = true, }) end) assert.spy(ctx.spawn.git).was_called(3) assert.spy(ctx.spawn.git).was_called_with { "clone", "--depth", "1", "--recursive", "https://github.com/mason-org/mason.nvim", ".", } assert .spy(ctx.spawn.git) .was_called_with { "fetch", "--depth", "1", "origin", "e1fd03b1856cb5ad8425f49e18353dc524b02f91" } assert.spy(ctx.spawn.git).was_called_with { "checkout", "--quiet", "FETCH_HEAD" } end) end) ================================================ FILE: tests/mason-core/optional_spec.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local spy = require "luassert.spy" describe("Optional.of_nilable", function() it("should create empty optionals", function() local empty = Optional.empty() assert.is_false(empty:is_present()) end) it("should create non-empty optionals", function() local empty = Optional.of_nilable "value" assert.is_true(empty:is_present()) end) it("should use memoized empty value", function() assert.is_true(Optional.empty() == Optional.empty()) end) end) describe("Optional.get()", function() it("should map non-empty values", function() local str = Optional.of_nilable("world!") :map(function(val) return "Hello " .. val end) :get() assert.equals("Hello world!", str) end) it("should raise error when getting empty value", function() local err = assert.has_error(function() Optional.empty():get() end) assert.equals("No value present.", err) end) end) describe("Optional.or_else()", function() it("should use .or_else() value if empty", function() local value = Optional.empty():or_else "Hello!" assert.equals("Hello!", value) end) it("should not use .or_else() value if not empty", function() local value = Optional.of_nilable("Good bye!"):or_else "Hello!" assert.equals("Good bye!", value) end) end) describe("Optional.if_present()", function() it("should not call .if_present() if value is empty", function() local present = spy.new() Optional.empty():if_present(present) assert.spy(present).was_not_called() end) it("should call .if_present() if value is not empty", function() local present = spy.new() Optional.of_nilable("value"):if_present(present) assert.spy(present).was_called(1) assert.spy(present).was_called_with "value" end) end) describe("Optional.if_not_present()", function() it("should not call .if_not_present() if value is not empty", function() local present = spy.new() Optional.of_nilable("value"):if_not_present(present) assert.spy(present).was_not_called() end) it("should call .if_not_present() if value is empty", function() local present = spy.new() Optional.empty():if_not_present(present) assert.spy(present).was_called(1) end) end) describe("Optional.ok_or()", function() it("should return success variant if non-empty", function() local result = Optional.of_nilable("Hello world!"):ok_or() assert.is_true(getmetatable(result) == Result) assert.equals("Hello world!", result:get_or_nil()) end) it("should return failure variant if empty", function() local result = Optional.empty():ok_or(function() return "I'm empty." end) assert.is_true(getmetatable(result) == Result) assert.equals("I'm empty.", result:err_or_nil()) end) end) describe("Optional.or_()", function() it("should run supplier if value is not present", function() local spy = spy.new(function() return Optional.of "Hello world!" end) assert.same(Optional.of "Hello world!", Optional.empty():or_(spy)) assert.spy(spy).was_called(1) assert.same(Optional.empty(), Optional.empty():or_(Optional.empty)) end) it("should not run supplier if value is present", function() local spy = spy.new(function() return Optional.of "Hello world!" end) assert.same(Optional.of "Hello world!", Optional.of("Hello world!"):or_(spy)) assert.spy(spy).was_called(0) end) end) describe("Optional.and_then()", function() it("should run supplier if value is present", function() local spy = spy.new(function(value) return Optional.of(("%s world!"):format(value)) end) assert.same(Optional.of "Hello world!", Optional.of("Hello"):and_then(spy)) assert.spy(spy).was_called(1) assert.same( Optional.empty(), Optional.empty():and_then(function() return Optional.of "Nothing." end) ) end) it("should not run supplier if value is not present", function() local spy = spy.new(function() return Optional.of "Hello world!" end) assert.same(Optional.empty(), Optional.empty():and_then(spy)) assert.spy(spy).was_called(0) end) end) ================================================ FILE: tests/mason-core/package/package_spec.lua ================================================ local Pkg = require "mason-core.package" local a = require "mason-core.async" local match = require "luassert.match" local mock = require "luassert.mock" local receipt = require "mason-core.receipt" local registry = require "mason-registry" local spy = require "luassert.spy" local stub = require "luassert.stub" local test_helpers = require "mason-test.helpers" describe("Package ::", function() local snapshot before_each(function() snapshot = assert.snapshot() local dummy = registry.get_package "dummy" if dummy:is_installed() then test_helpers.sync_uninstall(dummy) end end) after_each(function() snapshot:revert() end) it("should parse package specifiers", function() local function parse(str) local name, version = Pkg.Parse(str) return { name, version } end assert.same({ "rust-analyzer", nil }, parse "rust-analyzer") assert.same({ "rust-analyzer", "" }, parse "rust-analyzer@") assert.same({ "rust-analyzer", "nightly" }, parse "rust-analyzer@nightly") end) if vim.fn.has "nvim-0.11" == 1 then it("should validate spec", function() ---@type RegistryPackageSpec local valid_spec = { schema = "registry+v1", name = "Package name", description = "Package description", homepage = "https://example.com", categories = { "LSP" }, languages = { "Rust" }, licenses = {}, source = { id = "pkg:mason/package@1", install = function() end, }, } local function spec(fields) return setmetatable(fields, { __index = valid_spec }) end assert.equals( "name: expected string, got number", assert.has_error(function() Pkg:new(spec { name = 23 }) end) ) assert.equals( "description: expected string, got number", assert.has_error(function() Pkg:new(spec { description = 23 }) end) ) assert.equals( "homepage: expected string, got number", assert.has_error(function() Pkg:new(spec { homepage = 23 }) end) ) assert.equals( "categories: expected table, got number", assert.has_error(function() Pkg:new(spec { categories = 23 }) end) ) assert.equals( "languages: expected table, got number", assert.has_error(function() Pkg:new(spec { languages = 23 }) end) ) end) end it("should create new handle", function() local dummy = registry.get_package "dummy" local callback = spy.new() dummy:once("install:handle", callback) local handle = dummy:new_install_handle() assert.spy(callback).was_called(1) assert.spy(callback).was_called_with(match.ref(handle)) handle:close() end) it("should not create new handle if one already exists", function() local dummy = registry.get_package "dummy" dummy.install_handle = mock.new { is_closed = mockx.returns(false), } local handle_handler = spy.new() dummy:once("install:handle", handle_handler) local err = assert.has_error(function() dummy:new_install_handle() end) assert.equals("Cannot create new install handle because existing handle is not closed.", err) assert.spy(handle_handler).was_called(0) dummy.install_handle = nil end) it("should successfully install package", function() local dummy = registry.get_package "dummy" local package_install_success_handler = spy.new() local package_install_failed_handler = spy.new() local install_success_handler = spy.new() local install_failed_handler = spy.new() registry:once("package:install:success", package_install_success_handler) registry:once("package:install:failed", package_install_failed_handler) dummy:once("install:success", install_success_handler) dummy:once("install:failed", install_failed_handler) local handle = dummy:install { version = "1337" } assert.wait(function() assert.is_true(handle:is_closed()) assert.is_true(dummy:is_installed()) end) assert.wait(function() assert.spy(install_success_handler).was_called(1) assert.spy(install_success_handler).was_called_with(match.instanceof(receipt.InstallReceipt)) assert.spy(package_install_success_handler).was_called(1) assert .spy(package_install_success_handler) .was_called_with(match.is_ref(dummy), match.instanceof(receipt.InstallReceipt)) assert.spy(package_install_failed_handler).was_called(0) assert.spy(install_failed_handler).was_called(0) end) end) it("should fail to install package", function() local dummy = registry.get_package "dummy" stub(dummy.spec.source, "install", function() error("I simply refuse to be installed.", 0) end) local package_install_success_handler = spy.new() local package_install_failed_handler = spy.new() local install_success_handler = spy.new() local install_failed_handler = spy.new() registry:once("package:install:success", package_install_success_handler) registry:once("package:install:failed", package_install_failed_handler) dummy:once("install:success", install_success_handler) dummy:once("install:failed", install_failed_handler) local handle = dummy:install { version = "1337" } assert.wait(function() assert.is_true(handle:is_closed()) assert.is_false(dummy:is_installed()) end) assert.wait(function() assert.spy(install_failed_handler).was_called(1) assert.spy(install_failed_handler).was_called_with "I simply refuse to be installed." assert.spy(package_install_failed_handler).was_called(1) assert .spy(package_install_failed_handler) .was_called_with(match.is_ref(dummy), "I simply refuse to be installed.") assert.spy(package_install_success_handler).was_called(0) assert.spy(install_success_handler).was_called(0) end) end) it("should be able to start package installation outside of main loop", function() local dummy = registry.get_package "dummy" local handle = a.run_blocking(function() -- Move outside the main loop a.wait(function(resolve) local timer = vim.loop.new_timer() timer:start(0, 0, function() timer:close() resolve() end) end) assert.is_true(vim.in_fast_event()) return assert.is_not.has_error(function() return dummy:install() end) end) end) it("should be able to instantiate package outside of main loop", function() local dummy = registry.get_package "registry" -- Move outside the main loop a.run_blocking(function() a.wait(function(resolve) local timer = vim.loop.new_timer() timer:start(0, 0, function() timer:close() resolve() end) end) assert.is_true(vim.in_fast_event()) local pkg = assert.is_not.has_error(function() return Pkg:new(dummy.spec) end) assert.same(dummy.spec, pkg.spec) end) end) end) ================================================ FILE: tests/mason-core/path_spec.lua ================================================ local path = require "mason-core.path" describe("path", function() it("concatenates paths", function() assert.equals("foo/bar/baz", path.concat { "foo", "bar", "baz" }) assert.equals("foo/bar/baz", path.concat { "foo/", "bar/", "baz/" }) end) it("identifies subdirectories", function() assert.is_true(path.is_subdirectory("/foo/bar", "/foo/bar/baz")) assert.is_true(path.is_subdirectory("/foo/bar", "/foo/bar")) assert.is_false(path.is_subdirectory("/foo/bar", "/foo/bas/baz")) assert.is_false(path.is_subdirectory("/foo/bar", "/foo/bars/baz")) end) describe("relative ::", function() local matrix = { { from = "/home/user/dir1/fileA", to = "/home/user/dir1/fileB", expected = "fileB", }, { from = "/home/user/dir1/fileA", to = "/home/user/dir2/fileC", expected = "../dir2/fileC", }, { from = "/home/user/dir1/subdir/fileD", to = "/home/user/dir1/fileE", expected = "../fileE", }, { from = "/home/user/dir1/subdir/fileD", to = "/home/user/dir1/subdir/fileF", expected = "fileF", }, { from = "/home/user/dir1/fileG", to = "/home/user/dir2/subdir/fileH", expected = "../dir2/subdir/fileH", }, { from = "/home/user/dir1/subdir1/subdir2/fileI", to = "/home/user/dir1/fileJ", expected = "../../fileJ", }, { from = "/fileK", to = "/home/fileL", expected = "home/fileL", }, { from = "/home/user/fileM", to = "/home/user/dir1/dir2/fileL", expected = "dir1/dir2/fileL", }, } for _, test_case in ipairs(matrix) do it(("should resolve from %s to %s: %s"):format(test_case.from, test_case.to, test_case.expected), function() assert.equals(test_case.expected, path.relative(test_case.from, test_case.to)) end) end end) end) ================================================ FILE: tests/mason-core/pep440_spec.lua ================================================ local pep440 = require "mason-core.pep440" describe("pep440 version checking", function() it("should check single version specifier", function() assert.is_false(pep440.check_version("3.5.0", ">=3.6")) assert.is_true(pep440.check_version("3.6.0", ">=3.6")) assert.is_false(pep440.check_version("3.6.0", ">=3.6.1")) end) it("should check version specifier with lower and upper bound", function() assert.is_true(pep440.check_version("3.8.0", ">=3.8,<3.12")) assert.is_false(pep440.check_version("3.12.0", ">=3.8,<3.12")) assert.is_true(pep440.check_version("3.12.0", ">=3.8,<4.0.0")) end) it("should check multiple specifiers with different constraints", function() assert.is_false(pep440.check_version("3.5.0", "!=4.0,<=4.0,>=3.8")) assert.is_false(pep440.check_version("4.0.0", "!=4.0,<=4.0,>=3.8")) assert.is_true(pep440.check_version("3.8.1", "!=4.0,<=4.0,>=3.8")) assert.is_true(pep440.check_version("3.12.0", "!=4.0,<=4.0,>=3.8")) end) it("should support ~= operators", function() assert.is_true(pep440.check_version("3.12.0", "~=3.10")) assert.is_true(pep440.check_version("3.10.4", "~=3.10.0")) assert.is_true(pep440.check_version("3.12.4", "~=3.0")) assert.is_true(pep440.check_version("3.12.4", "~=3.12.4")) assert.is_false(pep440.check_version("4.0.0", "~=3.10")) assert.is_false(pep440.check_version("3.11.0", "~=3.10.0")) assert.is_false(pep440.check_version("3.10.0", "~=3.10.5")) assert.is_false(pep440.check_version("3.11.0", "~=4.0")) end) end) ================================================ FILE: tests/mason-core/platform_spec.lua ================================================ local Result = require "mason-core.result" local _ = require "mason-core.functional" local match = require "luassert.match" local spy = require "luassert.spy" local stub = require "luassert.stub" local spawn = require "mason-core.spawn" ---@param contents string local function stub_etc_os_release(contents) stub(spawn, "bash") spawn.bash.on_call_with({ "-c", "cat /etc/*-release" }).returns(Result.success { stdout = contents, }) end describe("platform", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) local function platform() package.loaded["mason-core.platform"] = nil return require "mason-core.platform" end local function stub_uname(uname) stub(vim.loop, "os_uname") vim.loop.os_uname.returns(uname) end ---@param libc '"glibc"' | '"musl"' local function stub_libc(libc) stub(os, "execute") stub(vim.fn, "executable") stub(vim.fn, "system") vim.fn.executable.on_call_with("ldd").returns(1) vim.fn.executable.on_call_with("getconf").returns(1) if libc == "musl" then vim.fn.system.on_call_with({ "getconf", "GNU_LIBC_VERSION" }).returns "" vim.fn.system.on_call_with({ "ldd", "--version" }).returns "musl libc (aarch64)" elseif libc == "glibc" then vim.fn.system.on_call_with({ "getconf", "GNU_LIBC_VERSION" }).returns "glibc 2.35" vim.fn.system.on_call_with({ "ldd", "--version" }).returns "ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35" end end local function stub_mac() stub(vim.fn, "has") vim.fn.has.on_call_with("mac").returns(1) vim.fn.has.on_call_with("unix").returns(1) vim.fn.has.on_call_with("linux").returns(0) vim.fn.has.on_call_with(match._).returns(0) end local function stub_linux() stub(vim.fn, "has") vim.fn.has.on_call_with("mac").returns(0) vim.fn.has.on_call_with("unix").returns(1) vim.fn.has.on_call_with("linux").returns(1) vim.fn.has.on_call_with(match._).returns(0) end local function stub_windows() stub(vim.fn, "has") vim.fn.has.on_call_with("win32").returns(1) vim.fn.has.on_call_with(match._).returns(0) end it("should be able to detect platform and arch", function() stub_mac() stub_uname { machine = "aarch64" } assert.is_true(platform().is.mac_arm64) assert.is_false(platform().is.mac_x64) assert.is_false(platform().is.nothing) end) it("should be able to detect macos", function() stub_mac() assert.is_true(platform().is.mac) assert.is_true(platform().is.darwin) assert.is_true(platform().is.unix) assert.is_false(platform().is.linux) assert.is_false(platform().is.win) end) it("should be able to detect linux", function() stub_linux() assert.is_false(platform().is.mac) assert.is_false(platform().is.darwin) assert.is_true(platform().is.unix) assert.is_true(platform().is.linux) assert.is_false(platform().is.win) end) it("should be able to detect windows", function() stub_windows() assert.is_false(platform().is.mac) assert.is_false(platform().is.darwin) assert.is_false(platform().is.unix) assert.is_false(platform().is.linux) assert.is_true(platform().is.win) end) it("should be able to detect correct triple based on libc", function() stub_linux() stub_uname { machine = "aarch64" } stub_libc "musl" assert.is_false(platform().is.linux_x64_musl) assert.is_false(platform().is.linux_x64_gnu) assert.is_true(platform().is.linux_arm64_musl) assert.is_false(platform().is.linux_arm64_gnu) assert.is_false(platform().is.linux_arm64_gnu) end) it("should be able to detect correct triple based on sysname", function() stub_linux() stub_uname { machine = "aarch64", sysname = "OpenBSD" } stub_libc "musl" assert.is_false(platform().is.linux_x64_musl) assert.is_false(platform().is.linux_x64_gnu) assert.is_false(platform().is.linux_arm64_gnu) assert.is_false(platform().is.linux_arm64_gnu) assert.is_true(platform().is.linux_arm64_openbsd) end) it("should run correct case on linux", function() local unix = spy.new() local win = spy.new() local mac = spy.new() local linux = spy.new() stub_linux() platform().when { unix = unix, win = win, linux = linux, mac = mac, } assert.spy(unix).was_not_called() assert.spy(mac).was_not_called() assert.spy(win).was_not_called() assert.spy(linux).was_called(1) end) it("should run correct case on mac", function() local unix = spy.new() local win = spy.new() local mac = spy.new() local linux = spy.new() stub_mac() platform().when { unix = unix, win = win, linux = linux, mac = mac, } assert.spy(unix).was_not_called() assert.spy(mac).was_called(1) assert.spy(win).was_not_called() assert.spy(linux).was_not_called() end) it("should run correct case on windows", function() local unix = spy.new() local win = spy.new() local mac = spy.new() local linux = spy.new() stub_windows() platform().when { unix = unix, win = win, linux = linux, mac = mac, } assert.spy(unix).was_not_called() assert.spy(mac).was_not_called() assert.spy(win).was_called(1) assert.spy(linux).was_not_called() end) it("should run correct case on mac (unix)", function() local unix = spy.new() local win = spy.new() stub_mac() platform().when { unix = unix, win = win, } assert.spy(unix).was_called(1) assert.spy(win).was_not_called() end) it("should run correct case on linux (unix)", function() local unix = spy.new() local win = spy.new() stub_linux() platform().when { unix = unix, win = win, } assert.spy(unix).was_called(1) assert.spy(win).was_not_called() end) describe("macOS distribution detection", function() before_each(function() stub_mac() end) it("detects macOS", function() assert.same({ id = "macOS", version = {} }, platform().os_distribution()) end) end) describe("Windows distribution detection", function() before_each(function() stub_windows() end) it("detects Windows", function() assert.same({ id = "windows", version = {} }, platform().os_distribution()) end) end) describe("Linux distribution detection", function() before_each(function() stub_linux() end) it("detects Ubuntu", function() stub_etc_os_release(_.dedent [[ NAME="Ubuntu" VERSION="20.04.5 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.5 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal ]]) assert.same( { id = "ubuntu", version = { major = 20, minor = 4 }, version_id = "20.04" }, platform().os_distribution() ) end) it("detects CentOS", function() stub_etc_os_release(_.dedent [[ NAME="CentOS Linux" VERSION="7 (Core)" ID="centos" ID_LIKE="rhel fedora" VERSION_ID="7" PRETTY_NAME="CentOS Linux 7 (Core)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:centos:centos:7" HOME_URL="https://www.centos.org/" BUG_REPORT_URL="https://bugs.centos.org/" CENTOS_MANTISBT_PROJECT="CentOS-7" CENTOS_MANTISBT_PROJECT_VERSION="7" REDHAT_SUPPORT_PRODUCT="centos" REDHAT_SUPPORT_PRODUCT_VERSION="7" ]]) assert.same({ id = "centos", version = { major = 7 }, version_id = "7" }, platform().os_distribution()) end) it("detects generic Linux", function() stub(spawn, "bash") spawn.bash.returns(Result.failure()) assert.same({ id = "linux-generic", version = {} }, platform().os_distribution()) end) it("detects generic Linux", function() stub_etc_os_release(_.dedent [[ UNKNOWN_ID=here ]]) assert.same({ id = "linux-generic", version = {} }, platform().os_distribution()) end) end) end) ================================================ FILE: tests/mason-core/process_spec.lua ================================================ local match = require "luassert.match" local process = require "mason-core.process" local spy = require "luassert.spy" describe("process.spawn", function() -- Unix only it("should spawn command and feed output to sink", function() local stdio = process.BufferedSink:new() local callback = spy.new() process.spawn("env", { args = {}, env = { "HELLO=world", "MY_ENV=var", }, stdio_sink = stdio, }, callback) assert.wait(function() assert.spy(callback).was_called(1) assert.spy(callback).was_called_with(true, 0, match.is_number()) assert.equals(table.concat(stdio.buffers.stdout, ""), "HELLO=world\nMY_ENV=var\n") end) end) end) ================================================ FILE: tests/mason-core/providers/provider_spec.lua ================================================ local Result = require "mason-core.result" local spy = require "luassert.spy" describe("providers", function() ---@module "mason-core.providers" local provider ---@module "mason.settings" local settings before_each(function() package.loaded["mason-core.providers"] = nil package.loaded["mason.settings"] = nil provider = require "mason-core.providers" settings = require "mason.settings" end) it("should run provided providers", function() package.loaded["failing-provider"] = { github = { get_all_release_versions = spy.new(function() return Result.failure "Failed." end), }, } package.loaded["really-failing-provider"] = { github = { get_all_release_versions = spy.new(function() error "Failed." end), }, } package.loaded["successful-provider"] = { github = { get_all_release_versions = spy.new(function() return Result.success { "1.0.0", "2.0.0" } end), }, } settings.set { providers = { "failing-provider", "really-failing-provider", "successful-provider" }, } assert.same( Result.success { "1.0.0", "2.0.0" }, provider.github.get_all_release_versions "sumneko/lua-language-server" ) assert.spy(package.loaded["failing-provider"].github.get_all_release_versions).was_called() assert .spy(package.loaded["failing-provider"].github.get_all_release_versions) .was_called_with "sumneko/lua-language-server" assert.spy(package.loaded["really-failing-provider"].github.get_all_release_versions).was_called() assert .spy(package.loaded["really-failing-provider"].github.get_all_release_versions) .was_called_with "sumneko/lua-language-server" assert.spy(package.loaded["successful-provider"].github.get_all_release_versions).was_called() assert .spy(package.loaded["successful-provider"].github.get_all_release_versions) .was_called_with "sumneko/lua-language-server" end) end) ================================================ FILE: tests/mason-core/purl_spec.lua ================================================ local Purl = require "mason-core.purl" local Result = require "mason-core.result" describe("purl", function() it("should parse well-formed PURLs", function() assert.same( Result.success { name = "rust-analyzer", namespace = "rust-lang", qualifiers = { target = "linux_x64_gnu", download_url = "https://github.com/rust-lang/rust-analyzer/releases/download/2022-11-28/rust-analyzer-x86_64-unknown-linux-gnu.gz", }, scheme = "pkg", type = "github", version = "2022-11-28", subpath = "bin/rust-analyzer", }, Purl.parse "pkg:github/rust-lang/rust-analyzer@2022-11-28?target=linux_x64_gnu&download_url=https://github.com/rust-lang/rust-analyzer/releases/download/2022-11-28/rust-analyzer-x86_64-unknown-linux-gnu.gz#bin/rust-analyzer" ) assert.same( Result.success { scheme = "pkg", type = "github", namespace = "rust-lang", name = "rust-analyzer", version = "2025-04-20", qualifiers = nil, subpath = nil, }, Purl.parse "pkg:github/rust-lang/rust-analyzer@2025-04-20" ) assert.same( Result.success { scheme = "pkg", type = "npm", namespace = nil, name = "typescript-language-server", version = "10.23.1", qualifiers = nil, subpath = nil, }, Purl.parse "pkg:npm/typescript-language-server@10.23.1" ) assert.same( Result.success { scheme = "pkg", type = "pypi", namespace = nil, name = "python-language-server", version = nil, qualifiers = nil, subpath = nil, }, Purl.parse "pkg:pypi/python-language-server" ) assert.same( Result.success { name = "cli", namespace = "@angular", scheme = "pkg", type = "npm", }, Purl.parse "pkg:npm/%40angular/cli" ) end) it("should fail to parse invalid PURLs", function() assert.same(Result.failure "Malformed purl (invalid scheme).", Purl.parse "scam:github/react@18.0.0") end) it("should treat percent-encoded components as case insensitive", function() local purl = { name = "sonarlint-vscode", namespace = "sonarsource", scheme = "pkg", type = "github", version = "3.18.0+70423" .. string.char(0xab), } assert.same(Result.success(purl), Purl.parse "pkg:github/SonarSource/sonarlint-vscode@3.18.0%2b70423%ab") assert.same(Result.success(purl), Purl.parse "pkg:github/SonarSource/sonarlint-vscode@3.18.0%2B70423%aB") assert.same(Result.success(purl), Purl.parse "pkg:github/SonarSource/sonarlint-vscode@3.18.0%2b70423%AB") assert.same(Result.success(purl), Purl.parse "pkg:github/SonarSource/sonarlint-vscode@3.18.0%2B70423%Ab") end) end) describe("purl test suite ::", function() local fs = require "mason-core.fs" ---@type { description: string, purl: string, type: string?, namespace: string, name: string?, version: string?, is_invalid: boolean, canonical_purl: string }[] local test_fixture = vim.json.decode(fs.sync.read_file "./tests/fixtures/purl-test-suite-data.json") local function not_vim_nil(val) if val == vim.NIL then return nil else return val end end for _, test in ipairs(test_fixture) do it(test.description, function() local result = Purl.parse(test.purl) if test.is_invalid then assert.is_true(result:is_failure()) else assert.same( Result.success { scheme = "pkg", type = not_vim_nil(test.type), namespace = not_vim_nil(test.namespace), name = not_vim_nil(test.name), version = not_vim_nil(test.version), qualifiers = not_vim_nil(test.qualifiers), subpath = not_vim_nil(test.subpath), }, result ) assert.equals(test.canonical_purl, Purl.compile(result:get_or_throw())) end end) end end) ================================================ FILE: tests/mason-core/receipt_spec.lua ================================================ local InstallReceipt = require("mason-core.receipt").InstallReceipt local fs = require "mason-core.fs" local function fixture(file) return vim.json.decode(fs.sync.read_file(("./tests/fixtures/receipts/%s"):format(file))) end describe("receipt ::", function() it("should parse 1.0 structures", function() local receipt = InstallReceipt:new(fixture "1.0.json") assert.equals("angular-language-server", receipt:get_name()) assert.equals("1.0", receipt:get_schema_version()) assert.same({ type = "npm", package = "@angular/language-server" }, receipt:get_source()) assert.same({ bin = { ngserver = "node_modules/.bin/ngserver", }, }, receipt:get_links()) assert.is_true(receipt:is_schema_min "1.0") end) it("should parse 1.1 structures", function() local receipt = InstallReceipt:new(fixture "1.1.json") assert.equals("angular-language-server", receipt:get_name()) assert.equals("1.1", receipt:get_schema_version()) assert.same({ type = "registry+v1", id = "pkg:npm/%40angular/language-server@16.1.8", source = { extra_packages = { "typescript@5.1.3" }, version = "16.1.8", package = "@angular/language-server", }, }, receipt:get_source()) assert.same({ bin = { ngserver = "node_modules/.bin/ngserver", }, opt = {}, share = {}, }, receipt:get_links()) assert.is_true(receipt:is_schema_min "1.1") end) it("should parse 2.0 structures", function() local receipt = InstallReceipt:new(fixture "2.0.json") assert.equals("angular-language-server", receipt:get_name()) assert.equals("2.0", receipt:get_schema_version()) assert.same({ type = "registry+v1", id = "pkg:npm/%40angular/language-server@19.1.0", raw = { id = "pkg:npm/%40angular/language-server@19.1.0", extra_packages = { "typescript@5.4.5", }, }, }, receipt:get_source()) assert.same({ bin = { ngserver = "node_modules/.bin/ngserver", }, opt = {}, share = {}, }, receipt:get_links()) assert.same({ name = "mason-registry", version = "2025-05-03-lawful-clave", checksums = { ["registry.json"] = "4ae083fe8e50d0bea5382be05c7ede8d2def55ff2b6b89dc129b153039d9f2a2", ["registry.json.zip"] = "2116d5db7676afe7052de329db4dfbf656054d8c35ce12414eb9d58561b2fde9", }, proto = "github", namespace = "mason-org", }, receipt:get_registry()) assert.is_true(receipt:is_schema_min "2.0") end) it("should retrieve purl information", function() local receipt_1_0 = InstallReceipt:new(fixture "1.0.json") local receipt_1_1 = InstallReceipt:new(fixture "1.1.json") local receipt_2_0 = InstallReceipt:new(fixture "2.0.json") assert.is_nil(receipt_1_0:get_installed_purl()) assert.equals("pkg:npm/%40angular/language-server@16.1.8", receipt_1_1:get_installed_purl()) assert.equals("pkg:npm/%40angular/language-server@19.1.0", receipt_2_0:get_installed_purl()) end) describe("schema versions ::", function() it("should check minimum compatibility", function() local receipt_1_0 = InstallReceipt:new { schema_version = "1.0" } local receipt_1_1 = InstallReceipt:new { schema_version = "1.1" } local receipt_2_0 = InstallReceipt:new { schema_version = "2.0" } assert.is_true(receipt_1_0:is_schema_min "1.0") assert.is_true(receipt_1_1:is_schema_min "1.0") assert.is_true(receipt_2_0:is_schema_min "1.0") assert.is_false(receipt_1_0:is_schema_min "1.1") assert.is_true(receipt_1_1:is_schema_min "1.1") assert.is_true(receipt_2_0:is_schema_min "1.1") assert.is_false(receipt_1_0:is_schema_min "1.2") assert.is_false(receipt_1_1:is_schema_min "1.2") assert.is_true(receipt_2_0:is_schema_min "2.0") end) end) end) ================================================ FILE: tests/mason-core/result_spec.lua ================================================ local Optional = require "mason-core.optional" local Result = require "mason-core.result" local a = require "mason-core.async" local match = require "luassert.match" local spy = require "luassert.spy" describe("Result ::", function() it("should create success", function() local result = Result.success "Hello!" assert.is_true(result:is_success()) assert.is_false(result:is_failure()) assert.equals("Hello!", result:get_or_nil()) end) it("should create failure", function() local result = Result.failure "Hello!" assert.is_true(result:is_failure()) assert.is_false(result:is_success()) assert.equals("Hello!", result:err_or_nil()) end) it("should return value on get_or_throw()", function() local result = Result.success "Hello!" local val = result:get_or_throw() assert.equals("Hello!", val) end) it("should throw failure on get_or_throw()", function() local result = Result.failure "Hello!" local err = assert.has_error(function() result:get_or_throw() end) assert.equals("Hello!", err) end) it("should map() success", function() local result = Result.success "Hello" local mapped = result:map(function(x) return x .. " World!" end) assert.equals("Hello World!", mapped:get_or_nil()) assert.is_nil(mapped:err_or_nil()) end) it("should not map() failure", function() local result = Result.failure "Hello" local mapped = result:map(function(x) return x .. " World!" end) assert.equals("Hello", mapped:err_or_nil()) assert.is_nil(mapped:get_or_nil()) end) it("should raise exceptions in map()", function() local result = Result.success "failure" local err = assert.has_error(function() result:map(function() error "error" end) end) assert.equals("error", err) end) it("should map_catching() success", function() local result = Result.success "Hello" local mapped = result:map_catching(function(x) return x .. " World!" end) assert.equals("Hello World!", mapped:get_or_nil()) assert.is_nil(mapped:err_or_nil()) end) it("should not map_catching() failure", function() local result = Result.failure "Hello" local mapped = result:map_catching(function(x) return x .. " World!" end) assert.equals("Hello", mapped:err_or_nil()) assert.is_nil(mapped:get_or_nil()) end) it("should catch errors in map_catching()", function() local result = Result.success "value" local mapped = result:map_catching(function() error "This is an error" end) assert.is_false(mapped:is_success()) assert.is_true(mapped:is_failure()) assert.is_true(match.has_match "This is an error$"(mapped:err_or_nil())) end) it("should recover errors", function() local result = Result.failure("call an ambulance"):recover(function(err) return err .. ". but not for me!" end) assert.is_true(result:is_success()) assert.equals("call an ambulance. but not for me!", result:get_or_nil()) end) it("should catch errors in recover", function() local result = Result.failure("call an ambulance"):recover_catching(function(err) error("Oh no... " .. err, 2) end) assert.is_true(result:is_failure()) assert.equals("Oh no... call an ambulance", result:err_or_nil()) end) it("should return results in run_catching", function() local result = Result.run_catching(function() return "Hello world!" end) assert.is_true(result:is_success()) assert.equals("Hello world!", result:get_or_nil()) end) it("should return failures in run_catching", function() local result = Result.run_catching(function() error("Oh noes", 2) end) assert.is_true(result:is_failure()) assert.equals("Oh noes", result:err_or_nil()) end) it("should run on_failure if failure", function() local on_success = spy.new() local on_failure = spy.new() local result = Result.failure("Oh noes"):on_failure(on_failure):on_success(on_success) assert.is_true(result:is_failure()) assert.equals("Oh noes", result:err_or_nil()) assert.spy(on_failure).was_called(1) assert.spy(on_success).was_called(0) assert.spy(on_failure).was_called_with "Oh noes" end) it("should run on_success if success", function() local on_success = spy.new() local on_failure = spy.new() local result = Result.success("Oh noes"):on_failure(on_failure):on_success(on_success) assert.is_true(result:is_success()) assert.equals("Oh noes", result:get_or_nil()) assert.spy(on_failure).was_called(0) assert.spy(on_success).was_called(1) assert.spy(on_success).was_called_with "Oh noes" end) it("should convert success variants to non-empty optionals", function() local opt = Result.success("Hello world!"):ok() assert.is_true(getmetatable(opt) == Optional) assert.equals("Hello world!", opt:get()) end) it("should convert failure variants to empty optionals", function() local opt = Result.failure("Hello world!"):ok() assert.is_true(getmetatable(opt) == Optional) assert.is_false(opt:is_present()) end) it("should chain successful results", function() local success = Result.success("First"):and_then(function(value) return Result.success(value .. " Second") end) local failure = Result.success("Error"):and_then(Result.failure) assert.is_true(success:is_success()) assert.equals("First Second", success:get_or_nil()) assert.is_true(failure:is_failure()) assert.equals("Error", failure:err_or_nil()) end) it("should not chain failed results", function() local chain = spy.new() local failure = Result.failure("Error"):and_then(chain) assert.is_true(failure:is_failure()) assert.equals("Error", failure:err_or_nil()) assert.spy(chain).was_not_called() end) it("should chain failed results", function() local failure = Result.failure("First"):or_else(function(value) return Result.failure(value .. " Second") end) local success = Result.failure("Error"):or_else(Result.success) assert.is_true(success:is_success()) assert.equals("Error", success:get_or_nil()) assert.is_true(failure:is_failure()) assert.equals("First Second", failure:err_or_nil()) end) it("should not chain successful results", function() local chain = spy.new() local failure = Result.success("Error"):or_else(chain) assert.is_true(failure:is_success()) assert.equals("Error", failure:get_or_nil()) assert.spy(chain).was_not_called() end) it("should pcall", function() assert.same( Result.success "Great success!", Result.pcall(function() return "Great success!" end) ) assert.same( Result.failure "Task failed successfully!", Result.pcall(function() error("Task failed successfully!", 0) end) ) end) end) describe("Result.try", function() it("should try functions", function() assert.same( Result.success "Hello, world!", Result.try(function(try) local hello = try(Result.success "Hello, ") local world = try(Result.success "world!") return hello .. world end) ) assert.same( Result.success(), Result.try(function(try) try(Result.success "Hello, ") try(Result.success "world!") end) ) assert.same( Result.failure "Trouble, world!", Result.try(function(try) local trouble = try(Result.success "Trouble, ") local world = try(Result.success "world!") return try(Result.failure(trouble .. world)) end) ) local failure = Result.try(function(try) local err = try(Result.success "42") error(err, 0) end) assert.is_true(failure:is_failure()) assert.equals("42", failure:err_or_nil()) end) it("should allow calling async functions inside try blocks", function() assert.same( Result.success "Hello, world!", a.run_blocking(function() return Result.try(function(try) a.sleep(10) local hello = try(Result.success "Hello, ") local world = try(Result.success "world!") return hello .. world end) end) ) local failure = a.run_blocking(function() return Result.try(function(try) a.sleep(10) local err = try(Result.success "42") error(err) end) end) assert.is_true(failure:is_failure()) assert.is_true(match.matches ": 42$"(failure:err_or_nil())) end) it("should not unwrap result values in try blocks", function() assert.same( Result.failure "Error!", Result.try(function() return Result.failure "Error!" end) ) assert.same( Result.success "Success!", Result.try(function() return Result.success "Success!" end) ) end) it("should allow nesting try blocks", function() assert.same( Result.success "Hello from the underworld!", Result.try(function(try) local greeting = try(Result.success "Hello from the %s!") return greeting:format(try(Result.try(function(try) return try(Result.success "underworld") end))) end) ) end) it("should allow nesting try blocks in async scope", function() assert.same( Result.success "Hello from the underworld!", a.run_blocking(function() return Result.try(function(try) a.sleep(10) local greeting = try(Result.success "Hello from the %s!") a.sleep(10) return greeting:format(try(Result.try(function(try) a.sleep(10) local value = try(Result.success "underworld") a.sleep(10) return value end))) end) end) ) end) end) ================================================ FILE: tests/mason-core/spawn_spec.lua ================================================ local a = require "mason-core.async" local match = require "luassert.match" local platform = require "mason-core.platform" local process = require "mason-core.process" local spawn = require "mason-core.spawn" local spy = require "luassert.spy" local stub = require "luassert.stub" describe("async spawn", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) it("should spawn commands and return stdout & stderr", function() local result = a.run_blocking(spawn.env, { env_raw = { "FOO=bar" }, }) assert.is_true(result:is_success()) assert.equals("FOO=bar\n", result:get_or_nil().stdout) assert.equals("", result:get_or_nil().stderr) end) it("should use provided stdio_sink", function() local stdout = spy.new() local stdio = process.StdioSink:new { stdout = stdout, } local result = a.run_blocking(spawn.env, { env_raw = { "FOO=bar" }, stdio_sink = stdio, }) assert.is_true(result:is_success()) assert.equals(nil, result:get_or_nil()) -- Not 100 %guaranteed it's only called once because output is always buffered, but it's extremely likely assert.spy(stdout).was_called(1) assert.spy(stdout).was_called_with "FOO=bar\n" end) it("should pass command arguments", function() local result = a.run_blocking(spawn.bash, { "-c", 'echo "Hello $VAR"', env = { VAR = "world" }, }) assert.is_true(result:is_success()) assert.equals("Hello world\n", result:get_or_nil().stdout) assert.equals("", result:get_or_nil().stderr) end) it("should ignore vim.NIL args", function() spy.on(process, "spawn") local result = a.run_blocking(spawn.bash, { vim.NIL, vim.NIL, "-c", { vim.NIL, vim.NIL }, 'echo "Hello $VAR"', env = { VAR = "world" }, }) assert.is_true(result:is_success()) assert.equals("Hello world\n", result:get_or_nil().stdout) assert.equals("", result:get_or_nil().stderr) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( "bash", match.tbl_containing { stdio_sink = match.instanceof(process.BufferedSink), env = match.list_containing "VAR=world", args = match.tbl_containing { "-c", 'echo "Hello $VAR"', }, }, match.is_function() ) end) it("should flatten table args", function() local result = a.run_blocking(spawn.bash, { { "-c", 'echo "Hello $VAR"' }, env = { VAR = "world" }, }) assert.is_true(result:is_success()) assert.equals("Hello world\n", result:get_or_nil().stdout) assert.equals("", result:get_or_nil().stderr) end) it("should call on_spawn", function() local on_spawn = spy.new(function(_, stdio) local stdin = stdio[1] stdin:write "im so piped rn" stdin:close() end) local result = a.run_blocking(spawn.cat, { { "-" }, on_spawn = on_spawn, }) assert.spy(on_spawn).was_called(1) assert.spy(on_spawn).was_called_with(match.is_not.is_nil(), match.is_table(), match.is_number()) assert.is_true(result:is_success()) assert.equals("im so piped rn", result:get_or_nil().stdout) end) it("should not call on_spawn if spawning fails", function() local on_spawn = spy.new() local result = a.run_blocking(spawn.this_cmd_doesnt_exist, { on_spawn = on_spawn, }) assert.spy(on_spawn).was_called(0) assert.is_true(result:is_failure()) end) it("should handle failure to spawn process", function() stub(process, "spawn", function(_, _, callback) callback(false) end) local result = a.run_blocking(spawn.my_cmd, {}) assert.is_true(result:is_failure()) assert.is_nil(result:err_or_nil().exit_code) end) it("should format failure message", function() stub(process, "spawn", function(cmd, opts, callback) opts.stdio_sink:stderr(("This is an error message for %s!"):format(cmd)) callback(false, 127) end) local result = a.run_blocking(spawn.bash, {}) assert.is_true(result:is_failure()) assert.equals( "spawn: bash failed with exit code 127 and signal -. This is an error message for bash!", tostring(result:err_or_nil()) ) end) describe("Windows", function() -- Note: Tests assume they're executed in a Unix environment (e.g. uses Unix path separators in tests). before_each(function() platform.is.win = true end) after_each(function() platform.is.win = nil end) it("should use exepath to get absolute path to executable", function() stub(process, "spawn", function(_, _, callback) callback(true, 0, 0) end) local result = a.run_blocking(spawn.bash, { "arg1" }) assert.is_true(result:is_success()) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( vim.fn.exepath "bash", match.tbl_containing { args = match.same { "arg1" }, }, match.is_function() ) end) it("should use exepath if env.PATH is set", function() stub(process, "spawn", function(_, _, callback) callback(true, 0, 0) end) local result = a.run_blocking(spawn.bash, { "arg1", env = { PATH = "C:\\some\\path:" .. vim.env.PATH } }) assert.is_true(result:is_success()) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( vim.fn.exepath "bash", match.tbl_containing { args = match.same { "arg1" }, env = match.is_table(), }, match.is_function() ) end) it("should use exepath if env_raw.PATH is set", function() stub(process, "spawn", function(_, _, callback) callback(true, 0, 0) end) local result = a.run_blocking(spawn.bash, { "arg1", env_raw = { "PATH=C:\\some\\path:" .. vim.env.PATH } }) assert.is_true(result:is_success()) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( vim.fn.exepath "bash", match.tbl_containing { args = match.same { "arg1" }, env = match.is_table(), }, match.is_function() ) end) it("should default to provided cmd if exepath returns nothing", function() stub(process, "spawn", function(_, _, callback) callback(true, 0, 0) end) local result = a.run_blocking(spawn.bash, { "arg1", env_raw = { "PATH=C:\\some\\path" } }) assert.is_true(result:is_success()) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( "bash", match.tbl_containing { args = match.same { "arg1" }, }, match.is_function() ) end) it("should use exepath if with_paths is provided", function() stub(process, "spawn", function(_, _, callback) callback(true, 0, 0) end) local result = a.run_blocking(spawn.bash, { "arg1", with_paths = { "C:\\some\\path" } }) assert.is_true(result:is_success()) assert.spy(process.spawn).was_called(1) assert.spy(process.spawn).was_called_with( vim.fn.exepath "bash", match.tbl_containing { args = match.same { "arg1" }, }, match.is_function() ) end) end) end) ================================================ FILE: tests/mason-core/terminator_spec.lua ================================================ local InstallHandle = require "mason-core.installer.InstallHandle" local _ = require "mason-core.functional" local a = require "mason-core.async" local match = require "luassert.match" local registry = require "mason-registry" local spy = require "luassert.spy" local stub = require "luassert.stub" local terminator = require "mason-core.terminator" -- describe("terminator", function() -- local snapshot -- -- before_each(function() -- snapshot = assert.snapshot() -- end) -- -- after_each(function() -- -- wait for scheduled calls to expire -- a.run_blocking(a.wait, vim.schedule) -- snapshot:revert() -- end) -- -- it("should terminate all active handles on nvim exit", function() -- spy.on(InstallHandle, "terminate") -- local dummy = registry.get_package "dummy" -- local dummy2 = registry.get_package "dummy2" -- for _, pkg in ipairs { dummy, dummy2 } do -- stub(pkg.spec.source, "install", function() -- a.sleep(10000) -- end) -- end -- -- local dummy_handle = dummy:install() -- local dummy2_handle = dummy2:install() -- -- assert.wait(function() -- assert.spy(dummy.spec.source.install).was_called() -- assert.spy(dummy2.spec.source.install).was_called() -- end) -- -- terminator.terminate(5000) -- -- assert.spy(InstallHandle.terminate).was_called(2) -- assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy_handle)) -- assert.spy(InstallHandle.terminate).was_called_with(match.is_ref(dummy2_handle)) -- assert.wait(function() -- assert.is_true(dummy_handle:is_closed()) -- assert.is_true(dummy2_handle:is_closed()) -- end) -- end) -- -- it("should print warning messages", function() -- spy.on(vim.api, "nvim_echo") -- spy.on(vim.api, "nvim_err_writeln") -- local dummy = registry.get_package "dummy" -- local dummy2 = registry.get_package "dummy2" -- for _, pkg in ipairs { dummy, dummy2 } do -- stub(pkg.spec.source, "install", function() -- a.sleep(10000) -- end) -- end -- -- local dummy_handle = dummy:install() -- local dummy2_handle = dummy2:install() -- -- assert.wait(function() -- assert.spy(dummy.spec.source.install).was_called() -- assert.spy(dummy2.spec.source.install).was_called() -- end) -- -- terminator.terminate(5000) -- -- assert.spy(vim.api.nvim_echo).was_called(1) -- assert.spy(vim.api.nvim_echo).was_called_with({ -- { -- "[mason.nvim] Neovim is exiting while packages are still installing. Terminating all installations…", -- "WarningMsg", -- }, -- }, true, {}) -- -- a.run_blocking(a.wait, vim.schedule) -- -- assert.spy(vim.api.nvim_err_writeln).was_called(1) -- assert.spy(vim.api.nvim_err_writeln).was_called_with(_.dedent [[ -- [mason.nvim] Neovim exited while the following packages were installing. Installation was aborted. -- - dummy -- - dummy2 -- ]]) -- assert.wait(function() -- assert.is_true(dummy_handle:is_closed()) -- assert.is_true(dummy2_handle:is_closed()) -- end) -- end) -- -- it("should send SIGTERM and then SIGKILL after grace period", function() -- spy.on(InstallHandle, "kill") -- local dummy = registry.get_package "dummy" -- stub(dummy.spec.source, "install", function(ctx) -- -- your signals have no power here -- ctx.spawn.bash { "-c", "function noop { :; }; trap noop SIGTERM; sleep 999999;" } -- end) -- -- local handle = dummy:install() -- -- assert.wait(function() -- assert.spy(dummy.spec.source.install).was_called() -- end) -- terminator.terminate(50) -- -- assert.wait(function() -- assert.spy(InstallHandle.kill).was_called(2) -- assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 15) -- SIGTERM -- assert.spy(InstallHandle.kill).was_called_with(match.is_ref(handle), 9) -- SIGKILL -- end) -- -- assert.wait(function() -- assert.is_true(handle:is_closed()) -- end) -- end) -- end) ================================================ FILE: tests/mason-core/ui_spec.lua ================================================ local Ui = require "mason-core.ui" local a = require "mason-core.async" local display = require "mason-core.ui.display" local match = require "luassert.match" local spy = require "luassert.spy" describe("ui", function() it("produces a correct tree", function() local function renderer(state) return Ui.CascadingStyleNode({ "INDENT" }, { Ui.When(not state.is_active, function() return Ui.Text { "I'm not active", "Another line", } end), Ui.When(state.is_active, function() return Ui.Text { "I'm active", "Yet another line", } end), }) end assert.same({ children = { { type = "HL_TEXT", lines = { { { "I'm not active", "" } }, { { "Another line", "" } }, }, }, { type = "NODE", children = {}, }, }, styles = { "INDENT" }, type = "CASCADING_STYLE", }, renderer { is_active = false }) assert.same({ children = { { type = "NODE", children = {}, }, { type = "HL_TEXT", lines = { { { "I'm active", "" } }, { { "Yet another line", "" } }, }, }, }, styles = { "INDENT" }, type = "CASCADING_STYLE", }, renderer { is_active = true }) end) it("renders a tree correctly", function() local render_output = display._render_node( { win_width = 120, }, Ui.CascadingStyleNode({ "INDENT" }, { Ui.Keybind("i", "INSTALL_PACKAGE", { "sumneko_lua" }, true), Ui.HlTextNode { { { "Hello World!", "MyHighlightGroup" }, }, { { "Another Line", "Comment" }, }, }, Ui.HlTextNode { { { "Install something idk", "Stuff" }, }, }, Ui.StickyCursor { id = "sticky" }, Ui.Keybind("", "INSTALL_PACKAGE", { "tsserver" }, false), Ui.DiagnosticsNode { message = "yeah this one's outdated", severity = vim.diagnostic.severity.WARN, source = "trust me bro", }, Ui.Text { "I'm a text node" }, }) ) assert.same({ highlights = { { col_start = 2, col_end = 14, line = 0, hl_group = "MyHighlightGroup", }, { col_start = 2, col_end = 14, line = 1, hl_group = "Comment", }, { col_start = 2, col_end = 23, line = 2, hl_group = "Stuff", }, }, lines = { " Hello World!", " Another Line", " Install something idk", " I'm a text node" }, virt_texts = {}, sticky_cursors = { line_map = { [3] = "sticky" }, id_map = { ["sticky"] = 3 } }, keybinds = { { effect = "INSTALL_PACKAGE", key = "i", line = -1, payload = { "sumneko_lua" }, }, { effect = "INSTALL_PACKAGE", key = "", line = 3, payload = { "tsserver" }, }, }, diagnostics = { { line = 3, message = "yeah this one's outdated", source = "trust me bro", severity = vim.diagnostic.severity.WARN, }, }, }, render_output) end) end) describe("integration test", function() it("calls vim APIs as expected during rendering", function() local window = display.new_view_only_win("test", "my-filetype") window.view(function(state) return Ui.Node { Ui.Keybind("U", "EFFECT", nil, true), Ui.Text { "Line number 1!", state.text, }, Ui.Keybind("R", "R_EFFECT", { state.text }), Ui.HlTextNode { { { "My highlighted text", "MyHighlightGroup" }, }, }, } end) local mutate_state = window.state { text = "Initial state" } local clear_namespace = spy.on(vim.api, "nvim_buf_clear_namespace") local buf_set_option = spy.on(vim.api, "nvim_buf_set_option") local win_set_option = spy.on(vim.api, "nvim_win_set_option") local set_lines = spy.on(vim.api, "nvim_buf_set_lines") local set_extmark = spy.on(vim.api, "nvim_buf_set_extmark") local add_highlight = spy.on(vim.api, "nvim_buf_add_highlight") local set_keymap = spy.on(vim.keymap, "set") window.init { effects = { ["EFFECT"] = function() end, ["R_EFFECT"] = function() end, }, winhighlight = { "NormalFloat:MasonNormal", "CursorLine:MasonCursorLine", }, } window.open() -- Initial window and buffer creation + initial render a.run_blocking(a.wait, vim.schedule) assert.spy(win_set_option).was_called(9) assert.spy(win_set_option).was_called_with(match.is_number(), "number", false) assert.spy(win_set_option).was_called_with(match.is_number(), "relativenumber", false) assert.spy(win_set_option).was_called_with(match.is_number(), "wrap", false) assert.spy(win_set_option).was_called_with(match.is_number(), "spell", false) assert.spy(win_set_option).was_called_with(match.is_number(), "foldenable", false) assert.spy(win_set_option).was_called_with(match.is_number(), "signcolumn", "no") assert.spy(win_set_option).was_called_with(match.is_number(), "colorcolumn", "") assert.spy(win_set_option).was_called_with(match.is_number(), "cursorline", true) assert .spy(win_set_option) .was_called_with(match.is_number(), "winhighlight", "NormalFloat:MasonNormal,CursorLine:MasonCursorLine") assert.spy(buf_set_option).was_called(10) assert.spy(buf_set_option).was_called_with(match.is_number(), "modifiable", false) assert.spy(buf_set_option).was_called_with(match.is_number(), "swapfile", false) assert.spy(buf_set_option).was_called_with(match.is_number(), "textwidth", 0) assert.spy(buf_set_option).was_called_with(match.is_number(), "buftype", "nofile") assert.spy(buf_set_option).was_called_with(match.is_number(), "bufhidden", "wipe") assert.spy(buf_set_option).was_called_with(match.is_number(), "buflisted", false) assert.spy(buf_set_option).was_called_with(match.is_number(), "filetype", "my-filetype") assert.spy(buf_set_option).was_called_with(match.is_number(), "undolevels", -1) assert.spy(set_lines).was_called(1) assert .spy(set_lines) .was_called_with(match.is_number(), 0, -1, false, { "Line number 1!", "Initial state", "My highlighted text" }) assert.spy(set_extmark).was_called(0) assert.spy(add_highlight).was_called(1) assert.spy(add_highlight).was_called_with(match.is_number(), match.is_number(), "MyHighlightGroup", 2, 0, 19) assert.spy(set_keymap).was_called(2) assert.spy(set_keymap).was_called_with( "n", "U", match.is_function(), match.tbl_containing { nowait = true, silent = true, buffer = match.is_number() } ) assert.spy(set_keymap).was_called_with( "n", "R", match.is_function(), match.tbl_containing { nowait = true, silent = true, buffer = match.is_number() } ) assert.spy(clear_namespace).was_called(1) assert.spy(clear_namespace).was_called_with(match.is_number(), match.is_number(), 0, -1) mutate_state(function(state) state.text = "New state" end) assert.spy(set_lines).was_called(1) a.run_blocking(a.wait, vim.schedule) assert.spy(set_lines).was_called(2) assert .spy(set_lines) .was_called_with(match.is_number(), 0, -1, false, { "Line number 1!", "New state", "My highlighted text" }) end) it("anchors to sticky cursor", function() local window = display.new_view_only_win("test", "my-filetype") window.view(function(state) local extra_lines = state.show_extra_lines and Ui.Text { "More", "Lines", "Here", } or Ui.Node {} return Ui.Node { extra_lines, Ui.Text { "Line 1", "Line 2", "Line 3", "Line 4", "Special line", }, Ui.StickyCursor { id = "special" }, Ui.Text { "Line 6", "Line 7", "Line 8", "Line 9", "Line 10", }, } end) local mutate_state = window.state { show_extra_lines = false } window.init {} window.open() a.run_blocking(a.wait, vim.schedule) window.set_cursor { 5, 3 } -- move cursor to sticky line mutate_state(function(state) state.show_extra_lines = true end) a.run_blocking(a.wait, vim.schedule) local cursor = window.get_cursor() assert.same({ 8, 3 }, cursor) end) it("should respect border ui setting", function() local nvim_open_win = spy.on(vim.api, "nvim_open_win") local window = display.new_view_only_win("test", "my-filetype") window.view(function() return Ui.Node {} end) window.state {} window.init { border = "rounded" } window.open() a.run_blocking(a.wait, vim.schedule) assert.spy(nvim_open_win).was_called(1) assert.spy(nvim_open_win).was_called_with( match.is_number(), true, match.tbl_containing { border = "rounded", } ) end) it("should not apply cascading styles to empty lines", function() local render_output = display._render_node( { win_width = 120, }, Ui.CascadingStyleNode({ "INDENT" }, { Ui.HlTextNode { { { "Hello World!", "MyHighlightGroup" }, }, { { "", "" }, }, }, }) ) assert.same({ " Hello World!", "" }, render_output.lines) end) end) ================================================ FILE: tests/mason-registry/api_spec.lua ================================================ local Result = require "mason-core.result" local match = require "luassert.match" local stub = require "luassert.stub" describe("mason-registry API", function() local snapshot before_each(function() snapshot = assert.snapshot() end) after_each(function() snapshot:revert() end) ---@module "mason-registry.api" local api local fetch before_each(function() fetch = stub.new() package.loaded["mason-core.fetch"] = fetch package.loaded["mason-registry.api"] = nil api = require "mason-registry.api" end) it("should stringify query parameters", function() fetch.returns(Result.success [[{}]]) api.get("/api/data", { params = { page = 2, page_limit = 10, sort = "ASC", }, }) assert.spy(fetch).was_called(1) assert.spy(fetch).was_called_with("https://api.mason-registry.dev/api/data?page=2&page_limit=10&sort=ASC", { headers = { Accept = "application/vnd.mason-registry.v1+json; q=1.0, application/json; q=0.8", }, }) end) it("should deserialize JSON", function() fetch.returns(Result.success [[{"field": ["value"]}]]) local result = api.get("/"):get_or_throw() assert.same({ field = { "value" } }, result) end) it("should interpolate path parameters", function() fetch.returns(Result.success [[{}]]) local result = api.github.releases.latest { repo = "myrepo/name" } assert.is_true(result:is_success()) assert.spy(fetch).was_called(1) assert.spy(fetch).was_called_with(match.is_match "/api/github/myrepo/name/releases/latest$", match.is_table()) end) it("should percent encode path parameters", function() assert.equals("golang.org%2fx%2ftools%2fgopls", api.encode_uri_component "golang.org/x/tools/gopls") end) end) ================================================ FILE: tests/mason-registry/registry_spec.lua ================================================ local Pkg = require "mason-core.package" local registry = require "mason-registry" local test_helpers = require "mason-test.helpers" describe("mason-registry", function() it("should return package", function() assert.is_true(getmetatable(registry.get_package "dummy").__index == Pkg) end) it("should error when getting non-existent package", function() local err = assert.has_error(function() registry.get_package "non-existent" end) assert.equals([[Cannot find package "non-existent".]], err) end) it("should check whether package exists", function() assert.is_true(registry.has_package "dummy") assert.is_false(registry.has_package "non-existent") end) it("should get all package specs", function() assert.equals(3, #registry.get_all_package_specs()) end) it("should check if package is installed", function() local dummy = registry.get_package "dummy" -- TODO unflake this in a better way if dummy:is_installed() then test_helpers.sync_uninstall(dummy) end assert.is_false(registry.is_installed "dummy") test_helpers.sync_install(dummy) assert.is_true(registry.is_installed "dummy") end) end) ================================================ FILE: tests/mason-registry/sources/collection_spec.lua ================================================ local LazySourceCollection = require "mason-registry.sources" local SynthesizedSource = require "mason-registry.sources.synthesized" describe("LazySourceCollection", function() it("should dedupe registries on append/prepend", function() local coll = LazySourceCollection:new() coll:append "github:mason-org/mason-registry" coll:prepend "github:mason-org/mason-registry@2025-05-16" coll:prepend "github:my-own/registry" coll:prepend "lua:registry" coll:append "lua:registry" coll:append "file:~/registry" coll:append "file:$HOME/registry" assert.equals(4, coll:size()) assert.same("lua:registry", coll:get(1):get_full_id()) assert.same("github:my-own/registry", coll:get(2):get_full_id()) assert.same("github:mason-org/mason-registry@2025-05-16", coll:get(3):get_full_id()) assert.same("file:~/registry", coll:get(4):get_full_id()) end) it("should fall back to synthesized source", function() local coll = LazySourceCollection:new() for source in coll:iterate() do assert.is_true(getmetatable(source) == SynthesizedSource) return end error "Did not fall back to synthesized source" end) it("should exclude synthesized source", function() local coll = LazySourceCollection:new() for source in coll:iterate { include_synthesized = false } do error "Should not iterate." end end) end) ================================================ FILE: tests/mason-registry/sources/lua_spec.lua ================================================ local LuaRegistrySource = require "mason-registry.sources.lua" describe("Lua registry source", function() it("should get package", function() local source = LuaRegistrySource:new { mod = "dummy-registry.index", } assert.is_true(source:install():is_success()) assert.is_not_nil(source:get_package "dummy") assert.is_nil(source:get_package "non-existent") end) it("should get all package names", function() local source = LuaRegistrySource:new { mod = "dummy-registry.index", } assert.is_true(source:install():is_success()) local package_names = source:get_all_package_names() table.sort(package_names) assert.same({ "dummy", "dummy2", "registry", }, package_names) end) it("should check if is installed", function() local installed_source = LuaRegistrySource:new { mod = "dummy-registry.index", } local uninstalled_source = LuaRegistrySource:new { mod = "non-existent", } assert.is_true(installed_source:install():is_success()) assert.is_true(installed_source:is_installed()) assert.is_false(uninstalled_source:is_installed()) end) it("should stringify instances", function() assert.equals("LuaRegistrySource(mod=pkg-index)", tostring(LuaRegistrySource:new { mod = "pkg-index" })) end) end) ================================================ FILE: tests/minimal_init.vim ================================================ " Avoid neovim/neovim#11362 set display=lastline set directory="" set noswapfile let $mason = getcwd() let $test_helpers = getcwd() .. "/tests/helpers" let $dependencies = getcwd() .. "/dependencies" set rtp^=$mason,$test_helpers set packpath=$dependencies packloadall lua require("luassertx") lua <