diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index ceddd542..d02e0044 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -170,14 +170,15 @@ Show the mappings: `g?` `e` Rename: Basename |nvim-tree-api.fs.rename_basename()| `]e` Next Diagnostic |nvim-tree-api.node.navigate.diagnostics.next()| `[e` Prev Diagnostic |nvim-tree-api.node.navigate.diagnostics.prev()| -`F` Clean Filter |nvim-tree-api.live_filter.clear()| -`f` Filter |nvim-tree-api.live_filter.start()| +`F` Live Filter: Clear |nvim-tree-api.live_filter.clear()| +`f` Live Filter: Start |nvim-tree-api.live_filter.start()| `g?` Help |nvim-tree-api.tree.toggle_help()| `gy` Copy Absolute Path |nvim-tree-api.fs.copy.absolute_path()| `H` Toggle Filter: Dotfiles |nvim-tree-api.tree.toggle_hidden_filter()| `I` Toggle Filter: Git Ignore |nvim-tree-api.tree.toggle_gitignore_filter()| `J` Last Sibling |nvim-tree-api.node.navigate.sibling.last()| `K` First Sibling |nvim-tree-api.node.navigate.sibling.first()| +`M` Toggle Filter: No Bookmark |nvim-tree-api.tree.toggle_no_bookmark_filter()| `m` Toggle Bookmark |nvim-tree-api.marks.toggle()| `o` Open |nvim-tree-api.node.open.edit()| `O` Open: No Window Picker |nvim-tree-api.node.open.no_window_picker()| @@ -502,6 +503,7 @@ Following is the default configuration. See |nvim-tree-opts| for details. dotfiles = false, git_clean = false, no_buffer = false, + no_bookmark = false, custom = {}, exclude = {}, }, @@ -1223,6 +1225,12 @@ For performance reasons this may not immediately update on buffer delete/wipe. A reload or filesystem event will result in an update. Type: `boolean`, Default: `false` +*nvim-tree.filters.no_bookmark* +Do not show files that are not bookarked. +Toggle via |nvim-tree-api.tree.toggle_no_bookmark_filter()|, default `M` +Enabling this is not useful as there is no means yet to persist bookmarks. + Type: `boolean`, Default: `false` + *nvim-tree.filters.custom* Custom list of vim regex for file/directory names that will not be shown. Backslashes must be escaped e.g. "^\\.git". See |string-match|. @@ -1666,6 +1674,10 @@ tree.toggle_git_clean_filter() tree.toggle_no_buffer_filter() Toggle |nvim-tree.filters.no_buffer| filter. + *nvim-tree-api.tree.toggle_no_bookmark_filter()* +tree.toggle_no_bookmark_filter() + Toggle |nvim-tree.filters.no_bookmark| filter. + *nvim-tree-api.tree.toggle_custom_filter()* tree.toggle_custom_filter() Toggle |nvim-tree.filters.custom| filter. @@ -1862,9 +1874,17 @@ node.open.preview_no_picker() *nvim-tree-api.node.open.preview_no_picker()* node.navigate.git.next() *nvim-tree-api.node.navigate.git.next()* Navigate to the next item showing git status. + *nvim-tree-api.node.navigate.git.next_skip_gitignored()* +node.navigate.git.next_skip_gitignored() + Same as |node.navigate.git.next()|, but skips gitignored files. + node.navigate.git.prev() *nvim-tree-api.node.navigate.git.prev()* Navigate to the previous item showing git status. + *nvim-tree-api.node.navigate.git.prev_skip_gitignored()* +node.navigate.git.prev_skip_gitignored() + Same as |node.navigate.git.prev()|, but skips gitignored files. + *nvim-tree-api.node.navigate.diagnostics.next()* node.navigate.diagnostics.next() Navigate to the next item showing diagnostic status. @@ -2138,14 +2158,15 @@ You are encouraged to copy these to your own |nvim-tree.on_attach| function. vim.keymap.set('n', 'e', api.fs.rename_basename, opts('Rename: Basename')) vim.keymap.set('n', ']e', api.node.navigate.diagnostics.next, opts('Next Diagnostic')) vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic')) - vim.keymap.set('n', 'F', api.live_filter.clear, opts('Clean Filter')) - vim.keymap.set('n', 'f', api.live_filter.start, opts('Filter')) + vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear')) + vim.keymap.set('n', 'f', api.live_filter.start, opts('Live Filter: Start')) vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help')) vim.keymap.set('n', 'gy', api.fs.copy.absolute_path, opts('Copy Absolute Path')) vim.keymap.set('n', 'H', api.tree.toggle_hidden_filter, opts('Toggle Filter: Dotfiles')) vim.keymap.set('n', 'I', api.tree.toggle_gitignore_filter, opts('Toggle Filter: Git Ignore')) vim.keymap.set('n', 'J', api.node.navigate.sibling.last, opts('Last Sibling')) vim.keymap.set('n', 'K', api.node.navigate.sibling.first, opts('First Sibling')) + vim.keymap.set('n', 'M', api.tree.toggle_no_bookmark_filter, opts('Toggle Filter: No Bookmark')) vim.keymap.set('n', 'm', api.marks.toggle, opts('Toggle Bookmark')) vim.keymap.set('n', 'o', api.node.open.edit, opts('Open')) vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker')) diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 61cdf413..ead71c33 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -507,6 +507,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS dotfiles = false, git_clean = false, no_buffer = false, + no_bookmark = false, custom = {}, exclude = {}, }, diff --git a/lua/nvim-tree/actions/moves/item.lua b/lua/nvim-tree/actions/moves/item.lua index 60b894e7..bf72c0ea 100644 --- a/lua/nvim-tree/actions/moves/item.lua +++ b/lua/nvim-tree/actions/moves/item.lua @@ -3,6 +3,7 @@ local view = require "nvim-tree.view" local core = require "nvim-tree.core" local lib = require "nvim-tree.lib" local explorer_node = require "nvim-tree.explorer.node" +local diagnostics = require "nvim-tree.diagnostics" local M = {} @@ -30,9 +31,11 @@ function M.fn(opts) local valid = false if opts.what == "git" then - valid = explorer_node.get_git_status(node) ~= nil + local git_status = explorer_node.get_git_status(node) + valid = git_status ~= nil and (not opts.skip_gitignored or git_status[1] ~= "!!") elseif opts.what == "diag" then - valid = node.diag_status ~= nil + local diag_status = diagnostics.get_diag_status(node) + valid = diag_status ~= nil and diag_status.value ~= nil elseif opts.what == "opened" then valid = vim.fn.bufloaded(node.absolute_path) ~= 0 end diff --git a/lua/nvim-tree/actions/tree-modifiers/toggles.lua b/lua/nvim-tree/actions/tree-modifiers/toggles.lua index 56b39d92..00ffaedb 100644 --- a/lua/nvim-tree/actions/tree-modifiers/toggles.lua +++ b/lua/nvim-tree/actions/tree-modifiers/toggles.lua @@ -31,6 +31,11 @@ function M.no_buffer() reload() end +function M.no_bookmark() + filters.config.filter_no_bookmark = not filters.config.filter_no_bookmark + reload() +end + function M.dotfiles() filters.config.filter_dotfiles = not filters.config.filter_dotfiles reload() diff --git a/lua/nvim-tree/api.lua b/lua/nvim-tree/api.lua index be7d3854..f7a7716e 100644 --- a/lua/nvim-tree/api.lua +++ b/lua/nvim-tree/api.lua @@ -136,6 +136,8 @@ Api.tree.toggle_custom_filter = wrap(require("nvim-tree.actions.tree-modifiers.t Api.tree.toggle_hidden_filter = wrap(require("nvim-tree.actions.tree-modifiers.toggles").dotfiles) +Api.tree.toggle_no_bookmark_filter = wrap(require("nvim-tree.actions.tree-modifiers.toggles").no_bookmark) + Api.tree.toggle_help = wrap(require("nvim-tree.help").toggle) Api.tree.is_tree_buf = wrap(require("nvim-tree.utils").is_nvim_tree_buf) @@ -214,6 +216,10 @@ Api.node.navigate.parent = wrap_node(require("nvim-tree.actions.moves.parent").f Api.node.navigate.parent_close = wrap_node(require("nvim-tree.actions.moves.parent").fn(true)) Api.node.navigate.git.next = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "next", what = "git" }) Api.node.navigate.git.prev = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "prev", what = "git" }) +-- stylua: ignore +Api.node.navigate.git.next_skip_gitignored = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "next", what = "git", skip_gitignored = true }) +-- stylua: ignore +Api.node.navigate.git.prev_skip_gitignored = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "prev", what = "git", skip_gitignored = true }) Api.node.navigate.diagnostics.next = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "next", what = "diag" }) Api.node.navigate.diagnostics.prev = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "prev", what = "diag" }) Api.node.navigate.opened.next = wrap_node(require("nvim-tree.actions.moves.item").fn { where = "next", what = "opened" }) diff --git a/lua/nvim-tree/diagnostics.lua b/lua/nvim-tree/diagnostics.lua index 93c68963..6ca13c9c 100644 --- a/lua/nvim-tree/diagnostics.lua +++ b/lua/nvim-tree/diagnostics.lua @@ -1,18 +1,44 @@ local utils = require "nvim-tree.utils" local view = require "nvim-tree.view" -local core = require "nvim-tree.core" local log = require "nvim-tree.log" local M = {} -local severity_levels = { +---TODO add "$VIMRUNTIME" to "workspace.library" and use the @enum instead of this integer +---@alias lsp.DiagnosticSeverity integer + +---COC severity level strings to LSP severity levels +---@enum COC_SEVERITY_LEVELS +local COC_SEVERITY_LEVELS = { Error = 1, Warning = 2, Information = 3, Hint = 4, } ----@return table +---Absolute Node path to LSP severity level +---@alias NodeSeverities table + +---@class DiagStatus +---@field value lsp.DiagnosticSeverity|nil +---@field cache_version integer + +--- The buffer-severity mappings derived during the last diagnostic list update. +---@type NodeSeverities +local NODE_SEVERITIES = {} + +---The cache version number of the buffer-severity mappings. +---@type integer +local NODE_SEVERITIES_VERSION = 0 + +---@param path string +---@return string +local function uniformize_path(path) + return utils.canonical_path(path:gsub("\\", "/")) +end + +---Marshal severities from LSP. Does nothing when LSP disabled. +---@return NodeSeverities local function from_nvim_lsp() local buffer_severity = {} @@ -25,11 +51,10 @@ local function from_nvim_lsp() for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do local buf = diagnostic.bufnr if vim.api.nvim_buf_is_valid(buf) then - local bufname = vim.api.nvim_buf_get_name(buf) - local lowest_severity = buffer_severity[bufname] - if not lowest_severity or diagnostic.severity < lowest_severity then - buffer_severity[bufname] = diagnostic.severity - end + local bufname = uniformize_path(vim.api.nvim_buf_get_name(buf)) + local severity = diagnostic.severity + local highest_severity = buffer_severity[bufname] or severity + buffer_severity[bufname] = math.min(highest_severity, severity) end end end @@ -37,91 +62,148 @@ local function from_nvim_lsp() return buffer_severity end ----@param severity integer +---Severity is within diagnostics.severity.min, diagnostics.severity.max +---@param severity lsp.DiagnosticSeverity ---@param config table ---@return boolean local function is_severity_in_range(severity, config) return config.max <= severity and severity <= config.min end ----@return table +---Handle any COC exceptions, preventing any propagation +---@param err string +local function handle_coc_exception(err) + log.line("diagnostics", "handle_coc_exception: %s", vim.inspect(err)) + local notify = true + + -- avoid distractions on interrupts (CTRL-C) + if err:find "Vim:Interrupt" or err:find "Keyboard interrupt" then + notify = false + end + + if notify then + require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err)) + end +end + +---COC service initialized +---@return boolean +local function is_using_coc() + return vim.g.coc_service_initialized == 1 +end + +---Marshal severities from COC. Does nothing when COC service not started. +---@return NodeSeverities local function from_coc() - if vim.g.coc_service_initialized ~= 1 then + if not is_using_coc() then return {} end - local diagnostic_list = vim.fn.CocAction "diagnosticList" - if type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then + local ok, diagnostic_list = xpcall(function() + return vim.fn.CocAction "diagnosticList" + end, handle_coc_exception) + if not ok or type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then return {} end - local diagnostics = {} - for _, diagnostic in ipairs(diagnostic_list) do - local bufname = diagnostic.file - local coc_severity = severity_levels[diagnostic.severity] - - local serverity = diagnostics[bufname] or vim.diagnostic.severity.HINT - diagnostics[bufname] = math.min(coc_severity, serverity) - end - local buffer_severity = {} - for bufname, severity in pairs(diagnostics) do - if is_severity_in_range(severity, M.severity) then - buffer_severity[bufname] = severity + for _, diagnostic in ipairs(diagnostic_list) do + local bufname = uniformize_path(diagnostic.file) + local coc_severity = COC_SEVERITY_LEVELS[diagnostic.severity] + local highest_severity = buffer_severity[bufname] or coc_severity + if is_severity_in_range(highest_severity, M.severity) then + buffer_severity[bufname] = math.min(highest_severity, coc_severity) end end return buffer_severity end -local function is_using_coc() - return vim.g.coc_service_initialized == 1 +---Maybe retrieve severity level from the cache +---@param node Node +---@return DiagStatus +local function from_cache(node) + local nodepath = uniformize_path(node.absolute_path) + local max_severity = nil + if not node.nodes then + -- direct cache hit for files + max_severity = NODE_SEVERITIES[nodepath] + else + -- dirs should be searched in the list of cached buffer names by prefix + for bufname, severity in pairs(NODE_SEVERITIES) do + local node_contains_buf = vim.startswith(bufname, nodepath .. "/") + if node_contains_buf then + if severity == M.severity.max then + max_severity = severity + break + else + max_severity = math.min(max_severity or severity, severity) + end + end + end + end + return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION } end +---Fired on DiagnosticChanged and CocDiagnosticChanged events: +---debounced retrieval, cache update, version increment and draw function M.update() - if not M.enable or not core.get_explorer() or not view.is_buf_valid(view.get_bufnr()) then + if not M.enable then return end utils.debounce("diagnostics", M.debounce_delay, function() local profile = log.profile_start "diagnostics update" - log.line("diagnostics", "update") - - local buffer_severity if is_using_coc() then - buffer_severity = from_coc() + NODE_SEVERITIES = from_coc() else - buffer_severity = from_nvim_lsp() + NODE_SEVERITIES = from_nvim_lsp() end - - local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line()) - for _, node in pairs(nodes_by_line) do - node.diag_status = nil - end - - for bufname, severity in pairs(buffer_severity) do - local bufpath = utils.canonical_path(bufname) - log.line("diagnostics", " bufpath '%s' severity %d", bufpath, severity) - if 0 < severity and severity < 5 then - for line, node in pairs(nodes_by_line) do - local nodepath = utils.canonical_path(node.absolute_path) - log.line("diagnostics", " %d checking nodepath '%s'", line, nodepath) - - local node_contains_buf = vim.startswith(bufpath:gsub("\\", "/"), nodepath:gsub("\\", "/") .. "/") - if M.show_on_dirs and node_contains_buf and (not node.open or M.show_on_open_dirs) then - log.line("diagnostics", " matched fold node '%s'", node.absolute_path) - node.diag_status = severity - elseif nodepath == bufpath then - log.line("diagnostics", " matched file node '%s'", node.absolute_path) - node.diag_status = severity - end - end + NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 + if log.enabled "diagnostics" then + for bufname, severity in pairs(NODE_SEVERITIES) do + log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity) end end log.profile_end(profile) - require("nvim-tree.renderer").draw() + if view.is_buf_valid(view.get_bufnr()) then + require("nvim-tree.renderer").draw() + end end) end +---Maybe retrieve diagnostic status for a node. +---Returns cached value when node's version matches. +---@param node Node +---@return DiagStatus|nil +function M.get_diag_status(node) + if not M.enable then + return nil + end + + -- dir but we shouldn't show on dirs at all + if node.nodes ~= nil and not M.show_on_dirs then + return nil + end + + -- here, we do a lazy update of the diagnostic status carried by the node. + -- This is by design, as diagnostics and nodes live in completely separate + -- worlds, and this module is the link between the two + if not node.diag_status or node.diag_status.cache_version < NODE_SEVERITIES_VERSION then + node.diag_status = from_cache(node) + end + + -- file + if not node.nodes then + return node.diag_status + end + + -- dir is closed or we should show on open_dirs + if not node.open or M.show_on_open_dirs then + return node.diag_status + end + return nil +end + function M.setup(opts) M.enable = opts.diagnostics.enable M.debounce_delay = opts.diagnostics.debounce_delay diff --git a/lua/nvim-tree/explorer/filters.lua b/lua/nvim-tree/explorer/filters.lua index 7c8d4723..18bce028 100644 --- a/lua/nvim-tree/explorer/filters.lua +++ b/lua/nvim-tree/explorer/filters.lua @@ -1,4 +1,5 @@ local utils = require "nvim-tree.utils" +local marks = require "nvim-tree.marks" local M = { ignore_list = {}, @@ -69,6 +70,12 @@ local function dotfile(path) return M.config.filter_dotfiles and utils.path_basename(path):sub(1, 1) == "." end +---@param path string +---@param bookmarks table absolute paths bookmarked +local function bookmark(path, bookmarks) + return M.config.filter_no_bookmark and not bookmarks[path] +end + ---@param path string ---@return boolean local function custom(path) @@ -103,17 +110,23 @@ end --- git_status: reference --- unloaded_bufnr: copy --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } +--- bookmarks: absolute paths to boolean function M.prepare(git_status, unloaded_bufnr) local status = { git_status = git_status or {}, unloaded_bufnr = unloaded_bufnr, bufinfo = {}, + bookmarks = {}, } if M.config.filter_no_buffer then status.bufinfo = vim.fn.getbufinfo { buflisted = 1 } end + for _, node in pairs(marks.get_marks()) do + status.bookmarks[node.absolute_path] = true + end + return status end @@ -127,7 +140,11 @@ function M.should_filter(path, status) return false end - return git(path, status.git_status) or buf(path, status.bufinfo, status.unloaded_bufnr) or dotfile(path) or custom(path) + return git(path, status.git_status) + or buf(path, status.bufinfo, status.unloaded_bufnr) + or dotfile(path) + or custom(path) + or bookmark(path, status.bookmarks) end function M.setup(opts) @@ -137,6 +154,7 @@ function M.setup(opts) filter_git_ignored = opts.filters.git_ignored, filter_git_clean = opts.filters.git_clean, filter_no_buffer = opts.filters.no_buffer, + filter_no_bookmark = opts.filters.no_bookmark, } M.ignore_list = {} diff --git a/lua/nvim-tree/keymap.lua b/lua/nvim-tree/keymap.lua index 991b9b33..700a4794 100644 --- a/lua/nvim-tree/keymap.lua +++ b/lua/nvim-tree/keymap.lua @@ -64,14 +64,15 @@ function M.default_on_attach(bufnr) vim.keymap.set('n', 'e', api.fs.rename_basename, opts('Rename: Basename')) vim.keymap.set('n', ']e', api.node.navigate.diagnostics.next, opts('Next Diagnostic')) vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic')) - vim.keymap.set('n', 'F', api.live_filter.clear, opts('Clean Filter')) - vim.keymap.set('n', 'f', api.live_filter.start, opts('Filter')) + vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear')) + vim.keymap.set('n', 'f', api.live_filter.start, opts('Live Filter: Start')) vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help')) vim.keymap.set('n', 'gy', api.fs.copy.absolute_path, opts('Copy Absolute Path')) vim.keymap.set('n', 'H', api.tree.toggle_hidden_filter, opts('Toggle Filter: Dotfiles')) vim.keymap.set('n', 'I', api.tree.toggle_gitignore_filter, opts('Toggle Filter: Git Ignore')) vim.keymap.set('n', 'J', api.node.navigate.sibling.last, opts('Last Sibling')) vim.keymap.set('n', 'K', api.node.navigate.sibling.first, opts('First Sibling')) + vim.keymap.set('n', 'M', api.tree.toggle_no_bookmark_filter, opts('Toggle Filter: No Bookmark')) vim.keymap.set('n', 'm', api.marks.toggle, opts('Toggle Bookmark')) vim.keymap.set('n', 'o', api.node.open.edit, opts('Open')) vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker')) diff --git a/lua/nvim-tree/node.lua b/lua/nvim-tree/node.lua index 55ee1942..323fc53c 100644 --- a/lua/nvim-tree/node.lua +++ b/lua/nvim-tree/node.lua @@ -17,6 +17,7 @@ ---@field parent DirNode ---@field type string ---@field watcher function|nil +---@field diag_status DiagStatus|nil ---@class DirNode: BaseNode ---@field has_children boolean diff --git a/lua/nvim-tree/renderer/components/diagnostics.lua b/lua/nvim-tree/renderer/components/diagnostics.lua index 3a145406..1fa864de 100644 --- a/lua/nvim-tree/renderer/components/diagnostics.lua +++ b/lua/nvim-tree/renderer/components/diagnostics.lua @@ -1,4 +1,5 @@ local HL_POSITION = require("nvim-tree.enum").HL_POSITION +local diagnostics = require "nvim-tree.diagnostics" local M = { HS_FILE = {}, @@ -17,10 +18,11 @@ function M.get_highlight(node) end local group + local diag_status = diagnostics.get_diag_status(node) if node.nodes then - group = M.HS_FOLDER[node.diag_status] + group = M.HS_FOLDER[diag_status and diag_status.value] else - group = M.HS_FILE[node.diag_status] + group = M.HS_FILE[diag_status and diag_status.value] end if group then @@ -35,7 +37,8 @@ end ---@return HighlightedString|nil modified icon function M.get_icon(node) if node and M.config.diagnostics.enable and M.config.renderer.icons.show.diagnostics then - return M.ICON[node.diag_status] + local diag_status = diagnostics.get_diag_status(node) + return M.ICON[diag_status and diag_status.value] end end diff --git a/release-please-config.json b/release-please-config.json index 8aafb4f2..bb81fab2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "include-v-in-tag": true, + "bootstrap-sha": "34780aca5bac0a58c163ea30719a276fead1bd95", "packages": { ".": { "package-name": "nvim-tree",