Compare commits

..

No commits in common. "main" and "v1.2.0" have entirely different histories.

102 changed files with 1962 additions and 132709 deletions

11
.busted
View file

@ -1,11 +0,0 @@
return {
_all = {
coverage = false,
},
default = {
verbose = true,
},
tests = {
verbose = true,
},
}

View file

@ -1,7 +0,0 @@
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8

View file

@ -1,3 +0,0 @@
{
".": "11.17.1"
}

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,36 @@
---
name: Bug report
about: "Create a report to help us improve"
title: ""
labels: "bug"
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**Which version of Neovim are you using?**
Gui(specify which GUI client you are using)? Nightly? Version?
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Log**
Please include any related errors from the Noice log file. (open with `:Lazy log`)
<details>
<summary>Lazy log</summary>
<pre>
paste log here
</pre>
</details>

View file

@ -1,75 +0,0 @@
name: Bug Report
description: File a bug/issue
title: "bug: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
**Before** reporting an issue, make sure to read the [documentation](https://github.com/folke/lazy.nvim)
and search [existing issues](https://github.com/folke/lazy.nvim/issues).
Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/folke/lazy.nvim/discussions) and will be closed.
- type: checkboxes
attributes:
label: Did you check docs and existing issues?
description: Make sure you checked all of the below before submitting an issue
options:
- label: I have read all the lazy.nvim docs
required: true
- label: I have updated the plugin to the latest version before submitting this issue
required: true
- label: I have searched the existing issues of lazy.nvim
required: true
- label: I have searched the existing issues of plugins related to this issue
required: true
- type: input
attributes:
label: "Neovim version (nvim -v)"
placeholder: "0.8.0 commit db1b0ee3b30f"
validations:
required: true
- type: input
attributes:
label: "Operating system/version"
placeholder: "MacOS 11.5"
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Repro
description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`
value: |
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
require("lazy.minit").repro({
spec = {
-- add any other plugins here
},
})
render: lua
validations:
required: false

View file

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/folke/lazy.nvim/discussions
about: Use Github discussions instead

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,36 +0,0 @@
name: Feature Request
description: Suggest a new feature
title: "feature: "
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Did you check the docs?
description: Make sure you read all the docs before submitting a feature request
options:
- label: I have read all the lazy.nvim docs
required: true
- type: textarea
validations:
required: true
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
- type: textarea
validations:
required: true
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
- type: textarea
validations:
required: true
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
validations:
required: false
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.

View file

@ -1,16 +0,0 @@
## Description
<!-- Describe the big picture of your changes to communicate to the maintainers
why we should accept this pull request. -->
## Related Issue(s)
<!--
If this PR fixes any issues, please link to the issue here.
- Fixes #<issue_number>
-->
## Screenshots
<!-- Add screenshots of the changes if applicable. -->

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View file

@ -1,9 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "simple",
"extra-files": ["lua/lazy/core/config.lua"]
}
}
}

View file

@ -1,15 +1,48 @@
name: CI name: CI
on: on:
push: push:
branches: [main, master]
pull_request: pull_request:
jobs: jobs:
ci: tests:
uses: folke/github/.github/workflows/ci.yml@main runs-on: ubuntu-latest
secrets: inherit steps:
with: - uses: actions/checkout@v3
plugin: lazy.nvim - name: Install Neovim
repo: folke/lazy.nvim run: |
docs: false wget -q https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.deb -O /tmp/nvim.deb
sudo dpkg -i /tmp/nvim.deb
- name: Run Tests
run: |
nvim --version
./tests/run
docs:
runs-on: ubuntu-latest
needs: tests
if: ${{ github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v3
- name: panvimdoc
uses: kdheepak/panvimdoc@main
with:
vimdoc: lazy.nvim
demojify: true
- name: Push changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "chore(build): auto-generate vimdoc"
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
release:
name: release
if: ${{ github.ref == 'refs/heads/main' }}
needs:
- docs
- tests
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
with:
release-type: simple
package-name: lazy.nvim

View file

@ -1,30 +0,0 @@
name: Community
on:
push:
branches:
- main
jobs:
community:
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'folke' }}
steps:
- uses: actions/checkout@v4
- uses: folke/github/neovim@main
- name: Rockspec Build
id: rockspec-build
uses: actions/cache@v4
with:
path: build
key: rockspec-build
- name: Generate Rockspec
if: steps.rockspec-build.cache-hit != 'true'
run: |
nvim -l lua/lazy/build.lua
- name: Push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore(build): auto-generate rockspec mappings"
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"

View file

@ -1,19 +0,0 @@
name: Docs
on:
push:
branches:
- main
jobs:
docs:
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'folke' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
ref: docs
- name: Generate Docs
shell: bash
run: gh workflow run "Deploy to Github Pages" --ref docs

View file

@ -1,8 +0,0 @@
name: "PR Labeler"
on:
- pull_request_target
jobs:
labeler:
uses: folke/github/.github/workflows/labeler.yml@main
secrets: inherit

View file

@ -1,18 +0,0 @@
name: PR Title
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
- ready_for_review
permissions:
pull-requests: read
jobs:
pr-title:
uses: folke/github/.github/workflows/pr.yml@main
secrets: inherit

View file

@ -1,11 +0,0 @@
name: Stale Issues & PRs
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
if: contains(fromJSON('["folke", "LazyVim"]'), github.repository_owner)
uses: folke/github/.github/workflows/stale.yml@main
secrets: inherit

View file

@ -1,13 +0,0 @@
name: Update Repo
on:
workflow_dispatch:
schedule:
# Run every hour
- cron: "0 * * * *"
jobs:
update:
if: contains(fromJSON('["folke", "LazyVim"]'), github.repository_owner)
uses: folke/github/.github/workflows/update.yml@main
secrets: inherit

12
.gitignore vendored
View file

@ -1,9 +1,3 @@
*.log tt.lua
/.repro .tests
/.tests doc/tags
/build
/debug
/doc/tags
foo.*
node_modules
tt.*

View file

@ -1,12 +0,0 @@
MD013:
line_length: 120
tables: false
MD033:
allowed_elements:
- "details"
- "summary"
- "b"
- "table"
- "tr"
- "td"
- "a"

View file

@ -7,7 +7,7 @@
} }
}, },
"lspconfig": { "lspconfig": {
"lua_ls": { "sumneko_lua": {
"Lua.runtime.version": "LuaJIT", "Lua.runtime.version": "LuaJIT",
"Lua.workspace.checkThirdParty": false "Lua.workspace.checkThirdParty": false
} }

View file

@ -1 +0,0 @@
lua/lazy/community/_generated.lua

File diff suppressed because it is too large Load diff

201
LICENSE
View file

@ -1,201 +0,0 @@
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.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

129
README.md
View file

@ -1,69 +1,74 @@
<h4 align="center"> # lazy.nvim
<a href="https://lazy.folke.io/installation">Install</a>
·
<a href="https://lazy.folke.io/configuration">Configure</a>
·
<a href="https://lazy.folke.io">Docs</a>
</h4>
<div align="center"><p>
<a href="https://github.com/folke/lazy.nvim/releases/latest">
<img alt="Latest release" src="https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver" />
</a>
<a href="https://github.com/folke/lazy.nvim/pulse">
<img alt="Last commit" src="https://img.shields.io/github/last-commit/folke/lazy.nvim?style=for-the-badge&logo=starship&color=8bd5ca&logoColor=D9E0EE&labelColor=302D41"/>
</a>
<a href="https://github.com/folke/lazy.nvim/blob/main/LICENSE">
<img alt="License" src="https://img.shields.io/github/license/folke/lazy.nvim?style=for-the-badge&logo=starship&color=ee999f&logoColor=D9E0EE&labelColor=302D41" />
</a>
<a href="https://github.com/folke/lazy.nvim/stargazers">
<img alt="Stars" src="https://img.shields.io/github/stars/folke/lazy.nvim?style=for-the-badge&logo=starship&color=c69ff5&logoColor=D9E0EE&labelColor=302D41" />
</a>
<a href="https://github.com/folke/lazy.nvim/issues">
<img alt="Issues" src="https://img.shields.io/github/issues/folke/lazy.nvim?style=for-the-badge&logo=bilibili&color=F5E0DC&logoColor=D9E0EE&labelColor=302D41" />
</a>
<a href="https://github.com/folke/lazy.nvim">
<img alt="Repo Size" src="https://img.shields.io/github/repo-size/folke/lazy.nvim?color=%23DDB6F2&label=SIZE&logo=codesandbox&style=for-the-badge&logoColor=D9E0EE&labelColor=302D41" />
</a>
<a href="https://twitter.com/intent/follow?screen_name=folke">
<img alt="follow on Twitter" src="https://img.shields.io/twitter/follow/folke?style=for-the-badge&logo=twitter&color=8aadf3&logoColor=D9E0EE&labelColor=302D41" />
</a>
</div>
**lazy.nvim** is a modern plugin manager for Neovim.
![image](https://user-images.githubusercontent.com/292349/208301737-68fb279c-ba70-43ef-a369-8c3e8367d6b1.png)
## ✨ Features ## ✨ Features
- 📦 Manage all your Neovim plugins with a powerful UI - [x] Partial clones instead of shallow clones
- 🚀 Fast startup times thanks to automatic caching and bytecode compilation of Lua modules - [x] waits till missing deps are installed (bootstrap Neovim and start using it right away)
- 💾 Partial clones instead of shallow clones - [x] Async
- 🔌 Automatic lazy-loading of Lua modules and lazy-loading on events, commands, filetypes, and key mappings - [x] No need for compile
- ⏳ Automatically install missing plugins before starting up Neovim, allowing you to start using it right away - [x] Fast
- 💪 Async execution for improved performance - [x] Correct sequencing of dependencies (deps should always be opt. Maybe make everything opt?)
- 🛠️ No need to manually compile plugins - [x] Config in multiple files
- 🧪 Correct sequencing of dependencies - [x] Patterns for local packages
- 📁 Configurable in multiple files - [x] Profiling
- 📚 Generates helptags of the headings in `README.md` files for plugins that don't have vimdocs - [x] lockfile
- 💻 Dev options and patterns for using local plugins - [x] upvalues in `config` & `init`
- 📊 Profiling tools to optimize performance - [x] check for updates
- 🔒 Lockfile `lazy-lock.json` to keep track of installed plugins - [x] lazy-lock.lua
- 🔎 Automatically check for updates - [x] tag/version support `git tag --sort version:refname`
- 📋 Commit, branch, tag, version, and full [Semver](https://devhints.io/semver) support - [x] auto-loading on completion for lazy-loaded commands
- 📈 Statusline component to see the number of pending updates - [x] bootstrap code
- 🎨 Automatically lazy-loads colorschemes - [x] semver https://devhints.io/semver
https://semver.npmjs.com/
## ⚡️ Requirements ## ✅ TODO
- Neovim >= **0.8.0** (needs to be built with **LuaJIT**) - [ ] health checks: check merge conflicts async
- Git >= **2.19.0** (for partial clones support) - [ ] allow setting up plugins through config
- a [Nerd Font](https://www.nerdfonts.com/) **_(optional)_** - [x] task timeout
- [luarocks](https://luarocks.org/) to install rockspecs. - [ ] log file
You can remove `rockspec` from `opts.pkg.sources` to disable this feature. - [ ] deal with resourcing init.lua. Check a global?
- [x] incorrect when switching TN from opt to start
- [ ] git tests
- [x] max concurrency
- [x] ui border
- [ ] make sure we can reload specs while keeping state
- [ ] show disabled plugins (strikethrough?)
- [ ] Import specs from Packer
- [ ] use uv file watcher (or stat) to check for config changes
- [ ] [packspec](https://github.com/nvim-lua/nvim-package-specification)
- [ ] add support to specify `engines`, `os` and `cpu` like in `package.json`
- [ ] semver merging. Should check if two or more semver ranges are compatible and calculate the union range
- default semver merging strategy: if no version matches all, then use highest version?
- [x] support for Plugin.lock
- [x] defaults for git log
- [x] view keybindings for update/clean/...
- [x] add profiler to view
- [x] add buttons for actions
- [x] show time taken for op in view
- [ ] package meta index (package.lua cache for all packages)
- [ ] auto lazy-loading of lua modules
- [x] clear errors
- [x] add support for versions `git tag --sort v:refname`
- [x] rename requires to dependencies
- [x] move tasks etc to Plugin.state
- [x] handlers imply opt
- [x] dependencies imply opt for deps
- [x] fix local plugin spec
- [ ] investigate all opt=true. Simplifies logic (easily switch between opt/start afterwards)
## 🚀 Getting Started ## 📦 Differences with Packer
Check the [documentation website](https://lazy.folke.io/) for more information. - **Plugin Spec**:
- `setup` => `init`
- `requires` => `dependencies`
- `as` => `name`
## 📦 Other Neovim Plugin Managers in Lua
- [packer.nvim](https://github.com/wbthomason/packer.nvim)
- [paq-nvim](https://github.com/savq/paq-nvim)
- [neopm](https://github.com/ii14/neopm)
- [dep](https://github.com/chiyadev/dep)
- [optpack.nvim](https://github.com/notomo/optpack.nvim)

79
TODO.md
View file

@ -1,79 +0,0 @@
# ✅ TODO
- [x] progress bar?
- [x] options when opening file
- [x] lazy notify? not ideal when installing missing stuff
- [x] topmods?
- [ ] better merging options?
- [ ] especially what to do with merging of handlers?
- [ ] overwriting keymaps probably doesn't work
- [ ] disabled deps?
- [x] fancy UI to manage all your Neovim plugins
- [x] auto lazy-loading of lua modules
- [x] lazy-loading on events, commands, filetypes and key mappings
- [x] Partial clones instead of shallow clones
- [x] waits till missing deps are installed (bootstrap Neovim and start using it right away)
- [x] Async
- [x] No need to manually compile
- [x] Fast. Automatically caches and compiles byte code of all lua modules needed during startup
- [x] Correct sequencing of dependencies (deps should always be opt. Maybe make everything opt?)
- [x] Config in multiple files
- [x] dev option and patterns for local packages
- [x] Profiling
- [x] lockfile `lazy-lock.json`
- [x] upvalues in `config` & `init`
- [x] automatically check for updates
- [x] commit, branch, tag, version and full semver support
- [x] statusline component to see number of pending updates
- [x] semver https://devhints.io/semver
- [x] auto-loading on completion for lazy-loaded commands
- [x] bootstrap code
- [x] Background update checker
- [x] health checks: check merge conflicts async
- [x] unsupported props or props from other managers
- [x] other packages still in site?
- [x] other package manager artifacts still present? compiled etc
- [x] status page showing running handlers and cache stats
- [x] temp colorscheme used during startup when installing missing plugins
- [x] automatically reloads when config changes are detected
- [x] handlers imply opt
- [x] dependencies imply opt for deps
- [x] show spec errors in health
- [x] fix plugin details
- [ ] show disabled plugins (strikethrough?)
- [ ] log file
- [x] git tests
- [x] Import specs from other plugin managers
- [ ] [packspec](https://github.com/nvim-lua/nvim-package-specification)
- [ ] add support to specify `engines`, `os` and `cpu` like in `package.json`
- [ ] semver merging. Should check if two or more semver ranges are compatible and calculate the union range
- default semver merging strategy: if no version matches all, then use the highest version?
- [ ] package meta index (package.lua cache for all packages)
- [x] document highlight groups
- [x] document user events
- [x] document API, like lazy.plugins()
- [x] icons
- [x] check in cache if rtp files match
- [x] I think the installation section, specifically the loading part, could use an
extra sentence or two. I was confused on what `config.plugins` was initially.
Maybe a quick, "for example, if you have a lua file
`~/.config/nvim/lua/config/plugins.lua` that returns a table" or something it'd
remove most question marks I think.
- [x] When auto-installing the plugins the cursor isn't focused on the floating
window, but on the non-floating window in the background.
- [x] Doing `:Lazy clean` doesn't show which plugins were removed.
- [x] Shouldn't the "Versioning" section be in the "Lockfile" chapter?
- [x] Why are personal dotfiles used as examples? Dotfiles change all the time,
there's no guarantee this will be relevant or even exist in two years.
- [x] What's the difference between lazy-loading and verylazy-loading?
- [x] Most emojis in "Configuration" aren't shown for me.
- [x] add section on how to uninstall
- [x] add `:Packadd` command or something similar
- [x] headless install
- [x] better keys handling

View file

@ -1,51 +0,0 @@
-- Lazy Bootstrapper
-- Usage:
-- ```lua
-- load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
-- ```
local M = {}
function M.setup()
local uv = vim.uv or vim.loop
if vim.env.LAZY_STDPATH then
local root = vim.fn.fnamemodify(vim.env.LAZY_STDPATH, ":p"):gsub("[\\/]$", "")
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
end
if vim.env.LAZY_PATH and not uv.fs_stat(vim.env.LAZY_PATH) then
vim.env.LAZY_PATH = nil
end
local lazypath = vim.env.LAZY_PATH or vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.env.LAZY_PATH and not uv.fs_stat(lazypath) then
vim.api.nvim_echo({
{
"Cloning lazy.nvim\n\n",
"DiagnosticInfo",
},
}, true, {})
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local ok, out = pcall(vim.fn.system, {
"git",
"clone",
"--filter=blob:none",
lazyrepo,
lazypath,
})
if not ok or vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim\n", "ErrorMsg" },
{ vim.trim(out or ""), "WarningMsg" },
{ "\nPress any key to exit...", "MoreMsg" },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
end
M.setup()
return M

View file

File diff suppressed because it is too large Load diff

View file

@ -1,222 +0,0 @@
local Util = require("lazy.core.util")
local M = {}
---@type Async[]
M._active = {}
---@type Async[]
M._suspended = {}
M._executor = assert(vim.loop.new_check())
M.BUDGET = 10
---@type table<thread, Async>
M._threads = setmetatable({}, { __mode = "k" })
---@alias AsyncEvent "done" | "error" | "yield" | "ok"
---@class Async
---@field _co thread
---@field _fn fun()
---@field _suspended? boolean
---@field _on table<AsyncEvent, fun(res:any, async:Async)[]>
local Async = {}
---@param fn async fun()
---@return Async
function Async.new(fn)
local self = setmetatable({}, { __index = Async })
return self:init(fn)
end
---@param fn async fun()
---@return Async
function Async:init(fn)
self._fn = fn
self._on = {}
self._co = coroutine.create(function()
local ok, err = pcall(self._fn)
if not ok then
self:_emit("error", err)
end
self:_emit("done")
end)
M._threads[self._co] = self
return M.add(self)
end
---@param event AsyncEvent
---@param cb async fun(res:any, async:Async)
function Async:on(event, cb)
self._on[event] = self._on[event] or {}
table.insert(self._on[event], cb)
return self
end
---@private
---@param event AsyncEvent
---@param res any
function Async:_emit(event, res)
for _, cb in ipairs(self._on[event] or {}) do
cb(res, self)
end
end
function Async:running()
return coroutine.status(self._co) ~= "dead"
end
---@async
function Async:sleep(ms)
vim.defer_fn(function()
self:resume()
end, ms)
self:suspend()
end
---@async
---@param yield? boolean
function Async:suspend(yield)
self._suspended = true
if coroutine.running() == self._co and yield ~= false then
M.yield()
end
end
function Async:resume()
self._suspended = false
M._run()
end
---@async
---@param yield? boolean
function Async:wake(yield)
local async = M.running()
assert(async, "Not in an async context")
self:on("done", function()
async:resume()
end)
async:suspend(yield)
end
---@async
function Async:wait()
if coroutine.running() == self._co then
error("Cannot wait on self")
end
local async = M.running()
if async then
self:wake()
else
while self:running() do
vim.wait(10)
end
end
return self
end
function Async:step()
if self._suspended then
return true
end
local status = coroutine.status(self._co)
if status == "suspended" then
local ok, res = coroutine.resume(self._co)
if not ok then
error(res)
elseif res then
self:_emit("yield", res)
end
end
return self:running()
end
function M.abort()
for _, async in ipairs(M._active) do
coroutine.resume(async._co, "abort")
end
end
function M.yield()
if coroutine.yield() == "abort" then
error("aborted", 2)
end
end
function M.step()
local start = vim.uv.hrtime()
for _ = 1, #M._active do
if Util.exiting() or vim.uv.hrtime() - start > M.BUDGET * 1e6 then
break
end
local state = table.remove(M._active, 1)
if state:step() then
if state._suspended then
table.insert(M._suspended, state)
else
table.insert(M._active, state)
end
end
end
for _ = 1, #M._suspended do
local state = table.remove(M._suspended, 1)
table.insert(state._suspended and M._suspended or M._active, state)
end
-- M.debug()
if #M._active == 0 or Util.exiting() then
return M._executor:stop()
end
end
function M.debug()
local lines = {
"- active: " .. #M._active,
"- suspended: " .. #M._suspended,
}
for _, async in ipairs(M._active) do
local info = debug.getinfo(async._fn)
local file = vim.fn.fnamemodify(info.short_src:sub(1), ":~:.")
table.insert(lines, ("%s:%d"):format(file, info.linedefined))
if #lines > 10 then
break
end
end
local msg = table.concat(lines, "\n")
M._notif = vim.notify(msg, nil, { replace = M._notif })
end
---@param async Async
function M.add(async)
table.insert(M._active, async)
M._run()
return async
end
function M._run()
if not Util.exiting() and not M._executor:is_active() then
M._executor:start(vim.schedule_wrap(M.step))
end
end
function M.running()
local co = coroutine.running()
if co then
return M._threads[co]
end
end
---@async
---@param ms number
function M.sleep(ms)
local async = M.running()
assert(async, "Not in an async context")
async:sleep(ms)
end
M.Async = Async
M.new = Async.new
return M

View file

@ -1,100 +0,0 @@
vim.opt.rtp:append(".")
local Rocks = require("lazy.pkg.rockspec")
local Semver = require("lazy.manage.semver")
local Util = require("lazy.util")
local M = {}
M.patterns = { "nvim", "treesitter", "tree-sitter", "cmp", "neo" }
function M.fetch(url, file, prefix)
if not vim.uv.fs_stat(file) then
print((prefix or "") .. "Fetching " .. url .. " to " .. file .. "\n")
vim.cmd.redraw()
local out = vim.fn.system("wget " .. url .. " -O " .. file)
if vim.v.shell_error ~= 0 then
pcall(vim.uv.fs_unlink, file)
error("Failed to fetch " .. url .. ":\n" .. out)
end
end
end
---@return RockManifest?
function M.fetch_manifest()
local manifest_file = "build/manifest.lua"
M.fetch("https://luarocks.org/manifest-5.1", manifest_file)
return Rocks.parse(manifest_file)
end
function M.fetch_rockspec(name, version, prefix)
version = version or "scm-1"
local url = "https://luarocks.org/" .. name .. "-" .. version .. ".rockspec"
M.fetch(url, "build/" .. name .. ".rockspec", prefix)
end
function M.build()
vim.fn.mkdir("build", "p")
local manifest = M.fetch_manifest() or {}
---@type {name:string, version:string, url:string}[]
local nvim_rocks = {}
for rock, vv in pairs(manifest.repository or {}) do
local matches = false
for _, pattern in ipairs(M.patterns) do
if rock:find(pattern, 1, true) then
matches = true
break
end
end
if matches then
local versions = vim.tbl_map(Semver.version, vim.tbl_keys(vv))
versions = vim.tbl_filter(function(v)
return not not v
end, versions)
local last = Semver.last(versions) or next(vv)
last = type(last) == "table" and last.input or last
table.insert(nvim_rocks, { name = rock, version = last })
end
end
table.sort(nvim_rocks, function(a, b)
return a.name < b.name
end)
for r, rock in ipairs(nvim_rocks) do
local progress = string.format("[%d/%d] ", r, #nvim_rocks)
local ok, err = pcall(M.fetch_rockspec, rock.name, rock.version, progress)
if not ok then
err = vim.trim("Error: " .. err)
local lines = vim.split(err, "\n")
lines = vim.tbl_map(function(line)
return " " .. line
end, lines)
print(table.concat(lines, "\n") .. "\n")
end
end
for _, rock in ipairs(nvim_rocks) do
local rockspec = Rocks.rockspec("build/" .. rock.name .. ".rockspec")
if rockspec then
local url = rockspec.source and rockspec.source.url
-- parse github short url
if url and url:find("://github.com/") then
url = url:gsub("^.*://github.com/", "")
local parts = vim.split(url, "/")
url = parts[1] .. "/" .. parts[2]
url = url:gsub("%.git$", "")
end
if url then
rock.url = url
print(rock.name .. " " .. url)
else
print("Error: " .. rock.name .. " missing source url\n\n")
print(vim.inspect(rockspec) .. "\n")
end
end
end
Util.write_file("lua/lazy/community/_generated.lua", "return \n" .. vim.inspect(nvim_rocks))
end
M.build()
return M

File diff suppressed because it is too large Load diff

View file

@ -1,28 +0,0 @@
local M = {}
---@type table<string, string>
local mapping = nil
local function load()
if not mapping then
mapping = {}
---@type {name:string, url:string, version:string}[]
local gen = require("lazy.community._generated")
for _, rock in ipairs(gen) do
mapping[rock.name] = rock.url
end
end
return mapping
end
---@param rock string
---@return string?
function M.get_url(rock)
return load()[rock]
end
function M.get_spec(name)
return require("lazy.community.specs")[name]
end
return M

View file

@ -1,7 +0,0 @@
---@type table<string, LazySpec>
return {
["plenary.nvim"] = {
"nvim-lua/plenary.nvim",
lazy = true,
},
}

View file

@ -1,528 +0,0 @@
local uv = vim.uv or vim.loop
local M = {}
---@alias CacheHash {mtime: {sec:number, nsec:number}, size:number}
---@alias CacheEntry {hash:CacheHash, chunk:string}
---@class ModuleFindOpts
---@field all? boolean Search for all matches (defaults to `false`)
---@field rtp? boolean Search for modname in the runtime path (defaults to `true`)
---@field patterns? string[] Patterns to use (defaults to `{"/init.lua", ".lua"}`)
---@field paths? string[] Extra paths to search for modname
---@class ModuleInfo
---@field modpath string Path of the module
---@field modname string Name of the module
---@field stat? uv_fs_t File stat of the module path
---@alias LoaderStats table<string, {total:number, time:number, [string]:number?}?>
M.path = vim.fn.stdpath("cache") .. "/luac"
M.enabled = false
---@class Loader
---@field _rtp string[]
---@field _rtp_pure string[]
---@field _rtp_key string
local Loader = {
VERSION = 3,
---@type table<string, table<string,ModuleInfo>>
_indexed = {},
---@type table<string, string[]>
_topmods = {},
_loadfile = loadfile,
---@type LoaderStats
_stats = {
find = { total = 0, time = 0, not_found = 0 },
},
}
--- Tracks the time spent in a function
---@private
function Loader.track(stat, start)
Loader._stats[stat] = Loader._stats[stat] or { total = 0, time = 0 }
Loader._stats[stat].total = Loader._stats[stat].total + 1
Loader._stats[stat].time = Loader._stats[stat].time + uv.hrtime() - start
end
--- slightly faster/different version than vim.fs.normalize
--- we also need to have it here, since the loader will load vim.fs
---@private
function Loader.normalize(path)
if path:sub(1, 1) == "~" then
local home = uv.os_homedir() or "~"
if home:sub(-1) == "\\" or home:sub(-1) == "/" then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end
path = path:gsub("\\", "/"):gsub("/+", "/")
return path:sub(-1) == "/" and path:sub(1, -2) or path
end
--- Gets the rtp excluding after directories.
--- The result is cached, and will be updated if the runtime path changes.
--- When called from a fast event, the cached value will be returned.
--- @return string[] rtp, boolean updated
---@private
function Loader.get_rtp()
local start = uv.hrtime()
if vim.in_fast_event() then
Loader.track("get_rtp", start)
return (Loader._rtp or {}), false
end
local updated = false
local key = vim.go.rtp
if key ~= Loader._rtp_key then
Loader._rtp = {}
for _, path in ipairs(vim.api.nvim_get_runtime_file("", true)) do
path = Loader.normalize(path)
-- skip after directories
if path:sub(-6, -1) ~= "/after" and not (Loader._indexed[path] and vim.tbl_isempty(Loader._indexed[path])) then
Loader._rtp[#Loader._rtp + 1] = path
end
end
updated = true
Loader._rtp_key = key
end
Loader.track("get_rtp", start)
return Loader._rtp, updated
end
--- Returns the cache file name
---@param name string can be a module name, or a file name
---@return string file_name
---@private
function Loader.cache_file(name)
local ret = M.path .. "/" .. name:gsub("[/\\:]", "%%")
return ret:sub(-4) == ".lua" and (ret .. "c") or (ret .. ".luac")
end
--- Saves the cache entry for a given module or file
---@param name string module name or filename
---@param entry CacheEntry
---@private
function Loader.write(name, entry)
local cname = Loader.cache_file(name)
local f = assert(uv.fs_open(cname, "w", 438))
local header = {
Loader.VERSION,
entry.hash.size,
entry.hash.mtime.sec,
entry.hash.mtime.nsec,
}
uv.fs_write(f, table.concat(header, ",") .. "\0")
uv.fs_write(f, entry.chunk)
uv.fs_close(f)
end
--- Loads the cache entry for a given module or file
---@param name string module name or filename
---@return CacheEntry?
---@private
function Loader.read(name)
local start = uv.hrtime()
local cname = Loader.cache_file(name)
local f = uv.fs_open(cname, "r", 438)
if f then
local hash = uv.fs_fstat(f) --[[@as CacheHash]]
local data = uv.fs_read(f, hash.size, 0) --[[@as string]]
uv.fs_close(f)
local zero = data:find("\0", 1, true)
if not zero then
return
end
---@type integer[]|{[0]:integer}
local header = vim.split(data:sub(1, zero - 1), ",")
if tonumber(header[1]) ~= Loader.VERSION then
return
end
Loader.track("read", start)
return {
hash = { size = tonumber(header[2]), mtime = { sec = tonumber(header[3]), nsec = tonumber(header[4]) } },
chunk = data:sub(zero + 1),
}
end
Loader.track("read", start)
end
--- The `package.loaders` loader for lua files using the cache.
---@param modname string module name
---@return string|function
---@private
function Loader.loader(modname)
local start = uv.hrtime()
local ret = M.find(modname)[1]
if ret then
local chunk, err = Loader.load(ret.modpath, { hash = ret.stat })
Loader.track("loader", start)
return chunk or error(err)
end
Loader.track("loader", start)
return "\ncache_loader: module " .. modname .. " not found"
end
--- The `package.loaders` loader for libs
---@param modname string module name
---@return string|function
---@private
function Loader.loader_lib(modname)
local start = uv.hrtime()
local sysname = uv.os_uname().sysname:lower() or ""
local is_win = sysname:find("win", 1, true) and not sysname:find("darwin", 1, true)
local ret = M.find(modname, { patterns = is_win and { ".dll" } or { ".so" } })[1]
---@type function?, string?
if ret then
-- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
-- a) strip prefix up to and including the first dash, if any
-- b) replace all dots by underscores
-- c) prepend "luaopen_"
-- So "foo-bar.baz" should result in "luaopen_bar_baz"
local dash = modname:find("-", 1, true)
local funcname = dash and modname:sub(dash + 1) or modname
local chunk, err = package.loadlib(ret.modpath, "luaopen_" .. funcname:gsub("%.", "_"))
Loader.track("loader_lib", start)
return chunk or error(err)
end
Loader.track("loader_lib", start)
return "\ncache_loader_lib: module " .. modname .. " not found"
end
--- `loadfile` using the cache
---@param filename? string
---@param mode? "b"|"t"|"bt"
---@param env? table
---@param hash? CacheHash
---@return function?, string? error_message
---@private
-- luacheck: ignore 312
function Loader.loadfile(filename, mode, env, hash)
local start = uv.hrtime()
filename = Loader.normalize(filename)
mode = nil -- ignore mode, since we byte-compile the lua source files
local chunk, err = Loader.load(filename, { mode = mode, env = env, hash = hash })
Loader.track("loadfile", start)
return chunk, err
end
--- Checks whether two cache hashes are the same based on:
--- * file size
--- * mtime in seconds
--- * mtime in nanoseconds
---@param h1 CacheHash
---@param h2 CacheHash
---@private
function Loader.eq(h1, h2)
return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec
end
--- Loads the given module path using the cache
---@param modpath string
---@param opts? {hash?: CacheHash, mode?: "b"|"t"|"bt", env?:table} (table|nil) Options for loading the module:
--- - hash: (table) the hash of the file to load if it is already known. (defaults to `vim.uv.fs_stat({modpath})`)
--- - mode: (string) the mode to load the module with. "b"|"t"|"bt" (defaults to `nil`)
--- - env: (table) the environment to load the module in. (defaults to `nil`)
---@see |luaL_loadfile()|
---@return function?, string? error_message
---@private
function Loader.load(modpath, opts)
local start = uv.hrtime()
opts = opts or {}
local hash = opts.hash or uv.fs_stat(modpath)
---@type function?, string?
local chunk, err
if not hash then
-- trigger correct error
chunk, err = Loader._loadfile(modpath, opts.mode, opts.env)
Loader.track("load", start)
return chunk, err
end
local entry = Loader.read(modpath)
if entry and Loader.eq(entry.hash, hash) then
-- found in cache and up to date
-- selene: allow(incorrect_standard_library_use)
chunk, err = load(entry.chunk --[[@as string]], "@" .. modpath, opts.mode, opts.env)
if not (err and err:find("cannot load incompatible bytecode", 1, true)) then
Loader.track("load", start)
return chunk, err
end
end
entry = { hash = hash, modpath = modpath }
chunk, err = Loader._loadfile(modpath, opts.mode, opts.env)
if chunk then
entry.chunk = string.dump(chunk)
Loader.write(modpath, entry)
end
Loader.track("load", start)
return chunk, err
end
--- Finds lua modules for the given module name.
---@param modname string Module name, or `"*"` to find the top-level modules instead
---@param opts? ModuleFindOpts (table|nil) Options for finding a module:
--- - rtp: (boolean) Search for modname in the runtime path (defaults to `true`)
--- - paths: (string[]) Extra paths to search for modname (defaults to `{}`)
--- - patterns: (string[]) List of patterns to use when searching for modules.
--- A pattern is a string added to the basename of the Lua module being searched.
--- (defaults to `{"/init.lua", ".lua"}`)
--- - all: (boolean) Return all matches instead of just the first one (defaults to `false`)
---@return ModuleInfo[] (list) A list of results with the following properties:
--- - modpath: (string) the path to the module
--- - modname: (string) the name of the module
--- - stat: (table|nil) the fs_stat of the module path. Won't be returned for `modname="*"`
function M.find(modname, opts)
local start = uv.hrtime()
opts = opts or {}
modname = modname:gsub("/", ".")
local basename = modname:gsub("%.", "/")
local idx = modname:find(".", 1, true)
-- HACK: fix incorrect require statements. Really not a fan of keeping this,
-- but apparently the regular lua loader also allows this
if idx == 1 then
modname = modname:gsub("^%.+", "")
basename = modname:gsub("%.", "/")
idx = modname:find(".", 1, true)
end
-- get the top-level module name
local topmod = idx and modname:sub(1, idx - 1) or modname
-- OPTIM: search for a directory first when topmod == modname
local patterns = opts.patterns or (topmod == modname and { "/init.lua", ".lua" } or { ".lua", "/init.lua" })
for p, pattern in ipairs(patterns) do
patterns[p] = "/lua/" .. basename .. pattern
end
---@type ModuleInfo[]
local results = {}
-- Only continue if we haven't found anything yet or we want to find all
---@private
local function continue()
return #results == 0 or opts.all
end
-- Checks if the given paths contain the top-level module.
-- If so, it tries to find the module path for the given module name.
---@param paths string[]
---@private
local function _find(paths)
for _, path in ipairs(paths) do
if topmod == "*" then
for _, r in pairs(Loader.lsmod(path)) do
results[#results + 1] = r
if not continue() then
return
end
end
elseif Loader.lsmod(path)[topmod] then
for _, pattern in ipairs(patterns) do
local modpath = path .. pattern
Loader._stats.find.stat = (Loader._stats.find.stat or 0) + 1
local hash = uv.fs_stat(modpath)
if hash then
results[#results + 1] = { modpath = modpath, stat = hash, modname = modname }
if not continue() then
return
end
end
end
end
end
end
-- always check the rtp first
if opts.rtp ~= false then
_find(Loader._rtp or {})
if continue() then
local rtp, updated = Loader.get_rtp()
if updated then
_find(rtp)
end
end
end
-- check any additional paths
if continue() and opts.paths then
_find(opts.paths)
end
Loader.track("find", start)
if #results == 0 then
-- module not found
Loader._stats.find.not_found = Loader._stats.find.not_found + 1
end
return results
end
--- Resets the topmods cache for the path, or all the paths
--- if path is nil.
---@param path string? path to reset
function M.reset(path)
if path then
Loader._indexed[Loader.normalize(path)] = nil
else
Loader._indexed = {}
end
end
--- Enables the experimental Lua module loader:
--- * overrides loadfile
--- * adds the lua loader using the byte-compilation cache
--- * adds the libs loader
--- * removes the default Neovim loader
function M.enable()
if M.enabled then
return
end
M.enabled = true
vim.fn.mkdir(vim.fn.fnamemodify(M.path, ":p"), "p")
-- selene: allow(global_usage)
_G.loadfile = Loader.loadfile
-- add lua loader
table.insert(package.loaders, 2, Loader.loader)
-- add libs loader
table.insert(package.loaders, 3, Loader.loader_lib)
-- remove Neovim loader
for l, loader in ipairs(package.loaders) do
if loader == vim._load_package then
table.remove(package.loaders, l)
break
end
end
-- this will reset the top-mods in case someone adds a new
-- top-level lua module to a path already on the rtp
vim.api.nvim_create_autocmd("BufWritePost", {
group = vim.api.nvim_create_augroup("cache_topmods_reset", { clear = true }),
callback = function(event)
local bufname = event.match ---@type string
local idx = bufname:find("/lua/", 1, true)
if idx then
M.reset(bufname:sub(1, idx - 1))
end
end,
})
end
--- Disables the experimental Lua module loader:
--- * removes the loaders
--- * adds the default Neovim loader
function M.disable()
if not M.enabled then
return
end
M.enabled = false
-- selene: allow(global_usage)
_G.loadfile = Loader._loadfile
---@diagnostic disable-next-line: no-unknown
for l, loader in ipairs(package.loaders) do
if loader == Loader.loader or loader == Loader.loader_lib then
table.remove(package.loaders, l)
end
end
table.insert(package.loaders, 2, vim._load_package)
vim.api.nvim_del_augroup_by_name("cache_topmods_reset")
end
--- Return the top-level `/lua/*` modules for this path
---@param path string path to check for top-level lua modules
---@private
function Loader.lsmod(path)
if not Loader._indexed[path] then
local start = uv.hrtime()
Loader._indexed[path] = {}
local handle = uv.fs_scandir(path .. "/lua")
while handle do
local name, t = uv.fs_scandir_next(handle)
if not name then
break
end
local modpath = path .. "/lua/" .. name
-- HACK: type is not always returned due to a bug in luv
t = t or uv.fs_stat(modpath).type
---@type string
local topname
local ext = name:sub(-4)
if ext == ".lua" or ext == ".dll" then
topname = name:sub(1, -5)
elseif name:sub(-3) == ".so" then
topname = name:sub(1, -4)
elseif t == "link" or t == "directory" then
topname = name
end
if topname then
Loader._indexed[path][topname] = { modpath = modpath, modname = topname }
Loader._topmods[topname] = Loader._topmods[topname] or {}
if not vim.tbl_contains(Loader._topmods[topname], path) then
table.insert(Loader._topmods[topname], path)
end
end
end
Loader.track("lsmod", start)
end
return Loader._indexed[path]
end
--- Debug function that wraps all loaders and tracks stats
---@private
function M._profile_loaders()
for l, loader in pairs(package.loaders) do
local loc = debug.getinfo(loader, "Sn").source:sub(2)
package.loaders[l] = function(modname)
local start = uv.hrtime()
local ret = loader(modname)
Loader.track("loader " .. l .. ": " .. loc, start)
Loader.track("loader_all", start)
return ret
end
end
end
--- Prints all cache stats
---@param opts? {print?:boolean}
---@return LoaderStats
---@private
function M._inspect(opts)
if opts and opts.print then
---@private
local function ms(nsec)
return math.floor(nsec / 1e6 * 1000 + 0.5) / 1000 .. "ms"
end
local chunks = {} ---@type string[][]
---@type string[]
local stats = vim.tbl_keys(Loader._stats)
table.sort(stats)
for _, stat in ipairs(stats) do
vim.list_extend(chunks, {
{ "\n" .. stat .. "\n", "Title" },
{ "* total: " },
{ tostring(Loader._stats[stat].total) .. "\n", "Number" },
{ "* time: " },
{ ms(Loader._stats[stat].time) .. "\n", "Bold" },
{ "* avg time: " },
{ ms(Loader._stats[stat].time / Loader._stats[stat].total) .. "\n", "Bold" },
})
for k, v in pairs(Loader._stats[stat]) do
if not vim.tbl_contains({ "time", "total" }, k) then
chunks[#chunks + 1] = { "* " .. k .. ":" .. string.rep(" ", 9 - #k) }
chunks[#chunks + 1] = { tostring(v) .. "\n", "Number" }
end
end
end
vim.api.nvim_echo(chunks, true, {})
end
return Loader._stats
end
return M

View file

@ -1,253 +1,59 @@
local Util = require("lazy.core.util") local Util = require("lazy.core.util")
---@class LazyCoreConfig
local M = {} local M = {}
---@class LazyConfig ---@class LazyConfig
M.defaults = { M.defaults = {
root = vim.fn.stdpath("data") .. "/lazy", -- directory where plugins will be installed plugins = "config.plugins",
defaults = { defaults = {
-- Set this to `true` to have all your plugins lazy-loaded by default. opt = false, -- should plugins default to "opt" or "start"
-- Only do this if you know what you are doing, as it can lead to unexpected behavior. version = nil,
lazy = false, -- should plugins be lazy-loaded? -- version = "*", -- enable this to try installing the latest stable versions of plugins
-- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
-- have outdated releases, which may break your Neovim install.
version = nil, -- always use the latest git commit
-- version = "*", -- try installing the latest stable version for plugins that support semver
-- default `cond` you can use to globally disable a lot of plugins
-- when running inside vscode for example
cond = nil, ---@type boolean|fun(self:LazyPlugin):boolean|nil
}, },
-- leave nil when passing the spec as the first argument to setup() packpath = vim.fn.stdpath("data") .. "/site/pack/lazy", -- package path where new plugins will be installed
spec = nil, ---@type LazySpec
local_spec = true, -- load project specific .lazy.lua spec files. They will be added at the end of the spec.
lockfile = vim.fn.stdpath("config") .. "/lazy-lock.json", -- lockfile generated after running update. lockfile = vim.fn.stdpath("config") .. "/lazy-lock.json", -- lockfile generated after running update.
---@type number? limit the maximum amount of concurrent tasks install_missing = true, -- install missing plugins on startup. This doesn't increase startup time.
concurrency = jit.os:find("Windows") and (vim.uv.available_parallelism() * 2) or nil, concurrency = nil, -- set to a number to limit the maximum amount of concurrent tasks
git = { git = {
-- defaults for the `Lazy log` command -- defaults for `Lazy log`
-- log = { "--since=3 days ago" }, -- show commits from the last 3 days -- log = { "-10" }, -- last 10 commits
log = { "-8" }, -- show the last 8 commits log = { "--since=1 days ago" }, -- commits from the last 3 days
timeout = 120, -- kill processes that take more than 2 minutes timeout = 120, -- processes taking over 2 minutes will be killed
url_format = "https://github.com/%s.git",
-- lazy.nvim requires git >=2.19.0. If you really want to use lazy with an older version,
-- then set the below to false. This should work, but is NOT supported and will
-- increase downloads a lot.
filter = true,
-- rate of network related git operations (clone, fetch, checkout)
throttle = {
enabled = false, -- not enabled by default
-- max 2 ops every 5 seconds
rate = 2,
duration = 5 * 1000, -- in ms
},
-- Time in seconds to wait before running fetch again for a plugin.
-- Repeated update/check operations will not run again until this
-- cooldown period has passed.
cooldown = 0,
},
pkg = {
enabled = true,
cache = vim.fn.stdpath("state") .. "/lazy/pkg-cache.lua",
-- the first package source that is found for a plugin will be used.
sources = {
"lazy",
"rockspec", -- will only be used when rocks.enabled is true
"packspec",
},
},
rocks = {
enabled = true,
root = vim.fn.stdpath("data") .. "/lazy-rocks",
server = "https://nvim-neorocks.github.io/rocks-binaries/",
-- use hererocks to install luarocks?
-- set to `nil` to use hererocks when luarocks is not found
-- set to `true` to always use hererocks
-- set to `false` to always use luarocks
hererocks = nil,
}, },
-- Any plugin spec that contains one of the patterns will use your
-- local repo in the projects folder instead of fetchig it from github
-- Mostly useful for plugin developers.
dev = { dev = {
-- Directory where you store your local plugin projects. If a function is used, path = vim.fn.expand("~/projects"), -- the path where you store your projects
-- the plugin directory (e.g. `~/projects/plugin-name`) must be returned. ---@type string[]
---@type string | fun(plugin: LazyPlugin): string
path = "~/projects",
---@type string[] plugins that match these patterns will use your local versions instead of being fetched from GitHub
patterns = {}, -- For example {"folke"} patterns = {}, -- For example {"folke"}
fallback = false, -- Fallback to git when local plugin doesn't exist
},
install = {
-- install missing plugins on startup. This doesn't increase startup time.
missing = true,
-- try to load one of these colorschemes when starting an installation during startup
colorscheme = { "habamax" },
}, },
ui = { ui = {
-- a number <1 is a percentage., >1 is a fixed size
size = { width = 0.8, height = 0.8 },
wrap = true, -- wrap the lines in the ui
-- The border to use for the UI window. Accepts same border values as |nvim_open_win()|. -- The border to use for the UI window. Accepts same border values as |nvim_open_win()|.
border = "none", border = "none",
-- The backdrop opacity. 0 is fully opaque, 100 is fully transparent.
backdrop = 60,
title = nil, ---@type string only works when border is not "none"
title_pos = "center", ---@type "center" | "left" | "right"
-- Show pills on top of the Lazy window
pills = true, ---@type boolean
icons = { icons = {
cmd = "", start = "",
config = "",
debug = "",
event = "",
favorite = "",
ft = "",
init = "",
import = "",
keys = "",
lazy = "󰒲 ",
loaded = "",
not_loaded = "",
plugin = "", plugin = "",
runtime = "",
require = "󰢱 ",
source = "", source = "",
start = "", config = "",
event = "",
keys = "",
cmd = "",
ft = "",
task = "", task = "",
list = {
"",
"",
"",
"",
},
},
-- leave nil, to automatically select a browser depending on your OS.
-- If you want to use a specific browser, you can define it here
browser = nil, ---@type string?
throttle = 1000 / 30, -- how frequently should the ui process render events
custom_keys = {
-- You can define custom key maps here. If present, the description will
-- be shown in the help menu.
-- To disable one of the defaults, set it to false.
["<localleader>l"] = {
function(plugin)
require("lazy.util").float_term({ "lazygit", "log" }, {
cwd = plugin.dir,
})
end,
desc = "Open lazygit log",
},
["<localleader>i"] = {
function(plugin)
Util.notify(vim.inspect(plugin), {
title = "Inspect " .. plugin.name,
lang = "lua",
})
end,
desc = "Inspect Plugin",
},
["<localleader>t"] = {
function(plugin)
require("lazy.util").float_term(nil, {
cwd = plugin.dir,
})
end,
desc = "Open terminal in plugin dir",
},
}, },
throttle = 20, -- how frequently should the ui process render events
}, },
-- Output options for headless mode
headless = {
-- show the output from process commands like git
process = true,
-- show log messages
log = true,
-- show task start/end
task = true,
-- use ansi colors
colors = true,
},
diff = {
-- diff command <d> can be one of:
-- * browser: opens the github compare view. Note that this is always mapped to <K> as well,
-- so you can have a different command for diff <d>
-- * git: will run git diff and open a buffer with filetype git
-- * terminal_git: will open a pseudo terminal with git diff
-- * diffview.nvim: will open Diffview to show the diff
cmd = "git",
},
checker = {
-- automatically check for plugin updates
enabled = false,
concurrency = nil, ---@type number? set to 1 to check for updates very slowly
notify = true, -- get a notification when new updates are found
frequency = 3600, -- check for updates every hour
check_pinned = false, -- check for pinned packages that can't be updated
},
change_detection = {
-- automatically check for config file changes and reload the ui
enabled = true,
notify = true, -- get a notification when changes are found
},
performance = {
cache = {
enabled = true,
},
reset_packpath = true, -- reset the package path to improve startup time
rtp = {
reset = true, -- reset the runtime path to $VIMRUNTIME and your config directory
---@type string[]
paths = {}, -- add any custom paths here that you want to includes in the rtp
---@type string[] list any plugins you want to disable here
disabled_plugins = {
-- "gzip",
-- "matchit",
-- "matchparen",
-- "netrwPlugin",
-- "tarPlugin",
-- "tohtml",
-- "tutor",
-- "zipPlugin",
},
},
},
-- lazy can generate helptags from the headings in markdown readme files,
-- so :help works even for plugins that don't have vim docs.
-- when the readme opens with :help it will be correctly displayed as markdown
readme = {
enabled = true,
root = vim.fn.stdpath("state") .. "/lazy/readme",
files = { "README.md", "lua/**/README.md" },
-- only generate markdown helptags for plugins that don't have docs
skip_if_doc_exists = true,
},
state = vim.fn.stdpath("state") .. "/lazy/state.json", -- state info for checker and other things
-- Enable profiling of lazy.nvim. This will add some overhead,
-- so only enable this when you are debugging lazy.nvim
profiling = {
-- Enables extra stats on the debug tab related to the loader cache.
-- Additionally gathers stats about all package.loaders
loader = false,
-- Track each new require in the Lazy profiling tab
require = false,
},
debug = false,
} }
function M.hererocks()
if M.options.rocks.hererocks == nil then
M.options.rocks.hererocks = vim.fn.executable("luarocks") == 0
end
return M.options.rocks.hererocks
end
M.version = "11.17.1" -- x-release-please-version
M.ns = vim.api.nvim_create_namespace("lazy") M.ns = vim.api.nvim_create_namespace("lazy")
---@type LazySpecLoader M.paths = {
M.spec = nil ---@type string
main = nil,
---@type string
plugins = nil,
}
---@type table<string, LazyPlugin> ---@type table<string, LazyPlugin>
M.plugins = {} M.plugins = {}
@ -258,114 +64,21 @@ M.to_clean = {}
---@type LazyConfig ---@type LazyConfig
M.options = {} M.options = {}
---@type string
M.me = nil
---@type string
M.mapleader = nil
---@type string
M.maplocalleader = nil
M.suspended = false
function M.headless()
return not M.suspended and #vim.api.nvim_list_uis() == 0
end
---@param opts? LazyConfig ---@param opts? LazyConfig
function M.setup(opts) function M.setup(opts)
M.options = vim.tbl_deep_extend("force", M.defaults, opts or {}) M.options = vim.tbl_deep_extend("force", M.defaults, opts or {})
M.paths.plugins = vim.fn.stdpath("config") .. "/lua/" .. M.options.plugins:gsub("%.", "/")
M.paths.main = M.paths.plugins .. (vim.loop.fs_stat(M.paths.plugins .. ".lua") and ".lua" or "/init.lua")
if type(M.options.spec) == "string" then vim.api.nvim_create_autocmd("User", {
M.options.spec = { import = M.options.spec } pattern = "VeryLazy",
end
table.insert(M.options.install.colorscheme, "habamax")
-- root
M.options.root = Util.norm(M.options.root)
if type(M.options.dev.path) == "string" then
M.options.dev.path = Util.norm(M.options.dev.path)
end
M.options.lockfile = Util.norm(M.options.lockfile)
M.options.readme.root = Util.norm(M.options.readme.root)
vim.fn.mkdir(M.options.root, "p")
if M.options.performance.reset_packpath then
vim.go.packpath = vim.env.VIMRUNTIME
end
M.me = debug.getinfo(1, "S").source:sub(2)
M.me = Util.norm(vim.fn.fnamemodify(M.me, ":p:h:h:h:h"))
local lib = vim.fn.fnamemodify(vim.v.progpath, ":p:h:h") .. "/lib"
lib = vim.uv.fs_stat(lib .. "64") and (lib .. "64") or lib
lib = lib .. "/nvim"
if M.options.performance.rtp.reset then
---@type vim.Option
vim.opt.rtp = {
vim.fn.stdpath("config"),
vim.fn.stdpath("data") .. "/site",
M.me,
vim.env.VIMRUNTIME,
lib,
vim.fn.stdpath("config") .. "/after",
}
end
for _, path in ipairs(M.options.performance.rtp.paths) do
vim.opt.rtp:append(path)
end
vim.opt.rtp:append(M.options.readme.root)
-- disable plugin loading since we do all of that ourselves
vim.go.loadplugins = false
M.mapleader = vim.g.mapleader
M.maplocalleader = vim.g.maplocalleader
vim.api.nvim_create_autocmd("UIEnter", {
once = true, once = true,
callback = function() callback = function()
require("lazy.stats").on_ui_enter() require("lazy.core.module").autosave()
require("lazy.view").setup()
end, end,
}) })
if M.headless() then
require("lazy.view.commands").setup()
else
vim.api.nvim_create_autocmd("User", {
pattern = "VeryLazy",
once = true,
callback = function()
require("lazy.view.commands").setup()
if M.options.change_detection.enabled then
require("lazy.manage.reloader").enable()
end
if M.options.checker.enabled then
vim.defer_fn(function()
require("lazy.manage.checker").start()
end, 10)
end
-- useful for plugin developers when making changes to a packspec file
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = { "lazy.lua", "pkg.json", "*.rockspec" },
callback = function()
local plugin = require("lazy.core.plugin").find(vim.uv.cwd() .. "/lua/")
if plugin then
require("lazy").pkg({ plugins = { plugin } })
end
end,
})
vim.api.nvim_create_autocmd({ "VimSuspend", "VimResume" }, {
callback = function(ev)
M.suspended = ev.event == "VimSuspend"
end,
})
end,
})
end
Util.very_lazy() Util.very_lazy()
end end

View file

@ -1,174 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.core.util")
--- This class is used to manage the fragments of a plugin spec.
--- It keeps track of the fragments and their relations to other fragments.
--- A fragment can be a dependency (dependencies) or a child (specs) of another fragment.
---@class LazyFragments
---@field fragments table<number, LazyFragment>
---@field frag_stack number[]
---@field dep_stack number[]
---@field dirty table<number, boolean>
---@field plugins table<LazyPlugin, number>
---@field spec LazySpecLoader
local M = {}
M._fid = 0
local function next_id()
M._fid = M._fid + 1
return M._fid
end
---@param spec LazySpecLoader
---@return LazyFragments
function M.new(spec)
local self = setmetatable({}, { __index = M })
self.fragments = {}
self.frag_stack = {}
self.dep_stack = {}
self.spec = spec
self.dirty = {}
self.plugins = {}
return self
end
---@param id number
function M:get(id)
return self.fragments[id]
end
--- Remove a fragment and all its children.
--- This will also remove the fragment from its parent's children list.
---@param id number
function M:del(id)
-- del fragment
local fragment = self.fragments[id]
if not fragment then
return
end
self.dirty[id] = true
-- remove from parent
local pid = fragment.pid
if pid then
local parent = self.fragments[pid]
if parent.frags then
---@param fid number
parent.frags = Util.filter(function(fid)
return fid ~= id
end, parent.frags)
end
if parent.deps then
---@param fid number
parent.deps = Util.filter(function(fid)
return fid ~= id
end, parent.deps)
end
self.dirty[pid] = true
end
-- remove children
if fragment.frags then
for _, fid in ipairs(fragment.frags) do
self:del(fid)
end
end
self.fragments[id] = nil
end
--- Add a fragment to the fragments list.
--- This also resolves its name, url, dir, dependencies and child specs.
---@param plugin LazyPluginSpec
function M:add(plugin)
if self.plugins[plugin] then
return self.fragments[self.plugins[plugin]]
end
local id = next_id()
setmetatable(plugin, nil)
self.plugins[plugin] = id
local pid = self.frag_stack[#self.frag_stack]
---@type LazyFragment
local fragment = {
id = id,
pid = pid,
name = plugin.name,
url = plugin.url,
dir = plugin.dir,
spec = plugin --[[@as LazyPlugin]],
}
-- short url / ref
if plugin[1] then
local slash = plugin[1]:find("/", 1, true)
if slash then
local prefix = plugin[1]:sub(1, 4)
if prefix == "http" or prefix == "git@" then
fragment.url = fragment.url or plugin[1]
else
fragment.name = fragment.name or plugin[1]:sub(slash + 1)
fragment.url = fragment.url or Config.options.git.url_format:format(plugin[1])
end
else
fragment.name = fragment.name or plugin[1]
end
end
-- name
fragment.name = fragment.name
or fragment.url and self.spec.get_name(fragment.url)
or fragment.dir and self.spec.get_name(fragment.dir)
if not fragment.name or fragment.name == "" then
return self.spec:error("Invalid plugin spec " .. vim.inspect(plugin))
end
if type(plugin.config) == "table" then
self.spec:warn(
"{" .. fragment.name .. "}: setting a table to `Plugin.config` is deprecated. Please use `Plugin.opts` instead"
)
---@diagnostic disable-next-line: assign-type-mismatch
plugin.opts = plugin.config
plugin.config = nil
end
self.fragments[id] = fragment
-- add to parent
if pid then
local parent = self.fragments[pid]
parent.frags = parent.frags or {}
table.insert(parent.frags, id)
end
-- add to parent's deps
local did = self.dep_stack[#self.dep_stack]
if did and did == pid then
fragment.dep = true
local parent = self.fragments[did]
parent.deps = parent.deps or {}
table.insert(parent.deps, id)
end
table.insert(self.frag_stack, id)
-- dependencies
if plugin.dependencies then
table.insert(self.dep_stack, id)
self.spec:normalize(plugin.dependencies)
table.remove(self.dep_stack)
end
-- child specs
if plugin.specs then
self.spec:normalize(plugin.specs)
end
table.remove(self.frag_stack)
return fragment
end
return M

150
lua/lazy/core/handler.lua Normal file
View file

@ -0,0 +1,150 @@
local Util = require("lazy.core.util")
local Loader = require("lazy.core.loader")
local Config = require("lazy.core.config")
---@class LazyPluginHandlers
---@field event? string|string[]
---@field cmd? string|string[]
---@field ft? string|string[]
---@field module? string|string[]
---@field keys? string|string[]
local M = {}
---@alias LazyHandler fun(grouped:table<string, string[]>)
function M.setup()
for key, handler in pairs(M.handlers) do
---@type table<string, string[]>
local group = {}
for _, plugin in pairs(Config.plugins) do
if plugin[key] then
---@diagnostic disable-next-line: no-unknown
for _, value in pairs(type(plugin[key]) == "table" and plugin[key] or { plugin[key] }) do
group[value] = group[value] or {}
table.insert(group[value], plugin.name)
end
end
end
handler(group)
end
end
---@type table<string, LazyHandler>
M.handlers = {}
function M.handlers.event(grouped)
local group = vim.api.nvim_create_augroup("lazy_handler_event", { clear = true })
for event, plugins in pairs(grouped) do
---@cast event string
if event == "VimEnter" and vim.v.vim_did_enter == 1 then
Loader.load(plugins, { event = event })
else
local _event, pattern = event:match("^(%w+)%s+(.*)$")
vim.api.nvim_create_autocmd(_event or event, {
group = group,
once = true,
pattern = pattern,
callback = function()
Util.track({ event = event })
Loader.load(plugins, { event = event })
Util.track()
end,
})
end
end
end
function M.handlers.keys(grouped)
for keys, plugins in pairs(grouped) do
---@cast keys string
vim.keymap.set("n", keys, function()
vim.keymap.del("n", keys)
Util.track({ keys = keys })
Loader.load(plugins, { keys = keys })
vim.api.nvim_input(keys)
Util.track()
end)
end
end
function M.handlers.ft(grouped)
local group = vim.api.nvim_create_augroup("lazy_handler_ft", { clear = true })
for ft, plugins in pairs(grouped) do
---@cast ft string
vim.api.nvim_create_autocmd("FileType", {
once = true,
pattern = ft,
group = group,
callback = function()
Util.track({ ft = ft })
Loader.load(plugins, { ft = ft })
Util.track()
end,
})
end
end
function M.handlers.cmd(grouped)
for cmd, plugins in pairs(grouped) do
---@cast cmd string
local function _load()
vim.api.nvim_del_user_command(cmd)
Util.track({ cmd = cmd })
Loader.load(plugins, { cmd = cmd })
Util.track()
end
vim.api.nvim_create_user_command(cmd, function(event)
_load()
vim.cmd(
("%s %s%s%s %s"):format(
event.mods or "",
event.line1 == event.line2 and "" or event.line1 .. "," .. event.line2,
cmd,
event.bang and "!" or "",
event.args or ""
)
)
end, {
bang = true,
nargs = "*",
complete = function()
_load()
-- HACK: trick Neovim to show the newly loaded command completion
vim.api.nvim_input("<space><bs><tab>")
end,
})
end
end
function M.handlers.module(grouped)
---@param modname string
table.insert(package.loaders, 2, function(modname)
local idx = modname:find(".", 1, true) or #modname + 1
while idx do
local name = modname:sub(1, idx - 1)
---@diagnostic disable-next-line: redefined-local
local plugins = grouped[name]
if plugins then
grouped[name] = nil
local reason = { require = modname }
if #Loader.loading == 0 then
local f = 3
while not reason.source do
local info = debug.getinfo(f, "S")
if not info then
break
end
if info.what ~= "C" then
reason.source = info.source:sub(2)
end
f = f + 1
end
end
Loader.load(plugins, reason)
end
idx = modname:find(".", idx + 1, true)
end
end)
end
return M

View file

@ -1,66 +0,0 @@
local Loader = require("lazy.core.loader")
local Util = require("lazy.core.util")
---@class LazyCmdHandler:LazyHandler
local M = {}
function M:_load(cmd)
vim.api.nvim_del_user_command(cmd)
Util.track({ cmd = cmd })
Loader.load(self.active[cmd], { cmd = cmd })
Util.track()
end
---@param cmd string
function M:_add(cmd)
vim.api.nvim_create_user_command(cmd, function(event)
local command = {
cmd = cmd,
bang = event.bang or nil,
mods = event.smods,
args = event.fargs,
count = event.count >= 0 and event.range == 0 and event.count or nil,
}
if event.range == 1 then
command.range = { event.line1 }
elseif event.range == 2 then
command.range = { event.line1, event.line2 }
end
---@type string
local plugins = "`" .. table.concat(vim.tbl_values(self.active[cmd]), ", ") .. "`"
self:_load(cmd)
local info = vim.api.nvim_get_commands({})[cmd] or vim.api.nvim_buf_get_commands(0, {})[cmd]
if not info then
vim.schedule(function()
Util.error("Command `" .. cmd .. "` not found after loading " .. plugins)
end)
return
end
command.nargs = info.nargs
if event.args and event.args ~= "" and info.nargs and info.nargs:find("[1?]") then
command.args = { event.args }
end
vim.cmd(command)
end, {
bang = true,
range = true,
nargs = "*",
complete = function(_, line)
self:_load(cmd)
-- NOTE: return the newly loaded command completion
return vim.fn.getcompletion(line, "cmdline")
end,
})
end
---@param value string
function M:_del(value)
pcall(vim.api.nvim_del_user_command, value)
end
return M

View file

@ -1,171 +0,0 @@
local Config = require("lazy.core.config")
local Loader = require("lazy.core.loader")
local Util = require("lazy.core.util")
---@class LazyEventOpts
---@field event string
---@field group? string
---@field exclude? string[]
---@field data? any
---@field buffer? number
---@alias LazyEvent {id:string, event:string[]|string, pattern?:string[]|string}
---@alias LazyEventSpec string|{event?:string|string[], pattern?:string|string[]}|string[]
---@class LazyEventHandler:LazyHandler
---@field events table<string,true>
---@field group number
local M = {}
-- Event dependencies
M.triggers = {
FileType = "BufReadPost",
BufReadPost = "BufReadPre",
}
-- A table of mappings for custom events
-- Can be used by distros to add custom events (see usage in LazyVim)
---@type table<string, LazyEvent>
M.mappings = {
VeryLazy = { id = "VeryLazy", event = "User", pattern = "VeryLazy" },
-- Example:
-- LazyFile = { id = "LazyFile", event = { "BufReadPost", "BufNewFile", "BufWritePre" } },
}
M.mappings["User VeryLazy"] = M.mappings.VeryLazy
M.group = vim.api.nvim_create_augroup("lazy_handler_event", { clear = true })
---@param spec LazyEventSpec
---@return LazyEvent
function M:_parse(spec)
local ret = M.mappings[spec] --[[@as LazyEvent?]]
if ret then
return ret
end
if type(spec) == "string" then
local event, pattern = spec:match("^(%w+)%s+(.*)$")
event = event or spec
return { id = spec, event = event, pattern = pattern }
elseif Util.is_list(spec) then
ret = { id = table.concat(spec, "|"), event = spec }
else
ret = spec --[[@as LazyEvent]]
if not ret.id then
---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch
ret.id = type(ret.event) == "string" and ret.event or table.concat(ret.event, "|")
if ret.pattern then
---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch
ret.id = ret.id .. " " .. (type(ret.pattern) == "string" and ret.pattern or table.concat(ret.pattern, ", "))
end
end
end
return ret
end
---@param event LazyEvent
function M:_add(event)
local done = false
vim.api.nvim_create_autocmd(event.event, {
group = self.group,
once = true,
pattern = event.pattern,
callback = function(ev)
if done or not self.active[event.id] then
return
end
-- HACK: work-around for https://github.com/neovim/neovim/issues/25526
done = true
if event.id ~= "VeryLazy" then
Util.track({ [self.type] = event.id })
end
local state = M.get_state(ev.event, ev.buf, ev.data)
-- load the plugins
Loader.load(self.active[event.id], { [self.type] = event.id })
-- check if any plugin created an event handler for this event and fire the group
for _, s in ipairs(state) do
M.trigger(s)
end
if event.id ~= "VeryLazy" then
Util.track()
end
end,
})
end
-- Get the current state of the event and all the events that will be fired
---@param event string
---@param buf number
---@param data any
function M.get_state(event, buf, data)
local state = {} ---@type LazyEventOpts[]
while event do
table.insert(state, 1, {
event = event,
exclude = event ~= "FileType" and M.get_augroups(event) or nil,
buffer = buf,
data = data,
})
data = nil -- only pass the data to the first event
event = M.triggers[event]
end
return state
end
-- Get all augroups for the events
---@param event string
function M.get_augroups(event)
local groups = {} ---@type string[]
for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ event = event })) do
if autocmd.group_name then
table.insert(groups, autocmd.group_name)
end
end
return groups
end
-- Trigger an event. When a group is given, only the events in that group will be triggered.
-- When exclude is set, the events in those groups will be skipped.
---@param opts LazyEventOpts
function M.trigger(opts)
if opts.group or opts.exclude == nil then
return M._trigger(opts)
end
local done = {} ---@type table<string,true>
for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ event = opts.event })) do
local id = autocmd.event .. ":" .. (autocmd.group or "") ---@type string
local skip = done[id] or (opts.exclude and vim.tbl_contains(opts.exclude, autocmd.group_name))
done[id] = true
if autocmd.group and not skip then
opts.group = autocmd.group_name
M._trigger(opts)
end
end
end
-- Trigger an event
---@param opts LazyEventOpts
function M._trigger(opts)
if Config.options.debug then
Util.info({
"# Firing Events",
" - **event:** " .. opts.event,
opts.group and (" - **group:** " .. opts.group),
opts.buffer and (" - **buffer:** " .. opts.buffer),
})
end
Util.track({ event = opts.group or opts.event })
Util.try(function()
vim.api.nvim_exec_autocmds(opts.event, {
buffer = opts.buffer,
group = opts.group,
modeline = false,
data = opts.data,
})
Util.track()
end)
end
return M

View file

@ -1,25 +0,0 @@
local Event = require("lazy.core.handler.event")
local Loader = require("lazy.core.loader")
---@class LazyFiletypeHandler:LazyEventHandler
local M = {}
M.extends = Event
---@param plugin LazyPlugin
function M:add(plugin)
self.super.add(self, plugin)
if plugin.ft then
Loader.ftdetect(plugin.dir)
end
end
---@return LazyEvent
function M:_parse(value)
return {
id = value,
event = "FileType",
pattern = value,
}
end
return M

View file

@ -1,139 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.core.util")
---@class LazyHandler
---@field type LazyHandlerTypes
---@field extends? LazyHandler
---@field active table<string,table<string,string>>
---@field managed table<string,string> mapping handler keys to plugin names
---@field super LazyHandler
local M = {}
---@enum LazyHandlerTypes
M.types = {
keys = "keys",
event = "event",
cmd = "cmd",
ft = "ft",
}
---@type table<string,LazyHandler>
M.handlers = {}
M.did_setup = false
function M.init()
for _, type in pairs(M.types) do
M.handlers[type] = M.new(type)
end
end
function M.setup()
M.did_setup = true
for _, plugin in pairs(Config.plugins) do
Util.try(function()
M.enable(plugin)
end, "Failed to setup handlers for " .. plugin.name)
end
end
---@param plugin LazyPlugin
function M.disable(plugin)
for type in pairs(plugin._.handlers or {}) do
M.handlers[type]:del(plugin)
end
end
---@param plugin LazyPlugin
function M.enable(plugin)
if not plugin._.loaded then
if not plugin._.handlers then
M.resolve(plugin)
end
for type in pairs(plugin._.handlers or {}) do
M.handlers[type]:add(plugin)
end
end
end
---@param type LazyHandlerTypes
function M.new(type)
---@type LazyHandler
local handler = require("lazy.core.handler." .. type)
local super = handler.extends or M
local self = setmetatable({}, { __index = setmetatable(handler, { __index = super }) })
self.super = super
self.active = {}
self.managed = {}
self.type = type
return self
end
---@param _value string
---@protected
function M:_add(_value) end
---@param _value string
---@protected
function M:_del(_value) end
---@param value any
---@param _plugin LazyPlugin
---@return string|{id:string}
function M:_parse(value, _plugin)
assert(type(value) == "string", "Expected string, got " .. vim.inspect(value))
return value
end
---@param values any[]
---@param plugin LazyPlugin
function M:_values(values, plugin)
---@type table<string,any>
local ret = {}
for _, value in ipairs(values) do
local parsed = self:_parse(value, plugin)
ret[type(parsed) == "string" and parsed or parsed.id] = parsed
end
return ret
end
---@param plugin LazyPlugin
function M.resolve(plugin)
local Plugin = require("lazy.core.plugin")
plugin._.handlers = {}
for type, handler in pairs(M.handlers) do
if plugin[type] then
plugin._.handlers[type] = handler:_values(Plugin.values(plugin, type, true), plugin)
end
end
end
---@param plugin LazyPlugin
function M:add(plugin)
for key, value in pairs(plugin._.handlers[self.type] or {}) do
if not self.active[key] then
self.active[key] = {}
self:_add(value)
self.managed[key] = plugin.name
end
self.active[key][plugin.name] = plugin.name
end
end
---@param plugin LazyPlugin
function M:del(plugin)
if not plugin._.handlers then
return
end
for key, value in pairs(plugin._.handlers[self.type] or {}) do
if self.active[key] and self.active[key][plugin.name] then
self.active[key][plugin.name] = nil
if vim.tbl_isempty(self.active[key]) then
self:_del(value)
self.active[key] = nil
end
end
end
end
return M

View file

@ -1,202 +0,0 @@
local Loader = require("lazy.core.loader")
local Util = require("lazy.core.util")
---@class LazyKeysBase
---@field desc? string
---@field noremap? boolean
---@field remap? boolean
---@field expr? boolean
---@field nowait? boolean
---@field ft? string|string[]
---@class LazyKeysSpec: LazyKeysBase
---@field [1] string lhs
---@field [2]? string|fun():string?|false rhs
---@field mode? string|string[]
---@class LazyKeys: LazyKeysBase
---@field lhs string lhs
---@field rhs? string|fun() rhs
---@field mode? string
---@field id string
---@field name string
---@class LazyKeysHandler:LazyHandler
local M = {}
local skip = { mode = true, id = true, ft = true, rhs = true, lhs = true }
---@param value string|LazyKeysSpec
---@param mode? string
---@return LazyKeys
function M.parse(value, mode)
value = type(value) == "string" and { value } or value --[[@as LazyKeysSpec]]
local ret = vim.deepcopy(value) --[[@as LazyKeys]]
ret.lhs = ret[1] or ""
ret.rhs = ret[2]
---@diagnostic disable-next-line: no-unknown
ret[1] = nil
---@diagnostic disable-next-line: no-unknown
ret[2] = nil
ret.mode = mode or "n"
ret.id = vim.api.nvim_replace_termcodes(ret.lhs, true, true, true)
if ret.ft then
local ft = type(ret.ft) == "string" and { ret.ft } or ret.ft --[[@as string[] ]]
ret.id = ret.id .. " (" .. table.concat(ft, ", ") .. ")"
end
if ret.mode ~= "n" then
ret.id = ret.id .. " (" .. ret.mode .. ")"
end
return ret
end
---@param keys LazyKeys
function M.to_string(keys)
return keys.lhs .. (keys.mode == "n" and "" or " (" .. keys.mode .. ")")
end
---@param lhs string
---@param mode? string
function M:have(lhs, mode)
local keys = M.parse(lhs, mode)
return self.managed[keys.id] ~= nil
end
function M:_values(values)
return M.resolve(values)
end
---@param spec? (string|LazyKeysSpec)[]
function M.resolve(spec)
---@type LazyKeys[]
local values = {}
---@diagnostic disable-next-line: no-unknown
for _, value in ipairs(spec or {}) do
value = type(value) == "string" and { value } or value --[[@as LazyKeysSpec]]
value.mode = value.mode or "n"
local modes = (type(value.mode) == "table" and value.mode or { value.mode }) --[=[@as string[]]=]
for _, mode in ipairs(modes) do
local keys = M.parse(value, mode)
if keys.rhs == vim.NIL or keys.rhs == false then
values[keys.id] = nil
else
values[keys.id] = keys
end
end
end
return values
end
---@param keys LazyKeys
function M.opts(keys)
local opts = {} ---@type LazyKeysBase
---@diagnostic disable-next-line: no-unknown
for k, v in pairs(keys) do
if type(k) ~= "number" and not skip[k] then
---@diagnostic disable-next-line: no-unknown
opts[k] = v
end
end
return opts
end
---@param keys LazyKeys
function M.is_nop(keys)
return type(keys.rhs) == "string" and (keys.rhs == "" or keys.rhs:lower() == "<nop>")
end
---@param keys LazyKeys
function M:_add(keys)
local lhs = keys.lhs
local opts = M.opts(keys)
---@param buf? number
local function add(buf)
if M.is_nop(keys) then
return self:_set(keys, buf)
end
vim.keymap.set(keys.mode, lhs, function()
local plugins = self.active[keys.id]
-- always delete the mapping immediately to prevent recursive mappings
self:_del(keys)
self.active[keys.id] = nil
if plugins then
local name = M.to_string(keys)
Util.track({ keys = name })
Loader.load(plugins, { keys = name })
Util.track()
end
if keys.mode:sub(-1) == "a" then
lhs = lhs .. "<C-]>"
end
local feed = vim.api.nvim_replace_termcodes("<Ignore>" .. lhs, true, true, true)
-- insert instead of append the lhs
vim.api.nvim_feedkeys(feed, "i", false)
end, {
desc = opts.desc,
nowait = opts.nowait,
-- we do not return anything, but this is still needed to make operator pending mappings work
expr = true,
buffer = buf,
})
end
-- buffer-local mappings
if keys.ft then
vim.api.nvim_create_autocmd("FileType", {
pattern = keys.ft,
callback = function(event)
if self.active[keys.id] and not M.is_nop(keys) then
add(event.buf)
else
-- Only create the mapping if its managed by lazy
-- otherwise the plugin is supposed to manage it
self:_set(keys, event.buf)
end
end,
})
else
add()
end
end
-- Delete a mapping and create the real global/buffer-local
-- mapping when needed
---@param keys LazyKeys
function M:_del(keys)
-- bufs will be all buffers of the filetype for a buffer-local mapping
-- OR `false` for a global mapping
local bufs = { false }
if keys.ft then
local ft = type(keys.ft) == "string" and { keys.ft } or keys.ft --[[@as string[] ]]
bufs = vim.tbl_filter(function(buf)
return vim.tbl_contains(ft, vim.bo[buf].filetype)
end, vim.api.nvim_list_bufs())
end
for _, buf in ipairs(bufs) do
pcall(vim.keymap.del, keys.mode, keys.lhs, { buffer = buf or nil })
self:_set(keys, buf or nil)
end
end
-- Create a mapping if it is managed by lazy
---@param keys LazyKeys
---@param buf number?
function M:_set(keys, buf)
if keys.rhs then
local opts = M.opts(keys)
---@diagnostic disable-next-line: inject-field
opts.buffer = buf
vim.keymap.set(keys.mode, keys.lhs, keys.rhs, opts)
end
end
return M

View file

@ -1,576 +1,92 @@
local Cache = require("lazy.core.cache")
local Config = require("lazy.core.config")
local Handler = require("lazy.core.handler")
local Plugin = require("lazy.core.plugin")
local Util = require("lazy.core.util") local Util = require("lazy.core.util")
local Config = require("lazy.core.config")
---@class LazyCoreLoader
local M = {} local M = {}
local DEFAULT_PRIORITY = 50
---@type LazyPlugin[] ---@type LazyPlugin[]
M.loading = {} M.loading = {}
M.init_done = false
---@type table<string,true>
M.disabled_rtp_plugins = { packer_compiled = true }
---@type table<string,string>
M.did_ftdetect = {}
M.did_handlers = false
function M.disable_rtp_plugin(plugin) function M.init_plugins()
M.disabled_rtp_plugins[plugin] = true Util.track("plugin_init")
end
function M.setup()
for _, file in ipairs(Config.options.performance.rtp.disabled_plugins) do
M.disable_rtp_plugin(file)
end
vim.api.nvim_create_autocmd("ColorSchemePre", {
callback = function(event)
M.colorscheme(event.match)
end,
})
-- load the plugins
Plugin.load()
Handler.init()
-- install missing plugins
if Config.options.install.missing then
Util.track("install")
local count = 0
while M.install_missing() do
count = count + 1
if count > 5 then
Util.error("Too many rounds of missing plugins")
break
end
end
Util.track()
end
Config.mapleader = vim.g.mapleader
Config.maplocalleader = vim.g.maplocalleader
-- report any warnings & errors
Config.spec:report()
-- setup handlers
Util.track("handlers")
Handler.setup()
M.did_handlers = true
Util.track()
end
-- this will incrementally install missing plugins
-- multiple rounds can happen when importing a spec from a missing plugin
function M.install_missing()
for _, plugin in pairs(Config.plugins) do
local installed = plugin._.installed
local has_errors = Plugin.has_errors(plugin)
if not has_errors and not (installed and not plugin._.build) then
for _, colorscheme in ipairs(Config.options.install.colorscheme) do
if colorscheme == "default" then
break
end
M.colorscheme(colorscheme)
if vim.g.colors_name or pcall(vim.cmd.colorscheme, colorscheme) then
break
end
end
Cache.reset()
require("lazy.manage").install({ wait = true, lockfile = true, clear = false })
-- remove any installed plugins from indexed, so cache will index again
for _, p in pairs(Config.plugins) do
if p._.installed then
Cache.reset(p.dir)
end
end
-- reload plugins
Plugin.load()
return true
end
end
end
-- Startup sequence
-- 1. load any start plugins and do init
function M.startup()
Util.track({ start = "startup" })
-- load filetype.lua first since plugins might depend on that
M.source(vim.env.VIMRUNTIME .. "/filetype.lua")
-- backup original rtp
local rtp = vim.opt.rtp:get() --[[@as string[] ]]
-- 1. run plugin init
Util.track({ start = "init" })
for _, plugin in pairs(Config.plugins) do for _, plugin in pairs(Config.plugins) do
if plugin.init then if plugin.init then
Util.track({ plugin = plugin.name, init = "init" }) Util.track({ plugin = plugin.name, start = "init" })
Util.try(function() Util.try(plugin.init, "Failed to run `init` for **" .. plugin.name .. "**")
plugin.init(plugin)
end, "Failed to run `init` for **" .. plugin.name .. "**")
Util.track() Util.track()
end end
end if plugin.opt == false then
Util.track()
-- 2. load start plugin
Util.track({ start = "start" })
for _, plugin in ipairs(M.get_start_plugins()) do
-- plugin may be loaded by another plugin in the meantime
if not plugin._.loaded then
M.load(plugin, { start = "start" }) M.load(plugin, { start = "start" })
end end
end end
Util.track() Util.track()
-- 3. load plugins from the original rtp, excluding after
Util.track({ start = "rtp plugins" })
for _, path in ipairs(rtp) do
if not path:find("after/?$") then
-- these paths don't will already have their ftdetect ran,
-- by sourcing filetype.lua above, so skip them
M.did_ftdetect[path] = path
M.packadd(path)
end
end
Util.track()
-- 4. load after plugins
Util.track({ start = "after" })
for _, path in
ipairs(vim.opt.rtp:get() --[[@as string[] ]])
do
if path:find("after/?$") then
M.source_runtime(path, "plugin")
end
end
Util.track()
M.init_done = true
Util.track()
end
function M.get_start_plugins()
---@type LazyPlugin[]
local start = {}
for _, plugin in pairs(Config.plugins) do
if not plugin._.loaded and (plugin._.rtp_loaded or plugin.lazy == false) then
start[#start + 1] = plugin
end
end
table.sort(start, function(a, b)
local ap = a.priority or DEFAULT_PRIORITY
local bp = b.priority or DEFAULT_PRIORITY
return ap > bp
end)
return start
end end
---@class Loader ---@class Loader
---@param plugins string|LazyPlugin|string[]|LazyPlugin[] ---@param plugins string|LazyPlugin|string[]|LazyPlugin[]
---@param reason {[string]:string} ---@param reason {[string]:string}
---@param opts? {force:boolean} when force is true, we skip the cond check ---@param opts? {load_start: boolean}
function M.load(plugins, reason, opts) function M.load(plugins, reason, opts)
---@diagnostic disable-next-line: cast-local-type ---@diagnostic disable-next-line: cast-local-type
plugins = (type(plugins) == "string" or plugins.name) and { plugins } or plugins plugins = type(plugins) == "string" or plugins.name and { plugins } or plugins
---@cast plugins (string|LazyPlugin)[] ---@cast plugins (string|LazyPlugin)[]
for _, plugin in pairs(plugins) do for _, plugin in ipairs(plugins) do
if type(plugin) == "string" then plugin = type(plugin) == "string" and Config.plugins[plugin] or plugin
if Config.plugins[plugin] then ---@cast plugin LazyPlugin
plugin = Config.plugins[plugin]
elseif Config.spec.disabled[plugin] then
plugin = nil
else
Util.error("Plugin " .. plugin .. " not found")
plugin = nil
end
end
if plugin and not plugin._.loaded then
M._load(plugin, reason, opts)
end
end
end
---@param plugin LazyPlugin
function M.deactivate(plugin)
if not plugin._.loaded then
return
end
local main = M.get_main(plugin)
if main then
Util.try(function()
local mod = require(main)
if mod.deactivate then
mod.deactivate(plugin)
end
end, "Failed to deactivate plugin " .. plugin.name)
end
-- execute deactivate when needed
if plugin.deactivate then
Util.try(function()
plugin.deactivate(plugin)
end, "Failed to deactivate plugin " .. plugin.name)
end
-- disable handlers
Handler.disable(plugin)
-- clear plugin properties cache
plugin._.cache = nil
-- remove loaded lua modules
Util.walkmods(plugin.dir .. "/lua", function(modname)
package.loaded[modname] = nil
package.preload[modname] = nil
end)
-- clear vim.g.loaded_ for plugins
Util.ls(plugin.dir .. "/plugin", function(_, name, type)
if type == "file" then
vim.g["loaded_" .. name:gsub("%..*", "")] = nil
end
end)
-- set as not loaded
plugin._.loaded = nil
end
--- reload a plugin
---@param plugin LazyPlugin|string
function M.reload(plugin)
if type(plugin) == "string" then
plugin = Config.plugins[plugin]
end
if not plugin then
error("Plugin not found")
end
local load = plugin._.loaded ~= nil
M.deactivate(plugin)
-- enable handlers
Handler.enable(plugin)
-- run init
if plugin.init then
Util.try(function()
plugin.init(plugin)
end, "Failed to run `init` for **" .. plugin.name .. "**")
end
-- if this is a start plugin, load it now
if plugin.lazy == false then
load = true
end
local events = plugin._.handlers and plugin._.handlers.event and plugin._.handlers.event or {}
for _, event in pairs(events) do
if event.id:find("VimEnter") or event.id:find("UIEnter") or event.id:find("VeryLazy") then
load = true
break
end
end
-- reload any vimscript files for this plugin
local scripts = vim.fn.getscriptinfo()
local loaded_scripts = {}
for _, s in ipairs(scripts) do
---@type string
local path = s.name
if
path:sub(-4) == ".vim"
and path:find(plugin.dir, 1, true) == 1
and not path:find("/plugin/", 1, true)
and not path:find("/ftplugin/", 1, true)
then
loaded_scripts[#loaded_scripts + 1] = path
end
end
if load then
M.load(plugin, { start = "reload" })
for _, s in ipairs(loaded_scripts) do
M.source(s)
end
end
end
---@param plugin LazyPlugin
---@param reason {[string]:string}
---@param opts? {force:boolean} when force is true, we skip the cond check
function M._load(plugin, reason, opts)
if not plugin._.installed then
return Util.error("Plugin " .. plugin.name .. " is not installed")
end
if plugin._.cond == false and not (opts and opts.force) then
return
end
if not Handler.did_setup then
Util.try(function()
Handler.enable(plugin)
end, "Failed to setup handlers for " .. plugin.name)
end
---@diagnostic disable-next-line: assign-type-mismatch
plugin._.loaded = {}
for k, v in pairs(reason) do
plugin._.loaded[k] = v
end
if #M.loading > 0 then
plugin._.loaded.plugin = M.loading[#M.loading].name
elseif reason.require then
plugin._.loaded.source = Util.get_source()
end
table.insert(M.loading, plugin)
Util.track({ plugin = plugin.name, start = reason.start })
Handler.disable(plugin)
if not plugin.virtual then
M.add_to_rtp(plugin)
end
if plugin._.pkg and plugin._.pkg.source == "rockspec" then
M.add_to_luapath(plugin)
end
if plugin.dependencies then
Util.try(function()
M.load(plugin.dependencies, {})
end, "Failed to load deps for " .. plugin.name)
end
if not plugin.virtual then
M.packadd(plugin.dir)
end
if plugin.config or plugin.opts then
M.config(plugin)
end
plugin._.loaded.time = Util.track().time
table.remove(M.loading)
vim.schedule(function()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyLoad", modeline = false, data = plugin.name })
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false })
end)
end
--- runs plugin config
---@param plugin LazyPlugin
function M.config(plugin)
local fn
if type(plugin.config) == "function" then
fn = function()
local opts = Plugin.values(plugin, "opts", false)
plugin.config(plugin, opts)
end
else
local main = M.get_main(plugin)
if main then
fn = function()
local opts = Plugin.values(plugin, "opts", false)
require(main).setup(opts)
end
else
return Util.error(
"Lua module not found for config of " .. plugin.name .. ". Please use a `config()` function instead"
)
end
end
Util.try(fn, "Failed to run `config` for " .. plugin.name)
end
---@param plugin LazyPlugin
function M.get_main(plugin)
if plugin.main then
return plugin.main
end
if plugin.name ~= "mini.nvim" and plugin.name:match("^mini%..*$") then
return plugin.name
end
local normname = Util.normname(plugin.name)
---@type string[]
local mods = {}
for _, mod in ipairs(Cache.find("*", { all = true, rtp = false, paths = { plugin.dir } })) do
local modname = mod.modname
mods[#mods + 1] = modname
local modnorm = Util.normname(modname)
-- if we found an exact match, then use that
if modnorm == normname then
mods = { modname }
break
end
end
return #mods == 1 and mods[1] or nil
end
---@param path string
function M.packadd(path)
M.source_runtime(path, "plugin")
M.ftdetect(path)
if M.init_done then
M.source_runtime(path, "after/plugin")
end
end
---@param path string
function M.ftdetect(path)
if not M.did_ftdetect[path] then
M.did_ftdetect[path] = path
vim.cmd("augroup filetypedetect")
M.source_runtime(path, "ftdetect")
vim.cmd("augroup END")
end
end
---@param ... string
function M.source_runtime(...)
local dir = table.concat({ ... }, "/")
---@type string[]
local files = {}
Util.walk(dir, function(path, name, t)
local ext = name:sub(-3)
name = name:sub(1, -5)
if (t == "file" or t == "link") and (ext == "lua" or ext == "vim") and not M.disabled_rtp_plugins[name] then
files[#files + 1] = path
end
end)
-- plugin files are sourced alphabetically per directory
table.sort(files)
for _, path in ipairs(files) do
M.source(path)
end
end
-- This does the same as runtime.c:add_pack_dir_to_rtp
-- * find first after
-- * find lazy pack path
-- * insert right after lazy pack path or right before first after or at the end
-- * insert after dir right before first after or append to the end
---@param plugin LazyPlugin
function M.add_to_rtp(plugin)
local rtp = vim.api.nvim_get_runtime_file("", true)
local idx_dir, idx_after
for i, path in ipairs(rtp) do
if Util.is_win then
path = Util.norm(path)
end
if path == Config.me then
idx_dir = i + 1
elseif not idx_after and path:sub(-6, -1) == "/after" then
idx_after = i + 1 -- +1 to offset the insert of the plugin dir
idx_dir = idx_dir or i
break
end
end
table.insert(rtp, idx_dir or (#rtp + 1), plugin.dir)
local after = plugin.dir .. "/after"
if vim.uv.fs_stat(after) then
table.insert(rtp, idx_after or (#rtp + 1), after)
end
---@type vim.Option
vim.opt.rtp = rtp
end
---@param plugin LazyPlugin
function M.add_to_luapath(plugin)
local root = Config.options.rocks.root .. "/" .. plugin.name
local path = root .. "/share/lua/5.1"
local cpath = root .. "/lib/lua/5.1"
local cpath2 = root .. "/lib64/lua/5.1"
package.path = package.path .. ";" .. path .. "/?.lua;" .. path .. "/?/init.lua;"
package.cpath = package.cpath .. ";" .. cpath .. "/?." .. (jit.os:find("Windows") and "dll" or "so") .. ";"
package.cpath = package.cpath .. ";" .. cpath2 .. "/?." .. (jit.os:find("Windows") and "dll" or "so") .. ";"
end
function M.source(path)
Util.track({ runtime = path })
Util.try(function()
vim.cmd("source " .. path)
end, "Failed to source `" .. path .. "`")
Util.track()
end
function M.colorscheme(name)
if vim.tbl_contains(vim.fn.getcompletion("", "color"), name) then
return
end
for _, plugin in pairs(Config.plugins) do
if not plugin._.loaded then if not plugin._.loaded then
for _, ext in ipairs({ "lua", "vim" }) do ---@diagnostic disable-next-line: assign-type-mismatch
local path = plugin.dir .. "/colors/" .. name .. "." .. ext plugin._.loaded = {}
if vim.uv.fs_stat(path) then for k, v in pairs(reason) do
return M.load(plugin, { colorscheme = name }) plugin._.loaded[k] = v
end
end end
if #M.loading > 0 then
plugin._.loaded.plugin = M.loading[#M.loading].name
end
table.insert(M.loading, plugin)
Util.track({ plugin = plugin.name, start = reason.start })
M.packadd(plugin, opts and opts.load_start)
if plugin.dependencies then
M.load(plugin.dependencies, {})
end
if plugin.config then
Util.try(plugin.config, "Failed to run `config` for " .. plugin.name)
end
plugin._.loaded.time = Util.track().time
table.remove(M.loading)
vim.schedule(function()
vim.cmd("do User LazyRender")
end)
end end
end end
end end
function M.auto_load(modname, modpath) ---@param plugin LazyPlugin
local plugin = Plugin.find(modpath, { fast = not M.did_handlers }) function M.packadd(plugin, load_start)
if plugin then if plugin.opt then
plugin._.rtp_loaded = true vim.cmd.packadd(plugin.name)
-- don't load if: M.source_runtime(plugin, "/after/plugin")
-- * handlers haven't been setup yet elseif load_start then
-- * we're loading specs vim.opt.runtimepath:append(plugin.dir)
-- * the plugin is already loaded M.source_runtime(plugin, "/plugin")
if M.did_handlers and not (Plugin.loading or plugin._.loaded) then M.source_runtime(plugin, "/after/plugin")
if plugin.module == false then
error("Plugin " .. plugin.name .. " is not loaded and is configured with module=false")
end
M.load(plugin, { require = modname })
if plugin._.cond == false then
error("You're trying to load `" .. plugin.name .. "` for which `cond==false`")
end
end
end end
end end
---@param modname string ---@param plugin LazyPlugin
function M.loader(modname) ---@param dir? string
local paths, cached = Util.get_unloaded_rtp(modname, { cache = true }) function M.source_runtime(plugin, dir)
local ret = Cache.find(modname, { rtp = false, paths = paths })[1] Util.walk(plugin.dir .. dir, function(path, _, t)
local ext = path:sub(-3)
if not ret and cached then if t == "file" and (ext == "lua" or ext == "vim") then
paths = Util.get_unloaded_rtp(modname) vim.cmd("silent source " .. path)
ret = Cache.find(modname, { rtp = false, paths = paths })[1]
end
if ret then
-- explicitly set to nil to prevent loading errors
package.loaded[modname] = nil
M.auto_load(modname, ret.modpath)
local mod = package.loaded[modname]
if type(mod) == "table" then
return function()
return mod
end
end end
-- selene: allow(incorrect_standard_library_use) end)
return loadfile(ret.modpath, nil, nil, ret.stat)
end
end end
return M return M

View file

@ -1,360 +0,0 @@
local Config = require("lazy.core.config")
local Pkg = require("lazy.pkg")
local Util = require("lazy.core.util")
--- This class is used to manage the plugins.
--- A plugin is a collection of fragments that are related to each other.
---@class LazyMeta
---@field plugins table<string, LazyPlugin>
---@field str_to_meta table<string, LazyPlugin>
---@field frag_to_meta table<number, LazyPlugin>
---@field dirty table<string, boolean>
---@field spec LazySpecLoader
---@field fragments LazyFragments
---@field pkgs table<string, number>
local M = {}
---@param spec LazySpecLoader
---@return LazyMeta
function M.new(spec)
local self = setmetatable({}, { __index = M })
self.spec = spec
self.fragments = require("lazy.core.fragments").new(spec)
self.plugins = {}
self.frag_to_meta = {}
self.str_to_meta = {}
self.dirty = {}
self.pkgs = {}
return self
end
-- import package specs
function M:load_pkgs()
if not Config.options.pkg.enabled then
return
end
for _, pkg in ipairs(Pkg.get()) do
local last_id = self.fragments._fid
local meta, fragment = self:add(pkg.spec)
if meta and fragment then
meta._.pkg = pkg
-- tag all top-level package fragments that were added as optional
for _, fid in ipairs(meta._.frags) do
if fid > last_id then
local frag = self.fragments:get(fid)
frag.spec.optional = true
end
end
-- keep track of the top-level package fragment
self.pkgs[pkg.dir] = fragment.id
end
end
end
--- Remove a plugin and all its fragments.
---@param name string
function M:del(name)
local meta = self.plugins[name]
if not meta then
return
end
for _, fid in ipairs(meta._.frags or {}) do
self.fragments:del(fid)
end
self.plugins[name] = nil
end
--- Add a fragment to a plugin.
--- This will create a new plugin if it does not exist.
--- It also keeps track of renames.
---@param plugin LazyPluginSpec
function M:add(plugin)
local fragment = self.fragments:add(plugin)
if not fragment then
return
end
local meta = self.plugins[fragment.name]
or fragment.url and self.str_to_meta[fragment.url]
or fragment.dir and self.str_to_meta[fragment.dir]
if not meta then
meta = { name = fragment.name, _ = { frags = {} } }
local url, dir = fragment.url, fragment.dir
-- add to index
if url then
self.str_to_meta[url] = meta
end
if dir then
self.str_to_meta[dir] = meta
end
end
table.insert(meta._.frags, fragment.id)
if meta._ and meta._.rtp_loaded and meta.dir then
local old_dir = meta.dir
self:_rebuild(meta.name)
local new_dir = meta.dir
if old_dir ~= new_dir then
local msg = "Plugin `" .. meta.name .. "` changed `dir`:\n- from: `" .. old_dir .. "`\n- to: `" .. new_dir .. "`"
msg = msg .. "\n\nThis plugin was already partially loaded, so things may break.\nPlease fix your config."
self.spec:error(msg)
end
end
if plugin.name then
-- handle renames
if meta.name ~= plugin.name then
self.plugins[meta.name] = nil
meta.name = plugin.name
end
end
self.plugins[meta.name] = meta
self.frag_to_meta[fragment.id] = meta
self.dirty[meta.name] = true
return meta, fragment
end
--- Rebuild all plugins based on dirty fragments,
--- or dirty plugins. Will remove plugins that no longer have fragments.
function M:rebuild()
local frag_count = vim.tbl_count(self.fragments.dirty)
local plugin_count = vim.tbl_count(self.dirty)
if frag_count == 0 and plugin_count == 0 then
return
end
if Config.options.debug then
Util.track("rebuild plugins frags=" .. frag_count .. " plugins=" .. plugin_count)
end
for fid in pairs(self.fragments.dirty) do
local meta = self.frag_to_meta[fid]
if meta then
if self.fragments:get(fid) then
-- fragment still exists, so mark plugin as dirty
self.dirty[meta.name] = true
else
-- fragment was deleted, so remove it from plugin
self.frag_to_meta[fid] = nil
---@param f number
meta._.frags = Util.filter(function(f)
return f ~= fid
end, meta._.frags)
-- if no fragments left, delete plugin
if #meta._.frags == 0 then
self:del(meta.name)
else
self.dirty[meta.name] = true
end
end
end
end
self.fragments.dirty = {}
for n, _ in pairs(self.dirty) do
self:_rebuild(n)
end
if Config.options.debug then
Util.track()
end
end
--- Rebuild a single plugin.
--- This will resolve the plugin based on its fragments using metatables.
--- This also resolves dependencies, dep, optional, dir, dev, and url.
---@param name string
function M:_rebuild(name)
if not self.dirty[name] then
return
end
self.dirty[name] = nil
local plugin = self.plugins[name]
if not plugin or #plugin._.frags == 0 then
self.plugins[name] = nil
return
end
setmetatable(plugin, nil)
plugin.dependencies = {}
local super = nil
plugin.url = nil
plugin._.dep = true
plugin._.top = true
plugin.optional = true
assert(#plugin._.frags > 0, "no fragments found for plugin " .. name)
---@type table<number, boolean>
local added = {}
for _, fid in ipairs(plugin._.frags) do
if not added[fid] then
added[fid] = true
local fragment = self.fragments:get(fid)
assert(fragment, "fragment " .. fid .. " not found, for plugin " .. name)
---@diagnostic disable-next-line: no-unknown
super = setmetatable(fragment.spec, super and { __index = super } or nil)
plugin._.dep = plugin._.dep and fragment.dep
plugin.optional = plugin.optional and (rawget(fragment.spec, "optional") == true)
plugin.url = fragment.url or plugin.url
plugin._.top = plugin._.top and fragment.pid == nil
-- dependencies
for _, dep in ipairs(fragment.deps or {}) do
local dep_meta = self.frag_to_meta[dep]
if dep_meta then
table.insert(plugin.dependencies, dep_meta.name)
end
end
end
end
super = super or {}
-- dir / dev
plugin.dev = super.dev
plugin.dir = super.dir
if plugin.dir then
plugin.dir = Util.norm(plugin.dir)
elseif super.virtual then
plugin.dir = Util.norm("/dev/null/" .. plugin.name)
else
if plugin.dev == nil and plugin.url then
for _, pattern in ipairs(Config.options.dev.patterns) do
if plugin.url:find(pattern, 1, true) then
plugin.dev = true
break
end
end
end
if plugin.dev == true then
local dev_dir = type(Config.options.dev.path) == "string" and Config.options.dev.path .. "/" .. plugin.name
or Util.norm(Config.options.dev.path(plugin))
if not Config.options.dev.fallback or vim.fn.isdirectory(dev_dir) == 1 then
plugin.dir = dev_dir
else
plugin.dev = false
end
end
plugin.dir = plugin.dir or Config.options.root .. "/" .. plugin.name
end
-- dependencies
if #plugin.dependencies == 0 and not super.dependencies then
plugin.dependencies = nil
end
-- optional
if not plugin.optional and not super.optional then
plugin.optional = nil
end
setmetatable(plugin, { __index = super })
return plugin
end
--- Disable a plugin.
---@param plugin LazyPlugin
function M:disable(plugin)
plugin._.kind = "disabled"
self:del(plugin.name)
self.spec.disabled[plugin.name] = plugin
end
--- Check if a plugin should be disabled, but ignore uninstalling it.
function M:fix_cond()
for _, plugin in pairs(self.plugins) do
local cond = plugin.cond
if cond == nil then
cond = Config.options.defaults.cond
end
if cond == false or (type(cond) == "function" and not cond(plugin)) then
plugin._.cond = false
local stack = { plugin }
while #stack > 0 do
local p = table.remove(stack) --[[@as LazyPlugin]]
if not self.spec.ignore_installed[p.name] then
for _, dep in ipairs(p.dependencies or {}) do
table.insert(stack, self.plugins[dep])
end
self.spec.ignore_installed[p.name] = true
end
end
plugin.enabled = false
end
end
end
--- Removes plugins for which all its fragments are optional.
function M:fix_optional()
if self.spec.optional then
return 0
end
local changes = 0
for _, plugin in pairs(self.plugins) do
if plugin.optional then
changes = changes + 1
self:del(plugin.name)
end
end
self:rebuild()
return changes
end
--- Removes plugins that are disabled.
function M:fix_disabled()
local changes = 0
local function check(top)
for _, plugin in pairs(self.plugins) do
if (plugin._.top or false) == top then
if plugin.enabled == false or (type(plugin.enabled) == "function" and not plugin.enabled()) then
changes = changes + 1
if plugin.optional then
self:del(plugin.name)
else
self:disable(plugin)
end
self:rebuild()
end
end
end
end
-- disable top-level plugins first, since they may have non-top-level frags
-- that disable other plugins
check(true)
-- then disable non-top-level plugins
check(false)
return changes
end
--- Removes package fragments for plugins that no longer use the same directory.
function M:fix_pkgs()
for dir, fid in pairs(self.pkgs) do
local plugin = self.frag_to_meta[fid]
plugin = plugin and self.plugins[plugin.name]
if plugin then
-- check if plugin is still in the same directory
if plugin.dir ~= dir then
self.fragments:del(fid)
end
end
end
self:rebuild()
end
--- Resolve all plugins, based on cond, enabled and optional.
function M:resolve()
Util.track("resolve plugins")
self:rebuild()
self:fix_pkgs()
self:fix_cond()
-- selene: allow(empty_loop)
while self:fix_disabled() + self:fix_optional() > 0 do
end
Util.track()
end
return M

121
lua/lazy/core/module.lua Normal file
View file

@ -0,0 +1,121 @@
local ffi = require("ffi")
---@diagnostic disable-next-line: no-unknown
local uv = vim.loop
local M = {}
M.dirty = false
local cache_path = vim.fn.stdpath("state") .. "/lazy.state"
---@type CacheHash
local cache_hash
---@alias CacheHash {mtime: {sec:number, nsec:number}, size:number}
---@alias CacheEntry {hash:CacheHash, chunk:string, used:boolean}
---@type table<string,CacheEntry?>
M.cache = {}
---@param modname string
---@param modpath string
---@return any
function M.load(modname, modpath)
local entry = M.cache[modname]
local hash = assert(M.hash(modpath))
if entry and not M.eq(entry.hash, hash) then
entry = nil
end
local chunk, err
if entry then
entry.used = true
chunk, err = load(entry.chunk --[[@as string]], "@" .. modpath, "b")
else
vim.schedule(function()
vim.notify("loadfile(" .. modname .. ")")
end)
chunk, err = loadfile(modpath)
if chunk then
M.dirty = true
M.cache[modname] = { hash = hash, chunk = string.dump(chunk), used = true }
end
end
return chunk and chunk() or error(err)
end
function M.setup()
M.load_cache()
-- preload core modules
local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h")
for _, name in ipairs({ "util", "config", "loader", "plugin", "handler" }) do
local modname = "lazy.core." .. name
---@diagnostic disable-next-line: no-unknown
package.preload[modname] = function()
return M.load(modname, root .. "/core/" .. name:gsub("%.", "/") .. ".lua")
end
end
return M
end
---@return CacheHash?
function M.hash(file)
return uv.fs_stat(file)
end
---@param h1 CacheHash
---@param h2 CacheHash
function M.eq(h1, h2)
return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec
end
function M.save_cache()
local f = assert(uv.fs_open(cache_path, "w", 438))
vim.loop.fs_ftruncate(f, 0)
for modname, entry in pairs(M.cache) do
if entry.used then
entry.modname = modname
local header = { entry.hash.size, entry.hash.mtime.sec, entry.hash.mtime.nsec, #modname, #entry.chunk }
uv.fs_write(f, ffi.string(ffi.new("const uint32_t[5]", header), 20))
uv.fs_write(f, modname)
uv.fs_write(f, entry.chunk)
end
end
uv.fs_close(f)
end
function M.load_cache()
M.cache = {}
local f = uv.fs_open(cache_path, "r", 438)
if f then
cache_hash = uv.fs_fstat(f) --[[@as CacheHash]]
local data = uv.fs_read(f, cache_hash.size, 0) --[[@as string]]
uv.fs_close(f)
local offset = 1
while offset + 1 < #data do
local header = ffi.cast("uint32_t*", ffi.new("const char[20]", data:sub(offset, offset + 19)))
offset = offset + 20
local modname = data:sub(offset, offset + header[3] - 1)
offset = offset + header[3]
local chunk = data:sub(offset, offset + header[4] - 1)
offset = offset + header[4]
M.cache[modname] = { hash = { size = header[0], mtime = { sec = header[1], nsec = header[2] } }, chunk = chunk }
end
end
end
function M.autosave()
vim.api.nvim_create_autocmd("VimLeavePre", {
callback = function()
if M.dirty then
local hash = M.hash(cache_path)
-- abort when the file was changed in the meantime
if hash == nil or M.eq(cache_hash, hash) then
M.save_cache()
end
end
end,
})
end
return M

View file

@ -1,484 +1,230 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Meta = require("lazy.core.meta")
local Pkg = require("lazy.pkg")
local Util = require("lazy.core.util") local Util = require("lazy.core.util")
local Module = require("lazy.core.module")
local Handler = require("lazy.core.handler")
---@class LazyCorePlugin
local M = {} local M = {}
M.loading = false
---@class LazyPluginHooks
---@field init? fun(LazyPlugin) Will always be run
---@field config? fun(LazyPlugin) Will be executed when loading the plugin
---@field run? string|fun()
---@class LazyPluginState
---@field loaded? {[string]:string, time:number}
---@field installed boolean
---@field tasks? LazyTask[]
---@field dirty? boolean
---@field updated? {from:string, to:string}
---@field is_local boolean
---@field is_symlink? boolean
---@field cloned? boolean
---@class LazyPluginRef
---@field branch? string
---@field tag? string
---@field commit? string
---@field version? string
---@field pin? boolean
---@class LazyPlugin: LazyPluginHandlers,LazyPluginHooks,LazyPluginRef
---@field [1] string
---@field name string display name and name used for plugin config files
---@field uri string
---@field dir string
---@field dep? boolean True if this plugin is only in the spec as a dependency
---@field enabled? boolean|(fun():boolean)
---@field opt? boolean
---@field dependencies? string[]
---@field _ LazyPluginState
---@alias LazySpec string|LazyPlugin|LazySpec[]|{dependencies:LazySpec}
---@class LazySpecLoader ---@class LazySpecLoader
---@field meta LazyMeta
---@field plugins table<string, LazyPlugin> ---@field plugins table<string, LazyPlugin>
---@field disabled table<string, LazyPlugin>
---@field ignore_installed table<string, true>
---@field modules string[]
---@field notifs {msg:string, level:number, file?:string}[]
---@field importing? string
---@field optional? boolean
local Spec = {} local Spec = {}
M.Spec = Spec M.Spec = Spec
M.LOCAL_SPEC = ".lazy.lua"
---@param spec? LazySpec ---@param spec? LazySpec
---@param opts? {optional?:boolean, pkg?:boolean} function Spec.new(spec)
function Spec.new(spec, opts) local self = setmetatable({}, { __index = Spec })
local self = setmetatable({}, Spec) self.plugins = {}
self.meta = Meta.new(self)
self.disabled = {}
self.modules = {}
self.notifs = {}
self.ignore_installed = {}
self.optional = opts and opts.optional
if not (opts and opts.pkg == false) then
self.meta:load_pkgs()
end
if spec then if spec then
self:parse(spec) self:normalize(spec)
end end
return self return self
end end
function Spec:__index(key) ---@param plugin LazyPlugin
if Spec[key] then ---@param is_dep? boolean
return Spec[key] function Spec:add(plugin, is_dep)
local pkg = plugin[1]
if type(pkg) ~= "string" then
Util.error("Invalid plugin spec " .. vim.inspect(plugin))
end end
if key == "plugins" then
self.meta:rebuild()
return self.meta.plugins
end
end
function Spec:parse(spec) if not plugin.uri then
self:normalize(spec) local c = pkg:sub(1, 1)
self.meta:resolve() if c == "~" then
end plugin.uri = vim.loop.os_getenv("HOME") .. pkg:sub(2)
elseif c == "/" then
-- PERF: optimized code to get package name without using lua patterns plugin.uri = pkg
---@return string elseif pkg:sub(1, 4) == "http" or pkg:sub(1, 3) == "ssh" then
function Spec.get_name(pkg) plugin.uri = pkg
local name = pkg:sub(-4) == ".git" and pkg:sub(1, -5) or pkg else
name = name:sub(-1) == "/" and name:sub(1, -2) or name plugin.uri = ("https://github.com/" .. pkg .. ".git")
local slash = name:reverse():find("/", 1, true) --[[@as number?]]
return slash and name:sub(#name - slash + 2) or pkg:gsub("%W+", "_")
end
function Spec:error(msg)
self:log(msg, vim.log.levels.ERROR)
end
function Spec:warn(msg)
self:log(msg, vim.log.levels.WARN)
end
---@param msg string
---@param level number
function Spec:log(msg, level)
self.notifs[#self.notifs + 1] = { msg = msg, level = level, file = self.importing }
end
function Spec:report(level)
level = level or vim.log.levels.ERROR
local count = 0
for _, notif in ipairs(self.notifs) do
if notif.level >= level then
Util.notify(notif.msg, { level = notif.level })
count = count + 1
end end
end end
return count
-- PERF: optimized code to get package name without using lua patterns
if not plugin.name then
local name = pkg:sub(-4) == ".git" and pkg:sub(1, -5) or pkg
local slash = name:reverse():find("/", 1, true) --[[@as number?]]
plugin.name = slash and name:sub(#name - slash + 2) or pkg:gsub("%W+", "_")
end
plugin.dep = is_dep
-- check for plugins that should be local
for _, pattern in ipairs(Config.options.dev.patterns) do
if plugin[1]:find(pattern, 1, true) then
plugin.uri = Config.options.dev.path .. "/" .. plugin.name
break
end
end
local other = self.plugins[plugin.name]
self.plugins[plugin.name] = other and self:merge(other, plugin) or plugin
return self.plugins[plugin.name]
end end
---@param spec LazySpec|LazySpecImport ---@param spec LazySpec
function Spec:normalize(spec) ---@param results? string[]
---@param is_dep? boolean
function Spec:normalize(spec, results, is_dep)
results = results or {}
if type(spec) == "string" then if type(spec) == "string" then
self.meta:add({ spec }) table.insert(results, self:add({ spec }, is_dep).name)
elseif #spec > 1 or Util.is_list(spec) then elseif #spec > 1 or Util.is_list(spec) then
---@cast spec LazySpec[] ---@cast spec LazySpec[]
for _, s in ipairs(spec) do for _, s in ipairs(spec) do
self:normalize(s) self:normalize(s, results, is_dep)
end end
elseif spec[1] or spec.dir or spec.url then elseif spec.enabled == nil or spec.enabled == true or (type(spec.enabled) == "function" and spec.enabled()) then
---@cast spec LazyPluginSpec ---@cast spec LazyPlugin
self.meta:add(spec) local plugin = self:add(spec, is_dep)
---@diagnostic disable-next-line: cast-type-mismatch plugin.dependencies = plugin.dependencies and self:normalize(plugin.dependencies, {}, true) or nil
---@cast spec LazySpecImport table.insert(results, plugin.name)
if spec and spec.import then
self:import(spec)
end
elseif spec.import then
---@cast spec LazySpecImport
self:import(spec)
else
self:error("Invalid plugin spec " .. vim.inspect(spec))
end end
return results
end end
---@param spec LazySpecImport ---@param old LazyPlugin
function Spec:import(spec) ---@param new LazyPlugin
if spec.import == "lazy" then ---@return LazyPlugin
return self:error("You can't name your plugins module `lazy`.") function Spec:merge(old, new)
end local is_dep = old.dep and new.dep
if type(spec.import) == "function" then
if not spec.name then
return self:error("Invalid import spec. Missing name: " .. vim.inspect(spec))
end
elseif type(spec.import) ~= "string" then
return self:error("Invalid import spec. `import` should be a string: " .. vim.inspect(spec))
end
local import_name = spec.name or spec.import ---@diagnostic disable-next-line: no-unknown
---@cast import_name string for k, v in pairs(new) do
if k == "dep" then
if vim.tbl_contains(self.modules, import_name) then elseif old[k] ~= nil and old[k] ~= v then
return if Handler.handlers[k] then
end local values = type(v) == "string" and { v } or v
if spec.cond == false or (type(spec.cond) == "function" and not spec.cond()) then vim.list_extend(values, type(old[k]) == "string" and { old[k] } or old[k])
return ---@diagnostic disable-next-line: no-unknown
end old[k] = values
if spec.enabled == false or (type(spec.enabled) == "function" and not spec.enabled()) then
return
end
self.modules[#self.modules + 1] = import_name
local import = spec.import
local imported = 0
---@type {modname: string, load: fun():(LazyPluginSpec?, string?)}[]
local modspecs = {}
if type(import) == "string" then
Util.lsmod(import, function(modname, modpath)
modspecs[#modspecs + 1] = {
modname = modname,
load = function()
local mod, err = loadfile(modpath)
if mod then
return mod()
else
return nil, err
end
end,
}
end)
table.sort(modspecs, function(a, b)
return a.modname < b.modname
end)
else
modspecs = { { modname = import_name, load = spec.import } }
end
for _, modspec in ipairs(modspecs) do
imported = imported + 1
local modname = modspec.modname
Util.track({ import = modname })
self.importing = modname
-- unload the module so we get a clean slate
---@diagnostic disable-next-line: no-unknown
package.loaded[modname] = nil
Util.try(function()
local mod, err = modspec.load()
if err then
self:error("Failed to load `" .. modname .. "`:\n" .. err)
elseif type(mod) ~= "table" then
return self:error(
"Invalid spec module: `"
.. modname
.. "`\nExpected a `table` of specs, but a `"
.. type(mod)
.. "` was returned instead"
)
else else
self:normalize(mod) error("Merging plugins is not supported for key `" .. k .. "`")
end end
end, { else
msg = "Failed to load `" .. modname .. "`", ---@diagnostic disable-next-line: no-unknown
on_error = function(msg) old[k] = v
self:error(msg) end
end,
})
self.importing = nil
Util.track()
end
if imported == 0 then
self:error("No specs found for module " .. vim.inspect(spec.import))
end end
old.dep = is_dep
return old
end end
function M.update_state() function M.update_state()
---@type string[] ---@type table<"opt"|"start", table<string,FileType>>
local cloning = {} local installed = { opt = {}, start = {} }
for opt, packs in pairs(installed) do
---@type table<string,FileType> Util.ls(Config.options.packpath .. "/" .. opt, function(_, name, type)
local installed = {} if type == "directory" or type == "link" then
Util.ls(Config.options.root, function(_, name, type) packs[name] = type
if type == "directory" and name ~= "readme" then end
installed[name] = type end)
elseif type == "file" and name:sub(-8) == ".cloning" then
name = name:sub(1, -9)
cloning[#cloning + 1] = name
end
end)
for _, failed in ipairs(cloning) do
installed[failed] = nil
end end
for _, plugin in pairs(Config.plugins) do for _, plugin in pairs(Config.plugins) do
plugin._ = plugin._ or {} plugin._ = plugin._ or {}
if plugin.lazy == nil then if plugin.opt == nil then
local lazy = plugin._.dep local opt = plugin.dep
or Config.options.defaults.lazy or Config.options.defaults.opt
or plugin.module
or plugin.event or plugin.event
or plugin.keys or plugin.keys
or plugin.ft or plugin.ft
or plugin.cmd or plugin.cmd
plugin.lazy = lazy and true or false plugin.opt = opt and true or false
end end
if plugin.virtual then local opt = plugin.opt and "opt" or "start"
plugin._.is_local = true plugin.dir = Config.options.packpath .. "/" .. opt .. "/" .. plugin.name
plugin._.installed = true -- local plugins are managed by the user plugin._.is_local = plugin.uri:sub(1, 4) ~= "http" and plugin.uri:sub(1, 3) ~= "git"
elseif plugin.dir:find(Config.options.root, 1, true) == 1 then plugin._.is_symlink = installed[opt][plugin.name] == "link"
plugin._.installed = installed[plugin.name] ~= nil plugin._.installed = installed[opt][plugin.name] ~= nil
installed[plugin.name] = nil if plugin._.is_local == plugin._.is_symlink then
else installed[opt][plugin.name] = nil
plugin._.is_local = true
plugin._.installed = vim.fn.isdirectory(plugin.dir) == 1
end end
end end
for name in pairs(Config.spec.ignore_installed) do
installed[name] = nil
end
M.update_rocks_state()
Config.to_clean = {} Config.to_clean = {}
for pack, dir_type in pairs(installed) do for opt, packs in pairs(installed) do
table.insert(Config.to_clean, { for pack, dir_type in pairs(packs) do
name = pack, table.insert(Config.to_clean, {
dir = Config.options.root .. "/" .. pack, name = pack,
_ = { dir = Config.options.packpath .. "/" .. opt .. "/" .. pack,
kind = "clean", opt = opt == "opt",
installed = true, _ = {
is_symlink = dir_type == "link", installed = true,
is_local = dir_type == "link", is_symlink = dir_type == "link",
}, is_local = dir_type == "link",
}) },
end })
end
function M.update_rocks_state()
local root = Config.options.rocks.root
---@type table<string,string>
local installed = {}
Util.ls(root, function(_, name, type)
if type == "directory" then
installed[name] = name
end
end)
for _, plugin in pairs(Config.plugins) do
if plugin.build == "rockspec" or plugin.name == "hererocks" then
plugin._.build = not installed[plugin.name]
end end
end end
end end
---@return LazySpecImport? function M.spec()
function M.find_local_spec() local spec = Spec.new()
if not Config.options.local_spec then
return local function _load(name, modpath)
end local modname = Config.options.plugins .. (name and ("." .. name) or "")
local path = vim.uv.cwd() Util.try(function()
while path and path ~= "" do local mod = Module.load(modname, modpath)
local file = path .. "/" .. M.LOCAL_SPEC spec:normalize(assert(mod))
if vim.fn.filereadable(file) == 1 then end, "Failed to load **" .. modname .. "**")
return {
name = vim.fn.fnamemodify(file, ":~:."),
import = function()
local data = vim.secure.read(file)
if data then
return loadstring(data, M.LOCAL_SPEC)()
end
return {}
end,
}
end
local p = vim.fn.fnamemodify(path, ":h")
if p == path then
break
end
path = p
end end
_load(nil, Config.paths.main)
Util.lsmod(Config.paths.plugins, _load)
return spec
end end
function M.load() function M.load()
M.loading = true
-- load specs -- load specs
Util.track("spec") Util.track("spec")
Config.spec = Spec.new() local spec = M.spec()
if not spec.plugins["lazy.nvim"] then
local specs = { spec:add({ "folke/lazy.nvim", opt = false })
---@diagnostic disable-next-line: param-type-mismatch
vim.deepcopy(Config.options.spec),
}
specs[#specs + 1] = M.find_local_spec()
specs[#specs + 1] = { "folke/lazy.nvim" }
Config.spec:parse(specs)
-- override some lazy props
local lazy = Config.spec.plugins["lazy.nvim"]
if lazy then
lazy.lazy = true
lazy.dir = Config.me
lazy.config = function()
error("lazy config should not be called")
end
lazy._.loaded = {}
end
-- add hererocks when enabled and needed
for _, plugin in pairs(Config.spec.plugins) do
if plugin.build == "rockspec" then
if Config.hererocks() then
Config.spec.meta:add({
"luarocks/hererocks",
build = "rockspec",
lazy = true,
})
end
break
end
end
local existing = Config.plugins
Config.plugins = Config.spec.plugins
-- copy state. This wont do anything during startup
for name, plugin in pairs(existing) do
if Config.plugins[name] then
local new_state = Config.plugins[name]._
Config.plugins[name]._ = plugin._
Config.plugins[name]._.dep = new_state.dep
Config.plugins[name]._.frags = new_state.frags
Config.plugins[name]._.pkg = new_state.pkg
end
end end
Config.plugins = spec.plugins
Util.track() Util.track()
Util.track("state") Util.track("state")
M.update_state() M.update_state()
Util.track() Util.track()
if Config.options.pkg.enabled and Pkg.dirty then
Pkg.update()
return M.load()
end
M.loading = false
vim.api.nvim_exec_autocmds("User", { pattern = "LazyPlugins", modeline = false })
end
-- Finds the plugin that has this path
---@param path string
---@param opts? {fast?:boolean}
function M.find(path, opts)
if not Config.spec then
return
end
opts = opts or {}
local lua = path:find("/lua/", 1, true)
if lua then
local name = path:sub(1, lua - 1)
local slash = name:reverse():find("/", 1, true)
if slash then
name = name:sub(#name - slash + 2)
if name then
if opts.fast then
return Config.spec.meta.plugins[name]
end
return Config.spec.plugins[name]
end
end
end
end
---@param plugin LazyPlugin
function M.has_errors(plugin)
for _, task in ipairs(plugin._.tasks or {}) do
if task:has_errors() then
return true
end
end
return false
end
-- Merges super values or runs the values function to override values or return new ones.
-- Values are cached for performance.
-- Used for opts, cmd, event, ft and keys
---@param plugin LazyPlugin
---@param prop string
---@param is_list? boolean
function M.values(plugin, prop, is_list)
if not plugin[prop] then
return {}
end
plugin._.cache = plugin._.cache or {}
local key = prop .. (is_list and "_list" or "")
if plugin._.cache[key] == nil then
plugin._.cache[key] = M._values(plugin, plugin, prop, is_list)
end
return plugin._.cache[key]
end
-- Merges super values or runs the values function to override values or return new ones
-- Used for opts, cmd, event, ft and keys
---@param root LazyPlugin
---@param plugin LazyPlugin
---@param prop string
---@param is_list? boolean
function M._values(root, plugin, prop, is_list)
if not plugin[prop] then
return {}
end
local super = getmetatable(plugin)
---@type table
local ret = super and M._values(root, super.__index, prop, is_list) or {}
local values = rawget(plugin, prop)
if not values then
return ret
elseif type(values) == "function" then
ret = values(root, ret) or ret
return type(ret) == "table" and ret or { ret }
end
values = type(values) == "table" and values or { values }
if is_list then
return Util.extend(ret, values)
else
---@type {path:string[], list:any[]}[]
local lists = {}
---@diagnostic disable-next-line: no-unknown
for _, key in ipairs(plugin[prop .. "_extend"] or {}) do
local path = vim.split(key, ".", { plain = true })
local r = Util.key_get(ret, path)
local v = Util.key_get(values, path)
if type(r) == "table" and type(v) == "table" then
lists[key] = { path = path, list = {} }
vim.list_extend(lists[key].list, r)
vim.list_extend(lists[key].list, v)
end
end
local t = Util.merge(ret, values)
for _, list in pairs(lists) do
Util.key_set(t, list.path, list.list)
end
return t
end
end end
return M return M

View file

@ -1,11 +1,9 @@
---@class LazyUtilCore
local M = {} local M = {}
---@alias LazyProfile {data: string|{[string]:string}, time: number, [number]:LazyProfile} ---@alias LazyProfile {data: string|{[string]:string}, time: number, [number]:LazyProfile}
---@type LazyProfile[] ---@type LazyProfile[]
M._profiles = { { name = "lazy" } } M._profiles = { { name = "lazy" } }
M.is_win = jit.os:find("Windows")
---@param data (string|{[string]:string})? ---@param data (string|{[string]:string})?
---@param time number? ---@param time number?
@ -13,7 +11,7 @@ function M.track(data, time)
if data then if data then
local entry = { local entry = {
data = data, data = data,
time = time or vim.uv.hrtime(), time = time or vim.loop.hrtime(),
} }
table.insert(M._profiles[#M._profiles], entry) table.insert(M._profiles[#M._profiles], entry)
@ -24,110 +22,43 @@ function M.track(data, time)
else else
---@type LazyProfile ---@type LazyProfile
local entry = table.remove(M._profiles) local entry = table.remove(M._profiles)
entry.time = vim.uv.hrtime() - entry.time entry.time = vim.loop.hrtime() - entry.time
return entry return entry
end end
end end
function M.exiting() function M.try(fn, msg)
return vim.v.exiting ~= vim.NIL
end
---@generic T
---@param list T[]
---@param fn fun(v: T):boolean?
---@return T[]
function M.filter(fn, list)
local ret = {}
for _, v in ipairs(list) do
if fn(v) then
table.insert(ret, v)
end
end
return ret
end
---@generic F: fun()
---@param data (string|{[string]:string})?
---@param fn F
---@return F
function M.trackfn(data, fn)
return function(...)
M.track(data)
local ok, ret = pcall(fn, ...)
M.track()
if not ok then
error(ret)
end
return ret
end
end
---@param name string
---@return string
function M.normname(name)
local ret = name:lower():gsub("^n?vim%-", ""):gsub("%.n?vim$", ""):gsub("[%.%-]lua", ""):gsub("[^a-z]+", "")
return ret
end
---@return string
function M.norm(path)
if path:sub(1, 1) == "~" then
local home = vim.uv.os_homedir()
if home:sub(-1) == "\\" or home:sub(-1) == "/" then
home = home:sub(1, -2)
end
path = home .. path:sub(2)
end
path = path:gsub("\\", "/"):gsub("/+", "/")
return path:sub(-1) == "/" and path:sub(1, -2) or path
end
---@param opts? {level?: number}
function M.pretty_trace(opts)
opts = opts or {}
local Config = require("lazy.core.config")
local trace = {}
local level = opts.level or 2
while true do
local info = debug.getinfo(level, "Sln")
if not info then
break
end
if info.what ~= "C" and (Config.options.debug or not info.source:find("lazy.nvim")) then
local source = info.source:sub(2)
if source:find(Config.options.root, 1, true) == 1 then
source = source:sub(#Config.options.root + 1)
end
source = vim.fn.fnamemodify(source, ":p:~:.") --[[@as string]]
local line = " - " .. source .. ":" .. info.currentline
if info.name then
line = line .. " _in_ **" .. info.name .. "**"
end
table.insert(trace, line)
end
level = level + 1
end
return #trace > 0 and ("\n\n# stacktrace:\n" .. table.concat(trace, "\n")) or ""
end
---@generic R
---@param fn fun():R?
---@param opts? string|{msg:string, on_error:fun(msg)}
---@return R
function M.try(fn, opts)
opts = type(opts) == "string" and { msg = opts } or opts or {}
local msg = opts.msg
-- error handler -- error handler
local error_handler = function(err) local error_handler = function(err)
msg = (msg and (msg .. "\n\n") or "") .. err .. M.pretty_trace() local Config = require("lazy.core.config")
if opts.on_error then local trace = {}
opts.on_error(msg) local level = 1
else while true do
vim.schedule(function() local info = debug.getinfo(level, "Sln")
M.error(msg) if not info then
end) break
end
if info.what == "Lua" and not info.source:find("lazy.nvim") then
local source = info.source:sub(2)
if source:find(Config.options.packpath, 1, true) == 1 then
source = source:sub(#Config.options.packpath + 1):gsub("^/opt/", ""):gsub("^/start/", "")
end
source = vim.fn.fnamemodify(source, ":p:~:.")
local line = " - " .. source .. ":" .. info.currentline
if info.name then
line = line .. " _in_ **" .. info.name .. "**"
end
table.insert(trace, line)
end
level = level + 1
end end
msg = msg .. "\n\n" .. err
if #trace > 0 then
msg = msg .. "\n\n# stacktrace:\n" .. table.concat(trace, "\n")
end
vim.schedule(function()
M.error(msg)
end)
return err return err
end end
@ -136,20 +67,6 @@ function M.try(fn, opts)
return ok and result or nil return ok and result or nil
end end
function M.get_source()
local f = 2
while true do
local info = debug.getinfo(f, "S")
if not info then
break
end
if info.what ~= "C" and not info.source:find("lazy.nvim", 1, true) and info.source ~= "@vim/loader.lua" then
return info.source:sub(2)
end
f = f + 1
end
end
-- Fast implementation to check if a table is a list -- Fast implementation to check if a table is a list
---@param t table ---@param t table
function M.is_list(t) function M.is_list(t)
@ -166,15 +83,9 @@ end
function M.very_lazy() function M.very_lazy()
local function _load() local function _load()
vim.schedule(function() vim.defer_fn(function()
if vim.v.exiting ~= vim.NIL then vim.cmd("do User VeryLazy")
return end, 100)
end
vim.g.did_very_lazy = true
M.track({ event = "VeryLazy" })
vim.api.nvim_exec_autocmds("User", { pattern = "VeryLazy", modeline = false })
M.track()
end)
end end
vim.api.nvim_create_autocmd("User", { vim.api.nvim_create_autocmd("User", {
@ -184,7 +95,7 @@ function M.very_lazy()
if vim.v.vim_did_enter == 1 then if vim.v.vim_did_enter == 1 then
_load() _load()
else else
vim.api.nvim_create_autocmd("UIEnter", { vim.api.nvim_create_autocmd("VimEnter", {
once = true, once = true,
callback = function() callback = function()
_load() _load()
@ -199,19 +110,13 @@ end
---@param path string ---@param path string
---@param fn fun(path: string, name:string, type:FileType):boolean? ---@param fn fun(path: string, name:string, type:FileType):boolean?
function M.ls(path, fn) function M.ls(path, fn)
local handle = vim.uv.fs_scandir(path) local handle = vim.loop.fs_scandir(path)
while handle do while handle do
local name, t = vim.uv.fs_scandir_next(handle) local name, t = vim.loop.fs_scandir_next(handle)
if not name then if not name then
break break
end end
if fn(path .. "/" .. name, name, t) == false then
local fname = path .. "/" .. name
-- HACK: type is not always returned due to a bug in luv,
-- so fecth it with fs_stat instead when needed.
-- see https://github.com/folke/lazy.nvim/issues/306
if fn(fname, name, t or vim.uv.fs_stat(fname).type) == false then
break break
end end
end end
@ -230,287 +135,35 @@ end
---@param root string ---@param root string
---@param fn fun(modname:string, modpath:string) ---@param fn fun(modname:string, modpath:string)
---@param modname? string function M.lsmod(root, fn)
function M.walkmods(root, fn, modname)
modname = modname and (modname:gsub("%.$", "") .. ".") or ""
M.ls(root, function(path, name, type) M.ls(root, function(path, name, type)
if name == "init.lua" then if type == "file" and name:sub(-4) == ".lua" and name ~= "init.lua" then
fn(modname:gsub("%.$", ""), path) fn(name:sub(1, -5), path)
elseif (type == "file" or type == "link") and name:sub(-4) == ".lua" then elseif type == "directory" and vim.loop.fs_stat(path .. "/init.lua") then
fn(modname .. name:sub(1, -5), path) fn(name, path .. "/init.lua")
elseif type == "directory" then
M.walkmods(path, fn, modname .. name .. ".")
end end
end) end)
end end
---@param modname string function M.notify(msg, level)
---@return string vim.notify(msg, level, {
function M.topmod(modname)
return modname:match("^[^./]+") or modname
end
---@type table<string, string[]>
M.unloaded_cache = {}
---@param modname string
---@param opts? {cache?:boolean}
function M.get_unloaded_rtp(modname, opts)
opts = opts or {}
local topmod = M.topmod(modname)
if opts.cache and M.unloaded_cache[topmod] then
return M.unloaded_cache[topmod], true
end
local norm = M.normname(topmod)
---@type string[]
local rtp = {}
local Config = require("lazy.core.config")
if Config.spec then
for _, plugin in pairs(Config.spec.plugins) do
if not (plugin._.loaded or plugin.module == false or plugin.virtual) then
if norm == M.normname(plugin.name) then
table.insert(rtp, 1, plugin.dir)
else
table.insert(rtp, plugin.dir)
end
end
end
end
M.unloaded_cache[topmod] = rtp
return rtp, false
end
function M.find_root(modname)
local paths, cached = M.get_unloaded_rtp(modname, { cache = true })
local ret = require("lazy.core.cache").find(modname, {
rtp = true,
paths = paths,
patterns = { ".lua", "" },
})[1]
if not ret and cached then
paths = M.get_unloaded_rtp(modname)
ret = require("lazy.core.cache").find(modname, {
rtp = false,
paths = paths,
patterns = { ".lua", "" },
})[1]
end
if ret then
return ret.modpath:gsub("%.lua$", ""), ret.modpath
end
end
---@param modname string
---@param fn fun(modname:string, modpath:string)
function M.lsmod(modname, fn)
local root, match = M.find_root(modname)
if not root then
return
end
if match:sub(-4) == ".lua" then
fn(modname, match)
if not vim.uv.fs_stat(root) then
return
end
end
M.ls(root, function(path, name, type)
if name == "init.lua" then
fn(modname, path)
elseif (type == "file" or type == "link") and name:sub(-4) == ".lua" then
fn(modname .. "." .. name:sub(1, -5), path)
elseif type == "directory" and vim.uv.fs_stat(path .. "/init.lua") then
fn(modname .. "." .. name, path .. "/init.lua")
end
end)
end
---@generic T
---@param list T[]
---@param add T[]
---@return T[]
function M.extend(list, add)
local idx = {}
for _, v in ipairs(list) do
idx[v] = v
end
for _, a in ipairs(add) do
if not idx[a] then
table.insert(list, a)
end
end
return list
end
---@alias LazyNotifyOpts {lang?:string, title?:string, level?:number, once?:boolean, stacktrace?:boolean, stacklevel?:number}
---@param msg string|string[]
---@param opts? LazyNotifyOpts
function M.notify(msg, opts)
if vim.in_fast_event() then
return vim.schedule(function()
M.notify(msg, opts)
end)
end
opts = opts or {}
if type(msg) == "table" then
msg = table.concat(
vim.tbl_filter(function(line)
return line or false
end, msg),
"\n"
)
end
if opts.stacktrace then
msg = msg .. M.pretty_trace({ level = opts.stacklevel or 2 })
end
local lang = opts.lang or "markdown"
local n = opts.once and vim.notify_once or vim.notify
n(msg, opts.level or vim.log.levels.INFO, {
ft = lang,
on_open = function(win) on_open = function(win)
local ok = pcall(function()
vim.treesitter.language.add("markdown")
end)
if not ok then
pcall(require, "nvim-treesitter")
end
vim.wo[win].conceallevel = 3 vim.wo[win].conceallevel = 3
vim.wo[win].concealcursor = "" vim.wo[win].concealcursor = ""
vim.wo[win].spell = false vim.wo[win].spell = false
local buf = vim.api.nvim_win_get_buf(win) local buf = vim.api.nvim_win_get_buf(win)
if not pcall(vim.treesitter.start, buf, lang) then vim.bo[buf].filetype = "markdown"
vim.bo[buf].filetype = lang
vim.bo[buf].syntax = lang
end
end, end,
title = opts.title or "lazy.nvim", title = "lazy.nvim",
}) })
end end
---@param msg string|string[] function M.error(msg)
---@param opts? LazyNotifyOpts M.notify(msg, vim.log.levels.ERROR)
function M.error(msg, opts)
opts = opts or {}
opts.level = vim.log.levels.ERROR
M.notify(msg, opts)
end end
---@param msg string|string[] function M.info(msg)
---@param opts? LazyNotifyOpts M.notify(msg, vim.log.levels.INFO)
function M.info(msg, opts)
opts = opts or {}
opts.level = vim.log.levels.INFO
M.notify(msg, opts)
end
---@param msg string|string[]
---@param opts? LazyNotifyOpts
function M.warn(msg, opts)
opts = opts or {}
opts.level = vim.log.levels.WARN
M.notify(msg, opts)
end
---@param msg string|table
---@param opts? LazyNotifyOpts
function M.debug(msg, opts)
if not require("lazy.core.config").options.debug then
return
end
opts = opts or {}
if opts.title then
opts.title = "lazy.nvim: " .. opts.title
end
if type(msg) == "string" then
M.notify(msg, opts)
else
opts.lang = "lua"
M.notify(vim.inspect(msg), opts)
end
end
local function can_merge(v)
return type(v) == "table" and (vim.tbl_isempty(v) or not M.is_list(v))
end
--- Merges the values similar to vim.tbl_deep_extend with the **force** behavior,
--- but the values can be any type, in which case they override the values on the left.
--- Values will me merged in-place in the first left-most table. If you want the result to be in
--- a new table, then simply pass an empty table as the first argument `vim.merge({}, ...)`
--- Supports clearing values by setting a key to `vim.NIL`
---@generic T
---@param ... T
---@return T
function M.merge(...)
local ret = select(1, ...)
if ret == vim.NIL then
ret = nil
end
for i = 2, select("#", ...) do
local value = select(i, ...)
if can_merge(ret) and can_merge(value) then
for k, v in pairs(value) do
ret[k] = M.merge(ret[k], v)
end
elseif value == vim.NIL then
ret = nil
elseif value ~= nil then
ret = value
end
end
return ret
end
function M.lazy_require(module)
local mod = nil
-- if already loaded, return the module
-- otherwise return a lazy module
return type(package.loaded[module]) == "table" and package.loaded[module]
or setmetatable({}, {
__index = function(_, key)
mod = mod or require(module)
return mod[key]
end,
})
end
---@param t table
---@param key string|string[]
---@return any
function M.key_get(t, key)
local path = type(key) == "table" and key or vim.split(key, ".", true)
local value = t
for _, k in ipairs(path) do
if type(value) ~= "table" then
return value
end
value = value[k]
end
return value
end
---@param t table
---@param key string|string[]
---@param value any
function M.key_set(t, key, value)
local path = type(key) == "table" and key or vim.split(key, ".", true)
local last = t
for i = 1, #path - 1 do
local k = path[i]
if type(last[k]) ~= "table" then
last[k] = {}
end
last = last[k]
end
last[path[#path]] = value
end end
return M return M

View file

@ -1,167 +0,0 @@
local Util = require("lazy.util")
local M = {}
function M.indent(str, indent)
local lines = vim.split(str, "\n")
for l, line in ipairs(lines) do
lines[l] = (" "):rep(indent) .. line
end
return table.concat(lines, "\n")
end
---@param str string
function M.fix_indent(str)
local lines = vim.split(str, "\n")
local first = table.remove(lines, 1)
local width = 120
for _, line in ipairs(lines) do
if not line:find("^%s*$") then
width = math.min(width, #line:match("^%s*"))
end
end
for l, line in ipairs(lines) do
lines[l] = line:sub(width + 1)
end
table.insert(lines, 1, first)
return table.concat(lines, "\n")
end
---@alias ReadmeBlock {content:string, lang?:string}
---@param contents table<string, ReadmeBlock|string>
---@param readme_file? string
function M.save(contents, readme_file)
local readme = Util.read_file(readme_file or "README.md")
for tag, block in pairs(contents) do
if type(block) == "string" then
block = { content = block, lang = "lua" }
end
---@cast block ReadmeBlock
local content = M.fix_indent(block.content)
content = content:gsub("%%", "%%%%")
content = vim.trim(content)
local pattern = "(<%!%-%- " .. tag .. ":start %-%->).*(<%!%-%- " .. tag .. ":end %-%->)"
if not readme:find(pattern) then
error("tag " .. tag .. " not found")
end
if block.lang then
readme = readme:gsub(pattern, "%1\n\n```" .. block.lang .. "\n" .. content .. "\n```\n\n%2")
else
readme = readme:gsub(pattern, "%1\n\n" .. content .. "\n\n%2")
end
end
Util.write_file(readme_file or "README.md", readme)
vim.cmd.checktime()
end
---@return string
function M.extract(file, pattern)
local init = Util.read_file(file)
return assert(init:match(pattern))
end
---@return ReadmeBlock
function M.commands()
local commands = require("lazy.view.commands").commands
local modes = require("lazy.view.config").commands
modes.load.opts = true
local lines = {
{ "Command", "Lua", "Description" },
{ "---", "---", "---" },
}
Util.foreach(modes, function(name, mode)
if commands[name] then
if mode.plugins_required then
lines[#lines + 1] = {
("`:Lazy %s {plugins}`"):format(name),
([[`require("lazy").%s(opts)`]]):format(name),
mode.desc,
}
elseif mode.plugins then
lines[#lines + 1] = {
("`:Lazy %s [plugins]`"):format(name),
([[`require("lazy").%s(opts?)`]]):format(name),
mode.desc,
}
else
lines[#lines + 1] = {
("`:Lazy %s`"):format(name),
([[`require("lazy").%s()`]]):format(name),
mode.desc,
}
end
end
end)
return { content = M.table(lines) }
end
---@param lines string[][]
function M.table(lines)
---@type string[]
local ret = {}
for _, line in ipairs(lines) do
ret[#ret + 1] = "| " .. table.concat(line, " | ") .. " |"
end
return table.concat(ret, "\n")
end
---@param opts? {name?:string, path?:string, modname?:string}
---@return ReadmeBlock
function M.colors(opts)
opts = vim.tbl_extend("force", {
name = "Lazy",
path = "lua/lazy/view/colors.lua",
modname = "lazy.view.colors",
}, opts or {})
local str = M.extract(opts.path, "\nM%.colors = ({.-\n})")
---@type table<string,string>
local comments = {}
for _, line in ipairs(vim.split(str, "\n")) do
local group, desc = line:match("^ (%w+) = .* -- (.*)")
if group then
comments[group] = desc
end
end
local lines = {
{ "Highlight Group", "Default Group", "Description" },
{ "---", "---", "---" },
}
Util.foreach(require(opts.modname).colors, function(group, link)
link = type(link) == "table" and "`" .. vim.inspect(link):gsub("%s+", " ") .. "`" or "***" .. link .. "***"
lines[#lines + 1] = { "**" .. opts.name .. group .. "**", link, comments[group] or "" }
end)
return { content = M.table(lines) }
end
function M.update()
local config = M.extract("lua/lazy/core/config.lua", "\nM%.defaults = ({.-\n})")
config = config:gsub("%s*debug = false.\n", "\n")
M.save({
bootstrap = M.extract("lua/lazy/init.lua", "function M%.bootstrap%(%)\n(.-)\nend"),
stats = M.extract("lua/lazy/stats.lua", "\nM%._stats = ({.-\n})"),
config = config,
spec = Util.read_file("lua/lazy/example.lua"),
commands = M.commands(),
colors = M.colors(),
})
end
---@param plugins? LazyPlugin[]
---@return ReadmeBlock
function M.plugins(plugins)
plugins = plugins or require("lazy.core.config").plugins
---@type string[]
local lines = {}
Util.foreach(plugins, function(name, plugin)
if plugin.url then
lines[#lines + 1] = "- [" .. name .. "](" .. plugin.url:gsub("%.git$", "") .. ")"
end
end)
return { content = table.concat(lines, "\n") }
end
return M

View file

@ -1,87 +0,0 @@
return {
-- the colorscheme should be available when starting Neovim
{
"folke/tokyonight.nvim",
lazy = false, -- make sure we load this during startup if it is your main colorscheme
priority = 1000, -- make sure to load this before all the other start plugins
config = function()
-- load the colorscheme here
vim.cmd([[colorscheme tokyonight]])
end,
},
-- I have a separate config.mappings file where I require which-key.
-- With lazy the plugin will be automatically loaded when it is required somewhere
{ "folke/which-key.nvim", lazy = true },
{
"nvim-neorg/neorg",
-- lazy-load on filetype
ft = "norg",
-- options for neorg. This will automatically call `require("neorg").setup(opts)`
opts = {
load = {
["core.defaults"] = {},
},
},
},
{
"dstein64/vim-startuptime",
-- lazy-load on a command
cmd = "StartupTime",
-- init is called during startup. Configuration for vim plugins typically should be set in an init function
init = function()
vim.g.startuptime_tries = 10
end,
},
{
"hrsh7th/nvim-cmp",
-- load cmp on InsertEnter
event = "InsertEnter",
-- these dependencies will only be loaded when cmp loads
-- dependencies are always lazy-loaded unless specified otherwise
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
},
config = function()
-- ...
end,
},
-- if some code requires a module from an unloaded plugin, it will be automatically loaded.
-- So for api plugins like devicons, we can always set lazy=true
{ "nvim-tree/nvim-web-devicons", lazy = true },
-- you can use the VeryLazy event for things that can
-- load later and are not important for the initial UI
{ "stevearc/dressing.nvim", event = "VeryLazy" },
{
"Wansmer/treesj",
keys = {
{ "J", "<cmd>TSJToggle<cr>", desc = "Join Toggle" },
},
opts = { use_default_keymaps = false, max_join_length = 150 },
},
{
"monaqa/dial.nvim",
-- lazy-load on keys
-- mode is `n` by default. For more advanced options, check the section on key mappings
keys = { "<C-a>", { "<C-x>", mode = "n" } },
},
-- local plugins need to be explicitly configured with dir
{ dir = "~/projects/secret.nvim" },
-- you can use a custom url to fetch a plugin
{ url = "git@github.com:folke/noice.nvim.git" },
-- local plugins can also be configured with the dev option.
-- This will use {config.dev.path}/noice.nvim/ instead of fetching it from GitHub
-- With the dev option, you can easily switch between the local and installed version of a plugin
{ "folke/noice.nvim", dev = true },
}

View file

@ -1,217 +0,0 @@
local Config = require("lazy.core.config")
local Process = require("lazy.manage.process")
local uv = vim.uv or vim.loop
local M = {}
-- "report_" prefix has been deprecated, use the recommended replacements if they exist.
local start = vim.health.start or vim.health.report_start
local ok = vim.health.ok or vim.health.report_ok
local warn = vim.health.warn or vim.health.report_warn
local error = vim.health.error or vim.health.report_error
local info = vim.health.info or vim.health.report_info
---@class LazyHealth
---@field error? fun(msg:string)
---@field warn? fun(msg:string)
---@field ok? fun(msg:string)
---@class LazyHealthHave : LazyHealth
---@field version? string
---@field version_pattern? string
---@field optional? boolean
---@param cmd string|string[]
---@param opts? LazyHealthHave
function M.have(cmd, opts)
opts = vim.tbl_extend("force", {
error = error,
warn = warn,
ok = ok,
version = "--version",
}, opts or {})
cmd = type(cmd) == "table" and cmd or { cmd }
---@cast cmd string[]
---@type string?
local found
for _, c in ipairs(cmd) do
if vim.fn.executable(c) == 1 then
local out, exit_code = Process.exec({ c, opts.version })
if exit_code ~= 0 then
opts.error(("failed to get version of {%s}\n%s"):format(c, table.concat(out, "\n")))
else
local version = vim.trim(out[1] or "")
version = version:gsub("^%s*" .. vim.pesc(c) .. "%s*", "")
if opts.version_pattern and not version:find(opts.version_pattern, 1, true) then
opts.warn(("`%s` version `%s` needed, but found `%s`"):format(c, opts.version_pattern, version))
else
found = ("{%s} `%s`"):format(c, version)
break
end
end
end
end
if found then
opts.ok(found)
return true
else
(opts.optional and opts.warn or opts.error)(
("{%s} %snot installed"):format(
table.concat(cmd, "} or {"),
opts.version_pattern and "version `" .. opts.version_pattern .. "` " or ""
)
)
end
end
function M.check()
start("lazy.nvim")
info("{lazy.nvim} version `" .. Config.version .. "`")
M.have("git")
local sites = vim.opt.packpath:get()
local default_site = vim.fn.stdpath("data") .. "/site"
if not vim.tbl_contains(sites, default_site) then
sites[#sites + 1] = default_site
end
local existing = false
for _, site in pairs(sites) do
for _, packs in ipairs(vim.fn.expand(site .. "/pack/*", false, true)) do
if not packs:find("[/\\]dist$") and uv.fs_stat(packs) then
existing = true
warn("found existing packages at `" .. packs .. "`")
end
end
end
if not existing then
ok("no existing packages found by other package managers")
end
for _, name in ipairs({ "packer", "plugged", "paq", "pckr", "mini.deps" }) do
for _, path in ipairs(vim.opt.rtp:get()) do
if path:find(name, 1, true) then
error("Found paths on the rtp from another plugin manager `" .. name .. "`")
break
end
end
end
local packer_compiled = vim.fn.stdpath("config") .. "/plugin/packer_compiled.lua"
if uv.fs_stat(packer_compiled) then
error("please remove the file `" .. packer_compiled .. "`")
else
ok("packer_compiled.lua not found")
end
local spec = Config.spec
if spec == nil then
error('No plugins loaded. Did you forget to run `require("lazy").setup()`?')
else
for _, plugin in pairs(spec.plugins) do
M.check_valid(plugin)
end
if #spec.notifs > 0 then
error("Issues were reported when loading your specs:")
for _, notif in ipairs(spec.notifs) do
local lines = vim.split(notif.msg, "\n")
for _, line in ipairs(lines) do
if notif.level == vim.log.levels.ERROR then
error(line)
else
warn(line)
end
end
end
end
end
start("luarocks")
if Config.options.rocks.enabled then
if Config.hererocks() then
info("checking `hererocks` installation")
else
info("checking `luarocks` installation")
end
local need_luarocks = {}
for _, plugin in pairs(spec.plugins) do
if plugin.build == "rockspec" then
table.insert(need_luarocks, plugin.name)
end
end
if #need_luarocks == 0 then
ok("no plugins require `luarocks`, so you can ignore any warnings below")
else
local lines = vim.tbl_map(function(name)
return " * `" .. name .. "`"
end, need_luarocks)
info("you have some plugins that require `luarocks`:\n" .. table.concat(lines, "\n"))
end
local ok = require("lazy.pkg.rockspec").check({
error = #need_luarocks > 0 and error or warn,
warn = warn,
ok = ok,
})
if not ok then
warn(table.concat({
"Lazy won't be able to install plugins that require `luarocks`.",
"Here's what you can do:",
" - fix your `luarocks` installation",
Config.hererocks() and " - disable *hererocks* with `opts.rocks.hererocks = false`"
or " - enable `hererocks` with `opts.rocks.hererocks = true`",
" - disable `luarocks` support completely with `opts.rocks.enabled = false`",
}, "\n"))
end
else
ok("luarocks disabled")
end
end
---@param plugin LazyPlugin
function M.check_valid(plugin)
for key in pairs(plugin) do
if not vim.tbl_contains(M.valid, key) then
if key ~= "module" or type(plugin.module) ~= "boolean" then
warn("{" .. plugin.name .. "}: unknown key <" .. key .. ">")
end
end
end
end
M.valid = {
1,
"_",
"branch",
"build",
"cmd",
"commit",
"cond",
"config",
"deactivate",
"dependencies",
"dev",
"dir",
"enabled",
"event",
"ft",
"import",
"init",
"keys",
"lazy",
"main",
"module",
"name",
"optional",
"opts",
"pin",
"priority",
"submodules",
"tag",
"url",
"version",
}
return M

View file

@ -1,71 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.util")
local M = {}
function M.index(plugin)
if Config.options.readme.skip_if_doc_exists and vim.uv.fs_stat(plugin.dir .. "/doc") then
return {}
end
local files = {}
for _, file in ipairs(Config.options.readme.files) do
vim.list_extend(files, vim.fn.expand(plugin.dir .. "/" .. file, false, true))
end
---@type table<string,{file:string, tag:string, line:string}>
local tags = {}
for _, file in ipairs(files) do
file = Util.norm(file)
if vim.uv.fs_stat(file) then
local rel_file = file:sub(#plugin.dir + 1)
local tag_filename = plugin.name .. vim.fn.fnamemodify(rel_file, ":h"):gsub("%W+", "-"):gsub("^%-$", "")
local lines = vim.split(Util.read_file(file), "\n")
for _, line in ipairs(lines) do
local title = line:match("^#+%s*(.*)")
if title then
local tag = tag_filename .. "-" .. title:lower():gsub("%W+", "-")
tag = tag:gsub("%-+", "-"):gsub("%-$", "")
line = line:gsub("([%[%]/])", "\\%1")
tags[tag] = { tag = tag, line = line, file = tag_filename .. ".md" }
end
end
table.insert(lines, [[<!-- vim: set ft=markdown: -->]])
Util.write_file(Config.options.readme.root .. "/doc/" .. tag_filename .. ".md", table.concat(lines, "\n"))
end
end
return tags
end
function M.update()
if Config.plugins["lazy.nvim"] then
vim.cmd.helptags(Config.plugins["lazy.nvim"].dir .. "/doc")
end
if Config.options.readme.enabled == false then
return
end
local docs = Config.options.readme.root .. "/doc"
vim.fn.mkdir(docs, "p")
Util.ls(docs, function(path, name, type)
if type == "file" and name:sub(-2) == "md" then
vim.uv.fs_unlink(path)
end
end)
---@type {file:string, tag:string, line:string}[]
local tags = {}
for _, plugin in pairs(Config.plugins) do
for key, tag in pairs(M.index(plugin)) do
tags[key] = tag
end
end
local lines = { [[!_TAG_FILE_ENCODING utf-8 //]] }
Util.foreach(tags, function(_, tag)
table.insert(lines, ("%s\t%s\t/%s"):format(tag.tag, tag.file, tag.line))
end, { case_sensitive = true })
Util.write_file(docs .. "/tags", table.concat(lines, "\n"))
end
return M

View file

@ -1,151 +1,78 @@
---@class Lazy: LazyCommands
local M = {} local M = {}
M._start = 0
vim.uv = vim.uv or vim.loop
local function profile_require()
local done = {} ---@type table<string, true>
local r = require
_G.require = function(modname)
local Util = package.loaded["lazy.core.util"]
if Util and not done[modname] then
done[modname] = true
Util.track({ require = modname })
local ok, ret = pcall(function()
return vim.F.pack_len(r(modname))
end)
Util.track()
if not ok then
error(ret, 2)
end
return vim.F.unpack_len(ret)
else
return r(modname)
end
end
end
---@overload fun(opts: LazyConfig)
---@overload fun(spec:LazySpec, opts: LazyConfig)
function M.setup(spec, opts)
if type(spec) == "table" and spec.spec then
---@cast spec LazyConfig
opts = spec
else
opts = opts or {}
opts.spec = spec
end
M._start = M._start == 0 and vim.uv.hrtime() or M._start
if vim.g.lazy_did_setup then
return vim.notify(
"Re-sourcing your config is not supported with lazy.nvim",
vim.log.levels.WARN,
{ title = "lazy.nvim" }
)
end
vim.g.lazy_did_setup = true
if not vim.go.loadplugins then
return
end
if vim.fn.has("nvim-0.8.0") ~= 1 then
return vim.notify("lazy.nvim requires Neovim >= 0.8.0", vim.log.levels.ERROR, { title = "lazy.nvim" })
end
if not (pcall(require, "ffi") and jit and jit.version) then
return vim.notify("lazy.nvim requires Neovim built with LuaJIT", vim.log.levels.ERROR, { title = "lazy.nvim" })
end
local start = vim.uv.hrtime()
-- use the Neovim cache if available
if vim.loader and vim.fn.has("nvim-0.9.1") == 1 then
package.loaded["lazy.core.cache"] = vim.loader
end
local Cache = require("lazy.core.cache")
local enable_cache = vim.tbl_get(opts, "performance", "cache", "enabled") ~= false
-- load module cache before anything else
if enable_cache then
Cache.enable()
end
if vim.tbl_get(opts, "profiling", "require") then
profile_require()
end
require("lazy.stats").track("LazyStart")
---@param opts? LazyConfig
function M.setup(opts)
local module_start = vim.loop.hrtime()
require("lazy.core.module").setup()
local Util = require("lazy.core.util") local Util = require("lazy.core.util")
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Loader = require("lazy.core.loader") local Loader = require("lazy.core.loader")
local Handler = require("lazy.core.handler")
local Plugin = require("lazy.core.plugin")
table.insert(package.loaders, 3, Loader.loader) Util.track("module", vim.loop.hrtime() - module_start)
if vim.tbl_get(opts, "profiling", "loader") then Util.track("setup")
if vim.loader then
vim.loader._profile({ loaders = true })
else
Cache._profile_loaders()
end
end
Util.track({ plugin = "lazy.nvim" }) -- setup start
Util.track("module", vim.uv.hrtime() - start)
-- load config
Util.track("config") Util.track("config")
Config.setup(opts) Config.setup(opts)
Util.track() Util.track()
-- setup loader and handlers Plugin.load()
Loader.setup()
-- correct time delta and loaded if Config.options.install_missing then
local delta = vim.uv.hrtime() - start Util.track("install")
Util.track().time = delta -- end setup for _, plugin in pairs(Config.plugins) do
if Config.plugins["lazy.nvim"] then if not plugin._.installed then
Config.plugins["lazy.nvim"]._.loaded = { time = delta, source = "init.lua" } vim.cmd("do User LazyInstallPre")
require("lazy.manage").install({ wait = true })
break
end
end
Util.track()
end end
-- load plugins with lazy=false or Plugin.init Util.track("handlers")
Loader.startup() Handler.setup()
Util.track()
-- all done! local lazy_delta = vim.loop.hrtime() - module_start
vim.api.nvim_exec_autocmds("User", { pattern = "LazyDone", modeline = false })
require("lazy.stats").track("LazyDone") Util.track() -- end setup
Loader.init_plugins()
if Config.plugins["lazy.nvim"] then
Config.plugins["lazy.nvim"]._.loaded.time = lazy_delta
end
vim.cmd("do User LazyDone")
end end
function M.stats() function M.stats()
return require("lazy.stats").stats() local ret = { count = 0, loaded = 0 }
for _, plugin in pairs(require("lazy.core.config").plugins) do
ret.count = ret.count + 1
if plugin._.loaded then
ret.loaded = ret.loaded + 1
end
end
return ret
end end
function M.bootstrap() function M.bootstrap()
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" local lazypath = vim.fn.stdpath("data") .. "/site/pack/lazy/start/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then if not vim.loop.fs_stat(lazypath) then
vim.fn.system({ vim.fn.system({
"git", "git",
"clone", "clone",
"--filter=blob:none", "--filter=blob:none",
"--single-branch",
"https://github.com/folke/lazy.nvim.git", "https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath, lazypath,
}) })
vim.opt.runtimepath:append(lazypath)
end end
vim.opt.rtp:prepend(lazypath)
end end
---@return LazyPlugin[]
function M.plugins()
return vim.tbl_values(require("lazy.core.config").plugins)
end
setmetatable(M, {
__index = function(_, key)
return function(...)
return require("lazy.view.commands").commands[key](...)
end
end,
})
return M return M

View file

@ -1,97 +0,0 @@
local Config = require("lazy.core.config")
local Git = require("lazy.manage.git")
local Manage = require("lazy.manage")
local Plugin = require("lazy.core.plugin")
local State = require("lazy.state")
local Util = require("lazy.util")
local M = {}
M.running = false
M.updated = {}
M.reported = {}
function M.start()
M.fast_check()
if M.schedule() > 0 and not M.has_errors() then
Manage.log({
clear = false,
show = false,
check = true,
concurrency = Config.options.checker.concurrency,
})
end
end
function M.schedule()
State.read() -- update state
local next_check = State.checker.last_check + Config.options.checker.frequency - os.time()
next_check = math.max(next_check, 0)
vim.defer_fn(M.check, next_check * 1000)
return next_check
end
---@param opts? {report:boolean} report defaults to true
function M.fast_check(opts)
opts = opts or {}
for _, plugin in pairs(Config.plugins) do
-- don't check local plugins here, since we mark them as needing updates
-- only if local is behind upstream (if the git log task gives no output)
if plugin._.installed and not (plugin.pin or plugin._.is_local) then
plugin._.updates = nil
local info = Git.info(plugin.dir)
local ok, target = pcall(Git.get_target, plugin)
if ok and info and target and not Git.eq(info, target) then
plugin._.updates = { from = info, to = target }
end
end
end
M.report(opts.report ~= false)
end
function M.has_errors()
for _, plugin in pairs(Config.plugins) do
if Plugin.has_errors(plugin) then
return true
end
end
return false
end
function M.check()
State.checker.last_check = os.time()
State.write() -- update state
if M.has_errors() then
M.schedule()
else
Manage.check({
clear = false,
show = false,
concurrency = Config.options.checker.concurrency,
}):wait(function()
M.report()
M.schedule()
end)
end
end
---@param notify? boolean
function M.report(notify)
local lines = {}
M.updated = {}
for _, plugin in pairs(Config.plugins) do
if plugin._.updates then
table.insert(M.updated, plugin.name)
if not vim.tbl_contains(M.reported, plugin.name) then
table.insert(lines, "- **" .. plugin.name .. "**")
table.insert(M.reported, plugin.name)
end
end
end
if #lines > 0 and notify and Config.options.checker.notify and not Config.headless() then
table.insert(lines, 1, "# Plugin Updates")
Util.info(lines)
end
end
return M

View file

@ -1,49 +1,36 @@
local Config = require("lazy.core.config")
local Process = require("lazy.manage.process")
local Semver = require("lazy.manage.semver")
local Util = require("lazy.util") local Util = require("lazy.util")
local Semver = require("lazy.manage.semver")
local Config = require("lazy.core.config")
local M = {} local M = {}
---@alias GitInfo {branch?:string, commit?:string, tag?:string, version?:Semver} ---@alias GitInfo {branch?:string, commit?:string, tag?:string, version?:Semver}
---@param repo string ---@param details? boolean
---@param details? boolean Fetching details is slow! Don't loop over a plugin to fetch all details!
---@return GitInfo? ---@return GitInfo?
function M.info(repo, details) function M.info(repo, details)
local line = M.head(repo) local line = Util.head(repo .. "/.git/HEAD")
if line then if line then
---@type string, string ---@type string, string
local ref, branch = line:match("ref: refs/(heads/(.*))") local ref, branch = line:match("ref: (refs/heads/(.*))")
local ret = ref and { local ret = ref and {
branch = branch, branch = branch,
commit = M.ref(repo, ref), commit = Util.head(repo .. "/.git/" .. ref),
} or { commit = line } } or { commit = line }
if details then if details then
for tag, tag_ref in pairs(M.get_tag_refs(repo)) do Util.ls(repo .. "/.git/refs/tags", function(_, name)
if tag_ref == ret.commit then if M.ref(repo, "tags/" .. name) == ret.commit then
ret.tag = tag ret.tag = name
ret.version = ret.version or Semver.version(tag) ret.version = Semver.version(name)
return false
end end
end end)
end end
return ret return ret
end end
end end
---@param a GitInfo
---@param b GitInfo
function M.eq(a, b)
local ra = a.commit and a.commit:sub(1, 7)
local rb = b.commit and b.commit:sub(1, 7)
return ra == rb
end
function M.head(repo)
return Util.head(repo .. "/.git/HEAD")
end
---@class TaggedSemver: Semver ---@class TaggedSemver: Semver
---@field tag string ---@field tag string
@ -52,198 +39,75 @@ function M.get_versions(repo, spec)
local range = Semver.range(spec or "*") local range = Semver.range(spec or "*")
---@type TaggedSemver[] ---@type TaggedSemver[]
local versions = {} local versions = {}
for _, tag in ipairs(M.get_tags(repo)) do Util.ls(repo .. "/.git/refs/tags", function(_, name)
local v = Semver.version(tag) local v = Semver.version(name)
---@cast v TaggedSemver ---@cast v TaggedSemver
if v and range:matches(v) then if v and range:matches(v) then
v.tag = tag v.tag = name
table.insert(versions, v) table.insert(versions, v)
end end
end end)
return versions return versions
end end
function M.get_tags(repo)
---@type string[]
local ret = {}
Util.ls(repo .. "/.git/refs/tags", function(_, name)
ret[#ret + 1] = name
end)
for name in pairs(M.packed_refs(repo)) do
local tag = name:match("^tags/(.*)")
if tag then
ret[#ret + 1] = tag
end
end
return ret
end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@return string? ---@return {branch:string, commit?:string}?
function M.get_branch(plugin) function M.get_branch(plugin)
if plugin.branch then if plugin.branch then
return plugin.branch return {
branch = plugin.branch,
commit = M.ref(plugin.dir, "heads/" .. plugin.branch),
}
else else
-- we need to return the default branch
-- Try origin first
local main = M.ref(plugin.dir, "remotes/origin/HEAD") local main = M.ref(plugin.dir, "remotes/origin/HEAD")
if main then if main then
local branch = main:match("ref: refs/remotes/origin/(.*)") local branch = main:match("ref: refs/remotes/origin/(.*)")
if branch then if branch then
return branch return {
branch = branch,
commit = M.ref(plugin.dir, "heads/" .. branch),
}
end end
end end
-- fallback to local HEAD
main = assert(M.head(plugin.dir))
return main and main:match("ref: refs/heads/(.*)")
end
end
-- Return the last commit for the given branch
---@param repo string
---@param branch string
---@param origin? boolean
function M.get_commit(repo, branch, origin)
if origin then
-- origin ref might not exist if it is the same as local
return M.ref(repo, "remotes/origin", branch) or M.ref(repo, "heads", branch)
else
return M.ref(repo, "heads", branch)
end end
end end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@return GitInfo? ---@return GitInfo?
function M.get_target(plugin) function M.get_target(plugin)
if plugin._.is_local then local branch = M.get_branch(plugin) or M.info(plugin.dir)
local info = M.info(plugin.dir)
local branch = assert(info and info.branch or M.get_branch(plugin))
return { branch = branch, commit = M.get_commit(plugin.dir, branch, true) }
end
local branch = assert(M.get_branch(plugin))
if plugin.commit then if plugin.commit then
return { return {
branch = branch, branch = branch and branch.branch,
commit = plugin.commit, commit = plugin.commit,
} }
end end
if plugin.tag then if plugin.tag then
return { return {
branch = branch, branch = branch and branch.branch,
tag = plugin.tag, tag = plugin.tag,
commit = M.ref(plugin.dir, "tags/" .. plugin.tag), commit = M.ref(plugin.dir, "tags/" .. plugin.tag),
} }
end end
local version = plugin.version or Config.options.defaults.version
local version = (plugin.version == nil and plugin.branch == nil) and Config.options.defaults.version or plugin.version
if version then if version then
local last = Semver.last(M.get_versions(plugin.dir, version)) local last = Semver.last(M.get_versions(plugin.dir, version))
if last then if last then
return { return {
branch = branch, branch = branch and branch.branch,
version = last, version = last,
tag = last.tag, tag = last.tag,
commit = M.ref(plugin.dir, "tags/" .. last.tag), commit = M.ref(plugin.dir, "tags/" .. last.tag),
} }
end end
end end
return { branch = branch, commit = M.get_commit(plugin.dir, branch, true) } ---@diagnostic disable-next-line: return-type-mismatch
return branch
end end
function M.ref(repo, ...) function M.ref(repo, ref)
local ref = table.concat({ ... }, "/") return Util.head(repo .. "/.git/refs/" .. ref)
-- if this is a tag ref, then dereference it instead
if ref:find("tags/", 1, true) == 1 then
local tags = M.get_tag_refs(repo, ref)
for _, tag_ref in pairs(tags) do
return tag_ref
end
end
-- otherwise just get the ref
return Util.head(repo .. "/.git/refs/" .. ref) or M.packed_refs(repo)[ref]
end
function M.packed_refs(repo)
local ok, refs = pcall(Util.read_file, repo .. "/.git/packed-refs")
---@type table<string,string>
local ret = {}
if ok then
for _, line in ipairs(vim.split(refs, "\n")) do
local ref, name = line:match("^(.*) refs/(.*)$")
if ref then
ret[name] = ref
end
end
end
return ret
end
-- this is slow, so don't use on a loop over all plugins!
---@param tagref string?
function M.get_tag_refs(repo, tagref)
tagref = tagref or "--tags"
---@type table<string,string>
local tags = {}
local ok, lines = pcall(function()
return Process.exec({ "git", "show-ref", "-d", tagref }, { cwd = repo })
end)
if not ok then
return {}
end
for _, line in ipairs(lines) do
local ref, tag = line:match("^(%w+) refs/tags/([^%^]+)%^?{?}?$")
if ref then
tags[tag] = ref
end
end
return tags
end
---@param repo string
function M.get_origin(repo)
return M.get_config(repo)["remote.origin.url"]
end
---@param repo string
function M.get_config(repo)
local ok, config = pcall(Util.read_file, repo .. "/.git/config")
if not ok then
return {}
end
---@type table<string, string>
local ret = {}
---@type string
local current_section = nil
for line in config:gmatch("[^\n]+") do
-- Check if the line is a section header
local section = line:match("^%s*%[(.+)%]%s*$")
if section then
---@type string
current_section = section:gsub('%s+"', "."):gsub('"+%s*$', "")
else
-- Ignore comments and blank lines
if not line:match("^%s*[#;]") and line:match("%S") then
local key, value = line:match("^%s*(%S+)%s*=%s*(.+)%s*$")
ret[current_section .. "." .. key] = value
end
end
end
return ret
end
function M.count(repo, commit1, commit2)
local lines = Process.exec({ "git", "rev-list", "--count", commit1 .. ".." .. commit2 }, { cwd = repo })
return tonumber(lines[1] or "0") or 0
end
function M.age(repo, commit)
local lines = Process.exec({ "git", "show", "-s", "--format=%cr", "--date=short", commit }, { cwd = repo })
return lines[1] or ""
end end
return M return M

View file

@ -1,6 +1,6 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Plugin = require("lazy.core.plugin")
local Runner = require("lazy.manage.runner") local Runner = require("lazy.manage.runner")
local Plugin = require("lazy.core.plugin")
local M = {} local M = {}
@ -9,34 +9,21 @@ local M = {}
---@field clear? boolean ---@field clear? boolean
---@field show? boolean ---@field show? boolean
---@field mode? string ---@field mode? string
---@field plugins? (LazyPlugin|string)[] ---@field plugins? LazyPlugin[]
---@field concurrency? number
---@field lockfile? boolean
---@param ropts RunnerOpts ---@param ropts RunnerOpts
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.run(ropts, opts) function M.run(ropts, opts)
opts = opts or {} opts = opts or {}
local mode = opts.mode
local event = mode and ("Lazy" .. mode:sub(1, 1):upper() .. mode:sub(2))
if event then
vim.api.nvim_exec_autocmds("User", { pattern = event .. "Pre", modeline = false })
end
if opts.plugins then if opts.plugins then
---@param plugin string|LazyPlugin
opts.plugins = vim.tbl_map(function(plugin)
return type(plugin) == "string" and Config.plugins[plugin] or plugin
end, vim.tbl_values(opts.plugins))
ropts.plugins = opts.plugins ropts.plugins = opts.plugins
end end
ropts.concurrency = ropts.concurrency or opts.concurrency or Config.options.concurrency ropts.concurrency = ropts.concurrency or Config.options.concurrency
if opts.clear then if opts.clear then
M.clear(opts.plugins) M.clear()
end end
if opts.show ~= false then if opts.show ~= false then
@ -49,16 +36,12 @@ function M.run(ropts, opts)
local runner = Runner.new(ropts) local runner = Runner.new(ropts)
runner:start() runner:start()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false }) vim.cmd([[do User LazyRender]])
-- wait for post-install to finish -- wait for post-install to finish
runner:wait(function() runner:wait(function()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false }) vim.cmd([[do User LazyRender]])
Plugin.update_state() Plugin.update_state()
require("lazy.manage.checker").fast_check({ report = false })
if event then
vim.api.nvim_exec_autocmds("User", { pattern = event, modeline = false })
end
end) end)
if opts.wait then if opts.wait then
@ -67,171 +50,81 @@ function M.run(ropts, opts)
return runner return runner
end end
---@generic O: ManagerOpts
---@param opts? O
---@param defaults? ManagerOpts
---@return O
function M.opts(opts, defaults)
return vim.tbl_deep_extend("force", { clear = true }, defaults or {}, opts or {})
end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.install(opts) function M.install(opts)
opts = M.opts(opts, { mode = "install" }) M.run({
return M.run({
pipeline = { pipeline = {
"plugin.exists", "fs.symlink",
"git.clone", "git.clone",
{ "git.checkout", lockfile = opts.lockfile }, "git.checkout",
"plugin.docs", "plugin.docs",
{ "wait",
"wait", "plugin.run",
---@param runner Runner
sync = function(runner)
require("lazy.pkg").update()
Plugin.load()
runner:update()
end,
},
"plugin.build",
}, },
plugins = function(plugin) plugins = function(plugin)
return not (plugin._.installed and not plugin._.build) return plugin.uri and not plugin._.installed
end, end,
}, opts):wait(function() }, opts)
require("lazy.manage.lock").update()
require("lazy.help").update()
end)
end end
---@param opts? ManagerOpts ---@param opts? ManagerOpts|{lockfile?:boolean}
function M.update(opts) function M.update(opts)
opts = M.opts(opts, { mode = "update" }) opts = opts or {}
return M.run({ M.run({
pipeline = { pipeline = {
"plugin.exists", "fs.symlink",
"git.origin",
"git.branch", "git.branch",
"git.fetch", "git.fetch",
"git.status",
{ "git.checkout", lockfile = opts.lockfile }, { "git.checkout", lockfile = opts.lockfile },
"plugin.docs", "plugin.docs",
{ "wait",
"wait", "plugin.run",
---@param runner Runner
sync = function(runner)
require("lazy.pkg").update()
Plugin.load()
runner:update()
end,
},
"plugin.build",
{ "git.log", updated = true }, { "git.log", updated = true },
}, },
plugins = function(plugin) plugins = function(plugin)
return plugin.url and plugin._.installed return plugin.uri and plugin._.installed
end, end,
}, opts):wait(function() }, opts):wait(function()
require("lazy.manage.lock").update() require("lazy.manage.lock").update()
require("lazy.help").update()
end) end)
end end
--
---@param opts? ManagerOpts
function M.restore(opts)
opts = M.opts(opts, { mode = "restore", lockfile = true })
return M.update(opts)
end
---@param opts? ManagerOpts
function M.check(opts) function M.check(opts)
opts = M.opts(opts, { mode = "check" })
opts = opts or {} opts = opts or {}
return M.run({ M.run({
pipeline = { pipeline = {
"plugin.exists",
{ "git.origin", check = true },
"git.fetch", "git.fetch",
"git.status",
"wait", "wait",
{ "git.log", check = true }, { "git.log", check = true },
}, },
plugins = function(plugin) plugins = function(plugin)
return plugin.url and plugin._.installed return plugin.uri and plugin._.installed
end, end,
}, opts) }, opts)
end end
---@param opts? ManagerOpts | {check?:boolean} ---@param opts? ManagerOpts
function M.log(opts) function M.log(opts)
opts = M.opts(opts, { mode = "log" }) M.run({
return M.run({ pipeline = { "git.log" },
pipeline = {
{ "git.origin", check = true },
{ "git.log", check = opts.check },
},
plugins = function(plugin) plugins = function(plugin)
return plugin.url and plugin._.installed return plugin.uri and plugin._.installed
end, end,
}, opts) }, opts)
end end
---@param opts? ManagerOpts
function M.build(opts)
opts = M.opts(opts, { mode = "build" })
return M.run({
pipeline = { { "plugin.build", force = true } },
plugins = function()
return false
end,
}, opts)
end
---@param opts? ManagerOpts
function M.sync(opts)
opts = M.opts(opts)
if opts.clear then
M.clear()
opts.clear = false
end
if opts.show ~= false then
vim.schedule(function()
require("lazy.view").show("sync")
end)
opts.show = false
end
vim.api.nvim_exec_autocmds("User", { pattern = "LazySyncPre", modeline = false })
local clean_opts = vim.deepcopy(opts)
clean_opts.plugins = nil
local clean = M.clean(clean_opts)
local install = M.install(opts)
local update = M.update(opts)
clean:wait(function()
install:wait(function()
update:wait(function()
vim.api.nvim_exec_autocmds("User", { pattern = "LazySync", modeline = false })
end)
end)
end)
end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.clean(opts) function M.clean(opts)
opts = M.opts(opts, { mode = "clean" }) Plugin.update_state()
return M.run({ M.run({
pipeline = { "fs.clean" }, pipeline = { "fs.clean" },
plugins = Config.to_clean, plugins = Config.to_clean,
}, opts):wait(function() }, opts)
require("lazy.manage.lock").update()
end)
end end
---@param plugins? LazyPlugin[] function M.clear()
function M.clear(plugins) Plugin.update_state()
for _, plugin in pairs(plugins or Config.plugins) do for _, plugin in pairs(Config.plugins) do
plugin._.updates = nil
plugin._.updated = nil plugin._.updated = nil
plugin._.cloned = nil plugin._.cloned = nil
plugin._.dirty = nil plugin._.dirty = nil
@ -239,11 +132,11 @@ function M.clear(plugins)
if plugin._.tasks then if plugin._.tasks then
---@param task LazyTask ---@param task LazyTask
plugin._.tasks = vim.tbl_filter(function(task) plugin._.tasks = vim.tbl_filter(function(task)
return task:running() or task:has_errors() return task:is_running()
end, plugin._.tasks) end, plugin._.tasks)
end end
end end
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false }) vim.cmd([[do User LazyRender]])
end end
return M return M

View file

@ -3,53 +3,50 @@ local Git = require("lazy.manage.git")
local M = {} local M = {}
---@alias LazyLockfile table<string, {commit:string, branch:string}> ---@type table<string, {commit:string, branch:string}>
---@type LazyLockfile
M.lock = {} M.lock = {}
M._loaded = false M._loaded = false
function M.update() function M.update()
M.load() local f = assert(io.open(Config.options.lockfile, "w"))
vim.fn.mkdir(vim.fn.fnamemodify(Config.options.lockfile, ":p:h"), "p")
local f = assert(io.open(Config.options.lockfile, "wb"))
f:write("{\n") f:write("{\n")
M.lock = {}
-- keep disabled and cond plugins ---@param plugin LazyPlugin
for name in pairs(M.lock) do local plugins = vim.tbl_filter(function(plugin)
if not (Config.spec.disabled[name] or Config.spec.ignore_installed[name]) then return not plugin._.is_local and plugin._.installed
M.lock[name] = nil end, Config.plugins)
end
end
for _, plugin in pairs(Config.plugins) do
if not plugin._.is_local and plugin._.installed then
local info = assert(Git.info(plugin.dir))
M.lock[plugin.name] = {
branch = info.branch or assert(Git.get_branch(plugin)),
commit = assert(info.commit, "commit is nil"),
}
end
end
---@param plugin LazyPlugin
---@type string[] ---@type string[]
local names = vim.tbl_keys(M.lock) local names = vim.tbl_map(function(plugin)
return plugin.name
end, plugins)
table.sort(names) table.sort(names)
for n, name in ipairs(names) do for n, name in ipairs(names) do
local info = M.lock[name] local plugin = Config.plugins[name]
f:write(([[ %q: { "branch": %q, "commit": %q }]]):format(name, info.branch, info.commit)) if not plugin._.is_local and plugin._.installed then
if n ~= #names then local info = assert(Git.info(plugin.dir))
f:write(",\n") if not info.branch then
local branch = assert(Git.get_branch(plugin))
info.branch = branch.branch
end
info.commit = info.commit
-- f:write(([[ [%q] = { branch = %q, commit = %q },]]):format(name, info.branch, info.commit) .. "\n")
f:write(([[ %q: { "branch": %q, "commit": %q }]]):format(name, info.branch, info.commit))
if n ~= #names then
f:write(",\n")
end
---@diagnostic disable-next-line: assign-type-mismatch
M.lock[plugin.name] = info
end end
end end
f:write("\n}\n") f:write("\n}")
f:close() f:close()
end end
function M.load() function M.load()
if M._loaded then
return
end
M.lock = {} M.lock = {}
M._loaded = true M._loaded = true
local f = io.open(Config.options.lockfile, "r") local f = io.open(Config.options.lockfile, "r")
@ -67,7 +64,9 @@ end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@return {commit:string, branch:string} ---@return {commit:string, branch:string}
function M.get(plugin) function M.get(plugin)
M.load() if not M._loaded then
M.load()
end
return M.lock[plugin.name] return M.lock[plugin.name]
end end

View file

@ -1,239 +1,111 @@
local Async = require("lazy.async")
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local M = {}
---@diagnostic disable-next-line: no-unknown ---@diagnostic disable-next-line: no-unknown
local uv = vim.uv local uv = vim.loop
---@class ProcessOpts ---@class ProcessOpts
---@field args string[] ---@field args string[]
---@field cwd? string ---@field cwd? string
---@field on_line? fun(line:string) ---@field on_line? fun(string)
---@field on_exit? fun(ok:boolean, output:string) ---@field on_exit? fun(ok:boolean, output:string)
---@field on_data? fun(data:string, is_stderr?:boolean)
---@field timeout? number ---@field timeout? number
---@field env? table<string,string>
local M = {}
---@type table<uv_process_t, LazyProcess>
M.running = setmetatable({}, { __mode = "k" })
---@class LazyProcess: Async
---@field handle? uv_process_t
---@field pid? number
---@field cmd string
---@field opts ProcessOpts
---@field timeout? uv_timer_t
---@field timedout? boolean
---@field data string
---@field check? uv_check_t
---@field code? number
---@field signal? number
local Process = setmetatable({}, { __index = Async.Async })
---@param cmd string|string[]
---@param opts? ProcessOpts
function Process.new(cmd, opts)
local self = setmetatable({}, { __index = Process })
---@async
Process.init(self, function()
self:_run()
end)
opts = opts or {}
opts.args = opts.args or {}
if type(cmd) == "table" then
self.cmd = cmd[1]
vim.list_extend(opts.args, vim.list_slice(cmd, 2))
else
self.cmd = cmd
end
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
-- make sure the cwd is valid
if not opts.cwd and type(uv.cwd()) ~= "string" then
opts.cwd = uv.os_homedir()
end
opts.on_line = opts.on_line and vim.schedule_wrap(opts.on_line) or nil
opts.on_data = opts.on_data and vim.schedule_wrap(opts.on_data) or nil
self.data = ""
self.opts = opts
self.code = 1
self.signal = 0
return self
end
---@async
function Process:_run()
self:guard()
local stdout = assert(uv.new_pipe())
local stderr = assert(uv.new_pipe())
self.handle = uv.spawn(self.cmd, {
stdio = { nil, stdout, stderr },
args = self.opts.args,
cwd = self.opts.cwd,
env = self:env(),
}, function(code, signal)
self.code = code
self.signal = signal
if self.timeout then
self.timeout:stop()
end
self.handle:close()
stdout:close()
stderr:close()
self:resume()
end)
if self.handle then
M.running[self.handle] = self
stdout:read_start(function(err, data)
self:on_data(err, data)
end)
stderr:read_start(function(err, data)
self:on_data(err, data, true)
end)
self:suspend()
while not (self.handle:is_closing() and stdout:is_closing() and stderr:is_closing()) do
Async.yield()
end
else
self.data = "Failed to spawn process " .. self.cmd .. " " .. vim.inspect(self.opts)
end
self:on_exit()
end
function Process:on_exit()
self.data = self.data:gsub("[^\r\n]+\r", "")
if self.timedout then
self.data = self.data .. "\n" .. "Process was killed because it reached the timeout"
elseif self.signal ~= 0 then
self.data = self.data .. "\n" .. "Process was killed with SIG" .. M.signals[self.signal]:upper()
end
if self.opts.on_exit then
self.opts.on_exit(self.code == 0 and self.signal == 0, self.data)
end
end
function Process:guard()
if self.opts.timeout then
self.timeout = assert(uv.new_timer())
self.timeout:start(self.opts.timeout, 0, function()
self.timedout = true
self:kill()
end)
end
end
function Process:env()
---@type table<string, string>
local env = vim.tbl_extend("force", {
GIT_SSH_COMMAND = "ssh -oBatchMode=yes",
}, uv.os_environ(), self.opts.env or {})
env.GIT_DIR = nil
env.GIT_WORK_TREE = nil
env.GIT_TERMINAL_PROMPT = "0"
env.GIT_INDEX_FILE = nil
---@type string[]
local env_flat = {}
for k, v in pairs(env) do
env_flat[#env_flat + 1] = k .. "=" .. v
end
return env_flat
end
---@param signals uv.aliases.signals|uv.aliases.signals[]|nil
function Process:kill(signals)
if not self.handle or self.handle:is_closing() then
return
end
signals = signals or { "sigterm", "sigkill" }
signals = type(signals) == "table" and signals or { signals }
---@cast signals uv.aliases.signals[]
local timer = assert(uv.new_timer())
timer:start(0, 1000, function()
if self.handle and not self.handle:is_closing() and #signals > 0 then
self.handle:kill(table.remove(signals, 1))
else
timer:stop()
end
end)
end
---@param err? string
---@param data? string
---@param is_stderr? boolean
function Process:on_data(err, data, is_stderr)
assert(not err, err)
if not data then
return
end
if self.opts.on_data then
self.opts.on_data(data, is_stderr)
end
self.data = self.data .. data:gsub("\r\n", "\n")
local lines = vim.split(vim.trim(self.data:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
if self.opts.on_line then
self.opts.on_line(lines[#lines])
end
end
M.signals = {
"hup",
"int",
"quit",
"ill",
"trap",
"abrt",
"bus",
"fpe",
"kill",
"usr1",
"segv",
"usr2",
"pipe",
"alrm",
"term",
"chld",
"cont",
"stop",
"tstp",
"ttin",
"ttou",
"urg",
"xcpu",
"xfsz",
"vtalrm",
"prof",
"winch",
"io",
"pwr",
"emt",
"sys",
"info",
}
---@param cmd string|string[]
---@param opts? ProcessOpts ---@param opts? ProcessOpts
function M.spawn(cmd, opts) function M.spawn(cmd, opts)
return Process.new(cmd, opts)
end
function M.abort()
for _, proc in pairs(M.running) do
proc:kill()
end
end
---@async
---@param cmd string|string[]
---@param opts? ProcessOpts
function M.exec(cmd, opts)
opts = opts or {} opts = opts or {}
local proc = M.spawn(cmd, opts) opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
proc:wait()
return vim.split(proc.data, "\n"), proc.code local env = {
"GIT_TERMINAL_PROMPT=0",
"GIT_SSH_COMMAND=ssh -oBatchMode=yes",
}
for key, value in
pairs(uv.os_environ() --[[@as string[] ]])
do
table.insert(env, key .. "=" .. value)
end
local stdout = uv.new_pipe()
local stderr = uv.new_pipe()
local output = ""
---@type vim.loop.Process
local handle = nil
local timeout
local killed = false
if opts.timeout then
timeout = uv.new_timer()
timeout:start(opts.timeout, 0, function()
if handle and not handle:is_closing() then
killed = true
uv.process_kill(handle, "sigint")
end
end)
end
handle = uv.spawn(cmd, {
stdio = { nil, stdout, stderr },
args = opts.args,
cwd = opts.cwd,
env = env,
}, function(exit_code, signal)
if timeout then
timeout:stop()
timeout:close()
end
handle:close()
stdout:close()
stderr:close()
local check = uv.new_check()
check:start(function()
if not stdout:is_closing() or not stderr:is_closing() then
return
end
check:stop()
if opts.on_exit then
output = output:gsub("[^\r\n]+\r", "")
if killed then
output = output .. "\n" .. "Process was killed because it reached the timeout"
end
vim.schedule(function()
opts.on_exit(exit_code == 0 and signal == 0, output)
end)
end
end)
end)
if not handle then
if opts.on_exit then
opts.on_exit(false, "Failed to spawn process " .. cmd .. " " .. vim.inspect(opts))
end
return
end
---@param data? string
local function on_output(err, data)
assert(not err, err)
if data then
output = output .. data:gsub("\r\n", "\n")
local lines = vim.split(vim.trim(output:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
if opts.on_line then
vim.schedule(function()
opts.on_line(lines[#lines])
end)
end
end
end
uv.read_start(stdout, on_output)
uv.read_start(stderr, on_output)
return handle
end end
return M return M

View file

@ -1,107 +0,0 @@
local Config = require("lazy.core.config")
local Loader = require("lazy.core.loader")
local Plugin = require("lazy.core.plugin")
local Util = require("lazy.util")
local M = {}
---@type table<string, uv.aliases.fs_stat_table>
M.files = {}
---@type uv_timer_t
M.timer = nil
function M.enable()
if M.timer then
M.timer:stop()
end
if #Config.spec.modules > 0 then
M.timer = assert(vim.uv.new_timer())
M.check(true)
M.timer:start(2000, 2000, M.check)
end
end
function M.disable()
if M.timer then
M.timer:stop()
M.timer = nil
end
end
---@param h1 uv.aliases.fs_stat_table
---@param h2 uv.aliases.fs_stat_table
function M.eq(h1, h2)
return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec
end
function M.check(start)
---@type table<string,true>
local checked = {}
---@type {file:string, what:string}[]
local changes = {}
-- spec is a module
local function check(_, modpath)
checked[modpath] = true
local hash = vim.uv.fs_stat(modpath)
if hash then
if M.files[modpath] then
if not M.eq(M.files[modpath], hash) then
M.files[modpath] = hash
table.insert(changes, { file = modpath, what = "changed" })
end
else
M.files[modpath] = hash
table.insert(changes, { file = modpath, what = "added" })
end
end
end
for _, modname in ipairs(Config.spec.modules) do
Util.lsmod(modname, check)
end
for file in pairs(M.files) do
if not checked[file] then
table.insert(changes, { file = file, what = "deleted" })
M.files[file] = nil
end
end
if Loader.init_done and Config.mapleader ~= vim.g.mapleader then
vim.schedule(function()
require("lazy.core.util").warn("You need to set `vim.g.mapleader` **BEFORE** loading lazy")
end)
Config.mapleader = vim.g.mapleader
end
if Loader.init_done and Config.maplocalleader ~= vim.g.maplocalleader then
vim.schedule(function()
require("lazy.core.util").warn("You need to set `vim.g.maplocalleader` **BEFORE** loading lazy")
end)
Config.maplocalleader = vim.g.maplocalleader
end
if not (start or #changes == 0) then
M.reload(changes)
end
end
---@param {file:string, what:string}[]
function M.reload(changes)
vim.schedule(function()
if Config.options.change_detection.notify and not Config.headless() then
local lines = { "# Config Change Detected. Reloading...", "" }
for _, change in ipairs(changes) do
table.insert(lines, "- **" .. change.what .. "**: `" .. vim.fn.fnamemodify(change.file, ":p:~:.") .. "`")
end
Util.warn(lines)
end
Plugin.load()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false })
vim.api.nvim_exec_autocmds("User", { pattern = "LazyReload", modeline = false })
end)
end
return M

View file

@ -1,23 +1,21 @@
local Async = require("lazy.async")
local Config = require("lazy.core.config")
local Task = require("lazy.manage.task") local Task = require("lazy.manage.task")
local Config = require("lazy.core.config")
local Util = require("lazy.util")
---@class RunnerOpts ---@class RunnerOpts
---@field pipeline (string|{[1]:string, [string]:any})[] ---@field pipeline (string|{[1]:string, [string]:any})[]
---@field plugins? LazyPlugin[]|fun(plugin:LazyPlugin):any? ---@field plugins? LazyPlugin[]|fun(plugin:LazyPlugin):any?
---@field concurrency? number ---@field concurrency? number
---@class RunnerTask ---@alias PipelineStep {task:string, opts?:TaskOptions}
---@field task? LazyTask ---@alias LazyRunnerTask {co:thread, status: {task?:LazyTask, waiting?:boolean}}
---@field step number
---@alias PipelineStep {task:string, opts?:TaskOptions }
---@class Runner ---@class Runner
---@field _plugins table<string,LazyPlugin> ---@field _plugins LazyPlugin[]
---@field _running LazyRunnerTask[]
---@field _pipeline PipelineStep[] ---@field _pipeline PipelineStep[]
---@field _on_done fun()[]
---@field _opts RunnerOpts ---@field _opts RunnerOpts
---@field _running? Async
local Runner = {} local Runner = {}
---@param opts RunnerOpts ---@param opts RunnerOpts
@ -26,17 +24,13 @@ function Runner.new(opts)
self._opts = opts or {} self._opts = opts or {}
local plugins = self._opts.plugins local plugins = self._opts.plugins
---@type LazyPlugin[]
local pp = {}
if type(plugins) == "function" then if type(plugins) == "function" then
pp = vim.tbl_filter(plugins, Config.plugins) self._plugins = vim.tbl_filter(plugins, Config.plugins)
else else
pp = plugins or Config.plugins self._plugins = plugins or Config.plugins
end
self._plugins = {}
for _, plugin in ipairs(pp) do
self._plugins[plugin.name] = plugin
end end
self._running = {}
self._on_done = {}
---@param step string|(TaskOptions|{[1]:string}) ---@param step string|(TaskOptions|{[1]:string})
self._pipeline = vim.tbl_map(function(step) self._pipeline = vim.tbl_map(function(step)
@ -46,154 +40,116 @@ function Runner.new(opts)
return self return self
end end
function Runner:plugin(name) ---@param entry LazyRunnerTask
return self._plugins[name] function Runner:_resume(entry)
end if entry.status.task and not entry.status.task:is_done() then
return true
--- Update plugins
function Runner:update()
for name in pairs(self._plugins) do
self._plugins[name] = Config.plugins[name] or self._plugins[name]
end end
local ok, status = coroutine.resume(entry.co)
if not ok then
Util.error("Could not resume a task\n" .. status)
end
entry.status = ok and status
return entry.status ~= nil
end end
function Runner:start() function Runner:resume(waiting)
---@async local running = 0
self._running = Async.new(function() for _, entry in ipairs(self._running) do
self:_start() if entry.status then
end) if waiting and entry.status.waiting then
end entry.status.waiting = false
---@async
function Runner:_start()
---@type string[]
local names = vim.tbl_keys(self._plugins)
table.sort(names)
---@type table<string,RunnerTask>
local state = {}
local active = 1
local waiting = 0
---@type number?
local wait_step = nil
---@async
---@param resume? boolean
local function continue(resume)
active = 0
waiting = 0
wait_step = nil
local next = {} ---@type string[]
-- check running tasks
for _, name in ipairs(names) do
state[name] = state[name] or { step = 0 }
local s = state[name]
local is_running = s.task and s.task:running()
local step = self._pipeline[s.step]
if is_running then
-- still running
active = active + 1
-- selene:allow(empty_if)
elseif s.task and s.task:has_errors() then
-- don't continue tasks if there are errors
elseif step and step.task == "wait" and not resume then
-- waiting for sync
waiting = waiting + 1
wait_step = s.step
else
next[#next + 1] = name
end end
end if not entry.status.waiting and self:_resume(entry) then
running = running + 1
-- schedule next tasks if self._opts.concurrency and running >= self._opts.concurrency then
for _, name in ipairs(next) do
if self._opts.concurrency and active >= self._opts.concurrency then
break
end
local s = state[name]
local plugin = self:plugin(name)
while s.step <= #self._pipeline do
if s.step == #self._pipeline then
-- done
s.task = nil
plugin._.working = false
break break
elseif s.step < #self._pipeline then
-- next
s.step = s.step + 1
local step = self._pipeline[s.step]
if step.task == "wait" then
plugin._.working = false
waiting = waiting + 1
wait_step = s.step
break
else
s.task = self:queue(plugin, step)
plugin._.working = true
if s.task then
active = active + 1
s.task:wake(false)
break
end
end
end end
end end
end end
end end
return running > 0 or (not waiting and self:resume(true))
end
while active > 0 do function Runner:start()
continue() for _, plugin in pairs(self._plugins) do
if active == 0 and waiting > 0 then local co = coroutine.create(self.run_pipeline)
local sync = self._pipeline[wait_step] local ok, err = coroutine.resume(co, self, plugin)
if sync and sync.opts and type(sync.opts.sync) == "function" then if ok then
sync.opts.sync(self) table.insert(self._running, { co = co, status = {} })
end else
continue(true) Util.error("Could not start tasks for " .. plugin.name .. "\n" .. err)
end end
if active > 0 then end
self._running:suspend()
local check = vim.loop.new_check()
check:start(function()
if self:resume() then
return
end
check:stop()
self._running = {}
for _, cb in ipairs(self._on_done) do
vim.schedule(cb)
end
self._on_done = {}
end)
end
---@async
---@param plugin LazyPlugin
function Runner:run_pipeline(plugin)
coroutine.yield()
for _, step in ipairs(self._pipeline) do
if step.task == "wait" then
coroutine.yield({ waiting = true })
else
local task = self:queue(plugin, step.task, step.opts)
if task then
coroutine.yield({ task = task })
assert(task:is_done())
if task.error then
return
end
end
end end
end end
end end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@param step PipelineStep ---@param task_name string
---@param opts? TaskOptions
---@return LazyTask? ---@return LazyTask?
function Runner:queue(plugin, step) function Runner:queue(plugin, task_name, opts)
assert(self._running and self._running:running(), "Runner is not running") assert(self._running)
local def = vim.split(step.task, ".", { plain = true }) local def = vim.split(task_name, ".", { plain = true })
---@type LazyTaskDef ---@type LazyTaskDef
local task_def = require("lazy.manage.task." .. def[1])[def[2]] local task_def = require("lazy.manage.task." .. def[1])[def[2]]
assert(task_def, "Task not found: " .. step.task) assert(task_def)
local opts = step.opts or {} opts = opts or {}
if not (task_def.skip and task_def.skip(plugin, opts)) then if not (task_def.skip and task_def.skip(plugin, opts)) then
return Task.new(plugin, def[2], task_def.run, opts) local task = Task.new(plugin, def[2], task_def.run, opts)
task:start()
return task
end end
end end
function Runner:is_running()
return self._running and self._running:running()
end
-- Execute the callback async when done. -- Execute the callback async when done.
-- When no callback is specified, this will wait sync -- When no callback is specified, this will wait sync
---@param cb? fun() ---@param cb? fun()
function Runner:wait(cb) function Runner:wait(cb)
if not self:is_running() then if #self._running == 0 then
if cb then return cb and cb()
cb()
end
return self
end end
if cb then if cb then
self._running:on("done", cb) table.insert(self._on_done, cb)
else else
self._running:wait() -- sync wait
while #self._running > 0 do
vim.wait(10)
end
end end
return self
end end
return Runner return Runner

View file

@ -9,7 +9,6 @@ local M = {}
---@field patch number ---@field patch number
---@field prerelease? string ---@field prerelease? string
---@field build? string ---@field build? string
---@field input? string
local Semver = {} local Semver = {}
Semver.__index = Semver Semver.__index = Semver
@ -91,7 +90,6 @@ function M.version(version)
patch = patch == "" and 0 or tonumber(patch), patch = patch == "" and 0 or tonumber(patch),
prerelease = prerelease ~= "" and prerelease or nil, prerelease = prerelease ~= "" and prerelease or nil,
build = build ~= "" and build or nil, build = build ~= "" and build or nil,
input = version,
}, Semver) }, Semver)
end end
end end

View file

@ -1,45 +1,54 @@
local Config = require("lazy.core.config")
local Util = require("lazy.util") local Util = require("lazy.util")
---@type table<string, LazyTaskDef> ---@type table<string, LazyTaskDef>
local M = {} local M = {}
local function rm(dir)
local stat = vim.uv.fs_lstat(dir)
assert(stat and stat.type == "directory", dir .. " should be a directory!")
Util.walk(dir, function(path, _, type)
if type == "directory" then
vim.uv.fs_rmdir(path)
else
vim.uv.fs_unlink(path)
end
end)
vim.uv.fs_rmdir(dir)
end
M.clean = { M.clean = {
skip = function(plugin) run = function(self)
return plugin._.is_local
end,
---@param opts? {rocks_only?:boolean}
run = function(self, opts)
opts = opts or {}
local dir = self.plugin.dir:gsub("/+$", "") local dir = self.plugin.dir:gsub("/+$", "")
assert(dir:find(Config.options.root, 1, true) == 1, self.plugin.dir .. " should be under packpath!") local stat = vim.loop.fs_lstat(dir)
local rock_root = Config.options.rocks.root .. "/" .. self.plugin.name if stat.type == "directory" then
if vim.uv.fs_stat(rock_root) then Util.walk(dir, function(path, _, type)
rm(rock_root) if type == "directory" then
vim.loop.fs_rmdir(path)
else
vim.loop.fs_unlink(path)
end
end)
vim.loop.fs_rmdir(dir)
else
vim.loop.fs_unlink(dir)
end end
if opts.rocks_only then
return
end
rm(dir)
self.plugin._.installed = false self.plugin._.installed = false
end, end,
} }
M.symlink = {
skip = function(plugin)
if not plugin._.is_local then
return true
end
return not plugin._.is_symlink and plugin._.installed
end,
run = function(self)
local stat = vim.loop.fs_lstat(self.plugin.dir)
if stat then
assert(stat.type == "link")
if vim.loop.fs_realpath(self.plugin.uri) == vim.loop.fs_realpath(self.plugin.dir) then
self.plugin._.installed = true
self.plugin._.cloned = true
return
else
vim.loop.fs_unlink(self.plugin.dir)
end
end
vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, { dir = true })
vim.opt.runtimepath:append(self.plugin.uri)
self.plugin._.installed = true
self.plugin._.cloned = true
end,
}
return M return M

View file

@ -1,53 +1,7 @@
local Async = require("lazy.async") local Util = require("lazy.util")
local Config = require("lazy.core.config")
local Git = require("lazy.manage.git") local Git = require("lazy.manage.git")
local Lock = require("lazy.manage.lock") local Lock = require("lazy.manage.lock")
local Util = require("lazy.util") local Config = require("lazy.core.config")
local throttle = {}
throttle.running = 0
throttle.waiting = {} ---@type Async[]
throttle.timer = vim.uv.new_timer()
function throttle.next()
throttle.running = 0
while #throttle.waiting > 0 and throttle.running < Config.options.git.throttle.rate do
---@type Async
local task = table.remove(throttle.waiting, 1)
task:resume()
throttle.running = throttle.running + 1
end
if throttle.running == 0 then
throttle.timer:stop()
end
end
function throttle.wait()
if not Config.options.git.throttle.enabled then
return
end
if not throttle.timer:is_active() then
throttle.timer:start(0, Config.options.git.throttle.duration, vim.schedule_wrap(throttle.next))
end
local running = Async.running()
if throttle.running < Config.options.git.throttle.rate then
throttle.running = throttle.running + 1
else
table.insert(throttle.waiting, running)
coroutine.yield("waiting")
running:suspend()
coroutine.yield("")
end
end
---@param plugin LazyPlugin
local function cooldown(plugin)
if not plugin._.last_check then
return false
end
local delta = (vim.uv.now() - plugin._.last_check) / 1000
return delta < Config.options.git.cooldown
end
---@type table<string, LazyTaskDef> ---@type table<string, LazyTaskDef>
local M = {} local M = {}
@ -55,19 +9,13 @@ local M = {}
M.log = { M.log = {
---@param opts {updated?:boolean, check?: boolean} ---@param opts {updated?:boolean, check?: boolean}
skip = function(plugin, opts) skip = function(plugin, opts)
if opts.check and plugin.pin then
return true
end
if opts.updated and not (plugin._.updated and plugin._.updated.from ~= plugin._.updated.to) then if opts.updated and not (plugin._.updated and plugin._.updated.from ~= plugin._.updated.to) then
return true return true
end end
local stat = vim.uv.fs_stat(plugin.dir .. "/.git") return not Util.file_exists(plugin.dir .. "/.git")
return not (stat and stat.type == "directory")
end, end,
---@async
---@param opts {args?: string[], updated?:boolean, check?:boolean} ---@param opts {args?: string[], updated?:boolean, check?:boolean}
run = function(self, opts) run = function(self, opts)
-- self:spawn({ "sleep", "5" })
local args = { local args = {
"log", "log",
"--pretty=format:%h %s (%cr)", "--pretty=format:%h %s (%cr)",
@ -75,35 +23,13 @@ M.log = {
"--decorate", "--decorate",
"--date=short", "--date=short",
"--color=never", "--color=never",
"--no-show-signature",
} }
local info, target
if opts.updated then if opts.updated then
table.insert(args, self.plugin._.updated.from .. ".." .. (self.plugin._.updated.to or "HEAD")) table.insert(args, self.plugin._.updated.from .. ".." .. (self.plugin._.updated.to or "HEAD"))
elseif opts.check then elseif opts.check then
info = assert(Git.info(self.plugin.dir)) local info = assert(Git.info(self.plugin.dir))
target = assert(Git.get_target(self.plugin)) local target = assert(Git.get_target(self.plugin))
if not target.commit then
for k, v in pairs(target) do
error(k .. " '" .. v .. "' not found")
end
error("no target commit found")
end
assert(target.commit, self.plugin.name .. " " .. target.branch)
if not self.plugin._.is_local then
if Git.eq(info, target) then
if Config.options.checker.check_pinned then
local last_commit = Git.get_commit(self.plugin.dir, target.branch, true)
if not Git.eq(info, { commit = last_commit }) then
self.plugin._.outdated = true
end
end
else
self.plugin._.updates = { from = info, to = target }
end
end
table.insert(args, info.commit .. ".." .. target.commit) table.insert(args, info.commit .. ".." .. target.commit)
else else
vim.list_extend(args, opts.args or Config.options.git.log) vim.list_extend(args, opts.args or Config.options.git.log)
@ -113,14 +39,6 @@ M.log = {
args = args, args = args,
cwd = self.plugin.dir, cwd = self.plugin.dir,
}) })
-- for local plugins, mark as needing updates only if local is
-- behind upstream, i.e. if git log gave no output
if opts.check and self.plugin._.is_local then
if not vim.tbl_isempty(self:get_log()) then
self.plugin._.updates = { from = info, to = target }
end
end
end, end,
} }
@ -128,45 +46,23 @@ M.clone = {
skip = function(plugin) skip = function(plugin)
return plugin._.installed or plugin._.is_local return plugin._.installed or plugin._.is_local
end, end,
---@async
run = function(self) run = function(self)
throttle.wait()
local args = { local args = {
"clone", "clone",
self.plugin.url, self.plugin.uri,
"--filter=blob:none",
"--recurse-submodules",
"--single-branch",
"--shallow-submodules",
"--no-checkout",
"--progress",
} }
if Config.options.git.filter then
args[#args + 1] = "--filter=blob:none"
end
if self.plugin.submodules ~= false then
args[#args + 1] = "--recurse-submodules"
end
args[#args + 1] = "--origin=origin"
-- If git config --global core.autocrlf is true on a Unix/Linux system, then the git clone
-- process will lead to files with CRLF endings. Vi / vim / neovim cannot handle this.
-- Force git to clone with core.autocrlf=false.
args[#args + 1] = "-c"
args[#args + 1] = "core.autocrlf=false"
args[#args + 1] = "--progress"
if self.plugin.branch then if self.plugin.branch then
vim.list_extend(args, { "-b", self.plugin.branch }) vim.list_extend(args, { "-b", self.plugin.branch })
end end
table.insert(args, self.plugin.dir) table.insert(args, self.plugin.dir)
if vim.fn.isdirectory(self.plugin.dir) == 1 then
require("lazy.manage.task.fs").clean.run(self, {})
end
local marker = self.plugin.dir .. ".cloning"
Util.write_file(marker, "")
self:spawn("git", { self:spawn("git", {
args = args, args = args,
on_exit = function(ok) on_exit = function(ok)
@ -174,31 +70,28 @@ M.clone = {
self.plugin._.cloned = true self.plugin._.cloned = true
self.plugin._.installed = true self.plugin._.installed = true
self.plugin._.dirty = true self.plugin._.dirty = true
vim.uv.fs_unlink(marker)
end end
end, end,
}) })
end, end,
} }
-- setup origin branches if needed
-- fetch will retrieve the data
M.branch = { M.branch = {
skip = function(plugin) skip = function(plugin)
if not plugin._.installed or plugin._.is_local then if not plugin._.installed or plugin._.is_local then
return true return true
end end
local branch = assert(Git.get_branch(plugin)) local branch = assert(Git.get_branch(plugin))
return Git.get_commit(plugin.dir, branch, true) return branch and branch.commit
end, end,
---@async
run = function(self) run = function(self)
local branch = assert(Git.get_branch(self.plugin))
local args = { local args = {
"remote", "remote",
"set-branches", "set-branches",
"--add", "--add",
"origin", "origin",
assert(Git.get_branch(self.plugin)), branch.branch,
} }
self:spawn("git", { self:spawn("git", {
@ -208,118 +101,37 @@ M.branch = {
end, end,
} }
-- check and switch origin M.fetch = {
M.origin = {
skip = function(plugin)
if not plugin._.installed or plugin._.is_local then
return true
end
local origin = Git.get_origin(plugin.dir)
return origin == plugin.url
end,
---@async
---@param opts {check?:boolean}
run = function(self, opts)
if opts.check then
local origin = Git.get_origin(self.plugin.dir)
self:error({
"Origin has changed:",
" * old: " .. origin,
" * new: " .. self.plugin.url,
"Please run update to fix",
})
return
end
require("lazy.manage.task.fs").clean.run(self, opts)
M.clone.run(self, opts)
end,
}
M.status = {
skip = function(plugin) skip = function(plugin)
return not plugin._.installed or plugin._.is_local return not plugin._.installed or plugin._.is_local
end, end,
---@async
run = function(self)
self:spawn("git", {
args = { "ls-files", "-d", "-m" },
cwd = self.plugin.dir,
on_exit = function(ok, output)
if ok then
local lines = vim.split(output, "\n")
---@type string[]
lines = vim.tbl_filter(function(line)
-- Fix doc/tags being marked as modified
if line:gsub("[\\/]", "/") == "doc/tags" then
local Process = require("lazy.manage.process")
Process.exec({ "git", "checkout", "--", "doc/tags" }, { cwd = self.plugin.dir })
return false
end
return line ~= ""
end, lines)
if #lines > 0 then
local msg = { "You have local changes in `" .. self.plugin.dir .. "`:" }
for _, line in ipairs(lines) do
msg[#msg + 1] = " * " .. line
end
msg[#msg + 1] = "Please remove them to update."
msg[#msg + 1] = "You can also press `x` to remove the plugin and then `I` to install it again."
self:error(msg)
end
end
end,
})
end,
}
-- fetches all needed origin branches
M.fetch = {
skip = function(plugin)
return not plugin._.installed or plugin._.is_local or cooldown(plugin)
end,
---@async
run = function(self) run = function(self)
throttle.wait()
local args = { local args = {
"fetch", "fetch",
"--recurse-submodules", "--recurse-submodules",
"--tags", -- also fetch remote tags "--update-shallow",
"--force", -- overwrite existing tags if needed
"--progress", "--progress",
} }
if self.plugin.submodules == false then
table.remove(args, 2)
end
self:spawn("git", { self:spawn("git", {
args = args, args = args,
cwd = self.plugin.dir, cwd = self.plugin.dir,
on_exit = function(ok)
if ok then
self.plugin._.last_check = vim.uv.now()
end
end,
}) })
end, end,
} }
-- checkout to the target commit
-- branches will exists at this point, so so will the commit
M.checkout = { M.checkout = {
skip = function(plugin) skip = function(plugin)
return not plugin._.installed or plugin._.is_local return not plugin._.installed or plugin._.is_local
end, end,
---@async
---@param opts {lockfile?:boolean} ---@param opts {lockfile?:boolean}
run = function(self, opts) run = function(self, opts)
throttle.wait()
local info = assert(Git.info(self.plugin.dir)) local info = assert(Git.info(self.plugin.dir))
local target = assert(Git.get_target(self.plugin)) local target = assert(Git.get_target(self.plugin))
-- if the plugin is pinned and we did not just clone it, -- if the plugin is locked and we did not just clone it,
-- then don't update -- then don't update
if self.plugin.pin and not self.plugin._.cloned then if self.plugin.pin and not self.plugin._.cloned then
target = info target = info
@ -327,7 +139,6 @@ M.checkout = {
local lock local lock
if opts.lockfile then if opts.lockfile then
-- restore to the lock if it exists
lock = Lock.get(self.plugin) lock = Lock.get(self.plugin)
if lock then if lock then
---@diagnostic disable-next-line: cast-local-type ---@diagnostic disable-next-line: cast-local-type
@ -335,9 +146,7 @@ M.checkout = {
end end
end end
-- don't run checkout if target is already reached. if not self.plugin._.cloned and info.commit == target.commit and info.branch == target.branch then
-- unless we just cloned, since then we won't have any data yet
if Git.eq(info, target) and info.branch == target.branch then
self.plugin._.updated = { self.plugin._.updated = {
from = info.commit, from = info.commit,
to = info.commit, to = info.commit,
@ -348,21 +157,16 @@ M.checkout = {
local args = { local args = {
"checkout", "checkout",
"--progress", "--progress",
"--recurse-submodules",
} }
if self.plugin.submodules == false then
table.remove(args, 3)
end
if lock then if lock then
table.insert(args, lock.commit) table.insert(args, lock.commit)
elseif target.tag then elseif target.tag then
table.insert(args, "tags/" .. target.tag) table.insert(args, "tags/" .. target.tag)
elseif self.plugin.commit then elseif self.plugin.commit then
table.insert(args, self.plugin.commit) table.insert(args, self.plugin.commit)
else elseif target.branch then
table.insert(args, target.commit) table.insert(args, target.branch)
end end
self:spawn("git", { self:spawn("git", {
@ -376,10 +180,8 @@ M.checkout = {
from = info.commit, from = info.commit,
to = new_info.commit, to = new_info.commit,
} }
if self.plugin._.updated.from ~= self.plugin._.updated.to then
self.plugin._.dirty = true
end
end end
self.plugin._.dirty = true
end end
end, end,
}) })

View file

@ -1,29 +1,24 @@
local Async = require("lazy.async")
local Config = require("lazy.core.config")
local Process = require("lazy.manage.process") local Process = require("lazy.manage.process")
local Terminal = require("lazy.terminal")
local colors = Config.options.headless.colors
---@class LazyTaskDef ---@class LazyTaskDef
---@field skip? fun(plugin:LazyPlugin, opts?:TaskOptions):any? ---@field skip? fun(plugin:LazyPlugin, opts?:TaskOptions):any?
---@field run async fun(task:LazyTask, opts:TaskOptions) ---@field run fun(task:LazyTask, opts:TaskOptions)
---@alias LazyTaskFn async fun(task:LazyTask, opts:TaskOptions) ---@alias LazyTaskState fun():boolean?
---@class LazyMsg ---@class LazyTask
---@field msg string
---@field level? number
---@class LazyTask: Async
---@field plugin LazyPlugin ---@field plugin LazyPlugin
---@field name string ---@field name string
---@field private _log LazyMsg[] ---@field type string
---@field private _started number ---@field output string
---@field status string
---@field error? string
---@field private _task fun(task:LazyTask)
---@field private _running LazyPluginState[]
---@field private _started? number
---@field private _ended? number ---@field private _ended? number
---@field private _opts TaskOptions ---@field private _opts TaskOptions
---@field private _level number local Task = {}
local Task = setmetatable({}, { __index = Async.Async })
---@class TaskOptions: {[string]:any} ---@class TaskOptions: {[string]:any}
---@field on_done? fun(task:LazyTask) ---@field on_done? fun(task:LazyTask)
@ -31,210 +26,142 @@ local Task = setmetatable({}, { __index = Async.Async })
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@param name string ---@param name string
---@param opts? TaskOptions ---@param opts? TaskOptions
---@param task LazyTaskFn ---@param task fun(task:LazyTask)
function Task.new(plugin, name, task, opts) function Task.new(plugin, name, task, opts)
local self = setmetatable({}, { __index = Task }) local self = setmetatable({}, {
---@async __index = Task,
Task.init(self, function() })
self:_run(task)
end)
self:set_level()
self._opts = opts or {} self._opts = opts or {}
self._log = {} self._running = {}
self._task = task
self._started = nil
self.plugin = plugin self.plugin = plugin
self.name = name self.name = name
self._started = vim.uv.hrtime() self.output = ""
---@param other LazyTask self.status = ""
plugin._.tasks = vim.tbl_filter(function(other) plugin._.tasks = plugin._.tasks or {}
return other.name ~= name or other:running()
end, plugin._.tasks or {})
table.insert(plugin._.tasks, self) table.insert(plugin._.tasks, self)
self:render()
return self return self
end end
---@param level? number function Task:has_started()
---@return LazyMsg[] return self._started ~= nil
function Task:get_log(level)
level = level or vim.log.levels.DEBUG
return vim.tbl_filter(function(msg)
return msg.level >= level
end, self._log)
end end
---@param level? number function Task:is_done()
function Task:output(level) return self:has_started() and not self:is_running()
return table.concat(
---@param m LazyMsg
vim.tbl_map(function(m)
return m.msg
end, self:get_log(level)),
"\n"
)
end end
function Task:status() function Task:is_running()
local ret = self._log[#self._log] return self:has_started() and self._ended == nil
local msg = ret and vim.trim(ret.msg) or ""
return msg ~= "" and msg or nil
end end
function Task:has_errors() function Task:start()
return self._level >= vim.log.levels.ERROR if vim.in_fast_event() then
end return vim.schedule(function()
self:start()
function Task:has_warnings()
return self._level >= vim.log.levels.WARN
end
---@param level? number
function Task:set_level(level)
self._level = level or vim.log.levels.TRACE
end
---@async
---@param task LazyTaskFn
function Task:_run(task)
if Config.headless() and Config.options.headless.task then
self:log("Running task " .. self.name, vim.log.levels.INFO)
end
self
:on("done", function()
self:_done()
end) end)
:on("error", function(err)
self:error(err)
end)
:on("yield", function(msg)
self:log(msg)
end)
task(self, self._opts)
end
---@param msg string|string[]|LazyMsg
---@param level? number
function Task:log(msg, level)
if type(msg) == "table" and msg.msg then
level = msg.level or level
msg = msg.msg
end end
level = level or vim.log.levels.DEBUG self._started = vim.loop.hrtime()
self._level = math.max(self._level or 0, level or 0) ---@type boolean, string|any
msg = type(msg) == "table" and table.concat(msg, "\n") or msg local ok, err = pcall(self._task, self, self._opts)
---@cast msg string if not ok then
table.insert(self._log, { msg = msg, level = level }) self.error = err or "failed"
self:render()
if Config.headless() then
self:headless()
end end
end self:_check()
function Task:render()
vim.schedule(function()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyRender", modeline = false })
end)
end
function Task:headless()
if not Config.options.headless.log then
return
end
local msg = self._log[#self._log]
if not msg or msg.level == vim.log.levels.TRACE then
return
end
local map = {
[vim.log.levels.ERROR] = Terminal.red,
[vim.log.levels.WARN] = Terminal.yellow,
[vim.log.levels.INFO] = Terminal.blue,
}
local color = Config.options.headless.colors and map[msg.level]
io.write(Terminal.prefix(color and color(msg.msg) or msg.msg, self:prefix()))
io.write("\n")
end
---@param msg string|string[]
function Task:error(msg)
self:log(msg, vim.log.levels.ERROR)
end
---@param msg string|string[]
function Task:warn(msg)
self:log(msg, vim.log.levels.WARN)
end end
---@private ---@private
function Task:_done() function Task:_check()
if Config.headless() and Config.options.headless.task then for _, state in ipairs(self._running) do
local ms = math.floor(self:time() + 0.5) if state() then
self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO) return
end
end end
self._ended = vim.uv.hrtime() self._ended = vim.loop.hrtime()
if self._opts.on_done then if self._opts.on_done then
self._opts.on_done(self) self._opts.on_done(self)
end end
self:render() vim.cmd("do User LazyRender")
vim.schedule(function() vim.api.nvim_exec_autocmds("User", {
vim.api.nvim_exec_autocmds("User", { pattern = "LazyPlugin" .. self.name:sub(1, 1):upper() .. self.name:sub(2),
pattern = "LazyPlugin" .. self.name:sub(1, 1):upper() .. self.name:sub(2), data = { plugin = self.plugin.name },
data = { plugin = self.plugin.name }, })
})
end)
end end
function Task:time() function Task:time()
return ((self._ended or vim.uv.hrtime()) - self._started) / 1e6 if not self:has_started() then
return 0
end
if not self:is_done() then
return (vim.loop.hrtime() - self._started) / 1e6
end
return (self._ended - self._started) / 1e6
end
---@param fn fun()
function Task:schedule(fn)
local done = false
table.insert(self._running, function()
return not done
end)
vim.schedule(function()
---@type boolean, string|any
local ok, err = pcall(fn)
if not ok then
self.error = err or "failed"
end
done = true
self:_check()
end)
end end
---@async
---@param cmd string ---@param cmd string
---@param opts? ProcessOpts ---@param opts? ProcessOpts
function Task:spawn(cmd, opts) function Task:spawn(cmd, opts)
opts = opts or {} opts = opts or {}
local on_line = opts.on_line local on_line = opts.on_line
local on_exit = opts.on_exit
local headless = Config.headless() and Config.options.headless.process
function opts.on_line(line) function opts.on_line(line)
if not headless then self.status = line
return self:log(line, vim.log.levels.TRACE)
end
if on_line then if on_line then
pcall(on_line, line) pcall(on_line, line)
end end
vim.cmd("do User LazyRender")
end end
if headless then ---@param output string
opts.on_data = function(data) function opts.on_exit(ok, output)
-- prefix with plugin name self.output = self.output .. output
io.write(Terminal.prefix(data, self:prefix())) if not ok then
self.error = self.error and (self.error .. "\n" .. output) or output
end end
if on_exit then
pcall(on_exit, ok, output)
end
self:_check()
end end
local proc = Process.spawn(cmd, opts) local proc = Process.spawn(cmd, opts)
proc:wait() table.insert(self._running, function()
return proc and not proc:is_closing()
local ok = proc.code == 0 and proc.signal == 0 end)
if not headless then
local msg = vim.trim(proc.data)
if #msg > 0 then
self:log(vim.trim(proc.data), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
end
end
if opts.on_exit then
pcall(opts.on_exit, ok, proc.data)
end
return ok
end end
function Task:prefix() ---@param tasks (LazyTask?)[]
local plugin = "[" .. self.plugin.name .. "] " function Task.all_done(tasks)
local task = string.rep(" ", 20 - #(self.name .. self.plugin.name)) .. self.name for _, task in ipairs(tasks) do
if task and not task:is_done() then
return false
end
end
return true
end
return colors and Terminal.magenta(plugin) .. Terminal.cyan(task) .. Terminal.bright_black(" | ") function Task:wait()
or plugin .. " " .. task .. " | " while self:is_running() do
vim.wait(10)
end
end end
return Task return Task

View file

@ -1,85 +1,29 @@
local Loader = require("lazy.core.loader")
local Rocks = require("lazy.pkg.rockspec")
local Util = require("lazy.util") local Util = require("lazy.util")
local Loader = require("lazy.core.loader")
---@type table<string, LazyTaskDef> ---@type table<string, LazyTaskDef>
local M = {} local M = {}
---@param plugin LazyPlugin M.run = {
local function get_build_file(plugin) skip = function(plugin)
for _, path in ipairs({ "build.lua", "build/init.lua" }) do return not (plugin._.dirty and (plugin.opt == false or plugin.run))
if Util.file_exists(plugin.dir .. "/" .. path) then
return path
end
end
end
local B = {}
---@param task LazyTask
---@param build string
function B.cmd(task, build)
if task.plugin.build ~= "rockspec" then
Loader.load(task.plugin, { task = "build" })
end
local cmd = vim.api.nvim_parse_cmd(build:sub(2), {}) --[[@as vim.api.keyset.cmd]]
task:log(vim.api.nvim_cmd(cmd, { output = true }))
end
---@async
---@param task LazyTask
---@param build string
function B.shell(task, build)
local shell = vim.env.SHELL or vim.o.shell
local shell_args = shell:find("cmd.exe", 1, true) and "/c" or "-c"
task:spawn(shell, {
args = { shell_args, build },
cwd = task.plugin.dir,
})
end
M.build = {
---@param opts? {force:boolean}
skip = function(plugin, opts)
if opts and opts.force then
return false
end
return not ((plugin._.dirty or plugin._.build) and (plugin.build or get_build_file(plugin)))
end, end,
---@async
run = function(self) run = function(self)
vim.cmd([[silent! runtime plugin/rplugin.vim]]) Loader.load(self.plugin, { task = "run" }, { load_start = true })
local builders = self.plugin.build local run = self.plugin.run
if run then
-- Skip if `build` is set to `false` if type(run) == "string" and run:sub(1, 1) == ":" then
if builders == false then local cmd = vim.api.nvim_parse_cmd(run:sub(2), {})
return self.output = vim.api.nvim_cmd(cmd, { output = true })
end elseif type(run) == "function" then
run()
builders = builders or get_build_file(self.plugin) else
local args = vim.split(run, "%s+")
if builders then return self:spawn(table.remove(args, 1), {
builders = type(builders) == "table" and builders or { builders } args = args,
---@cast builders (string|fun(LazyPlugin))[] cwd = self.plugin.dir,
for _, build in ipairs(builders) do })
if type(build) == "function" then
build(self.plugin)
elseif build == "rockspec" then
Rocks.build(self)
elseif build:sub(1, 1) == ":" then
B.cmd(self, build)
elseif build:match("%.lua$") then
local file = self.plugin.dir .. "/" .. build
local chunk, err = loadfile(file)
if not chunk or err then
error(err)
end
chunk()
else
B.shell(self, build)
end
end end
end end
end, end,
@ -87,23 +31,12 @@ M.build = {
M.docs = { M.docs = {
skip = function(plugin) skip = function(plugin)
return not plugin._.is_local and not plugin._.dirty return not plugin._.dirty
end, end,
run = function(self) run = function(self)
local docs = self.plugin.dir .. "/doc" local docs = self.plugin.dir .. "/doc/"
if Util.file_exists(docs) then if Util.file_exists(docs) then
self:log(vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true })) self.output = vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true })
end
end,
}
M.exists = {
skip = function(plugin)
return not plugin._.is_local or plugin.virtual
end,
run = function(self)
if not Util.file_exists(self.plugin.dir) then
self:error("Local plugin does not exist at `" .. self.plugin.dir .. "`")
end end
end, end,
} }

View file

@ -1,214 +0,0 @@
---@diagnostic disable: inject-field
local islist = vim.islist or vim.tbl_islist
local M = {}
---@param opts LazyConfig
---@return LazySpec[]
local function get_spec(opts)
local ret = opts.spec or {}
return ret and type(ret) == "table" and islist(ret) and ret or { ret }
end
---@param defaults LazyConfig
---@param opts LazyConfig
function M.extend(defaults, opts)
local spec = {}
vim.list_extend(spec, get_spec(defaults))
vim.list_extend(spec, get_spec(opts))
return vim.tbl_deep_extend("force", defaults, opts, { spec = spec })
end
---@param opts LazyConfig
function M.setup(opts)
opts = M.extend({
local_spec = false,
change_detection = { enabled = false },
dev = {
patterns = vim.env.LAZY_DEV and vim.split(vim.env.LAZY_DEV, ",") or nil,
},
}, opts)
local args = {}
local is_busted = false
local is_minitest = false
for _, a in ipairs(_G.arg) do
if a == "--busted" then
is_busted = true
elseif a == "--minitest" then
is_minitest = true
else
table.insert(args, a)
end
end
_G.arg = args
if is_busted then
opts = M.busted.setup(opts)
elseif is_minitest then
opts = M.minitest.setup(opts)
end
-- set stdpaths to use .tests
if vim.env.LAZY_STDPATH then
local root = vim.fn.fnamemodify(vim.env.LAZY_STDPATH, ":p")
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
end
vim.o.loadplugins = true
require("lazy").setup(opts)
if vim.g.colors_name == nil then
vim.cmd("colorscheme habamax")
end
require("lazy").update():wait()
if vim.bo.filetype == "lazy" then
local errors = false
for _, plugin in pairs(require("lazy.core.config").spec.plugins) do
errors = errors or require("lazy.core.plugin").has_errors(plugin)
end
if not errors then
vim.cmd.close()
end
end
if is_busted then
M.busted.run()
elseif is_minitest then
M.minitest.run()
end
end
function M.repro(opts)
opts = M.extend({
spec = {
{
"folke/tokyonight.nvim",
priority = 1000,
lazy = false,
config = function()
require("tokyonight").setup({ style = "moon" })
require("tokyonight").load()
end,
},
},
install = { colorscheme = { "tokyonight" } },
}, opts)
M.setup(opts)
end
M.minitest = {}
function M.minitest.run()
local Config = require("lazy.core.config")
-- disable termnial output for the tests
Config.options.headless = {}
if not require("lazy.core.config").headless() then
return vim.notify("busted can only run in headless mode. Please run with `nvim -l`", vim.log.levels.WARN)
end
package.path = package.path .. ";" .. vim.uv.cwd() .. "/tests/?.lua"
local Test = require("mini.test")
local expect = Test.expect
local _assert = assert
local Assert = {
__call = function(_, ...)
return _assert(...)
end,
same = expect.equality,
equal = expect.equality,
are = {
equal = expect.equality,
},
is_not = {
same = expect.no_equality,
},
is_not_nil = function(a)
return expect.no_equality(nil, a)
end,
is_true = function(a)
return expect.equality(true, a)
end,
is_false = function(a)
return expect.equality(false, a)
end,
}
Assert.__index = Assert
assert = setmetatable({}, Assert)
assert = require("luassert")
require("mini.test").run()
end
---@param opts LazyConfig
function M.minitest.setup(opts)
return M.extend({
spec = {
"lunarmodules/luassert",
{
"echasnovski/mini.test",
opts = {
collect = {
find_files = function()
return vim.fn.globpath("tests", "**/*_spec.lua", true, true)
end,
},
-- script_path = "tests/minit.lua",
},
},
{ dir = vim.uv.cwd() },
},
rocks = { hererocks = true },
}, opts)
end
M.busted = {}
function M.busted.run()
local Config = require("lazy.core.config")
-- disable termnial output for the tests
Config.options.headless = {}
if not require("lazy.core.config").headless() then
return vim.notify("busted can only run in headless mode. Please run with `nvim -l`", vim.log.levels.WARN)
end
package.path = package.path .. ";" .. vim.uv.cwd() .. "/tests/?.lua"
-- run busted
return pcall(require("busted.runner"), {
standalone = false,
}) or os.exit(1)
end
---@param opts LazyConfig
function M.busted.setup(opts)
local args = table.concat(_G.arg, " ")
local json = args:find("--output[ =]json")
return M.extend({
spec = {
"lunarmodules/busted",
{ dir = vim.uv.cwd() },
},
headless = {
process = not json,
log = not json,
task = not json,
},
rocks = { hererocks = true },
}, opts)
end
---@param opts LazyConfig
function M.busted.init(opts)
opts = M.busted.setup(opts)
M.setup(opts)
M.busted.run()
end
setmetatable(M.busted, {
__call = function(_, opts)
M.busted.init(opts)
end,
})
return M

View file

@ -1,137 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.core.util")
local M = {}
M.VERSION = 12
M.dirty = false
---@class LazyPkg
---@field name string
---@field dir string
---@field source "lazy" | "packspec" | "rockspec"
---@field file string
---@field spec LazyPluginSpec
---@class LazyPkgSpec
---@field file string
---@field source? string
---@field spec? LazySpec
---@field code? string
---@class LazyPkgSource
---@field name string
---@field get fun(plugin:LazyPlugin):LazyPkgSpec?
---@class LazyPkgCache
---@field pkgs LazyPkg[]
---@field version number
---@type LazyPkg[]?
M.cache = nil
function M.update()
---@type LazyPkgSource[]
local sources = {}
for _, s in ipairs(Config.options.pkg.sources) do
if s ~= "rockspec" or Config.options.rocks.enabled then
sources[#sources + 1] = {
name = s,
get = require("lazy.pkg." .. s).get,
}
end
end
---@type LazyPkgCache
local ret = {
version = M.VERSION,
pkgs = {},
}
for _, plugin in pairs(Config.plugins) do
if plugin._.installed then
for _, source in ipairs(sources) do
local spec = source.get(plugin)
if spec then
---@type LazyPkg
local pkg = {
name = plugin.name,
dir = plugin.dir,
source = spec.source or source.name,
file = spec.file,
spec = spec.spec or {},
}
if type(spec.code) == "string" then
pkg.spec = { _raw = spec.code }
end
table.insert(ret.pkgs, pkg)
break
end
end
end
end
table.sort(ret.pkgs, function(a, b)
return a.name < b.name
end)
local U = require("lazy.util")
local code = "return " .. U.dump(ret)
vim.fn.mkdir(vim.fn.fnamemodify(Config.options.pkg.cache, ":h"), "p")
U.write_file(Config.options.pkg.cache, code)
M.dirty = false
M.cache = nil
end
local function _load()
Util.track("pkg")
M.cache = nil
if vim.uv.fs_stat(Config.options.pkg.cache) then
Util.try(function()
local chunk, err = loadfile(Config.options.pkg.cache)
if not chunk then
error(err)
end
---@type LazyPkgCache?
local ret = chunk()
if ret and ret.version == M.VERSION then
M.cache = {}
for _, pkg in ipairs(ret.pkgs) do
if type(pkg.spec) == "function" then
pkg.spec = pkg.spec()
end
-- wrap in the scope of the plugin
pkg.spec = { pkg.name, specs = pkg.spec }
end
M.cache = ret.pkgs
end
end, "Error loading pkg:")
end
if rawget(M, "cache") then
M.dirty = false
else
M.cache = {}
M.dirty = true
end
Util.track()
end
---@param dir string
---@return LazyPkg?
---@overload fun():LazyPkg[]
function M.get(dir)
if dir then
for _, pkg in ipairs(M.cache) do
if pkg.dir == dir then
return pkg
end
end
return
end
return M.cache
end
return setmetatable(M, {
__index = function(_, key)
if key == "cache" then
_load()
return M.cache
end
end,
})

View file

@ -1,29 +0,0 @@
local Util = require("lazy.util")
local M = {}
M.lazy_file = "lazy.lua"
---@param plugin LazyPlugin
---@return LazyPkg?
function M.get(plugin)
local file = Util.norm(plugin.dir .. "/" .. M.lazy_file)
if Util.file_exists(file) then
---@type fun(): LazySpec
local chunk = Util.try(function()
local ret, err = loadfile(file)
return err and error(err) or ret
end, "`" .. M.lazy_file .. "` for **" .. plugin.name .. "** has errors:")
if not chunk then
Util.error("Invalid `" .. M.lazy_file .. "` for **" .. plugin.name .. "**")
return
end
return {
source = "lazy",
file = M.lazy_file,
code = "function()\n" .. Util.read_file(file) .. "\nend",
}
end
end
return M

View file

@ -1,54 +0,0 @@
local Util = require("lazy.util")
---@class PackSpec
---@field dependencies? table<string, string>
---@field lazy? LazyPluginSpec
---
local M = {}
M.pkg_file = "pkg.json"
---@param plugin LazyPlugin
---@return LazyPkg?
function M.get(plugin)
local file = Util.norm(plugin.dir .. "/" .. M.pkg_file)
if not Util.file_exists(file) then
return
end
---@type PackSpec
local pkg = Util.try(function()
return vim.json.decode(Util.read_file(file))
end, "`" .. M.pkg_file .. "` for **" .. plugin.name .. "** has errors:")
if not pkg then
return
end
---@type LazySpec
local ret = {}
if pkg.dependencies then
for url, version in pairs(pkg.dependencies) do
-- HACK: Add `.git` to github urls
if url:find("github") and not url:match("%.git$") then
url = url .. ".git"
end
ret[#ret + 1] = { url = url, version = version }
end
end
local p = pkg.lazy
if p then
p.url = p.url or plugin.url
p.dir = p.dir or plugin.dir
ret[#ret + 1] = p
end
if pkg.lazy then
ret[#ret + 1] = pkg.lazy
end
return {
source = "lazy",
file = M.pkg_file,
spec = ret,
}
end
return M

View file

@ -1,343 +0,0 @@
--# selene:allow(incorrect_standard_library_use)
local Community = require("lazy.community")
local Config = require("lazy.core.config")
local Health = require("lazy.health")
local Util = require("lazy.util")
---@class RockSpec
---@field rockspec_format string
---@field package string
---@field version string
---@field dependencies string[]
---@field build? {type?: string, modules?: any[]}
---@field source? {url?: string}
---@class RockManifest
---@field repository table<string, table<string,any>>
local M = {}
M.skip = { "lua" }
M.rewrites = {
["plenary.nvim"] = { "nvim-lua/plenary.nvim", lazy = true },
}
M.python = { "python3", "python" }
---@class HereRocks
M.hererocks = {}
---@param task LazyTask
function M.hererocks.build(task)
local root = Config.options.rocks.root .. "/hererocks"
---@param p string
local python = vim.tbl_filter(function(p)
return vim.fn.executable(p) == 1
end, M.python)[1]
task:spawn(python, {
args = {
"hererocks.py",
"--verbose",
"-l",
"5.1",
"-r",
"latest",
root,
},
cwd = task.plugin.dir,
})
end
---@param bin string
function M.hererocks.bin(bin)
local hererocks = Config.options.rocks.root .. "/hererocks/bin"
return Util.norm(hererocks .. "/" .. bin)
end
-- check if hererocks is building
---@return boolean?
function M.hererocks.building()
return vim.tbl_get(Config.plugins.hererocks or {}, "_", "build")
end
---@param opts? LazyHealth
function M.check(opts)
opts = vim.tbl_extend("force", {
error = Util.error,
warn = Util.warn,
ok = function() end,
}, opts or {})
local ok = false
if Config.hererocks() then
if M.hererocks.building() then
ok = true
else
ok = Health.have(M.python, opts)
ok = Health.have(M.hererocks.bin("luarocks")) and ok
Health.have(
M.hererocks.bin("lua"),
vim.tbl_extend("force", opts, {
version = "-v",
version_pattern = "5.1",
})
)
end
else
ok = Health.have("luarocks", opts)
Health.have(
{ "lua5.1", "lua", "lua-5.1" },
vim.tbl_extend("force", opts, {
version = "-v",
version_pattern = "5.1",
})
)
end
return ok
end
---@async
---@param task LazyTask
function M.build(task)
M.check({
error = function(msg)
task:error(msg:gsub("[{}]", "`"))
end,
warn = function(msg)
task:warn(msg)
end,
ok = function(msg) end,
})
if task:has_warnings() then
task:log({
"",
"This plugin requires `luarocks`. Try one of the following:",
" - fix your `luarocks` installation",
Config.hererocks() and " - disable *hererocks* with `opts.rocks.hererocks = false`"
or " - enable `hererocks` with `opts.rocks.hererocks = true`",
" - disable `luarocks` support completely with `opts.rocks.enabled = false`",
})
task:warn("\nWill try building anyway, but will likely fail...")
task:warn("\n" .. string.rep("-", 80) .. "\n")
task:set_level(vim.log.levels.WARN)
end
if task.plugin.name == "hererocks" then
return M.hererocks.build(task)
end
local env = {}
local luarocks = "luarocks"
if Config.hererocks() then
-- hererocks is still building, so skip for now
-- a new build will happen in the next round
if M.hererocks.building() then
return
end
local sep = Util.is_win and ";" or ":"
local hererocks = Config.options.rocks.root .. "/hererocks/bin"
if Util.is_win then
hererocks = hererocks:gsub("/", "\\")
end
local path = vim.split(vim.env.PATH, sep)
table.insert(path, 1, hererocks)
env = {
PATH = table.concat(path, sep),
}
if Util.is_win then
luarocks = luarocks .. ".bat"
end
end
local pkg = task.plugin._.pkg
assert(pkg, "missing rockspec pkg for " .. task.plugin.name .. "\nThis shouldn't happen, please report.")
local rockspec = M.rockspec(task.plugin.dir .. "/" .. pkg.file) or {}
assert(
rockspec.package,
"missing rockspec package name for " .. task.plugin.name .. "\nThis shouldn't happen, please report."
)
local root = Config.options.rocks.root .. "/" .. task.plugin.name
local ok = task:spawn(luarocks, {
args = {
"--tree",
root,
"--server",
Config.options.rocks.server,
"--lua-version",
"5.1",
"install", -- use install so that we can make use of pre-built rocks
"--force-fast",
"--deps-mode",
"one",
rockspec.package,
},
cwd = task.plugin.dir,
env = env,
})
if ok then
return
end
task:warn("Failed installing " .. rockspec.package .. " with `luarocks`.")
task:warn("\n" .. string.rep("-", 80) .. "\n")
task:warn("Trying to build from source.")
-- install failed, so try building from source
task:set_level() -- reset level
ok = task:spawn(luarocks, {
args = {
"--tree",
root,
"--dev",
"--lua-version",
"5.1",
"make",
"--force-fast",
"--deps-mode",
"one",
},
cwd = task.plugin.dir,
env = env,
})
if not ok then
require("lazy.manage.task.fs").clean.run(task, { rocks_only = true })
end
end
---@param rockspec RockSpec
function M.is_simple_build(rockspec)
local type = vim.tbl_get(rockspec, "build", "type")
return type == nil or type == "none" or (type == "builtin" and not rockspec.build.modules)
end
---@param file string
---@return table?
function M.parse(file)
local ret = {}
local ok = pcall(function()
loadfile(file, nil, ret)()
end) and ret or nil
return ok and ret or nil
end
---@param plugin LazyPlugin
function M.deps(plugin)
local root = Config.options.rocks.root .. "/" .. plugin.name
---@type RockManifest?
local manifest = M.parse(root .. "/lib/luarocks/rocks-5.1/manifest")
return manifest and vim.tbl_keys(manifest.repository or {})
end
---@param file string
---@return RockSpec?
function M.rockspec(file)
return M.parse(file)
end
---@param plugin LazyPlugin
function M.find_rockspec(plugin)
local rockspec_file ---@type string?
Util.ls(plugin.dir, function(path, name, t)
if t == "file" then
for _, suffix in ipairs({ "scm", "git", "dev" }) do
suffix = suffix .. "-1.rockspec"
if name:sub(-#suffix) == suffix then
rockspec_file = path
return false
end
end
end
end)
return rockspec_file
end
---@param plugin LazyPlugin
---@return LazyPkgSpec?
function M.get(plugin)
if Community.get_spec(plugin.name) then
return {
file = "community",
source = "lazy",
spec = Community.get_spec(plugin.name),
}
end
local rockspec_file = M.find_rockspec(plugin)
local rockspec = rockspec_file and M.rockspec(rockspec_file)
if not rockspec then
return
end
local has_lua = not not vim.uv.fs_stat(plugin.dir .. "/lua")
---@type LazyPluginSpec
local specs = {}
---@param dep string
local rocks = vim.tbl_filter(function(dep)
local name = dep:gsub("%s.*", "")
local url = Community.get_url(name)
local spec = Community.get_spec(name)
if spec then
-- community spec
table.insert(specs, spec)
return false
elseif url then
-- Neovim plugin rock
table.insert(specs, { url })
return false
end
return not vim.tbl_contains(M.skip, name)
end, rockspec.dependencies or {})
local use =
-- package without a /lua directory
not has_lua
-- has dependencies that are not skipped,
-- not in community specs,
-- and don't have a rockspec mapping
or #rocks > 0
-- has a complex build process
or not M.is_simple_build(rockspec)
if not use then
-- community specs only
return #specs > 0
and {
file = vim.fn.fnamemodify(rockspec_file, ":t"),
spec = {
plugin.name,
specs = specs,
build = false,
},
}
or nil
end
local lazy = nil
if not has_lua then
lazy = false
end
return {
file = vim.fn.fnamemodify(rockspec_file, ":t"),
spec = {
plugin.name,
build = "rockspec",
lazy = lazy,
},
}
end
return M

View file

@ -1,45 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.util")
---@type LazyState
local M = {}
---@class LazyState
local defaults = {
checker = {
last_check = 0,
},
}
---@type LazyState
local data = nil
function M.read()
pcall(function()
---@diagnostic disable-next-line: cast-local-type
data = vim.json.decode(Util.read_file(Config.options.state))
end)
data = vim.tbl_deep_extend("force", {}, defaults, data or {})
end
function M.write()
vim.fn.mkdir(vim.fn.fnamemodify(Config.options.state, ":p:h"), "p")
Util.write_file(Config.options.state, vim.json.encode(data))
end
function M.__index(_, key)
if not data then
M.read()
end
return data[key]
end
function M.__setindex(_, key, value)
if not data then
M.read()
end
---@diagnostic disable-next-line: no-unknown
data[key] = value
end
return setmetatable(M, M)

View file

@ -1,84 +0,0 @@
local ffi = require("ffi")
local M = {}
---@class LazyStats
M._stats = {
-- startuptime in milliseconds till UIEnter
startuptime = 0,
-- when true, startuptime is the accurate cputime for the Neovim process. (Linux & macOS)
-- this is more accurate than `nvim --startuptime`, and as such will be slightly higher
-- when false, startuptime is calculated based on a delta with a timestamp when lazy started.
real_cputime = false,
count = 0, -- total number of plugins
loaded = 0, -- number of loaded plugins
---@type table<string, number>
times = {},
}
---@type ffi.namespace*
M.C = nil
function M.on_ui_enter()
M._stats.startuptime = M.track("UIEnter")
require("lazy.core.util").track({ start = "startuptime" }, M._stats.startuptime * 1e6)
vim.api.nvim_exec_autocmds("User", { pattern = "LazyVimStarted", modeline = false })
end
function M.track(event)
local time = M.cputime()
M._stats.times[event] = time
return time
end
function M.cputime()
if M.C == nil then
pcall(function()
ffi.cdef([[
typedef long time_t;
typedef int clockid_t;
typedef struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
} nanotime;
int clock_gettime(clockid_t clk_id, struct timespec *tp);
]])
M.C = ffi.C
end)
end
local function real()
local pnano = assert(ffi.new("nanotime[?]", 1))
local CLOCK_PROCESS_CPUTIME_ID = jit.os == "OSX" and 12 or 2
ffi.C.clock_gettime(CLOCK_PROCESS_CPUTIME_ID, pnano)
return tonumber(pnano[0].tv_sec) * 1e3 + tonumber(pnano[0].tv_nsec) / 1e6
end
local function fallback()
return (vim.uv.hrtime() - require("lazy")._start) / 1e6
end
local ok, ret = pcall(real)
if ok then
M.cputime = real
M._stats.real_cputime = true
return ret
else
M.cputime = fallback
return fallback()
end
end
function M.stats()
M._stats.count = 0
M._stats.loaded = 0
for _, plugin in pairs(require("lazy.core.config").plugins) do
M._stats.count = M._stats.count + 1
if plugin._.loaded then
M._stats.loaded = M._stats.loaded + 1
end
end
return M._stats
end
return M

View file

@ -1,16 +0,0 @@
local Config = require("lazy.core.config")
local M = {}
function M.updates()
local Checker = require("lazy.manage.checker")
local updates = #Checker.updated
return updates > 0 and (Config.options.ui.icons.plugin .. "" .. updates)
end
function M.has_updates()
local Checker = require("lazy.manage.checker")
return #Checker.updated > 0
end
return M

View file

@ -1,71 +0,0 @@
---@class Ansi: table<string, fun(string):string>
local M = {}
M.colors = {
reset = "\27[0m",
black = "\27[30m",
red = "\27[31m",
green = "\27[32m",
yellow = "\27[33m",
blue = "\27[34m",
magenta = "\27[35m",
cyan = "\27[36m",
white = "\27[37m",
bright_black = "\27[90m",
bright_red = "\27[91m",
bright_green = "\27[92m",
bright_yellow = "\27[93m",
bright_blue = "\27[94m",
bright_magenta = "\27[95m",
bright_cyan = "\27[96m",
bright_white = "\27[97m",
}
function M.color(text, color)
return M.colors[color] .. text .. M.colors.reset
end
-- stylua: ignore start
function M.black(text) return M.color(text, "black") end
function M.red(text) return M.color(text, "red") end
function M.green(text) return M.color(text, "green") end
function M.yellow(text) return M.color(text, "yellow") end
function M.blue(text) return M.color(text, "blue") end
function M.magenta(text) return M.color(text, "magenta") end
function M.cyan(text) return M.color(text, "cyan") end
function M.white(text) return M.color(text, "white") end
function M.bright_black(text) return M.color(text, "bright_black") end
function M.bright_red(text) return M.color(text, "bright_red") end
function M.bright_green(text) return M.color(text, "bright_green") end
function M.bright_yellow(text) return M.color(text, "bright_yellow") end
function M.bright_blue(text) return M.color(text, "bright_blue") end
function M.bright_magenta(text) return M.color(text, "bright_magenta") end
function M.bright_cyan(text) return M.color(text, "bright_cyan") end
function M.bright_white(text) return M.color(text, "bright_white") end
-- stylua: ignore end
---@param data string
---@param prefix string
function M.prefix(data, prefix)
-- Normalize Windows-style newlines to simple newlines
data = data:gsub("\r\n", "\n")
-- Handle prefix for the first line, if data starts immediately
data = prefix .. data
-- Prefix new lines ensuring not to double prefix if a line starts with \r
data = data:gsub("(\n)([^\r])", "%1" .. prefix .. "%2")
-- Handle carriage returns properly to avoid double prefixing
-- Replace any \r not followed by \n with \r, then add a prefix only if the following character isn't the start of our prefix
data = data:gsub("\r([^\n])", function(nextChar)
if nextChar:sub(1, #prefix) == prefix then
return "\r" .. nextChar
else
return "\r" .. prefix .. nextChar
end
end)
return data
end
return M

View file

@ -1,102 +0,0 @@
---@alias LazyPluginKind "normal"|"clean"|"disabled"
---@class LazyPluginState
---@field cache? table<string,any>
---@field cloned? boolean
---@field cond? boolean
---@field dep? boolean True if this plugin is only in the spec as a dependency
---@field dir? string Explicit dir or dev set for this plugin
---@field dirty? boolean
---@field build? boolean
---@field frags? number[]
---@field top? boolean
---@field handlers? LazyPluginHandlers
---@field installed? boolean
---@field is_local? boolean
---@field kind? LazyPluginKind
---@field loaded? {[string]:string}|{time:number}
---@field outdated? boolean
---@field rtp_loaded? boolean
---@field tasks? LazyTask[]
---@field updated? {from:string, to:string}
---@field updates? {from:GitInfo, to:GitInfo}
---@field last_check? number
---@field working? boolean
---@field pkg? LazyPkg
---@alias PluginOpts table|fun(self:LazyPlugin, opts:table):table?
---@class LazyPluginHooks
---@field init? fun(self:LazyPlugin) Will always be run
---@field deactivate? fun(self:LazyPlugin) Unload/Stop a plugin
---@field config? fun(self:LazyPlugin, opts:table)|true Will be executed when loading the plugin
---@field build? false|string|async fun(self:LazyPlugin)|(string|async fun(self:LazyPlugin))[]
---@field opts? PluginOpts
---@class LazyPluginHandlers
---@field event? table<string,LazyEvent>
---@field ft? table<string,LazyEvent>
---@field keys? table<string,LazyKeys>
---@field cmd? table<string,string>
---@class LazyPluginRef
---@field branch? string
---@field tag? string
---@field commit? string
---@field version? string|boolean
---@field pin? boolean
---@field submodules? boolean Defaults to true
---@class LazyPluginBase
---@field [1] string?
---@field name string display name and name used for plugin config files
---@field main? string Entry module that has setup & deactivate
---@field url string?
---@field dir string
---@field enabled? boolean|(fun():boolean)
---@field cond? boolean|(fun():boolean)
---@field optional? boolean If set, then this plugin will not be added unless it is added somewhere else
---@field lazy? boolean
---@field priority? number Only useful for lazy=false plugins to force loading certain plugins first. Default priority is 50
---@field dev? boolean If set, then link to the respective folder under your ~/projects
---@field rocks? string[]
---@field virtual? boolean virtual plugins won't be installed or added to the rtp.
---@class LazyPlugin: LazyPluginBase,LazyPluginHandlers,LazyPluginHooks,LazyPluginRef
---@field dependencies? string[]
---@field specs? string|string[]|LazyPluginSpec[]
---@field _ LazyPluginState
---@class LazyPluginSpecHandlers
---@field event? string[]|string|LazyEventSpec[]|fun(self:LazyPlugin, event:string[]):string[]
---@field cmd? string[]|string|fun(self:LazyPlugin, cmd:string[]):string[]
---@field ft? string[]|string|fun(self:LazyPlugin, ft:string[]):string[]
---@field keys? string|string[]|LazyKeysSpec[]|fun(self:LazyPlugin, keys:string[]):((string|LazyKeys)[])
---@field module? false
---@class LazyPluginSpec: LazyPluginBase,LazyPluginSpecHandlers,LazyPluginHooks,LazyPluginRef
---@field name? string display name and name used for plugin config files
---@field dir? string
---@field dependencies? string|string[]|LazyPluginSpec[]
---@field specs? string|string[]|LazyPluginSpec[]
---@alias LazySpec string|LazyPluginSpec|LazySpecImport|LazySpec[]
---@class LazySpecImport
---@field import string|(fun():LazyPluginSpec) spec module to import
---@field name? string
---@field enabled? boolean|(fun():boolean)
---@field cond? boolean|(fun():boolean)
---@class LazyFragment
---@field id number
---@field pkg? boolean
---@field pid? number
---@field deps? number[]
---@field frags? number[]
---@field dep? boolean
---@field name string
---@field url? string
---@field dir? string
---@field spec LazyPlugin

View file

@ -1,50 +1,24 @@
---@class LazyUtil: LazyUtilCore
local M = setmetatable({}, { __index = require("lazy.core.util") }) local M = setmetatable({}, { __index = require("lazy.core.util") })
function M.file_exists(file) function M.file_exists(file)
return vim.uv.fs_stat(file) ~= nil return vim.loop.fs_stat(file) ~= nil
end end
---@param opts? LazyFloatOptions function M.open(uri)
---@return LazyFloat if M.file_exists(uri) then
function M.float(opts) return vim.cmd.view(uri)
return require("lazy.view.float")(opts)
end
function M.wo(win, k, v)
if vim.api.nvim_set_option_value then
vim.api.nvim_set_option_value(k, v, { scope = "local", win = win })
else
vim.wo[win][k] = v
end end
end
---@param opts? {system?:boolean}
function M.open(uri, opts)
opts = opts or {}
if not opts.system and M.file_exists(uri) then
return M.float({ style = "", file = uri })
end
local Config = require("lazy.core.config")
local cmd local cmd
if not opts.system and Config.options.ui.browser then if vim.fn.has("win32") == 1 then
cmd = { Config.options.ui.browser, uri } cmd = { "cmd.exe", "/c", "start", '""', vim.fn.shellescape(uri) }
elseif vim.fn.has("win32") == 1 then
cmd = { "explorer", uri }
elseif vim.fn.has("macunix") == 1 then elseif vim.fn.has("macunix") == 1 then
cmd = { "open", uri } cmd = { "open", uri }
else else
if vim.fn.executable("xdg-open") == 1 then cmd = { "xdg-open", uri }
cmd = { "xdg-open", uri }
elseif vim.fn.executable("wslview") == 1 then
cmd = { "wslview", uri }
else
cmd = { "open", uri }
end
end end
local ret = vim.fn.jobstart(cmd, { detach = true }) local ret = vim.fn.system(cmd)
if ret <= 0 then if vim.v.shell_error ~= 0 then
local msg = { local msg = {
"Failed to open uri", "Failed to open uri",
ret, ret,
@ -54,137 +28,28 @@ function M.open(uri, opts)
end end
end end
function M.read_file(file)
local fd = assert(io.open(file, "r"))
---@type string
local data = fd:read("*a")
fd:close()
return data
end
function M.write_file(file, contents)
local fd = assert(io.open(file, "w+"))
fd:write(contents)
fd:close()
end
---@generic F: fun()
---@param ms number ---@param ms number
---@param fn F ---@param fn fun()
---@return F
function M.throttle(ms, fn) function M.throttle(ms, fn)
---@type Async local timer = vim.loop.new_timer()
local async local running = false
local pending = false local first = true
return function() return function()
if async and async:running() then if not running then
pending = true if first then
return
end
---@async
async = require("lazy.async").new(function()
repeat
pending = false
fn() fn()
async:sleep(ms) first = false
end
until not pending timer:start(ms, 0, function()
end) running = false
end vim.schedule(fn)
end end)
--- Creates a weak reference to an object. running = true
--- Calling the returned function will return the object if it has not been garbage collected. end
---@generic T: table
---@param obj T
---@return T|fun():T?
function M.weak(obj)
local weak = { _obj = obj }
---@return table<any, any>
local function get()
local ret = rawget(weak, "_obj")
return ret == nil and error("Object has been garbage collected", 2) or ret
end end
local mt = {
__mode = "v",
__call = function(t)
return rawget(t, "_obj")
end,
__index = function(_, k)
return get()[k]
end,
__newindex = function(_, k, v)
get()[k] = v
end,
__pairs = function()
return pairs(get())
end,
}
return setmetatable(weak, mt)
end
---@class LazyCmdOptions: LazyFloatOptions
---@field cwd? string
---@field env? table<string,string>
---@field float? LazyFloatOptions
-- Opens a floating terminal (interactive by default)
---@param cmd? string[]|string
---@param opts? LazyCmdOptions|{interactive?:boolean}
function M.float_term(cmd, opts)
cmd = cmd or {}
if type(cmd) == "string" then
cmd = { cmd }
end
if #cmd == 0 then
cmd = { vim.o.shell }
end
opts = opts or {}
local float = M.float(opts)
vim.fn.termopen(cmd, vim.tbl_isempty(opts) and vim.empty_dict() or opts)
if opts.interactive ~= false then
vim.cmd.startinsert()
vim.api.nvim_create_autocmd("TermClose", {
once = true,
buffer = float.buf,
callback = function()
float:close({ wipe = true })
vim.cmd.checktime()
end,
})
end
return float
end
--- Runs the command and shows it in a floating window
---@param cmd string[]
---@param opts? LazyCmdOptions|{filetype?:string}
function M.float_cmd(cmd, opts)
opts = opts or {}
local Process = require("lazy.manage.process")
local lines, code = Process.exec(cmd, { cwd = opts.cwd })
if code ~= 0 then
M.error({
"`" .. table.concat(cmd, " ") .. "`",
"",
"## Error",
table.concat(lines, "\n"),
}, { title = "Command Failed (" .. code .. ")" })
return
end
local float = M.float(opts)
if opts.filetype then
vim.bo[float.buf].filetype = opts.filetype
end
vim.api.nvim_buf_set_lines(float.buf, 0, -1, false, lines)
vim.bo[float.buf].modifiable = false
return float
end
---@deprecated use float_term or float_cmd instead
function M.open_cmd()
M.warn([[`require("lazy.util").open_cmd()` is deprecated. Please use `float_term` instead. Check the docs]])
end end
---@return string? ---@return string?
@ -226,9 +91,9 @@ function M.markdown(msg, opts)
vim.tbl_deep_extend("force", { vim.tbl_deep_extend("force", {
title = "lazy.nvim", title = "lazy.nvim",
on_open = function(win) on_open = function(win)
M.wo(win, "conceallevel", 3) vim.wo[win].conceallevel = 3
M.wo(win, "concealcursor", "n") vim.wo[win].concealcursor = "n"
M.wo(win, "spell", false) vim.wo[win].spell = false
vim.treesitter.start(vim.api.nvim_win_get_buf(win), "markdown") vim.treesitter.start(vim.api.nvim_win_get_buf(win), "markdown")
end, end,
@ -242,25 +107,20 @@ function M._dump(value, result)
table.insert(result, tostring(value)) table.insert(result, tostring(value))
elseif t == "string" then elseif t == "string" then
table.insert(result, ("%q"):format(value)) table.insert(result, ("%q"):format(value))
elseif t == "table" and value._raw then
table.insert(result, value._raw)
elseif t == "table" then elseif t == "table" then
table.insert(result, "{") table.insert(result, "{")
for _, v in ipairs(value) do local i = 1
M._dump(v, result)
table.insert(result, ",")
end
---@diagnostic disable-next-line: no-unknown ---@diagnostic disable-next-line: no-unknown
for k, v in pairs(value) do for k, v in pairs(value) do
if type(k) == "string" then if k == i then
if k:match("^[a-zA-Z]+$") then elseif type(k) == "string" then
table.insert(result, ("%s="):format(k)) table.insert(result, ("[%q]="):format(k))
else else
table.insert(result, ("[%q]="):format(k)) table.insert(result, k .. "=")
end
M._dump(v, result)
table.insert(result, ",")
end end
M._dump(v, result)
table.insert(result, ",")
i = i + 1
end end
table.insert(result, "}") table.insert(result, "}")
else else
@ -274,22 +134,4 @@ function M.dump(value)
return table.concat(result, "") return table.concat(result, "")
end end
---@generic V
---@param t table<string, V>
---@param fn fun(key:string, value:V)
---@param opts? {case_sensitive?:boolean}
function M.foreach(t, fn, opts)
---@type string[]
local keys = vim.tbl_keys(t)
pcall(table.sort, keys, function(a, b)
if opts and opts.case_sensitive then
return a < b
end
return a:lower() < b:lower()
end)
for _, key in ipairs(keys) do
fn(key, t[key])
end
end
return M return M

View file

@ -1,51 +1,40 @@
local M = {} local M = {}
M.colors = { M.colors = {
H1 = "IncSearch", -- home button Error = "ErrorMsg",
H2 = "Bold", -- titles H1 = "IncSearch",
Comment = "Comment", H2 = "Bold",
Muted = "Comment",
Normal = "NormalFloat", Normal = "NormalFloat",
Commit = "@variable.builtin", -- commit ref Commit = "@variable.builtin",
CommitIssue = "Number", Key = "Conceal",
CommitType = "Title", -- conventional commit type Value = "@string",
CommitScope = "Italic", -- conventional commit scope ProgressDone = {
Dimmed = "Conceal", -- property bold = true,
Prop = "Conceal", -- property default = true,
Value = "@string", -- value of a property fg = "#ff007c",
NoCond = "DiagnosticWarn", -- unloaded icon for a plugin where `cond()` was false },
Local = "Constant", ProgressTodo = "LineNr",
ProgressDone = "Constant", -- progress bar done
ProgressTodo = "LineNr", -- progress bar todo
Special = "@punctuation.special", Special = "@punctuation.special",
ReasonRuntime = "@macro", LoaderPlugin = "Special",
ReasonPlugin = "Special", LoaderEvent = "Constant",
ReasonEvent = "Constant", LoaderKeys = "Statement",
ReasonKeys = "Statement", LoaderStart = "@field",
ReasonStart = "@variable.member", LoaderSource = "Character",
ReasonSource = "Character", LoaderCmd = "Operator",
ReasonFt = "Character",
ReasonCmd = "Operator",
ReasonImport = "Identifier",
ReasonRequire = "@variable.parameter",
Button = "CursorLine", Button = "CursorLine",
ButtonActive = "Visual", ButtonActive = "Visual",
TaskOutput = "MsgArea", -- task output
Error = "DiagnosticError", -- task errors
Warning = "DiagnosticWarn", -- task errors
Info = "DiagnosticInfo", -- task errors
Dir = "@markup.link", -- directory
Url = "@markup.link", -- url
Bold = { bold = true },
Italic = { italic = true },
} }
M.did_setup = false M.did_setup = false
function M.set_hl() function M.set_hl()
for hl_group, link in pairs(M.colors) do for hl_group, opts in pairs(M.colors) do
local hl = type(link) == "table" and link or { link = link } if type(opts) == "string" then
hl.default = true opts = { link = opts }
vim.api.nvim_set_hl(0, "Lazy" .. hl_group, hl) end
opts.default = true
vim.api.nvim_set_hl(0, "Lazy" .. hl_group, opts)
end end
end end
@ -57,12 +46,13 @@ function M.setup()
M.did_setup = true M.did_setup = true
M.set_hl() M.set_hl()
vim.api.nvim_create_autocmd("VimEnter", { vim.api.nvim_create_autocmd("ColorScheme", {
callback = function() callback = function()
M.set_hl() M.set_hl()
end, end,
}) })
vim.api.nvim_create_autocmd("ColorScheme", { vim.api.nvim_create_autocmd("User", {
pattern = "VeryLazy",
callback = function() callback = function()
M.set_hl() M.set_hl()
end, end,

View file

@ -1,158 +1,89 @@
local require = require("lazy.core.util").lazy_require local View = require("lazy.view")
local Config = require("lazy.core.config")
local Manage = require("lazy.manage") local Manage = require("lazy.manage")
local Util = require("lazy.util") local Util = require("lazy.util")
local View = require("lazy.view")
local ViewConfig = require("lazy.view.config")
local M = {} local M = {}
---@param cmd string ---@param cmd string
---@param opts? ManagerOpts ---@param plugins? LazyPlugin[]
function M.cmd(cmd, opts) function M.cmd(cmd, plugins)
cmd = cmd == "" and "home" or cmd cmd = cmd == "" and "show" or cmd
local command = M.commands[cmd] --[[@as fun(opts)]] local command = M.commands[cmd]
if command == nil then if command == nil then
Util.error("Invalid lazy command '" .. cmd .. "'") Util.error("Invalid lazy command '" .. cmd .. "'")
elseif
ViewConfig.commands[cmd]
and ViewConfig.commands[cmd].plugins_required
and not (opts and vim.tbl_count(opts.plugins or {}) > 0)
then
return Util.error("`Lazy " .. cmd .. "` requires at least one plugin")
else else
command(opts) command(plugins)
end end
end end
---@class LazyCommands
M.commands = { M.commands = {
clean = function(plugins)
Manage.clean({ clear = true, mode = "clean", plugins = plugins })
end,
clear = function() clear = function()
Manage.clear() Manage.clear()
View.show() View.show()
end, end,
health = function() install = function()
vim.cmd.checkhealth("lazy") Manage.install({ clear = true, mode = "install" })
end, end,
---@param opts ManagerOpts log = function(plugins)
pkg = function(opts) Manage.log({ clear = true, mode = "log", plugins = plugins })
local Pkg = require("lazy.pkg")
Pkg.update()
require("lazy.manage.reloader").reload({
{
file = "pkg",
what = "changed",
},
})
for _, plugin in pairs(opts and opts.plugins or {}) do
local spec = Pkg.get(plugin.dir)
Util.info(vim.inspect(spec), { lang = "lua", title = plugin.name })
end
end,
home = function()
View.show("home")
end, end,
show = function() show = function()
View.show("home") View.show()
end, end,
help = function() help = function()
View.show("help") View.show("help")
end, end,
debug = function()
View.show("debug")
end,
profile = function() profile = function()
View.show("profile") View.show("profile")
end, end,
---@param opts ManagerOpts sync = function()
load = function(opts) Manage.clean({ clear = true, wait = true, mode = "sync" })
-- when a command is executed with a bang, wait will be set Manage.update({ interactive = true })
require("lazy.core.loader").load(opts.plugins, { cmd = "Lazy load" }, { force = opts.wait }) Manage.install({ interactive = true })
end, end,
reload = function(opts) update = function(plugins)
for _, plugin in pairs(opts.plugins) do Manage.update({ clear = true, mode = "update", plugins = plugins })
if type(plugin) == "string" then end,
plugin = Config.plugins[plugin] check = function(plugins)
end Manage.check({ clear = true, mode = "check", plugins = plugins })
Util.warn("Reloading **" .. plugin.name .. "**") end,
require("lazy.core.loader").reload(plugin) restore = function(plugins)
end Manage.update({ clear = true, lockfile = true, mode = "restore", plugins = plugins })
end, end,
log = Manage.log,
build = Manage.build,
clean = Manage.clean,
install = Manage.install,
sync = Manage.sync,
update = Manage.update,
check = Manage.check,
restore = Manage.restore,
} }
function M.complete(cmd, prefix)
if not (ViewConfig.commands[cmd] or {}).plugins and cmd ~= "pkg" then
return
end
---@type string[]
local plugins = {}
if cmd == "load" then
plugins[#plugins + 1] = "all"
end
for name, plugin in pairs(Config.plugins) do
if cmd ~= "load" or not plugin._.loaded then
plugins[#plugins + 1] = name
end
end
table.sort(plugins)
---@param key string
return vim.tbl_filter(function(key)
return key:find(prefix, 1, true) == 1
end, plugins)
end
function M.setup() function M.setup()
vim.api.nvim_create_user_command("Lazy", function(cmd) vim.api.nvim_create_user_command("Lazy", function(args)
---@type ManagerOpts M.cmd(vim.trim(args.args or ""))
local opts = { wait = cmd.bang == true }
local prefix, args = M.parse(cmd.args)
if #args == 1 and args[1] == "all" then
args = vim.tbl_keys(Config.plugins)
end
if #args > 0 then
---@param plugin string
opts.plugins = vim.tbl_map(function(plugin)
return Config.plugins[plugin]
end, args)
end
M.cmd(prefix, opts)
end, { end, {
bar = true,
bang = true,
nargs = "?", nargs = "?",
desc = "Lazy", desc = "Lazy",
complete = function(_, line) complete = function(_, line)
local prefix, args = M.parse(line) if line:match("^%s*Lazy %w+ ") then
if #args > 0 then return {}
return M.complete(prefix, args[#args])
end end
local prefix = line:match("^%s*Lazy (%w*)") or ""
---@param key string ---@param key string
return vim.tbl_filter(function(key) return vim.tbl_filter(function(key)
return key:find(prefix, 1, true) == 1 return key:find(prefix) == 1
end, vim.tbl_keys(M.commands)) end, vim.tbl_keys(M.commands))
end, end,
}) })
end
---@return string, string[] for name in pairs(M.commands) do
function M.parse(args) local cmd = "Lazy" .. name:sub(1, 1):upper() .. name:sub(2)
local parts = vim.split(vim.trim(args), "%s+")
if vim.startswith("Lazy", parts[1]) then vim.api.nvim_create_user_command(cmd, function()
table.remove(parts, 1) M.cmd(name)
end, {
desc = "Lazy " .. name,
})
end end
if args:sub(-1) == " " then
parts[#parts + 1] = ""
end
return table.remove(parts, 1) or "", parts
end end
return M return M

View file

@ -1,161 +0,0 @@
local M = {}
---@class LazyViewCommand
---@field id number
---@field plugins? boolean
---@field plugins_required? boolean
---@field button? boolean
---@field desc? string
---@field desc_plugin? string
---@field key? string
---@field key_plugin? string
---@field toggle? boolean
function M.get_commands()
---@type (LazyViewCommand|{name:string})[]
local ret = {}
for k, v in pairs(M.commands) do
v.name = k
ret[#ret + 1] = v
end
table.sort(ret, function(a, b)
return a.id < b.id
end)
return ret
end
M.dimmed_commits = { "bot", "build", "ci", "chore", "doc", "style", "test" }
M.keys = {
hover = "K",
diff = "d",
close = "q",
details = "<cr>",
profile_sort = "<C-s>",
profile_filter = "<C-f>",
abort = "<C-c>",
next = "]]",
prev = "[[",
}
---@type table<string,LazyViewCommand>
M.commands = {
home = {
button = true,
desc = "Go back to plugin list",
id = 1,
key = "H",
},
install = {
button = true,
desc = "Install missing plugins",
desc_plugin = "Install a plugin",
id = 2,
key = "I",
key_plugin = "i",
plugins = true,
},
update = {
button = true,
desc = "Update plugins. This will also update the lockfile",
desc_plugin = "Update a plugin. This will also update the lockfile",
id = 3,
key = "U",
key_plugin = "u",
plugins = true,
},
sync = {
button = true,
desc = "Run install, clean and update",
desc_plugin = "Run install, clean and update",
id = 4,
key = "S",
plugins = true,
},
clean = {
button = true,
desc = "Clean plugins that are no longer needed",
desc_plugin = "Delete a plugin. WARNING: this will delete the plugin even if it should be installed!",
id = 5,
key = "X",
key_plugin = "x",
plugins = true,
},
check = {
button = true,
desc = "Check for updates and show the log (git fetch)",
desc_plugin = "Check for updates and show the log (git fetch)",
id = 6,
key = "C",
key_plugin = "c",
plugins = true,
},
log = {
button = true,
desc = "Show recent updates",
desc_plugin = "Show recent updates",
id = 7,
key = "L",
key_plugin = "gl",
plugins = true,
},
restore = {
button = true,
desc = "Updates all plugins to the state in the lockfile. For a single plugin: restore it to the state in the lockfile or to a given commit under the cursor",
desc_plugin = "Restore a plugin to the state in the lockfile or to a given commit under the cursor",
id = 8,
key = "R",
key_plugin = "r",
plugins = true,
},
profile = {
button = true,
desc = "Show detailed profiling",
id = 9,
key = "P",
toggle = true,
},
debug = {
button = true,
desc = "Show debug information",
id = 10,
key = "D",
toggle = true,
},
help = {
button = true,
desc = "Toggle this help page",
id = 11,
key = "?",
toggle = true,
},
clear = {
desc = "Clear finished tasks",
id = 12,
},
load = {
desc = "Load a plugin that has not been loaded yet. Similar to `:packadd`. Like `:Lazy load foo.nvim`. Use `:Lazy! load` to skip `cond` checks.",
id = 13,
plugins = true,
plugins_required = true,
},
health = {
desc = "Run `:checkhealth lazy`",
id = 14,
},
build = {
desc = "Rebuild a plugin",
id = 15,
plugins = true,
plugins_required = true,
key_plugin = "gb",
},
reload = {
desc = "Reload a plugin (experimental!!)",
plugins = true,
plugins_required = true,
id = 16,
},
}
return M

View file

@ -1,64 +0,0 @@
local Util = require("lazy.util")
local M = {}
---@alias LazyDiff {commit:string} | {from:string, to:string}
---@alias LazyDiffFun fun(plugin:LazyPlugin, diff:LazyDiff)
M.handlers = {
---@type LazyDiffFun
browser = function(plugin, diff)
if plugin.url then
local url = plugin.url:gsub("%.git$", "")
if diff.commit then
Util.open(url .. "/commit/" .. diff.commit)
else
Util.open(url .. "/compare/" .. diff.from .. ".." .. diff.to)
end
else
Util.error("No url for " .. plugin.name)
end
end,
---@type LazyDiffFun
["diffview.nvim"] = function(plugin, diff)
local args
if diff.commit then
args = ("-C=%s"):format(plugin.dir) .. " " .. diff.commit .. "^!"
else
args = ("-C=%s"):format(plugin.dir) .. " " .. diff.from .. ".." .. diff.to
end
vim.cmd("DiffviewOpen " .. args)
end,
---@type LazyDiffFun
git = function(plugin, diff)
local cmd = { "git" }
if diff.commit then
cmd[#cmd + 1] = "show"
cmd[#cmd + 1] = diff.commit
else
cmd[#cmd + 1] = "diff"
cmd[#cmd + 1] = diff.from
cmd[#cmd + 1] = diff.to
end
Util.float_cmd(cmd, { cwd = plugin.dir, filetype = "git" })
end,
---@type LazyDiffFun
terminal_git = function(plugin, diff)
local cmd = { "git" }
if diff.commit then
cmd[#cmd + 1] = "show"
cmd[#cmd + 1] = diff.commit
else
cmd[#cmd + 1] = "diff"
cmd[#cmd + 1] = diff.from
cmd[#cmd + 1] = diff.to
end
Util.float_term(cmd, { cwd = plugin.dir, interactive = false, env = { PAGER = "cat" } })
end,
}
return M

View file

@ -1,354 +0,0 @@
local Config = require("lazy.core.config")
local Util = require("lazy.util")
local ViewConfig = require("lazy.view.config")
---@class LazyFloatOptions
---@field buf? number
---@field file? string
---@field margin? {top?:number, right?:number, bottom?:number, left?:number}
---@field size? {width:number, height:number}
---@field zindex? number
---@field style? "" | "minimal"
---@field border? "none" | "single" | "double" | "rounded" | "solid" | "shadow"
---@field title? string
---@field title_pos? "center" | "left" | "right"
---@field persistent? boolean
---@field ft? string
---@field noautocmd? boolean
---@field backdrop? float
---@class LazyFloat
---@field buf number
---@field win number
---@field opts LazyFloatOptions
---@field win_opts LazyWinOpts
---@field backdrop_buf number
---@field backdrop_win number
---@field id number
---@overload fun(opts?:LazyFloatOptions):LazyFloat
local M = {}
setmetatable(M, {
__call = function(_, ...)
return M.new(...)
end,
})
local _id = 0
local function next_id()
_id = _id + 1
return _id
end
---@param opts? LazyFloatOptions
function M.new(opts)
local self = setmetatable({}, { __index = M })
return self:init(opts)
end
---@param opts? LazyFloatOptions
function M:init(opts)
require("lazy.view.colors").setup()
self.id = next_id()
self.opts = vim.tbl_deep_extend("force", {
size = Config.options.ui.size,
style = "minimal",
border = Config.options.ui.border or "none",
backdrop = Config.options.ui.backdrop or 60,
zindex = 50,
}, opts or {})
---@class LazyWinOpts
---@field width number
---@field height number
---@field row number
---@field col number
self.win_opts = {
relative = "editor",
style = self.opts.style ~= "" and self.opts.style or nil,
border = self.opts.border,
zindex = self.opts.zindex,
noautocmd = self.opts.noautocmd,
title = self.opts.title,
title_pos = self.opts.title and self.opts.title_pos or nil,
}
self:mount()
self:on("VimEnter", function()
vim.schedule(function()
if not self:win_valid() then
self:close()
end
end)
end, { buffer = false })
return self
end
function M:layout()
local function size(max, value)
return value > 1 and math.min(value, max) or math.floor(max * value)
end
self.win_opts.width = size(vim.o.columns, self.opts.size.width)
self.win_opts.height = size(vim.o.lines, self.opts.size.height)
self.win_opts.row = math.floor((vim.o.lines - self.win_opts.height) / 2)
self.win_opts.col = math.floor((vim.o.columns - self.win_opts.width) / 2)
if self.opts.border ~= "none" then
self.win_opts.row = self.win_opts.row - 1
self.win_opts.col = self.win_opts.col - 1
end
if self.opts.margin then
if self.opts.margin.top then
self.win_opts.height = self.win_opts.height - self.opts.margin.top
self.win_opts.row = self.win_opts.row + self.opts.margin.top
end
if self.opts.margin.right then
self.win_opts.width = self.win_opts.width - self.opts.margin.right
end
if self.opts.margin.bottom then
self.win_opts.height = self.win_opts.height - self.opts.margin.bottom
end
if self.opts.margin.left then
self.win_opts.width = self.win_opts.width - self.opts.margin.left
self.win_opts.col = self.win_opts.col + self.opts.margin.left
end
end
end
function M:mount()
if self:buf_valid() then
-- keep existing buffer
self.buf = self.buf
elseif self.opts.file then
self.buf = vim.fn.bufadd(self.opts.file)
vim.bo[self.buf].readonly = true
vim.bo[self.buf].swapfile = false
vim.fn.bufload(self.buf)
vim.bo[self.buf].modifiable = false
elseif self.opts.buf then
self.buf = self.opts.buf
else
self.buf = vim.api.nvim_create_buf(false, true)
end
local normal, has_bg
if vim.fn.has("nvim-0.9.0") == 0 then
normal = vim.api.nvim_get_hl_by_name("Normal", true)
has_bg = normal and normal.background ~= nil
else
normal = vim.api.nvim_get_hl(0, { name = "Normal" })
has_bg = normal and normal.bg ~= nil
end
if has_bg and self.opts.backdrop and self.opts.backdrop < 100 and vim.o.termguicolors then
self.backdrop_buf = vim.api.nvim_create_buf(false, true)
self.backdrop_win = vim.api.nvim_open_win(self.backdrop_buf, false, {
relative = "editor",
width = vim.o.columns,
height = vim.o.lines,
row = 0,
col = 0,
style = "minimal",
focusable = false,
zindex = self.opts.zindex - 1,
})
vim.api.nvim_set_hl(0, "LazyBackdrop", { bg = "#000000", default = true })
Util.wo(self.backdrop_win, "winhighlight", "Normal:LazyBackdrop")
Util.wo(self.backdrop_win, "winblend", self.opts.backdrop)
vim.bo[self.backdrop_buf].buftype = "nofile"
vim.bo[self.backdrop_buf].filetype = "lazy_backdrop"
end
self:layout()
self.win = vim.api.nvim_open_win(self.buf, true, self.win_opts)
self:on("WinClosed", function()
self:close()
self:augroup(true)
end, { win = true })
self:focus()
self:on_key(ViewConfig.keys.close, self.close, "Close")
self:on({ "BufDelete", "BufHidden" }, self.close)
if vim.bo[self.buf].buftype == "" then
vim.bo[self.buf].buftype = "nofile"
end
if vim.bo[self.buf].filetype == "" then
vim.bo[self.buf].filetype = self.opts.ft or "lazy"
end
local function opts()
vim.bo[self.buf].bufhidden = self.opts.persistent and "hide" or "wipe"
Util.wo(self.win, "conceallevel", 3)
Util.wo(self.win, "foldenable", false)
Util.wo(self.win, "spell", false)
Util.wo(self.win, "wrap", true)
Util.wo(self.win, "winhighlight", "Normal:LazyNormal")
Util.wo(self.win, "colorcolumn", "")
end
opts()
vim.api.nvim_create_autocmd("VimResized", {
callback = function()
if not (self.win and vim.api.nvim_win_is_valid(self.win)) then
return true
end
self:layout()
local config = {}
for _, key in ipairs({ "relative", "width", "height", "col", "row" }) do
---@diagnostic disable-next-line: no-unknown
config[key] = self.win_opts[key]
end
config.style = self.opts.style ~= "" and self.opts.style or nil
vim.api.nvim_win_set_config(self.win, config)
if self.backdrop_win and vim.api.nvim_win_is_valid(self.backdrop_win) then
vim.api.nvim_win_set_config(self.backdrop_win, {
width = vim.o.columns,
height = vim.o.lines,
})
end
opts()
vim.api.nvim_exec_autocmds("User", { pattern = "LazyFloatResized", modeline = false })
end,
})
end
---@param clear? boolean
function M:augroup(clear)
return vim.api.nvim_create_augroup("trouble.window." .. self.id, { clear = clear == true })
end
---@param events string|string[]
---@param fn fun(self:LazyFloat, event:{buf:number}):boolean?
---@param opts? vim.api.keyset.create_autocmd | {buffer: false, win?:boolean}
function M:on(events, fn, opts)
opts = opts or {}
if opts.win then
opts.pattern = self.win .. ""
opts.win = nil
elseif opts.buffer == nil then
opts.buffer = self.buf
elseif opts.buffer == false then
opts.buffer = nil
end
if opts.pattern then
opts.buffer = nil
end
local _self = Util.weak(self)
opts.callback = function(e)
local this = _self()
if not this then
-- delete the autocmd
return true
end
return fn(this, e)
end
opts.group = self:augroup()
vim.api.nvim_create_autocmd(events, opts)
end
---@param key string
---@param fn fun(self?)
---@param desc? string
---@param mode? string[]
function M:on_key(key, fn, desc, mode)
vim.keymap.set(mode or "n", key, function()
fn(self)
end, {
nowait = true,
buffer = self.buf,
desc = desc,
})
end
---@param opts? {wipe:boolean}
function M:close(opts)
self:augroup(true)
local buf = self.buf
local win = self.win
local wipe = opts and opts.wipe
if wipe == nil then
wipe = not self.opts.persistent
end
self.win = nil
if wipe then
self.buf = nil
end
local backdrop_buf = self.backdrop_buf
local backdrop_win = self.backdrop_win
self.backdrop_buf = nil
self.backdrop_win = nil
vim.schedule(function()
if backdrop_win and vim.api.nvim_win_is_valid(backdrop_win) then
vim.api.nvim_win_close(backdrop_win, true)
end
if backdrop_buf and vim.api.nvim_buf_is_valid(backdrop_buf) then
vim.api.nvim_buf_delete(backdrop_buf, { force = true })
end
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
if wipe and buf and vim.api.nvim_buf_is_valid(buf) then
vim.diagnostic.reset(Config.ns, buf)
vim.api.nvim_buf_delete(buf, { force = true })
end
vim.cmd.redraw()
end)
end
function M:win_valid()
return self.win and vim.api.nvim_win_is_valid(self.win)
end
function M:buf_valid()
return self.buf and vim.api.nvim_buf_is_valid(self.buf)
end
function M:hide()
if self:win_valid() then
self:close({ wipe = false })
end
end
function M:toggle()
if self:win_valid() then
self:hide()
return false
else
self:show()
return true
end
end
function M:show()
if self:win_valid() then
self:focus()
elseif self:buf_valid() then
self:mount()
else
error("LazyFloat: buffer closed")
end
end
function M:focus()
vim.api.nvim_set_current_win(self.win)
-- it seems that setting the current win doesn't work before VimEnter,
-- so do that then
if vim.v.vim_did_enter ~= 1 then
vim.api.nvim_create_autocmd("VimEnter", {
once = true,
callback = function()
if self.win and vim.api.nvim_win_is_valid(self.win) then
pcall(vim.api.nvim_set_current_win, self.win)
end
return true
end,
})
end
end
return M

View file

@ -1,364 +1,220 @@
local Config = require("lazy.core.config")
local Diff = require("lazy.view.diff")
local Float = require("lazy.view.float")
local Git = require("lazy.manage.git")
local Render = require("lazy.view.render")
local Util = require("lazy.util") local Util = require("lazy.util")
local ViewConfig = require("lazy.view.config") local Render = require("lazy.view.render")
local Config = require("lazy.core.config")
---@class LazyViewState
---@field mode string
---@field plugin? {name:string, kind?: LazyPluginKind}
local default_state = {
mode = "home",
profile = {
threshold = 0,
sort_time_taken = false,
},
}
---@class LazyView: LazyFloat
---@field render LazyRender
---@field state LazyViewState
local M = {} local M = {}
---@type LazyView M.modes = {
M.view = nil { name = "install", key = "I", desc = "Install missing plugins" },
{ name = "update", key = "U", desc = "Update all plugins. This will also update the lockfile" },
{ name = "sync", key = "S", desc = "Run install, clean and update" },
{ name = "clean", key = "X", desc = "Clean plugins that are no longer needed" },
{ name = "check", key = "C", desc = "Check for updates and show the log (git fetch)" },
{ name = "log", key = "L", desc = "Show recent updates for all plugins" },
{ name = "restore", key = "R", desc = "Updates all plugins to the state in the lockfile" },
{ name = "profile", key = "P", desc = "Show detailed profiling", toggle = true },
{ name = "help", key = "?", hide = true, desc = "Toggle this help page", toggle = true },
function M.visible() { plugin = true, name = "update", key = "u", desc = "Update this plugin. This will also update the lockfile" },
return M.view and M.view.win and vim.api.nvim_win_is_valid(M.view.win) {
plugin = true,
name = "clean",
key = "x",
desc = "Delete this plugin. WARNING: this will delete the plugin even if it should be installed!",
},
{ plugin = true, name = "check", key = "c", desc = "Check for updates for this plugin and show the log (git fetch)" },
{ plugin = true, name = "install", key = "i", desc = "Install this plugin" },
{ plugin = true, name = "log", key = "gl", desc = "Show recent updates for this plugin" },
{ plugin = true, name = "restore", key = "r", desc = "Restore this plugin to the state in the lockfile" },
}
---@type string?
M.mode = nil
function M.setup()
require("lazy.view.commands").setup()
require("lazy.view.colors").setup()
end end
---@param mode? string
function M.show(mode) function M.show(mode)
if Config.headless() then M.mode = mode or M.mode
require("lazy.view.colors").setup()
if M._buf and vim.api.nvim_buf_is_valid(M._buf) then
vim.api.nvim_win_set_cursor(M._win, { 1, 0 })
vim.cmd([[do User LazyRender]])
return return
end end
M.view = M.visible() and M.view or M.create() local buf = vim.api.nvim_create_buf(false, false)
if mode then M._buf = buf
M.view.state.mode = mode local vpad = 6
local hpad = 20
local opts = {
relative = "editor",
style = "minimal",
border = Config.options.ui.border,
width = math.min(vim.o.columns - hpad * 2, 200),
height = math.min(vim.o.lines - vpad * 2, 70),
}
opts.row = (vim.o.lines - opts.height) / 2
opts.col = (vim.o.columns - opts.width) / 2
local win = vim.api.nvim_open_win(buf, true, opts)
M._win = win
vim.api.nvim_set_current_win(win)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "wipe"
vim.wo[win].conceallevel = 3
vim.wo[win].spell = false
vim.wo[win].wrap = true
vim.wo[win].winhighlight = "Normal:LazyNormal"
local function close()
M._buf = nil
vim.diagnostic.reset(Config.ns, buf)
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_delete(buf, {
force = true,
})
end
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
end end
M.view:update()
end
---@param plugin LazyPlugin vim.keymap.set("n", "q", close, {
function M:is_selected(plugin) nowait = true,
return vim.deep_equal(self.state.plugin, { name = plugin.name, kind = plugin._.kind }) buffer = buf,
end
function M.create()
local self = setmetatable({}, { __index = setmetatable(M, { __index = Float }) })
---@cast self LazyView
Float.init(self, {
title = Config.options.ui.title,
title_pos = Config.options.ui.title_pos,
noautocmd = false,
}) })
if Config.options.ui.wrap then vim.api.nvim_create_autocmd({ "BufDelete", "BufLeave", "BufHidden" }, {
Util.wo(self.win, "wrap", true) once = true,
Util.wo(self.win, "linebreak", true) buffer = buf,
Util.wo(self.win, "breakindent", true) callback = close,
else })
Util.wo(self.win, "wrap", false)
end
self.state = vim.deepcopy(default_state) local render = Render.new(buf, win, 2)
local update = Util.throttle(Config.options.ui.throttle, function()
self.render = Render.new(self) if buf and vim.api.nvim_buf_is_valid(buf) then
local update = self.update vim.bo[buf].modifiable = true
self.update = Util.throttle(Config.options.ui.throttle, function() render:update()
update(self) vim.bo[buf].modifiable = false
vim.cmd.redraw()
end
end) end)
for _, pattern in ipairs({ "LazyRender", "LazyFloatResized" }) do local function get_plugin()
self:on({ "User" }, function() local pos = vim.api.nvim_win_get_cursor(win)
if not (self.buf and vim.api.nvim_buf_is_valid(self.buf)) then return render:get_plugin(pos[1])
return true
end
self:update()
end, { pattern = pattern })
end end
vim.keymap.set("n", ViewConfig.keys.abort, function() vim.keymap.set("n", "<cr>", function()
require("lazy.manage.process").abort() local plugin = get_plugin()
require("lazy.async").abort()
return ViewConfig.keys.abort
end, { silent = true, buffer = self.buf, expr = true, desc = "Abort" })
vim.keymap.set("n", "gx", "K", { buffer = self.buf, remap = true })
-- plugin details
self:on_key(ViewConfig.keys.details, function()
local plugin = self.render:get_plugin()
if plugin then if plugin then
local selected = { if render._details == plugin.name then
name = plugin.name, render._details = nil
kind = plugin._.kind, else
} render._details = plugin.name
local open = not vim.deep_equal(self.state.plugin, selected)
if not open then
local row = self.render:get_row(selected)
if row then
vim.api.nvim_win_set_cursor(self.view.win, { row, 8 })
end
end end
update()
self.state.plugin = open and selected or nil
self:update()
end end
end, "Details") end, {
nowait = true,
buffer = buf,
})
self:on_key(ViewConfig.keys.next, function() local function open(path)
local cursor = vim.api.nvim_win_get_cursor(self.view.win) local plugin = get_plugin()
for l = 1, #self.render.locations, 1 do if plugin then
local loc = self.render.locations[l] local url = plugin.uri:gsub("%.git$", "")
if loc.from > cursor[1] then if Util.file_exists(url) then
vim.api.nvim_win_set_cursor(self.view.win, { loc.from, 8 }) url = "https://github.com/" .. plugin[1]
return
end end
end
end, "Next Plugin")
self:on_key(ViewConfig.keys.prev, function()
local cursor = vim.api.nvim_win_get_cursor(self.view.win)
for l = #self.render.locations, 1, -1 do
local loc = self.render.locations[l]
if loc.from < cursor[1] then
vim.api.nvim_win_set_cursor(self.view.win, { loc.from, 8 })
return
end
end
end, "Prev Plugin")
self:on_key(ViewConfig.keys.profile_sort, function()
if self.state.mode == "profile" then
self.state.profile.sort_time_taken = not self.state.profile.sort_time_taken
self:update()
end
end, "Sort Profile")
self:on_key(ViewConfig.keys.profile_filter, function()
if self.state.mode == "profile" then
vim.ui.input({
prompt = "Enter time threshold in ms: ",
default = tostring(self.state.profile.threshold),
}, function(input)
if not input then
return
end
local num = input == "" and 0 or tonumber(input)
if not num then
Util.error("Please input a number")
else
self.state.profile.threshold = num
self:update()
end
end)
end
end, "Filter Profile")
for lhs, rhs in pairs(Config.options.ui.custom_keys) do
if rhs then
local handler = type(rhs) == "table" and rhs[1] or rhs
local desc = type(rhs) == "table" and rhs.desc or nil
self:on_key(lhs, function()
local plugin = self.render:get_plugin()
if plugin then
handler(plugin)
end
end, desc)
end
end
self:setup_patterns()
self:setup_modes()
return self
end
function M:update()
if self.buf and vim.api.nvim_buf_is_valid(self.buf) then
self.render:update()
vim.cmd.redraw()
end
end
function M:open_url(path)
local plugin = self.render:get_plugin()
if plugin then
if plugin.url then
local url = plugin.url:gsub("%.git$", "")
Util.open(url .. path) Util.open(url .. path)
else
Util.error("No url for " .. plugin.name)
end end
end end
end
function M:setup_patterns() M.keys(buf, {
local commit_pattern = "%f[%w](" .. string.rep("[a-f0-9]", 7) .. ")%f[%W]" ["%s(" .. string.rep("[a-z0-9]", 7) .. ")%s"] = function(hash)
self:on_pattern(ViewConfig.keys.hover, { open("/commit/" .. hash)
[commit_pattern] = function(hash) end,
self:diff({ commit = hash, browser = true }) ["%s(" .. string.rep("[a-z0-9]", 7) .. ")$"] = function(hash)
open("/commit/" .. hash)
end,
["^(" .. string.rep("[a-z0-9]", 7) .. ")%s"] = function(hash)
open("/commit/" .. hash)
end, end,
["#(%d+)"] = function(issue) ["#(%d+)"] = function(issue)
self:open_url("/issues/" .. issue) open("/issues/" .. issue)
end, end,
["README.md"] = function() ["README.md"] = function()
local plugin = self.render:get_plugin() local plugin = get_plugin()
if plugin then Util.open(plugin.dir .. "/README.md")
Util.open(plugin.dir .. "/README.md")
end
end,
["|(%S-)|"] = function(h)
vim.cmd.help(h)
self:close()
end, end,
["(https?://%S+)"] = function(url) ["(https?://%S+)"] = function(url)
Util.open(url) Util.open(url)
end, end,
}, self.hover, "Hover") })
self:on_pattern(ViewConfig.keys.diff, {
[commit_pattern] = function(hash)
self:diff({ commit = hash })
end,
}, self.diff, "Diff")
self:on_pattern(ViewConfig.commands.restore.key_plugin, {
[commit_pattern] = function(hash)
self:restore({ commit = hash })
end,
}, self.restore, "Restore")
end
---@param opts? {commit:string} for _, m in ipairs(M.modes) do
function M:restore(opts) vim.keymap.set("n", m.key, function()
opts = opts or {} local Commands = require("lazy.view.commands")
local Lockfile = require("lazy.manage.lock") if m.plugin then
local Commands = require("lazy.view.commands") local plugin = get_plugin()
local plugin = self.render:get_plugin() if plugin then
if plugin then Commands.cmd(m.name, { plugin })
if opts.commit then
Lockfile.get(plugin).commit = opts.commit
end
Commands.cmd("restore", { plugins = { plugin } })
end
end
function M:hover()
if self:diff({ browser = true, hover = true }) then
return
end
self:open_url("")
end
---@param opts? {commit?:string, browser:boolean, hover:boolean}
function M:diff(opts)
opts = opts or {}
local plugin = self.render:get_plugin()
if plugin then
local diff
if opts.commit then
diff = { commit = opts.commit }
elseif plugin._.updated then
diff = vim.deepcopy(plugin._.updated)
else
local info = assert(Git.info(plugin.dir))
local target = assert(Git.get_target(plugin))
diff = { from = info.commit, to = target.commit }
if opts.hover and diff.from == diff.to then
return
end
end
if not diff then
return
end
for k, v in pairs(diff) do
diff[k] = v:sub(1, 7)
end
if opts.browser then
Diff.handlers.browser(plugin, diff)
else
Diff.handlers[Config.options.diff.cmd](plugin, diff)
end
return true
end
end
--- will create a key mapping that can be used on certain patterns
---@param key string
---@param patterns table<string, fun(str:string)>
---@param fallback? fun(self)
---@param desc? string
function M:on_pattern(key, patterns, fallback, desc)
self:on_key(key, function()
local line = vim.api.nvim_get_current_line()
local pos = vim.api.nvim_win_get_cursor(0)
local col = pos[2] + 1
for pattern, handler in pairs(patterns) do
local from = 1
local to, url
while from do
from, to, url = line:find(pattern, from)
if from and col >= from and col <= to then
return handler(url)
end end
if from then else
from = to + 1 if M.mode == m.name and m.toggle then
M.mode = nil
return update()
end
Commands.cmd(m.name)
end
end, { buffer = buf })
end
vim.api.nvim_create_autocmd("User", {
pattern = "LazyRender",
callback = function()
if not vim.api.nvim_buf_is_valid(buf) then
return true
end
update()
end,
})
update()
end
---@param handlers table<string, fun(str:string)>
function M.keys(buf, handlers)
local function map(lhs)
vim.keymap.set("n", lhs, function()
local line = vim.api.nvim_get_current_line()
local pos = vim.api.nvim_win_get_cursor(0)
local col = pos[2] + 1
for pattern, handler in pairs(handlers) do
local from = 1
local to, url
while from do
from, to, url = line:find(pattern, from)
if from and col >= from and col <= to then
return handler(url)
end
if from then
from = to + 1
end
end end
end end
end end, { buffer = buf, silent = true })
if fallback then
fallback(self)
end
end, desc)
end
function M:setup_modes()
local Commands = require("lazy.view.commands")
for name, m in pairs(ViewConfig.commands) do
if m.key then
self:on_key(m.key, function()
if self.state.mode == name and m.toggle then
self.state.mode = "home"
return self:update()
end
Commands.cmd(name)
end, m.desc)
end
if m.key_plugin and name ~= "restore" then
self:on_key(m.key_plugin, function()
local esc = vim.api.nvim_replace_termcodes("<esc>", true, true, true)
vim.api.nvim_feedkeys(esc, "n", false)
local plugins = {}
if vim.api.nvim_get_mode().mode:lower() == "v" then
local f, t = vim.fn.line("."), vim.fn.line("v")
if f > t then
f, t = t, f
end
for i = f, t do
local plugin = self.render:get_plugin(i)
if plugin then
plugins[plugin.name] = plugin
end
end
plugins = vim.tbl_values(plugins)
else
plugins[1] = self.render:get_plugin()
end
if #plugins > 0 then
Commands.cmd(name, { plugins = plugins })
end
end, m.desc_plugin, { "n", "x" })
end
end end
map("K")
end end
return M return M

View file

@ -1,43 +1,40 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Git = require("lazy.manage.git")
local Handler = require("lazy.core.handler")
local Keys = require("lazy.core.handler.keys")
local Plugin = require("lazy.core.plugin")
local Sections = require("lazy.view.sections")
local Util = require("lazy.util") local Util = require("lazy.util")
local ViewConfig = require("lazy.view.config") local Sections = require("lazy.view.sections")
local Handler = require("lazy.core.handler")
local Git = require("lazy.manage.git")
local Text = require("lazy.view.text") local Text = require("lazy.view.text")
---@alias LazyDiagnostic {row: number, severity: number, message:string} ---@alias LazyDiagnostic {row: number, severity: number, message:string}
---@class LazyRender:Text ---@class Render:Text
---@field view LazyView ---@field buf buffer
---@field win window
---@field plugins LazyPlugin[] ---@field plugins LazyPlugin[]
---@field progress {total:number, done:number} ---@field progress {total:number, done:number}
---@field _diagnostics LazyDiagnostic[] ---@field _diagnostics LazyDiagnostic[]
---@field locations {name:string, from: number, to: number, kind?: LazyPluginKind}[] ---@field plugin_range table<string, {from: number, to: number}>
local M = {} ---@field _details? string
local M = setmetatable({}, {
__index = Text,
})
---@return LazyRender function M.new(buf, win, padding)
---@param view LazyView local self = setmetatable({}, { __index = M })
function M.new(view) self.buf = buf
---@type LazyRender self.win = win
local self = setmetatable({}, { __index = setmetatable(M, { __index = Text }) }) self.padding = padding or 0
self.view = view
self.padding = 2
self.wrap = view.win_opts.width
return self return self
end end
function M:update() function M:update()
self._lines = {} self._lines = {}
self._diagnostics = {} self._diagnostics = {}
self.locations = {} self.plugin_range = {}
self.plugins = vim.tbl_values(Config.plugins) self.plugins = vim.tbl_values(Config.plugins)
vim.list_extend(self.plugins, vim.tbl_values(Config.to_clean)) vim.list_extend(self.plugins, vim.tbl_values(Config.to_clean))
vim.list_extend(self.plugins, vim.tbl_values(Config.spec.disabled))
table.sort(self.plugins, function(a, b) table.sort(self.plugins, function(a, b)
return a.name < b.name return a.name < b.name
end) end)
@ -51,22 +48,19 @@ function M:update()
if plugin._.tasks then if plugin._.tasks then
for _, task in ipairs(plugin._.tasks) do for _, task in ipairs(plugin._.tasks) do
self.progress.total = self.progress.total + 1 self.progress.total = self.progress.total + 1
if not task:running() then if not task:is_running() then
self.progress.done = self.progress.done + 1 self.progress.done = self.progress.done + 1
end end
end end
end end
end end
self:title() local mode = self:title()
local mode = self.view.state.mode
if mode == "help" then if mode == "help" then
self:help() self:help()
elseif mode == "profile" then elseif mode == "profile" then
self:profile() self:profile()
elseif mode == "debug" then
self:debug()
else else
for _, section in ipairs(Sections) do for _, section in ipairs(Sections) do
self:section(section) self:section(section)
@ -74,179 +68,76 @@ function M:update()
end end
self:trim() self:trim()
self:render(self.buf)
vim.bo[self.view.buf].modifiable = true
local view = vim.api.nvim_win_call(self.view.win, vim.fn.winsaveview)
self:render(self.view.buf)
vim.api.nvim_win_call(self.view.win, function()
vim.fn.winrestview(view)
end)
vim.bo[self.view.buf].modifiable = false
vim.diagnostic.set( vim.diagnostic.set(
Config.ns, Config.ns,
self.view.buf, self.buf,
---@param diag LazyDiagnostic ---@param diag LazyDiagnostic
vim.tbl_map(function(diag) vim.tbl_map(function(diag)
diag.col = 0 diag.col = 0
diag.lnum = diag.row - 1 diag.lnum = diag.row - 1
return diag return diag
end, self._diagnostics), end, self._diagnostics),
{ signs = false, virtual_text = true, underline = false, virtual_lines = false } { signs = false }
) )
end end
---@param row? number ---@param row number
---@return LazyPlugin? ---@return LazyPlugin?
function M:get_plugin(row) function M:get_plugin(row)
if not (self.view.win and vim.api.nvim_win_is_valid(self.view.win)) then for name, range in pairs(self.plugin_range) do
return if row >= range.from and row <= range.to then
end return Config.plugins[name]
row = row or vim.api.nvim_win_get_cursor(self.view.win)[1]
for _, loc in ipairs(self.locations) do
if row >= loc.from and row <= loc.to then
if loc.kind == "clean" then
for _, plugin in ipairs(Config.to_clean) do
if plugin.name == loc.name then
return plugin
end
end
elseif loc.kind == "disabled" then
return Config.spec.disabled[loc.name]
else
return Config.plugins[loc.name]
end
end
end
end
---@param selected {name:string, kind?: LazyPluginKind}
function M:get_row(selected)
for _, loc in ipairs(self.locations) do
if loc.kind == selected.kind and loc.name == selected.name then
return loc.from
end end
end end
end end
function M:title() function M:title()
self:nl() self:append(" lazy.nvim ", "LazyH1"):center():nl()
local modes = vim.tbl_filter(function(c) self:append("press "):append("<?>", "LazySpecial"):append(" for help"):center():nl()
return c.button self:append("https://github.com/folke/lazy.nvim", "LazyMuted"):center():nl()
end, ViewConfig.get_commands())
if Config.options.ui.pills then local View = require("lazy.view")
self:nl() for _, mode in ipairs(View.modes) do
for c, mode in ipairs(modes) do if not mode.hide and not mode.plugin then
local title = " " .. mode.name:sub(1, 1):upper() .. mode.name:sub(2) .. " (" .. mode.key .. ") " local title = " " .. mode.name:sub(1, 1):upper() .. mode.name:sub(2) .. " (" .. mode.key .. ") "
if mode.name == "home" then self:append(title, View.mode == mode.name and "LazyButtonActive" or "LazyButton"):append(" ")
if self.view.state.mode == "home" then
title = " lazy.nvim " .. Config.options.ui.icons.lazy
end
end
if self.view.state.mode == mode.name then
if mode.name == "home" then
self:append(title, "LazyH1", { wrap = true })
else
self:append(title, "LazyButtonActive", { wrap = true })
self:highlight({ ["%(.%)"] = "LazySpecial" })
end
else
self:append(title, "LazyButton", { wrap = true })
self:highlight({ ["%(.%)"] = "LazySpecial" })
end
if c == #modes then
break
end
self:append(" ")
end end
self:nl()
end end
self:nl()
if self.progress.done < self.progress.total then if self.progress.done < self.progress.total then
self:progressbar() self:progressbar()
end end
self:nl() self:nl()
if self.view.state.mode ~= "help" and self.view.state.mode ~= "profile" and self.view.state.mode ~= "debug" then if View.mode ~= "help" and View.mode ~= "profile" then
if self.progress.done < self.progress.total then if self.progress.done < self.progress.total then
self:append("Tasks: ", "LazyH2") self:append("Tasks: ", "LazyH2")
self:append(self.progress.done .. "/" .. self.progress.total, "LazyComment") self:append(self.progress.done .. "/" .. self.progress.total, "LazyMuted")
else else
self:append("Total: ", "LazyH2") self:append("Total: ", "LazyH2")
self:append(#self.plugins .. " plugins", "LazyComment") self:append(#self.plugins .. " plugins", "LazyMuted")
end end
self:nl():nl() self:nl():nl()
end end
return View.mode
end end
function M:help() function M:help()
local View = require("lazy.view")
self:append("Help", "LazyH2"):nl():nl() self:append("Help", "LazyH2"):nl():nl()
self:append("Use "):append(ViewConfig.keys.abort, "LazySpecial"):append(" to abort all running tasks."):nl():nl()
self:append("You can press "):append("<CR>", "LazySpecial"):append(" on a plugin to show its details."):nl():nl()
self:append("Most properties can be hovered with ")
self:append("<K>", "LazySpecial")
self:append(" to open links, help files, readmes and git commits."):nl()
self
:append("When hovering with ")
:append("<K>", "LazySpecial")
:append(" on a plugin anywhere else, a diff will be opened if there are updates")
:nl()
self:append("or the plugin was just updated. Otherwise the plugin webpage will open."):nl():nl()
self:append("Use "):append("<d>", "LazySpecial"):append(" on a commit or plugin to open the diff view"):nl():nl()
self
:append("Use ")
:append("<]]>", "LazySpecial")
:append(" and ")
:append("<[[>", "LazySpecial")
:append(" to navigate between plugins")
:nl()
:nl()
self:nl()
self:append("Keyboard Shortcuts", "LazyH2"):nl() self:append("Keyboard Shortcuts", "LazyH2"):nl()
for _, mode in ipairs(ViewConfig.get_commands()) do for _, mode in ipairs(View.modes) do
if mode.key then local title = mode.name:sub(1, 1):upper() .. mode.name:sub(2)
local title = mode.name:sub(1, 1):upper() .. mode.name:sub(2) self:append("- ", "LazySpecial", { indent = 2 })
self:append("- ", "LazySpecial", { indent = 2 }) self:append(title, "Title"):append(" <" .. mode.key .. "> ", "LazyKey")
self:append(title, "Title") self:append(mode.desc or ""):nl()
if mode.key then
self:append(" <" .. mode.key .. ">", "LazyProp")
end
self:append(" " .. (mode.desc or "")):nl()
end
end
self:nl():append("Keyboard Shortcuts for Plugins", "LazyH2"):nl()
for _, mode in ipairs(ViewConfig.get_commands()) do
if mode.key_plugin then
local title = mode.name:sub(1, 1):upper() .. mode.name:sub(2)
self:append("- ", "LazySpecial", { indent = 2 })
self:append(title, "Title")
if mode.key_plugin then
self:append(" <" .. mode.key_plugin .. ">", "LazyProp")
end
self:append(" " .. (mode.desc_plugin or mode.desc)):nl()
end
end
for lhs, rhs in pairs(Config.options.ui.custom_keys) do
if type(rhs) == "table" and rhs.desc then
self:append("- ", "LazySpecial", { indent = 2 })
self:append("Custom key ", "Title")
self:append(lhs, "LazyProp")
self:append(" " .. rhs.desc):nl()
end
end end
end end
function M:progressbar() function M:progressbar()
local width = vim.api.nvim_win_get_width(self.view.win) - 2 * self.padding local width = vim.api.nvim_win_get_width(self.win) - 2 * self.padding
local done = math.floor((self.progress.done / self.progress.total) * width + 0.5) local done = math.floor((self.progress.done / self.progress.total) * width + 0.5)
if self.progress.done == self.progress.total then if self.progress.done == self.progress.total then
done = 0 done = 0
@ -275,11 +166,8 @@ function M:section(section)
end, self.plugins) end, self.plugins)
local count = #section_plugins local count = #section_plugins
table.sort(section_plugins, function(a, b)
return a.name:lower() < b.name:lower()
end)
if count > 0 then if count > 0 then
self:append(section.title, "LazyH2"):append(" (" .. count .. ")", "LazyComment"):nl() self:append(section.title, "LazyH2"):append(" (" .. count .. ")", "LazyMuted"):nl()
for _, plugin in ipairs(section_plugins) do for _, plugin in ipairs(section_plugins) do
self:plugin(plugin) self:plugin(plugin)
end end
@ -294,13 +182,6 @@ function M:diagnostic(diag)
table.insert(self._diagnostics, diag) table.insert(self._diagnostics, diag)
end end
---@param precision? number
function M:ms(nsec, precision)
precision = precision or 2
local e = math.pow(10, precision)
return math.floor(nsec / 1e6 * e + 0.5) / e .. "ms"
end
---@param reason? {[string]:string, time:number} ---@param reason? {[string]:string, time:number}
---@param opts? {time_right?:boolean} ---@param opts? {time_right?:boolean}
function M:reason(reason, opts) function M:reason(reason, opts)
@ -312,129 +193,89 @@ function M:reason(reason, opts)
---@type string? ---@type string?
local source = reason.source local source = reason.source
if source then if source then
source = Util.norm(source) ---@type string?
local plugin = Plugin.find(source) local modname = source:match("/lua/(.*)%.lua$")
if plugin then if modname then
reason.plugin = plugin.name modname = modname:gsub("/", ".")
reason.source = nil end
else local name = source:match("/([^/]-)/lua")
local config = Util.norm(vim.fn.stdpath("config")) for _, other in pairs(Config.plugins) do
if source == config .. "/init.lua" then if (modname and other.modname == modname) or (name and other.name == name) then
reason.source = "init.lua" reason.plugin = other.name
else reason.source = nil
config = config .. "/lua" break
if source:find(config, 1, true) == 1 then end
reason.source = source:sub(#config + 2):gsub("/", "."):gsub("%.lua$", "") end
end if reason.source then
reason.source = modname or reason.source
if reason.source == "lua" then
reason.source = Config.options.plugins
end end
end end
end end
if reason.runtime then local time = " " .. math.floor((reason.time or 0) / 1e6 * 100) / 100 .. "ms"
reason.runtime = Util.norm(reason.runtime) if not opts.time_right then
reason.runtime = reason.runtime:gsub(".*/([^/]+/plugin/.*)", "%1")
reason.runtime = reason.runtime:gsub(".*/([^/]+/after/.*)", "%1")
reason.runtime = reason.runtime:gsub(".*/([^/]+/ftdetect/.*)", "%1")
reason.runtime = reason.runtime:gsub(".*/(runtime/.*)", "%1")
end
local time = reason.time and (" " .. self:ms(reason.time))
if time and not opts.time_right then
self:append(time, "Bold") self:append(time, "Bold")
self:append(" ")
end end
self:append(" ")
-- self:append(" (", "Conceal") -- self:append(" (", "Conceal")
local first = true local first = true
local keys = vim.tbl_keys(reason) for key, value in pairs(reason) do
table.sort(keys) if type(key) == "number" then
if vim.tbl_contains(keys, "plugin") then elseif key == "require" then
keys = vim.tbl_filter(function(key) -- self:append("require", "@function.builtin")
return key ~= "plugin" -- self:append("(", "@punctuation.bracket")
end, keys) -- self:append('"' .. value .. '"', "@string")
table.insert(keys, "plugin") -- self:append(")", "@punctuation.bracket")
end elseif key ~= "time" then
for _, key in ipairs(keys) do
local value = reason[key]
local skip = type(key) == "number" or key == "time"
if not skip then
if first then if first then
first = false first = false
else else
self:append(" ") self:append(" ")
end end
local hl = "LazyReason" .. key:sub(1, 1):upper() .. key:sub(2) if key == "event" then
value = value:match("User (.*)") or value
end
local hl = "LazyLoader" .. key:sub(1, 1):upper() .. key:sub(2)
local icon = Config.options.ui.icons[key] local icon = Config.options.ui.icons[key]
if icon then if icon then
icon = icon:gsub("%s*$", "")
self:append(icon .. " ", hl) self:append(icon .. " ", hl)
self:append(value, hl) self:append(value, hl)
else else
self:append(key .. " ", hl) self:append(key .. " ", "@field")
self:append(value, hl) self:append(value, hl)
end end
end end
end end
if time and opts.time_right then if opts.time_right then
self:append(time, "Bold") self:append(time, "Bold")
end end
-- self:append(")", "Conceal")
end end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
function M:diagnostics(plugin) function M:diagnostics(plugin)
local skip = false if plugin._.updated then
for _, task in ipairs(plugin._.tasks or {}) do
if task:running() then
self:diagnostic({
severity = vim.diagnostic.severity.WARN,
message = task.name .. (task:status() and (": " .. task:status()) or ""),
})
skip = true
elseif task:has_errors() then
self:diagnostic({
message = task.name .. " failed",
severity = vim.diagnostic.severity.ERROR,
})
skip = true
elseif task:has_warnings() then
self:diagnostic({
message = task.name .. " warning",
severity = vim.diagnostic.severity.WARN,
})
skip = true
end
end
if skip then
return
end
if plugin._.build then
self:diagnostic({
message = "needs build",
severity = vim.diagnostic.severity.WARN,
})
elseif plugin._.updated then
if plugin._.updated.from == plugin._.updated.to then if plugin._.updated.from == plugin._.updated.to then
self:diagnostic({ self:diagnostic({
message = "already up to date", message = "already up to date",
}) })
else else
local version = Git.info(plugin.dir, true).version
if version then
self:diagnostic({
message = "updated to " .. tostring(version),
})
else
self:diagnostic({
message = "updated from " .. plugin._.updated.from:sub(1, 7) .. " to " .. plugin._.updated.to:sub(1, 7),
})
end
end
elseif plugin._.updates then
local version = plugin._.updates.to.version
if version then
self:diagnostic({ self:diagnostic({
message = "version " .. tostring(version) .. " is available", message = "updated from " .. plugin._.updated.from:sub(1, 7) .. " to " .. plugin._.updated.to:sub(1, 7),
}) })
else end
end
for _, task in ipairs(plugin._.tasks or {}) do
if task:is_running() then
self:diagnostic({ self:diagnostic({
message = "updates available", severity = vim.diagnostic.severity.WARN,
message = task.name .. (task.status == "" and "" or (": " .. task.status)),
})
elseif task.error then
self:diagnostic({
message = task.name .. " failed",
severity = vim.diagnostic.severity.ERROR,
}) })
end end
end end
@ -442,77 +283,43 @@ end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
function M:plugin(plugin) function M:plugin(plugin)
local hl = plugin._.is_local and "LazyLocal" or "LazySpecial"
if plugin._.loaded then if plugin._.loaded then
self:append(" " .. Config.options.ui.icons.loaded .. " ", hl):append(plugin.name) self:append("", "LazySpecial"):append(plugin.name)
elseif plugin._.cond == false then
self:append(" " .. Config.options.ui.icons.not_loaded .. " ", "LazyNoCond"):append(plugin.name)
else else
self:append(" " .. Config.options.ui.icons.not_loaded .. " ", hl):append(plugin.name) self:append("", "LazySpecial"):append(plugin.name)
end end
local plugin_start = self:row() local plugin_start = self:row()
if plugin._.loaded then if plugin._.loaded then
-- When the plugin is loaded, only show the loading reason
self:reason(plugin._.loaded) self:reason(plugin._.loaded)
else
-- otherwise show all lazy handlers
self:append(" ")
self:handlers(plugin)
for _, other in pairs(Config.plugins) do
if vim.tbl_contains(other.dependencies or {}, plugin.name) then
self:reason({ plugin = other.name })
self:append(" ")
end
end
end end
self:diagnostics(plugin) self:diagnostics(plugin)
self:nl() self:nl()
if self.view:is_selected(plugin) then if self._details == plugin.name then
self:details(plugin) self:details(plugin)
end end
self:tasks(plugin) self:tasks(plugin)
self.locations[#self.locations + 1] = self.plugin_range[plugin.name] = { from = plugin_start, to = self:row() - 1 }
{ name = plugin.name, from = plugin_start, to = self:row() - 1, kind = plugin._.kind }
end
---@param str string
---@param hl? string|Extmark
---@param opts? {indent?: number, prefix?: string, wrap?: boolean}
function M:markdown(str, hl, opts)
local lines = vim.split(str, "\n")
for _, line in ipairs(lines) do
self:append(line, hl, opts):highlight({
["`.-`"] = "@markup.raw.markdown_inline",
["%*.-%*"] = "LazyItalic",
["%*%*.-%*%*"] = "LazyBold",
["^%s*-"] = "Special",
})
self:nl()
end
end end
---@param plugin LazyPlugin ---@param plugin LazyPlugin
function M:tasks(plugin) function M:tasks(plugin)
for _, task in ipairs(plugin._.tasks or {}) do for _, task in ipairs(plugin._.tasks or {}) do
if self.view:is_selected(plugin) then if self._details == plugin.name then
self:append(Config.options.ui.icons.task .. "[task] ", "Title", { indent = 4 }):append(task.name) self:append("✔ [task] ", "Title", { indent = 4 }):append(task.name)
self:append(" " .. math.floor((task:time()) * 100) / 100 .. "ms", "Bold") self:append(" " .. math.floor((task:time()) * 100) / 100 .. "ms", "Bold")
self:nl() self:nl()
end end
if task.name == "log" and not task.error then
if not task:has_warnings() and task.name == "log" then
self:log(task) self:log(task)
else elseif task.error or self._details == plugin.name then
local hls = { if task.error then
[vim.log.levels.ERROR] = "LazyError", self:append(vim.trim(task.error), "LazyError", { indent = 4, prefix = "" })
[vim.log.levels.WARN] = "LazyWarning", self:nl()
[vim.log.levels.INFO] = "LazyInfo", end
} if task.output ~= "" and task.output ~= task.error then
for _, msg in ipairs(task:get_log()) do self:append(vim.trim(task.output), "MsgArea", { indent = 4, prefix = "" })
if task:has_warnings() or self.view:is_selected(plugin) then self:nl()
self:markdown(msg.msg, hls[msg.level] or "LazyTaskOutput", { indent = 6 })
end
end end
end end
end end
@ -520,36 +327,24 @@ end
---@param task LazyTask ---@param task LazyTask
function M:log(task) function M:log(task)
local log = vim.trim(task:output()) local log = vim.trim(task.output)
if log ~= "" then if log ~= "" then
local lines = vim.split(log, "\n") local lines = vim.split(log, "\n")
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
local ref, msg, time = line:match("^(%w+) (.*) (%(.*%))$") local ref, msg, time = line:match("^(%w+) (.*) (%(.*%))$")
if msg then if msg:find("^%S+!:") then
if msg:find("^%S+!:") then self:diagnostic({ message = "Breaking Changes", severity = vim.diagnostic.severity.WARN })
self:diagnostic({ message = "Breaking Changes", severity = vim.diagnostic.severity.WARN })
end
self:append(ref:sub(1, 7) .. " ", "LazyCommit", { indent = 6 })
local dimmed = false
for _, dim in ipairs(ViewConfig.dimmed_commits) do
if msg:find("^" .. dim) then
dimmed = true
end
end
self:append(vim.trim(msg), dimmed and "LazyDimmed" or nil):highlight({
["#%d+"] = "LazyCommitIssue",
["^%S+:"] = dimmed and "Bold" or "LazyCommitType",
["^%S+(%(.*%))!?:"] = "LazyCommitScope",
["`.-`"] = "@markup.raw.markdown_inline",
["%*.-%*"] = "Italic",
["%*%*.-%*%*"] = "Bold",
})
self:append(" " .. time, "LazyComment")
self:nl()
-- else
-- self:append(line, "LazyTaskOutput", { indent = 6 }):nl()
end end
self:append(ref .. " ", "LazyCommit", { indent = 6 })
self:append(vim.trim(msg)):highlight({
["#%d+"] = "Number",
["^%S+:"] = "Title",
["^%S+(%(.*%)):"] = "Italic",
["`.-`"] = "@text.literal.markdown_inline",
})
-- string.gsub
self:append(" " .. time, "Comment")
self:nl()
end end
self:nl() self:nl()
end end
@ -559,13 +354,9 @@ end
function M:details(plugin) function M:details(plugin)
---@type string[][] ---@type string[][]
local props = {} local props = {}
table.insert(props, { "dir", plugin.dir, "LazyDir" }) table.insert(props, { "uri", (plugin.uri:gsub("%.git$", "")), "@text.reference" })
if plugin.url then
table.insert(props, { "url", (plugin.url:gsub("%.git$", "")), "LazyUrl" })
end
local git = Git.info(plugin.dir, true) local git = Git.info(plugin.dir, true)
if git then if git then
git.branch = git.branch or Git.get_branch(plugin)
if git.version then if git.version then
table.insert(props, { "version", tostring(git.version) }) table.insert(props, { "version", tostring(git.version) })
end end
@ -575,225 +366,60 @@ function M:details(plugin)
if git.branch then if git.branch then
table.insert(props, { "branch", git.branch }) table.insert(props, { "branch", git.branch })
end end
if git.commit then table.insert(props, { "commit", git.commit:sub(1, 7), "LazyCommit" })
table.insert(props, { "commit", git.commit:sub(1, 7), "LazyCommit" })
end
end end
local rocks = require("lazy.pkg.rockspec").deps(plugin)
if rocks then
table.insert(props, { "rocks", vim.inspect(rocks) })
end
if Util.file_exists(plugin.dir .. "/README.md") then if Util.file_exists(plugin.dir .. "/README.md") then
table.insert(props, { "readme", "README.md" }) table.insert(props, { "readme", "README.md" })
end end
Util.ls(plugin.dir .. "/doc", function(path, name)
if name:sub(-3) == "txt" then
local data = Util.read_file(path)
local tag = data:match("%*(%S-)%*")
if tag then
table.insert(props, { "help", "|" .. tag .. "|" })
end
end
end)
for handler in pairs(plugin._.handlers or {}) do for handler in pairs(Handler.handlers) do
table.insert(props, { if plugin[handler] then
handler, table.insert(props, {
function() handler,
self:handlers(plugin, handler) type(plugin[handler]) == "string" and plugin[handler] or table.concat(plugin[handler], ", "),
end, "@string",
}) })
end
self:props(props, { indent = 6 })
self:nl()
end
---@param plugin LazyPlugin
---@param types? LazyHandlerTypes[]|LazyHandlerTypes
function M:handlers(plugin, types)
if not plugin._.handlers then
return
end
types = type(types) == "string" and { types } or types
types = types and types or vim.tbl_keys(Handler.types)
for _, t in ipairs(types) do
for id, value in pairs(plugin._.handlers[t] or {}) do
value = t == "keys" and Keys.to_string(value) or id
self:reason({ [t] = value })
self:append(" ")
end end
end end
end
---@alias LazyProps {[1]:string, [2]:string|fun(), [3]?:string}[]
---@param props LazyProps
---@param opts? {indent: number}
function M:props(props, opts)
opts = opts or {}
local width = 0 local width = 0
for _, prop in ipairs(props) do for _, prop in ipairs(props) do
width = math.max(width, #prop[1]) width = math.max(width, #prop[1])
end end
for _, prop in ipairs(props) do for _, prop in ipairs(props) do
self:append(prop[1] .. string.rep(" ", width - #prop[1] + 1), "LazyProp", { indent = opts.indent or 0 }) self:append(prop[1] .. string.rep(" ", width - #prop[1] + 1), "LazyKey", { indent = 6 })
if type(prop[2]) == "function" then self:append(prop[2], prop[3] or "LazyValue")
prop[2]()
else
self:append(tostring(prop[2]), prop[3] or "LazyValue")
end
self:nl() self:nl()
end end
self:nl()
end end
function M:profile() function M:profile()
local stats = require("lazy.stats").stats()
local ms = (math.floor(stats.startuptime * 100 + 0.5) / 100)
self:append("Startuptime: ", "LazyH2"):append(ms .. "ms", "Number"):nl():nl()
if stats.real_cputime then
self:append("Based on the actual CPU time of the Neovim process till "):append("UIEnter", "LazySpecial")
self:append("."):nl()
self:append("This is more accurate than ")
self:append("`nvim --startuptime`", "@markup.raw.markdown_inline")
self:append(".")
else
self:append("An accurate startuptime based on the actual CPU time of the Neovim process is not available."):nl()
self
:append("Startuptime is instead based on a delta with a timestamp when lazy started till ")
:append("UIEnter", "LazySpecial")
self:append(".")
end
self:nl()
local times = {}
for event, time in pairs(require("lazy.stats").stats().times) do
times[#times + 1] = { event, self:ms(time * 1e6), "Bold", time = time }
end
table.sort(times, function(a, b)
return a.time < b.time
end)
for p, prop in ipairs(times) do
if p > 1 then
prop[2] = prop[2] .. " (+" .. self:ms((prop.time - times[p - 1].time) * 1e6) .. ")"
end
end
self:props(times, { indent = 2 })
self:nl()
self:append("Profile", "LazyH2"):nl():nl() self:append("Profile", "LazyH2"):nl():nl()
self local symbols = {
:append("You can press ") "",
:append(ViewConfig.keys.profile_sort, "LazySpecial") "",
:append(" to change sorting between chronological order & time taken.") "",
:nl() "",
self }
:append("Press ")
:append(ViewConfig.keys.profile_filter, "LazySpecial")
:append(" to filter profiling entries that took more time than a given threshold")
:nl()
self:nl()
---@param a LazyProfile
---@param b LazyProfile
local function sort(a, b)
return a.time > b.time
end
---@param entry LazyProfile
local function get_children(entry)
---@type LazyProfile[]
local children = entry
if self.view.state.profile.sort_time_taken then
children = {}
for _, child in ipairs(entry) do
children[#children + 1] = child
end
table.sort(children, sort)
end
return children
end
---@param entry LazyProfile ---@param entry LazyProfile
local function _profile(entry, depth) local function _profile(entry, depth)
if entry.time / 1e6 < self.view.state.profile.threshold then
return
end
local data = type(entry.data) == "string" and { source = entry.data } or entry.data local data = type(entry.data) == "string" and { source = entry.data } or entry.data
data.time = entry.time data.time = entry.time
local symbol = M.list_icon(depth) local symbol = symbols[depth] or symbols[#symbols]
self:append((" "):rep(depth)):append(symbol, "LazySpecial"):append(" ") self:append((" "):rep(depth)):append(" " .. symbol, "LazySpecial")
self:reason(data, { time_right = true }) self:reason(data, { time_right = true })
self:nl() self:nl()
for _, child in ipairs(get_children(entry)) do
for _, child in ipairs(entry) do
_profile(child, depth + 1) _profile(child, depth + 1)
end end
end end
for _, entry in ipairs(get_children(Util._profiles[1])) do for _, entry in ipairs(Util._profiles[1]) do
_profile(entry, 1) _profile(entry, 1)
end end
end end
function M.list_icon(depth)
local symbols = Config.options.ui.icons.list
return symbols[(depth - 1) % #symbols + 1]
end
function M:debug()
self:append("Active Handlers", "LazyH2"):nl()
self
:append(
"This shows only the lazy handlers that are still active. When a plugin loads, its handlers are removed",
"LazyComment",
{ indent = 2 }
)
:nl()
Util.foreach(require("lazy.core.handler").handlers, function(handler_type, handler)
Util.foreach(handler.active, function(value, plugins)
if not vim.tbl_isempty(plugins) then
---@type string[]
plugins = vim.tbl_values(plugins)
table.sort(plugins)
self:append(Config.options.ui.icons.debug, "LazySpecial", { indent = 2 })
if handler_type == "keys" then
for k, v in pairs(Config.plugins[plugins[1]]._.handlers.keys) do
if k == value then
value = Keys.to_string(v)
break
end
end
end
self:reason({ [handler_type] = value })
for _, plugin in pairs(plugins) do
self:append(" ")
self:reason({ plugin = plugin })
end
self:nl()
end
end)
end)
self:nl()
Util.foreach(require("lazy.core.cache")._inspect(), function(name, stats)
self:append(name, "LazyH2"):nl()
local props = {
{ "total", stats.total or 0, "Number" },
{ "time", self:ms(stats.time or 0, 3), "Bold" },
{ "avg time", self:ms((stats.time or 0) / (stats.total or 0), 3), "Bold" },
}
for k, v in pairs(stats) do
if k ~= "total" and k ~= "time" then
props[#props + 1] = { k, v, "Number" }
end
end
self:props(props, { indent = 2 })
self:nl()
end)
end
return M return M

View file

@ -17,35 +17,27 @@ return {
{ {
filter = function(plugin) filter = function(plugin)
return has_task(plugin, function(task) return has_task(plugin, function(task)
return task:has_errors() return task.error ~= nil
end) end)
end, end,
title = "Failed", title = "Failed",
}, },
{ {
filter = function(plugin) filter = function(plugin)
if plugin._.working then
return true
end
return has_task(plugin, function(task) return has_task(plugin, function(task)
return task:running() return task:is_running()
end) end)
end, end,
title = "Working", title = "Working",
}, },
{
filter = function(plugin)
return plugin._.build
end,
title = "Build",
},
{ {
filter = function(plugin) filter = function(plugin)
return has_task(plugin, function(task) return has_task(plugin, function(task)
if task.name ~= "log" then if task.name ~= "log" then
return return
end end
for _, line in ipairs(vim.split(task:output(), "\n")) do local lines = vim.split(task.output, "\n")
for _, line in ipairs(lines) do
if line:find("^%w+ %S+!:") then if line:find("^%w+ %S+!:") then
return true return true
end end
@ -55,53 +47,42 @@ return {
title = "Breaking Changes", title = "Breaking Changes",
}, },
{ {
---@param plugin LazyPlugin
filter = function(plugin) filter = function(plugin)
return plugin._.updated and plugin._.updated.from ~= plugin._.updated.to return plugin._.updated and plugin._.updated.from ~= plugin._.updated.to
end, end,
title = "Updated", title = "Updated",
}, },
{ {
---@param plugin LazyPlugin
filter = function(plugin) filter = function(plugin)
return plugin._.cloned return plugin._.cloned
end, end,
title = "Installed", title = "Installed",
}, },
{
---@param plugin LazyPlugin
filter = function(plugin)
return plugin._.updates ~= nil
end,
title = "Updates",
},
{ {
filter = function(plugin) filter = function(plugin)
return has_task(plugin, function(task) return has_task(plugin, function(task)
return task.name == "log" and vim.trim(task:output()) ~= "" return task.name == "log" and vim.trim(task.output) ~= ""
end) end)
end, end,
title = "Log", title = "Log",
}, },
{ {
filter = function(plugin) filter = function(plugin)
return plugin._.kind == "clean" and plugin._.installed return plugin._.installed and not plugin.uri
end, end,
title = "Clean", title = "Clean",
}, },
{ {
filter = function(plugin) filter = function(plugin)
return not plugin._.installed and plugin._.kind ~= "disabled" return not plugin._.installed
end, end,
title = "Not Installed", title = "Not Installed",
}, },
{ {
filter = function(plugin) filter = function(plugin)
return plugin._.outdated return plugin._.loaded
end,
title = "Outdated",
},
{
filter = function(plugin)
return plugin._.loaded ~= nil
end, end,
title = "Loaded", title = "Loaded",
}, },
@ -109,12 +90,6 @@ return {
filter = function(plugin) filter = function(plugin)
return plugin._.installed return plugin._.installed
end, end,
title = "Not Loaded", title = "Installed",
},
{
filter = function(plugin)
return plugin._.kind == "disabled"
end,
title = "Disabled",
}, },
} }

View file

@ -1,5 +1,4 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Util = require("lazy.util")
---@alias TextSegment {str: string, hl?:string|Extmark} ---@alias TextSegment {str: string, hl?:string|Extmark}
---@alias Extmark {hl_group?:string, col?:number, end_col?:number} ---@alias Extmark {hl_group?:string, col?:number, end_col?:number}
@ -7,11 +6,12 @@ local Util = require("lazy.util")
---@class Text ---@class Text
---@field _lines TextSegment[][] ---@field _lines TextSegment[][]
---@field padding number ---@field padding number
---@field wrap number
local Text = {} local Text = {}
function Text.new() function Text.new()
local self = setmetatable({}, { __index = Text }) local self = setmetatable({}, {
__index = Text,
})
self._lines = {} self._lines = {}
return self return self
@ -19,7 +19,7 @@ end
---@param str string ---@param str string
---@param hl? string|Extmark ---@param hl? string|Extmark
---@param opts? {indent?: number, prefix?: string, wrap?: boolean} ---@param opts? {indent?: number, prefix?: string}
function Text:append(str, hl, opts) function Text:append(str, hl, opts)
opts = opts or {} opts = opts or {}
if #self._lines == 0 then if #self._lines == 0 then
@ -37,15 +37,6 @@ function Text:append(str, hl, opts)
if l > 1 then if l > 1 then
self:nl() self:nl()
end end
if
Config.options.ui.wrap
and opts.wrap
and str ~= ""
and self:col() > 0
and self:col() + vim.fn.strwidth(line) + self.padding > self.wrap
then
self:nl()
end
table.insert(self._lines[#self._lines], { table.insert(self._lines[#self._lines], {
str = line, str = line,
hl = hl, hl = hl,
@ -65,51 +56,35 @@ function Text:render(buf)
for _, line in ipairs(self._lines) do for _, line in ipairs(self._lines) do
local str = (" "):rep(self.padding) local str = (" "):rep(self.padding)
local has_extmark = false
for _, segment in ipairs(line) do for _, segment in ipairs(line) do
str = str .. segment.str str = str .. segment.str
if type(segment.hl) == "table" then
has_extmark = true
end
end end
if str:match("^%s*$") and not has_extmark then
str = ""
end
table.insert(lines, str) table.insert(lines, str)
end end
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.api.nvim_buf_clear_namespace(buf, Config.ns, 0, -1)
for l, line in ipairs(self._lines) do for l, line in ipairs(self._lines) do
if lines[l] ~= "" then local col = self.padding
local col = self.padding
for _, segment in ipairs(line) do for _, segment in ipairs(line) do
local width = vim.fn.strlen(segment.str) local width = vim.fn.strlen(segment.str)
local extmark = segment.hl local extmark = segment.hl
if extmark then if extmark then
if type(extmark) == "string" then if type(extmark) == "string" then
extmark = { hl_group = extmark, end_col = col + width } extmark = { hl_group = extmark, end_col = col + width }
end
---@cast extmark Extmark
local extmark_col = extmark.col or col
extmark.col = nil
local ok, err = pcall(vim.api.nvim_buf_set_extmark, buf, Config.ns, l - 1, extmark_col, extmark)
if not ok then
Util.error(
"Failed to set extmark. Please report a bug with this info:\n"
.. vim.inspect({ segment = segment, line = line, error = err })
)
end
end end
---@cast extmark Extmark
col = col + width local extmark_col = extmark.col or col
extmark.col = nil
vim.api.nvim_buf_set_extmark(buf, Config.ns, l - 1, extmark_col, extmark)
end end
col = col + width
end end
end end
end end
@ -145,7 +120,27 @@ function Text:highlight(patterns)
end end
end end
function Text:center()
local last = self._lines[#self._lines]
if not last then
return
end
local width = 0
for _, segment in ipairs(last) do
width = width + vim.fn.strwidth(segment.str)
end
width = vim.api.nvim_win_get_width(self.win) - 2 * self.padding - width
table.insert(last, 1, {
str = string.rep(" ", math.floor(width / 2 + 0.5)),
})
return self
end
function Text:trim() function Text:trim()
while #self._lines > 0 and #self._lines[1] == 0 do
table.remove(self._lines, 1)
end
while #self._lines > 0 and #self._lines[#self._lines] == 0 do while #self._lines > 0 and #self._lines[#self._lines] == 0 do
table.remove(self._lines) table.remove(self._lines)
end end

116407
manifest

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
#!/bin/env bash
nvim -l tests/minit.lua --minitest

View file

@ -1,4 +0,0 @@
std="vim"
[lints]
mixed_table="allow"

View file

@ -1,6 +1,3 @@
indent_type = "Spaces" indent_type = "Spaces"
indent_width = 2 indent_width = 2
column_width = 120 column_width = 120
[sort_requires]
enabled = true

View file

@ -1,10 +0,0 @@
local Util = require("lazy.core.util")
describe("init", function()
it("has correct environment for tests", function()
for _, name in ipairs({ "config", "data", "cache", "state" }) do
local path = Util.norm(vim.fn.stdpath(name) --[[@as string]])
assert(path:find(".tests/" .. name, 1, true), path .. " not in .tests")
end
end)
end)

View file

@ -1,526 +1,122 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Handler = require("lazy.core.handler")
local Plugin = require("lazy.core.plugin") local Plugin = require("lazy.core.plugin")
local function inspect(obj) local assert = require("luassert")
return vim.inspect(obj):gsub("%s+", " ")
end
---@param plugin LazyPlugin Config.setup()
local function resolve(plugin)
local meta = getmetatable(plugin)
local ret = meta and type(meta.__index) == "table" and resolve(meta.__index) or {}
for k, v in pairs(plugin) do
ret[k] = v
end
return ret
end
---@param plugins LazyPlugin[] describe("plugin spec uri/name", function()
local function clean(plugins)
return vim.tbl_map(function(plugin)
plugin = resolve(plugin)
plugin[1] = nil
plugin._.frags = nil
if plugin._.dep == false then
plugin._.dep = nil
end
plugin._.top = nil
return plugin
end, plugins)
end
describe("plugin spec url/name", function()
local tests = { local tests = {
{ { dir = "~/foo" }, { name = "foo", dir = vim.fn.fnamemodify("~/foo", ":p") } }, { { "~/foo" }, { [1] = "~/foo", name = "foo", uri = vim.fn.fnamemodify("~/foo", ":p") } },
{ { dir = "/tmp/foo" }, { dir = "/tmp/foo", name = "foo" } }, { { "/tmp/foo" }, { [1] = "/tmp/foo", name = "foo", uri = "/tmp/foo" } },
{ { "foo/bar" }, { [1] = "foo/bar", name = "bar", url = "https://github.com/foo/bar.git" } }, { { "foo/bar" }, { [1] = "foo/bar", name = "bar", uri = "https://github.com/foo/bar.git" } },
{ { "https://foo.bar" }, { [1] = "https://foo.bar", name = "foo.bar", url = "https://foo.bar" } }, { { "foo/bar", name = "foobar" }, { [1] = "foo/bar", name = "foobar", uri = "https://github.com/foo/bar.git" } },
{ { "foo/bar", name = "foobar" }, { [1] = "foo/bar", name = "foobar", url = "https://github.com/foo/bar.git" } }, { { "foo/bar", uri = "123" }, { [1] = "foo/bar", name = "bar", uri = "123" } },
{ { "foo/bar", url = "123" }, { [1] = "foo/bar", name = "bar", url = "123" } }, { { "https://foobar" }, { [1] = "https://foobar", name = "foobar", uri = "https://foobar" } },
{ { url = "https://foobar" }, { name = "foobar", url = "https://foobar" } }, { { "ssh://foobar" }, { [1] = "ssh://foobar", name = "foobar", uri = "ssh://foobar" } },
{ { "foo/bar", { [1] = "foo/bar", name = "bar", uri = "https://github.com/foo/bar.git" } },
{ { url = "https://foo", name = "foobar" }, { url = "https://foo" } }, { { { { "foo/bar" } } }, { [1] = "foo/bar", name = "bar", uri = "https://github.com/foo/bar.git" } },
{ name = "foobar", url = "https://foo" },
},
{
{ { url = "https://foo" }, { url = "https://foo", name = "foobar" } },
{ name = "foobar", url = "https://foo" },
},
{ { url = "ssh://foobar" }, { name = "foobar", url = "ssh://foobar" } },
{ "foo/bar", { [1] = "foo/bar", name = "bar", url = "https://github.com/foo/bar.git" } },
{ { { { "foo/bar" } } }, { [1] = "foo/bar", name = "bar", url = "https://github.com/foo/bar.git" } },
} }
for _, test in ipairs(tests) do for _, test in ipairs(tests) do
test[2]._ = {} it("parses " .. vim.inspect(test[1]):gsub("%s+", " "), function()
it("parses " .. inspect(test[1]), function()
if not test[2].dir then
test[2].dir = Config.options.root .. "/" .. test[2].name
end
local spec = Plugin.Spec.new(test[1]) local spec = Plugin.Spec.new(test[1])
local all = vim.deepcopy(spec.plugins) local plugins = vim.tbl_values(spec.plugins)
local plugins = vim.tbl_values(all) assert.equal(1, #plugins)
plugins = vim.tbl_map(function(plugin)
plugin._ = {}
return plugin
end, plugins)
local notifs = vim.tbl_filter(function(notif)
return notif.level > 3
end, spec.notifs)
assert(#notifs == 0, vim.inspect(spec.notifs))
assert.equal(1, #plugins, vim.inspect(all))
plugins[1]._.super = nil
assert.same(test[2], plugins[1]) assert.same(test[2], plugins[1])
end) end)
end end
end) end)
describe("plugin spec dir", function()
local tests = {
{
"~/projects/gitsigns.nvim",
{ "lewis6991/gitsigns.nvim", opts = {}, dev = true },
{ "lewis6991/gitsigns.nvim" },
},
{
"~/projects/gitsigns.nvim",
{ "lewis6991/gitsigns.nvim", opts = {}, dev = true },
{ "gitsigns.nvim" },
},
{
"~/projects/gitsigns.nvim",
{ "lewis6991/gitsigns.nvim", opts = {} },
{ "lewis6991/gitsigns.nvim", dev = true },
},
{
"~/projects/gitsigns.nvim",
{ "lewis6991/gitsigns.nvim", opts = {} },
{ "gitsigns.nvim", dev = true },
},
}
for _, test in ipairs(tests) do
local dir = vim.fn.expand(test[1])
local input = vim.list_slice(test, 2)
it("parses dir " .. inspect(input), function()
local spec = Plugin.Spec.new(input)
local plugins = vim.tbl_values(spec.plugins)
assert(spec:report() == 0)
assert.equal(1, #plugins)
assert.same(dir, plugins[1].dir)
end)
end
end)
describe("plugin dev", function()
local tests = {
{
{ "lewis6991/gitsigns.nvim", opts = {}, dev = true },
{ "lewis6991/gitsigns.nvim" },
},
{
{ "lewis6991/gitsigns.nvim", opts = {}, dev = true },
{ "gitsigns.nvim" },
},
{
{ "lewis6991/gitsigns.nvim", opts = {} },
{ "lewis6991/gitsigns.nvim", dev = true },
},
{
{ "lewis6991/gitsigns.nvim", opts = {} },
{ "gitsigns.nvim", dev = true },
},
}
for _, test in ipairs(tests) do
local dir = vim.fn.expand("~/projects/gitsigns.nvim")
local input = test
it("parses dir " .. inspect(input), function()
local spec = Plugin.Spec.new(input)
local plugins = vim.tbl_values(spec.plugins)
assert(spec:report() == 0)
assert.equal(1, #plugins)
assert.same(dir, plugins[1].dir)
end)
end
end)
describe("plugin spec opt", function() describe("plugin spec opt", function()
it("handles dependencies", function() it("handles dependencies", function()
Config.options.defaults.lazy = false Config.options.defaults.opt = false
local tests = { local tests = {
{ "foo/bar", dependencies = { "foo/dep1", "foo/dep2" } }, { "foo/bar", dependencies = { "foo/dep1", "foo/dep2" } },
{ "foo/bar", dependencies = { { "foo/dep1" }, "foo/dep2" } }, { "foo/bar", dependencies = { { "foo/dep1" }, "foo/dep2" } },
{ { { "foo/bar", dependencies = { { "foo/dep1" }, "foo/dep2" } } } }, { { { "foo/bar", dependencies = { { "foo/dep1" }, "foo/dep2" } } } },
} }
for _, test in ipairs(tests) do for _, test in ipairs(tests) do
local spec = Plugin.Spec.new(vim.deepcopy(test)) local spec = Plugin.Spec.new(test)
assert(#spec.notifs == 0)
Config.plugins = spec.plugins Config.plugins = spec.plugins
Config.spec = spec
Plugin.update_state() Plugin.update_state()
assert(vim.tbl_count(spec.plugins) == 3) assert(vim.tbl_count(spec.plugins) == 3)
assert(#spec.plugins.bar.dependencies == 2) assert(#spec.plugins.bar.dependencies == 2)
assert(spec.plugins.bar._.dep ~= true) assert(spec.plugins.bar.dep ~= true)
assert(spec.plugins.bar.lazy == false) assert(spec.plugins.bar.opt == false)
assert(spec.plugins.dep1._.dep == true) assert(spec.plugins.dep1.dep == true)
assert(spec.plugins.dep1.lazy == true) assert(spec.plugins.dep1.opt == true)
assert(spec.plugins.dep2._.dep == true) assert(spec.plugins.dep2.dep == true)
assert(spec.plugins.dep2.lazy == true) assert(spec.plugins.dep2.opt == true)
spec = Plugin.Spec.new(test)
for _, plugin in pairs(spec.plugins) do
plugin.dir = nil
end
assert.same({
bar = {
_ = {},
dependencies = { "dep1", "dep2" },
name = "bar",
url = "https://github.com/foo/bar.git",
},
dep1 = {
_ = {
dep = true,
},
name = "dep1",
url = "https://github.com/foo/dep1.git",
},
dep2 = {
_ = {
dep = true,
},
name = "dep2",
url = "https://github.com/foo/dep2.git",
},
}, clean(spec.plugins))
end end
end) end)
describe("deps", function()
before_each(function()
Handler.init()
end)
Config.options.defaults.lazy = false
local tests = {
{ { "foo/bar", dependencies = { { "dep1" }, "foo/dep2" } }, "foo/dep1" },
{ "foo/dep1", { "foo/bar", dependencies = { { "dep1" }, "foo/dep2" } } },
}
for _, test in ipairs(tests) do
it("handles dep names " .. inspect(test), function()
local spec = Plugin.Spec.new(vim.deepcopy(test))
assert(#spec.notifs == 0)
Config.plugins = spec.plugins
Plugin.update_state()
spec = Plugin.Spec.new(test)
for _, plugin in pairs(spec.plugins) do
plugin.dir = nil
end
assert.same({
bar = {
_ = {},
dependencies = { "dep1", "dep2" },
name = "bar",
url = "https://github.com/foo/bar.git",
},
dep1 = {
_ = {},
name = "dep1",
url = "https://github.com/foo/dep1.git",
},
dep2 = {
_ = {
dep = true,
},
name = "dep2",
url = "https://github.com/foo/dep2.git",
},
}, clean(spec.plugins))
end)
end
Config.options.defaults.lazy = false
local tests = {
{
{ "foo/baz", name = "bar" },
{ "foo/fee", dependencies = { "foo/baz" } },
},
{
{ "foo/fee", dependencies = { "foo/baz" } },
{ "foo/baz", name = "bar" },
},
-- {
-- { "foo/baz", name = "bar" },
-- { "foo/fee", dependencies = { "baz" } },
-- },
{
{ "foo/baz", name = "bar" },
{ "foo/fee", dependencies = { "bar" } },
},
}
for _, test in ipairs(tests) do
it("handles dep names " .. inspect(test), function()
local spec = Plugin.Spec.new(vim.deepcopy(test))
assert(#spec.notifs == 0)
Config.plugins = spec.plugins
Plugin.update_state()
spec = Plugin.Spec.new(test)
spec.meta:rebuild()
for _, plugin in pairs(spec.plugins) do
plugin.dir = nil
end
assert.same({
bar = {
_ = {},
name = "bar",
url = "https://github.com/foo/baz.git",
},
fee = {
_ = {},
name = "fee",
url = "https://github.com/foo/fee.git",
dependencies = { "bar" },
},
}, clean(spec.plugins))
end)
end
it("handles opt from dep", function()
Config.options.defaults.lazy = false
local spec = Plugin.Spec.new({ "foo/dep1", { "foo/bar", dependencies = { "foo/dep1", "foo/dep2" } } })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins
Plugin.update_state()
assert.same(3, vim.tbl_count(spec.plugins))
assert(spec.plugins.bar._.dep ~= true)
assert(spec.plugins.bar.lazy == false)
assert(spec.plugins.dep2._.dep == true)
assert(spec.plugins.dep2.lazy == true)
assert(spec.plugins.dep1._.dep ~= true)
assert(spec.plugins.dep1.lazy == false)
end)
it("handles defaults opt", function()
do
Config.options.defaults.lazy = true
local spec = Plugin.Spec.new({ "foo/bar" })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins
Plugin.update_state()
assert(spec.plugins.bar.lazy == true)
end
do
Config.options.defaults.lazy = false
local spec = Plugin.Spec.new({ "foo/bar" })
Config.plugins = spec.plugins
Plugin.update_state()
assert(spec.plugins.bar.lazy == false)
end
end)
it("handles opt from dep", function()
Config.options.defaults.lazy = false
local spec = Plugin.Spec.new({ "foo/bar", event = "foo" })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins
Plugin.update_state()
assert.same(1, vim.tbl_count(spec.plugins))
assert(spec.plugins.bar._.dep ~= true)
assert(spec.plugins.bar.lazy == true)
end)
it("merges lazy loaders", function()
local tests = {
{ { "foo/bar", event = "mod1" }, { "foo/bar", event = "mod2" } },
{ { "foo/bar", event = { "mod1" } }, { "foo/bar", event = { "mod2" } } },
{ { "foo/bar", event = "mod1" }, { "foo/bar", event = { "mod2" } } },
}
for _, test in ipairs(tests) do
local spec = Plugin.Spec.new(test)
assert(#spec.notifs == 0)
assert(vim.tbl_count(spec.plugins) == 1)
Handler.resolve(spec.plugins.bar)
-- vim.print(spec.plugins.bar._.handlers)
local events = vim.tbl_keys(spec.plugins.bar._.handlers.event or {})
assert(type(events) == "table")
assert(#events == 2)
assert(vim.tbl_contains(events, "mod1"))
assert(vim.tbl_contains(events, "mod2"))
end
end)
end)
it("handles opt from dep", function() it("handles opt from dep", function()
Config.options.defaults.lazy = false Config.options.defaults.opt = false
local spec = Plugin.Spec.new({ "foo/dep1", { "foo/bar", dependencies = { "foo/dep1", "foo/dep2" } } }) local spec = Plugin.Spec.new({ "foo/dep1", { "foo/bar", dependencies = { "foo/dep1", "foo/dep2" } } })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins Config.plugins = spec.plugins
Plugin.update_state() Plugin.update_state()
assert.same(3, vim.tbl_count(spec.plugins)) assert.same(3, vim.tbl_count(spec.plugins))
assert(spec.plugins.bar._.dep ~= true) assert(spec.plugins.bar.dep ~= true)
assert(spec.plugins.bar.lazy == false) assert(spec.plugins.bar.opt == false)
assert(spec.plugins.dep2._.dep == true) assert(spec.plugins.dep2.dep == true)
assert(spec.plugins.dep2.lazy == true) assert(spec.plugins.dep2.opt == true)
assert(spec.plugins.dep1._.dep ~= true) assert(spec.plugins.dep1.dep ~= true)
assert(spec.plugins.dep1.lazy == false) assert(spec.plugins.dep1.opt == false)
end) end)
it("handles defaults opt", function() it("handles defaults opt", function()
do do
Config.options.defaults.lazy = true Config.options.defaults.opt = true
local spec = Plugin.Spec.new({ "foo/bar" }) local spec = Plugin.Spec.new({ "foo/bar" })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins Config.plugins = spec.plugins
Plugin.update_state() Plugin.update_state()
assert(spec.plugins.bar.lazy == true) assert(spec.plugins.bar.opt == true)
end end
do do
Config.options.defaults.lazy = false Config.options.defaults.opt = false
local spec = Plugin.Spec.new({ "foo/bar" }) local spec = Plugin.Spec.new({ "foo/bar" })
Config.plugins = spec.plugins Config.plugins = spec.plugins
Plugin.update_state() Plugin.update_state()
assert(spec.plugins.bar.lazy == false) assert(spec.plugins.bar.opt == false)
end end
end) end)
it("handles opt from dep", function() it("handles opt from dep", function()
Config.options.defaults.lazy = false Config.options.defaults.opt = false
local spec = Plugin.Spec.new({ "foo/bar", event = "foo" }) local spec = Plugin.Spec.new({ "foo/bar", module = "foo" })
assert(#spec.notifs == 0)
Config.plugins = spec.plugins Config.plugins = spec.plugins
Plugin.update_state() Plugin.update_state()
assert.same(1, vim.tbl_count(spec.plugins)) assert.same(1, vim.tbl_count(spec.plugins))
assert(spec.plugins.bar._.dep ~= true) assert(spec.plugins.bar.dep ~= true)
assert(spec.plugins.bar.lazy == true) assert(spec.plugins.bar.opt == true)
end) end)
it("merges lazy loaders", function() it("merges lazy loaders", function()
local tests = { local tests = {
{ { "foo/bar", event = "mod1" }, { "foo/bar", event = "mod2" } }, { { "foo/bar", module = "mod1" }, { "foo/bar", module = "mod2" } },
{ { "foo/bar", event = { "mod1" } }, { "foo/bar", event = { "mod2" } } }, { { "foo/bar", module = { "mod1" } }, { "foo/bar", module = { "mod2" } } },
{ { "foo/bar", event = "mod1" }, { "foo/bar", event = { "mod2" } } }, { { "foo/bar", module = "mod1" }, { "foo/bar", module = { "mod2" } } },
} }
for _, test in ipairs(tests) do for _, test in ipairs(tests) do
Handler.init()
local spec = Plugin.Spec.new(test) local spec = Plugin.Spec.new(test)
assert(#spec.notifs == 0)
assert(vim.tbl_count(spec.plugins) == 1) assert(vim.tbl_count(spec.plugins) == 1)
Handler.resolve(spec.plugins.bar) assert(type(spec.plugins.bar.module) == "table")
local events = spec.plugins.bar._.handlers.event assert(#spec.plugins.bar.module == 2)
assert(type(events) == "table") assert(vim.tbl_contains(spec.plugins.bar.module, "mod1"))
assert(vim.tbl_count(events) == 2) assert(vim.tbl_contains(spec.plugins.bar.module, "mod2"))
assert(events["mod1"])
assert(events["mod2"])
end end
end) end)
it("handles disabled", function() it("refuses to merge", function()
local tests = { assert.has.errors(function()
[{ { "foo/bar" }, { "foo/bar", enabled = false } }] = false, Plugin.Spec.new({
[{ { "foo/bar", enabled = false }, { "foo/bar" } }] = false, { "foo/dep1", config = 1 },
[{ { "foo/bar", enabled = false }, { "foo/bar", enabled = true } }] = true, {
[{ { "foo/bar" }, { "foo/bar", enabled = true } }] = true, "foo/bar",
} dependencies = { { "foo/dep1", config = 2 }, "foo/dep2" },
for test, ret in pairs(tests) do
local spec = Plugin.Spec.new(test)
assert(#spec.notifs == 0)
if ret then
assert(spec.plugins.bar)
assert(not spec.disabled.bar)
else
assert(not spec.plugins.bar)
assert(spec.disabled.bar)
end
end
end)
it("handles the optional keyword", function()
local tests = {
[{ { "foo/bax" }, { "foo/bar", optional = true, dependencies = "foo/dep1" } }] = false,
[{ { "foo/bax", dependencies = "foo/dep1" }, { "foo/bar", optional = true, dependencies = "foo/dep1" } }] = true,
}
for test, ret in pairs(tests) do
local spec = Plugin.Spec.new(test)
assert(#spec.notifs == 0)
assert(spec.plugins.bax)
assert(not spec.plugins.bar)
assert(#spec.disabled == 0)
if ret then
assert(spec.plugins.dep1)
else
assert(not spec.plugins.opt1)
end
end
end)
end)
describe("plugin opts", function()
---@type {spec:LazySpec, opts:table}[]
local tests = {
{
spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } },
opts = { a = 2, b = 1 },
},
{
spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", opts = { a = 2 } } },
opts = { a = 2, b = 1 },
},
{
spec = { { "foo/foo", opts = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } },
opts = { a = 2, b = 1 },
},
{
spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = 2 } } },
opts = { a = 2, b = 1 },
},
{
spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo", config = { a = vim.NIL } } },
opts = { b = 1 },
},
{
spec = { { "foo/foo", config = { a = 1, b = 1 } }, { "foo/foo" } },
opts = { a = 1, b = 1 },
},
{
spec = { { "foo/foo" }, { "foo/foo" } },
opts = {},
},
}
for _, test in ipairs(tests) do
it("correctly parses opts for " .. inspect(test.spec), function()
local spec = Plugin.Spec.new(test.spec)
assert(spec.plugins.foo)
assert.same(test.opts, Plugin.values(spec.plugins.foo, "opts"))
end)
end
end)
describe("plugin spec", function()
it("only includes fragments from enabled plugins", function()
local tests = {
{
spec = {
{ "foo/disabled", enabled = false, dependencies = { "foo/bar", opts = { key_disabled = true } } },
{ "foo/disabled", dependencies = { "foo/bar", opts = { key_disabled_two = true } } },
{ "foo/conditional", cond = false, dependencies = { "foo/bar", opts = { key_cond = true } } },
{ "foo/optional", optional = true, dependencies = { "foo/bar", opts = { key_optional = true } } },
{ "foo/active", dependencies = { "foo/bar", opts = { key_active = true } } },
{
"foo/bar",
opts = { key = true },
},
}, },
expected_opts = { key = true, key_active = true }, })
}, -- for now, one test... end)
}
for _, test in ipairs(tests) do
local spec = Plugin.Spec.new(test.spec)
assert(#spec.notifs == 0)
assert(vim.tbl_count(spec.plugins) == 2)
assert(spec.plugins.active)
assert(spec.plugins.bar)
assert.same(test.expected_opts, Plugin.values(spec.plugins.bar, "opts"))
end
end) end)
end) end)

View file

@ -1,147 +0,0 @@
local Cache = require("lazy.core.cache")
local Helpers = require("tests.helpers")
local Util = require("lazy.util")
describe("util", function()
local rtp = vim.opt.rtp:get()
before_each(function()
---@type vim.Option
vim.opt.rtp = rtp
for k, v in pairs(package.loaded) do
if k:find("^foobar") then
package.loaded[k] = nil
end
end
Helpers.fs_rm("")
assert(not vim.uv.fs_stat(Helpers.path("")), "fs root should be deleted")
end)
it("lsmod lists all mods in dir", function()
vim.opt.rtp:append(Helpers.path(""))
local tests = {
{
root = "lua/foo",
mod = "foo",
files = { "lua/foo/one.lua", "lua/foo/two.lua", "lua/foo/init.lua" },
mods = { "foo.one", "foo.two", "foo" },
},
{
root = "lua/foo",
mod = "foo",
files = { "lua/foo/one.lua", "lua/foo/two.lua", "lua/foo.lua" },
mods = { "foo.one", "foo.two", "foo" },
},
{
root = "lua/foo",
mod = "foo",
files = { "lua/foo/one.lua", "lua/foo/two.lua" },
mods = { "foo.one", "foo.two" },
},
{
root = "lua/load-plugins",
mod = "load-plugins",
files = { "lua/load-plugins.lua" },
mods = { "load-plugins" },
},
}
for t, test in ipairs(tests) do
local expected = vim.deepcopy(test.mods)
table.sort(expected)
Helpers.fs_rm("")
local files = Helpers.fs_create(test.files)
-- test with empty cache
Cache.reset()
local root = Util.find_root(test.mod)
assert(root, "no root found for " .. test.mod .. " (test " .. t .. ")")
assert.same(Helpers.path(test.root), root)
local mods = {}
Util.lsmod(test.mod, function(modname, modpath)
mods[#mods + 1] = modname
end)
table.sort(mods)
assert.same(expected, mods)
-- fill the cache
Cache.reset()
root = Util.find_root(test.mod)
assert(root, "no root found for " .. test.mod .. " (test " .. t .. ")")
assert.same(Helpers.path(test.root), root)
mods = {}
Util.lsmod(test.mod, function(modname, modpath)
mods[#mods + 1] = modname
end)
table.sort(mods)
assert.same(expected, mods)
end
end)
it("find the correct root with dels", function()
Cache.reset()
vim.opt.rtp:append(Helpers.path("old"))
Helpers.fs_create({ "old/lua/foobar/init.lua" })
local root = Util.find_root("foobar")
assert(root, "foobar root not found")
assert.same(Helpers.path("old/lua/foobar"), root)
Helpers.fs_rm("old")
assert(not vim.uv.fs_stat(Helpers.path("old/lua/foobar")), "old/lua/foobar should not exist")
-- vim.opt.rtp = rtp
vim.opt.rtp:append(Helpers.path("new"))
Helpers.fs_create({ "new/lua/foobar/init.lua" })
root = Util.find_root("foobar")
assert(root, "foobar root not found")
assert.same(Helpers.path("new/lua/foobar"), root)
end)
it("merges correctly", function()
local tests = {
{
input = { { a = 1 }, { b = 2 } },
output = { a = 1, b = 2 },
},
{
input = { nil, { a = 1 }, { b = 2 } },
output = { a = 1, b = 2 },
},
{
input = { { a = 1 }, { b = 2 }, nil },
output = { a = 1, b = 2 },
},
{
input = { { a = 1 }, nil, { b = 2 } },
output = { a = 1, b = 2 },
},
{
input = { nil, { a = 1 }, nil, { b = 2 }, nil },
output = { a = 1, b = 2 },
},
{
input = { { a = 1 }, { a = 2 } },
output = { a = 2 },
},
{
input = { { a = { 1, 2 } }, { a = { 3 } } },
output = { a = { 3 } },
},
{
input = { { b = { 1, 2 } }, { a = { 3 }, b = vim.NIL } },
output = { a = { 3 } },
},
{
input = { { a = 1 }, { b = 2, a = vim.NIL } },
output = { b = 2 },
},
}
for _, test in ipairs(tests) do
local n = 0
for i in pairs(test.input) do
n = math.max(n, i)
end
assert.same(test.output, Util.merge(unpack(test.input, 1, n)))
end
end)
end)

View file

@ -1,18 +0,0 @@
local Keys = require("lazy.core.handler.keys")
describe("keys", function()
it("parses ids correctly", function()
local tests = {
{ "<C-/>", "<c-/>", true },
{ "<C-h>", "<c-H>", true },
{ "<C-h>k", "<c-H>K", false },
}
for _, test in ipairs(tests) do
if test[3] then
assert.same(Keys.parse(test[1]).id, Keys.parse(test[2]).id)
else
assert.is_not.same(Keys.parse(test[1]).id, Keys.parse(test[2]).id)
end
end
end)
end)

View file

@ -1,37 +0,0 @@
local Util = require("lazy.util")
local M = {}
M.fs_root = vim.fn.fnamemodify("./.tests/fs", ":p")
function M.path(path)
return Util.norm(M.fs_root .. "/" .. path)
end
---@param files string[]
function M.fs_create(files)
---@type string[]
local ret = {}
for _, file in ipairs(files) do
ret[#ret + 1] = Util.norm(M.fs_root .. "/" .. file)
local parent = vim.fn.fnamemodify(ret[#ret], ":h:p")
vim.fn.mkdir(parent, "p")
Util.write_file(ret[#ret], "")
end
return ret
end
function M.fs_rm(dir)
dir = Util.norm(M.fs_root .. "/" .. dir)
Util.walk(dir, function(path, _, type)
if type == "directory" then
vim.uv.fs_rmdir(path)
else
vim.uv.fs_unlink(path)
end
end)
vim.uv.fs_rmdir(dir)
end
return M

32
tests/init.lua Normal file
View file

@ -0,0 +1,32 @@
local M = {}
function M.root(root)
local f = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "")
end
---@param plugin string
function M.load(plugin)
local name = plugin:match(".*/(.*)")
local package_root = M.root(".tests/site/pack/deps/start/")
if not vim.loop.fs_stat(package_root .. name) then
print("Installing " .. plugin)
vim.fn.mkdir(package_root, "p")
vim.fn.system({
"git",
"clone",
"--depth=1",
"https://github.com/" .. plugin .. ".git",
package_root .. "/" .. name,
})
end
end
function M.setup()
vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.opt.runtimepath:append(M.root())
vim.opt.packpath = { M.root(".tests/site") }
M.load("nvim-lua/plenary.nvim")
end
M.setup()

View file

@ -1,19 +0,0 @@
local Async = require("lazy.async")
local Process = require("lazy.manage.process")
describe("process", function()
it("runs sync", function()
local lines = Process.exec({ "echo", "-n", "hello" })
assert.are.same({ "hello" }, lines)
end)
it("runs sync from async context", function()
local lines ---@type string[]
local async = Async.new(function()
lines = Process.exec({ "echo", "-n", "hello" })
end)
async:wait()
assert.are.same({ "hello" }, lines)
end)
end)

View file

@ -1,4 +1,3 @@
local Async = require("lazy.async")
local Runner = require("lazy.manage.runner") local Runner = require("lazy.manage.runner")
describe("runner", function() describe("runner", function()
@ -31,11 +30,11 @@ describe("runner", function()
end, end,
} }
package.loaded["lazy.manage.task.test"]["async" .. i] = { package.loaded["lazy.manage.task.test"]["async" .. i] = {
---@async
---@param task LazyTask ---@param task LazyTask
run = function(task) run = function(task)
Async.yield() task:schedule(function()
table.insert(runs, { plugin = task.plugin.name, task = task.name }) table.insert(runs, { plugin = task.plugin.name, task = task.name })
end)
end, end,
} }
end end
@ -65,7 +64,7 @@ describe("runner", function()
local runner = Runner.new({ plugins = plugins, pipeline = { "test.test1", "test.skip", "test.test2" } }) local runner = Runner.new({ plugins = plugins, pipeline = { "test.test1", "test.skip", "test.test2" } })
runner:start() runner:start()
runner:wait() runner:wait()
assert.equal(4, #runs, runs) assert.equal(4, #runs)
end) end)
it("handles opts", function() it("handles opts", function()

View file

@ -14,7 +14,6 @@ describe("semver version", function()
["1.2.3+build"] = { major = 1, minor = 2, patch = 3, build = "build" }, ["1.2.3+build"] = { major = 1, minor = 2, patch = 3, build = "build" },
} }
for input, output in pairs(tests) do for input, output in pairs(tests) do
output.input = input
it("correctly parses " .. input, function() it("correctly parses " .. input, function()
assert.same(output, v(input)) assert.same(output, v(input))
end) end)

View file

@ -1,82 +1,94 @@
--# selene:allow(incorrect_standard_library_use)
local Async = require("lazy.async")
local Task = require("lazy.manage.task") local Task = require("lazy.manage.task")
describe("task", function() describe("task", function()
local plugin = { name = "test", _ = {} } local plugin = { name = "test", _ = {} }
---@type {done?:boolean, error:string?} local done = false
local task_result = {} local error = nil
local opts = { local opts = {
---@param task LazyTask
on_done = function(task) on_done = function(task)
task_result = { done = true, error = task.error } done = true
error = task.error
end, end,
} }
before_each(function() before_each(function()
task_result = {} done = false
error = nil
end) end)
it("simple function", function() it("simple function", function()
local task = Task.new(plugin, "test", function() end, opts) local task = Task.new(plugin, "test", function() end, opts)
assert(task:running()) assert(not task:has_started())
task:wait() assert(not task:is_running())
assert(not task:running()) task:start()
assert(task_result.done) assert(not task:is_running())
assert(task:is_done())
assert(done)
end) end)
it("detects errors", function() it("detects errors", function()
local task = Task.new(plugin, "test", function() local task = Task.new(plugin, "test", function()
error("test") error("test")
end, opts) end, opts)
assert(task:running()) assert(not task:has_started())
task:wait() assert(not task:is_running())
assert(not task:running()) task:start()
assert(task_result.done) assert(task:is_done())
assert(task_result.error) assert(not task:is_running())
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test")) assert(done)
assert(error)
assert(task.error and task.error:find("test"))
end) end)
it("async", function() it("schedule", function()
local running = true local running = false
---@async local task = Task.new(plugin, "test", function(task)
local task = Task.new(plugin, "test", function() running = true
Async.yield() task:schedule(function()
running = false running = false
end)
end, opts) end, opts)
assert(task:running()) assert(not task:is_running())
assert(not task:has_started())
task:start()
assert(running) assert(running)
assert(task:running()) assert(#task._running == 1)
assert(task:is_running())
assert(not task:is_done())
task:wait() task:wait()
assert(not running) assert(task:is_done())
assert(not task:running()) assert(not task:is_running())
assert(task_result.done) assert(done)
assert(not task:has_errors()) assert(not task.error)
end) end)
it("spawn errors", function() it("spawn errors", function()
local task = Task.new(plugin, "spawn_errors", function(task) local task = Task.new(plugin, "test", function(task)
task:spawn("foobar") task:spawn("foobar")
end, opts) end, opts)
assert(task:running()) assert(not task:is_running())
task:wait() task:start()
assert(not task:running()) assert(not task:is_running())
assert(task_result.done) assert(done)
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task:output()) assert(task.error and task.error:find("Failed to spawn"))
end) end)
it("spawn", function() it("spawn", function()
local task = Task.new(plugin, "test", function(task) local task = Task.new(plugin, "test", function(task)
task:spawn("echo", { args = { "foo" } }) task:spawn("echo", { args = { "foo" } })
end, opts) end, opts)
assert(task:running()) assert(not task:is_running())
assert(task:running()) assert(not task:has_started())
task:start()
assert(task:has_started())
assert(task:is_running())
task:wait() task:wait()
assert.same(task:output(), "foo") assert(task:is_done())
assert(task_result.done) assert.same(task.output, "foo\n")
assert(not task:has_errors()) assert(done)
assert(not task.error)
end) end)
it("spawn 2x", function() it("spawn 2x", function()
@ -84,11 +96,12 @@ describe("task", function()
task:spawn("echo", { args = { "foo" } }) task:spawn("echo", { args = { "foo" } })
task:spawn("echo", { args = { "bar" } }) task:spawn("echo", { args = { "bar" } })
end, opts) end, opts)
assert(task:running()) assert(not task:is_running())
assert(task:running()) task:start()
assert(task:is_running())
task:wait() task:wait()
assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output()) assert.same(task.output, "foo\nbar\n")
assert(task_result.done) assert(done)
assert(not task:has_errors()) assert(not task.error)
end) end)
end) end)

View file

@ -1,12 +0,0 @@
#!/usr/bin/env -S nvim -l
vim.env.LAZY_STDPATH = ".tests"
vim.opt.rtp:prepend(".")
-- Setup lazy.nvim
require("lazy.minit").setup({
spec = {
{ dir = vim.uv.cwd() },
},
})

Some files were not shown because too many files have changed in this diff Show more