feat(view): indicate modified buffers (#1835)

* Outlined new options

* highlight_modified is highlight_opened_files

* prototype with autocmd

* moved modified into glyphs

* show_on_dirs and show_on_open_dirs

* icon placement before & after

* _get_filename_offset

* fixed :wq doesn't update modified indicator

* highlight_modified, signcolumn modified_placement

Refactored to make everything use HighlightedString to remove all the complex `insert_highlight` calculation.
Not tested.

* updated doc to match the reality of no multi char for glyphs.modified

* fixed git signcolumn doesn't show

* fixed highlight_modified gets replaced by highlight_opened_files

* fixed renderer.icons.show.modified = false crash

* updated doc to reflect empty icon not breaking rendering

* removed debounce_delay to implement in a later PR

* doc nit: order placement

* change modified dirs default to be consistent with git

* illegal git & modified placement changed to default

* don't assume icon exist

* nit remove comment

* Noted in doc that glyphs can't have more than 2 characters if in signcolumn

* Don't sign_define if placement isn't signcolumn

Co-authored-by: Alexander Courtis <alex@courtis.org>
This commit is contained in:
Richard Li
2022-12-31 17:54:04 +13:00
committed by GitHub
parent 9ad93b6ac0
commit dcc344cc72
10 changed files with 381 additions and 146 deletions

View File

@@ -4,6 +4,7 @@ local core = require "nvim-tree.core"
local git = require "nvim-tree.renderer.components.git"
local pad = require "nvim-tree.renderer.components.padding"
local icons = require "nvim-tree.renderer.components.icons"
local modified = require "nvim-tree.renderer.components.modified"
local Builder = {}
Builder.__index = Builder
@@ -50,21 +51,32 @@ end
function Builder:configure_opened_file_highlighting(highlight_opened_files)
self.highlight_opened_files = highlight_opened_files
return self
end
function Builder:configure_git_icons_padding(padding)
self.git_icon_padding = padding or " "
function Builder:configure_modified_highlighting(highlight_modified)
self.highlight_modified = highlight_modified
return self
end
function Builder:configure_icon_padding(padding)
self.icon_padding = padding or " "
return self
end
function Builder:configure_git_icons_placement(where)
if where == "signcolumn" then
vim.fn.sign_unplace(git.SIGN_GROUP)
self.is_git_sign = true
if where ~= "after" and where ~= "before" and where ~= "signcolumn" then
where = "before" -- default before
end
self.is_git_after = where == "after" and not self.is_git_sign
self.git_placement = where
return self
end
function Builder:configure_modified_placement(where)
if where ~= "after" and where ~= "before" and where ~= "signcolumn" then
where = "after" -- default after
end
self.modified_placement = where
return self
end
@@ -91,46 +103,48 @@ local function get_folder_name(node)
return name
end
function Builder:_unwrap_git_data(git_icons_and_hl_groups, offset)
if not git_icons_and_hl_groups then
---@class HighlightedString
---@field str string
---@field hl string|nil
---@param highlighted_strings HighlightedString[]
---@return string
function Builder:_unwrap_highlighted_strings(highlighted_strings)
if not highlighted_strings then
return ""
end
local icon = ""
for i, v in ipairs(git_icons_and_hl_groups) do
if #v.icon > 0 then
self:_insert_highlight(v.hl, offset + #icon, offset + #icon + #v.icon)
local remove_padding = self.is_git_after and i == #git_icons_and_hl_groups
icon = icon .. v.icon .. (remove_padding and "" or self.git_icon_padding)
local string = ""
for _, v in ipairs(highlighted_strings) do
if #v.str > 0 then
if v.hl then
self:_insert_highlight(v.hl, #string, #string + #v.str)
end
string = string .. v.str
end
end
return icon
return string
end
function Builder:_build_folder(node, padding, git_hl, git_icons_tbl)
local offset = string.len(padding)
local name = get_folder_name(node)
---@param node table
---@return HighlightedString icon, HighlightedString name
function Builder:_build_folder(node)
local has_children = #node.nodes ~= 0 or node.has_children
local icon = icons.get_folder_icon(node.open, node.link_to ~= nil, has_children)
local foldername = name .. self.trailing_slash
local foldername = get_folder_name(node) .. self.trailing_slash
if node.link_to and self.symlink_destination then
local arrow = icons.i.symlink_arrow
local link_to = utils.path_relative(node.link_to, core.get_cwd())
foldername = foldername .. arrow .. link_to
end
local git_icons = self:_unwrap_git_data(git_icons_tbl, offset + #icon + (self.is_git_after and #foldername + 1 or 0))
local fname_starts_at = offset + #icon + (self.is_git_after and 0 or #git_icons)
local line = self:_format_line(padding .. icon, foldername, git_icons)
self:_insert_line(line)
local icon_hl
if #icon > 0 then
if node.open then
self:_insert_highlight("NvimTreeOpenedFolderIcon", offset, offset + #icon)
icon_hl = "NvimTreeOpenedFolderIcon"
else
self:_insert_highlight("NvimTreeClosedFolderIcon", offset, offset + #icon)
icon_hl = "NvimTreeClosedFolderIcon"
end
end
@@ -143,27 +157,12 @@ function Builder:_build_folder(node, padding, git_hl, git_icons_tbl)
foldername_hl = "NvimTreeEmptyFolderName"
end
self:_insert_highlight(foldername_hl, fname_starts_at, fname_starts_at + #foldername)
if git_hl then
self:_insert_highlight(git_hl, fname_starts_at, fname_starts_at + #foldername)
end
return { str = icon, hl = icon_hl }, { str = foldername, hl = foldername_hl }
end
function Builder:_format_line(before, after, git_icons)
git_icons = self.is_git_after and git_icons and " " .. git_icons or git_icons
return string.format(
"%s%s%s%s",
before,
self.is_git_after and "" or git_icons,
after,
self.is_git_after and git_icons or ""
)
end
function Builder:_build_symlink(node, padding, git_highlight, git_icons_tbl)
local offset = string.len(padding)
---@param node table
---@return HighlightedString icon, HighlightedString name
function Builder:_build_symlink(node)
local icon = icons.i.symlink
local arrow = icons.i.symlink_arrow
local symlink_formatted = node.name
@@ -172,107 +171,173 @@ function Builder:_build_symlink(node, padding, git_highlight, git_icons_tbl)
symlink_formatted = symlink_formatted .. arrow .. link_to
end
local link_highlight = git_highlight or "NvimTreeSymlink"
local link_highlight = "NvimTreeSymlink"
local git_icons_starts_at = offset + #icon + (self.is_git_after and #symlink_formatted + 1 or 0)
local git_icons = self:_unwrap_git_data(git_icons_tbl, git_icons_starts_at)
local line = self:_format_line(padding .. icon, symlink_formatted, git_icons)
self:_insert_highlight(link_highlight, offset + (self.is_git_after and 0 or #git_icons), string.len(line))
self:_insert_line(line)
return { str = icon }, { str = symlink_formatted, hl = link_highlight }
end
function Builder:_build_file_icon(node, offset)
---@param node table
---@return HighlightedString icon
function Builder:_build_file_icon(node)
local icon, hl_group = icons.get_file_icon(node.name, node.extension)
if hl_group then
self:_insert_highlight(hl_group, offset, offset + #icon)
end
return icon, false
return { str = icon, hl = hl_group }
end
function Builder:_highlight_opened_files(node, offset, icon_length, git_icons_length)
local from = offset
local to = offset
if self.highlight_opened_files == "icon" then
to = from + icon_length
elseif self.highlight_opened_files == "name" then
from = offset + icon_length + git_icons_length
to = from + #node.name
elseif self.highlight_opened_files == "all" then
to = from + icon_length + git_icons_length + #node.name
end
self:_insert_highlight("NvimTreeOpenedFile", from, to)
end
function Builder:_build_file(node, padding, git_highlight, git_icons_tbl, unloaded_bufnr)
local offset = string.len(padding)
local icon = self:_build_file_icon(node, offset)
local git_icons_starts_at = offset + #icon + (self.is_git_after and #node.name + 1 or 0)
local git_icons = self:_unwrap_git_data(git_icons_tbl, git_icons_starts_at)
self:_insert_line(self:_format_line(padding .. icon, node.name, git_icons))
local git_icons_length = self.is_git_after and 0 or #git_icons
local col_start = offset + #icon + git_icons_length
local col_end = col_start + #node.name
---@param node table
---@return HighlightedString icon, HighlightedString name
function Builder:_build_file(node)
local icon = self:_build_file_icon(node)
local hl
if vim.tbl_contains(self.special_files, node.absolute_path) or vim.tbl_contains(self.special_files, node.name) then
self:_insert_highlight("NvimTreeSpecialFile", col_start, col_end)
hl = "NvimTreeSpecialFile"
elseif node.executable then
self:_insert_highlight("NvimTreeExecFile", col_start, col_end)
hl = "NvimTreeExecFile"
elseif self.picture_map[node.extension] then
self:_insert_highlight("NvimTreeImageFile", col_start, col_end)
hl = "NvimTreeImageFile"
end
local should_highlight_opened_files = self.highlight_opened_files
return icon, { str = node.name, hl = hl }
end
---@param node table
---@return HighlightedString[]|nil icon
function Builder:_get_git_icons(node)
local git_icons = git.get_icons(node)
if git_icons and #git_icons > 0 and self.git_placement == "signcolumn" then
local sign = git_icons[1]
table.insert(self.signs, { sign = sign.hl, lnum = self.index + 1, priority = 1 })
git_icons = nil
end
return git_icons
end
---@param node table
---@return HighlightedString|nil icon
function Builder:_get_modified_icon(node)
local modified_icon = modified.get_icon(node)
if modified_icon and self.modified_placement == "signcolumn" then
local sign = modified_icon
table.insert(self.signs, { sign = sign.hl, lnum = self.index + 1, priority = 3 })
modified_icon = nil
end
return modified_icon
end
---@param node table
---@return string icon_highlight, string name_highlight
function Builder:_get_highlight_override(node, unloaded_bufnr)
-- highlights precedence:
-- original < git < opened_file < modified
local name_hl, icon_hl
-- git
local git_highlight = git.get_highlight(node)
if git_highlight then
name_hl = git_highlight
end
-- opened file
if
self.highlight_opened_files
and vim.fn.bufloaded(node.absolute_path) > 0
and vim.fn.bufnr(node.absolute_path) ~= unloaded_bufnr
if should_highlight_opened_files then
self:_highlight_opened_files(node, offset, #icon, git_icons_length)
end
if git_highlight then
self:_insert_highlight(git_highlight, col_start, col_end)
end
end
function Builder:_build_line(node, idx, num_children, unloaded_bufnr)
local padding = pad.get_padding(self.depth, idx, num_children, node, self.markers)
if string.len(padding) > 0 then
self:_insert_highlight("NvimTreeIndentMarker", 0, string.len(padding))
end
local git_highlight = git.get_highlight(node)
local git_icons_tbl = git.get_icons(node)
if git_icons_tbl and #git_icons_tbl > 0 then
if self.is_git_sign then
local git_info = git_icons_tbl[1]
table.insert(self.signs, { sign = git_info.hl, lnum = self.index + 1 })
git_icons_tbl = {}
else
-- sort icons so it looks slightly better
table.sort(git_icons_tbl, function(a, b)
return a.ord < b.ord
end)
then
if self.highlight_opened_files == "all" or self.highlight_opened_files == "name" then
name_hl = "NvimTreeOpenedFile"
end
if self.highlight_opened_files == "all" or self.highlight_opened_files == "icon" then
icon_hl = "NvimTreeOpenedFile"
end
end
-- modified file
local modified_highlight = modified.get_highlight(node)
if modified_highlight then
if self.highlight_modified == "all" or self.highlight_modified == "name" then
name_hl = modified_highlight
end
if self.highlight_modified == "all" or self.highlight_modified == "icon" then
icon_hl = modified_highlight
end
end
return icon_hl, name_hl
end
---@param padding HighlightedString
---@param icon HighlightedString
---@param name HighlightedString
---@param git_icons HighlightedString[]|nil
---@param modified_icon HighlightedString|nil
---@return HighlightedString[]
function Builder:_format_line(padding, icon, name, git_icons, modified_icon)
local added_len = 0
local function add_to_end(t1, t2)
for _, v in ipairs(t2) do
if added_len > 0 then
table.insert(t1, { str = self.icon_padding })
end
table.insert(t1, v)
end
-- first add_to_end don't need padding
-- hence added_len is calculated at the end to be used next time
added_len = 0
for _, v in ipairs(t2) do
added_len = added_len + #v.str
end
end
local line = { padding }
add_to_end(line, { icon })
if git_icons and self.git_placement == "before" then
add_to_end(line, git_icons)
end
if modified_icon and self.modified_placement == "before" then
add_to_end(line, { modified_icon })
end
add_to_end(line, { name })
if git_icons and self.git_placement == "after" then
add_to_end(line, git_icons)
end
if modified_icon and self.modified_placement == "after" then
add_to_end(line, { modified_icon })
end
return line
end
function Builder:_build_line(node, idx, num_children, unloaded_bufnr)
-- various components
local padding = pad.get_padding(self.depth, idx, num_children, node, self.markers)
local git_icons = self:_get_git_icons(node)
local modified_icon = self:_get_modified_icon(node)
-- main components
local is_folder = node.nodes ~= nil
local is_symlink = node.link_to ~= nil
local icon, name
if is_folder then
self:_build_folder(node, padding, git_highlight, git_icons_tbl)
icon, name = self:_build_folder(node)
elseif is_symlink then
self:_build_symlink(node, padding, git_highlight, git_icons_tbl)
icon, name = self:_build_symlink(node)
else
self:_build_file(node, padding, git_highlight, git_icons_tbl, unloaded_bufnr)
icon, name = self:_build_file(node)
end
-- highlight override
local icon_hl, name_hl = self:_get_highlight_override(node, unloaded_bufnr)
if icon_hl then
icon.hl = icon_hl
end
if name_hl then
name.hl = name_hl
end
local line = self:_format_line(padding, icon, name, git_icons, modified_icon)
self:_insert_line(self:_unwrap_highlighted_strings(line))
self.index = self.index + 1
if node.open then

View File

@@ -1,19 +1,17 @@
local notify = require "nvim-tree.notify"
local explorer_node = require "nvim-tree.explorer.node"
local M = {
SIGN_GROUP = "NvimTreeGitSigns",
}
local M = {}
local function build_icons_table(i)
local icons = {
staged = { icon = i.staged, hl = "NvimTreeGitStaged", ord = 1 },
unstaged = { icon = i.unstaged, hl = "NvimTreeGitDirty", ord = 2 },
renamed = { icon = i.renamed, hl = "NvimTreeGitRenamed", ord = 3 },
deleted = { icon = i.deleted, hl = "NvimTreeGitDeleted", ord = 4 },
unmerged = { icon = i.unmerged, hl = "NvimTreeGitMerge", ord = 5 },
untracked = { icon = i.untracked, hl = "NvimTreeGitNew", ord = 6 },
ignored = { icon = i.ignored, hl = "NvimTreeGitIgnored", ord = 7 },
staged = { str = i.staged, hl = "NvimTreeGitStaged", ord = 1 },
unstaged = { str = i.unstaged, hl = "NvimTreeGitDirty", ord = 2 },
renamed = { str = i.renamed, hl = "NvimTreeGitRenamed", ord = 3 },
deleted = { str = i.deleted, hl = "NvimTreeGitDeleted", ord = 4 },
unmerged = { str = i.unmerged, hl = "NvimTreeGitMerge", ord = 5 },
untracked = { str = i.untracked, hl = "NvimTreeGitNew", ord = 6 },
ignored = { str = i.ignored, hl = "NvimTreeGitIgnored", ord = 7 },
}
return {
["M "] = { icons.staged },
@@ -59,6 +57,8 @@ local function warn_status(git_status)
)
end
---@param node table
---@return HighlightedString[]|nil
local function get_icons_(node)
local git_status = explorer_node.get_git_status(node)
if git_status == nil then
@@ -85,6 +85,11 @@ local function get_icons_(node)
end
end
-- sort icons so it looks slightly better
table.sort(iconss, function(a, b)
return a.ord < b.ord
end)
return iconss
end
@@ -145,7 +150,9 @@ function M.setup(opts)
M.git_icons = build_icons_table(opts.renderer.icons.glyphs.git)
M.setup_signs(opts.renderer.icons.glyphs.git)
if opts.renderer.icons.git_placement == "signcolumn" then
M.setup_signs(opts.renderer.icons.glyphs.git)
end
if opts.renderer.icons.show.git then
M.get_icons = get_icons_

View File

@@ -1,7 +1,7 @@
local M = { i = {} }
local function config_symlinks()
M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink .. M.config.padding or ""
M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink or ""
M.i.symlink_arrow = M.config.symlink_arrow
end
@@ -28,14 +28,14 @@ local function get_folder_icon(open, is_symlink, has_children)
n = M.config.glyphs.folder.empty
end
end
return n .. M.config.padding
return n
end
local function get_file_icon_default()
local hl_group = "NvimTreeFileIcon"
local icon = M.config.glyphs.default
if #icon > 0 then
return icon .. M.config.padding, hl_group
return icon, hl_group
else
return ""
end
@@ -47,7 +47,7 @@ local function get_file_icon_webdev(fname, extension)
hl_group = "NvimTreeFileIcon"
end
if icon and hl_group ~= "DevIconDefault" then
return icon .. M.config.padding, hl_group
return icon, hl_group
elseif string.match(extension, "%.(.*)") then
-- If there are more extensions to the file, try to grab the icon for them recursively
return get_file_icon_webdev(fname, string.match(extension, "%.(.*)"))

View File

@@ -0,0 +1,41 @@
local modified = require "nvim-tree.modified"
local M = {}
local HIGHLIGHT = "NvimTreeModifiedFile"
---return modified icon if node is modified, otherwise return empty string
---@param node table
---@return HighlightedString|nil modified icon
function M.get_icon(node)
if not modified.is_modified(node) or not M.show_icon then
return nil
end
return { str = M.icon, hl = HIGHLIGHT }
end
function M.setup_signs()
vim.fn.sign_define(HIGHLIGHT, { text = M.icon, texthl = HIGHLIGHT })
end
---@param node table
---@return string|nil
function M.get_highlight(node)
if not modified.is_modified(node) then
return nil
end
return HIGHLIGHT
end
function M.setup(opts)
M.icon = opts.renderer.icons.glyphs.modified
M.show_icon = opts.renderer.icons.show.modified
if opts.renderer.icons.modified_placement == "signcolumn" then
M.setup_signs()
end
end
return M

View File

@@ -68,6 +68,12 @@ local function get_padding_arrows(node, indent)
end
end
---@param depth integer
---@param idx integer
---@param nodes_number integer
---@param node table
---@param markers table
---@return HighlightedString
function M.get_padding(depth, idx, nodes_number, node, markers)
local padding = ""
@@ -86,7 +92,7 @@ function M.get_padding(depth, idx, nodes_number, node, markers)
padding = padding .. get_padding_arrows(node, not show_markers)
end
return padding
return { str = padding, hl = "NvimTreeIndentMarker" }
end
function M.setup(opts)

View File

@@ -2,6 +2,7 @@ local core = require "nvim-tree.core"
local diagnostics = require "nvim-tree.diagnostics"
local log = require "nvim-tree.log"
local view = require "nvim-tree.view"
local modified = require "nvim-tree.renderer.components.modified"
local _padding = require "nvim-tree.renderer.components.padding"
local icon_component = require "nvim-tree.renderer.components.icons"
@@ -16,6 +17,8 @@ local M = {
last_highlights = {},
}
local SIGN_GROUP = "NvimTreeRendererSigns"
local namespace_id = vim.api.nvim_create_namespace "NvimTreeHighlights"
local function _draw(bufnr, lines, hl, signs)
@@ -23,9 +26,9 @@ local function _draw(bufnr, lines, hl, signs)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
M.render_hl(bufnr, hl)
vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
vim.fn.sign_unplace(git.SIGN_GROUP)
vim.fn.sign_unplace(SIGN_GROUP)
for _, sign in pairs(signs) do
vim.fn.sign_place(0, git.SIGN_GROUP, sign.sign, bufnr, { lnum = sign.lnum, priority = 1 })
vim.fn.sign_place(0, SIGN_GROUP, sign.sign, bufnr, { lnum = sign.lnum, priority = sign.priority })
end
end
@@ -68,8 +71,10 @@ function M.draw(unloaded_bufnr)
:configure_special_files(M.config.special_files)
:configure_picture_map(picture_map)
:configure_opened_file_highlighting(M.config.highlight_opened_files)
:configure_git_icons_padding(M.config.icons.padding)
:configure_modified_highlighting(M.config.highlight_modified)
:configure_icon_padding(M.config.icons.padding)
:configure_git_icons_placement(M.config.icons.git_placement)
:configure_modified_placement(M.config.icons.modified_placement)
:configure_symlink_destination(M.config.symlink_destination)
:configure_filter(live_filter.filter, live_filter.prefix)
:build_header(view.is_root_folder_visible(core.get_cwd()))
@@ -100,10 +105,12 @@ end
function M.setup(opts)
M.config = opts.renderer
M.config.modified = opts.modified
_padding.setup(opts)
full_name.setup(opts)
git.setup(opts)
modified.setup(opts)
icon_component.setup(opts)
end