feat(#2948): add custom decorators, :help nvim-tree-decorators (#2996)

* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator

* feat(#2948): add UserDecorator

* feat(#2948): add UserDecorator

* feat(#2948): add Decorator node icon override

* feat(#2948): add nvim_tree.api.* node classes

* feat(#2948): extract _meta following nvim pattern

* feat(#2948): extract _meta following nvim pattern

* feat(#2948): add decorator registry and order

* feat(#2948): add decorator registry and order

* feat(#2948): tidy

* feat(#2948): document API

* feat(#2948): document API

* feat(#2948): document API

* feat(#2948): pass api nodes to user decorators

* feat(#2948): document API

* feat(#2948): use renderer.decorators to define order and register

* feat(#2948): tidy decorator args and complete documentation

* feat(#2948): decorator classes specified by prefix rather than suffix

* feat(#2948): improve doc

* feat(#2948): improve doc

* feat(#2948): improve doc

* feat(#2948): additional user decorator safety

* feat(#2948): create nvim_tree.api.decorator.UserDecorator class in API, add :extend

* feat(#2948): improve doc
This commit is contained in:
Alexander Courtis
2024-12-07 16:03:29 +11:00
committed by GitHub
parent ca7c4c33ca
commit 7a4ff1a516
25 changed files with 571 additions and 322 deletions

View File

@@ -1,21 +1,21 @@
local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorBookmarks: Decorator
---@field icon HighlightedString?
local DecoratorBookmarks = Decorator:extend()
---@class (exact) BookmarkDecorator: Decorator
---@field private explorer Explorer
---@field private icon HighlightedString?
local BookmarkDecorator = Decorator:extend()
---@class DecoratorBookmarks
---@overload fun(explorer: DecoratorArgs): DecoratorBookmarks
---@class BookmarkDecorator
---@overload fun(args: DecoratorArgs): BookmarkDecorator
---@protected
---@param args DecoratorArgs
function DecoratorBookmarks:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_bookmarks or "none",
icon_placement = args.explorer.opts.renderer.icons.bookmarks_placement or "none",
})
function BookmarkDecorator:new(args)
self.explorer = args.explorer
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_bookmarks or "none"
self.icon_placement = self.explorer.opts.renderer.icons.bookmarks_placement or "none"
if self.explorer.opts.renderer.icons.show.bookmarks then
self.icon = {
@@ -28,8 +28,8 @@ end
---Bookmark icon: renderer.icons.show.bookmarks and node is marked
---@param node Node
---@return HighlightedString[]|nil icons
function DecoratorBookmarks:calculate_icons(node)
---@return HighlightedString[]? icons
function BookmarkDecorator:icons(node)
if self.explorer.marks:get(node) then
return { self.icon }
end
@@ -37,11 +37,11 @@ end
---Bookmark highlight: renderer.highlight_bookmarks and node is marked
---@param node Node
---@return string|nil group
function DecoratorBookmarks:calculate_highlight(node)
if self.range ~= "none" and self.explorer.marks:get(node) then
---@return string? highlight_group
function BookmarkDecorator:highlight_group(node)
if self.highlight_range ~= "none" and self.explorer.marks:get(node) then
return "NvimTreeBookmarkHL"
end
end
return DecoratorBookmarks
return BookmarkDecorator

View File

@@ -1,29 +1,29 @@
local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorCopied: Decorator
local DecoratorCopied = Decorator:extend()
---@class (exact) CopiedDecorator: Decorator
---@field private explorer Explorer
local CopiedDecorator = Decorator:extend()
---@class DecoratorCopied
---@overload fun(explorer: DecoratorArgs): DecoratorCopied
---@class CopiedDecorator
---@overload fun(args: DecoratorArgs): CopiedDecorator
---@protected
---@param args DecoratorArgs
function DecoratorCopied:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_clipboard or "none",
icon_placement = "none",
})
function CopiedDecorator:new(args)
self.explorer = args.explorer
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_clipboard or "none"
self.icon_placement = "none"
end
---Copied highlight: renderer.highlight_clipboard and node is copied
---@param node Node
---@return string|nil group
function DecoratorCopied:calculate_highlight(node)
if self.range ~= "none" and self.explorer.clipboard:is_copied(node) then
---@return string? highlight_group
function CopiedDecorator:highlight_group(node)
if self.highlight_range ~= "none" and self.explorer.clipboard:is_copied(node) then
return "NvimTreeCopiedHL"
end
end
return DecoratorCopied
return CopiedDecorator

View File

@@ -1,29 +1,29 @@
local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorCut: Decorator
local DecoratorCut = Decorator:extend()
---@class (exact) CutDecorator: Decorator
---@field private explorer Explorer
local CutDecorator = Decorator:extend()
---@class DecoratorCut
---@overload fun(explorer: DecoratorArgs): DecoratorCut
---@class CutDecorator
---@overload fun(args: DecoratorArgs): CutDecorator
---@protected
---@param args DecoratorArgs
function DecoratorCut:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_clipboard or "none",
icon_placement = "none",
})
function CutDecorator:new(args)
self.explorer = args.explorer
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_clipboard or "none"
self.icon_placement = "none"
end
---Cut highlight: renderer.highlight_clipboard and node is cut
---@param node Node
---@return string|nil group
function DecoratorCut:calculate_highlight(node)
if self.range ~= "none" and self.explorer.clipboard:is_cut(node) then
---@return string? highlight_group
function CutDecorator:highlight_group(node)
if self.highlight_range ~= "none" and self.explorer.clipboard:is_cut(node) then
return "NvimTreeCutHL"
end
end
return DecoratorCut
return CutDecorator

View File

@@ -30,58 +30,54 @@ local ICON_KEYS = {
["hint"] = vim.diagnostic.severity.HINT,
}
---@class (exact) DecoratorDiagnostics: Decorator
---@field icons HighlightedString[]?
local DecoratorDiagnostics = Decorator:extend()
---@class (exact) DiagnosticsDecorator: Decorator
---@field private explorer Explorer
---@field private diag_icons HighlightedString[]?
local DiagnosticsDecorator = Decorator:extend()
---@class DecoratorDiagnostics
---@overload fun(explorer: DecoratorArgs): DecoratorDiagnostics
---@class DiagnosticsDecorator
---@overload fun(args: DecoratorArgs): DiagnosticsDecorator
---@protected
---@param args DecoratorArgs
function DecoratorDiagnostics:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_diagnostics or "none",
icon_placement = args.explorer.opts.renderer.icons.diagnostics_placement or "none",
})
function DiagnosticsDecorator:new(args)
self.explorer = args.explorer
if not self.enabled then
return
end
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_diagnostics or "none"
self.icon_placement = self.explorer.opts.renderer.icons.diagnostics_placement or "none"
if self.explorer.opts.renderer.icons.show.diagnostics then
self.icons = {}
self.diag_icons = {}
for name, sev in pairs(ICON_KEYS) do
self.icons[sev] = {
self.diag_icons[sev] = {
str = self.explorer.opts.diagnostics.icons[name],
hl = { HG_ICON[sev] },
}
self:define_sign(self.icons[sev])
self:define_sign(self.diag_icons[sev])
end
end
end
---Diagnostic icon: diagnostics.enable, renderer.icons.show.diagnostics and node has status
---@param node Node
---@return HighlightedString[]|nil icons
function DecoratorDiagnostics:calculate_icons(node)
if node and self.enabled and self.icons then
---@return HighlightedString[]? icons
function DiagnosticsDecorator:icons(node)
if node and self.diag_icons then
local diag_status = diagnostics.get_diag_status(node)
local diag_value = diag_status and diag_status.value
if diag_value then
return { self.icons[diag_value] }
return { self.diag_icons[diag_value] }
end
end
end
---Diagnostic highlight: diagnostics.enable, renderer.highlight_diagnostics and node has status
---@param node Node
---@return string|nil group
function DecoratorDiagnostics:calculate_highlight(node)
if not node or not self.enabled or self.range == "none" then
---@return string? highlight_group
function DiagnosticsDecorator:highlight_group(node)
if self.highlight_range == "none" then
return nil
end
@@ -106,4 +102,4 @@ function DecoratorDiagnostics:calculate_highlight(node)
end
end
return DecoratorDiagnostics
return DiagnosticsDecorator

View File

@@ -3,7 +3,7 @@ local notify = require("nvim-tree.notify")
local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) GitHighlightedString: HighlightedString
---@class (exact) GitHighlightedString: nvim_tree.api.HighlightedString
---@field ord number decreasing priority
---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked"
@@ -12,31 +12,31 @@ local DirectoryNode = require("nvim-tree.node.directory")
---@alias GitIconsByXY table<GitXY, GitHighlightedString[]> porcelain status
---@alias GitGlyphsByStatus table<GitStatusStrings, string> from opts
---@class (exact) DecoratorGit: Decorator
---@field file_hl_by_xy table<GitXY, string>?
---@field folder_hl_by_xy table<GitXY, string>?
---@field icons_by_status GitIconsByStatus?
---@field icons_by_xy GitIconsByXY?
local DecoratorGit = Decorator:extend()
---@class (exact) GitDecorator: Decorator
---@field private explorer Explorer
---@field private file_hl_by_xy table<GitXY, string>?
---@field private folder_hl_by_xy table<GitXY, string>?
---@field private icons_by_status GitIconsByStatus?
---@field private icons_by_xy GitIconsByXY?
local GitDecorator = Decorator:extend()
---@class DecoratorGit
---@overload fun(explorer: DecoratorArgs): DecoratorGit
---@class GitDecorator
---@overload fun(args: DecoratorArgs): GitDecorator
---@protected
---@param args DecoratorArgs
function DecoratorGit:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = args.explorer.opts.git.enable,
hl_pos = args.explorer.opts.renderer.highlight_git or "none",
icon_placement = args.explorer.opts.renderer.icons.git_placement or "none",
})
function GitDecorator:new(args)
self.explorer = args.explorer
self.enabled = self.explorer.opts.git.enable
self.highlight_range = self.explorer.opts.renderer.highlight_git or "none"
self.icon_placement = self.explorer.opts.renderer.icons.git_placement or "none"
if not self.enabled then
return
end
if self.range ~= "none" then
if self.highlight_range ~= "none" then
self:build_file_folder_hl_by_xy()
end
@@ -51,7 +51,7 @@ function DecoratorGit:new(args)
end
---@param glyphs GitGlyphsByStatus
function DecoratorGit:build_icons_by_status(glyphs)
function GitDecorator:build_icons_by_status(glyphs)
self.icons_by_status = {}
self.icons_by_status.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }
self.icons_by_status.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }
@@ -63,7 +63,7 @@ function DecoratorGit:build_icons_by_status(glyphs)
end
---@param icons GitIconsByXY
function DecoratorGit:build_icons_by_xy(icons)
function GitDecorator:build_icons_by_xy(icons)
self.icons_by_xy = {
["M "] = { icons.staged },
[" M"] = { icons.unstaged },
@@ -100,7 +100,7 @@ function DecoratorGit:build_icons_by_xy(icons)
}
end
function DecoratorGit:build_file_folder_hl_by_xy()
function GitDecorator:build_file_folder_hl_by_xy()
self.file_hl_by_xy = {
["M "] = "NvimTreeGitFileStagedHL",
["C "] = "NvimTreeGitFileStagedHL",
@@ -142,9 +142,9 @@ end
---Git icons: git.enable, renderer.icons.show.git and node has status
---@param node Node
---@return HighlightedString[]|nil modified icon
function DecoratorGit:calculate_icons(node)
if not node or not self.enabled or not self.icons_by_xy then
---@return HighlightedString[]? icons
function GitDecorator:icons(node)
if not self.icons_by_xy then
return nil
end
@@ -159,7 +159,7 @@ function DecoratorGit:calculate_icons(node)
for _, s in pairs(git_xy) do
local icons = self.icons_by_xy[s]
if not icons then
if self.range == "none" then
if self.highlight_range == "none" then
notify.warn(string.format("Unrecognized git state '%s'", git_xy))
end
return nil
@@ -190,12 +190,12 @@ end
---Get the first icon as the sign if appropriate
---@param node Node
---@return string|nil name
function DecoratorGit:sign_name(node)
function GitDecorator:sign_name(node)
if self.icon_placement ~= "signcolumn" then
return
end
local icons = self:calculate_icons(node)
local icons = self:icons(node)
if icons and #icons > 0 then
return icons[1].hl[1]
end
@@ -203,9 +203,9 @@ end
---Git highlight: git.enable, renderer.highlight_git and node has status
---@param node Node
---@return string|nil group
function DecoratorGit:calculate_highlight(node)
if not node or not self.enabled or self.range == "none" then
---@return string? highlight_group
function GitDecorator:highlight_group(node)
if self.highlight_range == "none" then
return nil
end
@@ -221,4 +221,4 @@ function DecoratorGit:calculate_highlight(node)
end
end
return DecoratorGit
return GitDecorator

View File

@@ -1,22 +1,22 @@
local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorHidden: Decorator
---@field icon HighlightedString?
local DecoratorHidden = Decorator:extend()
---@class (exact) HiddenDecorator: Decorator
---@field private explorer Explorer
---@field private icon HighlightedString?
local HiddenDecorator = Decorator:extend()
---@class DecoratorHidden
---@overload fun(explorer: DecoratorArgs): DecoratorHidden
---@class HiddenDecorator
---@overload fun(args: DecoratorArgs): HiddenDecorator
---@protected
---@param args DecoratorArgs
function DecoratorHidden:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_hidden or "none",
icon_placement = args.explorer.opts.renderer.icons.hidden_placement or "none",
})
function HiddenDecorator:new(args)
self.explorer = args.explorer
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_hidden or "none"
self.icon_placement = self.explorer.opts.renderer.icons.hidden_placement or "none"
if self.explorer.opts.renderer.icons.show.hidden then
self.icon = {
@@ -29,18 +29,18 @@ end
---Hidden icon: renderer.icons.show.hidden and node starts with `.` (dotfile).
---@param node Node
---@return HighlightedString[]|nil icons
function DecoratorHidden:calculate_icons(node)
if self.enabled and node:is_dotfile() then
---@return HighlightedString[]? icons
function HiddenDecorator:icons(node)
if node:is_dotfile() then
return { self.icon }
end
end
---Hidden highlight: renderer.highlight_hidden and node starts with `.` (dotfile).
---@param node Node
---@return string|nil group
function DecoratorHidden:calculate_highlight(node)
if not self.enabled or self.range == "none" or not node:is_dotfile() then
---@return string? highlight_group
function HiddenDecorator:highlight_group(node)
if self.highlight_range == "none" or not node:is_dotfile() then
return nil
end
@@ -51,4 +51,4 @@ function DecoratorHidden:calculate_highlight(node)
end
end
return DecoratorHidden
return HiddenDecorator

View File

@@ -1,48 +1,52 @@
local Class = require("nvim-tree.classic")
---@alias DecoratorRange "none" | "icon" | "name" | "all"
---@alias DecoratorIconPlacement "none" | "before" | "after" | "signcolumn" | "right_align"
---Abstract Decorator
---Uses the factory pattern to instantiate child instances.
---@class (exact) Decorator: Class
---@field protected explorer Explorer
---@field protected enabled boolean
---@field protected range DecoratorRange
---@field protected icon_placement DecoratorIconPlacement
---@field protected highlight_range nvim_tree.api.decorator.HighlightRange
---@field protected icon_placement nvim_tree.api.decorator.IconPlacement
local Decorator = Class:extend()
---@class (exact) DecoratorArgs
---@field explorer Explorer
---@class (exact) AbstractDecoratorArgs: DecoratorArgs
---@field enabled boolean
---@field hl_pos DecoratorRange
---@field icon_placement DecoratorIconPlacement
---@protected
---@param args AbstractDecoratorArgs
function Decorator:new(args)
self.explorer = args.explorer
self.enabled = args.enabled
self.range = args.hl_pos
self.icon_placement = args.icon_placement
---Abstract icon override, optionally implemented
---@param node Node
---@return HighlightedString? icon_node
function Decorator:icon_node(node)
return self:nop(node)
end
---Maybe highlight groups
---Abstract icons, optionally implemented
---@protected
---@param node Node
---@return string|nil icon highlight group
---@return string|nil name highlight group
function Decorator:groups_icon_name(node)
---@return HighlightedString[]? icons
function Decorator:icons(node)
self:nop(node)
end
---Abstract highlight group, optionally implemented
---@protected
---@param node Node
---@return string? highlight_group
function Decorator:highlight_group(node)
self:nop(node)
end
---Maybe highlight groups for icon and name
---@param node Node
---@return string? icon highlight group
---@return string? name highlight group
function Decorator:highlight_group_icon_name(node)
local icon_hl, name_hl
if self.enabled and self.range ~= "none" then
local hl = self:calculate_highlight(node)
if self.enabled and self.highlight_range ~= "none" then
local hl = self:highlight_group(node)
if self.range == "all" or self.range == "icon" then
if self.highlight_range == "all" or self.highlight_range == "icon" then
icon_hl = hl
end
if self.range == "all" or self.range == "name" then
if self.highlight_range == "all" or self.highlight_range == "name" then
name_hl = hl
end
end
@@ -52,13 +56,13 @@ end
---Maybe icon sign
---@param node Node
---@return string|nil name
---@return string? name
function Decorator:sign_name(node)
if not self.enabled or self.icon_placement ~= "signcolumn" then
return
end
local icons = self:calculate_icons(node)
local icons = self:icons(node)
if icons and #icons > 0 then
return icons[1].hl[1]
end
@@ -66,56 +70,40 @@ end
---Icons when "before"
---@param node Node
---@return HighlightedString[]|nil icons
---@return HighlightedString[]? icons
function Decorator:icons_before(node)
if not self.enabled or self.icon_placement ~= "before" then
return
end
return self:calculate_icons(node)
return self:icons(node)
end
---Icons when "after"
---@param node Node
---@return HighlightedString[]|nil icons
---@return HighlightedString[]? icons
function Decorator:icons_after(node)
if not self.enabled or self.icon_placement ~= "after" then
return
end
return self:calculate_icons(node)
return self:icons(node)
end
---Icons when "right_align"
---@param node Node
---@return HighlightedString[]|nil icons
---@return HighlightedString[]? icons
function Decorator:icons_right_align(node)
if not self.enabled or self.icon_placement ~= "right_align" then
return
end
return self:calculate_icons(node)
end
---Maybe icons, optionally implemented
---@protected
---@param _ Node
---@return HighlightedString[]|nil icons
function Decorator:calculate_icons(_)
return nil
end
---Maybe highlight group, optionally implemented
---@protected
---@param _ Node
---@return string|nil group
function Decorator:calculate_highlight(_)
return nil
return self:icons(node)
end
---Define a sign
---@protected
---@param icon HighlightedString|nil
---@param icon HighlightedString?
function Decorator:define_sign(icon)
if icon and #icon.hl > 0 then
local name = icon.hl[1]

View File

@@ -3,26 +3,22 @@ local buffers = require("nvim-tree.buffers")
local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorModified: Decorator
---@field icon HighlightedString?
local DecoratorModified = Decorator:extend()
---@class (exact) ModifiedDecorator: Decorator
---@field private explorer Explorer
---@field private icon HighlightedString?
local ModifiedDecorator = Decorator:extend()
---@class DecoratorModified
---@overload fun(explorer: DecoratorArgs): DecoratorModified
---@class ModifiedDecorator
---@overload fun(args: DecoratorArgs): ModifiedDecorator
---@protected
---@param args DecoratorArgs
function DecoratorModified:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_modified or "none",
icon_placement = args.explorer.opts.renderer.icons.modified_placement or "none",
})
function ModifiedDecorator:new(args)
self.explorer = args.explorer
if not self.enabled then
return
end
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_modified or "none"
self.icon_placement = self.explorer.opts.renderer.icons.modified_placement or "none"
if self.explorer.opts.renderer.icons.show.modified then
self.icon = {
@@ -35,18 +31,18 @@ end
---Modified icon: modified.enable, renderer.icons.show.modified and node is modified
---@param node Node
---@return HighlightedString[]|nil icons
function DecoratorModified:calculate_icons(node)
if self.enabled and buffers.is_modified(node) then
---@return HighlightedString[]? icons
function ModifiedDecorator:icons(node)
if buffers.is_modified(node) then
return { self.icon }
end
end
---Modified highlight: modified.enable, renderer.highlight_modified and node is modified
---@param node Node
---@return string|nil group
function DecoratorModified:calculate_highlight(node)
if not self.enabled or self.range == "none" or not buffers.is_modified(node) then
---@return string? highlight_group
function ModifiedDecorator:highlight_group(node)
if self.highlight_range == "none" or not buffers.is_modified(node) then
return nil
end
@@ -57,4 +53,4 @@ function DecoratorModified:calculate_highlight(node)
end
end
return DecoratorModified
return ModifiedDecorator

View File

@@ -2,31 +2,31 @@ local buffers = require("nvim-tree.buffers")
local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorOpened: Decorator
---@field icon HighlightedString|nil
local DecoratorOpened = Decorator:extend()
---@class (exact) OpenDecorator: Decorator
---@field private explorer Explorer
---@field private icon HighlightedString|nil
local OpenDecorator = Decorator:extend()
---@class DecoratorOpened
---@overload fun(explorer: DecoratorArgs): DecoratorOpened
---@class OpenDecorator
---@overload fun(args: DecoratorArgs): OpenDecorator
---@protected
---@param args DecoratorArgs
function DecoratorOpened:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_opened_files or "none",
icon_placement = "none",
})
function OpenDecorator:new(args)
self.explorer = args.explorer
self.enabled = true
self.highlight_range = self.explorer.opts.renderer.highlight_opened_files or "none"
self.icon_placement = "none"
end
---Opened highlight: renderer.highlight_opened_files and node has an open buffer
---@param node Node
---@return string|nil group
function DecoratorOpened:calculate_highlight(node)
if self.range ~= "none" and buffers.is_opened(node) then
---@return string? highlight_group
function OpenDecorator:highlight_group(node)
if self.highlight_range ~= "none" and buffers.is_opened(node) then
return "NvimTreeOpenedHL"
end
end
return DecoratorOpened
return OpenDecorator

View File

@@ -0,0 +1,7 @@
local Decorator = require("nvim-tree.renderer.decorator")
---Exposed as nvim_tree.api.decorator.UserDecorator
---@class (exact) UserDecorator: Decorator
local UserDecorator = Decorator:extend()
return UserDecorator