jarviliam


Making `mini.icons` and `aerial.nvim` Play Nicely

February 21, 2026

Around a month ago, I began slimming down some plugins again. However, this time I opted to utilize more of mini.nvim's ecosystem. Some reasons being:

  1. The plugins aren’t bloated but more QoL focused.
  2. vim.pack is more aligned with mini.deps.
  3. I have a better understanding of how to piece everything together. No longer needing batteries-included lazy.

Porting away from lazy was fairly easy because I had mini-max to use as a reference point. However, one thing was kind of broken. Aerial.nvim.

When I say kind of broken, what I mean is that the icons would show a ‘?’ and a vague error message would pop up. E5248: Invalid character in group name.

It was never really a blocker so I just kept using it with the icons basically being disabled.

Today I took the time to debug it out and figure out why. Why am I the only one with this problem when I search for the error message?

The reason? Nothing to fault aerial.nvim nor mini really. It was a lack of understanding on my part of what MiniIcons.tweak_lsp_kind does. You see, tweak_lsp_kind is required for mini.completion, which I migrated to over blink, to have the symbols show up in the suggestions. What I didn’t realize was that it actually rewrites the vim.lsp.protocol.SymbolKind entirely to have the icons prepended with it.

What do I mean by this? Well vim.lsp.protocol.SymbolKind is a table/map of valid symbols an LSP can return.

SymbolKind = {
    File = 1,
    Module = 2,
    Namespace = 3,
    Package = 4,
    Class = 5,
    Method = 6,
    Property = 7,
    ...
}

Icon providers like mini.icons map these ‘Kinds’ to icons.

H.lsp_icons = {
  array         = { glyph = '', hl = 'MiniIconsOrange' },
  class         = { glyph = '', hl = 'MiniIconsPurple' },
  ...
}

The effect of tweak_lsp_kind makes SymbolKind into (ignore the mismatch of ordering):

SymbolKind = {
    " Function" = 1,
    " Array" = 2,
    ...
}

As aerial.nvim receives these symbols back from the LSP as numbers, File = 1, it gets the symbol name from the vim.lsp.protocol.SymbolKind table.

When requesting the appropriate icon from the icon provider, mini.icons, aerial requests it with the tweaked values. Aerial does not know it’s tweaked, nor should it. It calls mini.icons to lookup the icon for  Function instead of Function. The disconnect? Despite mini.icons being the one who tweaked the lsp kind, it doesn’t handle fetching its own tweaked values. Maybe because the tweak function was made for mini.completion in mind? Not sure.

So mini.icons and any other icon provider will not expect be able to handle it and result in the icon being ?. Additionally, highlights that are made from the Symbol name will also be broken. This is what provided the error message listed above.

So what can we do? We need to provide a shim between aerial and mini.icons.

We add a configuration value for aerial.nvim to use a user-defined icon-provider instead. Add a callback option in aerial’s config

  -- Callback for custom icon provider.
  custom_icon_provider = nil,

Then when fetching the icon from the provider, prioritize the user defined one over the defaults.

local function get_icon_provider()
  if not M.use_icon_provider then -- skip if icon provider not used
    return false
  end
  if M.custom_icon_provider ~= nil then
    return M.custom_icon_provider
  end
  ...

For the icon provider, I found a similar solution in mini.extra where it was being used to provide the appropriate symbol after being tweaked for the LSP picker.

H.get_symbol_kind_map = function()
  -- Compute symbol kind map from "resolved" string kind to its "original" (as in
  -- LSP protocol). Those can be different after `MiniIcons.tweak_lsp_kind()`.
  local res = {}
  local double_map = vim.lsp.protocol.SymbolKind
  for k, v in pairs(double_map) do
    if type(k) == 'string' and type(v) == 'number' then res[double_map[v]] = k end
  end
  return res
end

This creates a table like the following:

{
  [" Event"] = "Event",
  [" Variable"] = "Variable",
  [" Array"] = "Array",
  [" Module"] = "Module",
  ...
}

By utilizing the tweaked symbol kind map instead of the default kind map, we can fetch the appropriate icon.

      local kind_map = Config._cachedSymbols
      return MiniIcons.get("lsp", kind_map[kind])

Ideally you should generate the tweaked symbol map once on startup and utilize that every time you look up the icon.

If done correctly the symbols should be properly showing up when opening aerial. There are two more things we need to do though to close it out.

  1. Address a similar issue with the highlights
  2. Handle non-LSP backend symbols.

Highlights can be addressed in a similar way by the following snippet:

    get_highlight = function(symbol, is_icon, is_collapsed)
      local kind_map = Config._cachedSymbols
      if kind_map == nil then
        return nil
      end
      local kind = kind_map[symbol.kind]
      -- If the symbol has a non-public scope, use that as the highlight group (e.g. AerialPrivate)
      if symbol.scope and not is_icon and symbol.scope ~= "public" then
        return string.format("Aerial%s", symbol.scope:gsub("^%l", string.upper))
      end

      local out = string.format("Aerial%s%s", kind, is_icon and "Icon" or "")
      return out
    end,

For non-LSP backend symbols, they will return non-tweaked names so our icon provider needs to also handle those. This can happen when you open aerial before the LSP has loaded and it fallsback to treesitter.

    custom_icon_provider = function(kind)
      local kind_map = Config._cachedSymbols
      if kind_map == nil then
        return ""
      end
      return MiniIcons.get("lsp", kind_map[kind] or kind)
    end,

That’s it! If you ever run into icon issues with other plugins and you are tweaking the LSP kind. You’ll know where to look.