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
================================================



[](https://github.com/mason-org/mason.nvim/actions?query=workflow%3ATests+branch%3Amain+event%3Apush)
[](https://github.com/sponsors/williamboman)
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
| | | |
| :----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------: |
|
|
|
|
|
|
|
|
## 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 <