Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] User Decorators #2948

Open
b0o opened this issue Oct 10, 2024 · 10 comments
Open

[Feature] User Decorators #2948

b0o opened this issue Oct 10, 2024 · 10 comments

Comments

@b0o
Copy link

b0o commented Oct 10, 2024

I would love to have an API for defining custom decorators. My use case is to add an icon/highlight for nodes which are in the quickfix list. I've come up with a hacky way to accomplish this, by overriding the bookmarks decorator:

-- Important: Put this in your config directory at lua/nvim-tree/renderer/decorator/bookmarks.lua
--
-- HACK: Although this file is named bookmarks.lua, it is actually a decorator
-- for the quickfix list. Because nvim-tree does not provide a way to add a custom decorator,
-- we are overriding the built-in bookmarks decorator with our own quickfix list decorator.

local HL_POSITION = require('nvim-tree.enum').HL_POSITION
local ICON_PLACEMENT = require('nvim-tree.enum').ICON_PLACEMENT

local Decorator = require 'nvim-tree.renderer.decorator'

---@class (exact) DecoratorQuickfix: Decorator
---@field icon HighlightedString|nil
local DecoratorQuickfix = Decorator:new()

local augroup = vim.api.nvim_create_augroup('nvim-tree-decorator-quickfix', { clear = true })

---@return DecoratorQuickfix
function DecoratorQuickfix:new(opts, explorer)
  local o = Decorator.new(self, {
    explorer = explorer,
    enabled = true,
    hl_pos = HL_POSITION[opts.renderer.highlight_bookmarks] or HL_POSITION.none,
    icon_placement = ICON_PLACEMENT[opts.renderer.icons.bookmarks_placement] or ICON_PLACEMENT.none,
  })
  ---@cast o DecoratorQuickfix
  if not o.enabled then
    return o
  end
  o.icon = {
    str = '',
    hl = { 'QuickFixLine' },
  }
  o:define_sign(o.icon)
  vim.api.nvim_create_autocmd('QuickfixCmdPost', {
    group = augroup,
    callback = function()
      explorer.renderer:draw()
    end,
  })
  return o
end

---@param node Node
local function is_qf_item(node)
  if node.name == '..' or node.type == 'directory' then
    return false
  end
  local bufnr = vim.fn.bufnr(node.absolute_path)
  return bufnr ~= -1 and vim.iter(vim.fn.getqflist()):any(function(qf)
    return qf.bufnr == bufnr
  end)
end

---@param node Node
---@return HighlightedString[]|nil icons
function DecoratorQuickfix:calculate_icons(node)
  if is_qf_item(node) then
    return { self.icon }
  end
end

---@param node Node
---@return string|nil group
function DecoratorQuickfix:calculate_highlight(node)
  if is_qf_item(node) then
    return 'QuickFixLine'
  end
end

It would be nice if there was an official way to do this.

@alex-courtis
Copy link
Member

That's a great idea! There's nothing blocking us from doing this:

Add a renderer.decorators option, which would take a table of the user's decorator class.

Instantiate them with highest priority.

Make the Decorator abstract class user friendly, with documentation on what must be implemented etc.

Add help entries that just direct to the Decorator class documentation which is the one and only source of truth.

Add DecoratorQuickfix recipe.

@alex-courtis
Copy link
Member

alex-courtis commented Oct 11, 2024

@alex-courtis alex-courtis added the PR please nvim-tree team does not have the bandwidth to implement; a PR will be gratefully appreciated label Oct 11, 2024
@alex-courtis
Copy link
Member

Safer approach:

Create a UserDecorator class, exposed and documented via a new API function, say, api.appearance.decorator.add.

This class will be similar to Decorator however:

  • will not expose internals (explorer)
  • provide a sanitized node
  • define explicit methods that must be implemented

@alex-courtis
Copy link
Member

alex-courtis commented Oct 20, 2024

Challenges:

  • Creating sanitized nodes is expensive and large. A sanitized node will be needed for every call to the decorator i.e. every node visible.

@alex-courtis
Copy link
Member

Migrating to classic to allow simple class instantiation and privacy: #2991

@alex-courtis alex-courtis changed the title [Feature] Custom Decorators Support [Feature] User Decorators Nov 9, 2024
@alex-courtis
Copy link
Member

A proof-of-concept for user decorators has been created: #2996

I'd be most grateful if you tested this, instructions to follow.

cd /path/to/nvim-tree.lua
git pull 2948-add-user-decorators
git checkout 

When you're finished testing:

git checkout master

@alex-courtis
Copy link
Member

alex-courtis commented Nov 9, 2024

renderer.user_decorators may be specified. They area references to your custom class.

Please note that this is a proof of concept for now. TODO:

  • Proper documentation
  • safety
  • specifying order
  • automatically handling sign creation; it's inefficient and obtuse

Example decorator class to highlight and add an icon to files named "example":

local UserDecorator = require("nvim-tree.renderer.decorator.user")

---A string with one or more highlight groups applied to it
---@class (exact) HighlightedString
---@field str string
---@field hl string[] highlight groups applied in order

---Custom user decorators must inherit from UserDecorator
---@class (exact) UserDecoratorExample: UserDecorator
---@field private my_icon HighlightedString
local UserDecoratorExample = UserDecorator:extend()

---Constructor will be called once per tree render, with no arguments.
function UserDecoratorExample:new()
  UserDecoratorExample.super.new(self, {
    enabled        = true,
    hl_pos         = "name",
    icon_placement = "right_align",
  })

  -- create your icon once, for convenience
  self.my_icon = { str = "E", hl = { "ExampleIcon" } }

  -- Define the icon sign only once
  -- Only needed if you are using icon_placement = "signcolumn"
  -- self:define_sign(self.my_icon)
end

---@param node Node
---@return HighlightedString[]|nil icons
function UserDecoratorExample:calculate_icons(node)
  if node.name == "example" then
    return { self.my_icon }
  else
    return nil
  end
end

---@param node Node
---@return string|nil group
function UserDecoratorExample:calculate_highlight(node)
  if node.name == "example" then
    return "ExampleHighlight"
  else
    return nil
  end
end

Registering it:

require("nvim-tree").setup({
  renderer = {
    user_decorators = {
      {
        class = UserDecoratorExample,
      },
    },
  },
})

@alex-courtis
Copy link
Member

@b0o and @DMyashkov I would be most grateful for your testing and feedback.

I'm open to any and all ideas and changes.

@alex-courtis alex-courtis removed the PR please nvim-tree team does not have the bandwidth to implement; a PR will be gratefully appreciated label Nov 9, 2024
@alex-courtis
Copy link
Member

  vim.api.nvim_create_autocmd('QuickfixCmdPost', {
    group = augroup,
    callback = function()
      explorer.renderer:draw()
    end,
  })

@b0o you won't have access to explorer, however you will be able to use API: tree.reload()

@b0o
Copy link
Author

b0o commented Nov 13, 2024

Sorry for the delay. This is really awesome! I've converted my quickfix decorator and it seems to be working great:

local UserDecorator = require 'nvim-tree.renderer.decorator.user'

---A string with one or more highlight groups applied to it
---@class (exact) HighlightedString
---@field str string
---@field hl string[] highlight groups applied in order

---Quickfix decorator
---@class (exact) DecoratorQuickfix: UserDecorator
---@field private qf_icon HighlightedString
local DecoratorQuickfix = UserDecorator:extend()

local augroup = vim.api.nvim_create_augroup('nvim-tree-decorator-quickfix', { clear = true })

function DecoratorQuickfix:new()
  DecoratorQuickfix.super.new(self, {
    enabled = true,
    hl_pos = 'name',
    icon_placement = 'signcolumn',
  })

  self.qf_icon = { str = '', hl = { 'QuickFixLine' } }

  self:define_sign(self.qf_icon)

  vim.api.nvim_create_autocmd('QuickfixCmdPost', {
    group = augroup,
    callback = function()
      require('nvim-tree.api').tree.reload()
    end,
  })

  vim.api.nvim_create_autocmd('FileType', {
    pattern = 'qf',
    group = augroup,
    callback = function(evt)
      vim.api.nvim_create_autocmd('TextChanged', {
        buffer = evt.buf,
        group = augroup,
        callback = function()
          require('nvim-tree.api').tree.reload()
        end,
      })
    end,
  })
end

---@param node Node
local function is_qf_item(node)
  if node.name == '..' or node.type == 'directory' then
    return false
  end
  local bufnr = vim.fn.bufnr(node.absolute_path)
  return bufnr ~= -1 and vim.iter(vim.fn.getqflist()):any(function(qf)
    return qf.bufnr == bufnr
  end)
end

---@param node Node
---@return HighlightedString[]|nil icons
---@diagnostic disable-next-line: duplicate-set-field
function DecoratorQuickfix:calculate_icons(node)
  if is_qf_item(node) then
    return { self.qf_icon }
  end
  return nil
end

---@param node Node
---@return string|nil group
---@diagnostic disable-next-line: duplicate-set-field
function DecoratorQuickfix:calculate_highlight(node)
  if is_qf_item(node) then
    return 'QuickFixLine'
  end
  return nil
end

return DecoratorQuickfix

Only issue is that this line seems to be causing this error:

...im/lazy/nvim-tree.lua/lua/nvim-tree/renderer/builder.lua:16: module 'nvim-tree.renderer.decorator.example' not found

Thank you @alex-courtis, this opens up a lot of cool possibilities for other custom decorators!

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

No branches or pull requests

2 participants