Skip to content

fix(icons): pattern match should take precedence over plugin#935

Open
minusfive wants to merge 2 commits intofolke:mainfrom
minusfive:fix-pattern-priority
Open

fix(icons): pattern match should take precedence over plugin#935
minusfive wants to merge 2 commits intofolke:mainfrom
minusfive:fix-pattern-priority

Conversation

@minusfive
Copy link
Copy Markdown

@minusfive minusfive commented Jan 23, 2025

Description

Otherwise all plugin keymaps are forced to use the same icon, can't be granularly customized. This seems to fix a bunch of icons on LazyVim.

E.g. adding an undotree pattern match for Snacks.picker.undo() (#936 😉) is ignored because plugin snacks.nvim was already matched and handled. Thus, all snacks pickers are forced to use the same icon.

Related Issue(s)

N/A

Screenshots

<leader> Before

Screenshot 2025-01-23 at 11 05 51

<leader> After

Screenshot 2025-01-23 at 11 07 07

<leader>g Before

Screenshot 2025-01-23 at 11 06 04

<leader>g After

Screenshot 2025-01-23 at 11 10 59

<leader>s Before

Screenshot 2025-01-23 at 11 05 58

<leader>s After

Screenshot 2025-01-23 at 11 07 12

<leader>f Before

Screenshot 2025-01-23 at 11 06 32

<leader>f After

Screenshot 2025-01-23 at 11 07 17

<leader>u Before

Screenshot 2025-01-23 at 11 06 09

<leader>u After

Screenshot 2025-01-23 at 11 07 22

Otherwise all plugin keymaps are forced to use the same icon, can't be
granularly customized
@minusfive
Copy link
Copy Markdown
Author

@folke if/when you have a chance, mind taking a look at this one?

@andysazima
Copy link
Copy Markdown

andysazima commented Mar 6, 2025

Wanted to chime in and say that I was just looking for a solution to this! I was getting annoyed at all the little candy icons in my which-key window as I have maybe 20 different searches with snacks pickers. Adding a bunch of custom rules just to override this (what I may even call a bug) was a chore. Nice PR!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2025

This PR is stale because it has been open 30 days with no activity.

@github-actions github-actions bot added the stale This issue or PR has been inactive for a while label Apr 6, 2025
iSplasher added a commit to isplasher-forks/which-key.nvim that referenced this pull request Jun 20, 2025
@minusfive
Copy link
Copy Markdown
Author

@folke any interest in getting this merged?

I also have #936, #939 and #940 awaiting your review if you'd like to tackle a few at once, they're tiny.

@github-actions github-actions bot removed the stale This issue or PR has been inactive for a while label Sep 21, 2025
@dpetka2001
Copy link
Copy Markdown

This will definitely be handy for overwriting specific icons without having to go through the trouble to disable the entry for plugin and then having to define all kinds of different patterns so that they can take effect.

@dpetka2001
Copy link
Copy Markdown

After some thought and experimenting, this PR will change all the keymaps that are plugin keymaps (tested on LazyVim) and pattern will take precedence. Only plugin keymaps that won't match a pattern will be matched against the plugin rules.

I went a different approach, that is the user should be able to overwrite plugin keymaps with pattern if he wants to do so. This is the diff patch

diff --git a/lua/which-key/icons.lua b/lua/which-key/icons.lua
index 43aaabc..491cd1f 100644
--- a/lua/which-key/icons.lua
+++ b/lua/which-key/icons.lua
@@ -14,6 +14,7 @@ local M = {}

 ---@type wk.IconRule[]
 M.rules = {
+  { pattern = "undotree", desc = true, icon = "󰙅 ", color = "orange" },
   { plugin = "fzf-lua", cat = "filetype", name = "fzf" },
   { plugin = "neo-tree.nvim", cat = "filetype", name = "neo-tree" },
   { plugin = "octo.nvim", cat = "filetype", name = "git" },
@@ -149,7 +150,12 @@ function M._get(rules, opts, check_ft)
   -- plugin icons
   if plugin then
     for _, icon in ipairs(rules) do
-      if icon.plugin == plugin then
+      if icon.desc and icon.pattern and opts.desc:lower():find(icon.pattern) then
+        local ico, hl = M.get_icon(icon)
+        if ico then
+          return ico, hl
+        end
+      elseif icon.plugin == plugin then
         local ico, hl = M.get_icon(icon)
         if ico then
           return ico, hl
diff --git a/lua/which-key/types.lua b/lua/which-key/types.lua
index 8d985d8..0ffd585 100644
--- a/lua/which-key/types.lua
+++ b/lua/which-key/types.lua
@@ -31,6 +31,7 @@
 ---@class wk.IconRule: wk.Icon
 ---@field pattern? string
 ---@field plugin? string
+---@field desc? boolean

 ---@class wk.Keymap: vim.api.keyset.keymap
 ---@field lhs string

This way we can put rules like { pattern = "undotree", desc = true, icon = "󰙅 ", color = "orange" } at the top of the rules table and they will take precedence. Nothing else changes with regards to the original workflow. Only the check for plugin mappings workflow changes, so that users are able to overwrite them with pattern if they choose to do so.

Ultimately, I believe this comes down to preferences. Folke will have to make a decision.

@dpetka2001
Copy link
Copy Markdown

dpetka2001 commented Feb 9, 2026

This is my final function M._get(). It tries to also take into account #1028 in addition to what's already proposed here.

function M._get(rules, opts, check_ft)
  opts = opts or {}
  opts.ft = type(opts.ft) == "string" and { opts.ft } or opts.ft

  ---@type string?
  local plugin
  local fts = opts.ft or {} --[[@as string[] ]]
  ---@type string?
  local desc = opts.desc and opts.desc:lower()

  if opts.keymap and package.loaded.lazy then
    local LazyUtil = require("lazy.core.util")
    local Keys = require("lazy.core.handler").handlers.keys --[[@as LazyKeysHandler]]
    local keys = Keys.parse(opts.keymap.lhs, opts.keymap.mode)
    plugin = Keys.managed[keys.id]
    if plugin then
      fts[#fts + 1] = LazyUtil.normname(plugin)
    end
  end

  -- plugin icons (pattern takes precedence)
  if plugin then
    if desc then
      -- explicit override by desc for non-plugin rules
      -- Example: `{ pattern = "undotree", override_plugin = true, icon = "󰙅 ", color = "orange" }`
      for _, icon in ipairs(rules) do
        if not icon.plugin and icon.override_plugin == true and icon.pattern and desc:find(icon.pattern) then
          local ico, hl = M.get_icon(icon)
          if ico then
            return ico, hl
          end
        end
      end

      -- plugin + pattern
      for _, icon in ipairs(rules) do
        if icon.plugin == plugin and icon.pattern and desc:find(icon.pattern) then
          local ico, hl = M.get_icon(icon)
          if ico then
            return ico, hl
          end
        end
      end
    end

    -- plugin only (fallback)
    for _, icon in ipairs(rules) do
      if icon.plugin == plugin and not icon.pattern then
        local ico, hl = M.get_icon(icon)
        if ico then
          return ico, hl
        end
      end
    end
  end

  -- filetype icons
  if check_ft then
    if opts.keymap and opts.keymap.buffer and opts.keymap.buffer ~= 0 then
      pcall(function()
        fts[#fts + 1] = vim.bo[opts.keymap.buffer].filetype
      end)
    end
    for _, ft in ipairs(fts) do
      local icon, hl = M.get_icon({ cat = "filetype", name = ft })
      if icon then
        return icon, hl
      end
    end
  end

  -- pattern icons
  if desc then
    for _, icon in ipairs(rules) do
      -- This will match pattern in both plugin and non-plugin rules, so order matters.
      -- The first pattern match will be used, so non-plugin patterns should be defined
      -- before plugin patterns to match non-plugin mapings that apply for the same pattern.
      -- Example:
      -- ```lua
      -- { plugin = "snacks.nvim", pattern = "file", icon = "🔍" }
      -- { pattern = "file", icon = "📁" }
      -- ```
      -- First rule takes precedence over second, so mappings with "file" in desc will get "🔍" icon,
      -- not "📁" when they are not Snacks plugin mappings. The order should be reversed.
      if icon.pattern and desc:find(icon.pattern) then
        local ico, hl = M.get_icon(icon)
        if ico then
          return ico, hl
        end
      end
    end
  end
end

I opted into different loops inside if plugin, instead of chained if..elseif in a single loop because this way the order of the rules won't matter. Not sure about perf (but based on the size of the rules table, I don't believe it'll matter much).

Again, this implementation is based on the current idea that plugin rules should take precedence over the global pattern ones, but still provide the user with a mechanism to override the plugin rules. Additionally, it takes into consideration plugin+pattern together as per #1028.

Of course, I don't know myself which approach should be preferred. My way of thinking is that you use plugin rules to define a whole bunch of plugin mappings regardless of non-plugin rules. And ideally, provide the user with a means for more granular control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants