1

Refresh generated neovim config

This commit is contained in:
2024-08-15 13:01:03 +02:00
parent 64b51cf53a
commit f5af8e2b28
1836 changed files with 38979 additions and 31094 deletions

View File

@ -6,7 +6,10 @@ body:
- type: markdown
attributes:
value: |
**Before** reporting an issue, make sure to read the [documentation](https://github.com/folke/which-key.nvim) and search [existing issues](https://github.com/folke/which-key.nvim/issues). Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/folke/which-key.nvim/discussions) and will be closed.
**Before** reporting an issue, make sure to read the [documentation](https://github.com/folke/which-key.nvim)
and search [existing issues](https://github.com/folke/which-key.nvim/issues).
Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/folke/which-key.nvim/discussions) and will be closed.
- type: checkboxes
attributes:
label: Did you check docs and existing issues?
@ -14,6 +17,8 @@ body:
options:
- label: I have read all the which-key.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 which-key.nvim
required: true
- label: I have searched the existing issues of plugins related to this issue
@ -52,38 +57,30 @@ body:
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Health
description: Attach the output of `:checkhealth which-key` here
render: log
- type: textarea
attributes:
label: Log
description: Please enable logging with `opts.debug = true` and attach the contents of `./wk.log` here
render: log
- 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: |
-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
"folke/tokyonight.nvim",
{ "folke/which-key.nvim", config = true },
-- add any other plugins here
}
require("lazy").setup(plugins, {
root = root .. "/plugins",
require("lazy.minit").repro({
spec = {
{ "folke/which-key.nvim", opts = {} },
-- add any other plugins here
},
})
vim.cmd.colorscheme("tokyonight")
-- add anything else here
render: Lua
render: lua
validations:
required: false

View File

@ -1,72 +1,14 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
jobs:
tests:
strategy:
matrix:
# os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install Neovim
shell: bash
run: |
mkdir -p /tmp/nvim
wget -q https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage -O /tmp/nvim/nvim.appimage
cd /tmp/nvim
chmod a+x ./nvim.appimage
./nvim.appimage --appimage-extract
echo "/tmp/nvim/squashfs-root/usr/bin/" >> $GITHUB_PATH
- name: Run Tests
run: |
nvim --version
[ ! -d tests ] && exit 0
nvim --headless -u tests/init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.lua', sequential = true}"
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: which-key.nvim
version: "Neovim >= 0.8.0"
demojify: true
treesitter: 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
id: release
with:
release-type: simple
package-name: which-key.nvim
- uses: actions/checkout@v3
- name: tag stable versions
if: ${{ steps.release.outputs.release_created }}
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git remote add gh-token "https://${{ secrets.GITHUB_TOKEN }}@github.com/google-github-actions/release-please-action.git"
git tag -d stable || true
git push origin :stable || true
git tag -a stable -m "Last Stable Release"
git push origin stable
ci:
uses: folke/github/.github/workflows/ci.yml@main
secrets: inherit
with:
plugin: which-key.nvim
repo: folke/which-key.nvim

View File

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

View File

@ -1,5 +1,368 @@
# Changelog
## [3.13.2](https://github.com/folke/which-key.nvim/compare/v3.13.1...v3.13.2) (2024-07-24)
### Bug Fixes
* **view:** fix epanded keys. Fixes [#795](https://github.com/folke/which-key.nvim/issues/795) ([f5e0cd5](https://github.com/folke/which-key.nvim/commit/f5e0cd5c7712ac63d8e6184785fb7bdac3b7b50d))
## [3.13.1](https://github.com/folke/which-key.nvim/compare/v3.13.0...v3.13.1) (2024-07-24)
### Bug Fixes
* **state:** better current buf/mode check ([711453b](https://github.com/folke/which-key.nvim/commit/711453bb945433362636e918a5238172ffd21e43))
* **state:** deal with the fact that ModeChanged doesn't always seems to trigger. Fixes [#787](https://github.com/folke/which-key.nvim/issues/787) ([388bd3f](https://github.com/folke/which-key.nvim/commit/388bd3f83de06d1a1758ea6a342cf3ae614401f1))
## [3.13.0](https://github.com/folke/which-key.nvim/compare/v3.12.1...v3.13.0) (2024-07-24)
### Features
* **debug:** add git info to log when using lazy ([550338d](https://github.com/folke/which-key.nvim/commit/550338dc292c014d83687afccb0afb06e3e769f2))
## [3.12.1](https://github.com/folke/which-key.nvim/compare/v3.12.0...v3.12.1) (2024-07-24)
### Bug Fixes
* **node:** dynamic mappings only support functions as rhs. Fixes [#790](https://github.com/folke/which-key.nvim/issues/790) ([ba91db7](https://github.com/folke/which-key.nvim/commit/ba91db72ffc745983f06ca4e7d969101287a9afe))
* **state:** use cached mode. Fixes [#787](https://github.com/folke/which-key.nvim/issues/787). Closes [#789](https://github.com/folke/which-key.nvim/issues/789) ([c1b062a](https://github.com/folke/which-key.nvim/commit/c1b062ae95c3ca3e6eb87c075da991523605d79b))
* **triggers:** check for existing keymaps in the correct buffer. Fixes [#783](https://github.com/folke/which-key.nvim/issues/783) ([977fa23](https://github.com/folke/which-key.nvim/commit/977fa23622425e3c8ae837b9f7c710d9c78bdeab))
* **triggers:** nil error ([dae3bd2](https://github.com/folke/which-key.nvim/commit/dae3bd271826887771a7fb6deec231d2eb344f02))
## [3.12.0](https://github.com/folke/which-key.nvim/compare/v3.11.1...v3.12.0) (2024-07-22)
### Features
* **api:** allow overriding delay. Closes [#778](https://github.com/folke/which-key.nvim/issues/778) ([e6fea48](https://github.com/folke/which-key.nvim/commit/e6fea4889c20f22a7c6267cf1f1d091bc96f8ca0))
### Bug Fixes
* dont expand nodes without children. Fixes [#782](https://github.com/folke/which-key.nvim/issues/782) ([53a1d2a](https://github.com/folke/which-key.nvim/commit/53a1d2a674df5fb667497fe3bbda625c39a2c0e1))
## [3.11.1](https://github.com/folke/which-key.nvim/compare/v3.11.0...v3.11.1) (2024-07-21)
### Bug Fixes
* **config:** keys can be any case. Fixes [#771](https://github.com/folke/which-key.nvim/issues/771) ([0a44d55](https://github.com/folke/which-key.nvim/commit/0a44d55d3bcdf75a134ec57c90aaec1055731014))
* **config:** normalize keys ([c96be9b](https://github.com/folke/which-key.nvim/commit/c96be9bd54ffbc2ec7fc818001cad712119d778c))
## [3.11.0](https://github.com/folke/which-key.nvim/compare/v3.10.0...v3.11.0) (2024-07-20)
### Features
* **icons:** icon for grug-far ([b2a2a0c](https://github.com/folke/which-key.nvim/commit/b2a2a0c9486211da23acdf18087f8203bfbca0e4))
* **state:** detect recursion by users mapping wk manually. Closes [#761](https://github.com/folke/which-key.nvim/issues/761) ([55fa07f](https://github.com/folke/which-key.nvim/commit/55fa07fbbd8a4c6d75399b1d1f9005d146cda22c))
* **view:** expand recursively. Closes [#767](https://github.com/folke/which-key.nvim/issues/767) ([5ae87af](https://github.com/folke/which-key.nvim/commit/5ae87af42914afe8b478ad6cdb3cb179fb73a62b))
### Bug Fixes
* **config:** disable wk by default for insert/command mode ([9d2b2e7](https://github.com/folke/which-key.nvim/commit/9d2b2e7059547c0481db2e93fd98547b26c7c05a))
* **config:** more checks in validate ([bdcc429](https://github.com/folke/which-key.nvim/commit/bdcc429afaecc5896b462b0b07c2b3a9e9c1c60f))
* **mappings:** preset descriptions should not override existing keymap descriptions. Fixes [#769](https://github.com/folke/which-key.nvim/issues/769) ([82d628f](https://github.com/folke/which-key.nvim/commit/82d628f4cfa397cf4bb233bd500e9cd9a018ded1))
* **state:** don't feed count in insert mode. Fixes [#770](https://github.com/folke/which-key.nvim/issues/770) ([63690ff](https://github.com/folke/which-key.nvim/commit/63690ff34a8921c2de44fad289e2e04ee324b031))
* **util:** better `&lt;nop&gt;` check. Fixes [#766](https://github.com/folke/which-key.nvim/issues/766) ([b7b3bd1](https://github.com/folke/which-key.nvim/commit/b7b3bd1b609524472f67e4a69d2bc14b80ea997f))
* **view:** dont set title when no border. Fixes [#764](https://github.com/folke/which-key.nvim/issues/764) ([6e61b09](https://github.com/folke/which-key.nvim/commit/6e61b0904e9c038b6c511c43591ae2d811b4975e))
### Performance Improvements
* prevent expanding nodes when not needed ([78cc92c](https://github.com/folke/which-key.nvim/commit/78cc92c6cb7da88df60227bc334a598a7e772e51))
## [3.10.0](https://github.com/folke/which-key.nvim/compare/v3.9.0...v3.10.0) (2024-07-18)
### Features
* **view:** expand all nodes by default when filter.global = false ([c168905](https://github.com/folke/which-key.nvim/commit/c168905d62d9b8859b261de69910dfb7e3438996))
### Bug Fixes
* **buf:** always use nowait. Fixes [#755](https://github.com/folke/which-key.nvim/issues/755) ([ae1a235](https://github.com/folke/which-key.nvim/commit/ae1a235c53233c58a2f7cc14e5cdd09346cf27ed))
* **buf:** early exit to determine if a trigger is safe to create. Fixes [#754](https://github.com/folke/which-key.nvim/issues/754) ([27e4716](https://github.com/folke/which-key.nvim/commit/27e47163165fee8e45b43d340db9335001403d2f))
* **icons:** added frontier pattern for `ai` ([#760](https://github.com/folke/which-key.nvim/issues/760)) ([6fe0657](https://github.com/folke/which-key.nvim/commit/6fe065716e08550328c471689e6f8c1e42a0effc))
* list_contains doesn't exists in Neovim &lt; 0.10. Fixes [#758](https://github.com/folke/which-key.nvim/issues/758) ([7e4eae8](https://github.com/folke/which-key.nvim/commit/7e4eae8836e4ad28d478fedc421700b1138d1e0c))
* **node:** allow custom mappings to override proxy/plugin/expand mappings ([9820900](https://github.com/folke/which-key.nvim/commit/982090080fa11da06038cf8e71af90d3a4fbd05a))
* **node:** is_local check now also includes children ([fdd27f9](https://github.com/folke/which-key.nvim/commit/fdd27f9b6a991586943eb865275b279fb411ff0b))
* **registers:** don't try to get `+*` registers when no clipboard is available. Fixes [#754](https://github.com/folke/which-key.nvim/issues/754) ([ae4ec03](https://github.com/folke/which-key.nvim/commit/ae4ec030489d7ecda908e473aea096a7594f84e8))
* **state:** always honor defer. Fixes [#690](https://github.com/folke/which-key.nvim/issues/690) ([c512d13](https://github.com/folke/which-key.nvim/commit/c512d135531be81e17c85e254994cc755d3016c5))
* **state:** redraw cursor before getchar ([cf6cbf2](https://github.com/folke/which-key.nvim/commit/cf6cbf2fd8f0c6497f130d07f6c88a2833c15d80))
* **triggers:** prevent creating triggers for single upper-case alpha keys expect for Z. Fixes [#756](https://github.com/folke/which-key.nvim/issues/756) ([d19fa07](https://github.com/folke/which-key.nvim/commit/d19fa07b6e818ab55c34815784470a6d5f023524))
## [3.9.0](https://github.com/folke/which-key.nvim/compare/v3.8.0...v3.9.0) (2024-07-18)
### Features
* **config:** simplified config. Some options are now deprecated ([8ddf2da](https://github.com/folke/which-key.nvim/commit/8ddf2da5a6aa76f5b3cec976f1d61e7c7fea42b5))
* **view:** show and expand localleader mappings with filter.global = false ([ed5f762](https://github.com/folke/which-key.nvim/commit/ed5f7622771d0b5c0ac3a5e286ec6cd17b6be131))
### Bug Fixes
* **ui:** remove deprecated opts.layout.align option. (wasn't used). Closes [#752](https://github.com/folke/which-key.nvim/issues/752) ([db32ac6](https://github.com/folke/which-key.nvim/commit/db32ac67abb36789a43fe497ff7d0b8ab7e8109e))
## [3.8.0](https://github.com/folke/which-key.nvim/compare/v3.7.0...v3.8.0) (2024-07-17)
### Features
* **mappings:** added health check for invalid modes ([640724a](https://github.com/folke/which-key.nvim/commit/640724a541af75e6bbfe98f78cdebbec701d23a8))
### Bug Fixes
* **buf:** never create proxy/plugin mappings when a keymap exists. Fixes [#738](https://github.com/folke/which-key.nvim/issues/738) ([b4c4e36](https://github.com/folke/which-key.nvim/commit/b4c4e3648261399a97bfdc44bb8fa31b485fd3b9))
* **registers:** use x instead of v ([#742](https://github.com/folke/which-key.nvim/issues/742)) ([5c3b3e8](https://github.com/folke/which-key.nvim/commit/5c3b3e834852a44efb26725f9c08917145f2c0c6))
* **state:** schedule redraw. Fixes [#740](https://github.com/folke/which-key.nvim/issues/740) ([09f21a1](https://github.com/folke/which-key.nvim/commit/09f21a133104b66a5cede8fc0a8082b85b0eee9b))
* **triggers:** allow overriding keymaps with empty rhs or &lt;Nop&gt;. Fixes [#748](https://github.com/folke/which-key.nvim/issues/748) ([843a93f](https://github.com/folke/which-key.nvim/commit/843a93fac6bca58167aafa392e6f7fd5a77633c9))
* **triggers:** make sure no keymaps exists for triggers ([e8b454f](https://github.com/folke/which-key.nvim/commit/e8b454fb03e3cab398c894e5d462c84595ee57ca))
* **typo:** replace 'exras' for 'extras' in README. ([#745](https://github.com/folke/which-key.nvim/issues/745)) ([af48cdc](https://github.com/folke/which-key.nvim/commit/af48cdc4bb8f1982a6124bf6bb5570349f690822))
## [3.7.0](https://github.com/folke/which-key.nvim/compare/v3.6.0...v3.7.0) (2024-07-17)
### Features
* added `expand` property to create dynamic mappings. An example for `buf` and `win` is included ([02f6e6f](https://github.com/folke/which-key.nvim/commit/02f6e6f4951ff993ad1d5c699784e6847a6c7b4c))
* proxy mappings ([c3cfc2b](https://github.com/folke/which-key.nvim/commit/c3cfc2bdb03c1b87943a6d02485ad50b86567341))
* **state:** allow defering on certain operators. Closes [#733](https://github.com/folke/which-key.nvim/issues/733) ([984d930](https://github.com/folke/which-key.nvim/commit/984d930711341ac118e6712804e8e22e575ba9d3))
### Bug Fixes
* **buf:** create triggers for xo anyway. Fixes [#728](https://github.com/folke/which-key.nvim/issues/728) ([96b2e93](https://github.com/folke/which-key.nvim/commit/96b2e93979373744056c921f82b0c356e6f900de))
* **icons:** added frontier pattern for `git`. Fixes [#727](https://github.com/folke/which-key.nvim/issues/727) ([bb4e82b](https://github.com/folke/which-key.nvim/commit/bb4e82bdaff50a4a93867e4c90938d18e7615af6))
* **state:** dont popup when switching between v and V mode. Fixes [#729](https://github.com/folke/which-key.nvim/issues/729) ([8ddb527](https://github.com/folke/which-key.nvim/commit/8ddb527bcffc6957a59518f11c34a84d91e075f9))
* **view:** always show a group as a group ([96a9eb3](https://github.com/folke/which-key.nvim/commit/96a9eb3f0b3299dffc241cf0f9ee5cf0509e6cd2))
* **view:** empty icons ([e2cacc6](https://github.com/folke/which-key.nvim/commit/e2cacc6f1e4ba77e82e7a34e0dc6b2ad69cf075b))
## [3.6.0](https://github.com/folke/which-key.nvim/compare/v3.5.0...v3.6.0) (2024-07-16)
### Features
* added icons for &lt;D mappings ([aaf71ab](https://github.com/folke/which-key.nvim/commit/aaf71ab078d86a48a26fafb5d451af609fd19c64))
* added option to disable all mapping icons. Fixes [#721](https://github.com/folke/which-key.nvim/issues/721) ([33f6ac0](https://github.com/folke/which-key.nvim/commit/33f6ac04bdbce855ce43eecacb4c421876e246d7))
* make which-key work without setup or calling add/register ([9ca5f4a](https://github.com/folke/which-key.nvim/commit/9ca5f4ab7cb541ef48dcaa4f03d3cd914a5e62fb))
* **presets:** added some missing mappings ([6e1b3f2](https://github.com/folke/which-key.nvim/commit/6e1b3f290a3f89ffca68148aa639c866c24e2b77))
* **state:** improve trigger/mode logic. Fixes [#715](https://github.com/folke/which-key.nvim/issues/715) ([3617e47](https://github.com/folke/which-key.nvim/commit/3617e47673d027989e9c3caa645edb6412c7fa30))
### Bug Fixes
* **config:** replacement for plug mappings ([495f9d9](https://github.com/folke/which-key.nvim/commit/495f9d953a86d630ef308f555ed452e332f417ee))
* **icons:** get icons from parent nodes when needed ([3f0a7ed](https://github.com/folke/which-key.nvim/commit/3f0a7ed4401b98764740cbe8e1b954ac6adeca1b))
* **icons:** use nerdfont symbol for BS. Fixes [#722](https://github.com/folke/which-key.nvim/issues/722) ([18c1ff5](https://github.com/folke/which-key.nvim/commit/18c1ff5ccb813d95c86f4ead6dac7e6cc5728f08))
* **mode:** never create triggers for xo mode ([15d3a70](https://github.com/folke/which-key.nvim/commit/15d3a70304607417b2dc1df3da4992d5b8ce077a))
* **presets:** motions in normal mode ([e2ffc26](https://github.com/folke/which-key.nvim/commit/e2ffc263fc05bf20f090ccaae7a06f88fd6e2fee))
* tmp fix for op mode ([91641e2](https://github.com/folke/which-key.nvim/commit/91641e2a3af116ffaf739302a65cdb2865fb2415))
* **view:** fix format for keymaps with 3+ keys ([#723](https://github.com/folke/which-key.nvim/issues/723)) ([0db7896](https://github.com/folke/which-key.nvim/commit/0db7896057d046576c829a87e2ff2de37c49e0fe))
## [3.5.0](https://github.com/folke/which-key.nvim/compare/v3.4.0...v3.5.0) (2024-07-15)
### Features
* **api:** using wk.show() always assumes you want to see the group, and not the actual mapping in case of overlap. Fixes [#714](https://github.com/folke/which-key.nvim/issues/714) ([f5067d2](https://github.com/folke/which-key.nvim/commit/f5067d2b244c19eca38b5b495b6eb3e361ac565d))
### Bug Fixes
* **state:** attach on BufNew as well. Fixes [#681](https://github.com/folke/which-key.nvim/issues/681) ([0f58176](https://github.com/folke/which-key.nvim/commit/0f581764dc2c89c0ac3d8363369152735ae265ab))
* **state:** make sure mode always exists even when not safe. See [#681](https://github.com/folke/which-key.nvim/issues/681) ([7915964](https://github.com/folke/which-key.nvim/commit/7915964e73c30ba5657e9a762c6570925dad421b))
### Performance Improvements
* **plugin:** only expand plugins when needed ([1fcfc72](https://github.com/folke/which-key.nvim/commit/1fcfc72374c705d68f0607a1dcbbbce13873b4e2))
* **view:** set buf/win opts with eventignore ([e81e55b](https://github.com/folke/which-key.nvim/commit/e81e55b647a781f306453734834eb543e1f43c20))
## [3.4.0](https://github.com/folke/which-key.nvim/compare/v3.3.0...v3.4.0) (2024-07-15)
### Features
* added icons for function keys ([9222280](https://github.com/folke/which-key.nvim/commit/9222280970e8a7d74b4e0f6dab06c2f7a54d668d))
* **mode:** allow certain modes to start hidden and only show after keypress. See [#690](https://github.com/folke/which-key.nvim/issues/690) ([b4fa48f](https://github.com/folke/which-key.nvim/commit/b4fa48f473796f5d9e3c9c31e6c9d7d509e51ca6))
* **presets:** added gw ([09b80a6](https://github.com/folke/which-key.nvim/commit/09b80a68085c1fc792b595a851f702bc071d6310))
* **presets:** better padding defaults for helix preset ([4c36b9b](https://github.com/folke/which-key.nvim/commit/4c36b9b8c722bcf51d038dcfba8b967f0ee818b8))
* **presets:** increase default height for helix ([df0ad20](https://github.com/folke/which-key.nvim/commit/df0ad205ebd661ef101666ae21a62b77b3024a83))
* simplified/documented/fixed mappings sorting. Closes [#694](https://github.com/folke/which-key.nvim/issues/694) ([eb73f7c](https://github.com/folke/which-key.nvim/commit/eb73f7c05785a83e07f1ea155b3b2833d8bbb532))
* **state:** skip mouse keys in debug ([5f85b77](https://github.com/folke/which-key.nvim/commit/5f85b770c386c9435eb8da5db3081aa19078211a))
* **ui:** show keys/help in an overlay and added scrolling hint ([50b2c43](https://github.com/folke/which-key.nvim/commit/50b2c43532e6ea5cca3ef4c2838d5a8bb535757f))
* **view:** get parent icon if possible ([b9de927](https://github.com/folke/which-key.nvim/commit/b9de9278bdc57adfa69a67d8a3309f07c83951d0))
### Bug Fixes
* **buf:** always detach " when executing keys. Fixes [#689](https://github.com/folke/which-key.nvim/issues/689) ([d36f722](https://github.com/folke/which-key.nvim/commit/d36f722f114dfdafc8098496e9b5dcbd9f8fc3e8))
* **config:** set expand=0 by default. Fixes [#693](https://github.com/folke/which-key.nvim/issues/693) ([89434aa](https://github.com/folke/which-key.nvim/commit/89434aa356abd4a694d2b89eccb203e0742bc0d7))
* **config:** warn when deprecated config options were used. Fixes [#696](https://github.com/folke/which-key.nvim/issues/696) ([81413ef](https://github.com/folke/which-key.nvim/commit/81413ef02dffbe6e4c73f418e4acc920e68b3aa7))
* **health:** move deprecated option check to health ([af7a30f](https://github.com/folke/which-key.nvim/commit/af7a30fa24ce0a13dba00cbd7b836330facf9f1a))
* **mappings:** allow creating keymaps without desc. Fixes [#695](https://github.com/folke/which-key.nvim/issues/695) ([c442aaa](https://github.com/folke/which-key.nvim/commit/c442aaa6aafe2742c2e92df7ee127df90099ce17))
* **plugins:** add existing keymaps to plugin view. Fixes [#681](https://github.com/folke/which-key.nvim/issues/681) ([26f6fd2](https://github.com/folke/which-key.nvim/commit/26f6fd258b66e9656bb86c7269c6497a9ce8a5fa))
* **presets:** don't override title setting for classic. See [#649](https://github.com/folke/which-key.nvim/issues/649) ([9a53c1f](https://github.com/folke/which-key.nvim/commit/9a53c1ff46421450b5563baab1599591d81de111))
* **presets:** shorter descriptions ([20600e4](https://github.com/folke/which-key.nvim/commit/20600e422277b383e8c921feec2111a281935217))
* **state:** always do full update on BufReadPost since buffer-local keymaps would be deleted. Fixes [#709](https://github.com/folke/which-key.nvim/issues/709) ([6068887](https://github.com/folke/which-key.nvim/commit/60688872f4ecc552a5e2bcbd01e7629a155f377f))
* **state:** don't show when coming from cmdline mode. Fixes [#692](https://github.com/folke/which-key.nvim/issues/692) ([8cba66b](https://github.com/folke/which-key.nvim/commit/8cba66b5a1a0ea8fe8dd5d3d55a42755924e47d8))
* **state:** honor timeoutlen and nowait. Fixes [#648](https://github.com/folke/which-key.nvim/issues/648). Closes [#697](https://github.com/folke/which-key.nvim/issues/697) ([80f20ee](https://github.com/folke/which-key.nvim/commit/80f20ee62311505fe6d675212f7b246900570450))
* **state:** properly disable which-key when recording macros. Fixes [#702](https://github.com/folke/which-key.nvim/issues/702) ([b506275](https://github.com/folke/which-key.nvim/commit/b506275acfb4383f678b9ba3aa8db88787c24680))
* **state:** scrolling ([dce9167](https://github.com/folke/which-key.nvim/commit/dce9167025a0801e4bab146a2856508a9af52ea2))
* **tree:** rawget for existing plugin node children ([c77cda8](https://github.com/folke/which-key.nvim/commit/c77cda8cd2f54965e4316699f1d124a2b3bf9d49))
* **util:** when no clipboard provider exists, use the " register as default. Fixes [#687](https://github.com/folke/which-key.nvim/issues/687) ([d077a3f](https://github.com/folke/which-key.nvim/commit/d077a3f36d4b4d29eccc7feb1ba8e78a421df920))
* **view:** disable footer on Neovim &lt; 0.10 ([6d544a4](https://github.com/folke/which-key.nvim/commit/6d544a43a21a228482155d65c3ca18fd7038b422))
* **view:** ensure highlights get set for title padding ([#684](https://github.com/folke/which-key.nvim/issues/684)) ([2e4f7af](https://github.com/folke/which-key.nvim/commit/2e4f7afa4aa444483d8ade5989d524c7f4131368))
* **view:** hide existing title/footer when no trail ([4f589a1](https://github.com/folke/which-key.nvim/commit/4f589a1368e100a6e33aabd904f34716b75360f6))
* **view:** include group keymaps in expand results. See [#682](https://github.com/folke/which-key.nvim/issues/682) ([39e703c](https://github.com/folke/which-key.nvim/commit/39e703ceaa9a05dcc664e0ab0ea88c03c3b6bf90))
* **view:** overlap protection should keep at least 4 lines ([0d89475](https://github.com/folke/which-key.nvim/commit/0d89475f87756199efc2bc52537fc4d11b0f695a))
* **view:** padding & column spacing. Fixes [#704](https://github.com/folke/which-key.nvim/issues/704) ([11eec49](https://github.com/folke/which-key.nvim/commit/11eec49509490c023bf0272efef955f86f18c1d2))
* **view:** spacing when more than one box ([89568f3](https://github.com/folke/which-key.nvim/commit/89568f3438f1fbc6c340a8af05ea67feac494c46))
* **view:** special handling of `&lt;NL&gt;/<C-J>`. Fixes [#706](https://github.com/folke/which-key.nvim/issues/706) ([f8c91b2](https://github.com/folke/which-key.nvim/commit/f8c91b2b4a2d239d3b1d49f901a393e7326a5da8))
## [3.3.0](https://github.com/folke/which-key.nvim/compare/v3.2.0...v3.3.0) (2024-07-14)
### Features
* **expand:** allow expand to be a function. Closes [#670](https://github.com/folke/which-key.nvim/issues/670) ([dfaa10c](https://github.com/folke/which-key.nvim/commit/dfaa10cd24badb321a4667fb9135f242393e5680))
* **mappings:** mapping `desc` and `icon` can now be a function that is evaluated when which-key is show. Fixes [#666](https://github.com/folke/which-key.nvim/issues/666) ([c634af1](https://github.com/folke/which-key.nvim/commit/c634af1295512dc2062fbec38f563f5793de245c))
* **mappings:** opts.filter to exclude certain mappings from showing up in which-key. ([763ea00](https://github.com/folke/which-key.nvim/commit/763ea000cce9589124515ba34f6d9a6347a02891))
* **view:** add operator to trail in op mode ([5a6eaaa](https://github.com/folke/which-key.nvim/commit/5a6eaaa4ebc072625b9fc906943e3798028bd817))
* **view:** when in visual mode, propagate esc. See [#656](https://github.com/folke/which-key.nvim/issues/656) ([30ef44a](https://github.com/folke/which-key.nvim/commit/30ef44a13065a157f97d3fb5bbf23a5c23e513eb))
### Bug Fixes
* default preset ([38987d3](https://github.com/folke/which-key.nvim/commit/38987d3f18a8ffc5eaa404d746fd8ee4017b5f37))
* **mappings:** don't show `&lt;SNR&gt;` mappings ([d700244](https://github.com/folke/which-key.nvim/commit/d700244acc1f1474b34737e14a45df2aa3a324ba))
* **presets:** max 1 column in helix mode. Fixes [#665](https://github.com/folke/which-key.nvim/issues/665) ([b2a6910](https://github.com/folke/which-key.nvim/commit/b2a6910e9e97526f2327327d2751834049cbd334))
* **presets:** shorter descriptions ([9a73d6a](https://github.com/folke/which-key.nvim/commit/9a73d6a0b0d5f456a9768d434a83d6d4cdb83efa))
* **state:** cooldown till next tick when not safe to open which-key. Fixes [#672](https://github.com/folke/which-key.nvim/issues/672) ([bdf3b27](https://github.com/folke/which-key.nvim/commit/bdf3b272ea34ac137af3cb1ebcd5cf8c9745abbb))
* **util:** nt mode should map to n ([969afc9](https://github.com/folke/which-key.nvim/commit/969afc95d374bc0d6ce397d3d2357d8faa38041a))
* **view:** set nowrap for the which-key window ([6e1c098](https://github.com/folke/which-key.nvim/commit/6e1c0987024adf63ab91f281f8f9c355abf3f3d8))
* **view:** set winhl groups. Fixes [#661](https://github.com/folke/which-key.nvim/issues/661) ([baff8ea](https://github.com/folke/which-key.nvim/commit/baff8ea846cbb613dee79333aad7a1d2b912a5bc))
## [3.2.0](https://github.com/folke/which-key.nvim/compare/v3.1.0...v3.2.0) (2024-07-13)
### Features
* added `opts.debug` that writes to wk.log in the current directory ([c23df71](https://github.com/folke/which-key.nvim/commit/c23df711884d97963d0c17ed29f5d8c1064d4adc))
* hydra mode. will document later ([65f2e72](https://github.com/folke/which-key.nvim/commit/65f2e7236a3bc278dd163d7c98c9ea5d9ab6e42e))
* **icons:** add telescope icon ([#643](https://github.com/folke/which-key.nvim/issues/643)) ([fca3d9e](https://github.com/folke/which-key.nvim/commit/fca3d9eaef57ddb3ce438d208ebc32e23c9f290a))
### Bug Fixes
* layout stuff ([7423096](https://github.com/folke/which-key.nvim/commit/742309697cff6aa7f377b72e2f54d34afef09ee1))
* **mappings:** always use mapping even when it's creating a keymap. Fixes [#637](https://github.com/folke/which-key.nvim/issues/637) ([2d744cb](https://github.com/folke/which-key.nvim/commit/2d744cb824c0f310be420bf33688bc005f164f46))
* **mappings:** make replace_keycodes default to false in v1 spec ([6ec0a1e](https://github.com/folke/which-key.nvim/commit/6ec0a1ef89209680c799269227b4d0c28de1d877))
* **state:** dont start which-key during dot repeat. Fixes [#636](https://github.com/folke/which-key.nvim/issues/636) ([5971ecd](https://github.com/folke/which-key.nvim/commit/5971ecdf4465425d6bc6e2277101c6fc896cbe06))
* **state:** dont start which-key more than once during the same tick in xo mode. Fixes [#635](https://github.com/folke/which-key.nvim/issues/635) ([0218fce](https://github.com/folke/which-key.nvim/commit/0218fce1c3d54307217391215db28e63de9b8980))
* **state:** dont start wk when chars are pending. Fixes [#658](https://github.com/folke/which-key.nvim/issues/658). Fixes [#655](https://github.com/folke/which-key.nvim/issues/655). Fixes [#648](https://github.com/folke/which-key.nvim/issues/648) ([877ce16](https://github.com/folke/which-key.nvim/commit/877ce163d764bbe7c82a7fec5671c32188607754))
* **state:** only hide on focus lost when still hidden after 1s. Fixes [#638](https://github.com/folke/which-key.nvim/issues/638) ([649a51b](https://github.com/folke/which-key.nvim/commit/649a51bc81b09443c326d390e3d182e0cdf98c15))
* **types:** spec field types ([#645](https://github.com/folke/which-key.nvim/issues/645)) ([c6ffb1c](https://github.com/folke/which-key.nvim/commit/c6ffb1ce63959d5f1effe5924712f36eac1e940e))
* **util:** set local window opts for notify. Fixes [#641](https://github.com/folke/which-key.nvim/issues/641) ([63f2112](https://github.com/folke/which-key.nvim/commit/63f2112361a53b0cf68245868977773f210bb5cd))
* **view:** check for real overlap instead of just row overlap. See [#649](https://github.com/folke/which-key.nvim/issues/649) ([0427e91](https://github.com/folke/which-key.nvim/commit/0427e91dbbd9c37eb20e6fbc2386f890dc0d7e2a))
* **view:** disable folds. Fixes [#99](https://github.com/folke/which-key.nvim/issues/99) ([6860e3b](https://github.com/folke/which-key.nvim/commit/6860e3b681b40e3620049f714ae53a6bad594701))
## [3.1.0](https://github.com/folke/which-key.nvim/compare/v3.0.0...v3.1.0) (2024-07-12)
### Features
* allow disabling any trigger ([94b7951](https://github.com/folke/which-key.nvim/commit/94b795154fb213db6ed8aeba3d7f53cbce7c147c))
### Bug Fixes
* added support for vim.loop ([54db192](https://github.com/folke/which-key.nvim/commit/54db1928c17ac420e897a40f5ad560ee9f28b186))
* automatically do setup if setup wasn't called within 500ms. Fixes [#630](https://github.com/folke/which-key.nvim/issues/630) ([632ad41](https://github.com/folke/which-key.nvim/commit/632ad41b5fcf60fac897d0b6530a699eb980748d))
* **buf:** buffer-local mappings were broken (not keymaps). Fixes [#629](https://github.com/folke/which-key.nvim/issues/629) ([58d7f82](https://github.com/folke/which-key.nvim/commit/58d7f822ecc80ca4b43e9c14fd6ec962483e2168))
* **colors:** compat with older Neovim vesions. Fixes [#631](https://github.com/folke/which-key.nvim/issues/631) ([4516dc9](https://github.com/folke/which-key.nvim/commit/4516dc9422f571c9e189ff6696853d445a3058d6))
## [3.0.0](https://github.com/folke/which-key.nvim/compare/v2.1.0...v3.0.0) (2024-07-12)
### ⚠ BREAKING CHANGES
* v3 release
### Features
* added health check back with better wording on what actually gets checked ([97e6e41](https://github.com/folke/which-key.nvim/commit/97e6e4166134aad826454588ae764c7a54f5d298))
* added WhichKey command ([7c12ab9](https://github.com/folke/which-key.nvim/commit/7c12ab9c2569a7459932bc19a4e52ea5a48437b2))
* automatically use nowait based on delay and timeoutlen ([110ed72](https://github.com/folke/which-key.nvim/commit/110ed728bedd0182e4d11726194f7eb5db63e2fb))
* bring config back and create mappings when needed ([add7ab9](https://github.com/folke/which-key.nvim/commit/add7ab92163399c47f7149c96387d382e9d8996b))
* buffer-local sort & refactor API ([14309d0](https://github.com/folke/which-key.nvim/commit/14309d0446dcc6a24421c56e914e06b1fe2d4f41))
* close which-key on FocusLost ([aa99460](https://github.com/folke/which-key.nvim/commit/aa99460e117d0348c7f1f77ab669398c04fcba6b))
* config, and presets ([541989d](https://github.com/folke/which-key.nvim/commit/541989db167e04eb3db24ba57decab0326614f0f))
* expand groups with less than n mappings. Closes [#374](https://github.com/folke/which-key.nvim/issues/374). Fixes [#90](https://github.com/folke/which-key.nvim/issues/90). Closes [#208](https://github.com/folke/which-key.nvim/issues/208) ([5caf057](https://github.com/folke/which-key.nvim/commit/5caf057b3a204a94d53b4b0200ce915463b4a922))
* fancy key icons ([e4d0134](https://github.com/folke/which-key.nvim/commit/e4d01347434b31e8a90720463076bbbeebbef199))
* fix hidden and empty groups ([afc4aa9](https://github.com/folke/which-key.nvim/commit/afc4aa96ae5671f5d4d14f332789dec72dd5db02))
* **health:** duplicate mappings check ([4762e06](https://github.com/folke/which-key.nvim/commit/4762e06f9dc45b3470ab5b2efa0a4b3de6148298))
* **health:** icon providers & overlapping keys ([dcbf29a](https://github.com/folke/which-key.nvim/commit/dcbf29ae337bd4d621e326b6f1caad66cfe0770a))
* initial rewrite ([eb3ad2e](https://github.com/folke/which-key.nvim/commit/eb3ad2eb062392497d0fed3489e2582d4e5bc289))
* keep track of virtual mappings ([4537d3e](https://github.com/folke/which-key.nvim/commit/4537d3ea52b2b11b96ca2fdde2bb4573f0ca7c73))
* key/desc replacements ([cf34ffe](https://github.com/folke/which-key.nvim/commit/cf34ffe9384941dc833ed2a3bb2a3bf3aa050373))
* layout ([347288a](https://github.com/folke/which-key.nvim/commit/347288acd8398ae7c641bd6159261e98f9a6b929))
* manual sorting. Closes [#131](https://github.com/folke/which-key.nvim/issues/131), Closes [#362](https://github.com/folke/which-key.nvim/issues/362), Closes [#264](https://github.com/folke/which-key.nvim/issues/264) ([c2daf9d](https://github.com/folke/which-key.nvim/commit/c2daf9dcf48e8c8cca61cfc27b1731272b9bc2c6))
* **mappings:** added support for lazy.nvim style mappings ([6f7a945](https://github.com/folke/which-key.nvim/commit/6f7a945f1dc679ce2c35064e12e4dc531ebf2c3c))
* **mappings:** added support for setting custom icons from the spec ([951ae7a](https://github.com/folke/which-key.nvim/commit/951ae7a89d164f39f8aa49f51da424539370f6c4))
* new spec and migration recommendation for health ([41374bc](https://github.com/folke/which-key.nvim/commit/41374bcae462d897fa98c904a44127e258c0438c))
* option to disable icon colors ([79c8ac8](https://github.com/folke/which-key.nvim/commit/79c8ac87139dcb816072c1a5ca1800d9ce5d64aa))
* option to disable notify ([4cc46ff](https://github.com/folke/which-key.nvim/commit/4cc46ffa57b8a6ebf6ca7a07128d353f5569a802))
* play nice with macros ([1abc2bf](https://github.com/folke/which-key.nvim/commit/1abc2bf96472e7816252719de06d60e9b09035dc))
* plugins partially working again ([b925b31](https://github.com/folke/which-key.nvim/commit/b925b31bab1f91507d15a96f226f7f7423c4fced))
* **registers:** show non-printable with keytrans ([1832197](https://github.com/folke/which-key.nvim/commit/183219772d01e0ea744c0ff8bf656895f7d7c8d3))
* spec parser rewrite & proper typings ([07065fe](https://github.com/folke/which-key.nvim/commit/07065fe345bc9dd20aff11ab9a6a3b078aacd42e))
* state management ([e2ee1fa](https://github.com/folke/which-key.nvim/commit/e2ee1fae13f7a6c38652994dedb0cb34e2608918))
* state management ([e6beb88](https://github.com/folke/which-key.nvim/commit/e6beb8845e80558194c6027b7a985e1211e76878))
* title trail ([aef2e53](https://github.com/folke/which-key.nvim/commit/aef2e535c5b7c8f100b534a4b781a82e36f20e39))
* **ui:** added scrolling ([5f1ab35](https://github.com/folke/which-key.nvim/commit/5f1ab35d099a252f204e2806747980c192a9c265))
* **ui:** keymap icons ([21d7108](https://github.com/folke/which-key.nvim/commit/21d71081d86872189a3ce90b7c13593f15b78459))
* **ui:** sorters ([ffeea79](https://github.com/folke/which-key.nvim/commit/ffeea7933249d5ce33b2b3838171cc5299ef1893))
* update ui when new mappings become available ([a8f66f5](https://github.com/folke/which-key.nvim/commit/a8f66f5ebd9b94f409a88c4a77244167f6edd05f))
* v3 release ([da258a8](https://github.com/folke/which-key.nvim/commit/da258a89a700916ad0e6af1ad8f9889ff0308253))
* **view:** nerd font icons for cmd keys ([2787dbd](https://github.com/folke/which-key.nvim/commit/2787dbd158184af67ead5af5bcc0cbdb17856c31))
### Bug Fixes
* **api:** show view immediately when opened through the API ([b0e0af0](https://github.com/folke/which-key.nvim/commit/b0e0af0957a648735a43fae52ef34059721f7b42))
* autmatically blacklist all single key hooks except for z and g ([87c5a4b](https://github.com/folke/which-key.nvim/commit/87c5a4b1be1f882c8b27252464d777a76ea15839))
* **icons:** check that mini icons hl groups exist in the current colorscheme. If not use which-key default groups ([2336350](https://github.com/folke/which-key.nvim/commit/233635039bf828e341f5ca9b4b8444ac3c56b974))
* **icons:** proper icons check ([2eaed99](https://github.com/folke/which-key.nvim/commit/2eaed99585f08787d6b5060c89184973eb5aa276))
* **keys:** delete nop keymaps with a description ([ccf0276](https://github.com/folke/which-key.nvim/commit/ccf027625df6c4e22febfdd786c5e1f7521c2ccb))
* **layout:** display vs multibyte ellipsis ([0442a73](https://github.com/folke/which-key.nvim/commit/0442a7340cebe13cc5a5fd70dd6cdc989f9086fe))
* **layout:** empty columns ([600881a](https://github.com/folke/which-key.nvim/commit/600881a9b0cf8119819a97d8900d99fd7a406d36))
* op-mode, count and reg ([e4d54d1](https://github.com/folke/which-key.nvim/commit/e4d54d11cc247edd0ed4bde7a501caa8e119c1ff))
* pcall keymap.del ([e47ee13](https://github.com/folke/which-key.nvim/commit/e47ee139b6a082deab16e436cbd2711923e01625))
* plugin actions & spelling ([e7da411](https://github.com/folke/which-key.nvim/commit/e7da411b45415e8d0d6a5e14b9c1bd5207d09869))
* presets ([bcf52ba](https://github.com/folke/which-key.nvim/commit/bcf52ba08a57a90e85d4397245a0350c34f2b9d1))
* readme ([5fe6c91](https://github.com/folke/which-key.nvim/commit/5fe6c91e6f2d7d6dd1a8473ac0cd9bbe311512d9))
* respect mappings with `&lt;esc&gt;` and close on cursor moved ([22deda5](https://github.com/folke/which-key.nvim/commit/22deda5458b15a10b02b516c68dd409cbaeb53f4))
* set check debounce to 50 ([754bcc7](https://github.com/folke/which-key.nvim/commit/754bcc7be77b9f9ecac02598121eb97a243b7efa))
* **state:** dont return or autocmd will cancel ([9a77986](https://github.com/folke/which-key.nvim/commit/9a779869ef557ff6fa84a8b0b478a0f84781c67e))
* **state:** keyboard interrupts ([1ed9182](https://github.com/folke/which-key.nvim/commit/1ed91823d47f34ce5c52da9ca14e202606caf215))
* **state:** make sure the buffer mode exists when changing modes ([df64366](https://github.com/folke/which-key.nvim/commit/df64366d8633ac13ba2da7134cc6bbe242a97237))
* stuff ([f67eb19](https://github.com/folke/which-key.nvim/commit/f67eb192ca6d579add84086d4d1b4ce6ce8732ac))
* **tree:** check for which_key_ignore in existing keymaps ([f17d78b](https://github.com/folke/which-key.nvim/commit/f17d78bdf8a0afce5bec97c70e68203a6cddf2b7))
* **ui:** box height ([528fc43](https://github.com/folke/which-key.nvim/commit/528fc43b87cfc29bbc1dddc17051a99cdfdf9ad2))
* **ui:** make sure the which-key window never overlaps the user's cursor position ([1bb30a7](https://github.com/folke/which-key.nvim/commit/1bb30a7a6901aa842f31c96af7009ef645b29edd))
* **ui:** scroll and topline=1 on refresh ([28b648d](https://github.com/folke/which-key.nvim/commit/28b648daeabfd2aad8496ffc7a2096bf7d2441b5))
* which_key_ignore ([ab5ffa8](https://github.com/folke/which-key.nvim/commit/ab5ffa83b4f10ea2360a32d855b016f72a2be6b6))
* which-key ignore and cleanup ([aeae826](https://github.com/folke/which-key.nvim/commit/aeae826f948cbaeb3a89d9025c423e8300cb5dd3))
## [2.1.0](https://github.com/folke/which-key.nvim/compare/v2.0.1...v2.1.0) (2024-06-06)

View File

@ -1,32 +1,37 @@
# 💥 Which Key
**WhichKey** is a lua plugin for Neovim 0.5 that displays a popup with possible key bindings of
the command you started typing. Heavily inspired by the original [emacs-which-key](https://github.com/justbur/emacs-which-key) and [vim-which-key](https://github.com/liuchengxu/vim-which-key).
**WhichKey** helps you remember your Neovim keymaps, by showing available keybindings
in a popup as you type.
![image](https://user-images.githubusercontent.com/292349/116439438-669f8d00-a804-11eb-9b5b-c7122bd9acac.png)
![image](https://github.com/user-attachments/assets/89277334-dcdc-4b0f-9fd4-02f27012f589)
![image](https://github.com/user-attachments/assets/f8d71a75-312e-4a42-add8-d153493b2633)
![image](https://github.com/user-attachments/assets/e4400a1d-7e71-4439-b6ff-6cbc40647a6f)
## ✨ Features
- for Neovim 0.7 and higher, it uses the `desc` attributes of your mappings as the default label
- for Neovim 0.7 and higher, new mappings will be created with a `desc` attribute
- opens a popup with suggestions to complete a key binding
- works with any setting for [timeoutlen](https://neovim.io/doc/user/options.html#'timeoutlen'), including instantly (`timeoutlen=0`)
- works correctly with built-in key bindings
- works correctly with buffer-local mappings
- extensible plugin architecture
- built-in plugins:
- **marks:** shows your marks when you hit one of the jump keys.
- **registers:** shows the contents of your registers
- **presets:** built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g`
- **spelling:** spelling suggestions inside the which-key popup
- 🔍 **Key Binding Help**: show available keybindings in a popup as you type.
- ⌨️ **Modes**: works in normal, insert, visual, operator pending, terminal and command mode.
Every mode can be enabled/disabled.
- 🛠️ **Customizable Layouts**: choose from `classic`, `modern`, and `helix` presets or customize the window.
- 🔄 **Flexible Sorting**: sort by `local`, `order`, `group`, `alphanum`, `mod`, `lower`, `icase`, `desc`, or `manual`.
- 🎨 **Formatting**: customizable key labels and descriptions
- 🖼️ **Icons**: integrates with [mini.icons](https://github.com/echasnovski/mini.icons) and [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
- ⏱️ **Delay**: delay is independent of `timeoutlen`
- 🌐 **Plugins**: built-in plugins for marks, registers, presets, and spelling suggestions
- 🚀 **Operators, Motions, Text Objects**: help for operators, motions and text objects
- 🐙 **Hydra Mode**: keep the popup open until you hit `<esc>`
## ⚡️ Requirements
- Neovim >= 0.5.0
- **Neovim** >= 0.9.4
- for proper icons support:
- [mini.icons](https://github.com/echasnovski/mini.icons) _(optional)_
- [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) _(optional)_
- a [Nerd Font](https://www.nerdfonts.com/) **_(optional)_**
## 📦 Installation
Install the plugin with your preferred package manager:
Install the plugin with your package manager:
### [lazy.nvim](https://github.com/folke/lazy.nvim)
@ -34,51 +39,68 @@ Install the plugin with your preferred package manager:
{
"folke/which-key.nvim",
event = "VeryLazy",
init = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
end,
opts = {
-- your configuration comes here
-- or leave it empty to use the default settings
-- refer to the configuration section below
}
}
```
### [packer](https://github.com/wbthomason/packer.nvim)
```lua
-- Lua
use {
"folke/which-key.nvim",
config = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
require("which-key").setup {
-- your configuration comes here
-- or leave it empty to use the default settings
-- refer to the configuration section below
}
end
},
keys = {
{
"<leader>?",
function()
require("which-key").show({ global = false })
end,
desc = "Buffer Local Keymaps (which-key)",
},
},
}
```
## ⚙️ Configuration
> ❗️ IMPORTANT: the [timeout](https://neovim.io/doc/user/options.html#'timeout') when **WhichKey** opens is controlled by the vim setting [timeoutlen](https://neovim.io/doc/user/options.html#'timeoutlen').
> Please refer to the documentation to properly set it up. Setting it to `0`, will effectively
> always show **WhichKey** immediately, but a setting of `500` (500ms) is probably more appropriate.
> [!important]
> Make sure to run `:checkhealth which-key` if something isn't working properly
> ❗️ don't create any keymappings yourself to trigger WhichKey. Unlike with _vim-which-key_, we do this fully automatically.
> Please remove any left-over triggers you might have from using _vim-which-key_.
**WhichKey** is highly configurable. Expand to see the list of all the default options below.
> 🚑 You can run `:checkhealth which-key` to see if there's any conflicting keymaps that will prevent triggering **WhichKey**
<details><summary>Default Options</summary>
WhichKey comes with the following defaults:
<!-- config:start -->
```lua
{
---@class wk.Opts
local defaults = {
---@type false | "classic" | "modern" | "helix"
preset = "classic",
-- Delay before showing the popup. Can be a number or a function that returns a number.
---@type number | fun(ctx: { keys: string, mode: string, plugin?: string }):number
delay = function(ctx)
return ctx.plugin and 0 or 200
end,
---@param mapping wk.Mapping
filter = function(mapping)
-- example to exclude mappings without a description
-- return mapping.desc and mapping.desc ~= ""
return true
end,
--- You can add any mappings here, or use `require('which-key').add()` later
---@type wk.Spec
spec = {},
-- show a warning when issues were detected with your mappings
notify = true,
-- Which-key automatically sets up triggers for your mappings.
-- But you can disable this and setup the triggers manually.
-- Check the docs for more info.
---@type wk.Spec
triggers = {
{ "<auto>", mode = "nxsot" },
},
-- Start hidden and wait for a key to be pressed before showing the popup
-- Only used by enabled xo mapping modes.
---@param ctx { mode: string, operator: string }
defer = function(ctx)
return ctx.mode == "V" or ctx.mode == "<C-V>"
end,
plugins = {
marks = true, -- shows a list of your marks on ' and `
registers = true, -- shows your registers on " in NORMAL or <C-r> in INSERT mode
@ -98,205 +120,270 @@ WhichKey comes with the following defaults:
g = true, -- bindings for prefixed with g
},
},
-- add operators that will trigger motion and text object completion
-- to enable all native operators, set the preset / operators plugin above
operators = { gc = "Comments" },
key_labels = {
-- override the label used to display some keys. It doesn't effect WK in any other way.
-- For example:
-- ["<space>"] = "SPC",
-- ["<cr>"] = "RET",
-- ["<tab>"] = "TAB",
---@type wk.Win.opts
win = {
-- don't allow the popup to overlap with the cursor
no_overlap = true,
-- width = 1,
-- height = { min = 4, max = 25 },
-- col = 0,
-- row = math.huge,
-- border = "none",
padding = { 1, 2 }, -- extra window padding [top/bottom, right/left]
title = true,
title_pos = "center",
zindex = 1000,
-- Additional vim.wo and vim.bo options
bo = {},
wo = {
-- winblend = 10, -- value between 0-100 0 for fully opaque and 100 for fully transparent
},
},
motions = {
count = true,
layout = {
width = { min = 20 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
},
keys = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
---@type (string|wk.Sorter)[]
--- Mappings are sorted using configured sorters and natural sort of the keys
--- Available sorters:
--- * local: buffer-local mappings first
--- * order: order of the items (Used by plugins like marks / registers)
--- * group: groups last
--- * alphanum: alpha-numerical first
--- * mod: special modifier keys last
--- * manual: the order the mappings were added
--- * case: lower-case first
sort = { "local", "order", "group", "alphanum", "mod" },
---@type number|fun(node: wk.Node):boolean?
expand = 0, -- expand groups when <= n mappings
-- expand = function(node)
-- return not node.desc -- expand all nodes without a description
-- end,
-- Functions/Lua Patterns for formatting the labels
---@type table<string, ({[1]:string, [2]:string}|fun(str:string):string)[]>
replace = {
key = {
function(key)
return require("which-key.view").format(key)
end,
-- { "<Space>", "SPC" },
},
desc = {
{ "<Plug>%(?(.*)%)?", "%1" },
{ "^%+", "" },
{ "<[cC]md>", "" },
{ "<[cC][rR]>", "" },
{ "<[sS]ilent>", "" },
{ "^lua%s+", "" },
{ "^call%s+", "" },
{ "^:%s*", "" },
},
},
icons = {
breadcrumb = "»", -- symbol used in the command line area that shows your active key combo
separator = "➜", -- symbol used between a key and it's label
group = "+", -- symbol prepended to a group
},
popup_mappings = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
window = {
border = "none", -- none, single, double, shadow
position = "bottom", -- bottom, top
margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left]. When between 0 and 1, will be treated as a percentage of the screen size.
padding = { 1, 2, 1, 2 }, -- extra window padding [top, right, bottom, left]
winblend = 0, -- value between 0-100 0 for fully opaque and 100 for fully transparent
zindex = 1000, -- positive value to position WhichKey above other floating windows.
},
layout = {
height = { min = 4, max = 25 }, -- min and max height of the columns
width = { min = 20, max = 50 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
align = "left", -- align columns left, center or right
},
ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label
hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "^:", "^ ", "^call ", "^lua " }, -- hide mapping boilerplate
show_help = true, -- show a help message in the command line for using WhichKey
show_keys = true, -- show the currently pressed key and its label as a message in the command line
triggers = "auto", -- automatically setup triggers
-- triggers = {"<leader>"} -- or specify a list manually
-- list of triggers, where WhichKey should not wait for timeoutlen and show immediately
triggers_nowait = {
-- marks
"`",
"'",
"g`",
"g'",
-- registers
'"',
"<c-r>",
-- spelling
"z=",
},
triggers_blacklist = {
-- list of mode / prefixes that should never be hooked by WhichKey
-- this is mostly relevant for keymaps that start with a native binding
i = { "j", "k" },
v = { "j", "k" },
},
-- disable the WhichKey popup for certain buf types and file types.
-- Disabled by default for Telescope
disable = {
buftypes = {},
filetypes = {},
},
}
```
## 🪄 Setup
With the default settings, **WhichKey** will work out of the box for most builtin keybindings,
but the real power comes from documenting and organizing your own keybindings.
To document and/or setup your own mappings, you need to call the `register` method
```lua
local wk = require("which-key")
wk.register(mappings, opts)
```
Default options for `opts`
```lua
{
mode = "n", -- NORMAL mode
-- prefix: use "<leader>f" for example for mapping everything related to finding files
-- the prefix is prepended to every mapping part of `mappings`
prefix = "",
buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings
silent = true, -- use `silent` when creating keymaps
noremap = true, -- use `noremap` when creating keymaps
nowait = false, -- use `nowait` when creating keymaps
expr = false, -- use `expr` when creating keymaps
}
```
> ❕ When you specify a command in your mapping that starts with `<Plug>`, then we automatically set `noremap=false`, since you always want recursive keybindings in this case
### ⌨️ Mappings
> ⌨ for **Neovim 0.7** and higher, which key will use the `desc` attribute of existing mappings as the default label
Group names use the special `name` key in the tables. There's multiple ways to define the mappings. `wk.register` can be called multiple times from anywhere in your config files.
```lua
local wk = require("which-key")
-- As an example, we will create the following mappings:
-- * <leader>ff find files
-- * <leader>fr show recent files
-- * <leader>fb Foobar
-- we'll document:
-- * <leader>fn new file
-- * <leader>fe edit file
-- and hide <leader>1
wk.register({
f = {
name = "file", -- optional group name
f = { "<cmd>Telescope find_files<cr>", "Find File" }, -- create a binding with label
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File", noremap=false, buffer = 123 }, -- additional options for creating the keymap
n = { "New File" }, -- just a label. don't create any mapping
e = "Edit File", -- same as above
["1"] = "which_key_ignore", -- special label to hide it in the popup
b = { function() print("bar") end, "Foobar" } -- you can also pass functions!
},
}, { prefix = "<leader>" })
```
<details>
<summary>Click to see more examples</summary>
```lua
-- all of the mappings below are equivalent
-- method 2
wk.register({
["<leader>"] = {
f = {
name = "+file",
f = { "<cmd>Telescope find_files<cr>", "Find File" },
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
n = { "<cmd>enew<cr>", "New File" },
ellipsis = "…",
-- set to false to disable all mapping icons,
-- both those explicitely added in a mapping
-- and those from rules
mappings = true,
--- See `lua/which-key/icons.lua` for more details
--- Set to `false` to disable keymap icons from rules
---@type wk.IconRule[]|false
rules = {},
-- use the highlights from mini.icons
-- When `false`, it will use `WhichKeyIcon` instead
colors = true,
-- used by key format
keys = {
Up = " ",
Down = " ",
Left = " ",
Right = "",
C = "󰘴 ",
M = "󰘵 ",
D = "󰘳 ",
S = "󰘶 ",
CR = "󰌑 ",
Esc = "󱊷 ",
ScrollWheelDown = "󱕐 ",
ScrollWheelUp = "󱕑 ",
NL = "󰌑 ",
BS = "󰁮",
Space = "󱁐 ",
Tab = "󰌒 ",
F1 = "󱊫",
F2 = "󱊬",
F3 = "󱊭",
F4 = "󱊮",
F5 = "󱊯",
F6 = "󱊰",
F7 = "󱊱",
F8 = "󱊲",
F9 = "󱊳",
F10 = "󱊴",
F11 = "󱊵",
F12 = "󱊶",
},
},
})
-- method 3
wk.register({
["<leader>f"] = {
name = "+file",
f = { "<cmd>Telescope find_files<cr>", "Find File" },
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
n = { "<cmd>enew<cr>", "New File" },
show_help = true, -- show a help message in the command line for using WhichKey
show_keys = true, -- show the currently pressed key and its label as a message in the command line
-- disable WhichKey for certain buf types and file types.
disable = {
ft = {},
bt = {},
},
})
-- method 4
wk.register({
["<leader>f"] = { name = "+file" },
["<leader>ff"] = { "<cmd>Telescope find_files<cr>", "Find File" },
["<leader>fr"] = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
["<leader>fn"] = { "<cmd>enew<cr>", "New File" },
})
debug = false, -- enable wk.log in the current directory
}
```
<!-- config:end -->
</details>
**Tips:** The default label is `keymap.desc` or `keymap.rhs` or `""`,
`:h nvim_set_keymap()` to get more details about `desc` and `rhs`.
## ⌨️ Mappings
### 🚙 Operators, Motions and Text Objects
**WhichKey** automatically gets the descriptions of your keymaps from the `desc`
attribute of the keymap. So for most use-cases, you don't need to do anything else.
**WhichKey** provides help to work with operators, motions and text objects.
However, the **mapping spec** is still useful to configure group descriptions and mappings that don't really exist as a regular keymap.
> `[count]operator[count][text-object]`
> [!WARNING]
> The **mappings spec** changed in `v3`, so make sure to only use the new `add` method if
> you updated your existing mappings.
- operators can be configured with the `operators` option
- set `plugins.presets.operators` to `true` to automatically configure vim built-in operators
- set this to `false`, to only include the list you configured in the `operators` option.
- see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L5) for the full list part of the preset
- text objects are automatically retrieved from **operator pending** key maps (`omap`)
- set `plugins.presets.text_objects` to `true` to configure built-in text objects
- see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L43)
- motions are part of the preset `plugins.presets.motions` setting
- see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L20)
Mappings can be added as part of the config `opts.spec`, or can be added later
using `require("which-key").add()`.
`wk.add()` can be called multiple times from anywhere in your config files.
<details>
<summary>How to disable some operators? (like v)</summary>
A mapping has the following attributes:
- **[1]**: (`string`) lhs **_(required)_**
- **[2]**: (`string|fun()`) rhs **_(optional)_**: when present, it will create the mapping
- **desc**: (`string|fun():string`) description **_(required for non-groups)_**
- **group**: (`string|fun():string`) group name **_(optional)_**
- **mode**: (`string|string[]`) mode **_(optional, defaults to `"n"`)_**
- **cond**: (`boolean|fun():boolean`) condition to enable the mapping **_(optional)_**
- **hidden**: (`boolean`) hide the mapping **_(optional)_**
- **icon**: (`string|wk.Icon|fun():(wk.Icon|string)`) icon spec **_(optional)_**
- **proxy**: (`string`) proxy to another mapping **_(optional)_**
- **expand**: (`fun():wk.Spec`) nested mappings **_(optional)_**
- any other option valid for `vim.keymap.set`. These are only used for creating mappings.
When `desc`, `group`, or `icon` are functions, they are evaluated every time
the popup is shown.
The `expand` property allows to create dynamic mappings. Only functions as `rhs` are supported for dynamic mappings.
Two examples are included in `which-key.extras`:
- `require("which-key.extras").expand.buf`: creates numerical key to buffer mappings
- `require("which-key.extras").expand.win`: creates numerical key to window mappings
```lua
-- make sure to run this code before calling setup()
-- refer to the full lists at https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua
local presets = require("which-key.plugins.presets")
presets.operators["v"] = nil
local wk = require("which-key")
wk.add({
{ "<leader>f", group = "file" }, -- group
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find File", mode = "n" },
{ "<leader>fb", function() print("hello") end, desc = "Foobar" },
{ "<leader>fn", desc = "New File" },
{ "<leader>f1", hidden = true }, -- hide this keymap
{ "<leader>w", proxy = "<c-w>", group = "windows" }, -- proxy to window mappings
{ "<leader>b", group = "buffers", expand = function()
return require("which-key.extras").expand.buf()
end
},
{
-- Nested mappings are allowed and can be added in any order
-- Most attributes can be inherited or overridden on any level
-- There's no limit to the depth of nesting
mode = { "n", "v" }, -- NORMAL and VISUAL mode
{ "<leader>q", "<cmd>q<cr>", desc = "Quit" }, -- no need to specify mode since it's inherited
{ "<leader>w", "<cmd>w<cr>", desc = "Write" },
}
})
```
</details>
## 🎯 Triggers
There's two ways that **which-key** can be triggered:
- by a trigger keymap
- by a `ModeChanged` event for visual and operator pending mode
Both can be configured using `opts.triggers` and `opts.defer`.
By default `opts.triggers` includes `{ "<auto>", mode = "nixsotc" }`, which
will setup keymap triggers for every mode automatically and will trigger during
`ModeChanged`.
> [!NOTE]
> Auto triggers will never be created for existing keymaps.
> That includes every valid single key Neovim builtin mapping.
> If you want to trigger on a builtin keymap, you have to add it manually.
>
> ```lua
> triggers = {
> { "<auto>", mode = "nixsotc" },
> { "a", mode = { "n", "v" } },
> }
> ```
> [!TIP]
> To manually setup triggers, you can set `opts.triggers` to:
>
> ```lua
> triggers = {
> { "<leader>", mode = { "n", "v" } },
> }
> ```
For `ModeChanged` triggers, you can configure the `opts.defer` option.
When it returns `true`, the popup will be shown only after an additional key is pressed.
So `yaf`, would show which-key after pressing `ya`, but not after `y`.
> [!TIP]
> Defer some operators:
>
> ```lua
> ---@param ctx { mode: string, operator: string }
> defer = function(ctx)
> if vim.list_contains({ "d", "y" }, ctx.operator) then
> return true
> end
> return vim.list_contains({ "<C-V>", "V" }, ctx.mode)
> end,
> ```
## 🎨 Icons
> [!note]
> For full support, you need to install either [mini.icons](https://github.com/echasnovski/mini.icons) or [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
There's multiple ways to set icons for your keymaps:
- if you use lazy.nvim, then some icons will be autodetected for keymaps belonging to certain plugins.
- custom rules to decide what icon to use
- in your mapping spec, you can specify what icon to use at any level, so at the node for `<leader>g` for example, to apply to all git keymaps.
The `icon` attribute of a mapping can be a `string`, which will be used as the actual icon,
or an `wk.Icon` object, which can have the following attributes:
- `icon` (`string`): the icon to use **_(optional)_**
- `hl` (`string`): the highlight group to use for the icon **_(optional)_**
- `color` (`string`): the color to use for the icon **_(optional)_**
valid colors are: `azure`, `blue`, `cyan`, `green`, `grey`, `orange`, `purple`, `red`, `yellow`
- `cat` (`string`): the category of the icon **_(optional)_**
valid categories are: `file`, `filetype`, `extension`
- `name` (`string`): the name of the icon in the specified category **_(optional)_**
> [!TIP]
> If you'd rather not use icons, you can disable them
> by setting `opts.icons.mappings` to `false`.
## 🚀 Usage
@ -308,60 +395,69 @@ When the **WhichKey** popup is open, you can use the following key bindings (the
- `<c-d>` scroll down
- `<c-u>` scroll up
Apart from the automatic opening, you can also manually open **WhichKey** for a certain `prefix`:
## 🐙 Hydra Mode
> ❗️ don't create any keymappings yourself to trigger WhichKey. Unlike with _vim-which-key_, we do this fully automatically.
> Please remove any left-over triggers you might have from using _vim-which-key_.
Hydra mode is a special mode that keeps the popup open until you hit `<esc>`.
```vim
:WhichKey " show all mappings
:WhichKey <leader> " show all <leader> mappings
:WhichKey <leader> v " show all <leader> mappings for VISUAL mode
:WhichKey '' v " show ALL mappings for VISUAL mode
```lua
-- Show hydra mode for changing windows
require("which-key").show({
keys = "<c-w>",
loop = true, -- this will keep the popup open until you hit <esc>
})
```
## 🔥 Plugins
Four built-in plugins are included with **WhichKey**.
### Presets
Built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g` and more.
### Marks
Shows a list of your buffer local and global marks when you hit \` or '
![image](https://user-images.githubusercontent.com/292349/116439573-8f278700-a804-11eb-80ca-bb9263e6d937.png)
![image](https://github.com/user-attachments/assets/43fb0874-7f79-4521-aee9-03e2b0841758)
### Registers
Shows a list of your buffer local and global registers when you hit " in _NORMAL_ mode, or `<c-r>` in _INSERT_ mode.
![image](https://user-images.githubusercontent.com/292349/116439609-98b0ef00-a804-11eb-9385-97c7d5ff4113.png)
### Presets
Built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g`
![image](https://user-images.githubusercontent.com/292349/116439871-df9ee480-a804-11eb-9529-800e167db65c.png)
![image](https://github.com/user-attachments/assets/d8077dcb-56fb-47b0-ad9e-1aba5db16950)
### Spelling
When enabled, this plugin hooks into `z=` and replaces the full-screen spelling suggestions window by a list of suggestions within **WhichKey**.
![image](https://user-images.githubusercontent.com/292349/118102022-1c361880-b38d-11eb-8e82-79ad266d9bb8.png)
![image](https://github.com/user-attachments/assets/102c7963-329a-40b9-b0a8-72c8656318b7)
## 🎨 Colors
The table below shows all the highlight groups defined for **WhichKey** with their default link.
| Highlight Group | Defaults to | Description |
| ------------------- | ----------- | ------------------------------------------- |
| _WhichKey_ | Function | the key |
| _WhichKeyGroup_ | Keyword | a group |
| _WhichKeySeparator_ | DiffAdd | the separator between the key and its label |
| _WhichKeyDesc_ | Identifier | the label of the key |
| _WhichKeyFloat_ | NormalFloat | Normal in the popup window |
| _WhichKeyBorder_ | FloatBorder | Normal in the popup window |
| _WhichKeyValue_ | Comment | used by plugins that provide values |
<!-- colors:start -->
<!-- markdownlint-disable-file MD033 -->
<!-- markdownlint-configure-file { "MD013": { "line_length": 120 } } -->
<!-- markdownlint-configure-file { "MD004": { "style": "sublist" } } -->
| Highlight Group | Default Group | Description |
| --- | --- | --- |
| **WhichKey** | ***Function*** | |
| **WhichKeyBorder** | ***FloatBorder*** | Border of the which-key window |
| **WhichKeyDesc** | ***Identifier*** | description |
| **WhichKeyGroup** | ***Keyword*** | group name |
| **WhichKeyIcon** | ***@markup.link*** | icons |
| **WhichKeyIconAzure** | ***Function*** | |
| **WhichKeyIconBlue** | ***DiagnosticInfo*** | |
| **WhichKeyIconCyan** | ***DiagnosticHint*** | |
| **WhichKeyIconGreen** | ***DiagnosticOk*** | |
| **WhichKeyIconGrey** | ***Normal*** | |
| **WhichKeyIconOrange** | ***DiagnosticWarn*** | |
| **WhichKeyIconPurple** | ***Constant*** | |
| **WhichKeyIconRed** | ***DiagnosticError*** | |
| **WhichKeyIconYellow** | ***DiagnosticWarn*** | |
| **WhichKeyNormal** | ***NormalFloat*** | Normal in th which-key window |
| **WhichKeySeparator** | ***Comment*** | the separator between the key and its description |
| **WhichKeyTitle** | ***FloatTitle*** | Title of the which-key window |
| **WhichKeyValue** | ***Comment*** | values by plugins (like marks, registers, etc) |
<!-- colors:end -->

View File

@ -1,21 +1,50 @@
# Todo
* [x] hook into all groups
* [x] show mappings without keymap (zz etc)
* [x] plugin support for marks, registers, text objects
* [x] `<bs>` to go up a level
* [x] config modes
* [x] update buf only
* [x] + thingy for groups
* [x] text objects
* [x] get label from global when not found for buffer
* [x] operators & motions
* [x] show window after timeout?
* [x] make plugins a list of key value with config in value
* [x] cleanup text objects text
* [x] buf local mappings seems to interfere with global mappings (push K in help)
* [x] fix help in visual mode
* [x] Plug>whichkey nop
* [x] preset plugin
* [x] command should auto stuff
* [x] timeoutlen is always respected and should still work when zero
- [x] create keymaps in register()
- [x] distinction between actual keymap and just a desc
- [x] virtual mappings wihtout real children?
- [x] registers / counts?
- [x] presets / plugins
- [x] config?
- [x] auto blacklist single keys for default keymaps (for mappings like `aa`, a hook would be created for `a` and `a` would be ignored)
- [x] custom sorting
- [x] gr doesn't work because of grn and friends
- [x] same for gc opmode and gc normal mode
- [x] yank and shift-paste hangs
- [x] macro recording / macro execution
- [x] spell
- [x] spell with count, like `1z=`
- [x] which-key-ignore
- [x] empty groups?
- [x] timeoutlen and nowait
- [x] ui presets
- [x] ui opts & columns etc
- [x] scroll window
- [x] help text?
- [x] plugin layout?
- [x] ✅ 🔥🔥🚀
- [x] minimize attach
- [x] sometimes incorrectly attached `gcc` not working
- [x] error handling for view
- [x] spelling layout
- [x] better mappings parser? Especially needs typings
- [x] Mappings with mode `v`
- [x] allow register from opts
- [x] auto gen docs
- [x] health
- [x] `<leader>gh_`
- [x] devicons support
- [x] nowait, timeoutlen and delay
- [x] new mappings DSL
- [x] News
- [x] normal mode mappings in terminal mode?
- [x] dynamic size
- [x] situation with visual mode
- [x] fix timeoutlen
- [x] document hydra mode
- [x] floating help text?
- [x] move old option check to checkhealth
- [x] show scrolling hint when can't fit all mappings
- [ ] more tests
- [ ] hint characters in desc
- [ ] intgerate with lazy.nvim. Get description there if set

View File

@ -4,9 +4,12 @@ which-key.nvim-which-key which-key.nvim.txt /*which-key.nvim-which-key*
which-key.nvim-which-key-colors which-key.nvim.txt /*which-key.nvim-which-key-colors*
which-key.nvim-which-key-configuration which-key.nvim.txt /*which-key.nvim-which-key-configuration*
which-key.nvim-which-key-features which-key.nvim.txt /*which-key.nvim-which-key-features*
which-key.nvim-which-key-hydra-mode which-key.nvim.txt /*which-key.nvim-which-key-hydra-mode*
which-key.nvim-which-key-icons which-key.nvim.txt /*which-key.nvim-which-key-icons*
which-key.nvim-which-key-installation which-key.nvim.txt /*which-key.nvim-which-key-installation*
which-key.nvim-which-key-mappings which-key.nvim.txt /*which-key.nvim-which-key-mappings*
which-key.nvim-which-key-plugins which-key.nvim.txt /*which-key.nvim-which-key-plugins*
which-key.nvim-which-key-requirements which-key.nvim.txt /*which-key.nvim-which-key-requirements*
which-key.nvim-which-key-setup which-key.nvim.txt /*which-key.nvim-which-key-setup*
which-key.nvim-which-key-triggers which-key.nvim.txt /*which-key.nvim-which-key-triggers*
which-key.nvim-which-key-usage which-key.nvim.txt /*which-key.nvim-which-key-usage*
which-key.nvim.txt which-key.nvim.txt /*which-key.nvim.txt*

View File

@ -1,4 +1,4 @@
*which-key.nvim.txt* For Neovim >= 0.8.0 Last change: 2024 June 07
*which-key.nvim.txt* For Neovim Last change: 2024 July 24
==============================================================================
Table of Contents *which-key.nvim-table-of-contents*
@ -8,8 +8,11 @@ Table of Contents *which-key.nvim-table-of-contents*
- Requirements |which-key.nvim-which-key-requirements|
- Installation |which-key.nvim-which-key-installation|
- Configuration |which-key.nvim-which-key-configuration|
- Setup |which-key.nvim-which-key-setup|
- Mappings |which-key.nvim-which-key-mappings|
- Triggers |which-key.nvim-which-key-triggers|
- Icons |which-key.nvim-which-key-icons|
- Usage |which-key.nvim-which-key-usage|
- Hydra Mode |which-key.nvim-which-key-hydra-mode|
- Plugins |which-key.nvim-which-key-plugins|
- Colors |which-key.nvim-which-key-colors|
2. Links |which-key.nvim-links|
@ -17,36 +20,39 @@ Table of Contents *which-key.nvim-table-of-contents*
==============================================================================
1. Which Key *which-key.nvim-which-key*
**WhichKey** is a lua plugin for Neovim 0.5 that displays a popup with possible
key bindings of the command you started typing. Heavily inspired by the
original emacs-which-key <https://github.com/justbur/emacs-which-key> and
vim-which-key <https://github.com/liuchengxu/vim-which-key>.
**WhichKey** helps you remember your Neovim keymaps, by showing available
keybindings in a popup as you type.
FEATURES *which-key.nvim-which-key-features*
- for Neovim 0.7 and higher, it uses the `desc` attributes of your mappings as the default label
- for Neovim 0.7 and higher, new mappings will be created with a `desc` attribute
- opens a popup with suggestions to complete a key binding
- works with any setting for |timeoutlen|, including instantly (`timeoutlen=0`)
- works correctly with built-in key bindings
- works correctly with buffer-local mappings
- extensible plugin architecture
- built-in plugins:
- **marks:** shows your marks when you hit one of the jump keys.
- **registers:** shows the contents of your registers
- **presets:** built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g`
- **spelling:** spelling suggestions inside the which-key popup
- **Key Binding Help**show available keybindings in a popup as you type.
- **Modes**works in normal, insert, visual, operator pending, terminal and command mode.
Every mode can be enabled/disabled.
- **Customizable Layouts**choose from `classic`, `modern`, and `helix` presets or customize the window.
- **Flexible Sorting**sort by `local`, `order`, `group`, `alphanum`, `mod`, `lower`, `icase`, `desc`, or `manual`.
- **Formatting**customizable key labels and descriptions
- **Icons**integrates with mini.icons <https://github.com/echasnovski/mini.icons> and nvim-web-devicons <https://github.com/nvim-tree/nvim-web-devicons>
- **Delay**delay is independent of `timeoutlen`
- **Plugins**built-in plugins for marks, registers, presets, and spelling suggestions
- **Operators, Motions, Text Objects**help for operators, motions and text objects
- **Hydra Mode**keep the popup open until you hit `<esc>`
REQUIREMENTS *which-key.nvim-which-key-requirements*
- Neovim >= 0.5.0
- **Neovim** >= 0.9.4
- for proper icons support:
- mini.icons <https://github.com/echasnovski/mini.icons> _(optional)_
- nvim-web-devicons <https://github.com/nvim-tree/nvim-web-devicons> _(optional)_
- a Nerd Font <https://www.nerdfonts.com/> **(optional)**
INSTALLATION *which-key.nvim-which-key-installation*
Install the plugin with your preferred package manager:
Install the plugin with your package manager:
LAZY.NVIM ~
@ -55,34 +61,20 @@ LAZY.NVIM ~
{
"folke/which-key.nvim",
event = "VeryLazy",
init = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
end,
opts = {
-- your configuration comes here
-- or leave it empty to use the default settings
-- refer to the configuration section below
}
}
<
PACKER ~
>lua
-- Lua
use {
"folke/which-key.nvim",
config = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
require("which-key").setup {
-- your configuration comes here
-- or leave it empty to use the default settings
-- refer to the configuration section below
}
end
},
keys = {
{
"<leader>?",
function()
require("which-key").show({ global = false })
end,
desc = "Buffer Local Keymaps (which-key)",
},
},
}
<
@ -90,21 +82,47 @@ PACKER ~
CONFIGURATION *which-key.nvim-which-key-configuration*
IMPORTANT: the |timeout| when **WhichKey** opens is controlled by the vim
setting |timeoutlen|. Please refer to the documentation to properly set it up.
Setting it to `0`, will effectively always show **WhichKey** immediately, but a
setting of `500` (500ms) is probably more appropriate.
[!important] Make sure to run `:checkhealth which-key` if something isnt
working properly
**WhichKey** is highly configurable. Expand to see the list of all the default
options below.
dont create any keymappings yourself to trigger WhichKey. Unlike with
_vim-which-key_, we do this fully automatically. Please remove any left-over
triggers you might have from using _vim-which-key_.
You can run `:checkhealth which-key` to see if theres any conflicting
keymaps that will prevent triggering **WhichKey**
WhichKey comes with the following defaults:
Default Options ~
>lua
{
---@class wk.Opts
local defaults = {
---@type false | "classic" | "modern" | "helix"
preset = "classic",
-- Delay before showing the popup. Can be a number or a function that returns a number.
---@type number | fun(ctx: { keys: string, mode: string, plugin?: string }):number
delay = function(ctx)
return ctx.plugin and 0 or 200
end,
---@param mapping wk.Mapping
filter = function(mapping)
-- example to exclude mappings without a description
-- return mapping.desc and mapping.desc ~= ""
return true
end,
--- You can add any mappings here, or use `require('which-key').add()` later
---@type wk.Spec
spec = {},
-- show a warning when issues were detected with your mappings
notify = true,
-- Which-key automatically sets up triggers for your mappings.
-- But you can disable this and setup the triggers manually.
-- Check the docs for more info.
---@type wk.Spec
triggers = {
{ "<auto>", mode = "nxsot" },
},
-- Start hidden and wait for a key to be pressed before showing the popup
-- Only used by enabled xo mapping modes.
---@param ctx { mode: string, operator: string }
defer = function(ctx)
return ctx.mode == "V" or ctx.mode == "<C-V>"
end,
plugins = {
marks = true, -- shows a list of your marks on ' and `
registers = true, -- shows your registers on " in NORMAL or <C-r> in INSERT mode
@ -124,211 +142,267 @@ WhichKey comes with the following defaults:
g = true, -- bindings for prefixed with g
},
},
-- add operators that will trigger motion and text object completion
-- to enable all native operators, set the preset / operators plugin above
operators = { gc = "Comments" },
key_labels = {
-- override the label used to display some keys. It doesn't effect WK in any other way.
-- For example:
-- ["<space>"] = "SPC",
-- ["<cr>"] = "RET",
-- ["<tab>"] = "TAB",
---@type wk.Win.opts
win = {
-- don't allow the popup to overlap with the cursor
no_overlap = true,
-- width = 1,
-- height = { min = 4, max = 25 },
-- col = 0,
-- row = math.huge,
-- border = "none",
padding = { 1, 2 }, -- extra window padding [top/bottom, right/left]
title = true,
title_pos = "center",
zindex = 1000,
-- Additional vim.wo and vim.bo options
bo = {},
wo = {
-- winblend = 10, -- value between 0-100 0 for fully opaque and 100 for fully transparent
},
},
motions = {
count = true,
layout = {
width = { min = 20 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
},
keys = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
---@type (string|wk.Sorter)[]
--- Mappings are sorted using configured sorters and natural sort of the keys
--- Available sorters:
--- * local: buffer-local mappings first
--- * order: order of the items (Used by plugins like marks / registers)
--- * group: groups last
--- * alphanum: alpha-numerical first
--- * mod: special modifier keys last
--- * manual: the order the mappings were added
--- * case: lower-case first
sort = { "local", "order", "group", "alphanum", "mod" },
---@type number|fun(node: wk.Node):boolean?
expand = 0, -- expand groups when <= n mappings
-- expand = function(node)
-- return not node.desc -- expand all nodes without a description
-- end,
-- Functions/Lua Patterns for formatting the labels
---@type table<string, ({[1]:string, [2]:string}|fun(str:string):string)[]>
replace = {
key = {
function(key)
return require("which-key.view").format(key)
end,
-- { "<Space>", "SPC" },
},
desc = {
{ "<Plug>%(?(.*)%)?", "%1" },
{ "^%+", "" },
{ "<[cC]md>", "" },
{ "<[cC][rR]>", "" },
{ "<[sS]ilent>", "" },
{ "^lua%s+", "" },
{ "^call%s+", "" },
{ "^:%s*", "" },
},
},
icons = {
breadcrumb = "»", -- symbol used in the command line area that shows your active key combo
separator = "➜", -- symbol used between a key and it's label
group = "+", -- symbol prepended to a group
},
popup_mappings = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
window = {
border = "none", -- none, single, double, shadow
position = "bottom", -- bottom, top
margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left]. When between 0 and 1, will be treated as a percentage of the screen size.
padding = { 1, 2, 1, 2 }, -- extra window padding [top, right, bottom, left]
winblend = 0, -- value between 0-100 0 for fully opaque and 100 for fully transparent
zindex = 1000, -- positive value to position WhichKey above other floating windows.
},
layout = {
height = { min = 4, max = 25 }, -- min and max height of the columns
width = { min = 20, max = 50 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
align = "left", -- align columns left, center or right
},
ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label
hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "^:", "^ ", "^call ", "^lua " }, -- hide mapping boilerplate
show_help = true, -- show a help message in the command line for using WhichKey
show_keys = true, -- show the currently pressed key and its label as a message in the command line
triggers = "auto", -- automatically setup triggers
-- triggers = {"<leader>"} -- or specify a list manually
-- list of triggers, where WhichKey should not wait for timeoutlen and show immediately
triggers_nowait = {
-- marks
"`",
"'",
"g`",
"g'",
-- registers
'"',
"<c-r>",
-- spelling
"z=",
},
triggers_blacklist = {
-- list of mode / prefixes that should never be hooked by WhichKey
-- this is mostly relevant for keymaps that start with a native binding
i = { "j", "k" },
v = { "j", "k" },
},
-- disable the WhichKey popup for certain buf types and file types.
-- Disabled by default for Telescope
disable = {
buftypes = {},
filetypes = {},
},
}
<
SETUP *which-key.nvim-which-key-setup*
With the default settings, **WhichKey** will work out of the box for most
builtin keybindings, but the real power comes from documenting and organizing
your own keybindings.
To document and/or setup your own mappings, you need to call the `register`
method
>lua
local wk = require("which-key")
wk.register(mappings, opts)
<
Default options for `opts`
>lua
{
mode = "n", -- NORMAL mode
-- prefix: use "<leader>f" for example for mapping everything related to finding files
-- the prefix is prepended to every mapping part of `mappings`
prefix = "",
buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings
silent = true, -- use `silent` when creating keymaps
noremap = true, -- use `noremap` when creating keymaps
nowait = false, -- use `nowait` when creating keymaps
expr = false, -- use `expr` when creating keymaps
}
<
When you specify a command in your mapping that starts with `<Plug>`, then we
automatically set `noremap=false`, since you always want recursive keybindings
in this case
MAPPINGS ~
for **Neovim 0.7** and higher, which key will use the `desc` attribute of
existing mappings as the default label
Group names use the special `name` key in the tables. Theres multiple ways
to define the mappings. `wk.register` can be called multiple times from
anywhere in your config files.
>lua
local wk = require("which-key")
-- As an example, we will create the following mappings:
-- * <leader>ff find files
-- * <leader>fr show recent files
-- * <leader>fb Foobar
-- we'll document:
-- * <leader>fn new file
-- * <leader>fe edit file
-- and hide <leader>1
wk.register({
f = {
name = "file", -- optional group name
f = { "<cmd>Telescope find_files<cr>", "Find File" }, -- create a binding with label
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File", noremap=false, buffer = 123 }, -- additional options for creating the keymap
n = { "New File" }, -- just a label. don't create any mapping
e = "Edit File", -- same as above
["1"] = "which_key_ignore", -- special label to hide it in the popup
b = { function() print("bar") end, "Foobar" } -- you can also pass functions!
},
}, { prefix = "<leader>" })
<
Click to see more examples ~
>lua
-- all of the mappings below are equivalent
-- method 2
wk.register({
["<leader>"] = {
f = {
name = "+file",
f = { "<cmd>Telescope find_files<cr>", "Find File" },
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
n = { "<cmd>enew<cr>", "New File" },
ellipsis = "…",
-- set to false to disable all mapping icons,
-- both those explicitely added in a mapping
-- and those from rules
mappings = true,
--- See `lua/which-key/icons.lua` for more details
--- Set to `false` to disable keymap icons from rules
---@type wk.IconRule[]|false
rules = {},
-- use the highlights from mini.icons
-- When `false`, it will use `WhichKeyIcon` instead
colors = true,
-- used by key format
keys = {
Up = " ",
Down = " ",
Left = " ",
Right = " ",
C = "󰘴 ",
M = "󰘵 ",
D = "󰘳 ",
S = "󰘶 ",
CR = "󰌑 ",
Esc = "󱊷 ",
ScrollWheelDown = "󱕐 ",
ScrollWheelUp = "󱕑 ",
NL = "󰌑 ",
BS = "󰁮",
Space = "󱁐 ",
Tab = "󰌒 ",
F1 = "󱊫",
F2 = "󱊬",
F3 = "󱊭",
F4 = "󱊮",
F5 = "󱊯",
F6 = "󱊰",
F7 = "󱊱",
F8 = "󱊲",
F9 = "󱊳",
F10 = "󱊴",
F11 = "󱊵",
F12 = "󱊶",
},
},
})
-- method 3
wk.register({
["<leader>f"] = {
name = "+file",
f = { "<cmd>Telescope find_files<cr>", "Find File" },
r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
n = { "<cmd>enew<cr>", "New File" },
show_help = true, -- show a help message in the command line for using WhichKey
show_keys = true, -- show the currently pressed key and its label as a message in the command line
-- disable WhichKey for certain buf types and file types.
disable = {
ft = {},
bt = {},
},
})
-- method 4
wk.register({
["<leader>f"] = { name = "+file" },
["<leader>ff"] = { "<cmd>Telescope find_files<cr>", "Find File" },
["<leader>fr"] = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" },
["<leader>fn"] = { "<cmd>enew<cr>", "New File" },
})
debug = false, -- enable wk.log in the current directory
}
<
**Tips:** The default label is `keymap.desc` or `keymap.rhs` or `""`,
|nvim_set_keymap()| to get more details about `desc` and `rhs`.
MAPPINGS *which-key.nvim-which-key-mappings*
**WhichKey** automatically gets the descriptions of your keymaps from the
`desc` attribute of the keymap. So for most use-cases, you dont need to do
anything else.
However, the **mapping spec** is still useful to configure group descriptions
and mappings that dont really exist as a regular keymap.
OPERATORS, MOTIONS AND TEXT OBJECTS ~
[!WARNING] The **mappings spec** changed in `v3`, so make sure to only use the
new `add` method if you updated your existing mappings.
Mappings can be added as part of the config `opts.spec`, or can be added later
using `require("which-key").add()`. `wk.add()` can be called multiple times
from anywhere in your config files.
**WhichKey** provides help to work with operators, motions and text objects.
A mapping has the following attributes:
- **[1]**(`string`) lhs **(required)**
- **[2]**(`string|fun()`) rhs **(optional)**when present, it will create the mapping
- **desc**(`string|fun():string`) description **(required for non-groups)**
- **group**(`string|fun():string`) group name **(optional)**
- **mode**(`string|string[]`) mode **(optional, defaults to "n")**
- **cond**(`boolean|fun():boolean`) condition to enable the mapping **(optional)**
- **hidden**(`boolean`) hide the mapping **(optional)**
- **icon**(`string|wk.Icon|fun():(wk.Icon|string)`) icon spec **(optional)**
- **proxy**(`string`) proxy to another mapping **(optional)**
- **expand**(`fun():wk.Spec`) nested mappings **(optional)**
- any other option valid for `vim.keymap.set`. These are only used for creating mappings.
`[count]operator[count][text-object]`
- operators can be configured with the `operators` option
- set `plugins.presets.operators` to `true` to automatically configure vim built-in operators
- set this to `false`, to only include the list you configured in the `operators` option.
- see here <https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L5> for the full list part of the preset
- text objects are automatically retrieved from **operator pending** key maps (`omap`)
- set `plugins.presets.text_objects` to `true` to configure built-in text objects
- see here <https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L43>
- motions are part of the preset `plugins.presets.motions` setting
- see here <https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L20>
When `desc`, `group`, or `icon` are functions, they are evaluated every time
the popup is shown.
How to disable some operators? (like v) ~
The `expand` property allows to create dynamic mappings. Only functions as
`rhs` are supported for dynamic mappings. Two examples are included in
`which-key.extras`
- `require("which-key.extras").expand.buf`creates numerical key to buffer mappings
- `require("which-key.extras").expand.win`creates numerical key to window mappings
>lua
-- make sure to run this code before calling setup()
-- refer to the full lists at https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua
local presets = require("which-key.plugins.presets")
presets.operators["v"] = nil
local wk = require("which-key")
wk.add({
{ "<leader>f", group = "file" }, -- group
{ "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find File", mode = "n" },
{ "<leader>fb", function() print("hello") end, desc = "Foobar" },
{ "<leader>fn", desc = "New File" },
{ "<leader>f1", hidden = true }, -- hide this keymap
{ "<leader>w", proxy = "<c-w>", group = "windows" }, -- proxy to window mappings
{ "<leader>b", group = "buffers", expand = function()
return require("which-key.extras").expand.buf()
end
},
{
-- Nested mappings are allowed and can be added in any order
-- Most attributes can be inherited or overridden on any level
-- There's no limit to the depth of nesting
mode = { "n", "v" }, -- NORMAL and VISUAL mode
{ "<leader>q", "<cmd>q<cr>", desc = "Quit" }, -- no need to specify mode since it's inherited
{ "<leader>w", "<cmd>w<cr>", desc = "Write" },
}
})
<
TRIGGERS *which-key.nvim-which-key-triggers*
Theres two ways that **which-key** can be triggered:
- by a trigger keymap
- by a `ModeChanged` event for visual and operator pending mode
Both can be configured using `opts.triggers` and `opts.defer`.
By default `opts.triggers` includes `{ "<auto>", mode = "nixsotc" }`, which
will setup keymap triggers for every mode automatically and will trigger during
`ModeChanged`.
[!NOTE] Auto triggers will never be created for existing keymaps. That includes
every valid single key Neovim builtin mapping. If you want to trigger on a
builtin keymap, you have to add it manually.
>lua
triggers = {
{ "<auto>", mode = "nixsotc" },
{ "a", mode = { "n", "v" } },
}
<
[!TIP] To manually setup triggers, you can set `opts.triggers` to:
>lua
triggers = {
{ "<leader>", mode = { "n", "v" } },
}
<
For `ModeChanged` triggers, you can configure the `opts.defer` option. When it
returns `true`, the popup will be shown only after an additional key is
pressed. So `yaf`, would show which-key after pressing `ya`, but not after `y`.
[!TIP] Defer some operators:
>lua
---@param ctx { mode: string, operator: string }
defer = function(ctx)
if vim.list_contains({ "d", "y" }, ctx.operator) then
return true
end
return vim.list_contains({ "<C-V>", "V" }, ctx.mode)
end,
<
ICONS *which-key.nvim-which-key-icons*
[!note] For full support, you need to install either mini.icons
<https://github.com/echasnovski/mini.icons> or nvim-web-devicons
<https://github.com/nvim-tree/nvim-web-devicons>
Theres multiple ways to set icons for your keymaps:
- if you use lazy.nvim, then some icons will be autodetected for keymaps belonging to certain plugins.
- custom rules to decide what icon to use
- in your mapping spec, you can specify what icon to use at any level, so at the node for `<leader>g` for example, to apply to all git keymaps.
The `icon` attribute of a mapping can be a `string`, which will be used as the
actual icon, or an `wk.Icon` object, which can have the following attributes:
- `icon` (`string`): the icon to use **(optional)**
- `hl` (`string`): the highlight group to use for the icon **(optional)**
- `color` (`string`): the color to use for the icon **(optional)**
valid colors are: `azure`, `blue`, `cyan`, `green`, `grey`, `orange`, `purple`, `red`, `yellow`
- `cat` (`string`): the category of the icon **(optional)**
valid categories are: `file`, `filetype`, `extension`
- `name` (`string`): the name of the icon in the specified category **(optional)**
[!TIP] If youd rather not use icons, you can disable them by setting
`opts.icons.mappings` to `false`.
USAGE *which-key.nvim-which-key-usage*
When the **WhichKey** popup is open, you can use the following key bindings
@ -340,18 +414,17 @@ When the **WhichKey** popup is open, you can use the following key bindings
- `<c-d>` scroll down
- `<c-u>` scroll up
Apart from the automatic opening, you can also manually open **WhichKey** for a
certain `prefix`
HYDRA MODE *which-key.nvim-which-key-hydra-mode*
dont create any keymappings yourself to trigger WhichKey. Unlike with
_vim-which-key_, we do this fully automatically. Please remove any left-over
triggers you might have from using _vim-which-key_.
>vim
:WhichKey " show all mappings
:WhichKey <leader> " show all <leader> mappings
:WhichKey <leader> v " show all <leader> mappings for VISUAL mode
:WhichKey '' v " show ALL mappings for VISUAL mode
Hydra mode is a special mode that keeps the popup open until you hit `<esc>`.
>lua
-- Show hydra mode for changing windows
require("which-key").show({
keys = "<c-w>",
loop = true, -- this will keep the popup open until you hit <esc>
})
<
@ -360,6 +433,12 @@ PLUGINS *which-key.nvim-which-key-plugins*
Four built-in plugins are included with **WhichKey**.
PRESETS ~
Built-in key binding help for `motions`, `text-objects`, `operators`,
`windows`, `nav`, `z` and `g` and more.
MARKS ~
Shows a list of your buffer local and global marks when you hit ` or
@ -371,12 +450,6 @@ Shows a list of your buffer local and global registers when you hit ” in
_NORMAL_ mode, or `<c-r>` in _INSERT_ mode.
PRESETS ~
Built-in key binding help for `motions`, `text-objects`, `operators`,
`windows`, `nav`, `z` and `g`
SPELLING ~
When enabled, this plugin hooks into `z=` and replaces the full-screen spelling
@ -388,32 +461,60 @@ COLORS *which-key.nvim-which-key-colors*
The table below shows all the highlight groups defined for **WhichKey** with
their default link.
---------------------------------------------------------------------------
Highlight Group Defaults to Description
------------------- ------------- -----------------------------------------
WhichKey Function the key
-----------------------------------------------------------------------
Highlight Group Default Group Description
----------------------- ----------------------- -----------------------
WhichKey Function
WhichKeyGroup Keyword a group
WhichKeyBorder FloatBorder Border of the which-key
window
WhichKeySeparator DiffAdd the separator between the key and its
label
WhichKeyDesc Identifier description
WhichKeyDesc Identifier the label of the key
WhichKeyGroup Keyword group name
WhichKeyFloat NormalFloat Normal in the popup window
WhichKeyIcon @markup.link icons
WhichKeyBorder FloatBorder Normal in the popup window
WhichKeyIconAzure Function
WhichKeyValue Comment used by plugins that provide values
---------------------------------------------------------------------------
WhichKeyIconBlue DiagnosticInfo
WhichKeyIconCyan DiagnosticHint
WhichKeyIconGreen DiagnosticOk
WhichKeyIconGrey Normal
WhichKeyIconOrange DiagnosticWarn
WhichKeyIconPurple Constant
WhichKeyIconRed DiagnosticError
WhichKeyIconYellow DiagnosticWarn
WhichKeyNormal NormalFloat Normal in th which-key
window
WhichKeySeparator Comment the separator between
the key and its
description
WhichKeyTitle FloatTitle Title of the which-key
window
WhichKeyValue Comment values by plugins (like
marks, registers, etc)
-----------------------------------------------------------------------
==============================================================================
2. Links *which-key.nvim-links*
1. *image*: https://user-images.githubusercontent.com/292349/116439438-669f8d00-a804-11eb-9b5b-c7122bd9acac.png
2. *image*: https://user-images.githubusercontent.com/292349/116439573-8f278700-a804-11eb-80ca-bb9263e6d937.png
3. *image*: https://user-images.githubusercontent.com/292349/116439609-98b0ef00-a804-11eb-9385-97c7d5ff4113.png
4. *image*: https://user-images.githubusercontent.com/292349/116439871-df9ee480-a804-11eb-9529-800e167db65c.png
5. *image*: https://user-images.githubusercontent.com/292349/118102022-1c361880-b38d-11eb-8e82-79ad266d9bb8.png
1. *image*: https://github.com/user-attachments/assets/89277334-dcdc-4b0f-9fd4-02f27012f589
2. *image*: https://github.com/user-attachments/assets/f8d71a75-312e-4a42-add8-d153493b2633
3. *image*: https://github.com/user-attachments/assets/e4400a1d-7e71-4439-b6ff-6cbc40647a6f
4. *image*: https://github.com/user-attachments/assets/43fb0874-7f79-4521-aee9-03e2b0841758
5. *image*: https://github.com/user-attachments/assets/d8077dcb-56fb-47b0-ad9e-1aba5db16950
6. *image*: https://github.com/user-attachments/assets/102c7963-329a-40b9-b0a8-72c8656318b7
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>

View File

@ -1,19 +1,56 @@
local M = {}
local links = {
[""] = "Function",
Separator = "Comment",
Group = "Keyword",
Desc = "Identifier",
Float = "NormalFloat",
Border = "FloatBorder",
Value = "Comment",
M.colors = {
[""] = "Function", -- the key
Separator = "Comment", -- the separator between the key and its description
Group = "Keyword", -- group name
Desc = "Identifier", -- description
Normal = "NormalFloat", -- Normal in th which-key window
Title = "FloatTitle", -- Title of the which-key window
Border = "FloatBorder", -- Border of the which-key window
Value = "Comment", -- values by plugins (like marks, registers, etc)
Icon = "@markup.link", -- icons
IconAzure = "Function",
IconBlue = "DiagnosticInfo",
IconCyan = "DiagnosticHint",
IconGreen = "DiagnosticOk",
IconGrey = "Normal",
IconOrange = "DiagnosticWarn",
IconPurple = "Constant",
IconRed = "DiagnosticError",
IconYellow = "DiagnosticWarn",
}
function M.setup()
for k, v in pairs(links) do
for k, v in pairs(M.colors) do
vim.api.nvim_set_hl(0, "WhichKey" .. k, { link = v, default = true })
end
M.fix_colors()
vim.api.nvim_create_autocmd("ColorScheme", {
group = vim.api.nvim_create_augroup("wk-colors", { clear = true }),
callback = M.fix_colors,
})
end
function M.fix_colors()
for k in pairs(M.colors) do
if k:find("^Icon") then
local color = k:gsub("^Icon", "")
local wk_hl_group = "WhichKeyIcon" .. color
local mini_hl_group = "MiniIcons" .. color
local wk_hl = vim.api.nvim_get_hl(0, {
name = wk_hl_group,
link = true,
})
local mini_hl = vim.api.nvim_get_hl(0, {
name = mini_hl_group,
link = true,
})
if wk_hl.default and not vim.tbl_isempty(mini_hl) then
vim.api.nvim_set_hl(0, wk_hl_group, { link = mini_hl_group })
end
end
end
end
return M

View File

@ -1,9 +1,42 @@
---@class wk.Config: wk.Opts
---@field triggers {mappings: wk.Mapping[], modes: table<string,boolean>}
local M = {}
M.namespace = vim.api.nvim_create_namespace("WhichKey")
M.version = "3.13.2" -- x-release-please-version
---@class Options
---@class wk.Opts
local defaults = {
---@type false | "classic" | "modern" | "helix"
preset = "classic",
-- Delay before showing the popup. Can be a number or a function that returns a number.
---@type number | fun(ctx: { keys: string, mode: string, plugin?: string }):number
delay = function(ctx)
return ctx.plugin and 0 or 200
end,
---@param mapping wk.Mapping
filter = function(mapping)
-- example to exclude mappings without a description
-- return mapping.desc and mapping.desc ~= ""
return true
end,
--- You can add any mappings here, or use `require('which-key').add()` later
---@type wk.Spec
spec = {},
-- show a warning when issues were detected with your mappings
notify = true,
-- Which-key automatically sets up triggers for your mappings.
-- But you can disable this and setup the triggers manually.
-- Check the docs for more info.
---@type wk.Spec
triggers = {
{ "<auto>", mode = "nxsot" },
},
-- Start hidden and wait for a key to be pressed before showing the popup
-- Only used by enabled xo mapping modes.
---@param ctx { mode: string, operator: string }
defer = function(ctx)
return ctx.mode == "V" or ctx.mode == "<C-V>"
end,
plugins = {
marks = true, -- shows a list of your marks on ' and `
registers = true, -- shows your registers on " in NORMAL or <C-r> in INSERT mode
@ -23,86 +56,288 @@ local defaults = {
g = true, -- bindings for prefixed with g
},
},
-- add operators that will trigger motion and text object completion
-- to enable all native operators, set the preset / operators plugin above
operators = { gc = "Comments" },
key_labels = {
-- override the label used to display some keys. It doesn't effect WK in any other way.
-- For example:
-- ["<space>"] = "SPC",
-- ["<cr>"] = "RET",
-- ["<tab>"] = "TAB",
---@type wk.Win.opts
win = {
-- don't allow the popup to overlap with the cursor
no_overlap = true,
-- width = 1,
-- height = { min = 4, max = 25 },
-- col = 0,
-- row = math.huge,
-- border = "none",
padding = { 1, 2 }, -- extra window padding [top/bottom, right/left]
title = true,
title_pos = "center",
zindex = 1000,
-- Additional vim.wo and vim.bo options
bo = {},
wo = {
-- winblend = 10, -- value between 0-100 0 for fully opaque and 100 for fully transparent
},
},
motions = {
count = true,
layout = {
width = { min = 20 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
},
keys = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
---@type (string|wk.Sorter)[]
--- Mappings are sorted using configured sorters and natural sort of the keys
--- Available sorters:
--- * local: buffer-local mappings first
--- * order: order of the items (Used by plugins like marks / registers)
--- * group: groups last
--- * alphanum: alpha-numerical first
--- * mod: special modifier keys last
--- * manual: the order the mappings were added
--- * case: lower-case first
sort = { "local", "order", "group", "alphanum", "mod" },
---@type number|fun(node: wk.Node):boolean?
expand = 0, -- expand groups when <= n mappings
-- expand = function(node)
-- return not node.desc -- expand all nodes without a description
-- end,
-- Functions/Lua Patterns for formatting the labels
---@type table<string, ({[1]:string, [2]:string}|fun(str:string):string)[]>
replace = {
key = {
function(key)
return require("which-key.view").format(key)
end,
-- { "<Space>", "SPC" },
},
desc = {
{ "<Plug>%(?(.*)%)?", "%1" },
{ "^%+", "" },
{ "<[cC]md>", "" },
{ "<[cC][rR]>", "" },
{ "<[sS]ilent>", "" },
{ "^lua%s+", "" },
{ "^call%s+", "" },
{ "^:%s*", "" },
},
},
icons = {
breadcrumb = "»", -- symbol used in the command line area that shows your active key combo
separator = "", -- symbol used between a key and it's label
group = "+", -- symbol prepended to a group
ellipsis = "",
-- set to false to disable all mapping icons,
-- both those explicitely added in a mapping
-- and those from rules
mappings = true,
--- See `lua/which-key/icons.lua` for more details
--- Set to `false` to disable keymap icons from rules
---@type wk.IconRule[]|false
rules = {},
-- use the highlights from mini.icons
-- When `false`, it will use `WhichKeyIcon` instead
colors = true,
-- used by key format
keys = {
Up = "",
Down = "",
Left = "",
Right = "",
C = "󰘴 ",
M = "󰘵 ",
D = "󰘳 ",
S = "󰘶 ",
CR = "󰌑 ",
Esc = "󱊷 ",
ScrollWheelDown = "󱕐 ",
ScrollWheelUp = "󱕑 ",
NL = "󰌑 ",
BS = "󰁮",
Space = "󱁐 ",
Tab = "󰌒 ",
F1 = "󱊫",
F2 = "󱊬",
F3 = "󱊭",
F4 = "󱊮",
F5 = "󱊯",
F6 = "󱊰",
F7 = "󱊱",
F8 = "󱊲",
F9 = "󱊳",
F10 = "󱊴",
F11 = "󱊵",
F12 = "󱊶",
},
},
popup_mappings = {
scroll_down = "<c-d>", -- binding to scroll down inside the popup
scroll_up = "<c-u>", -- binding to scroll up inside the popup
},
window = {
border = "none", -- none, single, double, shadow
position = "bottom", -- bottom, top
margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left]. When between 0 and 1, will be treated as a percentage of the screen size.
padding = { 1, 2, 1, 2 }, -- extra window padding [top, right, bottom, left]
winblend = 0, -- value between 0-100 0 for fully opaque and 100 for fully transparent
zindex = 1000, -- positive value to position WhichKey above other floating windows.
},
layout = {
height = { min = 4, max = 25 }, -- min and max height of the columns
width = { min = 20, max = 50 }, -- min and max width of the columns
spacing = 3, -- spacing between columns
align = "left", -- align columns left, center or right
},
ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label
hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "^:", "^ ", "^call ", "^lua " }, -- hide mapping boilerplate
show_help = true, -- show a help message in the command line for using WhichKey
show_keys = true, -- show the currently pressed key and its label as a message in the command line
triggers = "auto", -- automatically setup triggers
-- triggers = {"<leader>"} -- or specify a list manually
-- list of triggers, where WhichKey should not wait for timeoutlen and show immediately
triggers_nowait = {
-- marks
"`",
"'",
"g`",
"g'",
-- registers
'"',
"<c-r>",
-- spelling
"z=",
},
triggers_blacklist = {
-- list of mode / prefixes that should never be hooked by WhichKey
-- this is mostly relevant for keymaps that start with a native binding
i = { "j", "k" },
v = { "j", "k" },
},
-- disable the WhichKey popup for certain buf types and file types.
-- Disabled by default for Telescope
-- disable WhichKey for certain buf types and file types.
disable = {
buftypes = {},
filetypes = {},
ft = {},
bt = {},
},
debug = false, -- enable wk.log in the current directory
}
---@type Options
M.options = {}
M.loaded = false
---@param options? Options
function M.setup(options)
if vim.fn.has("nvim-0.9") == 0 then
return vim.notify("WhichKey.nvim requires Neovim 0.9 or higher", vim.log.levels.ERROR)
---@type wk.Keymap[]
M.mappings = {}
---@type wk.Opts
M.options = nil
---@type {opt:string, msg:string}[]
M.issues = {}
function M.validate()
local deprecated = {
["operators"] = "see `opts.defer`",
["key_labels"] = "see `opts.replace`",
"motions",
["popup_mappings"] = "see `opts.keys`",
["window"] = "see `opts.win`",
["ignore_missing"] = "see `opts.filter`",
"hidden",
["triggers_nowait"] = "see `opts.delay`",
["triggers_blacklist"] = "see `opts.triggers`",
["disable.trigger"] = "see `opts.triggers`",
["modes"] = "see `opts.triggers`",
}
for k, v in pairs(deprecated) do
local opt = type(k) == "number" and v or k
local msg = "option is deprecated." .. (type(k) == "number" and "" or " " .. v)
local parts = vim.split(opt, ".", { plain = true })
if vim.tbl_get(M.options, unpack(parts)) ~= nil then
table.insert(M.issues, { opt = opt, msg = msg })
end
end
if type(M.options.triggers) ~= "table" then
table.insert(M.issues, { opt = "triggers", msg = "triggers must be a table" })
end
M.options = vim.tbl_deep_extend("force", {}, defaults, options or {})
end
M.setup()
---@param opts? wk.Opts
function M.setup(opts)
if vim.fn.has("nvim-0.9.4") == 0 then
return vim.notify("which-key.nvim requires Neovim >= 0.9.4", vim.log.levels.ERROR)
end
M.options = vim.tbl_deep_extend("force", {}, defaults, opts or {})
return M
local function load()
if M.loaded then
return
end
local Util = require("which-key.util")
if M.options.preset then
local Presets = require("which-key.presets")
M.options = vim.tbl_deep_extend("force", {}, defaults, Presets[M.options.preset] or {}, opts or {})
end
M.validate()
if #M.issues > 0 then
Util.warn({
"There are issues with your config.",
"Use `:checkhealth which-key` to find out more.",
}, { once = true })
end
for k, v in pairs(M.options.keys) do
M.options.keys[k] = Util.norm(v)
end
if M.options.debug then
Util.debug("\n\nDebug Started for v" .. M.version)
if package.loaded.lazy then
local Git = require("lazy.manage.git")
local plugin = require("lazy.core.config").plugins["which-key.nvim"]
Util.debug(vim.inspect(Git.info(plugin.dir)))
end
end
local wk = require("which-key")
-- replace by the real add function
wk.add = M.add
if type(M.options.triggers) ~= "table" then
---@diagnostic disable-next-line: inject-field
M.options.triggers = defaults.triggers
end
M.triggers = {
mappings = require("which-key.mappings").parse(M.options.triggers),
modes = {},
}
---@param m wk.Mapping
M.triggers.mappings = vim.tbl_filter(function(m)
if m.lhs == "<auto>" then
M.triggers.modes[m.mode] = true
return false
end
return true
end, M.triggers.mappings)
-- load presets first so that they can be overriden by the user
require("which-key.plugins").setup()
-- process mappings queue
for _, todo in ipairs(wk._queue) do
M.add(todo.spec, todo.opts)
end
wk._queue = {}
-- finally, add the mapppings from the config
M.add(M.options.spec)
-- setup colors and start which-key
require("which-key.colors").setup()
require("which-key.state").setup()
M.loaded = true
end
load = vim.schedule_wrap(load)
if vim.v.vim_did_enter == 1 then
load()
else
vim.api.nvim_create_autocmd("VimEnter", { once = true, callback = load })
end
vim.api.nvim_create_user_command("WhichKey", function(cmd)
local mode, keys = cmd.args:match("^([nixsotc]?)%s*(.*)$")
if not mode then
return require("which-key.util").error("Usage: WhichKey [mode] [keys]")
end
if mode == "" then
mode = "n"
end
require("which-key").show({ mode = mode, keys = keys })
end, {
nargs = "*",
})
end
---@param opts? wk.Parse
---@param mappings wk.Spec
function M.add(mappings, opts)
opts = opts or {}
opts.create = opts.create ~= false
local Mappings = require("which-key.mappings")
for _, km in ipairs(Mappings.parse(mappings, opts)) do
table.insert(M.mappings, km)
km.idx = #M.mappings
end
if M.loaded then
require("which-key.buf").clear()
end
end
return setmetatable(M, {
__index = function(_, k)
if rawget(M, "options") == nil then
M.setup()
end
local opts = rawget(M, "options")
return k == "options" and opts or opts[k]
end,
})

View File

@ -1,4 +1,10 @@
local Keys = require("which-key.keys")
local Buf = require("which-key.buf")
local Config = require("which-key.config")
local Icons = require("which-key.icons")
local Mappings = require("which-key.mappings")
local Migrate = require("which-key.migrate")
local Tree = require("which-key.tree")
local Util = require("which-key.util")
local M = {}
@ -8,49 +14,169 @@ 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
function M.check()
start("WhichKey: checking conflicting keymaps")
local conflicts = 0
for _, tree in pairs(Keys.mappings) do
Keys.update_keymaps(tree.mode, tree.buf)
tree.tree:walk(
---@param node Node
function(node)
local count = 0
for _ in pairs(node.children) do
count = count + 1
end
-- TODO: Add more checks
-- * duplicate desc
-- * mapping.desc ~= keymap.desc
-- * check for old-style mappings
local auto_prefix = not node.mapping or (node.mapping.group == true and not node.mapping.cmd)
if node.prefix_i ~= "" and count > 0 and not auto_prefix then
conflicts = conflicts + 1
local msg = ("conflicting keymap exists for mode **%q**, lhs: **%q**"):format(tree.mode, node.mapping.prefix)
warn(msg)
local cmd = node.mapping.cmd or " "
info(("rhs: `%s`"):format(cmd))
end
end
function M.check()
ok(
"Most of these checks are for informational purposes only.\n"
.. "WARNINGS should be treated as a warning, and don't necessarily indicate a problem with your config.\n"
.. "Please |DON't| report these warnings as an issue."
)
start("Checking your config")
if #Config.issues > 0 then
local msg = {
"There are issues with your config:",
}
vim.list_extend(
msg,
vim.tbl_map(function(issue)
return "- `opts." .. issue.opt .. "`: " .. issue.msg
end, Config.issues)
)
msg[#msg + 1] = "Please refer to the docs for more info."
warn(table.concat(msg, "\n"))
end
if conflicts == 0 then
ok("No conflicting keymaps found")
return
local have_icons = false
for _, provider in ipairs(Icons.providers) do
if provider.available == nil then
provider.available = pcall(require, provider.name)
end
if provider.available then
ok("|" .. provider.name .. "| is installed")
have_icons = true
else
warn("|" .. provider.name .. "| is not installed")
end
end
for _, dup in ipairs(Keys.duplicates) do
local msg = ""
if dup.buf == dup.other.buffer then
msg = "duplicate keymap"
else
msg = "buffer-local keymap overriding global"
if not have_icons then
warn("Keymap icon support will be limited.")
end
start("Checking for issues with your mappings")
if #Mappings.notifs == 0 then
ok("No issues reported")
end
for _, notif in ipairs(Mappings.notifs) do
local msg = notif.msg
if notif.spec then
msg = msg .. ": >\n" .. vim.inspect(notif.spec)
if msg:find("old version") then
local fixed = Migrate.migrate(notif.spec)
msg = msg .. "\n\n-- Suggested Spec:\n" .. fixed
end
end
msg = (msg .. " for mode **%q**, buf: %d, lhs: **%q**"):format(dup.mode, dup.buf or 0, dup.prefix)
if dup.buf == dup.other.buffer then
error(msg)
else
warn(msg)
(notif.level >= vim.log.levels.ERROR and error or warn)(msg)
end
start("checking for overlapping keymaps")
local found = false
Buf.cleanup()
---@type table<string, boolean>
local reported = {}
local mapmodes = vim.split("nixsotc", "")
for _, buf in pairs(Buf.bufs) do
for _, mapmode in ipairs(mapmodes) do
local mode = buf:get({ mode = mapmode })
if mode then
mode.tree:walk(function(node)
local km = node.keymap
if not km or Util.is_nop(km.rhs) or node.keys:sub(1, 6) == "<Plug>" then
return
end
if node.keymap and node:count() > 0 then
local id = mode.mode .. ":" .. node.keys
if reported[id] then
return
end
reported[id] = true
local overlaps = {}
local descs = {}
if node.desc and node.desc ~= "" then
descs[#descs + 1] = "- <" .. node.keys .. ">: " .. node.desc
end
local queue = node:children()
while #queue > 0 do
local child = table.remove(queue)
if child.keymap then
table.insert(overlaps, "<" .. child.keys .. ">")
if child.desc and child.desc ~= "" then
descs[#descs + 1] = "- <" .. child.keys .. ">: " .. child.desc
end
end
vim.list_extend(queue, child:children())
end
if #overlaps > 0 then
found = true
warn(
"In mode `"
.. mode.mode
.. "`, <"
.. node.keys
.. "> overlaps with "
.. table.concat(overlaps, ", ")
.. ":\n"
.. table.concat(descs, "\n")
)
end
return false
end
end)
end
end
info(("old rhs: `%s`"):format(dup.other.rhs or ""))
info(("new rhs: `%s`"):format(dup.cmd or ""))
end
if found then
ok(
"Overlapping keymaps are only reported for informational purposes.\n"
.. "This doesn't necessarily mean there is a problem with your config."
)
else
ok("No overlapping keymaps found")
end
start("Checking for duplicate mappings")
if vim.tbl_isempty(Tree.dups) then
ok("No duplicate mappings found")
else
for _, mappings in pairs(Tree.dups) do
---@type wk.Mapping[]
mappings = vim.tbl_keys(mappings)
local first = mappings[1]
warn(
"Duplicates for <"
.. first.lhs
.. "> in mode `"
.. first.mode
.. "`:\n"
.. table.concat(
vim.tbl_map(function(m)
m = vim.deepcopy(m)
local desc = (m.desc and (m.desc .. ": ") or "")
m.desc = nil
m.idx = nil
m.mode = nil
m.lhs = nil
return "* " .. desc .. "`" .. vim.inspect(m):gsub("%s+", " ") .. "`"
end, mappings),
"\n"
)
)
end
ok(
"Duplicate mappings are only reported for informational purposes.\n"
.. "This doesn't necessarily mean there is a problem with your config."
)
end
end

View File

@ -1,96 +1,52 @@
local Keys = require("which-key.keys")
local Util = require("which-key.util")
---@class WhichKey
---@class wk
---@field private _queue {spec: wk.Spec, opts?: wk.Parse}[]
local M = {}
local loaded = false -- once we loaded everything
local scheduled = false
M._queue = {}
M.did_setup = false
local function schedule_load()
if scheduled then
return
end
scheduled = true
if vim.v.vim_did_enter == 0 then
vim.cmd([[au VimEnter * ++once lua require("which-key").load()]])
else
M.load()
end
end
---@param options? Options
function M.setup(options)
require("which-key.config").setup(options)
schedule_load()
end
function M.show(keys, opts)
--- Open which-key
---@param opts? wk.Filter|string
function M.show(opts)
opts = opts or {}
if type(opts) == "string" then
opts = { mode = opts }
opts = type(opts) == "string" and { keys = opts } or opts
if opts.delay == nil then
opts.delay = 0
end
keys = keys or ""
opts.mode = opts.mode or Util.get_mode()
local buf = vim.api.nvim_get_current_buf()
-- make sure the trees exist for update
Keys.get_tree(opts.mode)
Keys.get_tree(opts.mode, buf)
-- update only trees related to buf
Keys.update(buf)
-- trigger which key
require("which-key.view").open(keys, opts)
end
function M.show_command(keys, mode)
keys = keys or ""
keys = (keys == '""' or keys == "''") and "" or keys
mode = (mode == '""' or mode == "''") and "" or mode
mode = mode or "n"
keys = Util.t(keys)
if not Util.check_mode(mode) then
Util.error(
"Invalid mode passed to :WhichKey (Don't create any keymappings to trigger WhichKey. WhichKey does this automatically)"
opts.waited = vim.o.timeoutlen
---@diagnostic disable-next-line: param-type-mismatch
if not require("which-key.state").start(opts) then
require("which-key.util").warn(
"No mappings found for mode `" .. (opts.mode or "n") .. "` and keys `" .. (opts.keys or "") .. "`"
)
else
M.show(keys, { mode = mode })
end
end
local queue = {}
---@param opts? wk.Opts
function M.setup(opts)
M.did_setup = true
require("which-key.config").setup(opts)
end
-- Defer registering keymaps until VimEnter
-- Use `require("which-key").add()` instead.
-- The spec is different though, so check the docs!
---@deprecated
---@param mappings wk.Spec
---@param opts? wk.Mapping
function M.register(mappings, opts)
schedule_load()
if loaded then
Keys.register(mappings, opts)
Keys.update()
else
table.insert(queue, { mappings, opts })
if opts then
for k, v in pairs(opts) do
mappings[k] = v
end
end
M.add(mappings, { version = 1 })
end
-- Load mappings and update only once
function M.load()
if loaded then
return
end
require("which-key.plugins").setup()
require("which-key.colors").setup()
Keys.register({}, { prefix = "<leader>", mode = "n" })
Keys.register({}, { prefix = "<leader>", mode = "v" })
Keys.setup()
for _, reg in pairs(queue) do
local opts = reg[2] or {}
opts.update = false
Keys.register(reg[1], opts)
end
Keys.update()
queue = {}
loaded = true
--- Add mappings to which-key
---@param mappings wk.Spec
---@param opts? wk.Parse
function M.add(mappings, opts)
table.insert(M._queue, { spec = mappings, opts = opts })
end
return M

View File

@ -1,449 +0,0 @@
local Tree = require("which-key.tree")
local Util = require("which-key.util")
local Config = require("which-key.config")
-- secret character that will be used to create <nop> mappings
local secret = "Þ"
---@class Keys
local M = {}
M.operators = {} ---@type table<string, boolean>
M.nowait = {} ---@type table<string, boolean>
M.blacklist = {} ---@type table<string, table<string, boolean>>
function M.setup()
local builtin_ops = require("which-key.plugins.presets").operators
for op, _ in pairs(builtin_ops) do
M.operators[op] = true
end
local mappings = {}
for op, label in pairs(Config.options.operators) do
M.operators[op] = true
if builtin_ops[op] then
mappings[op] = { name = label, i = { name = "inside" }, a = { name = "around" } }
end
end
for _, t in pairs(Config.options.triggers_nowait) do
M.nowait[t] = true
end
M.register(mappings, { mode = "n", preset = true })
M.register({ i = { name = "inside" }, a = { name = "around" } }, { mode = "v", preset = true })
for mode, blacklist in pairs(Config.options.triggers_blacklist) do
for _, prefix_n in ipairs(blacklist) do
M.blacklist[mode] = M.blacklist[mode] or {}
M.blacklist[mode][prefix_n] = true
end
end
end
function M.get_operator(prefix_i)
local ret = { i = nil, n = nil, len = nil }
for op_n, _ in pairs(Config.options.operators) do
local op_i = Util.t(op_n)
if prefix_i:sub(1, #op_i) == op_i and (ret.len == nil or #op_i > ret.len) then
ret = { i = op_i, n = op_n, len = #op_i }
end
end
return ret.i, ret.n
end
function M.process_motions(ret, mode, prefix_i, buf)
local op_i, op_n = "", ""
if mode ~= "v" then
op_i, op_n = M.get_operator(prefix_i)
end
if (mode == "n" or mode == "v") and op_i then
local op_prefix_i = prefix_i:sub(#op_i + 1)
local op_count = op_prefix_i:match("^(%d+)")
if op_count == "0" then
op_count = nil
end
if Config.options.motions.count == false then
op_count = nil
end
if op_count then
op_prefix_i = op_prefix_i:sub(#op_count + 1)
end
local op_results = M.get_mappings("o", op_prefix_i, buf)
if not ret.mapping and op_results.mapping then
ret.mapping = op_results.mapping
ret.mapping.prefix = op_n .. (op_count or "") .. ret.mapping.prefix
ret.mapping.keys = Util.parse_keys(ret.mapping.prefix)
end
for _, mapping in pairs(op_results.mappings) do
mapping.prefix = op_n .. (op_count or "") .. mapping.prefix
mapping.keys = Util.parse_keys(mapping.prefix)
table.insert(ret.mappings, mapping)
end
end
end
---@return MappingGroup
function M.get_mappings(mode, prefix_i, buf)
---@class MappingGroup
---@field mode string
---@field prefix_i string
---@field buf number
---@field mapping? Mapping
---@field mappings VisualMapping[]
local ret
ret = { mapping = nil, mappings = {}, mode = mode, buf = buf, prefix_i = prefix_i }
local prefix_len = #Util.parse_internal(prefix_i)
---@param node? Node
local function add(node)
if node then
if node.mapping then
ret.mapping = vim.tbl_deep_extend("force", {}, ret.mapping or {}, node.mapping)
end
for k, child in pairs(node.children) do
if
child.mapping
and child.mapping.desc ~= "which_key_ignore"
and not (child.mapping.group and vim.tbl_isempty(child.children))
then
local child_mapping = vim.deepcopy(child.mapping)
ret.mappings[k] = vim.tbl_deep_extend("force", ret.mappings[k] or {}, child_mapping)
end
end
end
end
local plugin_context = { buf = buf, mode = mode }
add(M.get_tree(mode).tree:get(prefix_i, nil, plugin_context))
add(M.get_tree(mode, buf).tree:get(prefix_i, nil, plugin_context))
-- Handle motions
M.process_motions(ret, mode, prefix_i, buf)
-- Fix labels
local tmp = {}
for _, value in pairs(ret.mappings) do
value.key = value.keys.notation[prefix_len + 1]
if Config.options.key_labels[value.key] then
value.key = Config.options.key_labels[value.key]
end
local skip = not value.desc and Config.options.ignore_missing == true
if Util.t(value.key) == Util.t("<esc>") then
skip = true
end
if not skip then
if value.group then
value.desc = value.desc or "+prefix"
value.desc = value.desc:gsub("^%+", "")
value.desc = Config.options.icons.group .. value.desc
elseif not value.desc then
value.desc = value.cmd or ""
for _, v in ipairs(Config.options.hidden) do
value.desc = value.desc:gsub(v, "")
end
end
if value.value then
value.value = vim.fn.strtrans(value.value)
end
-- remove duplicated keymap
local exists = false
for k, v in pairs(tmp) do
if type(v) == "table" and v.key == value.key then
tmp[k] = value
exists = true
break
end
end
if not exists then
table.insert(tmp, value)
end
end
end
-- Sort items, but not for plugins
table.sort(tmp, function(a, b)
if a.order and b.order then
return a.order < b.order
end
if a.group == b.group then
local ak = (a.key or ""):lower()
local bk = (b.key or ""):lower()
local aw = ak:match("[a-z]") and 1 or 0
local bw = bk:match("[a-z]") and 1 or 0
if aw == bw then
return ak < bk
end
return aw < bw
else
return (a.group and 1 or 0) < (b.group and 1 or 0)
end
end)
ret.mappings = tmp
return ret
end
---@type table<string, MappingTree>
M.mappings = {}
M.duplicates = {}
function M.map(mode, prefix_n, cmd, buf, opts)
local other = vim.api.nvim_buf_call(buf or 0, function()
local ret = vim.fn.maparg(prefix_n, mode, false, true)
---@diagnostic disable-next-line: undefined-field
return (ret and ret.lhs and ret.rhs and ret.rhs ~= cmd) and ret or nil
end)
if other and other.buffer == buf then
table.insert(M.duplicates, { mode = mode, prefix = prefix_n, cmd = cmd, buf = buf, other = other })
end
if buf ~= nil then
pcall(vim.api.nvim_buf_set_keymap, buf, mode, prefix_n, cmd, opts)
else
pcall(vim.api.nvim_set_keymap, mode, prefix_n, cmd, opts)
end
end
function M.register(mappings, opts)
opts = opts or {}
mappings = require("which-key.mappings").parse(mappings, opts)
-- always create the root node for the mode, even if there's no mappings,
-- to ensure we have at least a trigger hooked for non documented keymaps
local modes = {}
for _, mapping in pairs(mappings) do
if not modes[mapping.mode] then
modes[mapping.mode] = true
M.get_tree(mapping.mode)
end
if mapping.cmd ~= nil then
M.map(mapping.mode, mapping.prefix, mapping.cmd, mapping.buf, mapping.opts)
end
M.get_tree(mapping.mode, mapping.buf).tree:add(mapping)
end
end
M.hooked = {}
function M.hook_id(prefix_n, mode, buf)
return mode .. (buf or "") .. Util.t(prefix_n)
end
function M.is_hooked(prefix_n, mode, buf)
return M.hooked[M.hook_id(prefix_n, mode, buf)]
end
function M.hook_del(prefix_n, mode, buf)
local id = M.hook_id(prefix_n, mode, buf)
M.hooked[id] = nil
if buf then
pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n)
pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n .. secret)
else
pcall(vim.api.nvim_del_keymap, mode, prefix_n)
pcall(vim.api.nvim_del_keymap, mode, prefix_n .. secret)
end
end
function M.hook_add(prefix_n, mode, buf, secret_only)
-- check if this trigger is blacklisted
if M.blacklist[mode] and M.blacklist[mode][prefix_n] then
return
end
-- don't hook numbers. See #118
if tonumber(prefix_n) then
return
end
-- don't hook to j or k in INSERT mode
if mode == "i" and (prefix_n == "j" or prefix_n == "k") then
return
end
-- never hook q
if mode == "n" and prefix_n == "q" then
return
end
-- never hook into select mode
if mode == "s" then
return
end
-- never hook into operator pending mode
-- this is handled differently
if mode == "o" then
return
end
if Util.t(prefix_n) == Util.t("<esc>") then
return
end
-- never hook into operators in visual mode
if (mode == "v" or mode == "x") and (prefix_n == "a" or prefix_n == "i" or M.operators[prefix_n]) then
return
end
-- Check if we need to create the hook
if type(Config.options.triggers) == "string" and Config.options.triggers ~= "auto" then
if Util.t(prefix_n) ~= Util.t(Config.options.triggers) then
return
end
end
if type(Config.options.triggers) == "table" then
local ok = false
for _, trigger in pairs(Config.options.triggers) do
if Util.t(trigger) == Util.t(prefix_n) then
ok = true
break
end
end
if not ok then
return
end
end
local opts = { noremap = true, silent = true }
local id = M.hook_id(prefix_n, mode, buf)
local id_global = M.hook_id(prefix_n, mode)
-- hook up if needed
if not M.hooked[id] and not M.hooked[id_global] then
local cmd = [[<cmd>lua require("which-key").show(%q, {mode = %q, auto = true})<cr>]]
cmd = string.format(cmd, Util.t(prefix_n), mode)
-- map group triggers and nops
-- nops are needed, so that WhichKey always respects timeoutlen
local mapmode = mode == "v" and "x" or mode
if secret_only ~= true then
M.map(mapmode, prefix_n, cmd, buf, opts)
end
if not M.nowait[prefix_n] then
M.map(mapmode, prefix_n .. secret, "<nop>", buf, opts)
end
M.hooked[id] = true
end
end
function M.update(buf)
for k, tree in pairs(M.mappings) do
if tree.buf and not vim.api.nvim_buf_is_valid(tree.buf) then
-- remove group for invalid buffers
M.mappings[k] = nil
elseif not buf or not tree.buf or buf == tree.buf then
-- only update buffer maps, if:
-- 1. we dont pass a buffer
-- 2. this is a global node
-- 3. this is a local buffer node for the passed buffer
M.update_keymaps(tree.mode, tree.buf)
M.add_hooks(tree.mode, tree.buf, tree.tree.root)
end
end
end
---@param node Node
function M.add_hooks(mode, buf, node, secret_only)
if not node.mapping then
node.mapping = { prefix = node.prefix_n, group = true, keys = Util.parse_keys(node.prefix_n) }
end
if node.prefix_n ~= "" and node.mapping.group == true and not (node.mapping.cmd or node.mapping.callback) then
-- first non-cmd level, so create hook and make all descendents secret only
M.hook_add(node.prefix_n, mode, buf, secret_only)
secret_only = true
end
for _, child in pairs(node.children) do
M.add_hooks(mode, buf, child, secret_only)
end
end
function M.dump()
local ok = {}
local todo = {}
for _, tree in pairs(M.mappings) do
M.update_keymaps(tree.mode, tree.buf)
tree.tree:walk(
---@param node Node
function(node)
if node.mapping then
if node.mapping.desc then
ok[node.mapping.prefix] = true
todo[node.mapping.prefix] = nil
elseif not ok[node.mapping.prefix] then
todo[node.mapping.prefix] = { node.mapping.cmd or "" }
end
end
end
)
end
return todo
end
---@param mode string
---@param buf? number
function M.get_tree(mode, buf)
if mode == "s" or mode == "x" then
mode = "v"
end
Util.check_mode(mode, buf)
local idx = mode .. (buf or "")
if not M.mappings[idx] then
M.mappings[idx] = { mode = mode, buf = buf, tree = Tree:new() }
end
return M.mappings[idx]
end
---@param prefix string
---@param cmd string?
function M.is_hook(prefix, cmd)
-- skip mappings with our secret nop command
if prefix:find(secret, 1, true) then
return true
end
-- skip auto which-key mappings
return cmd and cmd:find("which-key", 1, true) and cmd:find("auto", 1, true)
end
---@param mode string
---@param buf? number
function M.update_keymaps(mode, buf)
---@type Keymap[]
local keymaps = buf and vim.api.nvim_buf_get_keymap(buf, mode) or vim.api.nvim_get_keymap(mode)
local tree = M.get_tree(mode, buf).tree
local function is_nop(keymap)
return not keymap.callback and (keymap.rhs == "" or keymap.rhs:lower() == "<nop>")
end
for _, keymap in pairs(keymaps) do
local skip = M.is_hook(keymap.lhs, keymap.rhs)
local is_group = false
if not skip and is_nop(keymap) then
if keymap.desc then
pcall(vim.keymap.del, { mode }, keymap.lhs, { buffer = buf })
is_group = true
else
skip = true
end
end
if not skip then
---@type Mapping
local mapping = {
prefix = keymap.lhs,
cmd = not is_group and keymap.rhs or nil,
desc = keymap.desc,
group = is_group,
callback = keymap.callback,
keys = Util.parse_keys(keymap.lhs),
buf = buf,
mode = mode,
}
-- don't include Plug keymaps
if mapping.keys.notation[1]:lower() ~= "<plug>" then
local node = tree:add(mapping)
if node.mapping and node.mapping.preset and mapping.desc then
node.mapping.desc = mapping.desc
end
end
end
end
end
return M

View File

@ -1,216 +1,147 @@
local Config = require("which-key.config")
local Text = require("which-key.text")
local Keys = require("which-key.keys")
local Util = require("which-key.util")
local M = {}
---@class Layout
---@field mapping Mapping
---@field items VisualMapping[]
---@field options Options
---@field text Text
---@field results MappingGroup
local Layout = {}
Layout.__index = Layout
local dw = vim.fn.strdisplaywidth
---@param mappings MappingGroup
---@param options? Options
function Layout:new(mappings, options)
options = options or Config.options
local this = {
results = mappings,
mapping = mappings.mapping,
items = mappings.mappings,
options = options,
text = Text:new(),
}
setmetatable(this, self)
return this
end
--- When `size` is a number, it is returned as is (fixed dize).
--- Otherwise, it is a percentage of `parent` (relative size).
--- If `size` is negative, it is subtracted from `parent`.
--- If `size` is a table, it is a range of values.
---@alias wk.Dim number|{min:number, max:number}
function Layout:max_width(key)
local max = 0
for _, item in pairs(self.items) do
if item[key] and Text.len(item[key]) > max then
max = Text.len(item[key])
---@param size number
---@param parent number
---@param ... wk.Dim
---@return number
function M.dim(size, parent, ...)
size = math.abs(size) < 1 and parent * size or size
size = size < 0 and parent + size or size
for _, dim in ipairs({ ... } --[[ @as wk.Dim[] ]]) do
if type(dim) == "number" then
size = M.dim(dim, parent)
else
local min = dim.min and M.dim(dim.min, parent) or 0
local max = dim.max and M.dim(dim.max, parent) or parent
size = math.max(min, math.min(max, size))
end
end
return max
return math.floor(math.max(0, math.min(parent, size)) + 0.5)
end
function Layout:trail()
local prefix_i = self.results.prefix_i
local buf_path = Keys.get_tree(self.results.mode, self.results.buf).tree:path(prefix_i)
local path = Keys.get_tree(self.results.mode).tree:path(prefix_i)
local len = #self.results.mapping.keys.notation
local cmd_line = { { " " } }
for i = 1, len, 1 do
local node = buf_path[i]
if not (node and node.mapping and node.mapping.desc) then
node = path[i]
---@class wk.Table: wk.Table.opts
local Table = {}
Table.__index = Table
---@param opts wk.Table.opts
function Table.new(opts)
local self = setmetatable({}, Table)
self.cols = opts.cols
self.rows = opts.rows
return self
end
---@param opts? {spacing?: number}
---@return string[][], number[], number
function Table:cells(opts)
opts = opts or {}
opts.spacing = opts.spacing or 1
local widths = {} ---@type number[] actual column widths
local cells = {} ---@type string[][]
local total = 0
for c, col in ipairs(self.cols) do
widths[c] = 0
local all_ws = true
for r, row in ipairs(self.rows) do
cells[r] = cells[r] or {}
local value = row[col.key] or col.default or ""
value = tostring(value)
value = value:gsub("%s*$", "")
value = value:gsub("\n", Config.icons.keys.NL)
value = vim.fn.strtrans(value)
if value:find("%S") then
all_ws = false
end
if col.padding then
value = (" "):rep(col.padding[1] or 0) .. value .. (" "):rep(col.padding[2] or 0)
end
if c ~= #self.cols then
value = value .. (" "):rep(opts.spacing)
end
cells[r][c] = value
widths[c] = math.max(widths[c], dw(value))
end
local step = self.mapping.keys.notation[i]
if node and node.mapping and node.mapping.desc then
step = self.options.icons.group .. node.mapping.desc
end
if Config.options.key_labels[step] then
step = Config.options.key_labels[step]
end
if Config.options.show_keys then
table.insert(cmd_line, { step, "WhichKeyGroup" })
if i ~= #self.mapping.keys.notation then
table.insert(cmd_line, { " " .. self.options.icons.breadcrumb .. " ", "WhichKeySeparator" })
if all_ws then
widths[c] = 0
for _, cell in pairs(cells) do
cell[c] = ""
end
end
end
local width = 0
if Config.options.show_keys then
for _, line in pairs(cmd_line) do
width = width + Text.len(line[1])
end
end
local help = { --
["<bs>"] = "go up one level",
["<esc>"] = "close",
}
if #self.text.lines > self.options.layout.height.max then
help[Config.options.popup_mappings.scroll_down] = "scroll down"
help[Config.options.popup_mappings.scroll_up] = "scroll up"
end
local help_line = {}
local help_width = 0
for key, label in pairs(help) do
help_width = help_width + Text.len(key) + Text.len(label) + 2
table.insert(help_line, { key .. " ", "WhichKey" })
table.insert(help_line, { label .. " ", "WhichKeySeparator" })
end
if Config.options.show_keys then
table.insert(cmd_line, { string.rep(" ", math.floor(vim.o.columns / 2 - help_width / 2) - width) })
total = total + widths[c]
end
if self.options.show_help then
for _, l in pairs(help_line) do
table.insert(cmd_line, l)
end
end
if vim.o.cmdheight > 0 then
vim.api.nvim_echo(cmd_line, false, {})
vim.cmd([[redraw]])
else
local col = 1
self.text:nl()
local row = #self.text.lines
for _, text in ipairs(cmd_line) do
self.text:set(row, col, text[1], text[2] and text[2]:gsub("WhichKey", "") or nil)
col = col + vim.fn.strwidth(text[1])
end
end
return cells, widths, total
end
function Layout:layout(win)
local pad_top, pad_right, pad_bot, pad_left = unpack(self.options.window.padding)
local window_width = vim.api.nvim_win_get_width(win)
local width = window_width
width = width - pad_right - pad_left
---@param opts {width: number, spacing?: number}
function Table:layout(opts)
local cells, widths = self:cells(opts)
local max_key_width = self:max_width("key")
local max_label_width = self:max_width("desc")
local max_value_width = self:max_width("value")
local free = opts.width
local intro_width = max_key_width + 2 + Text.len(self.options.icons.separator) + self.options.layout.spacing
local max_width = max_label_width + intro_width + max_value_width
if max_width > width then
max_width = width
end
local column_width = max_width
if max_value_width == 0 then
if column_width > self.options.layout.width.max then
column_width = self.options.layout.width.max
for c, col in ipairs(self.cols) do
if not col.width then
free = free - widths[c]
end
if column_width < self.options.layout.width.min then
column_width = self.options.layout.width.min
end
else
max_value_width = math.min(max_value_width, math.floor((column_width - intro_width) / 2))
end
free = math.max(free, 0)
max_label_width = column_width - (intro_width + max_value_width)
local columns = math.floor(width / column_width)
local height = math.ceil(#self.items / columns)
if height < self.options.layout.height.min then
height = self.options.layout.height.min
end
-- if height > self.options.layout.height.max then height = self.options.layout.height.max end
local col = 1
local row = 1
local columns_used = math.min(columns, math.ceil(#self.items / height))
local offset_x = 0
if columns_used < columns then
if self.options.layout.align == "right" then
offset_x = (columns - columns_used) * column_width
elseif self.options.layout.align == "center" then
offset_x = math.floor((columns - columns_used) * column_width / 2)
for c, col in ipairs(self.cols) do
if col.width then
widths[c] = M.dim(widths[c], free, { max = col.width })
free = free - widths[c]
end
end
for _, item in pairs(self.items) do
local start = (col - 1) * column_width + self.options.layout.spacing + offset_x + pad_left
local key = item.key or ""
if key == "<lt>" then
key = "<"
end
if key == Util.t("<esc>") then
key = "<esc>"
end
if Text.len(key) < max_key_width then
key = string.rep(" ", max_key_width - Text.len(key)) .. key
end
---@type {value: string, hl?:string}[][]
local ret = {}
self.text:set(row + pad_top, start, key, "")
start = start + Text.len(key) + 1
self.text:set(row + pad_top, start, self.options.icons.separator, "Separator")
start = start + Text.len(self.options.icons.separator) + 1
if item.value then
local value = item.value
start = start + 1
if Text.len(value) > max_value_width then
value = vim.fn.strcharpart(value, 0, max_value_width - 2) .. ""
end
self.text:set(row + pad_top, start, value, "Value")
if item.highlights then
for _, hl in pairs(item.highlights) do
self.text:highlight(row + pad_top, start + hl[1] - 1, start + hl[2] - 1, hl[3])
for _, row in ipairs(cells) do
---@type {value: string, hl?:string}[]
local line = {}
for c, col in ipairs(self.cols) do
local value = row[c]
local width = dw(value)
if width > widths[c] then
local old = value
value = ""
for i = 0, vim.fn.strchars(old) do
value = value .. vim.fn.strcharpart(old, i, 1)
if dw(value) >= widths[c] - 1 - (opts.spacing or 1) then
break
end
end
value = value .. Config.icons.ellipsis .. string.rep(" ", opts.spacing or 1)
else
local align = col.align or "left"
if align == "left" then
value = value .. (" "):rep(widths[c] - width)
elseif align == "right" then
value = (" "):rep(widths[c] - width) .. value
elseif align == "center" then
local pad = (widths[c] - width) / 2
value = (" "):rep(math.floor(pad)) .. value .. (" "):rep(math.ceil(pad))
end
end
start = start + max_value_width + 2
end
local label = item.desc
if Text.len(label) > max_label_width then
label = vim.fn.strcharpart(label, 0, max_label_width - 2) .. ""
end
self.text:set(row + pad_top, start, label, item.group and "Group" or "Desc")
if row % height == 0 then
col = col + 1
row = 1
else
row = row + 1
line[#line + 1] = { value = value, hl = col.hl }
end
ret[#ret + 1] = line
end
for _ = 1, pad_bot, 1 do
self.text:nl()
end
self:trail()
return self.text
return ret
end
return Layout
M.new = Table.new
return M

View File

@ -1,215 +1,320 @@
local Config = require("which-key.config")
local Util = require("which-key.util")
local M = {}
local function lookup(...)
local ret = {}
for _, t in ipairs({ ... }) do
for _, v in ipairs(t) do
ret[v] = v
end
end
return ret
end
M.VERSION = 2
M.notifs = {} ---@type {msg:string, level:number, spec?:wk.Spec}[]
local mapargs = {
"noremap",
"desc",
"expr",
"silent",
"nowait",
"script",
"unique",
"callback",
"replace_keycodes", -- TODO: add config setting for default value
---@class wk.Field
---@field transform? string|(fun(value: any, parent:table): (value:any, key:string?))
---@field inherit? boolean
---@field deprecated? boolean
---@class wk.Parse
---@field version? number
---@field create? boolean
---@field notify? boolean
M.notify = true
---@type table<string, wk.Field>
M.fields = {
-- map args
rhs = {},
lhs = {},
buffer = { inherit = true },
callback = { transform = "rhs" },
desc = {},
expr = { inherit = true },
mode = { inherit = true },
noremap = {
transform = function(value)
return not value, "remap"
end,
},
nowait = { inherit = true },
remap = { inherit = true },
replace_keycodes = { inherit = true },
script = {},
silent = { inherit = true },
unique = { inherit = true },
-- wk args
plugin = { inherit = true },
group = {},
hidden = { inherit = true },
cond = { inherit = true },
preset = { inherit = true },
icon = { inherit = true },
proxy = {},
expand = {},
-- deprecated
name = { transform = "group", deprecated = true },
prefix = { inherit = true, deprecated = true },
cmd = { transform = "rhs", deprecated = true },
}
local wkargs = {
"prefix",
"mode",
"plugin",
"buffer",
"remap",
"cmd",
"name",
"group",
"preset",
"cond",
}
local transargs = lookup({
"noremap",
"expr",
"silent",
"nowait",
"script",
"unique",
"prefix",
"mode",
"buffer",
"preset",
"replace_keycodes",
})
local args = lookup(mapargs, wkargs)
function M.child_opts(opts)
local ret = {}
for k, v in pairs(opts) do
if transargs[k] then
ret[k] = v
end
end
return ret
---@param msg string
---@param spec? wk.Spec
function M.error(msg, spec)
M.log(msg, vim.log.levels.ERROR, spec)
end
function M._process(value, opts)
local list = {}
local children = {}
for k, v in pairs(value) do
if type(k) == "number" then
if type(v) == "table" then
-- nested child, without key
table.insert(children, v)
else
-- list value
table.insert(list, v)
end
elseif args[k] then
-- option
opts[k] = v
else
-- nested child, with key
children[k] = v
end
end
return list, children
---@param msg string
---@param spec? wk.Spec
function M.warn(msg, spec)
M.log(msg, vim.log.levels.WARN, spec)
end
function M._parse(value, mappings, opts)
if type(value) ~= "table" then
value = { value }
---@param msg string
---@param level number
---@param spec? wk.Spec
function M.log(msg, level, spec)
if not M.notify then
return
end
local list, children = M._process(value, opts)
if opts.plugin then
opts.group = true
end
if opts.name then
-- remove + from group names
opts.name = opts.name and opts.name:gsub("^%+", "")
opts.group = true
end
-- fix remap
if opts.remap then
opts.noremap = not opts.remap
opts.remap = nil
end
-- fix buffer
if opts.buffer == 0 then
opts.buffer = vim.api.nvim_get_current_buf()
end
if opts.cond ~= nil then
if type(opts.cond) == "function" then
if not opts.cond() then
return
end
elseif not opts.cond then
return
end
end
-- process any array child mappings
for k, v in pairs(children) do
local o = M.child_opts(opts)
if type(k) == "string" then
o.prefix = (o.prefix or "") .. k
end
M._try_parse(v, mappings, o)
end
-- { desc }
if #list == 1 then
if type(list[1]) ~= "string" then
error("Invalid mapping for " .. vim.inspect({ value = value, opts = opts }))
end
opts.desc = opts.desc or list[1]
-- { cmd, desc }
elseif #list == 2 then
-- desc
assert(type(list[2]) == "string")
opts.desc = list[2]
-- cmd
if type(list[1]) == "string" then
opts.cmd = list[1]
elseif type(list[1]) == "function" then
opts.cmd = ""
opts.callback = list[1]
else
error("Incorrect mapping " .. vim.inspect(list))
end
elseif #list > 2 then
error("Incorrect mapping " .. vim.inspect(list))
end
if opts.desc or opts.group then
if type(opts.mode) == "table" then
for _, mode in pairs(opts.mode) do
local mode_opts = vim.deepcopy(opts)
mode_opts.mode = mode
table.insert(mappings, mode_opts)
end
else
table.insert(mappings, opts)
end
M.notifs[#M.notifs + 1] = { msg = msg, level = level, spec = spec }
if Config.notify then
Util.warn({
"There were issues reported with your **which-key** mappings.",
"Use `:checkhealth which-key` to find out more.",
}, { once = true })
end
end
---@return Mapping
function M.to_mapping(mapping)
mapping.silent = mapping.silent ~= false
mapping.noremap = mapping.noremap ~= false
if mapping.cmd and mapping.cmd:lower():find("^<plug>") then
mapping.noremap = false
---@param spec wk.Spec
---@param field string|number
---@param types string|string[]
function M.expect(spec, field, types)
types = type(types) == "string" and { types } or types
local ok = false
for _, t in ipairs(types) do
if type(spec[field]) == t then
ok = true
break
end
end
mapping.buf = mapping.buffer
mapping.buffer = nil
mapping.mode = mapping.mode or "n"
mapping.desc = mapping.desc or mapping.name
mapping.name = nil
mapping.keys = Util.parse_keys(mapping.prefix or "")
local opts = {}
for _, o in ipairs(mapargs) do
opts[o] = mapping[o]
mapping[o] = nil
end
-- restore desc
mapping.desc = opts.desc
mapping.opts = opts
return mapping
end
function M._try_parse(value, mappings, opts)
local ok, err = pcall(M._parse, value, mappings, opts)
if not ok then
Util.error(err)
M.error("Expected `" .. field .. "` to be " .. table.concat(types, ", "), spec)
end
return ok
end
---@param spec wk.Spec
---@param ret? wk.Mapping[]
---@param opts? wk.Parse
function M._parse(spec, ret, opts)
opts = opts or {}
opts.version = opts.version or M.VERSION
if spec.version then
opts.version = spec.version
spec.version = nil
end
if ret == nil and opts.version ~= M.VERSION then
M.warn(
"You're using an old version of the which-key spec.\n"
.. "Your mappings will work, but it's recommended to update them to the new version.\n"
.. "Please check the docs and suggested spec below for more info.\nMappings",
vim.deepcopy(spec)
)
end
ret = ret or {}
spec = type(spec) == "string" and { desc = spec } or spec
---@type wk.Mapping
local mapping = {}
---@type wk.Spec[]
local children = {}
local keys = vim.tbl_keys(spec)
table.sort(keys, function(a, b)
local ta, tb = type(a), type(b)
if type(a) == type(b) then
return a < b
end
return ta < tb
end)
-- process fields
for _, k in ipairs(keys) do
local v = spec[k]
local field = M.fields[k] ---@type wk.Field?
if field then
if type(field.transform) == "string" then
k = field.transform --[[@as string]]
elseif type(field.transform) == "function" then
local vv, kk = field.transform(v, spec)
v, k = vv, (kk or k)
end
mapping[k] = v
elseif type(k) == "string" then
if opts.version == 1 then
if M.expect(spec, k, { "string", "table" }) then
if type(v) == "string" then
table.insert(children, { prefix = (spec.prefix or "") .. k, desc = v })
elseif type(v) == "table" then
v.prefix = (spec.prefix or "") .. k
table.insert(children, v)
end
end
else
M.error("Invalid field `" .. k .. "`", spec)
end
elseif type(k) == "number" and type(v) == "table" then
if opts.version == 1 then
v.prefix = spec.prefix or ""
end
table.insert(children, v)
spec[k] = nil
end
end
local count = #spec
-- process mapping
if opts.version == M.VERSION then
if count == 1 then
if M.expect(spec, 1, "string") then
mapping.lhs = spec[1] --[[@as string]]
end
elseif count == 2 then
if M.expect(spec, 1, "string") and M.expect(spec, 2, { "string", "function" }) then
mapping.lhs = spec[1] --[[@as string]]
mapping.rhs = spec[2] --[[@as string]]
end
elseif count > 2 then
M.error("expected 1 or 2 elements, got " .. count, spec)
end
elseif opts.version == 1 then
if mapping.expr and mapping.replace_keycodes == nil then
mapping.replace_keycodes = false
end
if count == 1 then
if M.expect(spec, 1, "string") then
if mapping.desc then
M.warn("overwriting desc", spec)
end
mapping.desc = spec[1] --[[@as string]]
end
elseif count == 2 then
if M.expect(spec, 1, { "string", "function" }) and M.expect(spec, 2, "string") then
if mapping.desc then
M.warn("overwriting desc", spec)
end
mapping.rhs = spec[1] --[[@as string]]
mapping.desc = spec[2] --[[@as string]]
end
elseif count > 2 then
M.error("expected 1 or 2 elements, got " .. count, spec)
end
end
-- add mapping
M.add(mapping, ret, opts)
-- process children
for _, child in ipairs(children) do
for k, v in pairs(mapping) do
if M.fields[k] and M.fields[k].inherit and child[k] == nil then
child[k] = v
end
end
M._parse(child, ret, opts)
end
return ret
end
---@param mapping wk.Spec
---@param opts? wk.Parse
---@param ret wk.Mapping[]
function M.add(mapping, ret, opts)
opts = opts or {}
if mapping.cond == false or ((type(mapping.cond) == "function") and not mapping.cond()) then
return
end
---@cast mapping wk.Mapping|wk.Spec
mapping.cond = nil
if mapping.desc == "which_key_ignore" then
mapping.hidden = true
mapping.desc = nil
end
if type(mapping.group) == "string" or type(mapping.group) == "function" then
mapping.desc = mapping.group --[[@as string]]
mapping.group = true
end
if mapping.plugin then
mapping.group = true
end
if mapping.group and mapping.desc then
mapping.desc = mapping.desc
if type(mapping.desc) == "string" then
mapping.desc = mapping.desc:gsub("^%+", "")
end
end
if mapping.buffer == 0 or mapping.buffer == true then
mapping.buffer = vim.api.nvim_get_current_buf()
end
if mapping.rhs then
mapping.silent = mapping.silent ~= false
end
mapping.lhs = mapping.lhs or mapping.prefix
if not mapping.lhs then
return
end
mapping.prefix = nil
local has_desc = mapping.desc ~= nil
Util.getters(mapping, { "desc", "icon" })
if has_desc or mapping.group or mapping.hidden or mapping.rhs or (opts.version == M.VERSION and mapping.lhs) then
local modes = mapping.mode or { "n" } --[[@as string|string[] ]]
modes = type(modes) == "string" and vim.split(modes, "") or modes --[[@as string[] ]]
for _, mode in ipairs(modes) do
if mode ~= "v" and mode ~= Util.mapmode(mode) then
M.warn("Invalid mode `" .. mode .. "`", mapping)
end
local m = vim.deepcopy(mapping)
m.mode = mode
table.insert(ret, m)
end
end
end
---@return Mapping[]
function M.parse(mappings, opts)
---@param mapping wk.Mapping
function M.create(mapping)
assert(mapping.lhs, "Missing lhs")
assert(mapping.mode, "Missing mode")
assert(mapping.rhs, "Missing rhs")
local valid =
{ "remap", "noremap", "buffer", "silent", "nowait", "expr", "unique", "script", "desc", "replace_keycodes" }
local opts = {} ---@type vim.keymap.set.Opts
for _, k in ipairs(valid) do
if mapping[k] ~= nil then
opts[k] = mapping[k]
end
end
vim.keymap.set(mapping.mode, mapping.lhs, mapping.rhs, opts)
end
---@param spec wk.Spec
---@param opts? wk.Parse
function M.parse(spec, opts)
opts = opts or {}
local ret = {}
M._try_parse(mappings, ret, opts)
return vim.tbl_map(function(m)
return M.to_mapping(m)
end, ret)
M.notify = opts.notify ~= false
local ret = M._parse(spec, nil, opts)
M.notify = true
for _, m in ipairs(ret) do
if m.rhs and opts.create then
M.create(m)
end
end
return ret
end
return M

View File

@ -1,13 +1,13 @@
local Keys = require("which-key.keys")
local Util = require("which-key.util")
local Config = require("which-key.config")
local Util = require("which-key.util")
local M = {}
---@type table<string, wk.Plugin>
M.plugins = {}
function M.setup()
for name, opts in pairs(Config.options.plugins) do
for name, opts in pairs(Config.plugins) do
-- only setup plugin if we didnt load it before
if not M.plugins[name] then
if type(opts) == "boolean" then
@ -22,38 +22,27 @@ function M.setup()
end
end
---@param plugin Plugin
---@param plugin wk.Plugin
function M._setup(plugin, opts)
if plugin.actions then
for _, trigger in pairs(plugin.actions) do
local prefix = trigger.trigger
local mode = trigger.mode or "n"
local label = trigger.label or plugin.name
Keys.register({ [prefix] = { label, plugin = plugin.name } }, { mode = mode })
end
if plugin.mappings then
Config.add(plugin.mappings)
end
if plugin.setup then
plugin.setup(require("which-key"), opts, Config.options)
plugin.setup(opts)
end
end
---@param mapping Mapping
function M.invoke(mapping, context)
local plugin = M.plugins[mapping.plugin]
local prefix = mapping.prefix
local items = plugin.run(prefix, context.mode, context.buf)
local ret = {}
for i, item in
ipairs(items --[[@as VisualMapping[] ]])
do
item.order = i
item.keys = Util.parse_keys(prefix .. item.key)
item.prefix = prefix .. item.key
table.insert(ret, item)
end
---@param name string
function M.cols(name)
local plugin = M.plugins[name]
assert(plugin, "plugin not found")
local ret = {} ---@type wk.Col[]
vim.list_extend(ret, plugin.cols or {})
ret[#ret + 1] = { key = "value", hl = "WhichKeyValue", width = 0.5 }
return ret
end
---@class wk.Node.plugin.item: wk.Node,wk.Plugin.item
return M

View File

@ -1,16 +1,18 @@
---@diagnostic disable: missing-fields, inject-field
---@type wk.Plugin
local M = {}
M.name = "marks"
M.actions = {
{ trigger = "`", mode = "n" },
{ trigger = "'", mode = "n" },
{ trigger = "g`", mode = "n" },
{ trigger = "g'", mode = "n" },
M.mappings = {
icon = { icon = "󰸕 ", color = "orange" },
plugin = "marks",
{ "`", desc = "marks" },
{ "'", desc = "marks" },
{ "g`", desc = "marks" },
{ "g'", desc = "marks" },
}
function M.setup(_wk, _config, options) end
local labels = {
["^"] = "Last position of cursor in insert mode",
["."] = "Last change in current buffer",
@ -24,42 +26,38 @@ local labels = {
[">"] = "To end of last visual selection",
}
---@type Plugin
---@return PluginItem[]
function M.run(_trigger, _mode, buf)
local items = {}
M.cols = {
{ key = "lnum", hl = "Number", align = "right" },
}
local marks = {}
function M.expand()
local buf = vim.api.nvim_get_current_buf()
local items = {} ---@type wk.Plugin.item[]
local marks = {} ---@type vim.fn.getmarklist.ret.item[]
vim.list_extend(marks, vim.fn.getmarklist(buf))
vim.list_extend(marks, vim.fn.getmarklist())
for _, mark in pairs(marks) do
local key = mark.mark:sub(2, 2)
if key == "<" then
key = "<lt>"
end
local lnum = mark.pos[2]
local line
local line ---@type string?
if mark.pos[1] and mark.pos[1] ~= 0 then
local lines = vim.fn.getbufline(mark.pos[1], lnum)
if lines and lines[1] then
line = lines[1]
end
line = vim.api.nvim_buf_get_lines(mark.pos[1], lnum - 1, lnum, false)[1]
end
local file = mark.file and vim.fn.fnamemodify(mark.file, ":p:~:.")
local value = string.format("%4d ", lnum)
value = value .. (line or file or "")
table.insert(items, {
key = key,
desc = labels[key] or file and ("file: " .. file) or "",
value = value,
value = vim.trim(line or file or ""),
highlights = { { 1, 5, "Number" } },
lnum = lnum,
})
end
return items
end

View File

@ -1,107 +0,0 @@
local M = {}
M.name = "presets"
M.operators = {
d = "Delete",
c = "Change",
y = "Yank (copy)",
["g~"] = "Toggle case",
["gu"] = "Lowercase",
["gU"] = "Uppercase",
[">"] = "Indent right",
["<lt>"] = "Indent left",
["zf"] = "Create fold",
["!"] = "Filter through external program",
["v"] = "Visual Character Mode",
-- ["V"] = "Visual Line Mode",
}
M.motions = {
["h"] = "Left",
["j"] = "Down",
["k"] = "Up",
["l"] = "Right",
["w"] = "Next word",
["%"] = "Matching character: '()', '{}', '[]'",
["b"] = "Previous word",
["e"] = "Next end of word",
["ge"] = "Previous end of word",
["0"] = "Start of line",
["^"] = "Start of line (non-blank)",
["$"] = "End of line",
["f"] = "Move to next char",
["F"] = "Move to previous char",
["t"] = "Move before next char",
["T"] = "Move before previous char",
["gg"] = "First line",
["G"] = "Last line",
["{"] = "Previous empty line",
["}"] = "Next empty line",
}
M.objects = {
a = { name = "around" },
i = { name = "inside" },
['a"'] = [[double quoted string]],
["a'"] = [[single quoted string]],
["a("] = [[same as ab]],
["a)"] = [[same as ab]],
["a<lt>"] = [[a <> from '<' to the matching '>']],
["a>"] = [[same as a<]],
["aB"] = [[a Block from [{ to ]} (with brackets)]],
["aW"] = [[a WORD (with white space)]],
["a["] = [[a [] from '[' to the matching ']']],
["a]"] = [[same as a[]],
["a`"] = [[string in backticks]],
["ab"] = [[a block from [( to ]) (with braces)]],
["ap"] = [[a paragraph (with white space)]],
["as"] = [[a sentence (with white space)]],
["at"] = [[a tag block (with white space)]],
["aw"] = [[a word (with white space)]],
["a{"] = [[same as aB]],
["a}"] = [[same as aB]],
['i"'] = [[double quoted string without the quotes]],
["i'"] = [[single quoted string without the quotes]],
["i("] = [[same as ib]],
["i)"] = [[same as ib]],
["i<lt>"] = [[inner <> from '<' to the matching '>']],
["i>"] = [[same as i<]],
["iB"] = [[inner Block from [{ and ]}]],
["iW"] = [[inner WORD]],
["i["] = [[inner [] from '[' to the matching ']']],
["i]"] = [[same as i[]],
["i`"] = [[string in backticks without the backticks]],
["ib"] = [[inner block from [( to ])]],
["ip"] = [[inner paragraph]],
["is"] = [[inner sentence]],
["it"] = [[inner tag block]],
["iw"] = [[inner word]],
["i{"] = [[same as iB]],
["i}"] = [[same as iB]],
}
---@param config Options
function M.setup(wk, opts, config)
require("which-key.plugins.presets.misc").setup(wk, opts)
-- Operators
if opts.operators then
for op, label in pairs(M.operators) do
config.operators[op] = label
end
end
-- Motions
if opts.motions then
wk.register(M.motions, { mode = "n", prefix = "", preset = true })
wk.register(M.motions, { mode = "o", prefix = "", preset = true })
end
-- Text objects
if opts.text_objects then
wk.register(M.objects, { mode = "o", prefix = "", preset = true })
end
end
return M

View File

@ -1,101 +0,0 @@
local M = {}
M.name = "misc"
local misc = {
windows = {
["<c-w>"] = {
name = "window",
s = "Split window",
v = "Split window vertically",
w = "Switch windows",
q = "Quit a window",
o = "Close all other windows",
T = "Break out into a new tab",
x = "Swap current with next",
["-"] = "Decrease height",
["+"] = "Increase height",
["<lt>"] = "Decrease width",
[">"] = "Increase width",
["|"] = "Max out the width",
["_"] = "Max out the height",
["="] = "Equally high and wide",
h = "Go to the left window",
l = "Go to the right window",
k = "Go to the up window",
j = "Go to the down window",
},
},
z = {
["z"] = {
o = "Open fold under cursor",
O = "Open all folds under cursor",
c = "Close fold under cursor",
C = "Close all folds under cursor",
d = "Delete fold under cursor",
D = "Delete all folds under cursor",
E = "Delete all folds in file",
a = "Toggle fold under cursor",
A = "Toggle all folds under cursor",
v = "Show cursor line",
M = "Close all folds",
R = "Open all folds",
m = "Fold more",
r = "Fold less",
x = "Update folds",
z = "Center this line",
t = "Top this line",
["<CR>"] = "Top this line, 1st non-blank col",
b = "Bottom this line",
g = "Add word to spell list",
w = "Mark word as bad/misspelling",
e = "Right this line",
s = "Left this line",
H = "Half screen to the left",
L = "Half screen to the right",
i = "Toggle folding",
["="] = "Spelling suggestions",
},
},
nav = {
["[{"] = "Previous {",
["[("] = "Previous (",
["[<lt>"] = "Previous <",
["[m"] = "Previous method start",
["[M"] = "Previous method end",
["[%"] = "Previous unmatched group",
["[s"] = "Previous misspelled word",
["]{"] = "Next {",
["]("] = "Next (",
["]<lt>"] = "Next <",
["]m"] = "Next method start",
["]M"] = "Next method end",
["]%"] = "Next unmatched group",
["]s"] = "Next misspelled word",
["H"] = "Home line of window (top)",
["M"] = "Middle line of window",
["L"] = "Last line of window",
},
g = {
["gf"] = "Go to file under cursor",
["gx"] = "Open the file under cursor with system app",
["gi"] = "Move to the last insertion and INSERT",
["gv"] = "Switch to VISUAL using last selection",
["gn"] = "Search forwards and select",
["gN"] = "Search backwards and select",
["g%"] = "Cycle backwards through results",
["gt"] = "Go to next tab page",
["gT"] = "Go to previous tab page",
},
}
function M.setup(wk, config)
for key, mappings in pairs(misc) do
if config[key] ~= false then
wk.register(mappings, { mode = "n", prefix = "", preset = true })
end
end
wk.register({ ["zf"] = "Create fold from selection" }, { mode = "x", prefix = "", preset = true })
end
return M

View File

@ -1,18 +1,18 @@
---@type Plugin
local Util = require("which-key.util")
---@diagnostic disable: missing-fields, inject-field
---@type wk.Plugin
local M = {}
M.name = "registers"
M.actions = {
{ trigger = '"', mode = "n" },
{ trigger = '"', mode = "v" },
-- { trigger = "@", mode = "n" },
{ trigger = "<c-r>", mode = "i" },
{ trigger = "<c-r>", mode = "c" },
M.mappings = {
icon = { icon = "󰅍 ", color = "blue" },
plugin = "registers",
{ '"', mode = { "n", "x" }, desc = "registers" },
{ "<c-r>", mode = { "i", "c" }, desc = "registers" },
}
function M.setup(_wk, _config, options) end
M.registers = '*+"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789'
local labels = {
@ -30,43 +30,33 @@ local labels = {
["/"] = "last search pattern",
}
-- This function makes the assumption that OSC 52 is set up per :help osc-52
M.osc52_active = function()
-- If no clipboard set, can't be OSC 52
if not vim.g.clipboard then
return false
end
M.replace = {
["<Space>"] = " ",
["<lt>"] = "<",
["<NL>"] = "\n",
["\r"] = "",
}
-- Per the docs, OSC 52 should be set up with a name field in the table
if vim.g.clipboard.name == "OSC 52" then
return true
end
function M.expand()
local items = {} ---@type wk.Plugin.item[]
return false
end
---@type Plugin
---@return PluginItem[]
function M.run(_trigger, _mode, _buf)
local items = {}
local osc52_skip_keys = { "+", "*" }
local is_osc52 = vim.g.clipboard and vim.g.clipboard.name == "OSC 52"
local has_clipboard = vim.g.loaded_clipboard_provider == 2
for i = 1, #M.registers, 1 do
local key = M.registers:sub(i, i)
local value = ""
if M.osc52_active() and vim.tbl_contains(osc52_skip_keys, key) then
if is_osc52 and key:match("[%+%*]") then
value = "OSC 52 detected, register not checked to maintain compatibility"
else
elseif has_clipboard or not key:match("[%+%*]") then
local ok, reg_value = pcall(vim.fn.getreg, key, 1)
if ok then
value = reg_value
end
value = (ok and reg_value or "") --[[@as string]]
end
if value ~= "" then
value = vim.fn.keytrans(value) --[[@as string]]
for k, v in pairs(M.replace) do
value = value:gsub(k, v) --[[@as string]]
end
table.insert(items, { key = key, desc = labels[key] or "", value = value })
end
end

View File

@ -1,38 +1,42 @@
---@diagnostic disable: missing-fields, inject-field
---@type wk.Plugin
local M = {}
M.name = "spelling"
M.actions = { { trigger = "z=", mode = "n" } }
M.mappings = {
{
"z=",
icon = { icon = "", color = "red" },
plugin = "spelling",
desc = "Spelling Suggestions",
},
}
---@type table<string, any>
M.opts = {}
function M.setup(_, config, options)
M.opts = config
function M.setup(opts)
M.opts = opts
end
---@type Plugin
---@return PluginItem[]
function M.run()
function M.expand()
-- if started with a count, let the default keybinding work
local count = vim.api.nvim_get_vvar("count")
local count = vim.v.count
if count and count > 0 then
return {}
end
---@diagnostic disable-next-line: missing-parameter
local cursor_word = vim.fn.expand("<cword>")
-- get a misspelled word from under the cursor, if not found, then use the cursor_word instead
---@diagnostic disable-next-line: redundant-parameter
local bad = vim.fn.spellbadword(cursor_word)
local word = bad[1]
if word == "" then
word = cursor_word
end
local word = bad[1] == "" and cursor_word or bad[1]
---@type string[]
local suggestions = vim.fn.spellsuggest(word, M.opts.suggestions or 20, bad[2] == "caps" and 1 or 0)
local items = {}
local keys = "1234567890abcdefghijklmnopqrstuvwxyz"
local items = {} ---@type wk.Plugin.item[]
local keys = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i, label in ipairs(suggestions) do
local key = keys:sub(i, i)
@ -40,8 +44,8 @@ function M.run()
table.insert(items, {
key = key,
desc = label,
fn = function()
vim.cmd('norm! "_ciw' .. label)
action = function()
vim.cmd("norm! " .. i .. "z=")
end,
})
end

View File

@ -1,72 +1,164 @@
---@class Highlight
---@field group string
---@field line number
---@field from number
---@field to number
local Util = require("which-key.util")
---@class Text
---@field lines string[]
---@field hl Highlight[]
---@field lineNr number
---@field current string
local Text = {}
Text.__index = Text
---@class wk.Segment
---@field str string Text
---@field hl? string Extmark hl group
---@field line? number line number in a multiline segment
---@field width? number
function Text.len(str)
return vim.fn.strwidth(str)
---@class wk.Text.opts
---@field multiline? boolean
---@field indent? boolean
---@class wk.Text
---@field _lines wk.Segment[][]
---@field _col number
---@field _indents string[]
---@field _opts wk.Text.opts
local M = {}
M.__index = M
M.ns = vim.api.nvim_create_namespace("wk.text")
---@param opts? wk.Text.opts
function M.new(opts)
local self = setmetatable({}, M)
self._lines = {}
self._col = 0
self._opts = opts or {}
self._indents = {}
for i = 0, 100, 1 do
self._indents[i] = (" "):rep(i)
end
return self
end
function Text:new()
local this = { lines = {}, hl = {}, lineNr = 0, current = "" }
setmetatable(this, self)
return this
function M:height()
return #self._lines
end
function Text:fix_nl(line)
return line:gsub("[\n]", "")
---@return number
function M:width()
local width = 0
for _, line in ipairs(self._lines) do
local w = 0
for _, segment in ipairs(line) do
w = w + vim.fn.strdisplaywidth(segment.str)
end
width = math.max(width, w)
end
return width
end
function Text:nl()
local line = self:fix_nl(self.current)
table.insert(self.lines, line)
self.current = ""
self.lineNr = self.lineNr + 1
---@param text string|wk.Segment[]
---@param opts? string|{hl?:string, line?:number}
function M:append(text, opts)
opts = opts or {}
if #self._lines == 0 then
self:nl()
end
if type(text) == "table" then
for _, s in ipairs(text) do
s.width = s.width or #s.str
self._col = self._col + s.width
table.insert(self._lines[#self._lines], s)
end
return self
end
opts = type(opts) == "string" and { hl = opts } or opts
for l, line in ipairs(vim.split(text, "\n", { plain = true })) do
local width = #line
self._col = self._col + width
table.insert(self._lines[#self._lines], {
str = line,
width = width,
hl = opts.hl,
line = opts.line or l,
})
end
return self
end
function Text:set(row, col, str, group)
str = self:fix_nl(str)
function M:nl()
table.insert(self._lines, {})
self._col = 0
return self
end
-- extend lines if needed
for i = 1, row, 1 do
if not self.lines[i] then
self.lines[i] = ""
---@param opts? {sep?:string}
function M:statusline(opts)
local sep = opts and opts.sep or " "
local lines = {} ---@type string[]
for _, line in ipairs(self._lines) do
local parts = {}
for _, segment in ipairs(line) do
local str = segment.str:gsub("%%", "%%%%")
if segment.hl then
str = ("%%#%s#%s%%*"):format(segment.hl, str)
end
parts[#parts + 1] = str
end
table.insert(lines, table.concat(parts, ""))
end
return table.concat(lines, sep)
end
function M:render(buf)
local lines = {}
for _, line in ipairs(self._lines) do
local parts = {} ---@type string[]
for _, segment in ipairs(line) do
parts[#parts + 1] = segment.str
end
table.insert(lines, table.concat(parts, ""))
end
vim.bo[buf].modifiable = true
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
for l, line in ipairs(self._lines) do
local col = 0
local row = l - 1
for _, segment in ipairs(line) do
local width = segment.width
if segment.hl then
Util.set_extmark(buf, M.ns, row, col, {
hl_group = segment.hl,
end_col = col + width,
})
end
col = col + width
end
end
-- extend columns when needed
local width = Text.len(self.lines[row])
if width < col then
self.lines[row] = self.lines[row] .. string.rep(" ", col - width)
end
local before = vim.fn.strcharpart(self.lines[row], 0, col)
local after = vim.fn.strcharpart(self.lines[row], col)
self.lines[row] = before .. str .. after
if not group then
return
end
-- set highlights
self:highlight(row, col, col + Text.len(str), "WhichKey" .. group)
vim.bo[buf].modifiable = false
end
function Text:highlight(row, from, to, group)
local line = self.lines[row]
local before = vim.fn.strcharpart(line, 0, from)
local str = vim.fn.strcharpart(line, 0, to)
from = vim.fn.strlen(before)
to = vim.fn.strlen(str)
table.insert(self.hl, { line = row - 1, from = from, to = to, group = group })
function M:trim()
while #self._lines > 0 and #self._lines[#self._lines] == 0 do
table.remove(self._lines)
end
end
return Text
function M:row()
return #self._lines == 0 and 1 or #self._lines
end
---@param opts? {display:boolean}
function M:col(opts)
if opts and opts.display then
local ret = 0
for _, segment in ipairs(self._lines[#self._lines] or {}) do
ret = ret + vim.fn.strdisplaywidth(segment.str)
end
return ret
end
return self._col
end
return M

View File

@ -1,112 +1,103 @@
local Config = require("which-key.config")
local Node = require("which-key.node")
local Util = require("which-key.util")
---@class Tree
---@field root Node
---@field nodes table<string, Node>
local Tree = {}
Tree.__index = Tree
---@class wk.Tree
---@field root wk.Node
local M = {}
M.__index = M
---@class Node
---@field mapping Mapping
---@field prefix_i string
---@field prefix_n string
---@field children table<string, Node>
-- selene: allow(unused_variable)
local Node
---@type table<string, table<wk.Mapping|wk.Keymap, true>>
M.dups = {}
---@return Tree
function Tree:new()
local this = { root = { children = {}, prefix_i = "", prefix_n = "" }, nodes = {} }
setmetatable(this, self)
return this
function M.new()
local self = setmetatable({}, M)
self:clear()
return self
end
---@param prefix_i string
---@param index? number defaults to last. If < 0, then offset from last
---@param plugin_context? any
---@return Node?
function Tree:get(prefix_i, index, plugin_context)
local prefix = Util.parse_internal(prefix_i)
local node = self.root
index = index or #prefix
if index < 0 then
index = #prefix + index
end
for i = 1, index, 1 do
node = node.children[prefix[i]]
if node and plugin_context and node.mapping and node.mapping.plugin then
local children = require("which-key.plugins").invoke(node.mapping, plugin_context)
node.children = {}
for _, child in pairs(children) do
self:add(child, { cache = false })
end
end
if not node then
return nil
end
end
return node
function M:clear()
self.root = Node.new()
end
-- Returns the path (possibly incomplete) for the prefix
---@param prefix_i string
---@return Node[]
function Tree:path(prefix_i)
local prefix = Util.parse_internal(prefix_i)
local node = self.root
local path = {}
for i = 1, #prefix, 1 do
node = node.children[prefix[i]]
table.insert(path, node)
if not node then
break
end
---@param keymap wk.Mapping|wk.Keymap
---@param virtual? boolean
function M:add(keymap, virtual)
if not Config.filter(keymap) then
return
end
local keys = Util.keys(keymap.lhs, { norm = true })
local node = assert(self.root:find(keys, { create = true }))
node.plugin = node.plugin or keymap.plugin
if virtual then
---@cast node wk.Node
if node.mapping and not keymap.preset and not node.mapping.preset then
local id = keymap.mode .. ":" .. node.keys
M.dups[id] = M.dups[id] or {}
M.dups[id][keymap] = true
M.dups[id][node.mapping] = true
end
if not (keymap.preset and node.keymap and node.keymap.desc) then
node.mapping = keymap --[[@as wk.Mapping]]
end
else
node.keymap = keymap
end
return path
end
---@param mapping Mapping
---@param opts? {cache?: boolean}
---@return Node
function Tree:add(mapping, opts)
---@param node wk.Node
function M:del(node)
if node == self.root then
return self:clear()
end
local parent = node.parent
assert(parent, "node has no parent")
parent._children[node.key] = nil
if not self:keep(parent) then
self:del(parent)
end
end
---@param node wk.Node
function M:keep(node)
if node.hidden or (node.keymap and node.keymap.desc == "which_key_ignore") then
return false
end
return node:can_expand() or node.keymap or node:is_group() or (node.mapping and not node.group)
end
function M:fix()
self:walk(function(node)
if not self:keep(node) then
self:del(node)
return false
end
end)
end
---@param keys string|string[]
---@param opts? { create?: boolean, expand?: boolean }
---@return wk.Node?
function M:find(keys, opts)
keys = type(keys) == "string" and Util.keys(keys) or keys
return self.root:find(keys, opts)
end
---@param fn fun(node: wk.Node):boolean?
---@param opts? {expand?: boolean}
function M:walk(fn, opts)
opts = opts or {}
opts.cache = opts.cache ~= false
local node_key = mapping.keys.keys
local node = opts.cache and self.nodes[node_key]
if not node then
local prefix_i = mapping.keys.internal
local prefix_n = mapping.keys.notation
node = self.root
local path_i = ""
local path_n = ""
for i = 1, #prefix_i, 1 do
path_i = path_i .. prefix_i[i]
path_n = path_n .. prefix_n[i]
if not node.children[prefix_i[i]] then
node.children[prefix_i[i]] = {
children = {},
prefix_i = path_i,
prefix_n = path_n,
}
---@type wk.Node[]
local queue = { self.root }
while #queue > 0 do
local node = table.remove(queue, 1) ---@type wk.Node
if node == self.root or fn(node) ~= false then
local children = opts.expand and node:children() or node._children
for _, child in pairs(children) do
queue[#queue + 1] = child
end
node = node.children[prefix_i[i]]
end
if opts.cache then
self.nodes[node_key] = node
end
end
node.mapping = vim.tbl_deep_extend("force", node.mapping or {}, mapping)
return node
end
---@param cb fun(node:Node)
---@param node? Node
function Tree:walk(cb, node)
node = node or self.root
cb(node)
for _, child in pairs(node.children) do
self:walk(cb, child)
end
end
return Tree
return M

View File

@ -2,72 +2,115 @@
--# selene: allow(unused_variable)
---@class Keymap
---@field rhs string
---@field lhs string
---@field buffer number
---@field expr number
---@field lnum number
---@field mode string
---@field noremap number
---@field nowait number
---@field script number
---@field sid number
---@field silent number
---@field callback fun()|nil
---@field id string terminal keycodes for lhs
---@field desc string
---@class KeyCodes
---@field keys string
---@field internal string[]
---@field notation string[]
---@class MappingOptions
---@field noremap boolean
---@field silent boolean
---@field nowait boolean
---@field expr boolean
---@class Mapping
---@field buf number
---@field group boolean
---@field desc string
---@field prefix string
---@field cmd string
---@field opts MappingOptions
---@field keys KeyCodes
---@class wk.Filter
---@field mode? string
---@field callback fun()|nil
---@field preset boolean
---@field plugin string
---@field fn fun()
---@class MappingTree
---@field mode string
---@field buf? number
---@field tree Tree
---@field keys? string
---@field global? boolean
---@field local? boolean
---@field update? boolean
---@field delay? number
---@field loop? boolean
---@field defer? boolean don't show the popup immediately. Wait for the first key to be pressed
---@field waited? number
---@field check? boolean
---@field expand? boolean
---@class VisualMapping : Mapping
---@field key string
---@field highlights table
---@field value string
---@class wk.Icon
---@field icon? string
---@field hl? string
---@field cat? "file" | "filetype" | "extension"
---@field name? string
---@field color? false | "azure" | "blue" | "cyan" | "green" | "grey" | "orange" | "purple" | "red" | "yellow"
---@class PluginItem
---@field key string
---@field label string
---@field value string
---@field cmd string
---@field highlights table
---@class PluginAction
---@field trigger string
---@field mode string
---@field label? string
---@field delay? boolean
---@class Plugin
---@class wk.IconProvider
---@field name string
---@field actions PluginAction[]
---@field run fun(trigger:string, mode:string, buf:number):PluginItem[]
---@field setup fun(wk, opts, Options)
---@field available? boolean
---@field get fun(icon: wk.Icon):(icon: string?, hl: string?)
---@class wk.IconRule: wk.Icon
---@field pattern? string
---@field plugin? string
---@class wk.Keymap: vim.api.keyset.keymap
---@field lhs string
---@field mode string
---@field rhs? string|fun()
---@field lhsraw? string
---@field buffer? number
--- Represents a node in the which-key tree
---@class wk.Node: wk.Mapping
---@field key string single key of the node
---@field path string[] path to the node (all keys leading to this node)
---@field keys string full key sequence
---@field parent? wk.Node parent node
---@field keymap? wk.Keymap Real keymap
---@field mapping? wk.Mapping Mapping info supplied by user
---@field action? fun() action to execute when node is selected (used by plugins)
---@class wk.Mapping: wk.Keymap
---@field idx? number
---@field plugin? string
---@field group? boolean
---@field remap? boolean
---@field hidden? boolean
---@field preset? boolean
---@field icon? wk.Icon|string
---@field proxy? string
---@field expand? fun():wk.Spec
---@class wk.Spec: {[number]: wk.Spec} , wk.Mapping
---@field [1]? string
---@field [2]? string|fun()
---@field lhs? string
---@field group? string|fun():string
---@field desc? string|fun():string
---@field icon? wk.Icon|string|fun():(wk.Icon|string)
---@field buffer? number|boolean
---@field mode? string|string[]
---@field cond? boolean|fun():boolean?
---@class wk.Win.opts: vim.api.keyset.win_config
---@field width? wk.Dim
---@field height? wk.Dim
---@field wo? vim.wo
---@field bo? vim.bo
---@field padding? {[1]: number, [2]:number}
---@field no_overlap? boolean
---@class wk.Col
---@field key string
---@field hl? string
---@field width? number
---@field padding? number[]
---@field default? string
---@field align? "left"|"right"|"center"
---@class wk.Table.opts
---@field cols wk.Col[]
---@field rows table<string, string>[]
---@class wk.Plugin.item
---@field key string
---@field value string
---@field desc string
---@field order? number
---@field action? fun()
---@class wk.Plugin
---@field name string
---@field cols? wk.Col[]
---@field mappings? wk.Spec
---@field expand fun():wk.Plugin.item[]
---@field setup fun(opts: table<string, any>)
---@class wk.Item: wk.Node
---@field node wk.Node
---@field key string
---@field raw_key string
---@field desc string
---@field group? boolean
---@field order? number
---@field icon? string
---@field icon_hl? string

View File

@ -1,196 +1,301 @@
---@class Util
local M = {}
local strbyte = string.byte
local strsub = string.sub
---@type table<string, KeyCodes>
local cache = {}
---@type table<string,string>
local tcache = {}
local cache_leaders = ""
function M.check_cache()
---@type string
local leaders = (vim.g.mapleader or "") .. ":" .. (vim.g.maplocalleader or "")
if leaders ~= cache_leaders then
cache = {}
tcache = {}
cache_leaders = leaders
end
end
function M.count(tab)
local ret = 0
for _, _ in pairs(tab) do
ret = ret + 1
end
return ret
end
function M.get_mode()
local mode = vim.api.nvim_get_mode().mode
mode = mode:gsub(M.t("<C-V>"), "v")
mode = mode:gsub(M.t("<C-S>"), "s")
return mode:lower()
end
function M.is_empty(tab)
return M.count(tab) == 0
end
M.cache = {
keys = {}, ---@type table<string, string[]>
norm = {}, ---@type table<string, string>
termcodes = {}, ---@type table<string, string>
}
function M.t(str)
M.check_cache()
if not tcache[str] then
-- https://github.com/neovim/neovim/issues/17369
tcache[str] = vim.api.nvim_replace_termcodes(str, false, true, true):gsub("\128\254X", "\128")
end
return tcache[str]
M.cache.termcodes[str] = M.cache.termcodes[str] or vim.api.nvim_replace_termcodes(str, true, true, true)
return M.cache.termcodes[str]
end
-- stylua: ignore start
local utf8len_tab = {
-- ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 ?A ?B ?C ?D ?E ?F
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 0?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 1?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 2?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 3?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 4?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 5?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 6?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 7?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 8?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 9?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- A?
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- B?
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- C?
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- D?
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, -- E?
4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1, -- F?
}
-- stylua: ignore end
M.CR = M.t("<cr>")
M.ESC = M.t("<esc>")
M.BS = M.t("<bs>")
M.EXIT = M.t("<C-\\><C-n>")
M.LUA_CALLBACK = "\x80\253g"
M.CMD = "\x80\253h"
local Tokens = {
["<"] = strbyte("<"),
[">"] = strbyte(">"),
["-"] = strbyte("-"),
}
---@return KeyCodes
function M.parse_keys(keystr)
M.check_cache()
if cache[keystr] then
return cache[keystr]
function M.exit()
vim.api.nvim_feedkeys(M.EXIT, "n", false)
vim.api.nvim_feedkeys(M.ESC, "n", false)
end
---@param rhs string|fun()
function M.is_nop(rhs)
return type(rhs) == "string" and (rhs == "" or rhs:lower() == "<nop>")
end
--- Normalizes (and fixes) the lhs of a keymap
---@param lhs string
function M.norm(lhs)
M.cache.norm[lhs] = M.cache.norm[lhs] or vim.fn.keytrans(M.t(lhs))
return M.cache.norm[lhs]
end
-- Default register
function M.reg()
-- this will be set to 2 if there is a non-empty clipboard
-- tool available
if vim.g.loaded_clipboard_provider ~= 2 then
return '"'
end
local cb = vim.o.clipboard
return cb:find("unnamedplus") and "+" or cb:find("unnamed") and "*" or '"'
end
local keys = M.t(keystr)
local internal = M.parse_internal(keys)
if #internal == 0 then
local ret = { keys = keys, internal = {}, notation = {} }
cache[keystr] = ret
return ret
--- Returns the keys of a keymap, taking multibyte and special keys into account
---@param lhs string
---@param opts? {norm?: boolean}
function M.keys(lhs, opts)
lhs = opts and opts.norm == false and lhs or M.norm(lhs)
if M.cache.keys[lhs] then
return M.cache.keys[lhs]
end
local keystr_orig = keystr
keystr = keystr:gsub("<lt>", "<")
local notation = {}
---@alias ParseState
--- | "Character"
--- | "Special"
--- | "SpecialNoClose"
local start = 1
local i = start
---@type ParseState
local state = "Character"
while i <= #keystr do
local c = strbyte(keystr, i, i)
if state == "Character" then
start = i
-- Only interpret special tokens if neovim also replaces it
state = c == Tokens["<"] and internal[#notation + 1] ~= "<" and "Special" or state
elseif state == "Special" then
state = (c == Tokens["-"] and "SpecialNoClose") or (c == Tokens[">"] and "Character") or state
local ret = {} ---@type string[]
local bytes = vim.fn.str2list(lhs) ---@type number[]
local special = nil ---@type string?
for _, byte in ipairs(bytes) do
local char = vim.fn.nr2char(byte) ---@type string
if char == "<" then
special = "<"
elseif special then
special = special .. char
if char == ">" then
ret[#ret + 1] = special == "<lt>" and "<" or special
special = nil
end
else
state = "Special"
end
i = i + utf8len_tab[c + 1]
if state == "Character" then
local k = strsub(keystr, start, i - 1)
notation[#notation + 1] = k == " " and "<space>" or k
ret[#ret + 1] = char
end
end
local mapleader = vim.g.mapleader
mapleader = mapleader and M.t(mapleader)
notation[1] = internal[1] == mapleader and "<leader>" or notation[1]
if #notation ~= #internal then
error(vim.inspect({ keystr = keystr, internal = internal, notation = notation }))
end
local ret = {
keys = keys,
internal = internal,
notation = notation,
}
cache[keystr_orig] = ret
M.cache.keys[lhs] = ret
return ret
end
-- @return string[]
function M.parse_internal(keystr)
local keys = {}
---@alias ParseInternalState
--- | "Character"
--- | "Special"
---@type ParseInternalState
local state = "Character"
local start = 1
local i = 1
while i <= #keystr do
local c = strbyte(keystr, i, i)
if state == "Character" then
state = c == 128 and "Special" or state
i = i + utf8len_tab[c + 1]
if state == "Character" then
keys[#keys + 1] = strsub(keystr, start, i - 1)
start = i
end
else
-- This state is entered on the second byte of K_SPECIAL sequence.
if c == 252 then
-- K_SPECIAL KS_MODIFIER: skip this byte and the next
i = i + 2
else
-- K_SPECIAL _: skip this byte
i = i + 1
end
-- The last byte of this sequence should be between 0x02 and 0x7f,
-- switch to Character state to collect.
state = "Character"
end
---@param mode? string
function M.mapmode(mode)
mode = mode or vim.api.nvim_get_mode().mode
mode = mode:gsub(M.t("<C-V>"), "v"):gsub(M.t("<C-S>"), "s"):lower()
if mode:sub(1, 2) == "no" then
return "o"
end
return keys
if mode:sub(1, 1) == "v" then
return "x" -- mapmode is actually "x" for visual only mappings
end
return mode:sub(1, 1):match("[ncitsxo]") or "n"
end
function M.warn(msg)
vim.notify(msg, vim.log.levels.WARN, { title = "WhichKey" })
function M.xo()
return M.mapmode():find("[xo]") ~= nil
end
function M.error(msg)
vim.notify(msg, vim.log.levels.ERROR, { title = "WhichKey" })
---@alias NotifyOpts {level?: number, title?: string, once?: boolean, id?:string}
---@param msg string|string[]
---@param opts? NotifyOpts
function M.notify(msg, opts)
opts = opts or {}
msg = type(msg) == "table" and table.concat(msg, "\n") or msg
---@cast msg string
msg = vim.trim(msg)
return vim[opts.once and "notify_once" or "notify"](msg, opts.level, {
title = opts.title or "which-key.nvim",
on_open = function(win)
M.wo(win, { conceallevel = 3, spell = false, concealcursor = "n" })
vim.treesitter.start(vim.api.nvim_win_get_buf(win), "markdown")
end,
})
end
function M.check_mode(mode, buf)
if not ("nvsxoiRct"):find(mode) then
M.error(string.format("Invalid mode %q for buf %d", mode, buf or 0))
---@param msg string|string[]
---@param opts? NotifyOpts
function M.warn(msg, opts)
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.WARN }, opts or {}))
end
---@param msg string|string[]
---@param opts? NotifyOpts
function M.info(msg, opts)
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.INFO }, opts or {}))
end
---@param msg string|string[]
---@param opts? NotifyOpts
function M.error(msg, opts)
M.notify(msg, vim.tbl_extend("keep", { level = vim.log.levels.ERROR }, opts or {}))
end
---@generic F: fun()
---@param ms number
---@param fn F
---@return F
function M.debounce(ms, fn)
local timer = (vim.uv or vim.loop).new_timer()
return function(...)
local args = { ... }
timer:start(
ms,
0,
vim.schedule_wrap(function()
fn(args)
end)
)
end
end
---@param opts? {msg?: string}
function M.try(fn, opts)
local ok, err = pcall(fn)
if not ok then
local msg = opts and opts.msg or "Something went wrong:"
msg = msg .. "\n" .. err
M.error(msg)
end
end
---@param buf number
---@param row number
---@param ns number
---@param col number
---@param opts vim.api.keyset.set_extmark
---@param debug_info? any
function M.set_extmark(buf, ns, row, col, opts, debug_info)
local ok, err = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts)
if not ok then
M.error(
"Failed to set extmark for preview:\n"
.. vim.inspect({ info = debug_info, row = row, col = col, opts = opts, error = err })
)
end
end
---@param n number buffer or window number
---@param type "win" | "buf"
---@param opts vim.wo | vim.bo
local function set_opts(n, type, opts)
---@diagnostic disable-next-line: no-unknown
for k, v in pairs(opts or {}) do
---@diagnostic disable-next-line: no-unknown
pcall(vim.api.nvim_set_option_value, k, v, type == "win" and {
scope = "local",
win = n,
} or { buf = n })
end
end
---@param win number
---@param opts vim.wo
function M.wo(win, opts)
set_opts(win, "win", opts)
end
---@param buf number
---@param opts vim.bo
function M.bo(buf, opts)
set_opts(buf, "buf", opts)
end
local trace_level = 0
---@param msg? string
---@param ...? any
function M.trace(msg, ...)
if not msg then
trace_level = trace_level - 1
return
end
trace_level = math.max(trace_level, 0)
M.debug(msg, ...)
trace_level = trace_level + 1
end
---@param msg? string
---@param ...? any
function M.debug(msg, ...)
if not require("which-key.config").debug then
return
end
local data = { ... }
if #data == 0 then
data = nil
elseif #data == 1 then
data = data[1]
end
if type(data) == "function" then
data = data()
end
if type(data) == "table" then
data = table.concat(
vim.tbl_map(function(value)
return type(value) == "string" and value or vim.inspect(value):gsub("%s+", " ")
end, data),
" "
)
end
if data and type(data) ~= "string" then
data = vim.inspect(data):gsub("%s+", " ")
end
msg = data and ("%s: %s"):format(msg, data) or msg
msg = string.rep(" ", trace_level) .. msg
M.log(msg .. "\n")
end
function M.log(msg)
local file = "./wk.log"
local fd = io.open(file, "a+")
if not fd then
error(("Could not open file %s for writing"):format(file))
end
fd:write(msg)
fd:close()
end
--- Returns a function that returns true if the cooldown is active.
--- The cooldown will be active for the given duration or 0 if no duration is given.
--- Runs in the main loop.
--- cooldown(true) will wait till the next tick.
---@return fun(cooldown?: number|boolean): boolean
function M.cooldown()
local waiting = false
---@param cooldown? number|boolean
return function(cooldown)
if waiting then
return true
elseif cooldown then
waiting = true
vim.defer_fn(function()
waiting = false
end, type(cooldown) == "number" and cooldown or 0)
end
return false
end
return true
end
---@generic T: table
---@param t T
---@param fields string[]
---@return T
function M.getters(t, fields)
local getters = {} ---@type table<string, fun():any>
for _, prop in ipairs(fields) do
if type(t[prop]) == "function" then
getters[prop] = t[prop]
rawset(t, prop, nil)
end
end
if not vim.tbl_isempty(getters) then
setmetatable(t, {
__index = function(_, key)
if getters[key] then
return getters[key](t)
end
end,
})
end
end
return M

View File

@ -1,349 +1,508 @@
local Keys = require("which-key.keys")
local config = require("which-key.config")
local Buf = require("which-key.buf")
local Config = require("which-key.config")
local Icons = require("which-key.icons")
local Layout = require("which-key.layout")
local Plugins = require("which-key.plugins")
local State = require("which-key.state")
local Text = require("which-key.text")
local Tree = require("which-key.tree")
local Util = require("which-key.util")
local Win = require("which-key.win")
local highlight = vim.api.nvim_buf_add_highlight
---@class View
local M = {}
M.view = nil ---@type wk.Win?
M.footer = nil ---@type wk.Win?
M.timer = (vim.uv or vim.loop).new_timer()
M.keys = ""
M.mode = "n"
M.reg = nil
M.auto = false
M.count = 0
M.buf = nil
M.win = nil
---@alias wk.Sorter fun(node:wk.Item): (string|number)
function M.is_valid()
return M.buf
and M.win
and vim.api.nvim_buf_is_valid(M.buf)
and vim.api.nvim_buf_is_loaded(M.buf)
and vim.api.nvim_win_is_valid(M.win)
end
---@type table<string, wk.Sorter>
M.fields = {
order = function(item)
return item.order and item.order or 1000
end,
["local"] = function(item)
return item.keymap and item.keymap.buffer ~= 0 and 0 or 1000
end,
manual = function(item)
return item.mapping and item.mapping.idx or 10000
end,
desc = function(item)
return item.desc or "~"
end,
group = function(item)
return item.group and 1 or 0
end,
alphanum = function(item)
return item.key:find("^%w+$") and 0 or 1
end,
mod = function(item)
return item.key:find("^<.*>$") and 0 or 1
end,
case = function(item)
return item.key:lower() == item.key and 0 or 1
end,
natural = function(item)
local ret = item.key:gsub("%d+", function(d)
return ("%09d"):format(tonumber(d))
end)
return ret:lower()
end,
}
function M.show()
if vim.b.visual_multi then
vim.b.VM_skip_reset_once_on_bufleave = true
end
if M.is_valid() then
return
end
-- non-floating windows
local wins = vim.tbl_filter(function(w)
return vim.api.nvim_win_is_valid(w) and vim.api.nvim_win_get_config(w).relative == ""
end, vim.api.nvim_list_wins())
---@type number[]
local margins = {}
for i, m in ipairs(config.options.window.margin) do
if m > 0 and m < 1 then
if i % 2 == 0 then
m = math.floor(vim.o.columns * m)
else
m = math.floor(vim.o.lines * m)
---@param lhs string
function M.format(lhs)
local keys = Util.keys(lhs)
local ret = vim.tbl_map(function(key)
local inner = key:match("^<(.*)>$")
if not inner then
return key
end
if inner == "NL" then
inner = "C-J"
end
local parts = vim.split(inner, "-", { plain = true })
for i, part in ipairs(parts) do
if i == 1 or i ~= #parts or not part:match("^%w$") then
parts[i] = Config.icons.keys[part] or parts[i]
end
end
margins[i] = m
end
local opts = {
relative = "editor",
width = vim.o.columns - margins[2] - margins[4],
height = config.options.layout.height.min,
focusable = false,
anchor = "SW",
border = config.options.window.border,
row = vim.o.lines
- margins[3]
+ ((vim.o.laststatus == 0 or vim.o.laststatus == 1 and #wins == 1) and 1 or 0)
- vim.o.cmdheight,
col = margins[4],
style = "minimal",
noautocmd = true,
zindex = config.options.window.zindex,
}
if config.options.window.position == "top" then
opts.anchor = "NW"
opts.row = margins[1]
end
M.buf = vim.api.nvim_create_buf(false, true)
M.win = vim.api.nvim_open_win(M.buf, false, opts)
vim.bo[M.buf].filetype = "WhichKey"
vim.bo[M.buf].buftype = "nofile"
vim.bo[M.buf].bufhidden = "wipe"
vim.bo[M.buf].modifiable = true
vim.wo[M.win].winhighlight = "NormalFloat:WhichKeyFloat,FloatBorder:WhichKeyBorder"
vim.wo[M.win].foldmethod = "manual"
vim.wo[M.win].winblend = config.options.window.winblend
return table.concat(parts, "")
end, keys)
return table.concat(ret, "")
end
function M.read_pending()
local esc = ""
while true do
local n = vim.fn.getchar(0)
if n == 0 then
break
end
local c = (type(n) == "number" and vim.fn.nr2char(n) or n)
-- HACK: for some reason, when executing a :norm command,
-- vim keeps feeding <esc> at the end
if c == Util.t("<esc>") then
esc = esc .. c
-- more than 10 <esc> in a row? most likely the norm bug
if #esc > 10 then
return
end
else
-- we have <esc> characters, so add them to keys
if esc ~= "" then
M.keys = M.keys .. esc
esc = ""
end
M.keys = M.keys .. c
end
end
if esc ~= "" then
M.keys = M.keys .. esc
esc = ""
end
end
function M.getchar()
local ok, n = pcall(vim.fn.getchar)
-- bail out on keyboard interrupt
if not ok then
return Util.t("<esc>")
end
local c = (type(n) == "number" and vim.fn.nr2char(n) or n)
return c
end
function M.scroll(up)
local height = vim.api.nvim_win_get_height(M.win)
local cursor = vim.api.nvim_win_get_cursor(M.win)
if up then
cursor[1] = math.max(cursor[1] - height, 1)
else
cursor[1] = math.min(cursor[1] + height, vim.api.nvim_buf_line_count(M.buf))
end
vim.api.nvim_win_set_cursor(M.win, cursor)
end
function M.on_close()
M.hide()
end
function M.hide()
vim.api.nvim_echo({ { "" } }, false, {})
M.hide_cursor()
if M.buf and vim.api.nvim_buf_is_valid(M.buf) then
vim.api.nvim_buf_delete(M.buf, { force = true })
M.buf = nil
end
if M.win and vim.api.nvim_win_is_valid(M.win) then
vim.api.nvim_win_close(M.win, true)
M.win = nil
end
vim.cmd("redraw")
end
function M.show_cursor()
local buf = vim.api.nvim_get_current_buf()
local cursor = vim.api.nvim_win_get_cursor(0)
vim.api.nvim_buf_add_highlight(buf, config.namespace, "Cursor", cursor[1] - 1, cursor[2], cursor[2] + 1)
end
function M.hide_cursor()
local buf = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_clear_namespace(buf, config.namespace, 0, -1)
end
function M.back()
local node = Keys.get_tree(M.mode, M.buf).tree:get(M.keys, -1) or Keys.get_tree(M.mode).tree:get(M.keys, -1)
if node then
M.keys = node.prefix_i
end
end
function M.execute(prefix_i, mode, buf)
local global_node = Keys.get_tree(mode).tree:get(prefix_i)
local buf_node = buf and Keys.get_tree(mode, buf).tree:get(prefix_i) or nil
if global_node and global_node.mapping and Keys.is_hook(prefix_i, global_node.mapping.cmd) then
return
end
if buf_node and buf_node.mapping and Keys.is_hook(prefix_i, buf_node.mapping.cmd) then
return
end
local hooks = {}
local function unhook(nodes, nodes_buf)
for _, node in pairs(nodes) do
if Keys.is_hooked(node.mapping.prefix, mode, nodes_buf) then
table.insert(hooks, { node.mapping.prefix, nodes_buf })
Keys.hook_del(node.mapping.prefix, mode, nodes_buf)
---@param nodes wk.Item[]
---@param fields? (string|wk.Sorter)[]
function M.sort(nodes, fields)
fields = vim.deepcopy(fields or Config.sort)
vim.list_extend(fields, { "natural", "case" })
table.sort(nodes, function(a, b)
for _, f in ipairs(fields) do
local field = type(f) == "function" and f or M.fields[f]
if field then
local aa = field(a)
local bb = field(b)
if aa ~= bb then
return aa < bb
end
end
end
end
-- make sure we remove all WK hooks before executing the sequence
-- this is to make existing keybindings work and prevent recursion
unhook(Keys.get_tree(mode).tree:path(prefix_i))
if buf then
unhook(Keys.get_tree(mode, buf).tree:path(prefix_i), buf)
end
-- feed CTRL-O again if called from CTRL-O
local full_mode = Util.get_mode()
if full_mode == "nii" or full_mode == "nir" or full_mode == "niv" or full_mode == "vs" then
vim.api.nvim_feedkeys(Util.t("<C-O>"), "n", false)
end
-- handle registers that were passed when opening the popup
if M.reg ~= '"' and M.mode ~= "i" and M.mode ~= "c" then
vim.api.nvim_feedkeys('"' .. M.reg, "n", false)
end
if M.count and M.count ~= 0 then
prefix_i = M.count .. prefix_i
end
-- feed the keys with remap
vim.api.nvim_feedkeys(prefix_i, "m", true)
-- defer hooking WK until after the keys were executed
vim.defer_fn(function()
for _, hook in pairs(hooks) do
Keys.hook_add(hook[1], mode, hook[2])
end
end, 0)
end
function M.open(keys, opts)
opts = opts or {}
M.keys = keys or ""
M.mode = opts.mode or Util.get_mode()
M.count = vim.api.nvim_get_vvar("count")
M.reg = vim.api.nvim_get_vvar("register")
if string.find(vim.o.clipboard, "unnamedplus") and M.reg == "+" then
M.reg = '"'
end
if string.find(vim.o.clipboard, "unnamed") and M.reg == "*" then
M.reg = '"'
end
M.show_cursor()
M.on_keys(opts)
end
function M.is_enabled(buf)
local buftype = vim.bo[buf].buftype
for _, bt in ipairs(config.options.disable.buftypes) do
if bt == buftype then
return false
end
end
local filetype = vim.bo[buf].filetype
for _, bt in ipairs(config.options.disable.filetypes) do
if bt == filetype then
return false
end
end
if vim.fn.getcmdwintype() ~= "" then
return false
end
return true
end
function M.on_keys(opts)
local buf = vim.api.nvim_get_current_buf()
while true do
-- loop
M.read_pending()
local results = Keys.get_mappings(M.mode, M.keys, buf)
--- Check for an exact match. Feedkeys with remap
if results.mapping and not results.mapping.group and #results.mappings == 0 then
M.hide()
if results.mapping.fn then
results.mapping.fn()
else
M.execute(M.keys, M.mode, buf)
end
return
end
-- Check for no mappings found. Feedkeys without remap
if #results.mappings == 0 then
M.hide()
-- only execute if an actual key was typed while WK was open
if opts.auto then
M.execute(M.keys, M.mode, buf)
end
return
end
local layout = Layout:new(results)
if M.is_enabled(buf) then
if not M.is_valid() then
M.show()
end
M.render(layout:layout(M.win))
end
vim.cmd([[redraw]])
local c = M.getchar()
if c == Util.t("<esc>") then
M.hide()
break
elseif c == Util.t(config.options.popup_mappings.scroll_down) then
M.scroll(false)
elseif c == Util.t(config.options.popup_mappings.scroll_up) then
M.scroll(true)
elseif c == Util.t("<bs>") then
M.back()
else
M.keys = M.keys .. c
end
end
end
---@param text Text
function M.render(text)
local view = vim.api.nvim_win_call(M.win, vim.fn.winsaveview)
vim.api.nvim_buf_set_lines(M.buf, 0, -1, false, text.lines)
local height = #text.lines
if height > config.options.layout.height.max then
height = config.options.layout.height.max
end
vim.api.nvim_win_set_height(M.win, height)
if vim.api.nvim_buf_is_valid(M.buf) then
vim.api.nvim_buf_clear_namespace(M.buf, config.namespace, 0, -1)
end
for _, data in ipairs(text.hl) do
highlight(M.buf, config.namespace, data.group, data.line, data.from, data.to)
end
vim.api.nvim_win_call(M.win, function()
vim.fn.winrestview(view)
return a.raw_key < b.raw_key
end)
end
function M.valid()
return M.view and M.view:valid()
end
---@param opts? {delay?: number, schedule?: boolean, waited?: number}
function M.update(opts)
local state = State.state
if not state then
M.hide()
return
end
opts = opts or {}
if M.valid() then
M.show()
elseif opts.schedule ~= false then
local delay = opts.delay
or State.delay({
mode = state.mode.mode,
keys = state.node.keys,
plugin = state.node.plugin,
waited = opts.waited,
})
M.timer:start(
delay,
0,
vim.schedule_wrap(function()
Util.try(M.show)
end)
)
end
end
function M.hide()
if M.view then
M.view:hide()
M.view = nil
end
if M.footer then
M.footer:hide()
M.footer = nil
end
end
---@param field string
---@param value string
---@return string
function M.replace(field, value)
for _, repl in pairs(Config.replace[field]) do
value = type(repl) == "function" and (repl(value) or value) or value:gsub(repl[1], repl[2])
end
return value
end
---@param node wk.Node
function M.icon(node)
-- plugin items should not get icons
if node.parent and node.parent.plugin then
return
end
if node.mapping and node.mapping.icon then
return Icons.get(node.mapping.icon)
end
local icon, icon_hl = Icons.get({ keymap = node.keymap, desc = node.desc })
if icon then
return icon, icon_hl
end
if node.parent then
return M.icon(node.parent)
end
end
---@param node wk.Node
---@param opts? {default?: "count"|"path", parent?: wk.Node, group?: boolean}
function M.item(node, opts)
opts = opts or {}
opts.default = opts.default or "count"
local child_count = (node:can_expand() or opts.group == false) and 0 or node:count()
local desc = node.desc
if not desc and node.keymap and node.keymap.rhs ~= "" and type(node.keymap.rhs) == "string" then
desc = node.keymap.rhs --[[@as string]]
end
if not desc and opts.default == "count" and child_count > 0 then
desc = child_count .. " keymap" .. (child_count > 1 and "s" or "")
end
if not desc and opts.default == "path" then
desc = node.keys
end
desc = M.replace("desc", desc or "")
local icon, icon_hl = M.icon(node)
local raw_key = node.key
if opts.parent and opts.parent ~= node and node.keys:find(opts.parent.keys, 1, true) == 1 then
raw_key = node.keys:sub(opts.parent.keys:len() + 1)
end
local group = node:is_group()
---@type wk.Item
return setmetatable({
node = node,
icon = icon or "",
icon_hl = icon_hl,
key = M.replace("key", raw_key),
raw_key = raw_key,
desc = group and Config.icons.group .. desc or desc,
group = group,
}, { __index = node })
end
---@param node wk.Node
---@param opts? {title?: boolean}
function M.trail(node, opts)
opts = opts or {}
---@param group? string
local function hl(group)
return opts.title and "WhichKeyTitle" or (group and ("WhichKey" .. group) or "WhichKeyGroup")
end
local trail = {} ---@type string[][]
local did_op = false
while node do
local desc = node.desc and (Config.icons.group .. M.replace("desc", node.desc))
or node.key and M.replace("key", node.key)
or ""
node = node.parent
if desc ~= "" then
if node and #trail > 0 then
table.insert(trail, 1, { " " .. Config.icons.breadcrumb .. " ", hl("Separator") })
end
table.insert(trail, 1, { desc, hl() })
end
local m = State.state.mode.mode
if not did_op and not node and (m == "x" or m == "o") then
did_op = true
local mode = Buf.get({ buf = State.state.mode.buf.buf, mode = "n" })
if mode then
node = mode.tree:find(m == "x" and "v" or vim.v.operator)
end
end
end
if #trail > 0 then
table.insert(trail, 1, { " ", hl() })
table.insert(trail, { " ", hl() })
return trail
end
end
---@param root wk.Node
---@param node wk.Node
---@param expand fun(node:wk.Node): boolean
---@param filter fun(node:wk.Node): boolean
---@param ret? wk.Item[]
function M.expand(root, node, expand, filter, ret)
ret = ret or {}
if not filter(node) then
return ret
end
if not node:is_plugin() and expand(node) then
if node.keymap then
ret[#ret + 1] = M.item(node, { group = false, parent = root })
end
for _, child in ipairs(node:children()) do
M.expand(root, child, expand, filter, ret)
end
else
ret[#ret + 1] = M.item(node, { parent = root })
end
return ret
end
function M.show()
local state = State.state
if not (state and state.show and state.node:is_group()) then
M.hide()
return
end
local text = Text.new()
---@type wk.Node[]
local children = state.node:children()
if state.filter.global == false and state.filter.expand == nil then
state.filter.expand = true
end
---@param node wk.Node
local function filter(node)
local l = state.filter["local"] ~= false
local g = state.filter.global ~= false
if not g and not l then
return false
end
if g and l then
return true
end
local is_local = node:is_local()
return l and is_local or g and not is_local
end
---@param node wk.Node
local function expand(node)
if node:is_plugin() then
return false
end
if state.filter.expand then
return true
end
if node:can_expand() then
return false
end
if type(Config.expand) == "function" then
return Config.expand(node)
end
local child_count = node:count()
return child_count > 0 and child_count <= Config.expand
end
---@type wk.Item[]
local items = {}
for _, node in ipairs(children) do
vim.list_extend(items, M.expand(state.node, node, expand, filter))
end
M.sort(items)
---@type wk.Col[]
local cols = {
{ key = "key", hl = "WhichKey", align = "right" },
{ key = "sep", hl = "WhichKeySeparator", default = Config.icons.separator },
{ key = "icon", padding = { 0, 0 } },
}
if state.node.plugin then
vim.list_extend(cols, Plugins.cols(state.node.plugin))
end
cols[#cols + 1] = { key = "desc", width = math.huge }
local t = Layout.new({ cols = cols, rows = items })
local opts = Win.defaults(Config.win)
local container = {
width = Layout.dim(vim.o.columns, vim.o.columns, opts.width),
height = Layout.dim(vim.o.lines, vim.o.lines, opts.height),
}
local _, _, max_row_width = t:cells()
local box_width = Layout.dim(max_row_width, container.width, Config.layout.width)
local box_count = math.max(math.floor(container.width / (box_width + Config.layout.spacing)), 1)
box_width = math.floor(container.width / box_count)
local box_height = math.max(math.ceil(#items / box_count), 2)
local rows = t:layout({ width = box_width - Config.layout.spacing })
for _ = 1, Config.win.padding[1] + 1 do
text:nl()
end
for l = 1, box_height do
text:append(string.rep(" ", Config.win.padding[2]))
for b = 1, box_count do
local i = (b - 1) * box_height + l
local item = items[i]
local row = rows[i]
if b ~= 1 or box_count > 1 then
text:append(string.rep(" ", Config.layout.spacing))
end
if item then
for c, col in ipairs(row) do
local hl = col.hl
if cols[c].key == "desc" then
hl = item.group and "WhichKeyGroup" or "WhichKeyDesc"
end
if cols[c].key == "icon" then
hl = item.icon_hl
end
text:append(col.value, hl)
end
end
end
text:append(string.rep(" ", Config.win.padding[2]))
text:nl()
end
text:trim()
for _ = 1, Config.win.padding[1] do
text:nl()
end
local show_keys = Config.show_keys
local has_border = opts.border and opts.border ~= "none"
if has_border then
if opts.title == true then
opts.title = M.trail(state.node, { title = true })
show_keys = false
end
if opts.footer == true then
opts.footer = M.trail(state.node, { title = true })
show_keys = false
end
if not opts.title then
opts.title = ""
opts.title_pos = nil
end
if not opts.footer then
opts.footer = ""
opts.footer_pos = nil
end
else
opts.footer = nil
opts.footer_pos = nil
opts.title = nil
opts.title_pos = nil
end
local bw = has_border and 2 or 0
opts.width = Layout.dim(text:width() + bw, vim.o.columns, opts.width)
opts.height = Layout.dim(text:height() + bw, vim.o.lines, opts.height)
if Config.show_help then
opts.height = opts.height + 1
end
-- top-left
opts.col = Layout.dim(opts.col, vim.o.columns - opts.width)
opts.row = Layout.dim(opts.row, vim.o.lines - opts.height - vim.o.cmdheight)
opts.width = opts.width - bw
opts.height = opts.height - bw
M.check_overlap(opts)
M.view = M.view or Win.new(opts)
M.view:show(opts)
if Config.show_help or show_keys then
text:nl()
local footer = Text.new()
if show_keys then
footer:append(" ")
for _, segment in ipairs(M.trail(state.node) or {}) do
footer:append(segment[1], segment[2])
end
end
if Config.show_help then
---@type {key: string, desc: string}[]
local keys = {
{ key = "<esc>", desc = "close" },
}
if state.node.parent then
keys[#keys + 1] = { key = "<bs>", desc = "back" }
end
if opts.height < text:height() then
keys[#keys + 1] = { key = "<c-d>/<c-u>", desc = "scroll" }
end
local help = Text.new()
for k, key in ipairs(keys) do
help:append(M.replace("key", Util.norm(key.key)), "WhichKey"):append(" " .. key.desc, "WhichKeySeparator")
if k < #keys then
help:append(" ")
end
end
local col = footer:col({ display = true })
local ws = string.rep(" ", math.floor((opts.width - help:width()) / 2) - col)
footer:append(ws)
footer:append(help._lines[1])
end
footer:trim()
M.footer = M.footer or Win.new()
M.footer:show({
relative = "win",
win = M.view.win,
col = 0,
row = opts.height - 1,
width = opts.width,
height = 1,
zindex = M.view.opts.zindex + 1,
})
footer:render(M.footer.buf)
end
text:render(M.view.buf)
vim.api.nvim_win_call(M.view.win, function()
vim.fn.winrestview({ topline = 1 })
end)
vim.cmd.redraw()
end
---@param opts wk.Win.opts
function M.check_overlap(opts)
if Config.win.no_overlap == false then
return
end
local row, col = vim.fn.screenrow(), vim.fn.screencol()
local overlaps = (row >= opts.row and row <= opts.row + opts.height)
and (col >= opts.col and col <= opts.col + opts.width)
-- dd(overlaps and "overlaps" or "no overlap", {
-- editor = { lines = vim.o.lines, columns = vim.o.columns },
-- cursor = { col = col, row = row },
-- win = { row = opts.row, col = opts.col, height = opts.height, width = opts.width },
-- overlaps = overlaps,
-- })
if overlaps then
opts.row = row + 1
opts.height = math.max(vim.o.lines - opts.row, 4)
end
end
---@param up boolean
function M.scroll(up)
return M.view and M.view:scroll(up)
end
return M

View File

@ -1 +0,0 @@
command! -nargs=* WhichKey lua require('which-key').show_command(<f-args>)

View File

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

View File

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

View File

@ -1,2 +1,21 @@
[selene]
base = "lua51"
name = "vim"
[vim]
any = true
[jit]
any = true
[assert]
any = true
[describe]
any = true
[it]
any = true
[before_each.args]
any = true