Compare commits

...

37 Commits

Author SHA1 Message Date
github-actions[bot]
375e38673b chore(master): release nvim-tree 1.9.0 (#2999)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-07 16:04:32 +11:00
Alexander Courtis
7a4ff1a516 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
2024-12-07 16:03:29 +11:00
Alexander Courtis
ca7c4c33ca fix(#3009): nvim < 0.10 apply view options locally (#3010) 2024-11-24 17:00:58 +11:00
Alexander Courtis
1f3ffd6af1 fix(#2954): more efficient LSP updates, increase diagnostics.debounce_delay from 50ms to 500ms (#3007)
* fix(#2954): use LSP diagnostic data deltas from events instead of a full query

* fix(#2954): use LSP diagnostic data deltas from events instead of a full query
2024-11-22 10:12:47 +11:00
devxpain
f7c65e11d6 fix(api): correct argument types in wrap_node and wrap_node_or_nil (#3006)
The `wrap_node` and `wrap_node_or_nil` functions now correctly accept `Node?` to handle nil values, resolving a warning about incorrect argument counts in `api.tree.change_root_to_node()`.
2024-11-18 10:00:19 +11:00
des-b
28eac2801b fix(#2990): Do not check if buffer is buflisted in diagnostics.update() (#2998)
This ensures that LSP diagnostics of files which are not manually opened
by users are rendered by nvim-tree diagnostic indicators.

However when users attach an LSP to nvim-tree this will bring back
flashing as attempted to fix in #2980. Fixing this should probably done
by checking data passed via diagnostic events (DiagnosticChanged and
CocDiagnosticChanged).

Signed-off-by: des-b <66919647+des-b@users.noreply.github.com>
2024-11-11 08:57:06 +11:00
github-actions[bot]
c7639482a1 chore(master): release nvim-tree 1.8.0 (#2943)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-09 14:46:26 +11:00
Alexander Courtis
2ee1c5e17f feat(#2819): add actions.open_file.relative_path, default enabled, following successful experiment (#2995) 2024-11-09 14:44:59 +11:00
Alexander Courtis
3fc8de198c chore: migrate to classic (#2991)
* add classic, migrating nodes classes

* add mixins to classic

* typechecked optargs constructors for nodes

* typechecked optargs constructors for watcher and event

* luacheck

* typechecked optargs constructors for GitRunner

* typechecked optargs constructors for Sorter

* typechecked optargs constructors for decorators, WIP

* typechecked optargs constructors for decorators, WIP

* typechecked optargs constructors for decorators

* remove class

* replace enums with named maps

* Renderer and Builder use classic, tidy opts

* LiveFilter uses classic, tidy opts

* Filter uses classic, tidy opts

* add FilterTypes named map

* move toggles into filters

* Marks uses classic, tidy opts

* Sorter uses classic, tidy opts

* Clipboard uses classic, tidy opts

* use supers for node methods

* HighlightDisplay uses classic

* protected :new

* Watcher tidy

* Revert "use supers for node methods"

This reverts commit 9fc7a866ec.

* Watcher tidy

* format

* format

* Filters private methods

* format

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* tidy Runner

* tidy hi-test name
2024-11-09 14:14:04 +11:00
Alexander Courtis
610a1c189b chore: resolve undefined-field warnings, fix link git statuses, rewrite devicons (#2968)
* add todo

* refactor(#2886): multi instance: node class refactoring: extract links, *_git_status (#2944)

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

* refactor(#2886): multi instance: node class refactoring: DirectoryNode:expand_or_collapse (#2957)

move expand_or_collapse to DirectoryNode

* refactor(#2886): multi instance: node group functions refactoring (#2959)

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

* move lib.get_cursor_position to Explorer

* move lib.get_node_at_cursor to Explorer

* move lib.get_nodes to Explorer

* move place_cursor_on_node to Explorer

* resolve resource leak in purge_all_state

* move many autocommands into Explorer

* post merge tidy

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit be546ff18d41f28466b065c857e1e041659bd2c8.

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit e82db1c44d.

* chore: resolve undefined-field

* chore: class new is now generic

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit 0e9b844d22.

* move icon builders into node classes

* move icon builders into node classes

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move file specifics from icons to File

* clean up sorters

* chore: resolve undefined-field

* tidy hl icon name

* file devicon uses library to fall back

* file devicon uses library to fall back

* file devicon uses library to fall back
2024-11-03 14:06:12 +11:00
Jie Liu
c22124b374 fix(#2981): windows: root changed when navigating with LSP (#2982)
* fix #2981: nvim-tree root changed when navigating with LSP

* add error checks
2024-11-03 13:52:20 +11:00
Alexander Courtis
2156bc08c9 fix: symlink file icons rendered when renderer.icons.show.file = false, folder.symlink* was incorrectly rendered as folder.default|open (#2983)
* fix: folder.symlink* was incorrectly rendered as folder.default|open

* fix: symlink file icons rendered when renderer.icons.show.file = false
2024-11-03 12:10:00 +11:00
des-b
82ab19ebf7 fix(#2954): resolve occasional tree flashing on diagnostics, set tree buffer options in deterministic order (#2980)
* fix(#2954): set buffer options in deterministic order

This ensures related autocmd's (e.g. on FileType) will be called in a
similar environment.

* fix(#2954): redraw only for diagnostics if source buffer is 'buflisted'

is_buf_valid has been inlined since it is only used for diagnostics
and its name is misleading.
2024-11-02 12:07:42 +11:00
Alexander Courtis
120ba58254 fix(#2978): grouped folder not showing closed icon (#2979) 2024-10-29 11:07:48 +11:00
Alexander Courtis
00dff482f9 fix(#2976): use vim.loop to preserve neovim 0.9 compatibility (#2977) 2024-10-29 08:01:52 +11:00
Alexander Courtis
8f974879a0 chore: luals runtime.version only set during check, to prevent lua version ambuguity at dev time (#2975)
* chore: luals runtime.version only set during check, to prevent lua version ambuguity at dev time

* inject lua 5.1 check failure

* Revert "inject lua 5.1 check failure"

This reverts commit eed966dc7b.
2024-10-28 11:57:53 +11:00
cpp_programmer
14039337a5 fix(#2969): After a rename, the node loses selection (#2974)
Co-authored-by: Lucian Ion <lucian.ion.2005@gmail.com>
2024-10-27 17:26:47 +11:00
Alexander Courtis
e4bc05b415 doc: remove outdated warning from actions.change_dir.global 2024-10-27 10:48:17 +11:00
Alexander Courtis
3cddd28177 doc: add windows specifics to CONTRIBUTING 2024-10-27 10:32:41 +11:00
Alexander Courtis
6e5a204ca6 fix(#2972): error on :colorscheme (#2973) 2024-10-27 10:18:21 +11:00
Alexander Courtis
f3efc25e56 refactor(#2941): move lib methods to explorer (#2964)
* add todo

* refactor(#2886): multi instance: node class refactoring: extract links, *_git_status (#2944)

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

* refactor(#2886): multi instance: node class refactoring: DirectoryNode:expand_or_collapse (#2957)

move expand_or_collapse to DirectoryNode

* refactor(#2886): multi instance: node group functions refactoring (#2959)

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

* move lib.get_cursor_position to Explorer

* move lib.get_node_at_cursor to Explorer

* move lib.get_nodes to Explorer

* move place_cursor_on_node to Explorer

* resolve resource leak in purge_all_state

* move many autocommands into Explorer

* post merge tidy
2024-10-27 09:03:26 +11:00
Alexander Courtis
8760d76c1d chore: enable missing-local-export-doc 2024-10-25 14:35:48 +11:00
Alexander Courtis
077af9f990 chore: enable incomplete-signature-doc, format nvt-min.lua, assorted formatting tidies (#2967)
* chore: luacheckrc uses table

* chore: format nvt-min.lua

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: enable incomplete-signature-doc

* chore: enable incomplete-signature-doc

* chore: complete lua doc

* chore: complete lua doc
2024-10-25 14:25:30 +11:00
Alexander Courtis
68be6df2fc refactor(#2886): multi instance: node class refactoring (#2950)
* add todo

* refactor(#2886): multi instance: node class refactoring: extract links, *_git_status (#2944)

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

* refactor(#2886): multi instance: node class refactoring: DirectoryNode:expand_or_collapse (#2957)

move expand_or_collapse to DirectoryNode

* refactor(#2886): multi instance: node group functions refactoring (#2959)

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

* more type safety
2024-10-25 12:24:59 +11:00
Jie Liu
63c7ad9037 fix(#2961): windows: escape brackets and parentheses when opening file (#2962)
* Revert "fix(#2862): windows path replaces backslashes with forward slashes (#2903)"

This reverts commit 45a93d9979.

* fix the case when '()' and '[]' are both in file path

* remove debug messages

* remove unnecessary comments

* add is_windows feature flag when normalizing path

* add is_windows flag for filename change

* Revert "add is_windows flag for filename change"

This reverts commit ada77cb7e9.

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2024-10-25 11:11:21 +11:00
Alexander Courtis
9b82ff9bba chore: fix lib prompt doc for neovim nightly (#2966) 2024-10-25 11:10:07 +11:00
Alexander Courtis
2a268f631d doc: help: syntax highlighting for lua and vimscript 2024-10-18 18:29:19 +11:00
Alexander Courtis
f5f6789299 fix(#2947): root is never a dotfile, so that it doesn't propagate to children (#2958) 2024-10-14 18:56:43 +11:00
Alexander Courtis
ce09bfb95f chore: TODO issue links 2024-10-14 10:47:41 +11:00
Alexander Courtis
0fede9f813 chore: nvt-min.lua: ensure only one instance of lua-language-server runs (#2956) 2024-10-14 10:24:15 +11:00
Alexander Courtis
1c9553a19f fix(#2951): highlights incorrect following cancelled pick (#2952) 2024-10-12 15:54:12 +11:00
Alexander Courtis
ca0904e4c5 chore: add utils.enumerate_options (#2953) 2024-10-12 15:53:23 +11:00
Alexander Courtis
5ad87620ec fix(#2945): stack overflow on api.git.reload or fugitive event with watchers disabled (#2949)
* Reapply "refactor(#2871, #2886): multi instance: node classes created (#2916)"

This reverts commit 50e919426a.

* fix(#2945): stack overflow on api.git.reload or fugitive event
2024-10-11 13:47:01 +11:00
Alexander Courtis
50e919426a Revert "refactor(#2871, #2886): multi instance: node classes created (#2916)"
This reverts commit 38aac09151.
2024-10-08 18:07:47 +11:00
Alexander Courtis
010ae0365a feat(#2938): add default filesystem_watchers.ignore_dirs = { "/.ccls-cache", "/build", "/node_modules", "/target", } (#2940)
* feat(#2938): filesystem_watchers.ignore_dirs defaults to { node_modules } to resolve pathalogical issues

* feat(#2938): more filesystem_watchers.ignore_dirs defaults to to resolve pathalogical issues

* feat(#2938): more filesystem_watchers.ignore_dirs defaults to to resolve pathalogical issues
2024-10-07 15:25:24 +11:00
Alexander Courtis
38aac09151 refactor(#2871, #2886): multi instance: node classes created (#2916)
* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* node classes and constructors

* node methods

* refactor(#2875): multi instance renderer

* node classes and constructors

* explorer is a directory node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from lib

* use .. name for root node for compatibility

* use node.explorer

* extract node factory, remove unused code

* factories for all nodes, add RootNode

* factories for all nodes, add RootNode

* use factory pattern for decorators

* note regression and commit

* fix dir git status regression

* destroy nodes, not explorer

* add BaseNode:is

* revert changes to create-file, handle in #2924

* extract methods from explorer

* extract methods from explorer

* extract methods from explorer

* use Node everywhere in luadoc

* extract methods from lib

* extract methods from lib

* lint

* remove unused code

* don't call methods on fake root node

* get_node_at_cursor returns explorer (root) node instead of { name = '..' }

* remove unused inject_node

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* extract methods from lib

* node factory uses stat only

* temporary DirectoryNode casting until method extraction into child classes

* lua-language-server 3.10.5 -> 3.11.0

* explicitly call Explorer constructor

* normalise explorer RootNode new call, tidy annotations
2024-10-07 13:46:56 +11:00
Alexander Courtis
c9104a5d07 chore: style: align_continuous_similar_call_args (#2937)
* chore: style: align_continuous_similar_call_args

* chore: style: align_continuous_similar_call_args

* chore: style: align_continuous_similar_call_args

* chore: style: align_continuous_similar_call_args

* chore: style: consistent use of double quotes
2024-09-30 15:34:01 +10:00
83 changed files with 3613 additions and 2886 deletions

View File

@@ -18,3 +18,4 @@ continuation_indent = 2
quote_style = double quote_style = double
call_arg_parentheses = always call_arg_parentheses = always
space_before_closure_open_parenthesis = false space_before_closure_open_parenthesis = false
align_continuous_similar_call_args = true

View File

@@ -1,12 +1,12 @@
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
vim.cmd [[set runtimepath=$VIMRUNTIME]] vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.cmd [[set packpath=/tmp/nvt-min/site]] vim.cmd([[set packpath=/tmp/nvt-min/site]])
local package_root = "/tmp/nvt-min/site/pack" local package_root = "/tmp/nvt-min/site/pack"
local install_path = package_root .. "/packer/start/packer.nvim" local install_path = package_root .. "/packer/start/packer.nvim"
local function load_plugins() local function load_plugins()
require("packer").startup { require("packer").startup({
{ {
"wbthomason/packer.nvim", "wbthomason/packer.nvim",
"nvim-tree/nvim-tree.lua", "nvim-tree/nvim-tree.lua",
@@ -18,21 +18,21 @@ local function load_plugins()
compile_path = install_path .. "/plugin/packer_compiled.lua", compile_path = install_path .. "/plugin/packer_compiled.lua",
display = { non_interactive = true }, display = { non_interactive = true },
}, },
} })
end end
if vim.fn.isdirectory(install_path) == 0 then if vim.fn.isdirectory(install_path) == 0 then
print "Installing nvim-tree and dependencies." print("Installing nvim-tree and dependencies.")
vim.fn.system { "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path } vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path })
end end
load_plugins() load_plugins()
require("packer").sync() require("packer").sync()
vim.cmd [[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]] vim.cmd([[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]])
vim.opt.termguicolors = true vim.opt.termguicolors = true
vim.opt.cursorline = true vim.opt.cursorline = true
-- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE -- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE
_G.setup = function() _G.setup = function()
require("nvim-tree").setup {} require("nvim-tree").setup({})
end end
-- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate. -- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate.
@@ -41,7 +41,11 @@ end
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = "lua", pattern = "lua",
callback = function() callback = function()
vim.lsp.start { cmd = { "lua-language-server" } } vim.lsp.start {
name = "my-luals",
cmd = { "lua-language-server" },
root_dir = vim.loop.cwd(),
}
end, end,
}) })
]] ]]

View File

@@ -69,7 +69,7 @@ jobs:
strategy: strategy:
matrix: matrix:
nvim_version: [ stable, nightly ] nvim_version: [ stable, nightly ]
luals_version: [ 3.10.5 ] luals_version: [ 3.11.0 ]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@@ -1,14 +1,15 @@
-- vim: ft=lua tw=80 local M = {}
-- Don't report unused self arguments of methods. -- Don't report unused self arguments of methods.
self = false M.self = false
ignore = { M.ignore = {
"631", -- max_line_length "631", -- max_line_length
} }
-- Global objects defined by the C code -- Global objects defined by the C code
globals = { M.globals = {
"vim", "vim",
"TreeExplorer"
} }
return M

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.1", "runtime.version.luals-check-only": "Lua 5.1",
"workspace": { "workspace": {
"library": [ "library": [
"$VIMRUNTIME/lua/vim", "$VIMRUNTIME/lua/vim",
@@ -33,13 +33,13 @@
"empty-block": "Any", "empty-block": "Any",
"global-element": "Any", "global-element": "Any",
"global-in-nil-env": "Any", "global-in-nil-env": "Any",
"incomplete-signature-doc": "None", "incomplete-signature-doc": "Any",
"inject-field": "Any", "inject-field": "Any",
"invisible": "Any", "invisible": "Any",
"lowercase-global": "Any", "lowercase-global": "Any",
"missing-fields": "Any", "missing-fields": "Any",
"missing-global-doc": "Any", "missing-global-doc": "Any",
"missing-local-export-doc": "None", "missing-local-export-doc": "Any",
"missing-parameter": "Any", "missing-parameter": "Any",
"missing-return": "Any", "missing-return": "Any",
"missing-return-value": "Any", "missing-return-value": "Any",

View File

@@ -1,3 +1,3 @@
{ {
".": "1.7.1" ".": "1.9.0"
} }

View File

@@ -1,5 +1,43 @@
# Changelog # Changelog
## [1.9.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.8.0...nvim-tree-v1.9.0) (2024-12-07)
### Features
* **#2948:** add custom decorators, :help nvim-tree-decorators ([#2996](https://github.com/nvim-tree/nvim-tree.lua/issues/2996)) ([7a4ff1a](https://github.com/nvim-tree/nvim-tree.lua/commit/7a4ff1a516fe92a5ed6b79d7ce31ea4d8f341a72))
### Bug Fixes
* **#2954:** more efficient LSP updates, increase diagnostics.debounce_delay from 50ms to 500ms ([#3007](https://github.com/nvim-tree/nvim-tree.lua/issues/3007)) ([1f3ffd6](https://github.com/nvim-tree/nvim-tree.lua/commit/1f3ffd6af145af2a4930a61c50f763264922c3fe))
* **#2990:** Do not check if buffer is buflisted in diagnostics.update() ([#2998](https://github.com/nvim-tree/nvim-tree.lua/issues/2998)) ([28eac28](https://github.com/nvim-tree/nvim-tree.lua/commit/28eac2801b201f301449e976d7a9e8cfde053ba3))
* **#3009:** nvim &lt; 0.10 apply view options locally ([#3010](https://github.com/nvim-tree/nvim-tree.lua/issues/3010)) ([ca7c4c3](https://github.com/nvim-tree/nvim-tree.lua/commit/ca7c4c33cac2ad66ec69d45e465379716ef0cc97))
* **api:** correct argument types in `wrap_node` and `wrap_node_or_nil` ([#3006](https://github.com/nvim-tree/nvim-tree.lua/issues/3006)) ([f7c65e1](https://github.com/nvim-tree/nvim-tree.lua/commit/f7c65e11d695a084ca10b93df659bb7e68b71f9f))
## [1.8.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.1...nvim-tree-v1.8.0) (2024-11-09)
### Features
* **#2819:** add actions.open_file.relative_path, default enabled, following successful experiment ([#2995](https://github.com/nvim-tree/nvim-tree.lua/issues/2995)) ([2ee1c5e](https://github.com/nvim-tree/nvim-tree.lua/commit/2ee1c5e17fdfbf5013af31b1410e4a5f28f4cadd))
* **#2938:** add default filesystem_watchers.ignore_dirs = { "/.ccls-cache", "/build", "/node_modules", "/target", } ([#2940](https://github.com/nvim-tree/nvim-tree.lua/issues/2940)) ([010ae03](https://github.com/nvim-tree/nvim-tree.lua/commit/010ae0365aafd6275c478d932515d2e8e897b7bb))
### Bug Fixes
* **#2945:** stack overflow on api.git.reload or fugitive event with watchers disabled ([#2949](https://github.com/nvim-tree/nvim-tree.lua/issues/2949)) ([5ad8762](https://github.com/nvim-tree/nvim-tree.lua/commit/5ad87620ec9d1190d15c88171a3f0122bc16b0fe))
* **#2947:** root is never a dotfile, so that it doesn't propagate to children ([#2958](https://github.com/nvim-tree/nvim-tree.lua/issues/2958)) ([f5f6789](https://github.com/nvim-tree/nvim-tree.lua/commit/f5f67892996b280ae78b1b0a2d07c4fa29ae0905))
* **#2951:** highlights incorrect following cancelled pick ([#2952](https://github.com/nvim-tree/nvim-tree.lua/issues/2952)) ([1c9553a](https://github.com/nvim-tree/nvim-tree.lua/commit/1c9553a19f70df3dcb171546a3d5e034531ef093))
* **#2954:** resolve occasional tree flashing on diagnostics, set tree buffer options in deterministic order ([#2980](https://github.com/nvim-tree/nvim-tree.lua/issues/2980)) ([82ab19e](https://github.com/nvim-tree/nvim-tree.lua/commit/82ab19ebf79c1839d7351f2fed213d1af13a598e))
* **#2961:** windows: escape brackets and parentheses when opening file ([#2962](https://github.com/nvim-tree/nvim-tree.lua/issues/2962)) ([63c7ad9](https://github.com/nvim-tree/nvim-tree.lua/commit/63c7ad9037fb7334682dd0b3a177cee25c5c8a0f))
* **#2969:** After a rename, the node loses selection ([#2974](https://github.com/nvim-tree/nvim-tree.lua/issues/2974)) ([1403933](https://github.com/nvim-tree/nvim-tree.lua/commit/14039337a563f4efd72831888f332a15585f0ea1))
* **#2972:** error on :colorscheme ([#2973](https://github.com/nvim-tree/nvim-tree.lua/issues/2973)) ([6e5a204](https://github.com/nvim-tree/nvim-tree.lua/commit/6e5a204ca659bb8f2a564df75df2739edec03cb0))
* **#2976:** use vim.loop to preserve neovim 0.9 compatibility ([#2977](https://github.com/nvim-tree/nvim-tree.lua/issues/2977)) ([00dff48](https://github.com/nvim-tree/nvim-tree.lua/commit/00dff482f9a8fb806a54fd980359adc6cd45d435))
* **#2978:** grouped folder not showing closed icon ([#2979](https://github.com/nvim-tree/nvim-tree.lua/issues/2979)) ([120ba58](https://github.com/nvim-tree/nvim-tree.lua/commit/120ba58254835d412bbc91cffe847e9be835fadd))
* **#2981:** windows: root changed when navigating with LSP ([#2982](https://github.com/nvim-tree/nvim-tree.lua/issues/2982)) ([c22124b](https://github.com/nvim-tree/nvim-tree.lua/commit/c22124b37409bee6d1a0da77f4f3a1526f7a204d))
* symlink file icons rendered when renderer.icons.show.file = false, folder.symlink* was incorrectly rendered as folder.default|open ([#2983](https://github.com/nvim-tree/nvim-tree.lua/issues/2983)) ([2156bc0](https://github.com/nvim-tree/nvim-tree.lua/commit/2156bc08c982d3c4b4cfc2b8fd7faeff58a88e10))
## [1.7.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.0...nvim-tree-v1.7.1) (2024-09-30) ## [1.7.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.0...nvim-tree-v1.7.1) (2024-09-30)

View File

@@ -2,7 +2,7 @@
Thank you for contributing. Thank you for contributing.
See [Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools. See [wiki: Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools.
# Tools # Tools
@@ -90,6 +90,14 @@ Documentation for options should also be added to `nvim-tree-opts` in `doc/nvim-
When adding or changing API please update :help nvim-tree-api When adding or changing API please update :help nvim-tree-api
# Windows
Please note that nvim-tree team members do not have access to nor expertise with Windows.
You will need to be an active participant during development and raise a PR to resolve any issues that may arise.
Please ensure that windows specific features and fixes are behind the appropriate feature flag, see [wiki: OS Feature Flags](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development#os-feature-flags)
# Pull Request # Pull Request
Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge. Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge.

View File

@@ -13,7 +13,7 @@ check: luals
# subtasks # subtasks
# #
luacheck: luacheck:
luacheck -q lua luacheck --codes --quiet lua --exclude-files "**/_meta/**"
# --diagnosis-as-error does not function for workspace, hence we post-process the output # --diagnosis-as-error does not function for workspace, hence we post-process the output
style-check: style-check:

View File

@@ -52,14 +52,16 @@ CONTENTS *nvim-tree*
8.2 Highlight: Overhaul |nvim-tree-highlight-overhaul| 8.2 Highlight: Overhaul |nvim-tree-highlight-overhaul|
9. Events |nvim-tree-events| 9. Events |nvim-tree-events|
10. Prompts |nvim-tree-prompts| 10. Prompts |nvim-tree-prompts|
11. OS Specific Restrictions |nvim-tree-os-specific| 11. Decorators |nvim-tree-decorators|
12. Netrw |nvim-tree-netrw| 11.1 Decorator Example |nvim-tree-decorator-example|
13. Legacy |nvim-tree-legacy| 12. OS Specific Restrictions |nvim-tree-os-specific|
13.1 Legacy: Opts |nvim-tree-legacy-opts| 13. Netrw |nvim-tree-netrw|
13.2 Legacy: Highlight |nvim-tree-legacy-highlight| 14. Legacy |nvim-tree-legacy|
14. Index |nvim-tree-index| 14.1 Legacy: Opts |nvim-tree-legacy-opts|
14.1 Index: Opts |nvim-tree-index-opts| 14.2 Legacy: Highlight |nvim-tree-legacy-highlight|
14.2 Index: API |nvim-tree-index-api| 15. Index |nvim-tree-index|
15.1 Index: Opts |nvim-tree-index-opts|
15.2 Index: API |nvim-tree-index-api|
============================================================================== ==============================================================================
1. INTRODUCTION *nvim-tree-introduction* 1. INTRODUCTION *nvim-tree-introduction*
@@ -117,7 +119,7 @@ Disabling |netrw| is strongly advised, see |nvim-tree-netrw|
============================================================================== ==============================================================================
2.1 QUICKSTART: SETUP *nvim-tree-quickstart-setup* 2.1 QUICKSTART: SETUP *nvim-tree-quickstart-setup*
Setup the plugin in your `init.lua` > Setup the plugin in your `init.lua` e.g. >lua
-- disable netrw at the very start of your init.lua -- disable netrw at the very start of your init.lua
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
@@ -215,7 +217,7 @@ Show the mappings: `g?`
2.3 QUICKSTART: CUSTOM MAPPINGS *nvim-tree-quickstart-custom-mappings* 2.3 QUICKSTART: CUSTOM MAPPINGS *nvim-tree-quickstart-custom-mappings*
|nvim-tree-mappings-default| are applied by default however you may customise |nvim-tree-mappings-default| are applied by default however you may customise
via |nvim-tree.on_attach| e.g. > via |nvim-tree.on_attach| e.g. >lua
local function my_on_attach(bufnr) local function my_on_attach(bufnr)
local api = require "nvim-tree.api" local api = require "nvim-tree.api"
@@ -228,8 +230,8 @@ via |nvim-tree.on_attach| e.g. >
api.config.mappings.default_on_attach(bufnr) api.config.mappings.default_on_attach(bufnr)
-- custom mappings -- custom mappings
vim.keymap.set('n', '<C-t>', api.tree.change_root_to_parent, opts('Up')) vim.keymap.set("n", "<C-t>", api.tree.change_root_to_parent, opts("Up"))
vim.keymap.set('n', '?', api.tree.toggle_help, opts('Help')) vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help"))
end end
-- pass to setup along with your other options -- pass to setup along with your other options
@@ -245,7 +247,7 @@ via |nvim-tree.on_attach| e.g. >
Run |:NvimTreeHiTest| to show all the highlights that nvim-tree uses. Run |:NvimTreeHiTest| to show all the highlights that nvim-tree uses.
They can be customised before or after setup is called and will be immediately They can be customised before or after setup is called and will be immediately
applied at runtime. e.g. > applied at runtime. e.g. >lua
vim.cmd([[ vim.cmd([[
:hi NvimTreeExecFile guifg=#ffa0a0 :hi NvimTreeExecFile guifg=#ffa0a0
@@ -366,15 +368,15 @@ again to apply a change in configuration without restarting nvim.
setup() function takes one optional argument: configuration table. If omitted setup() function takes one optional argument: configuration table. If omitted
nvim-tree will be initialised with default configuration. nvim-tree will be initialised with default configuration.
>
The first setup() call is cheap: it does nothing more than validate / apply The first setup() call is cheap: it does nothing more than validate / apply
the configuration. Nothing happens until the tree is first opened. the configuration. Nothing happens until the tree is first opened.
Subsequent setup() calls are expensive as they tear down the world before Subsequent setup() calls are expensive as they tear down the world before
applying configuration. applying configuration.
Following is the default configuration. See |nvim-tree-opts| for details. Following is the default configuration. See |nvim-tree-opts| for details. >lua
>
require("nvim-tree").setup { -- BEGIN_DEFAULT_OPTS require("nvim-tree").setup { -- BEGIN_DEFAULT_OPTS
on_attach = "default", on_attach = "default",
hijack_cursor = false, hijack_cursor = false,
@@ -425,6 +427,7 @@ Following is the default configuration. See |nvim-tree-opts| for details.
special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" },
hidden_display = "none", hidden_display = "none",
symlink_destination = true, symlink_destination = true,
decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", },
highlight_git = "none", highlight_git = "none",
highlight_diagnostics = "none", highlight_diagnostics = "none",
highlight_opened_files = "none", highlight_opened_files = "none",
@@ -527,7 +530,7 @@ Following is the default configuration. See |nvim-tree-opts| for details.
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
show_on_open_dirs = true, show_on_open_dirs = true,
debounce_delay = 50, debounce_delay = 500,
severity = { severity = {
min = vim.diagnostic.severity.HINT, min = vim.diagnostic.severity.HINT,
max = vim.diagnostic.severity.ERROR, max = vim.diagnostic.severity.ERROR,
@@ -561,7 +564,12 @@ Following is the default configuration. See |nvim-tree-opts| for details.
filesystem_watchers = { filesystem_watchers = {
enable = true, enable = true,
debounce_delay = 50, debounce_delay = 50,
ignore_dirs = {}, ignore_dirs = {
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
}, },
actions = { actions = {
use_system_clipboard = true, use_system_clipboard = true,
@@ -587,6 +595,7 @@ Following is the default configuration. See |nvim-tree-opts| for details.
quit_on_open = false, quit_on_open = false,
eject = true, eject = true,
resize_window = true, resize_window = true,
relative_path = true,
window_picker = { window_picker = {
enable = true, enable = true,
picker = "default", picker = "default",
@@ -626,11 +635,6 @@ Following is the default configuration. See |nvim-tree-opts| for details.
}, },
}, },
experimental = { experimental = {
actions = {
open_file = {
relative_path = false,
},
},
}, },
log = { log = {
enable = false, enable = false,
@@ -672,7 +676,7 @@ Completely disable netrw
It is strongly advised to eagerly disable netrw, due to race conditions at vim It is strongly advised to eagerly disable netrw, due to race conditions at vim
startup. startup.
Set the following at the very beginning of your `init.lua` / `init.vim`: > Set the following at the very beginning of your `init.lua` / `init.vim`: >lua
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
< <
@@ -734,7 +738,7 @@ Can be one of `"name"`, `"case_sensitive"`, `"modification_time"`, `"extension"`
- `name`: `string` - `name`: `string`
- `type`: `"directory"` | `"file"` | `"link"` - `type`: `"directory"` | `"file"` | `"link"`
Example: sort by name length: > Example: sort by name length: >lua
local sorter = function(nodes) local sorter = function(nodes)
table.sort(nodes, function(a, b) table.sort(nodes, function(a, b)
return #a.name < #b.name return #a.name < #b.name
@@ -841,9 +845,6 @@ Use nvim-tree in a floating window.
============================================================================== ==============================================================================
5.3 OPTS: RENDERER *nvim-tree-opts-renderer* 5.3 OPTS: RENDERER *nvim-tree-opts-renderer*
Highlight precedence, additive:
git < opened < modified < bookmarked < diagnostics < copied < cut
*nvim-tree.renderer.add_trailing* *nvim-tree.renderer.add_trailing*
Appends a trailing slash to folder names. Appends a trailing slash to folder names.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`
@@ -866,7 +867,7 @@ Set to `false` to hide the root folder.
Type: `string` or `boolean` or `function(root_cwd)`, Default: `":~:s?$?/..?"` Type: `string` or `boolean` or `function(root_cwd)`, Default: `":~:s?$?/..?"`
Function is passed the absolute path of the root folder and should Function is passed the absolute path of the root folder and should
return a string. e.g. > return a string. e.g. >lua
my_root_folder_label = function(path) my_root_folder_label = function(path)
return ".../" .. vim.fn.fnamemodify(path, ":t") return ".../" .. vim.fn.fnamemodify(path, ":t")
end end
@@ -898,7 +899,7 @@ Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay
reasons and values are the count of hidden files for that reason. reasons and values are the count of hidden files for that reason.
The `hidden_stats` argument is structured as follows, where <num> is the The `hidden_stats` argument is structured as follows, where <num> is the
number of hidden files related to the field: > number of hidden files related to the field: >lua
hidden_stats = { hidden_stats = {
bookmark = <num>, bookmark = <num>,
buf = <num>, buf = <num>,
@@ -908,7 +909,7 @@ Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay
live_filter = <num>, live_filter = <num>,
} }
< <
Example of function that can be passed: > Example of function that can be passed: >lua
function(hidden_stats) function(hidden_stats)
local total_count = 0 local total_count = 0
for reason, count in pairs(hidden_stats) do for reason, count in pairs(hidden_stats) do
@@ -926,6 +927,22 @@ Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay
Whether to show the destination of the symlink. Whether to show the destination of the symlink.
Type: `boolean`, Default: `true` Type: `boolean`, Default: `true`
*nvim-tree.renderer.decorators*
Highlighting and icons for the nodes, in increasing order of precedence.
Uses strings to specify builtin decorators otherwise specify your
`nvim_tree.api.decorator.UserDecorator` class.
Type: `nvim_tree.api.decorator.Name[]`, Default: >lua
{
"Git",
"Open",
"Hidden",
"Modified",
"Bookmark",
"Diagnostics",
"Copied",
"Cut",
}
<
*nvim-tree.renderer.highlight_git* *nvim-tree.renderer.highlight_git*
Enable highlight for git attributes using `NvimTreeGit*HL` highlight groups. Enable highlight for git attributes using `NvimTreeGit*HL` highlight groups.
Requires |nvim-tree.git.enable| Requires |nvim-tree.git.enable|
@@ -983,7 +1000,7 @@ Configuration options for tree indent markers.
*nvim-tree.renderer.indent_markers.icons* *nvim-tree.renderer.indent_markers.icons*
Icons shown before the file/directory. Length 1. Icons shown before the file/directory. Length 1.
Type: `table`, Default: > Type: `table`, Default: >lua
{ {
corner = "└", corner = "└",
edge = "│", edge = "│",
@@ -995,9 +1012,6 @@ Configuration options for tree indent markers.
*nvim-tree.renderer.icons* *nvim-tree.renderer.icons*
Configuration options for icons. Configuration options for icons.
Icon order and sign column precedence:
git < hidden < modified < bookmarked < diagnostics
`renderer.icons.*_placement` options may be: `renderer.icons.*_placement` options may be:
- `"before"` : before file/folder, after the file/folders icons - `"before"` : before file/folder, after the file/folders icons
- `"after"` : after file/folder - `"after"` : after file/folder
@@ -1271,7 +1285,7 @@ Enable/disable the feature.
*nvim-tree.diagnostics.debounce_delay* *nvim-tree.diagnostics.debounce_delay*
Idle milliseconds between diagnostic event and update. Idle milliseconds between diagnostic event and update.
Type: `number`, Default: `50` (ms) Type: `number`, Default: `500` (ms)
*nvim-tree.diagnostics.show_on_dirs* *nvim-tree.diagnostics.show_on_dirs*
Show diagnostic icons on parent directories. Show diagnostic icons on parent directories.
@@ -1295,7 +1309,7 @@ Severity for which the diagnostics will be displayed. See |diagnostic-severity|
*nvim-tree.diagnostics.icons* *nvim-tree.diagnostics.icons*
Icons for diagnostic severity. Icons for diagnostic severity.
Type: `table`, Default: > Type: `table`, Default: >lua
{ {
hint = "", hint = "",
info = "", info = "",
@@ -1416,8 +1430,14 @@ function returning whether a path should be ignored.
Strings must be backslash escaped e.g. `"my-proj/\\.build$"`. See |string-match|. Strings must be backslash escaped e.g. `"my-proj/\\.build$"`. See |string-match|.
Function is passed an absolute path. Function is passed an absolute path.
Useful when path is not in `.gitignore` or git integration is disabled. Useful when path is not in `.gitignore` or git integration is disabled.
Type: `string[] | fun(path: string): boolean`, Default: `{}` Type: `string[] | fun(path: string): boolean`, Default: >lua
{
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
}
<
============================================================================== ==============================================================================
5.13 OPTS: ACTIONS *nvim-tree-opts-actions* 5.13 OPTS: ACTIONS *nvim-tree-opts-actions*
@@ -1436,8 +1456,6 @@ vim |current-directory| behaviour.
*nvim-tree.actions.change_dir.global* *nvim-tree.actions.change_dir.global*
Use `:cd` instead of `:lcd` when changing directories. Use `:cd` instead of `:lcd` when changing directories.
Consider that this might cause issues with the
|nvim-tree.sync_root_with_cwd| option.
Type: `boolean`, Default: `false` Type: `boolean`, Default: `false`
*nvim-tree.actions.change_dir.restrict_above_cwd* *nvim-tree.actions.change_dir.restrict_above_cwd*
@@ -1488,6 +1506,11 @@ Configuration options for opening a file from nvim-tree.
Resizes the tree when opening a file. Resizes the tree when opening a file.
Type: `boolean`, Default: `true` Type: `boolean`, Default: `true`
*nvim-tree.experimental.actions.open_file.relative_path*
Buffers opened by nvim-tree will use with relative paths instead of
absolute.
Type: `boolean`, Default: `true`
*nvim-tree.actions.open_file.window_picker* *nvim-tree.actions.open_file.window_picker*
Window picker configuration. Window picker configuration.
@@ -1502,10 +1525,10 @@ Configuration options for opening a file from nvim-tree.
or `nil` if an invalid window is picked or user cancelled the action. or `nil` if an invalid window is picked or user cancelled the action.
The picker may create a new window. The picker may create a new window.
Type: `string` | `function`, Default: `"default"` Type: `string` | `function`, Default: `"default"`
e.g. s1n7ax/nvim-window-picker plugin: > e.g. s1n7ax/nvim-window-picker plugin: >lua
window_picker = { window_picker = {
enable = true, enable = true,
picker = require('window-picker').pick_window, picker = require("window-picker").pick_window,
< <
*nvim-tree.actions.open_file.window_picker.chars* *nvim-tree.actions.open_file.window_picker.chars*
A string of chars used as identifiers by the window picker. A string of chars used as identifiers by the window picker.
@@ -1515,7 +1538,7 @@ Configuration options for opening a file from nvim-tree.
Table of buffer option names mapped to a list of option values that Table of buffer option names mapped to a list of option values that
indicates to the picker that the buffer's window should not be indicates to the picker that the buffer's window should not be
selectable. selectable.
Type: `table`, Default: > Type: `table`, Default: >lua
{ {
filetype = { filetype = {
"notify", "notify",
@@ -1616,12 +1639,6 @@ Confirmation prompts.
Experimental features that may become default or optional functionality. Experimental features that may become default or optional functionality.
In the event of a problem please disable the experiment and raise an issue. In the event of a problem please disable the experiment and raise an issue.
*nvim-tree.experimental.actions.open_file.relative_path*
Buffers opened by nvim-tree will use with relative paths instead of
absolute.
Execute |:ls| to see the paths of all open buffers.
Type: `boolean`, Default: `false`
============================================================================== ==============================================================================
5.20 OPTS: LOG *nvim-tree-opts-log* 5.20 OPTS: LOG *nvim-tree-opts-log*
@@ -1674,9 +1691,7 @@ Specify which information to log.
============================================================================== ==============================================================================
6. API *nvim-tree-api* 6. API *nvim-tree-api*
Nvim-tree's public API can be used to access features. Nvim-tree's public API can be used to access features. e.g. >lua
>
e.g. >
local api = require("nvim-tree.api") local api = require("nvim-tree.api")
api.tree.toggle() api.tree.toggle()
< <
@@ -2275,30 +2290,30 @@ Active mappings may be viewed via HELP, default `g?`. The mapping's description
is used when displaying HELP. is used when displaying HELP.
The `on_attach` function is passed the `bufnr` of nvim-tree. Use The `on_attach` function is passed the `bufnr` of nvim-tree. Use
|vim.keymap.set()| or |nvim_set_keymap()| to define mappings as usual. e.g. > |vim.keymap.set()| or |nvim_set_keymap()| to define mappings as usual. e.g. >lua
local function my_on_attach(bufnr) local function my_on_attach(bufnr)
local api = require('nvim-tree.api') local api = require("nvim-tree.api")
local function opts(desc) local function opts(desc)
return { desc = 'nvim-tree: ' .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true }
end end
-- copy default mappings here from defaults in next section -- copy default mappings here from defaults in next section
vim.keymap.set('n', '<C-]>', api.tree.change_root_to_node, opts('CD')) vim.keymap.set("n", "<C-]>", api.tree.change_root_to_node, opts("CD"))
vim.keymap.set('n', '<C-e>', api.node.open.replace_tree_buffer, opts('Open: In Place')) vim.keymap.set("n", "<C-e>", api.node.open.replace_tree_buffer, opts("Open: In Place"))
--- ---
-- OR use all default mappings -- OR use all default mappings
api.config.mappings.default_on_attach(bufnr) api.config.mappings.default_on_attach(bufnr)
-- remove a default -- remove a default
vim.keymap.del('n', '<C-]>', { buffer = bufnr }) vim.keymap.del("n", "<C-]>", { buffer = bufnr })
-- override a default -- override a default
vim.keymap.set('n', '<C-e>', api.tree.reload, opts('Refresh')) vim.keymap.set("n", "<C-e>", api.tree.reload, opts("Refresh"))
-- add your mappings -- add your mappings
vim.keymap.set('n', '?', api.tree.toggle_help, opts('Help')) vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help"))
--- ---
end end
@@ -2315,16 +2330,16 @@ Single left mouse mappings can be achieved via `<LeftRelease>`.
Single right / middle mouse mappings will require changes to |mousemodel| or |mouse|. Single right / middle mouse mappings will require changes to |mousemodel| or |mouse|.
|vim.keymap.set()| {rhs} is a `(function|string)` thus it may be necessary to |vim.keymap.set()| {rhs} is a `(function|string)` thus it may be necessary to
define your own function to map complex functionality e.g. > define your own function to map complex functionality e.g. >lua
local function print_node_path() local function print_node_path()
local api = require('nvim-tree.api') local api = require("nvim-tree.api")
local node = api.tree.get_node_under_cursor() local node = api.tree.get_node_under_cursor()
print(node.absolute_path) print(node.absolute_path)
end end
-- on_attach -- on_attach
vim.keymap.set('n', '<C-P>', print_node_path, opts('Print Path')) vim.keymap.set("n", "<C-P>", print_node_path, opts("Print Path"))
< <
============================================================================== ==============================================================================
7.1 MAPPINGS: DEFAULT *nvim-tree-mappings-default* 7.1 MAPPINGS: DEFAULT *nvim-tree-mappings-default*
@@ -2332,83 +2347,83 @@ define your own function to map complex functionality e.g. >
In the absence of an |nvim-tree.on_attach| function, the following defaults In the absence of an |nvim-tree.on_attach| function, the following defaults
will be applied. will be applied.
You are encouraged to copy these to your own |nvim-tree.on_attach| function. You are encouraged to copy these to your own |nvim-tree.on_attach| function. >lua
>
local api = require('nvim-tree.api') local api = require("nvim-tree.api")
local function opts(desc) local function opts(desc)
return { desc = 'nvim-tree: ' .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true }
end end
-- BEGIN_DEFAULT_ON_ATTACH -- BEGIN_DEFAULT_ON_ATTACH
vim.keymap.set('n', '<C-]>', api.tree.change_root_to_node, opts('CD')) vim.keymap.set("n", "<C-]>", api.tree.change_root_to_node, opts("CD"))
vim.keymap.set('n', '<C-e>', api.node.open.replace_tree_buffer, opts('Open: In Place')) vim.keymap.set("n", "<C-e>", api.node.open.replace_tree_buffer, opts("Open: In Place"))
vim.keymap.set('n', '<C-k>', api.node.show_info_popup, opts('Info')) vim.keymap.set("n", "<C-k>", api.node.show_info_popup, opts("Info"))
vim.keymap.set('n', '<C-r>', api.fs.rename_sub, opts('Rename: Omit Filename')) vim.keymap.set("n", "<C-r>", api.fs.rename_sub, opts("Rename: Omit Filename"))
vim.keymap.set('n', '<C-t>', api.node.open.tab, opts('Open: New Tab')) vim.keymap.set("n", "<C-t>", api.node.open.tab, opts("Open: New Tab"))
vim.keymap.set('n', '<C-v>', api.node.open.vertical, opts('Open: Vertical Split')) vim.keymap.set("n", "<C-v>", api.node.open.vertical, opts("Open: Vertical Split"))
vim.keymap.set('n', '<C-x>', api.node.open.horizontal, opts('Open: Horizontal Split')) vim.keymap.set("n", "<C-x>", api.node.open.horizontal, opts("Open: Horizontal Split"))
vim.keymap.set('n', '<BS>', api.node.navigate.parent_close, opts('Close Directory')) vim.keymap.set("n", "<BS>", api.node.navigate.parent_close, opts("Close Directory"))
vim.keymap.set('n', '<CR>', api.node.open.edit, opts('Open')) vim.keymap.set("n", "<CR>", api.node.open.edit, opts("Open"))
vim.keymap.set('n', '<Tab>', api.node.open.preview, opts('Open Preview')) vim.keymap.set("n", "<Tab>", api.node.open.preview, opts("Open Preview"))
vim.keymap.set('n', '>', api.node.navigate.sibling.next, opts('Next Sibling')) vim.keymap.set("n", ">", api.node.navigate.sibling.next, opts("Next Sibling"))
vim.keymap.set('n', '<', api.node.navigate.sibling.prev, opts('Previous Sibling')) vim.keymap.set("n", "<", api.node.navigate.sibling.prev, opts("Previous Sibling"))
vim.keymap.set('n', '.', api.node.run.cmd, opts('Run Command')) vim.keymap.set("n", ".", api.node.run.cmd, opts("Run Command"))
vim.keymap.set('n', '-', api.tree.change_root_to_parent, opts('Up')) vim.keymap.set("n", "-", api.tree.change_root_to_parent, opts("Up"))
vim.keymap.set('n', 'a', api.fs.create, opts('Create File Or Directory')) vim.keymap.set("n", "a", api.fs.create, opts("Create File Or Directory"))
vim.keymap.set('n', 'bd', api.marks.bulk.delete, opts('Delete Bookmarked')) vim.keymap.set("n", "bd", api.marks.bulk.delete, opts("Delete Bookmarked"))
vim.keymap.set('n', 'bt', api.marks.bulk.trash, opts('Trash Bookmarked')) vim.keymap.set("n", "bt", api.marks.bulk.trash, opts("Trash Bookmarked"))
vim.keymap.set('n', 'bmv', api.marks.bulk.move, opts('Move Bookmarked')) vim.keymap.set("n", "bmv", api.marks.bulk.move, opts("Move Bookmarked"))
vim.keymap.set('n', 'B', api.tree.toggle_no_buffer_filter, opts('Toggle Filter: No Buffer')) vim.keymap.set("n", "B", api.tree.toggle_no_buffer_filter, opts("Toggle Filter: No Buffer"))
vim.keymap.set('n', 'c', api.fs.copy.node, opts('Copy')) vim.keymap.set("n", "c", api.fs.copy.node, opts("Copy"))
vim.keymap.set('n', 'C', api.tree.toggle_git_clean_filter, opts('Toggle Filter: Git Clean')) vim.keymap.set("n", "C", api.tree.toggle_git_clean_filter, opts("Toggle Filter: Git Clean"))
vim.keymap.set('n', '[c', api.node.navigate.git.prev, opts('Prev Git')) vim.keymap.set("n", "[c", api.node.navigate.git.prev, opts("Prev Git"))
vim.keymap.set('n', ']c', api.node.navigate.git.next, opts('Next Git')) vim.keymap.set("n", "]c", api.node.navigate.git.next, opts("Next Git"))
vim.keymap.set('n', 'd', api.fs.remove, opts('Delete')) vim.keymap.set("n", "d", api.fs.remove, opts("Delete"))
vim.keymap.set('n', 'D', api.fs.trash, opts('Trash')) vim.keymap.set("n", "D", api.fs.trash, opts("Trash"))
vim.keymap.set('n', 'E', api.tree.expand_all, opts('Expand All')) vim.keymap.set("n", "E", api.tree.expand_all, opts("Expand All"))
vim.keymap.set('n', 'e', api.fs.rename_basename, opts('Rename: Basename')) 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.next, opts("Next Diagnostic"))
vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic')) vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic"))
vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear')) 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", "f", api.live_filter.start, opts("Live Filter: Start"))
vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help')) 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", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set('n', 'ge', api.fs.copy.basename, opts('Copy Basename')) vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set('n', 'H', api.tree.toggle_hidden_filter, opts('Toggle Filter: Dotfiles')) 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", "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", "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", "K", api.node.navigate.sibling.first, opts("First Sibling"))
vim.keymap.set('n', 'L', api.node.open.toggle_group_empty, opts('Toggle Group Empty')) vim.keymap.set("n", "L", api.node.open.toggle_group_empty, opts("Toggle Group Empty"))
vim.keymap.set('n', 'M', api.tree.toggle_no_bookmark_filter, opts('Toggle Filter: No Bookmark')) 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", "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.edit, opts("Open"))
vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker')) vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
vim.keymap.set('n', 'p', api.fs.paste, opts('Paste')) vim.keymap.set("n", "p", api.fs.paste, opts("Paste"))
vim.keymap.set('n', 'P', api.node.navigate.parent, opts('Parent Directory')) vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory"))
vim.keymap.set('n', 'q', api.tree.close, opts('Close')) vim.keymap.set("n", "q", api.tree.close, opts("Close"))
vim.keymap.set('n', 'r', api.fs.rename, opts('Rename')) vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
vim.keymap.set('n', 'R', api.tree.reload, opts('Refresh')) vim.keymap.set("n", "R", api.tree.reload, opts("Refresh"))
vim.keymap.set('n', 's', api.node.run.system, opts('Run System')) vim.keymap.set("n", "s", api.node.run.system, opts("Run System"))
vim.keymap.set('n', 'S', api.tree.search_node, opts('Search')) vim.keymap.set("n", "S", api.tree.search_node, opts("Search"))
vim.keymap.set('n', 'u', api.fs.rename_full, opts('Rename: Full Path')) vim.keymap.set("n", "u", api.fs.rename_full, opts("Rename: Full Path"))
vim.keymap.set('n', 'U', api.tree.toggle_custom_filter, opts('Toggle Filter: Hidden')) vim.keymap.set("n", "U", api.tree.toggle_custom_filter, opts("Toggle Filter: Hidden"))
vim.keymap.set('n', 'W', api.tree.collapse_all, opts('Collapse')) vim.keymap.set("n", "W", api.tree.collapse_all, opts("Collapse"))
vim.keymap.set('n', 'x', api.fs.cut, opts('Cut')) vim.keymap.set("n", "x", api.fs.cut, opts("Cut"))
vim.keymap.set('n', 'y', api.fs.copy.filename, opts('Copy Name')) vim.keymap.set("n", "y", api.fs.copy.filename, opts("Copy Name"))
vim.keymap.set('n', 'Y', api.fs.copy.relative_path, opts('Copy Relative Path')) vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path"))
vim.keymap.set('n', '<2-LeftMouse>', api.node.open.edit, opts('Open')) vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open"))
vim.keymap.set('n', '<2-RightMouse>', api.tree.change_root_to_node, opts('CD')) vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD"))
-- END_DEFAULT_ON_ATTACH -- END_DEFAULT_ON_ATTACH
< <
Alternatively, you may apply these default mappings from your |nvim-tree.on_attach| via Alternatively, you may apply these default mappings from your |nvim-tree.on_attach| via
|nvim-tree-api.config.mappings.default_on_attach()| e.g. |nvim-tree-api.config.mappings.default_on_attach()| e.g. >lua
>
local function my_on_attach(bufnr) local function my_on_attach(bufnr)
local api = require('nvim-tree.api') local api = require("nvim-tree.api")
local function opts(desc) local function opts(desc)
return { desc = 'nvim-tree: ' .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true }
end end
api.config.mappings.default_on_attach(bufnr) api.config.mappings.default_on_attach(bufnr)
@@ -2423,10 +2438,10 @@ All the following highlight groups can be configured by hand. Aside from
`NvimTreeWindowPicker`, it is not advised to colorize the background of these `NvimTreeWindowPicker`, it is not advised to colorize the background of these
groups. groups.
Example |:highlight| > Example |:highlight| >vim
:hi NvimTreeSymlink guifg=blue gui=bold,underline :hi NvimTreeSymlink guifg=blue gui=bold,underline
< <
It is recommended to enable 'termguicolors' for the more pleasant 24-bit It is recommended to enable |termguicolors| for the more pleasant 24-bit
colours. colours.
To view the nvim-tree highlight groups run |:NvimTreeHiTest| To view the nvim-tree highlight groups run |:NvimTreeHiTest|
@@ -2437,16 +2452,18 @@ as per |:highlight|
The `*HL` groups are additive as per |nvim-tree-opts-renderer| precedence. The `*HL` groups are additive as per |nvim-tree-opts-renderer| precedence.
Only present attributes will clobber each other. Only present attributes will clobber each other.
In this example a modified, opened file will have magenta text, with cyan In this example a modified, opened file will have magenta text, with cyan
undercurl: > undercurl: >vim
:hi NvimTreeOpenedHL guifg=magenta guisp=red gui=underline :hi NvimTreeOpenedHL guifg=magenta guisp=red gui=underline
:hi NvimTreeModifiedFileHL guisp=cyan gui=undercurl :hi NvimTreeModifiedFileHL guisp=cyan gui=undercurl
< <
To prevent usage of a highlight: To prevent usage of a highlight:
- Before setup: link the group to `Normal` e.g.
`:hi NvimTreeExecFile Normal`
- After setup: link it to `NONE`, to override the default link e.g.
`:hi! link NvimTreeExecFile NONE`
- Before setup: link the group to `Normal` e.g. >vim
:hi NvimTreeExecFile Normal
<
- After setup: link it to `NONE`, to override the default link e.g. >lua
:hi! link NvimTreeExecFile NONE
<
============================================================================== ==============================================================================
8.1 HIGHLIGHT: DEFAULT *nvim-tree-highlight-default* 8.1 HIGHLIGHT: DEFAULT *nvim-tree-highlight-default*
@@ -2601,7 +2618,7 @@ See |nvim-tree-legacy-highlight| for old highlight group compatibility.
- NvimTreeSpecialFile PreProc -> SpellCap - NvimTreeSpecialFile PreProc -> SpellCap
- NvimTreeSymlink Statement -> SpellCap - NvimTreeSymlink Statement -> SpellCap
Approximate pre-overhaul values for the `SpellCap` groups may be set via: > Approximate pre-overhaul values for the `SpellCap` groups may be set via: >lua
vim.cmd([[ vim.cmd([[
:hi NvimTreeExecFile gui=bold guifg=#ffa0a0 :hi NvimTreeExecFile gui=bold guifg=#ffa0a0
@@ -2628,7 +2645,8 @@ to |nvim_tree_registering_handlers| for more information.
Handlers are registered by calling |nvim-tree-api.events.subscribe()| Handlers are registered by calling |nvim-tree-api.events.subscribe()|
function with an |nvim-tree-api.events.Event| function with an |nvim-tree-api.events.Event|
e.g. handler for node renamed: > e.g. handler for node renamed: >lua
local api = require("nvim-tree.api") local api = require("nvim-tree.api")
local Event = api.events.Event local Event = api.events.Event
@@ -2715,7 +2733,8 @@ There are two special startup events in the form of User autocommands:
Immediately before firing: a global variable of the same name will be set to a Immediately before firing: a global variable of the same name will be set to a
value of 1. value of 1.
Example subscription: > Example subscription: >lua
vim.api.nvim_create_autocmd("User", { vim.api.nvim_create_autocmd("User", {
pattern = "NvimTreeRequired", pattern = "NvimTreeRequired",
callback = function(data) callback = function(data)
@@ -2749,16 +2768,102 @@ configurations for different types of prompts.
send all bookmarked to trash during |nvim-tree-api.marks.bulk.trash()| send all bookmarked to trash during |nvim-tree-api.marks.bulk.trash()|
============================================================================== ==============================================================================
11. OS SPECIFIC RESTRICTIONS *nvim-tree-os-specific* 11. DECORATORS *nvim-tree-decorators*
Highlighting and icons for nodes are provided by Decorators. You may provide
your own in addition to the builtin decorators.
Decorators may:
- Add icons
- Set highlight group for the name or icons
- Override node icon
Specify decorators and their precedence via |nvim-tree.renderer.decorators|
e.g. defaults with a user decorator class being overridden only by Cut: >lua
{
"Git",
"Open",
"Hidden",
"Modified",
"Bookmark",
"Diagnostics",
"Copied",
MyDecorator,
"Cut",
}
See `nvim-tree/_meta/api_decorator.lua` for full
`nvim_tree.api.decorator.UserDecorator` class documentation.
<
==============================================================================
11.1. DECORATOR EXAMPLE *nvim-tree-decorator-example*
>lua
---Create your decorator class
---@class (exact) MyDecorator: nvim_tree.api.decorator.UserDecorator
---@field private my_icon nvim_tree.api.HighlightedString
local MyDecorator = require("nvim-tree.api").decorator.UserDecorator:extend()
---Mandatory constructor :new() will be called once per tree render, with no arguments.
function MyDecorator:new()
self.enabled = true
self.highlight_range = "all"
self.icon_placement = "signcolumn"
-- create your icon once, for convenience
self.my_icon = { str = "I", hl = { "MyIcon" } }
-- Define the icon sign only once
-- Only needed if you are using icon_placement = "signcolumn"
self:define_sign(self.my_icon)
end
---Override node icon
---@param node nvim_tree.api.Node
---@return nvim_tree.api.HighlightedString? icon_node
function MyDecorator:icon_node(node)
if node.name == "example" then
return self.my_icon
else
return nil
end
end
---Return one icon for DecoratorIconPlacement
---@param node nvim_tree.api.Node
---@return nvim_tree.api.HighlightedString[]? icons
function MyDecorator:icons(node)
if node.name == "example" then
return { self.my_icon }
else
return nil
end
end
---Exactly one highlight group for DecoratorHighlightRange
---@param node nvim_tree.api.Node
---@return string? highlight_group
function MyDecorator:highlight_group(node)
if node.name == "example" then
return "MyHighlight"
else
return nil
end
end
<
==============================================================================
12. OS SPECIFIC RESTRICTIONS *nvim-tree-os-specific*
Windows WSL and PowerShell Windows WSL and PowerShell
- Trash is synchronized - Trash is synchronized
- Executable file detection is disabled as this is non-performant and can - Executable file detection is disabled as this is non-performant and can
freeze nvim freeze nvim
- Some filesystem watcher error related to permissions will not be reported - Some filesystem watcher error related to permissions will not be reported
- Some users have reported unspecified issues with
|nvim-tree.experimental.actions.open_file.relative_path|. Please report any
issues or disable this feature.
============================================================================== ==============================================================================
12. NETRW *nvim-tree-netrw* 13. NETRW *nvim-tree-netrw*
|netrw| is a standard neovim plugin that is enabled by default. It provides, |netrw| is a standard neovim plugin that is enabled by default. It provides,
amongst other functionality, a file/directory browser. amongst other functionality, a file/directory browser.
@@ -2767,7 +2872,7 @@ It interferes with nvim-tree and the intended user experience is nvim-tree
replacing the |netrw| browser. replacing the |netrw| browser.
It is strongly recommended to disable |netrw|. As it is a bundled plugin it It is strongly recommended to disable |netrw|. As it is a bundled plugin it
must be disabled manually at the start of your `init.lua` as per |netrw-noload|: > must be disabled manually at the start of your `init.lua` as per |netrw-noload|: >lua
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
@@ -2779,14 +2884,14 @@ keep using |netrw| without its browser features please ensure:
|nvim-tree.hijack_netrw| ` = true` |nvim-tree.hijack_netrw| ` = true`
============================================================================== ==============================================================================
13. LEGACY *nvim-tree-legacy* 14. LEGACY *nvim-tree-legacy*
Breaking refactors have been made however the legacy versions will be silently Breaking refactors have been made however the legacy versions will be silently
migrated and used. migrated and used.
There are no plans to remove this migration. There are no plans to remove this migration.
============================================================================== ==============================================================================
13.1 LEGACY: OPTS *nvim-tree-legacy-opts* 14.1 LEGACY: OPTS *nvim-tree-legacy-opts*
Legacy options are translated to the current, making type and value changes as Legacy options are translated to the current, making type and value changes as
needed. needed.
@@ -2804,7 +2909,7 @@ needed.
`renderer.icons.webdev_colors` |nvim-tree.renderer.icons.web_devicons.file.color| `renderer.icons.webdev_colors` |nvim-tree.renderer.icons.web_devicons.file.color|
============================================================================== ==============================================================================
13.2 LEGACY: HIGHLIGHT *nvim-tree-legacy-highlight* 14.2 LEGACY: HIGHLIGHT *nvim-tree-legacy-highlight*
Legacy highlight group are still obeyed when they are defined and the current Legacy highlight group are still obeyed when they are defined and the current
highlight group is not, hard linking as follows: > highlight group is not, hard linking as follows: >
@@ -2853,10 +2958,10 @@ highlight group is not, hard linking as follows: >
NvimTreeLspDiagnosticsHintFolderText NvimTreeDiagnosticHintFolderHL NvimTreeLspDiagnosticsHintFolderText NvimTreeDiagnosticHintFolderHL
< <
============================================================================== ==============================================================================
14 INDEX *nvim-tree-index* 15 INDEX *nvim-tree-index*
============================================================================== ==============================================================================
14.1 INDEX: OPTS *nvim-tree-index-opts* 15.1 INDEX: OPTS *nvim-tree-index-opts*
|nvim-tree.actions.change_dir| |nvim-tree.actions.change_dir|
|nvim-tree.actions.change_dir.enable| |nvim-tree.actions.change_dir.enable|
@@ -2934,6 +3039,7 @@ highlight group is not, hard linking as follows: >
|nvim-tree.prefer_startup_root| |nvim-tree.prefer_startup_root|
|nvim-tree.reload_on_bufenter| |nvim-tree.reload_on_bufenter|
|nvim-tree.renderer.add_trailing| |nvim-tree.renderer.add_trailing|
|nvim-tree.renderer.decorators|
|nvim-tree.renderer.full_name| |nvim-tree.renderer.full_name|
|nvim-tree.renderer.group_empty| |nvim-tree.renderer.group_empty|
|nvim-tree.renderer.hidden_display| |nvim-tree.renderer.hidden_display|
@@ -3024,7 +3130,7 @@ highlight group is not, hard linking as follows: >
|nvim-tree.view.width.padding| |nvim-tree.view.width.padding|
============================================================================== ==============================================================================
14.2 INDEX: API *nvim-tree-index-api* 15.2 INDEX: API *nvim-tree-index-api*
|nvim-tree-api.commands.get()| |nvim-tree-api.commands.get()|
|nvim-tree-api.config.mappings.default_on_attach()| |nvim-tree-api.config.mappings.default_on_attach()|

View File

@@ -1,6 +1,4 @@
local lib = require("nvim-tree.lib")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local appearance = require("nvim-tree.appearance")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local actions = require("nvim-tree.actions") local actions = require("nvim-tree.actions")
@@ -115,27 +113,6 @@ function M.open_on_directory()
actions.root.change_dir.force_dirchange(bufname, true) actions.root.change_dir.force_dirchange(bufname, true)
end end
function M.place_cursor_on_node()
local ok, search = pcall(vim.fn.searchcount)
if ok and search and search.exact_match == 1 then
return
end
local node = lib.get_node_at_cursor()
if not node or node.name == ".." then
return
end
node = utils.get_parent_of_group(node)
local line = vim.api.nvim_get_current_line()
local cursor = vim.api.nvim_win_get_cursor(0)
local idx = vim.fn.stridx(line, node.name)
if idx >= 0 then
vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
end
end
---@return table ---@return table
function M.get_config() function M.get_config()
return M.config return M.config
@@ -173,19 +150,6 @@ local function setup_autocommands(opts)
vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts)) vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts))
end end
-- reset and draw (highlights) when colorscheme is changed
create_nvim_tree_autocmd("ColorScheme", {
callback = function()
appearance.setup()
view.reset_winhl()
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end,
})
-- prevent new opened file from opening in the same window as nvim-tree -- prevent new opened file from opening in the same window as nvim-tree
create_nvim_tree_autocmd("BufWipeout", { create_nvim_tree_autocmd("BufWipeout", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
@@ -201,76 +165,9 @@ local function setup_autocommands(opts)
end, end,
}) })
create_nvim_tree_autocmd("BufWritePost", {
callback = function()
if opts.auto_reload_on_write and not opts.filesystem_watchers.enable then
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end
end,
})
create_nvim_tree_autocmd("BufReadPost", {
callback = function(data)
-- update opened file buffers
local explorer = core.get_explorer()
if not explorer then
return
end
if
(explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
explorer:reload_explorer()
end)
end
end,
})
create_nvim_tree_autocmd("BufUnload", {
callback = function(data)
-- update opened file buffers
local explorer = core.get_explorer()
if not explorer then
return
end
if
(explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
explorer:reload_explorer()
end)
end
end,
})
create_nvim_tree_autocmd("User", {
pattern = { "FugitiveChanged", "NeogitStatusRefreshed" },
callback = function()
if not opts.filesystem_watchers.enable and opts.git.enable then
local explorer = core.get_explorer()
if explorer then
explorer:reload_git()
end
end
end,
})
if opts.tab.sync.open then if opts.tab.sync.open then
create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) }) create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
end end
if opts.hijack_cursor then
create_nvim_tree_autocmd("CursorMoved", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
M.place_cursor_on_node()
end
end,
})
end
if opts.sync_root_with_cwd then if opts.sync_root_with_cwd then
create_nvim_tree_autocmd("DirChanged", { create_nvim_tree_autocmd("DirChanged", {
callback = function() callback = function()
@@ -296,20 +193,6 @@ local function setup_autocommands(opts)
create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory }) create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory })
end end
create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
if vim.fn.getcwd() ~= core.get_cwd() or (opts.reload_on_bufenter and not opts.filesystem_watchers.enable) then
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end
end
end,
})
if opts.view.centralize_selection then if opts.view.centralize_selection then
create_nvim_tree_autocmd("BufEnter", { create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
@@ -325,16 +208,16 @@ local function setup_autocommands(opts)
if opts.diagnostics.enable then if opts.diagnostics.enable then
create_nvim_tree_autocmd("DiagnosticChanged", { create_nvim_tree_autocmd("DiagnosticChanged", {
callback = function() callback = function(ev)
log.line("diagnostics", "DiagnosticChanged") log.line("diagnostics", "DiagnosticChanged")
require("nvim-tree.diagnostics").update() require("nvim-tree.diagnostics").update_lsp(ev)
end, end,
}) })
create_nvim_tree_autocmd("User", { create_nvim_tree_autocmd("User", {
pattern = "CocDiagnosticChange", pattern = "CocDiagnosticChange",
callback = function() callback = function()
log.line("diagnostics", "CocDiagnosticChange") log.line("diagnostics", "CocDiagnosticChange")
require("nvim-tree.diagnostics").update() require("nvim-tree.diagnostics").update_coc()
end, end,
}) })
end end
@@ -349,20 +232,6 @@ local function setup_autocommands(opts)
end, end,
}) })
end end
if opts.modified.enable then
create_nvim_tree_autocmd({ "BufModifiedSet", "BufWritePost" }, {
callback = function()
utils.debounce("Buf:modified", opts.view.debounce_delay, function()
require("nvim-tree.buffers").reload_modified()
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end)
end,
})
end
end end
local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
@@ -415,6 +284,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" },
hidden_display = "none", hidden_display = "none",
symlink_destination = true, symlink_destination = true,
decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", },
highlight_git = "none", highlight_git = "none",
highlight_diagnostics = "none", highlight_diagnostics = "none",
highlight_opened_files = "none", highlight_opened_files = "none",
@@ -517,7 +387,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
show_on_open_dirs = true, show_on_open_dirs = true,
debounce_delay = 50, debounce_delay = 500,
severity = { severity = {
min = vim.diagnostic.severity.HINT, min = vim.diagnostic.severity.HINT,
max = vim.diagnostic.severity.ERROR, max = vim.diagnostic.severity.ERROR,
@@ -551,7 +421,12 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
filesystem_watchers = { filesystem_watchers = {
enable = true, enable = true,
debounce_delay = 50, debounce_delay = 50,
ignore_dirs = {}, ignore_dirs = {
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
}, },
actions = { actions = {
use_system_clipboard = true, use_system_clipboard = true,
@@ -577,6 +452,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
quit_on_open = false, quit_on_open = false,
eject = true, eject = true,
resize_window = true, resize_window = true,
relative_path = true,
window_picker = { window_picker = {
enable = true, enable = true,
picker = "default", picker = "default",
@@ -616,11 +492,6 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
}, },
}, },
experimental = { experimental = {
actions = {
open_file = {
relative_path = false,
},
},
}, },
log = { log = {
enable = false, enable = false,
@@ -800,13 +671,16 @@ local function localise_default_opts()
end end
function M.purge_all_state() function M.purge_all_state()
require("nvim-tree.watcher").purge_watchers()
view.close_all_tabs() view.close_all_tabs()
view.abandon_all_windows() view.abandon_all_windows()
if core.get_explorer() ~= nil then local explorer = core.get_explorer()
if explorer then
require("nvim-tree.git").purge_state() require("nvim-tree.git").purge_state()
explorer:destroy()
core.reset_explorer() core.reset_explorer()
end end
-- purge orphaned that were not destroyed by their nodes
require("nvim-tree.watcher").purge_watchers()
end end
---@param conf table|nil ---@param conf table|nil
@@ -849,7 +723,8 @@ function M.setup(conf)
require("nvim-tree.keymap").setup(opts) require("nvim-tree.keymap").setup(opts)
require("nvim-tree.appearance").setup() require("nvim-tree.appearance").setup()
require("nvim-tree.diagnostics").setup(opts) require("nvim-tree.diagnostics").setup(opts)
require("nvim-tree.explorer").setup(opts) require("nvim-tree.explorer"):setup(opts)
require("nvim-tree.explorer.watch").setup(opts)
require("nvim-tree.git").setup(opts) require("nvim-tree.git").setup(opts)
require("nvim-tree.git.utils").setup(opts) require("nvim-tree.git.utils").setup(opts)
require("nvim-tree.view").setup(opts) require("nvim-tree.view").setup(opts)
@@ -858,9 +733,6 @@ function M.setup(conf)
require("nvim-tree.buffers").setup(opts) require("nvim-tree.buffers").setup(opts)
require("nvim-tree.help").setup(opts) require("nvim-tree.help").setup(opts)
require("nvim-tree.watcher").setup(opts) require("nvim-tree.watcher").setup(opts)
if M.config.renderer.icons.show.file and pcall(require, "nvim-web-devicons") then
require("nvim-web-devicons").setup()
end
setup_autocommands(opts) setup_autocommands(opts)

View File

@@ -0,0 +1,51 @@
---@meta
error("Cannot require a meta file")
--
-- Nodes
--
---Base Node, Abstract
---@class (exact) nvim_tree.api.Node
---@field type "file" | "directory" | "link" uv.fs_stat.result.type
---@field absolute_path string
---@field executable boolean
---@field fs_stat uv.fs_stat.result?
---@field git_status GitNodeStatus?
---@field hidden boolean
---@field name string
---@field parent nvim_tree.api.DirectoryNode?
---@field diag_severity lsp.DiagnosticSeverity?
---File
---@class (exact) nvim_tree.api.FileNode: nvim_tree.api.Node
---@field extension string
---Directory
---@class (exact) nvim_tree.api.DirectoryNode: nvim_tree.api.Node
---@field has_children boolean
---@field nodes nvim_tree.api.Node[]
---@field open boolean
---Root Directory
---@class (exact) nvim_tree.api.RootNode: nvim_tree.api.DirectoryNode
---Link mixin
---@class (exact) nvim_tree.api.LinkNode
---@field link_to string
---@field fs_stat_target uv.fs_stat.result
---File Link
---@class (exact) nvim_tree.api.FileLinkNode: nvim_tree.api.FileNode, nvim_tree.api.LinkNode
---DirectoryLink
---@class (exact) nvim_tree.api.DirectoryLinkNode: nvim_tree.api.DirectoryNode, nvim_tree.api.LinkNode
--
-- Various Types
--
---A string for rendering, with optional highlight groups to apply to it
---@class (exact) nvim_tree.api.HighlightedString
---@field str string
---@field hl string[]

View File

@@ -0,0 +1,54 @@
---@meta
error("Cannot require a meta file")
local nvim_tree = { api = { decorator = {} } }
---Highlight group range as per nvim-tree.renderer.highlight_*
---@alias nvim_tree.api.decorator.HighlightRange "none" | "icon" | "name" | "all"
---Icon position as per renderer.icons.*_placement
---@alias nvim_tree.api.decorator.IconPlacement "none" | "before" | "after" | "signcolumn" | "right_align"
---Names of builtin decorators or your decorator classes. Builtins are ordered lowest to highest priority.
---@alias nvim_tree.api.decorator.Name "Git" | "Opened" | "Hidden" | "Modified" | "Bookmarks" | "Diagnostics" | "Copied" | "Cut" | nvim_tree.api.decorator.UserDecorator
---Custom decorator, see :help nvim-tree-decorators
---
---@class (exact) nvim_tree.api.decorator.UserDecorator
---@field protected enabled boolean
---@field protected highlight_range nvim_tree.api.decorator.HighlightRange
---@field protected icon_placement nvim_tree.api.decorator.IconPlacement
nvim_tree.api.decorator.UserDecorator = {}
---Create your decorator class
---
function nvim_tree.api.decorator.UserDecorator:extend() end
---Abstract: no-args constructor must be implemented and will be called once per tree render.
---Must set all fields.
---
function nvim_tree.api.decorator.UserDecorator:new() end
---Abstract: optionally implement to set the node's icon
---
---@param node nvim_tree.api.Node
---@return nvim_tree.api.HighlightedString? icon_node
function nvim_tree.api.decorator.UserDecorator:icon_node(node) end
---Abstract: optionally implement to provide icons and the highlight groups for your icon_placement.
---
---@param node nvim_tree.api.Node
---@return nvim_tree.api.HighlightedString[]? icons
function nvim_tree.api.decorator.UserDecorator:icons(node) end
---Abstract: optionally implement to provide one highlight group to apply to your highlight_range.
---
---@param node nvim_tree.api.Node
---@return string? highlight_group
function nvim_tree.api.decorator.UserDecorator:highlight_group(node) end
---Define a sign. This should be called in the constructor.
---
---@protected
---@param icon nvim_tree.api.HighlightedString?
function nvim_tree.api.decorator.UserDecorator:define_sign(icon) end

View File

@@ -2,6 +2,8 @@ local log = require("nvim-tree.log")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local DirectoryNode = require("nvim-tree.node.directory")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local M = {} local M = {}
@@ -58,19 +60,27 @@ function M.fn(path)
local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator) local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator)
if abs_match or link_match then if abs_match or link_match then
if not node.group_next then local dir = node:as(DirectoryNode)
node.open = true if dir then
end if not dir.group_next then
if #node.nodes == 0 then dir.open = true
core.get_explorer():expand(node) end
if node.group_next and incremented_line then if #dir.nodes == 0 then
line = line - 1 core.get_explorer():expand(dir)
if dir.group_next and incremented_line then
line = line - 1
end
end end
end end
end end
end) end)
:recursor(function(node) :recursor(function(node)
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) node = node and node:as(DirectoryNode)
if node then
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
else
return nil
end
end) end)
:iterate() :iterate()

View File

@@ -7,37 +7,39 @@ local notify = require("nvim-tree.notify")
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
---@enum ACTION local Class = require("nvim-tree.classic")
local ACTION = { local DirectoryNode = require("nvim-tree.node.directory")
copy = "copy",
cut = "cut",
}
---@class Clipboard to handle all actions.fs clipboard API ---@alias ClipboardAction "copy" | "cut"
---@field config table hydrated user opts.filters ---@alias ClipboardData table<ClipboardAction, Node[]>
---@alias ClipboardActionFn fun(source: string, dest: string): boolean, string?
---@class (exact) Clipboard: Class
---@field private explorer Explorer ---@field private explorer Explorer
---@field private data table<ACTION, Node[]> ---@field private data ClipboardData
local Clipboard = {} ---@field private clipboard_name string
---@field private reg string
local Clipboard = Class:extend()
---@param opts table user options ---@class Clipboard
---@param explorer Explorer ---@overload fun(args: ClipboardArgs): Clipboard
---@return Clipboard
function Clipboard:new(opts, explorer) ---@class (exact) ClipboardArgs
local o = { ---@field explorer Explorer
explorer = explorer,
data = { ---@protected
[ACTION.copy] = {}, ---@param args ClipboardArgs
[ACTION.cut] = {}, function Clipboard:new(args)
}, self.explorer = args.explorer
config = {
filesystem_watchers = opts.filesystem_watchers, self.data = {
actions = opts.actions, copy = {},
}, cut = {},
} }
setmetatable(o, self) self.clipboard_name = self.explorer.opts.actions.use_system_clipboard and "system" or "neovim"
self.__index = self self.reg = self.explorer.opts.actions.use_system_clipboard and "+" or "1"
return o
end end
---@param source string ---@param source string
@@ -45,13 +47,11 @@ end
---@return boolean ---@return boolean
---@return string|nil ---@return string|nil
local function do_copy(source, destination) local function do_copy(source, destination)
local source_stats, handle local source_stats, err = vim.loop.fs_stat(source)
local success, errmsg
source_stats, errmsg = vim.loop.fs_stat(source)
if not source_stats then if not source_stats then
log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, errmsg) log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, err)
return false, errmsg return false, err
end end
log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination)
@@ -62,25 +62,28 @@ local function do_copy(source, destination)
end end
if source_stats.type == "file" then if source_stats.type == "file" then
success, errmsg = vim.loop.fs_copyfile(source, destination) local success
success, err = vim.loop.fs_copyfile(source, destination)
if not success then if not success then
log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg) log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err)
return false, errmsg return false, err
end end
return true return true
elseif source_stats.type == "directory" then elseif source_stats.type == "directory" then
handle, errmsg = vim.loop.fs_scandir(source) local handle
handle, err = vim.loop.fs_scandir(source)
if type(handle) == "string" then if type(handle) == "string" then
return false, handle return false, handle
elseif not handle then elseif not handle then
log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, errmsg) log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, err)
return false, errmsg return false, err
end end
success, errmsg = vim.loop.fs_mkdir(destination, source_stats.mode) local success
success, err = vim.loop.fs_mkdir(destination, source_stats.mode)
if not success then if not success then
log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, errmsg) log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, err)
return false, errmsg return false, err
end end
while true do while true do
@@ -91,15 +94,15 @@ local function do_copy(source, destination)
local new_name = utils.path_join({ source, name }) local new_name = utils.path_join({ source, name })
local new_destination = utils.path_join({ destination, name }) local new_destination = utils.path_join({ destination, name })
success, errmsg = do_copy(new_name, new_destination) success, err = do_copy(new_name, new_destination)
if not success then if not success then
return false, errmsg return false, err
end end
end end
else else
errmsg = string.format("'%s' illegal file type '%s'", source, source_stats.type) err = string.format("'%s' illegal file type '%s'", source, source_stats.type)
log.line("copy_paste", "do_copy %s", errmsg) log.line("copy_paste", "do_copy %s", err)
return false, errmsg return false, err
end end
return true return true
@@ -107,28 +110,26 @@ end
---@param source string ---@param source string
---@param dest string ---@param dest string
---@param action ACTION ---@param action ClipboardAction
---@param action_fn fun(source: string, dest: string) ---@param action_fn ClipboardActionFn
---@return boolean|nil -- success ---@return boolean|nil -- success
---@return string|nil -- error message ---@return string|nil -- error message
local function do_single_paste(source, dest, action, action_fn) local function do_single_paste(source, dest, action, action_fn)
local dest_stats
local success, errmsg, errcode
local notify_source = notify.render_path(source) local notify_source = notify.render_path(source)
log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest)
dest_stats, errmsg, errcode = vim.loop.fs_stat(dest) local dest_stats, err, err_name = vim.loop.fs_stat(dest)
if not dest_stats and errcode ~= "ENOENT" then if not dest_stats and err_name ~= "ENOENT" then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???"))
return false, errmsg return false, err
end end
local function on_process() local function on_process()
success, errmsg = action_fn(source, dest) local success, error = action_fn(source, dest)
if not success then if not success then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???"))
return false, errmsg return false, error
end end
find_file(utils.path_remove_trailing(dest)) find_file(utils.path_remove_trailing(dest))
@@ -171,7 +172,7 @@ local function do_single_paste(source, dest, action, action_fn)
end end
---@param node Node ---@param node Node
---@param clip table ---@param clip ClipboardData
local function toggle(node, clip) local function toggle(node, clip)
if node.name == ".." then if node.name == ".." then
return return
@@ -189,8 +190,8 @@ end
---Clear copied and cut ---Clear copied and cut
function Clipboard:clear_clipboard() function Clipboard:clear_clipboard()
self.data[ACTION.copy] = {} self.data.copy = {}
self.data[ACTION.cut] = {} self.data.cut = {}
notify.info("Clipboard has been emptied.") notify.info("Clipboard has been emptied.")
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
@@ -198,29 +199,32 @@ end
---Copy one node ---Copy one node
---@param node Node ---@param node Node
function Clipboard:copy(node) function Clipboard:copy(node)
utils.array_remove(self.data[ACTION.cut], node) utils.array_remove(self.data.cut, node)
toggle(node, self.data[ACTION.copy]) toggle(node, self.data.copy)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
---Cut one node ---Cut one node
---@param node Node ---@param node Node
function Clipboard:cut(node) function Clipboard:cut(node)
utils.array_remove(self.data[ACTION.copy], node) utils.array_remove(self.data.copy, node)
toggle(node, self.data[ACTION.cut]) toggle(node, self.data.cut)
self.explorer.renderer:draw() self.explorer.renderer:draw()
end end
---Paste cut or cop ---Paste cut or cop
---@private ---@private
---@param node Node ---@param node Node
---@param action ACTION ---@param action ClipboardAction
---@param action_fn fun(source: string, dest: string) ---@param action_fn ClipboardActionFn
function Clipboard:do_paste(node, action, action_fn) function Clipboard:do_paste(node, action, action_fn)
node = lib.get_last_group_node(node) if node.name == ".." then
local explorer = core.get_explorer() node = self.explorer
if node.name == ".." and explorer then else
node = explorer local dir = node:as(DirectoryNode)
if dir then
node = dir:last_group_node()
end
end end
local clip = self.data[action] local clip = self.data[action]
if #clip == 0 then if #clip == 0 then
@@ -228,10 +232,10 @@ function Clipboard:do_paste(node, action, action_fn)
end end
local destination = node.absolute_path local destination = node.absolute_path
local stats, errmsg, errcode = vim.loop.fs_stat(destination) local stats, err, err_name = vim.loop.fs_stat(destination)
if not stats and errcode ~= "ENOENT" then if not stats and err_name ~= "ENOENT" then
log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, errmsg) log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err)
notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (errmsg or "???")) notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???"))
return return
end end
local is_dir = stats and stats.type == "directory" local is_dir = stats and stats.type == "directory"
@@ -245,7 +249,7 @@ function Clipboard:do_paste(node, action, action_fn)
end end
self.data[action] = {} self.data[action] = {}
if not self.config.filesystem_watchers.enable then if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer() self.explorer:reload_explorer()
end end
end end
@@ -276,24 +280,24 @@ end
---Paste cut (if present) or copy (if present) ---Paste cut (if present) or copy (if present)
---@param node Node ---@param node Node
function Clipboard:paste(node) function Clipboard:paste(node)
if self.data[ACTION.cut][1] ~= nil then if self.data.cut[1] ~= nil then
self:do_paste(node, ACTION.cut, do_cut) self:do_paste(node, "cut", do_cut)
elseif self.data[ACTION.copy][1] ~= nil then elseif self.data.copy[1] ~= nil then
self:do_paste(node, ACTION.copy, do_copy) self:do_paste(node, "copy", do_copy)
end end
end end
function Clipboard:print_clipboard() function Clipboard:print_clipboard()
local content = {} local content = {}
if #self.data[ACTION.cut] > 0 then if #self.data.cut > 0 then
table.insert(content, "Cut") table.insert(content, "Cut")
for _, node in pairs(self.data[ACTION.cut]) do for _, node in pairs(self.data.cut) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path))) table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end end
end end
if #self.data[ACTION.copy] > 0 then if #self.data.copy > 0 then
table.insert(content, "Copy") table.insert(content, "Copy")
for _, node in pairs(self.data[ACTION.copy]) do for _, node in pairs(self.data.copy) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path))) table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end end
end end
@@ -303,65 +307,45 @@ end
---@param content string ---@param content string
function Clipboard:copy_to_reg(content) function Clipboard:copy_to_reg(content)
local clipboard_name
local reg
if self.config.actions.use_system_clipboard == true then
clipboard_name = "system"
reg = "+"
else
clipboard_name = "neovim"
reg = "1"
end
-- manually firing TextYankPost does not set vim.v.event -- manually firing TextYankPost does not set vim.v.event
-- workaround: create a scratch buffer with the clipboard contents and send a yank command -- workaround: create a scratch buffer with the clipboard contents and send a yank command
local temp_buf = vim.api.nvim_create_buf(false, true) local temp_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content }) vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content })
vim.api.nvim_buf_call(temp_buf, function() vim.api.nvim_buf_call(temp_buf, function()
vim.cmd(string.format('normal! "%sy$', reg)) vim.cmd(string.format('normal! "%sy$', self.reg))
end) end)
vim.api.nvim_buf_delete(temp_buf, {}) vim.api.nvim_buf_delete(temp_buf, {})
notify.info(string.format("Copied %s to %s clipboard!", content, clipboard_name)) notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
end end
---@param node Node ---@param node Node
function Clipboard:copy_filename(node) function Clipboard:copy_filename(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- root
content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t") self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t"))
else else
-- node -- node
content = node.name self:copy_to_reg(node.name)
end end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
function Clipboard:copy_basename(node) function Clipboard:copy_basename(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- root
content = vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r") self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r"))
else else
-- node -- node
content = vim.fn.fnamemodify(node.name, ":r") self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
end end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
function Clipboard:copy_path(node) function Clipboard:copy_path(node)
local content
if node.name == ".." then if node.name == ".." then
-- root -- root
content = utils.path_add_trailing("") self:copy_to_reg(utils.path_add_trailing(""))
else else
-- node -- node
local absolute_path = node.absolute_path local absolute_path = node.absolute_path
@@ -371,10 +355,12 @@ function Clipboard:copy_path(node)
end end
local relative_path = utils.path_relative(absolute_path, cwd) local relative_path = utils.path_relative(absolute_path, cwd)
content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path if node:is(DirectoryNode) then
self:copy_to_reg(utils.path_add_trailing(relative_path))
else
self:copy_to_reg(relative_path)
end
end end
self:copy_to_reg(content)
end end
---@param node Node ---@param node Node
@@ -392,14 +378,14 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
function Clipboard:is_cut(node) function Clipboard:is_cut(node)
return vim.tbl_contains(self.data[ACTION.cut], node) return vim.tbl_contains(self.data.cut, node)
end end
---Node is copied. Will not be cut. ---Node is copied. Will not be cut.
---@param node Node ---@param node Node
---@return boolean ---@return boolean
function Clipboard:is_copied(node) function Clipboard:is_copied(node)
return vim.tbl_contains(self.data[ACTION.copy], node) return vim.tbl_contains(self.data.copy, node)
end end
return Clipboard return Clipboard

View File

@@ -1,11 +1,13 @@
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local lib = require("nvim-tree.lib")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@param file string ---@param file string
@@ -30,34 +32,21 @@ local function get_num_nodes(iter)
return i return i
end end
---@param node Node ---@param node Node?
---@return string
local function get_containing_folder(node)
if node.nodes ~= nil then
return utils.path_add_trailing(node.absolute_path)
end
local node_name_size = #(node.name or "")
return node.absolute_path:sub(0, -node_name_size - 1)
end
---@param node Node|nil
function M.fn(node) function M.fn(node)
local cwd = core.get_cwd() node = node or core.get_explorer()
if cwd == nil then if not node then
return return
end end
node = node and lib.get_last_group_node(node) local dir = node:is(FileNode) and node.parent or node:as(DirectoryNode)
if not node or node.name == ".." then if not dir then
node = { return
absolute_path = cwd,
name = "",
nodes = core.get_explorer().nodes,
open = true,
}
end end
local containing_folder = get_containing_folder(node) dir = dir:last_group_node()
local containing_folder = utils.path_add_trailing(dir.absolute_path)
local input_opts = { local input_opts = {
prompt = "Create file ", prompt = "Create file ",

View File

@@ -5,6 +5,9 @@ local view = require("nvim-tree.view")
local lib = require("nvim-tree.lib") local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@@ -89,7 +92,7 @@ end
---@param node Node ---@param node Node
function M.remove(node) function M.remove(node)
local notify_node = notify.render_path(node.absolute_path) local notify_node = notify.render_path(node.absolute_path)
if node.nodes ~= nil and not node.link_to then if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
local success = remove_dir(node.absolute_path) local success = remove_dir(node.absolute_path)
if not success then if not success then
notify.error("Could not remove " .. notify_node) notify.error("Could not remove " .. notify_node)

View File

@@ -1,11 +1,12 @@
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@@ -102,11 +103,15 @@ function M.fn(default_modifier)
default_modifier = default_modifier or ":t" default_modifier = default_modifier or ":t"
return function(node, modifier) return function(node, modifier)
if type(node) ~= "table" then local explorer = core.get_explorer()
node = lib.get_node_at_cursor() if not explorer then
return
end end
if node == nil then if type(node) ~= "table" then
node = explorer:get_node_at_cursor()
end
if not node then
return return
end end
@@ -120,7 +125,10 @@ function M.fn(default_modifier)
return return
end end
node = lib.get_last_group_node(node) local dir = node:as(DirectoryNode)
if dir then
node = dir:last_group_node()
end
if node.name == ".." then if node.name == ".." then
return return
end end
@@ -154,15 +162,14 @@ function M.fn(default_modifier)
return return
end end
M.rename(node, prepend .. new_file_path .. append) local full_new_path = prepend .. new_file_path .. append
M.rename(node, full_new_path)
if not M.config.filesystem_watchers.enable then if not M.config.filesystem_watchers.enable then
local explorer = core.get_explorer() explorer:reload_explorer()
if explorer then
explorer:reload_explorer()
end
end end
find_file(utils.path_remove_trailing(new_file_path)) find_file(utils.path_remove_trailing(full_new_path))
end) end)
end end
end end

View File

@@ -2,6 +2,9 @@ local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib") local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@@ -54,7 +57,7 @@ function M.remove(node)
local explorer = core.get_explorer() local explorer = core.get_explorer()
if node.nodes ~= nil and not node.link_to then if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
trash_path(function(_, rc) trash_path(function(_, rc)
if rc ~= 0 then if rc ~= 0 then
notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash")

View File

@@ -1,23 +1,24 @@
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local core = require("nvim-tree.core") 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 diagnostics = require("nvim-tree.diagnostics")
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
local MAX_DEPTH = 100 local MAX_DEPTH = 100
---Return the status of the node or nil if no status, depending on the type of ---Return the status of the node or nil if no status, depending on the type of
---status. ---status.
---@param node table node to inspect ---@param node Node to inspect
---@param what string type of status ---@param what string? type of status
---@param skip_gitignored boolean default false ---@param skip_gitignored boolean? default false
---@return boolean ---@return boolean
local function status_is_valid(node, what, skip_gitignored) local function status_is_valid(node, what, skip_gitignored)
if what == "git" then if what == "git" then
local git_status = explorer_node.get_git_status(node) local git_xy = node:get_git_xy()
return git_status ~= nil and (not skip_gitignored or git_status[1] ~= "!!") return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!")
elseif what == "diag" then elseif what == "diag" then
local diag_status = diagnostics.get_diag_status(node) local diag_status = diagnostics.get_diag_status(node)
return diag_status ~= nil and diag_status.value ~= nil return diag_status ~= nil and diag_status.value ~= nil
@@ -29,15 +30,16 @@ local function status_is_valid(node, what, skip_gitignored)
end end
---Move to the next node that has a valid status. If none found, don't move. ---Move to the next node that has a valid status. If none found, don't move.
---@param where string where to move (forwards or backwards) ---@param explorer Explorer
---@param what string type of status ---@param where string? where to move (forwards or backwards)
---@param skip_gitignored boolean default false ---@param what string? type of status
local function move(where, what, skip_gitignored) ---@param skip_gitignored boolean? default false
local function move(explorer, where, what, skip_gitignored)
local first_node_line = core.get_nodes_starting_line() local first_node_line = core.get_nodes_starting_line()
local nodes_by_line = utils.get_nodes_by_line(core.get_explorer().nodes, first_node_line) local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line)
local iter_start, iter_end, iter_step, cur, first, nex local iter_start, iter_end, iter_step, cur, first, nex
local cursor = lib.get_cursor_position() local cursor = explorer:get_cursor_position()
if cursor and cursor[1] < first_node_line then if cursor and cursor[1] < first_node_line then
cur = cursor[1] cur = cursor[1]
end end
@@ -71,25 +73,27 @@ local function move(where, what, skip_gitignored)
end end
end end
---@param node DirectoryNode
local function expand_node(node) local function expand_node(node)
if not node.open then if not node.open then
-- Expand the node. -- Expand the node.
-- Should never collapse since we checked open. -- Should never collapse since we checked open.
lib.expand_or_collapse(node) node:expand_or_collapse(false)
end end
end end
--- Move to the next node recursively. --- Move to the next node recursively.
---@param what string type of status ---@param explorer Explorer
---@param skip_gitignored boolean default false ---@param what string? type of status
local function move_next_recursive(what, skip_gitignored) ---@param skip_gitignored? boolean default false
local function move_next_recursive(explorer, what, skip_gitignored)
-- If the current node: -- If the current node:
-- * is a directory -- * is a directory
-- * and is not the root node -- * and is not the root node
-- * and has a git/diag status -- * and has a git/diag status
-- * and is not opened -- * and is not opened
-- expand it. -- expand it.
local node_init = lib.get_node_at_cursor() local node_init = explorer:get_node_at_cursor()
if not node_init then if not node_init then
return return
end end
@@ -97,13 +101,14 @@ local function move_next_recursive(what, skip_gitignored)
if node_init.name ~= ".." then -- root node cannot have a status if node_init.name ~= ".." then -- root node cannot have a status
valid = status_is_valid(node_init, what, skip_gitignored) valid = status_is_valid(node_init, what, skip_gitignored)
end end
if node_init.nodes ~= nil and valid and not node_init.open then local node_dir = node_init:as(DirectoryNode)
lib.expand_or_collapse(node_init) if node_dir and valid and not node_dir.open then
node_dir:expand_or_collapse(false)
end end
move("next", what, skip_gitignored) move(explorer, "next", what, skip_gitignored)
local node_cur = lib.get_node_at_cursor() local node_cur = explorer:get_node_at_cursor()
if not node_cur then if not node_cur then
return return
end end
@@ -115,20 +120,15 @@ local function move_next_recursive(what, skip_gitignored)
-- i is used to limit iterations. -- i is used to limit iterations.
local i = 0 local i = 0
local is_dir = node_cur.nodes ~= nil local dir_cur = node_cur:as(DirectoryNode)
while is_dir and i < MAX_DEPTH do while dir_cur and i < MAX_DEPTH do
expand_node(node_cur) expand_node(dir_cur)
move("next", what, skip_gitignored) move(explorer, "next", what, skip_gitignored)
-- Save current node. -- Save current node.
node_cur = lib.get_node_at_cursor() node_cur = explorer:get_node_at_cursor()
-- Update is_dir. dir_cur = node_cur and node_cur:as(DirectoryNode)
if node_cur then
is_dir = node_cur.nodes ~= nil
else
is_dir = false
end
i = i + 1 i = i + 1
end end
@@ -149,24 +149,25 @@ end
--- 4.4) Call a non-recursive prev. --- 4.4) Call a non-recursive prev.
--- 4.5) Save the current node and start back from 4.1. --- 4.5) Save the current node and start back from 4.1.
--- ---
---@param what string type of status ---@param explorer Explorer
---@param skip_gitignored boolean default false ---@param what string? type of status
local function move_prev_recursive(what, skip_gitignored) ---@param skip_gitignored boolean? default false
local function move_prev_recursive(explorer, what, skip_gitignored)
local node_init, node_cur local node_init, node_cur
-- 1) -- 1)
node_init = lib.get_node_at_cursor() node_init = explorer:get_node_at_cursor()
if node_init == nil then if node_init == nil then
return return
end end
-- 2) -- 2)
move("prev", what, skip_gitignored) move(explorer, "prev", what, skip_gitignored)
node_cur = lib.get_node_at_cursor() node_cur = explorer:get_node_at_cursor()
if node_cur == node_init.parent then if node_cur == node_init.parent then
-- 3) -- 3)
move_prev_recursive(what, skip_gitignored) move_prev_recursive(explorer, what, skip_gitignored)
else else
-- i is used to limit iterations. -- i is used to limit iterations.
local i = 0 local i = 0
@@ -175,14 +176,16 @@ local function move_prev_recursive(what, skip_gitignored)
if if
node_cur == nil node_cur == nil
or node_cur == node_init -- we didn't move or node_cur == node_init -- we didn't move
or not node_cur.nodes -- node is a file or node_cur:is(FileNode) -- node is a file
then then
return return
end end
-- 4.2) -- 4.2)
local node_dir = node_cur local node_dir = node_cur:as(DirectoryNode)
expand_node(node_dir) if node_dir then
expand_node(node_dir)
end
-- 4.3) -- 4.3)
if node_init.name == ".." then -- root node if node_init.name == ".." then -- root node
@@ -196,10 +199,10 @@ local function move_prev_recursive(what, skip_gitignored)
end end
-- 4.4) -- 4.4)
move("prev", what, skip_gitignored) move(explorer, "prev", what, skip_gitignored)
-- 4.5) -- 4.5)
node_cur = lib.get_node_at_cursor() node_cur = explorer:get_node_at_cursor()
i = i + 1 i = i + 1
end end
@@ -207,34 +210,36 @@ local function move_prev_recursive(what, skip_gitignored)
end end
---@class NavigationItemOpts ---@class NavigationItemOpts
---@field where string ---@field where string?
---@field what string ---@field what string?
---@field skip_gitignored boolean?
---@field recurse boolean?
---@param opts NavigationItemOpts ---@param opts NavigationItemOpts
---@return fun() ---@return fun()
function M.fn(opts) function M.fn(opts)
return function() return function()
local explorer = core.get_explorer()
if not explorer then
return
end
local recurse = false local recurse = false
local skip_gitignored = false
-- recurse only valid for git and diag moves. -- recurse only valid for git and diag moves.
if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then
recurse = opts.recurse recurse = opts.recurse
end end
if opts.skip_gitignored ~= nil then
skip_gitignored = opts.skip_gitignored
end
if not recurse then if not recurse then
move(opts.where, opts.what, skip_gitignored) move(explorer, opts.where, opts.what, opts.skip_gitignored)
return return
end end
if opts.where == "next" then if opts.where == "next" then
move_next_recursive(opts.what, skip_gitignored) move_next_recursive(explorer, opts.what, opts.skip_gitignored)
elseif opts.where == "prev" then elseif opts.where == "prev" then
move_prev_recursive(opts.what, skip_gitignored) move_prev_recursive(explorer, opts.what, opts.skip_gitignored)
end end
end end
end end

View File

@@ -1,7 +1,7 @@
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib") local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
@@ -10,33 +10,33 @@ local M = {}
function M.fn(should_close) function M.fn(should_close)
should_close = should_close or false should_close = should_close or false
---@param node Node
return function(node) return function(node)
local explorer = core.get_explorer() local dir = node:as(DirectoryNode)
node = lib.get_last_group_node(node) if dir then
if should_close and node.open then dir = dir:last_group_node()
node.open = false if should_close and dir.open then
if explorer then dir.open = false
explorer.renderer:draw() dir.explorer.renderer:draw()
return
end end
end
local parent = (node:get_parent_of_group() or node).parent
if not parent or not parent.parent then
view.set_cursor({ 1, 0 })
return return
end end
local parent = utils.get_parent_of_group(node).parent local _, line = utils.find_node(parent.explorer.nodes, function(n)
if not parent or not parent.parent then
return view.set_cursor({ 1, 0 })
end
local _, line = utils.find_node(core.get_explorer().nodes, function(n)
return n.absolute_path == parent.absolute_path return n.absolute_path == parent.absolute_path
end) end)
view.set_cursor({ line + 1, 0 }) view.set_cursor({ line + 1, 0 })
if should_close then if should_close then
parent.open = false parent.open = false
if explorer then parent.explorer.renderer:draw()
explorer.renderer:draw()
end
end end
end end
end end

View File

@@ -15,7 +15,7 @@ function M.fn(direction)
local first, last, next, prev = nil, nil, nil, nil local first, last, next, prev = nil, nil, nil, nil
local found = false local found = false
local parent = node.parent or core.get_explorer() local parent = node.parent or core.get_explorer()
Iterator.builder(parent.nodes) Iterator.builder(parent and parent.nodes or {})
:recursor(function() :recursor(function()
return nil return nil
end) end)

View File

@@ -75,7 +75,8 @@ local function pick_win_id()
end end
local i = 1 local i = 1
local win_opts = {} local win_opts_selectable = {}
local win_opts_unselectable = {}
local win_map = {} local win_map = {}
local laststatus = vim.o.laststatus local laststatus = vim.o.laststatus
vim.o.laststatus = 2 vim.o.laststatus = 2
@@ -89,19 +90,16 @@ local function pick_win_id()
if laststatus == 3 then if laststatus == 3 then
for _, win_id in ipairs(not_selectable) do for _, win_id in ipairs(not_selectable) do
local ok_status, statusline, ok_hl, winhl local ok_status, statusline
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id }) ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id })
ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = win_id })
else else
ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, win_id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts[win_id] = { win_opts_unselectable[win_id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "",
} }
-- Clear statusline for windows not selectable -- Clear statusline for windows not selectable
@@ -126,18 +124,18 @@ local function pick_win_id()
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts[id] = { win_opts_selectable[id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "", winhl = ok_hl and winhl or "",
} }
win_map[char] = id win_map[char] = id
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id }) vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id })
vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id }) vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id })
else else
vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated
vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated
end end
i = i + 1 i = i + 1
@@ -156,7 +154,7 @@ local function pick_win_id()
-- Restore window options -- Restore window options
for _, id in ipairs(selectable) do for _, id in ipairs(selectable) do
for opt, value in pairs(win_opts[id]) do for opt, value in pairs(win_opts_selectable[id]) do
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_set_option_value(opt, value, { win = id })
else else
@@ -169,7 +167,7 @@ local function pick_win_id()
for _, id in ipairs(not_selectable) do for _, id in ipairs(not_selectable) do
-- Ensure window still exists at this point -- Ensure window still exists at this point
if vim.api.nvim_win_is_valid(id) then if vim.api.nvim_win_is_valid(id) then
for opt, value in pairs(win_opts[id]) do for opt, value in pairs(win_opts_unselectable[id]) do
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_set_option_value(opt, value, { win = id })
else else
@@ -333,9 +331,9 @@ local function open_in_new_window(filename, mode)
local fname local fname
if M.relative_path then if M.relative_path then
fname = vim.fn.fnameescape(utils.path_relative(filename, vim.fn.getcwd())) fname = utils.escape_special_chars(vim.fn.fnameescape(utils.path_relative(filename, vim.fn.getcwd())))
else else
fname = vim.fn.fnameescape(filename) fname = utils.escape_special_chars(vim.fn.fnameescape(filename))
end end
local command local command
@@ -371,29 +369,29 @@ end
---@param mode string ---@param mode string
---@param filename string ---@param filename string
---@return nil
function M.fn(mode, filename) function M.fn(mode, filename)
local fname = utils.escape_special_chars(filename)
if type(mode) ~= "string" then if type(mode) ~= "string" then
mode = "" mode = ""
end end
if mode == "tabnew" then if mode == "tabnew" then
return open_file_in_tab(fname) return open_file_in_tab(filename)
end end
if mode == "drop" then if mode == "drop" then
return drop(fname) return drop(filename)
end end
if mode == "tab_drop" then if mode == "tab_drop" then
return tab_drop(fname) return tab_drop(filename)
end end
if mode == "edit_in_place" then if mode == "edit_in_place" then
return edit_in_current_buf(fname) return edit_in_current_buf(filename)
end end
local buf_loaded = is_already_loaded(fname) local buf_loaded = is_already_loaded(filename)
local found_win = utils.get_win_buf_from_path(filename) local found_win = utils.get_win_buf_from_path(filename)
if found_win and (mode == "preview" or mode == "preview_no_picker") then if found_win and (mode == "preview" or mode == "preview_no_picker") then
@@ -401,7 +399,7 @@ function M.fn(mode, filename)
end end
if not found_win then if not found_win then
open_in_new_window(fname, mode) open_in_new_window(filename, mode)
else else
vim.api.nvim_set_current_win(found_win) vim.api.nvim_set_current_win(found_win)
vim.bo.bufhidden = "" vim.bo.bufhidden = ""
@@ -423,7 +421,7 @@ end
function M.setup(opts) function M.setup(opts)
M.quit_on_open = opts.actions.open_file.quit_on_open M.quit_on_open = opts.actions.open_file.quit_on_open
M.resize_window = opts.actions.open_file.resize_window M.resize_window = opts.actions.open_file.resize_window
M.relative_path = opts.experimental.actions.open_file.relative_path M.relative_path = opts.actions.open_file.relative_path
if opts.actions.open_file.window_picker.chars then if opts.actions.open_file.window_picker.chars then
opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper() opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper()
end end

View File

@@ -64,7 +64,7 @@ function M.fn(node)
M.open(node) M.open(node)
end end
-- TODO always use native once 0.10 is the minimum neovim version -- TODO #2430 always use native once 0.10 is the minimum neovim version
function M.setup(opts) function M.setup(opts)
M.config = {} M.config = {}
M.config.system_open = opts.system_open or {} M.config.system_open = opts.system_open or {}

View File

@@ -1,8 +1,9 @@
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@return fun(path: string): boolean ---@return fun(path: string): boolean
@@ -24,10 +25,13 @@ end
---@param keep_buffers boolean ---@param keep_buffers boolean
function M.fn(keep_buffers) function M.fn(keep_buffers)
local node = lib.get_node_at_cursor()
local explorer = core.get_explorer() local explorer = core.get_explorer()
if not explorer then
return
end
if explorer == nil then local node = explorer:get_node_at_cursor()
if not node then
return return
end end
@@ -36,8 +40,9 @@ function M.fn(keep_buffers)
Iterator.builder(explorer.nodes) Iterator.builder(explorer.nodes)
:hidden() :hidden()
:applier(function(n) :applier(function(n)
if n.nodes ~= nil then local dir = n:as(DirectoryNode)
n.open = keep_buffers == true and matches(n.absolute_path) if dir then
dir.open = keep_buffers and matches(dir.absolute_path)
end end
end) end)
:recursor(function(n) :recursor(function(n)

View File

@@ -1,7 +1,8 @@
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local lib = require("nvim-tree.lib")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
@@ -16,9 +17,9 @@ local function to_lookup_table(list)
return table return table
end end
---@param node Node ---@param node DirectoryNode
local function expand(node) local function expand(node)
node = lib.get_last_group_node(node) node = node:last_group_node()
node.open = true node.open = true
if #node.nodes == 0 then if #node.nodes == 0 then
core.get_explorer():expand(node) core.get_explorer():expand(node)
@@ -29,9 +30,13 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
local function should_expand(expansion_count, node) local function should_expand(expansion_count, node)
local dir = node:as(DirectoryNode)
if not dir then
return false
end
local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY
local should_exclude = M.EXCLUDE[node.name] local should_exclude = M.EXCLUDE[dir.name]
return not should_halt and node.nodes and not node.open and not should_exclude return not should_halt and not dir.open and not should_exclude
end end
local function gen_iterator() local function gen_iterator()
@@ -48,7 +53,10 @@ local function gen_iterator()
:applier(function(node) :applier(function(node)
if should_expand(expansion_count, node) then if should_expand(expansion_count, node) then
expansion_count = expansion_count + 1 expansion_count = expansion_count + 1
expand(node) node = node:as(DirectoryNode)
if node then
expand(node)
end
end end
end) end)
:recursor(function(node) :recursor(function(node)
@@ -62,11 +70,16 @@ local function gen_iterator()
end end
end end
---@param base_node table ---Expand the directory node or the root
function M.fn(base_node) ---@param node Node
function M.fn(node)
local explorer = core.get_explorer() local explorer = core.get_explorer()
local node = base_node.nodes and base_node or explorer local parent = node:as(DirectoryNode) or explorer
if gen_iterator()(node) then if not parent then
return
end
if gen_iterator()(parent) then
notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders") notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders")
end end
if explorer then if explorer then

View File

@@ -2,7 +2,6 @@ local M = {}
M.collapse_all = require("nvim-tree.actions.tree.modifiers.collapse-all") M.collapse_all = require("nvim-tree.actions.tree.modifiers.collapse-all")
M.expand_all = require("nvim-tree.actions.tree.modifiers.expand-all") M.expand_all = require("nvim-tree.actions.tree.modifiers.expand-all")
M.toggles = require("nvim-tree.actions.tree.modifiers.toggles")
function M.setup(opts) function M.setup(opts)
M.expand_all.setup(opts) M.expand_all.setup(opts)

View File

@@ -1,72 +0,0 @@
local lib = require("nvim-tree.lib")
local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local M = {}
---@param explorer Explorer
local function reload(explorer)
local node = lib.get_node_at_cursor()
explorer:reload_explorer()
utils.focus_node_or_parent(node)
end
local function wrap_explorer(fn)
return function(...)
local explorer = core.get_explorer()
if explorer then
return fn(explorer, ...)
end
end
end
---@param explorer Explorer
local function custom(explorer)
explorer.filters.config.filter_custom = not explorer.filters.config.filter_custom
reload(explorer)
end
---@param explorer Explorer
local function git_ignored(explorer)
explorer.filters.config.filter_git_ignored = not explorer.filters.config.filter_git_ignored
reload(explorer)
end
---@param explorer Explorer
local function git_clean(explorer)
explorer.filters.config.filter_git_clean = not explorer.filters.config.filter_git_clean
reload(explorer)
end
---@param explorer Explorer
local function no_buffer(explorer)
explorer.filters.config.filter_no_buffer = not explorer.filters.config.filter_no_buffer
reload(explorer)
end
---@param explorer Explorer
local function no_bookmark(explorer)
explorer.filters.config.filter_no_bookmark = not explorer.filters.config.filter_no_bookmark
reload(explorer)
end
---@param explorer Explorer
local function dotfiles(explorer)
explorer.filters.config.filter_dotfiles = not explorer.filters.config.filter_dotfiles
reload(explorer)
end
---@param explorer Explorer
local function enable(explorer)
explorer.filters.config.enable = not explorer.filters.config.enable
reload(explorer)
end
M.custom = wrap_explorer(custom)
M.git_ignored = wrap_explorer(git_ignored)
M.git_clean = wrap_explorer(git_clean)
M.no_buffer = wrap_explorer(no_buffer)
M.no_bookmark = wrap_explorer(no_bookmark)
M.dotfiles = wrap_explorer(dotfiles)
M.enable = wrap_explorer(enable)
return M

View File

@@ -1,14 +1,18 @@
local lib = require("nvim-tree.lib")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local actions = require("nvim-tree.actions") local actions = require("nvim-tree.actions")
local appearance_diagnostics = require("nvim-tree.appearance.diagnostics") local appearance_hi_test = require("nvim-tree.appearance.hi-test")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local help = require("nvim-tree.help") local help = require("nvim-tree.help")
local keymap = require("nvim-tree.keymap") local keymap = require("nvim-tree.keymap")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryNode = require("nvim-tree.node.directory")
local FileLinkNode = require("nvim-tree.node.file-link")
local RootNode = require("nvim-tree.node.root")
local UserDecorator = require("nvim-tree.renderer.decorator.user")
local Api = { local Api = {
tree = {}, tree = {},
node = { node = {
@@ -36,46 +40,26 @@ local Api = {
}, },
commands = {}, commands = {},
diagnostics = {}, diagnostics = {},
decorator = {},
} }
--- Print error when setup not called. ---Print error when setup not called.
--- f function to invoke ---@param fn fun(...): any
---@param f function ---@return fun(...): any
---@return fun(...) : any local function wrap(fn)
local function wrap(f)
return function(...) return function(...)
if vim.g.NvimTreeSetup == 1 then if vim.g.NvimTreeSetup == 1 then
return f(...) return fn(...)
else else
notify.error("nvim-tree setup not called") notify.error("nvim-tree setup not called")
end end
end end
end end
---Inject the node as the first argument if present otherwise do nothing.
---@param fn function function to invoke
local function wrap_node(fn)
return function(node, ...)
node = node or lib.get_node_at_cursor()
if node then
return fn(node, ...)
end
end
end
---Inject the node or nil as the first argument if absent.
---@param fn function function to invoke
local function wrap_node_or_nil(fn)
return function(node, ...)
node = node or lib.get_node_at_cursor()
return fn(node, ...)
end
end
---Invoke a method on the singleton explorer. ---Invoke a method on the singleton explorer.
---Print error when setup not called. ---Print error when setup not called.
---@param explorer_method string explorer method name ---@param explorer_method string explorer method name
---@return fun(...) : any ---@return fun(...): any
local function wrap_explorer(explorer_method) local function wrap_explorer(explorer_method)
return wrap(function(...) return wrap(function(...)
local explorer = core.get_explorer() local explorer = core.get_explorer()
@@ -85,11 +69,49 @@ local function wrap_explorer(explorer_method)
end) end)
end end
---Inject the node as the first argument if present otherwise do nothing.
---@param fn fun(node: Node, ...): any
---@return fun(node: Node?, ...): any
local function wrap_node(fn)
return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")()
if node then
return fn(node, ...)
end
end
end
---Inject the node or nil as the first argument if absent.
---@param fn fun(node: Node?, ...): any
---@return fun(node: Node?, ...): any
local function wrap_node_or_nil(fn)
return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")()
return fn(node, ...)
end
end
---Invoke a member's method on the singleton explorer. ---Invoke a member's method on the singleton explorer.
---Print error when setup not called. ---Print error when setup not called.
---@param explorer_member string explorer member name ---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member ---@param member_method string method name to invoke on member
---@return fun(...) : any ---@param ... any passed to method
---@return fun(...): any
local function wrap_explorer_member_args(explorer_member, member_method, ...)
local method_args = ...
return wrap(function(...)
local explorer = core.get_explorer()
if explorer then
return explorer[explorer_member][member_method](explorer[explorer_member], method_args, ...)
end
end)
end
---Invoke a member's method on the singleton explorer.
---Print error when setup not called.
---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member
---@return fun(...): any
local function wrap_explorer_member(explorer_member, member_method) local function wrap_explorer_member(explorer_member, member_method)
return wrap(function(...) return wrap(function(...)
local explorer = core.get_explorer() local explorer = core.get_explorer()
@@ -135,16 +157,19 @@ Api.tree.change_root = wrap(function(...)
end) end)
Api.tree.change_root_to_node = wrap_node(function(node) Api.tree.change_root_to_node = wrap_node(function(node)
if node.name == ".." then if node.name == ".." or node:is(RootNode) then
actions.root.change_dir.fn("..") actions.root.change_dir.fn("..")
elseif node.nodes ~= nil then else
actions.root.change_dir.fn(lib.get_last_group_node(node).absolute_path) node = node:as(DirectoryNode)
if node then
actions.root.change_dir.fn(node:last_group_node().absolute_path)
end
end end
end) end)
Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn) Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn)
Api.tree.get_node_under_cursor = wrap(lib.get_node_at_cursor) Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor")
Api.tree.get_nodes = wrap(lib.get_nodes) Api.tree.get_nodes = wrap_explorer("get_nodes")
---@class ApiTreeFindFileOpts ---@class ApiTreeFindFileOpts
---@field buf string|number|nil ---@field buf string|number|nil
@@ -158,13 +183,13 @@ Api.tree.find_file = wrap(actions.tree.find_file.fn)
Api.tree.search_node = wrap(actions.finders.search_node.fn) Api.tree.search_node = wrap(actions.finders.search_node.fn)
Api.tree.collapse_all = wrap(actions.tree.modifiers.collapse_all.fn) Api.tree.collapse_all = wrap(actions.tree.modifiers.collapse_all.fn)
Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand_all.fn) Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand_all.fn)
Api.tree.toggle_enable_filters = wrap(actions.tree.modifiers.toggles.enable) Api.tree.toggle_enable_filters = wrap_explorer_member("filters", "toggle")
Api.tree.toggle_gitignore_filter = wrap(actions.tree.modifiers.toggles.git_ignored) Api.tree.toggle_gitignore_filter = wrap_explorer_member_args("filters", "toggle", "git_ignored")
Api.tree.toggle_git_clean_filter = wrap(actions.tree.modifiers.toggles.git_clean) Api.tree.toggle_git_clean_filter = wrap_explorer_member_args("filters", "toggle", "git_clean")
Api.tree.toggle_no_buffer_filter = wrap(actions.tree.modifiers.toggles.no_buffer) Api.tree.toggle_no_buffer_filter = wrap_explorer_member_args("filters", "toggle", "no_buffer")
Api.tree.toggle_custom_filter = wrap(actions.tree.modifiers.toggles.custom) Api.tree.toggle_custom_filter = wrap_explorer_member_args("filters", "toggle", "custom")
Api.tree.toggle_hidden_filter = wrap(actions.tree.modifiers.toggles.dotfiles) Api.tree.toggle_hidden_filter = wrap_explorer_member_args("filters", "toggle", "dotfiles")
Api.tree.toggle_no_bookmark_filter = wrap(actions.tree.modifiers.toggles.no_bookmark) Api.tree.toggle_no_bookmark_filter = wrap_explorer_member_args("filters", "toggle", "no_bookmark")
Api.tree.toggle_help = wrap(help.toggle) Api.tree.toggle_help = wrap(help.toggle)
Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf) Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf)
@@ -198,23 +223,26 @@ Api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basenam
Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path")) Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path"))
---@param mode string ---@param mode string
---@param node table ---@param node Node
local function edit(mode, node) local function edit(mode, node)
local path = node.absolute_path local file_link = node:as(FileLinkNode)
if node.link_to and not node.nodes then local path = file_link and file_link.link_to or node.absolute_path
path = node.link_to
end
actions.node.open_file.fn(mode, path) actions.node.open_file.fn(mode, path)
end end
---@param mode string ---@param mode string
---@return fun(node: table) ---@param toggle_group boolean?
---@return fun(node: Node)
local function open_or_expand_or_dir_up(mode, toggle_group) local function open_or_expand_or_dir_up(mode, toggle_group)
---@param node Node
return function(node) return function(node)
if node.name == ".." then local root = node:as(RootNode)
local dir = node:as(DirectoryNode)
if root or node.name == ".." then
actions.root.change_dir.fn("..") actions.root.change_dir.fn("..")
elseif node.nodes then elseif dir then
lib.expand_or_collapse(node, toggle_group) dir:expand_or_collapse(toggle_group)
elseif not toggle_group then elseif not toggle_group then
edit(mode, node) edit(mode, node)
end end
@@ -279,10 +307,15 @@ Api.config.mappings.get_keymap = wrap(keymap.get_keymap)
Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default) Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default)
Api.config.mappings.default_on_attach = keymap.default_on_attach Api.config.mappings.default_on_attach = keymap.default_on_attach
Api.diagnostics.hi_test = wrap(appearance_diagnostics.hi_test) Api.diagnostics.hi_test = wrap(appearance_hi_test)
Api.commands.get = wrap(function() Api.commands.get = wrap(function()
return require("nvim-tree.commands").get() return require("nvim-tree.commands").get()
end) end)
---Create a decorator class by calling :extend()
---See :help nvim-tree-decorators
---@type nvim_tree.api.decorator.UserDecorator
Api.decorator.UserDecorator = UserDecorator --[[@as nvim_tree.api.decorator.UserDecorator]]
return Api return Api

View File

@@ -1,45 +1,46 @@
local appearance = require("nvim-tree.appearance") local appearance = require("nvim-tree.appearance")
local Class = require("nvim-tree.classic")
-- others with name and links less than this arbitrary value are short -- others with name and links less than this arbitrary value are short
local SHORT_LEN = 50 local SHORT_LEN = 50
local M = {} ---@class (exact) HighlightDisplay: Class for :NvimTreeHiTest
---@class HighlightDisplay for :NvimTreeHiTest
---@field group string nvim-tree highlight group name ---@field group string nvim-tree highlight group name
---@field links string link chain to a concretely defined group ---@field links string link chain to a concretely defined group
---@field def string :hi concrete definition after following any links ---@field def string :hi concrete definition after following any links
local HighlightDisplay = {} local HighlightDisplay = Class:extend()
---@param group string nvim-tree highlight group name ---@class HighlightDisplay
---@return HighlightDisplay ---@overload fun(args: HighlightDisplayArgs): HighlightDisplay
function HighlightDisplay:new(group)
local o = {}
setmetatable(o, self)
self.__index = self
o.group = group ---@class (exact) HighlightDisplayArgs
local concrete = o.group ---@field group string nvim-tree highlight group name
---@protected
---@param args HighlightDisplayArgs
function HighlightDisplay:new(args)
self.group = args.group
local concrete = self.group
-- maybe follow links -- maybe follow links
local links = {} local links = {}
local link = vim.api.nvim_get_hl(0, { name = o.group }).link local link = vim.api.nvim_get_hl(0, { name = self.group }).link
while link do while link do
table.insert(links, link) table.insert(links, link)
concrete = link concrete = link
link = vim.api.nvim_get_hl(0, { name = link }).link link = vim.api.nvim_get_hl(0, { name = link }).link
end end
o.links = table.concat(links, " ") self.links = table.concat(links, " ")
-- concrete definition -- concrete definition
local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { concrete } }, { output = true }) local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { concrete } }, { output = true })
if ok and type(res) == "string" then if ok and type(res) == "string" then
o.def = res:gsub(".*xxx *", "") self.def = res:gsub(".*xxx *", "")
else else
o.def = "" self.def = ""
end end
return o
end end
---Render one group. ---Render one group.
@@ -87,7 +88,7 @@ end
---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim ---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim
---Display all nvim-tree and neovim highlight groups, their link chain and actual definition ---Display all nvim-tree and neovim highlight groups, their link chain and actual definition
function M.hi_test() return function()
-- create a buffer -- create a buffer
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
@@ -96,7 +97,7 @@ function M.hi_test()
-- nvim-tree groups, ordered -- nvim-tree groups, ordered
local displays = {} local displays = {}
for _, highlight_group in ipairs(appearance.HIGHLIGHT_GROUPS) do for _, highlight_group in ipairs(appearance.HIGHLIGHT_GROUPS) do
local display = HighlightDisplay:new(highlight_group.group) local display = HighlightDisplay({ group = highlight_group.group })
table.insert(displays, display) table.insert(displays, display)
end end
l = render_displays("nvim-tree", displays, bufnr, l) l = render_displays("nvim-tree", displays, bufnr, l)
@@ -110,7 +111,7 @@ function M.hi_test()
if ok then if ok then
for group in string.gmatch(out, "(%w*)%s+xxx") do for group in string.gmatch(out, "(%w*)%s+xxx") do
if group:find("NvimTree", 1, true) ~= 1 then if group:find("NvimTree", 1, true) ~= 1 then
local display = HighlightDisplay:new(group) local display = HighlightDisplay({ group = group })
if #display.group + #display.links > SHORT_LEN then if #display.group + #display.links > SHORT_LEN then
table.insert(displays_long, display) table.insert(displays_long, display)
else else
@@ -137,5 +138,3 @@ function M.hi_test()
vim.cmd.buffer(bufnr) vim.cmd.buffer(bufnr)
end end
return M

View File

@@ -1,3 +1,5 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@type table<string, boolean> record of which file is modified ---@type table<string, boolean> record of which file is modified
@@ -21,18 +23,33 @@ function M.reload_modified()
end end
end end
---@param node table ---@param node Node
---@return boolean ---@return boolean
function M.is_modified(node) function M.is_modified(node)
return node if not M.config.modified.enable then
and M.config.modified.enable return false
and M._modified[node.absolute_path] end
and (not node.nodes or M.config.modified.show_on_dirs)
and (not node.open or M.config.modified.show_on_open_dirs) if not M._modified[node.absolute_path] then
return false
end
local dir = node:as(DirectoryNode)
if dir then
if not M.config.modified.show_on_dirs then
return false
end
if dir.open and not M.config.modified.show_on_open_dirs then
return false
end
end
return true
end end
---A buffer exists for the node's absolute path ---A buffer exists for the node's absolute path
---@param node table ---@param node Node
---@return boolean ---@return boolean
function M.is_opened(node) function M.is_opened(node)
return node and vim.fn.bufloaded(node.absolute_path) > 0 return node and vim.fn.bufloaded(node.absolute_path) > 0

91
lua/nvim-tree/classic.lua Normal file
View File

@@ -0,0 +1,91 @@
--
-- classic
--
-- Copyright (c) 2014, rxi
--
-- This module is free software; you can redistribute it and/or modify it under
-- the terms of the MIT license. See LICENSE for details.
--
-- https://github.com/rxi/classic
--
---@class (exact) Class
---@field super Class
---@field private implements table<Class, boolean>
local Class = {}
Class.__index = Class ---@diagnostic disable-line: inject-field
---Default constructor
---@protected
function Class:new(...) --luacheck: ignore 212
end
---Extend a class, setting .super
function Class:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
---Implement the functions of a mixin
---Add the mixin to .implements
---@param mixin Class
function Class:implement(mixin)
if not rawget(self, "implements") then
-- set on the class itself instead of parents
rawset(self, "implements", {})
end
self.implements[mixin] = true
for k, v in pairs(mixin) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
---Object is an instance of class or implements a mixin
---@generic T
---@param class T
---@return boolean
function Class:is(class)
local mt = getmetatable(self)
while mt do
if mt == class then
return true
end
if mt.implements and mt.implements[class] then
return true
end
mt = getmetatable(mt)
end
return false
end
---Return object if :is otherwise nil
---@generic T
---@param class T
---@return T|nil
function Class:as(class)
return self:is(class) and self or nil
end
---Constructor to create instance, call :new and return
function Class:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
-- avoid unused param warnings in abstract methods
---@param ... any
function Class:nop(...) --luacheck: ignore 212
end
return Class

View File

@@ -1,4 +1,5 @@
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
@@ -15,7 +16,21 @@ function M.init(foldername)
if TreeExplorer then if TreeExplorer then
TreeExplorer:destroy() TreeExplorer:destroy()
end end
TreeExplorer = require("nvim-tree.explorer"):new(foldername)
local err, path
if foldername then
path, err = vim.loop.fs_realpath(foldername)
else
path, err = vim.loop.cwd()
end
if path then
TreeExplorer = require("nvim-tree.explorer")({ path = path })
else
notify.error(err)
TreeExplorer = nil
end
if not first_init_done then if not first_init_done then
events._dispatch_ready() events._dispatch_ready()
first_init_done = true first_init_done = true

View File

@@ -3,6 +3,8 @@ local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---COC severity level strings to LSP severity levels ---COC severity level strings to LSP severity levels
@@ -15,7 +17,7 @@ local COC_SEVERITY_LEVELS = {
} }
---Absolute Node path to LSP severity level ---Absolute Node path to LSP severity level
---@alias NodeSeverities table<string, lsp.DiagnosticSeverity> ---@alias NodeSeverities table<string, vim.diagnostic.Severity>
---@class DiagStatus ---@class DiagStatus
---@field value lsp.DiagnosticSeverity|nil ---@field value lsp.DiagnosticSeverity|nil
@@ -35,33 +37,6 @@ local function uniformize_path(path)
return utils.canonical_path(path:gsub("\\", "/")) return utils.canonical_path(path:gsub("\\", "/"))
end end
---Marshal severities from LSP. Does nothing when LSP disabled.
---@return NodeSeverities
local function from_nvim_lsp()
local buffer_severity = {}
-- is_enabled is not present in all 0.10 builds/releases, see #2781
local is_enabled = false
if vim.fn.has("nvim-0.10") == 1 and type(vim.diagnostic.is_enabled) == "function" then
is_enabled = vim.diagnostic.is_enabled()
elseif type(vim.diagnostic.is_disabled) == "function" then ---@diagnostic disable-line: deprecated
is_enabled = not vim.diagnostic.is_disabled() ---@diagnostic disable-line: deprecated
end
if is_enabled then
for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do
if diagnostic.severity and diagnostic.bufnr and vim.api.nvim_buf_is_valid(diagnostic.bufnr) then
local bufname = uniformize_path(vim.api.nvim_buf_get_name(diagnostic.bufnr))
if not buffer_severity[bufname] or diagnostic.severity < buffer_severity[bufname] then
buffer_severity[bufname] = diagnostic.severity
end
end
end
end
return buffer_severity
end
---Severity is within diagnostics.severity.min, diagnostics.severity.max ---Severity is within diagnostics.severity.min, diagnostics.severity.max
---@param severity lsp.DiagnosticSeverity ---@param severity lsp.DiagnosticSeverity
---@param config table ---@param config table
@@ -125,7 +100,7 @@ end
local function from_cache(node) local function from_cache(node)
local nodepath = uniformize_path(node.absolute_path) local nodepath = uniformize_path(node.absolute_path)
local max_severity = nil local max_severity = nil
if not node.nodes then if not node:is(DirectoryNode) then
-- direct cache hit for files -- direct cache hit for files
max_severity = NODE_SEVERITIES[nodepath] max_severity = NODE_SEVERITIES[nodepath]
else else
@@ -133,11 +108,8 @@ local function from_cache(node)
for bufname, severity in pairs(NODE_SEVERITIES) do for bufname, severity in pairs(NODE_SEVERITIES) do
local node_contains_buf = vim.startswith(bufname, nodepath .. "/") local node_contains_buf = vim.startswith(bufname, nodepath .. "/")
if node_contains_buf then if node_contains_buf then
if severity == M.severity.max then if not max_severity or severity < max_severity then
max_severity = severity max_severity = severity
break
else
max_severity = math.min(max_severity or severity, severity)
end end
end end
end end
@@ -145,27 +117,75 @@ local function from_cache(node)
return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION } return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION }
end end
---Fired on DiagnosticChanged and CocDiagnosticChanged events: ---Fired on DiagnosticChanged for a single buffer.
---This will be called on set and reset of diagnostics.
---On disabling LSP, a reset event will be sent for all buffers.
---@param ev table standard event with data.diagnostics populated
function M.update_lsp(ev)
if not M.enable or not ev or not ev.data or not ev.data.diagnostics then
return
end
local profile_event = log.profile_start("DiagnosticChanged event")
---@type vim.Diagnostic[]
local diagnostics = ev.data.diagnostics
-- use the buffer from the event, as ev.data.diagnostics will be empty on resolved diagnostics
local bufname = uniformize_path(vim.api.nvim_buf_get_name(ev.buf))
---@type vim.diagnostic.Severity?
local new_severity = nil
-- most severe (lowest) severity in user range
for _, diagnostic in ipairs(diagnostics) do
if diagnostic.severity >= M.severity.max and diagnostic.severity <= M.severity.min then
if not new_severity or diagnostic.severity < new_severity then
new_severity = diagnostic.severity
end
end
end
-- record delta and schedule a redraw
if new_severity ~= NODE_SEVERITIES[bufname] then
NODE_SEVERITIES[bufname] = new_severity
NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1
utils.debounce("DiagnosticChanged redraw", M.debounce_delay, function()
local profile_redraw = log.profile_start("DiagnosticChanged redraw")
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
log.profile_end(profile_redraw)
end)
end
log.profile_end(profile_event)
end
---Fired on CocDiagnosticChanged events:
---debounced retrieval, cache update, version increment and draw ---debounced retrieval, cache update, version increment and draw
function M.update() function M.update_coc()
if not M.enable then if not M.enable then
return return
end end
utils.debounce("diagnostics", M.debounce_delay, function() utils.debounce("CocDiagnosticChanged update", M.debounce_delay, function()
local profile = log.profile_start("diagnostics update") local profile = log.profile_start("CocDiagnosticChanged update")
if is_using_coc() then NODE_SEVERITIES = from_coc()
NODE_SEVERITIES = from_coc()
else
NODE_SEVERITIES = from_nvim_lsp()
end
NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1
if log.enabled("diagnostics") then if log.enabled("diagnostics") then
for bufname, severity in pairs(NODE_SEVERITIES) do for bufname, severity in pairs(NODE_SEVERITIES) do
log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity) log.line("diagnostics", "COC Indexing bufname '%s' with severity %d", bufname, severity)
end end
end end
log.profile_end(profile) log.profile_end(profile)
if view.is_buf_valid(view.get_bufnr()) then
local bufnr = view.get_bufnr()
local should_draw = bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
if should_draw then
local explorer = core.get_explorer() local explorer = core.get_explorer()
if explorer then if explorer then
explorer.renderer:draw() explorer.renderer:draw()
@@ -184,7 +204,7 @@ function M.get_diag_status(node)
end end
-- dir but we shouldn't show on dirs at all -- dir but we shouldn't show on dirs at all
if node.nodes ~= nil and not M.show_on_dirs then if node:is(DirectoryNode) and not M.show_on_dirs then
return nil return nil
end end
@@ -195,13 +215,15 @@ function M.get_diag_status(node)
node.diag_status = from_cache(node) node.diag_status = from_cache(node)
end end
local dir = node:as(DirectoryNode)
-- file -- file
if not node.nodes then if not dir then
return node.diag_status return node.diag_status
end end
-- dir is closed or we should show on open_dirs -- dir is closed or we should show on open_dirs
if not node.open or M.show_on_open_dirs then if not dir.open or M.show_on_open_dirs then
return node.diag_status return node.diag_status
end end
return nil return nil

View File

@@ -1,24 +1,5 @@
local M = {} local M = {}
---Setup options for "highlight_*"
---@enum HL_POSITION
M.HL_POSITION = {
none = 0,
icon = 1,
name = 2,
all = 4,
}
---Setup options for "*_placement"
---@enum ICON_PLACEMENT
M.ICON_PLACEMENT = {
none = 0,
signcolumn = 1,
before = 2,
after = 3,
right_align = 4,
}
---Reason for filter in filter.lua ---Reason for filter in filter.lua
---@enum FILTER_REASON ---@enum FILTER_REASON
M.FILTER_REASON = { M.FILTER_REASON = {

View File

@@ -1,52 +1,59 @@
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
---@class Filters to handle all opts.filters and related API local Class = require("nvim-tree.classic")
---@field config table hydrated user opts.filters
---@alias FilterType "custom" | "dotfiles" | "git_ignored" | "git_clean" | "no_buffer" | "no_bookmark"
---@class (exact) Filters: Class
---@field enabled boolean
---@field state table<FilterType, boolean>
---@field private explorer Explorer ---@field private explorer Explorer
---@field private exclude_list string[] filters.exclude ---@field private exclude_list string[] filters.exclude
---@field private ignore_list string[] filters.custom string table ---@field private ignore_list table<string, boolean> filters.custom string table
---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function ---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function
local Filters = {} local Filters = Class:extend()
---@param opts table user options ---@class Filters
---@param explorer Explorer ---@overload fun(args: FiltersArgs): Filters
---@return Filters
function Filters:new(opts, explorer) ---@class (exact) FiltersArgs
local o = { ---@field explorer Explorer
explorer = explorer,
ignore_list = {}, ---@protected
exclude_list = opts.filters.exclude, ---@param args FiltersArgs
custom_function = nil, function Filters:new(args)
config = { self.explorer = args.explorer
enable = opts.filters.enable, self.ignore_list = {}
filter_custom = true, self.exclude_list = self.explorer.opts.filters.exclude
filter_dotfiles = opts.filters.dotfiles, self.custom_function = nil
filter_git_ignored = opts.filters.git_ignored,
filter_git_clean = opts.filters.git_clean, self.enabled = self.explorer.opts.filters.enable
filter_no_buffer = opts.filters.no_buffer, self.state = {
filter_no_bookmark = opts.filters.no_bookmark, custom = true,
}, dotfiles = self.explorer.opts.filters.dotfiles,
git_ignored = self.explorer.opts.filters.git_ignored,
git_clean = self.explorer.opts.filters.git_clean,
no_buffer = self.explorer.opts.filters.no_buffer,
no_bookmark = self.explorer.opts.filters.no_bookmark,
} }
local custom_filter = opts.filters.custom local custom_filter = self.explorer.opts.filters.custom
if type(custom_filter) == "function" then if type(custom_filter) == "function" then
o.custom_function = custom_filter self.custom_function = custom_filter
else else
if custom_filter and #custom_filter > 0 then if custom_filter and #custom_filter > 0 then
for _, filter_name in pairs(custom_filter) do for _, filter_name in pairs(custom_filter) do
o.ignore_list[filter_name] = true self.ignore_list[filter_name] = true
end end
end end
end end
setmetatable(o, self)
self.__index = self
return o
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
local function is_excluded(self, path) function Filters:is_excluded(path)
for _, node in ipairs(self.exclude_list) do for _, node in ipairs(self.exclude_list) do
if path:match(node) then if path:match(node) then
return true return true
@@ -56,26 +63,27 @@ local function is_excluded(self, path)
end end
---Check if the given path is git clean/ignored ---Check if the given path is git clean/ignored
---@private
---@param path string Absolute path ---@param path string Absolute path
---@param git_status table from prepare ---@param project GitProject from prepare
---@return boolean ---@return boolean
local function git(self, path, git_status) function Filters:git(path, project)
if type(git_status) ~= "table" or type(git_status.files) ~= "table" or type(git_status.dirs) ~= "table" then if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then
return false return false
end end
-- default status to clean -- default status to clean
local status = git_status.files[path] local xy = project.files[path]
status = status or git_status.dirs.direct[path] and git_status.dirs.direct[path][1] xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1]
status = status or git_status.dirs.indirect[path] and git_status.dirs.indirect[path][1] xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1]
-- filter ignored; overrides clean as they are effectively dirty -- filter ignored; overrides clean as they are effectively dirty
if self.config.filter_git_ignored and status == "!!" then if self.state.git_ignored and xy == "!!" then
return true return true
end end
-- filter clean -- filter clean
if self.config.filter_git_clean and not status then if self.state.git_clean and not xy then
return true return true
end end
@@ -83,11 +91,12 @@ local function git(self, path, git_status)
end end
---Check if the given path has no listed buffer ---Check if the given path has no listed buffer
---@private
---@param path string Absolute path ---@param path string Absolute path
---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 } ---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 }
---@return boolean ---@return boolean
local function buf(self, path, bufinfo) function Filters:buf(path, bufinfo)
if not self.config.filter_no_buffer or type(bufinfo) ~= "table" then if not self.state.no_buffer or type(bufinfo) ~= "table" then
return false return false
end end
@@ -101,17 +110,21 @@ local function buf(self, path, bufinfo)
return true return true
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
local function dotfile(self, path) function Filters:dotfile(path)
return self.config.filter_dotfiles and utils.path_basename(path):sub(1, 1) == "." return self.state.dotfiles and utils.path_basename(path):sub(1, 1) == "."
end end
---Bookmark is present
---@private
---@param path string ---@param path string
---@param path_type string|nil filetype of path ---@param path_type string|nil filetype of path
---@param bookmarks table<string, string|nil> path, filetype table of bookmarked files ---@param bookmarks table<string, string|nil> path, filetype table of bookmarked files
local function bookmark(self, path, path_type, bookmarks) ---@return boolean
if not self.config.filter_no_bookmark then function Filters:bookmark(path, path_type, bookmarks)
if not self.state.no_bookmark then
return false return false
end end
-- if bookmark is empty, we should see a empty filetree -- if bookmark is empty, we should see a empty filetree
@@ -143,10 +156,11 @@ local function bookmark(self, path, path_type, bookmarks)
return true return true
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
local function custom(self, path) function Filters:custom(path)
if not self.config.filter_custom then if not self.state.custom then
return false return false
end end
@@ -176,19 +190,19 @@ local function custom(self, path)
end end
---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons. ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons.
---@param git_status table|nil optional results of git.load_project_status(...) ---@param project GitProject? optional results of git.load_projects(...)
---@return table ---@return table
--- git_status: reference --- project: reference
--- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
--- bookmarks: absolute paths to boolean --- bookmarks: absolute paths to boolean
function Filters:prepare(git_status) function Filters:prepare(project)
local status = { local status = {
git_status = git_status or {}, project = project or {},
bufinfo = {}, bufinfo = {},
bookmarks = {}, bookmarks = {},
} }
if self.config.filter_no_buffer then if self.state.no_buffer then
status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 }) status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 })
end end
@@ -208,20 +222,20 @@ end
---@param status table from prepare ---@param status table from prepare
---@return boolean ---@return boolean
function Filters:should_filter(path, fs_stat, status) function Filters:should_filter(path, fs_stat, status)
if not self.config.enable then if not self.enabled then
return false return false
end end
-- exclusions override all filters -- exclusions override all filters
if is_excluded(self, path) then if self:is_excluded(path) then
return false return false
end end
return git(self, path, status.git_status) return self:git(path, status.project)
or buf(self, path, status.bufinfo) or self:buf(path, status.bufinfo)
or dotfile(self, path) or self:dotfile(path)
or custom(self, path) or self:custom(path)
or bookmark(self, path, fs_stat and fs_stat.type, status.bookmarks) or self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks)
end end
--- Check if the given path should be filtered, and provide the reason why it was --- Check if the given path should be filtered, and provide the reason why it was
@@ -230,27 +244,44 @@ end
---@param status table from prepare ---@param status table from prepare
---@return FILTER_REASON ---@return FILTER_REASON
function Filters:should_filter_as_reason(path, fs_stat, status) function Filters:should_filter_as_reason(path, fs_stat, status)
if not self.config.enable then if not self.enabled then
return FILTER_REASON.none return FILTER_REASON.none
end end
if is_excluded(self, path) then if self:is_excluded(path) then
return FILTER_REASON.none return FILTER_REASON.none
end end
if git(self, path, status.git_status) then if self:git(path, status.project) then
return FILTER_REASON.git return FILTER_REASON.git
elseif buf(self, path, status.bufinfo) then elseif self:buf(path, status.bufinfo) then
return FILTER_REASON.buf return FILTER_REASON.buf
elseif dotfile(self, path) then elseif self:dotfile(path) then
return FILTER_REASON.dotfile return FILTER_REASON.dotfile
elseif custom(self, path) then elseif self:custom(path) then
return FILTER_REASON.custom return FILTER_REASON.custom
elseif bookmark(self, path, fs_stat and fs_stat.type, status.bookmarks) then elseif self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks) then
return FILTER_REASON.bookmark return FILTER_REASON.bookmark
else else
return FILTER_REASON.none return FILTER_REASON.none
end end
end end
---Toggle a type and refresh
---@private
---@param type FilterType? nil to disable all
function Filters:toggle(type)
if not type or self.state[type] == nil then
self.enabled = not self.enabled
else
self.state[type] = not self.state[type]
end
local node = self.explorer:get_node_at_cursor()
self.explorer:reload_explorer()
if node then
utils.focus_node_or_parent(node)
end
end
return Filters return Filters

View File

@@ -1,20 +1,23 @@
local builders = require("nvim-tree.explorer.node-builders") local appearance = require("nvim-tree.appearance")
local buffers = require("nvim-tree.buffers")
local core = require("nvim-tree.core")
local git = require("nvim-tree.git") local git = require("nvim-tree.git")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local notify = require("nvim-tree.notify")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local watch = require("nvim-tree.explorer.watch") local node_factory = require("nvim-tree.node.factory")
local explorer_node = require("nvim-tree.explorer.node")
local DirectoryNode = require("nvim-tree.node.directory")
local RootNode = require("nvim-tree.node.root")
local Watcher = require("nvim-tree.watcher")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local NodeIterator = require("nvim-tree.iterators.node-iterator") local NodeIterator = require("nvim-tree.iterators.node-iterator")
local Watcher = require("nvim-tree.watcher")
local Filters = require("nvim-tree.explorer.filters") local Filters = require("nvim-tree.explorer.filters")
local Marks = require("nvim-tree.marks") local Marks = require("nvim-tree.marks")
local LiveFilter = require("nvim-tree.explorer.live-filter") local LiveFilter = require("nvim-tree.explorer.live-filter")
local Sorters = require("nvim-tree.explorer.sorters") local Sorter = require("nvim-tree.explorer.sorter")
local Clipboard = require("nvim-tree.actions.fs.clipboard") local Clipboard = require("nvim-tree.actions.fs.clipboard")
local Renderer = require("nvim-tree.renderer") local Renderer = require("nvim-tree.renderer")
@@ -22,78 +25,158 @@ local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
local config local config
---@class Explorer ---@class (exact) Explorer: RootNode
---@field uid_explorer number vim.loop.hrtime() at construction time
---@field opts table user options ---@field opts table user options
---@field absolute_path string ---@field augroup_id integer
---@field nodes Node[]
---@field open boolean
---@field watcher Watcher|nil
---@field renderer Renderer ---@field renderer Renderer
---@field filters Filters ---@field filters Filters
---@field live_filter LiveFilter ---@field live_filter LiveFilter
---@field sorters Sorter ---@field sorters Sorter
---@field marks Marks ---@field marks Marks
---@field clipboard Clipboard ---@field clipboard Clipboard
local Explorer = {} local Explorer = RootNode:extend()
---@param path string|nil ---@class Explorer
---@return Explorer|nil ---@overload fun(args: ExplorerArgs): Explorer
function Explorer:new(path)
local err
if path then ---@class (exact) ExplorerArgs
path, err = vim.loop.fs_realpath(path) ---@field path string
else
path, err = vim.loop.cwd()
end
if not path then
notify.error(err)
return
end
local o = { ---@protected
opts = config, ---@param args ExplorerArgs
absolute_path = path, function Explorer:new(args)
nodes = {}, Explorer.super.new(self, {
open = true, explorer = self,
sorters = Sorters:new(config), absolute_path = args.path,
} name = "..",
})
setmetatable(o, self) self.uid_explorer = vim.loop.hrtime()
self.__index = self self.augroup_id = vim.api.nvim_create_augroup("NvimTree_Explorer_" .. self.uid_explorer, {})
o.watcher = watch.create_watcher(o) self.open = true
o.renderer = Renderer:new(config, o) self.opts = config
o.filters = Filters:new(config, o)
o.live_filter = LiveFilter:new(config, o)
o.marks = Marks:new(config, o)
o.clipboard = Clipboard:new(config, o)
o:_load(o) self.sorters = Sorter({ explorer = self })
self.renderer = Renderer({ explorer = self })
self.filters = Filters({ explorer = self })
self.live_filter = LiveFilter({ explorer = self })
self.marks = Marks({ explorer = self })
self.clipboard = Clipboard({ explorer = self })
return o self:create_autocmds()
self:_load(self)
end end
---@param node Node function Explorer:destroy()
log.line("dev", "Explorer:destroy")
vim.api.nvim_del_augroup_by_id(self.augroup_id)
RootNode.destroy(self)
end
function Explorer:create_autocmds()
-- reset and draw (highlights) when colorscheme is changed
vim.api.nvim_create_autocmd("ColorScheme", {
group = self.augroup_id,
callback = function()
appearance.setup()
view.reset_winhl()
self.renderer:draw()
end,
})
vim.api.nvim_create_autocmd("BufWritePost", {
group = self.augroup_id,
callback = function()
if self.opts.auto_reload_on_write and not self.opts.filesystem_watchers.enable then
self:reload_explorer()
end
end,
})
vim.api.nvim_create_autocmd("BufReadPost", {
group = self.augroup_id,
callback = function(data)
if (self.filters.state.no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then
utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
self:reload_explorer()
end)
end
end,
})
-- update opened file buffers
vim.api.nvim_create_autocmd("BufUnload", {
group = self.augroup_id,
callback = function(data)
if (self.filters.state.no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then
utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
self:reload_explorer()
end)
end
end,
})
vim.api.nvim_create_autocmd("BufEnter", {
group = self.augroup_id,
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
if vim.fn.getcwd() ~= core.get_cwd() or (self.opts.reload_on_bufenter and not self.opts.filesystem_watchers.enable) then
self:reload_explorer()
end
end
end,
})
vim.api.nvim_create_autocmd("User", {
group = self.augroup_id,
pattern = { "FugitiveChanged", "NeogitStatusRefreshed" },
callback = function()
if not self.opts.filesystem_watchers.enable and self.opts.git.enable then
self:reload_git()
end
end,
})
if self.opts.hijack_cursor then
vim.api.nvim_create_autocmd("CursorMoved", {
group = self.augroup_id,
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
self:place_cursor_on_node()
end
end,
})
end
if self.opts.modified.enable then
vim.api.nvim_create_autocmd({ "BufModifiedSet", "BufWritePost" }, {
group = self.augroup_id,
callback = function()
utils.debounce("Buf:modified_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
buffers.reload_modified()
self:reload_explorer()
end)
end,
})
end
end
---@param node DirectoryNode
function Explorer:expand(node) function Explorer:expand(node)
self:_load(node) self:_load(node)
end end
function Explorer:destroy() ---@param node DirectoryNode
local function iterate(node) ---@param project GitProject?
explorer_node.node_destroy(node) ---@return Node[]?
if node.nodes then function Explorer:reload(node, project)
for _, child in pairs(node.nodes) do
iterate(child)
end
end
end
iterate(self)
end
---@param node Node
---@param git_status table|nil
function Explorer:reload(node, git_status)
local cwd = node.link_to or node.absolute_path local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd) local handle = vim.loop.fs_scandir(cwd)
if not handle then if not handle then
@@ -102,7 +185,7 @@ function Explorer:reload(node, git_status)
local profile = log.profile_start("reload %s", node.absolute_path) local profile = log.profile_start("reload %s", node.absolute_path)
local filter_status = self.filters:prepare(git_status) local filter_status = self.filters:prepare(project)
if node.group_next then if node.group_next then
node.nodes = { node.group_next } node.nodes = { node.group_next }
@@ -111,16 +194,16 @@ function Explorer:reload(node, git_status)
local remain_childs = {} local remain_childs = {}
local node_ignored = explorer_node.is_git_ignored(node) local node_ignored = node:is_git_ignored()
---@type table<string, Node> ---@type table<string, Node>
local nodes_by_path = utils.key_by(node.nodes, "absolute_path") local nodes_by_path = utils.key_by(node.nodes, "absolute_path")
-- To reset we must 'zero' everything that we use -- To reset we must 'zero' everything that we use
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0, git = 0,
buf = 0, buf = 0,
dotfile = 0, dotfile = 0,
custom = 0, custom = 0,
bookmark = 0, bookmark = 0,
}) })
@@ -138,32 +221,25 @@ function Explorer:reload(node, git_status)
if filter_reason == FILTER_REASON.none then if filter_reason == FILTER_REASON.none then
remain_childs[abs] = true remain_childs[abs] = true
-- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility
local t = stat and stat.type or nil
-- Recreate node if type changes. -- Recreate node if type changes.
if nodes_by_path[abs] then if nodes_by_path[abs] then
local n = nodes_by_path[abs] local n = nodes_by_path[abs]
if n.type ~= t then if not stat or n.type ~= stat.type then
utils.array_remove(node.nodes, n) utils.array_remove(node.nodes, n)
explorer_node.node_destroy(n) n:destroy()
nodes_by_path[abs] = nil nodes_by_path[abs] = nil
end end
end end
if not nodes_by_path[abs] then if not nodes_by_path[abs] then
local new_child = nil local new_child = node_factory.create({
if t == "directory" and vim.loop.fs_access(abs, "R") and Watcher.is_fs_event_capable(abs) then explorer = self,
new_child = builders.folder(node, abs, name, stat) parent = node,
elseif t == "file" then absolute_path = abs,
new_child = builders.file(node, abs, name, stat) name = name,
elseif t == "link" then fs_stat = stat
local link = builders.link(node, abs, name, stat) })
if link.link_to ~= nil then
new_child = link
end
end
if new_child then if new_child then
table.insert(node.nodes, new_child) table.insert(node.nodes, new_child)
nodes_by_path[abs] = new_child nodes_by_path[abs] = new_child
@@ -171,7 +247,7 @@ function Explorer:reload(node, git_status)
else else
local n = nodes_by_path[abs] local n = nodes_by_path[abs]
if n then if n then
n.executable = builders.is_executable(abs) or false n.executable = utils.is_executable(abs) or false
n.fs_stat = stat n.fs_stat = stat
end end
end end
@@ -185,22 +261,21 @@ function Explorer:reload(node, git_status)
end end
node.nodes = vim.tbl_map( node.nodes = vim.tbl_map(
self:update_status(nodes_by_path, node_ignored, git_status), self:update_git_statuses(nodes_by_path, node_ignored, project),
vim.tbl_filter(function(n) vim.tbl_filter(function(n)
if remain_childs[n.absolute_path] then if remain_childs[n.absolute_path] then
return remain_childs[n.absolute_path] return remain_childs[n.absolute_path]
else else
explorer_node.node_destroy(n) n:destroy()
return false return false
end end
end, node.nodes) end, node.nodes)
) )
local is_root = not node.parent local single_child = node:single_child_directory()
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] if config.renderer.group_empty and node.parent and single_child then
if config.renderer.group_empty and not is_root and child_folder_only then node.group_next = single_child
node.group_next = child_folder_only local ns = self:reload(single_child, project)
local ns = self:reload(child_folder_only, git_status)
node.nodes = ns or {} node.nodes = ns or {}
log.profile_end(profile) log.profile_end(profile)
return ns return ns
@@ -212,26 +287,6 @@ function Explorer:reload(node, git_status)
return node.nodes return node.nodes
end end
---TODO #2837 #2871 move this and similar to node
---Refresh contents and git status for a single node
---@param node Node
---@param callback function
function Explorer:refresh_node(node, callback)
if type(node) ~= "table" then
callback()
end
local parent_node = utils.get_parent_of_group(node)
self:reload_and_get_git_project(node.absolute_path, function(toplevel, project)
self:reload(parent_node, project)
self:update_parent_statuses(parent_node, project, toplevel)
callback()
end)
end
---Refresh contents of all nodes to a path: actual directory and links. ---Refresh contents of all nodes to a path: actual directory and links.
---Groups will be expanded if needed. ---Groups will be expanded if needed.
---@param path string absolute path ---@param path string absolute path
@@ -259,97 +314,51 @@ function Explorer:refresh_parent_nodes_for_path(path)
local project = git.get_project(toplevel) or {} local project = git.get_project(toplevel) or {}
self:reload(node, project) self:reload(node, project)
self:update_parent_statuses(node, project, toplevel) git.update_parent_projects(node, project, toplevel)
end end
log.profile_end(profile) log.profile_end(profile)
end end
---@private ---@private
---@param node Node ---@param node DirectoryNode
function Explorer:_load(node) function Explorer:_load(node)
local cwd = node.link_to or node.absolute_path local cwd = node.link_to or node.absolute_path
local git_status = git.load_project_status(cwd) local project = git.load_project(cwd)
self:explore(node, git_status, self) self:explore(node, project, self)
end end
---@private ---@private
---@param nodes_by_path table ---@param nodes_by_path Node[]
---@param node_ignored boolean ---@param node_ignored boolean
---@param status table|nil ---@param project GitProject?
---@return fun(node: Node): table ---@return fun(node: Node): Node
function Explorer:update_status(nodes_by_path, node_ignored, status) function Explorer:update_git_statuses(nodes_by_path, node_ignored, project)
return function(node) return function(node)
if nodes_by_path[node.absolute_path] then if nodes_by_path[node.absolute_path] then
explorer_node.update_git_status(node, node_ignored, status) node:update_git_status(node_ignored, project)
end end
return node return node
end end
end end
---TODO #2837 #2871 move this and similar to node
---@private
---@param path string
---@param callback fun(toplevel: string|nil, project: table|nil)
function Explorer:reload_and_get_git_project(path, callback)
local toplevel = git.get_toplevel(path)
git.reload_project(toplevel, path, function()
callback(toplevel, git.get_project(toplevel) or {})
end)
end
---TODO #2837 #2871 move this and similar to node
---@private
---@param node Node
---@param project table|nil
---@param root string|nil
function Explorer:update_parent_statuses(node, project, root)
while project and node do
-- step up to the containing project
if node.absolute_path == root then
-- stop at the top of the tree
if not node.parent then
break
end
root = git.get_toplevel(node.parent.absolute_path)
-- stop when no more projects
if not root then
break
end
-- update the containing project
project = git.get_project(root)
git.reload_project(root, node.absolute_path, nil)
end
-- update status
explorer_node.update_git_status(node, explorer_node.is_git_ignored(node.parent), project)
-- maybe parent
node = node.parent
end
end
---@private ---@private
---@param handle uv.uv_fs_t ---@param handle uv.uv_fs_t
---@param cwd string ---@param cwd string
---@param node Node ---@param node DirectoryNode
---@param git_status table ---@param project GitProject
---@param parent Explorer ---@param parent Explorer
function Explorer:populate_children(handle, cwd, node, git_status, parent) function Explorer:populate_children(handle, cwd, node, project, parent)
local node_ignored = explorer_node.is_git_ignored(node) local node_ignored = node:is_git_ignored()
local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") local nodes_by_path = utils.bool_record(node.nodes, "absolute_path")
local filter_status = parent.filters:prepare(git_status) local filter_status = parent.filters:prepare(project)
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0, git = 0,
buf = 0, buf = 0,
dotfile = 0, dotfile = 0,
custom = 0, custom = 0,
bookmark = 0, bookmark = 0,
}) })
@@ -368,23 +377,17 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
local stat = vim.loop.fs_lstat(abs) local stat = vim.loop.fs_lstat(abs)
local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status) local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status)
if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] then if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] then
-- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility local child = node_factory.create({
local t = stat and stat.type or nil explorer = self,
local child = nil parent = node,
if t == "directory" and vim.loop.fs_access(abs, "R") then absolute_path = abs,
child = builders.folder(node, abs, name, stat) name = name,
elseif t == "file" then fs_stat = stat
child = builders.file(node, abs, name, stat) })
elseif t == "link" then
local link = builders.link(node, abs, name, stat)
if link.link_to ~= nil then
child = link
end
end
if child then if child then
table.insert(node.nodes, child) table.insert(node.nodes, child)
nodes_by_path[child.absolute_path] = true nodes_by_path[child.absolute_path] = true
explorer_node.update_git_status(child, node_ignored, git_status) child:update_git_status(node_ignored, project)
end end
else else
for reason, value in pairs(FILTER_REASON) do for reason, value in pairs(FILTER_REASON) do
@@ -400,11 +403,11 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
end end
---@private ---@private
---@param node Node ---@param node DirectoryNode
---@param status table ---@param project GitProject
---@param parent Explorer ---@param parent Explorer
---@return Node[]|nil ---@return Node[]|nil
function Explorer:explore(node, status, parent) function Explorer:explore(node, project, parent)
local cwd = node.link_to or node.absolute_path local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd) local handle = vim.loop.fs_scandir(cwd)
if not handle then if not handle then
@@ -413,15 +416,15 @@ function Explorer:explore(node, status, parent)
local profile = log.profile_start("explore %s", node.absolute_path) local profile = log.profile_start("explore %s", node.absolute_path)
self:populate_children(handle, cwd, node, status, parent) self:populate_children(handle, cwd, node, project, parent)
local is_root = not node.parent local is_root = not node.parent
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1] local single_child = node:single_child_directory()
if config.renderer.group_empty and not is_root and child_folder_only then if config.renderer.group_empty and not is_root and single_child then
local child_cwd = child_folder_only.link_to or child_folder_only.absolute_path local child_cwd = single_child.link_to or single_child.absolute_path
local child_status = git.load_project_status(child_cwd) local child_project = git.load_project(child_cwd)
node.group_next = child_folder_only node.group_next = single_child
local ns = self:explore(child_folder_only, child_status, parent) local ns = self:explore(single_child, child_project, parent)
node.nodes = ns or {} node.nodes = ns or {}
log.profile_end(profile) log.profile_end(profile)
@@ -436,13 +439,14 @@ function Explorer:explore(node, status, parent)
end end
---@private ---@private
---@param projects table ---@param projects GitProject[]
function Explorer:refresh_nodes(projects) function Explorer:refresh_nodes(projects)
Iterator.builder({ self }) Iterator.builder({ self })
:applier(function(n) :applier(function(n)
if n.nodes then local dir = n:as(DirectoryNode)
local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path) if dir then
self:reload(n, projects[toplevel] or {}) local toplevel = git.get_toplevel(dir.cwd or dir.link_to or dir.absolute_path)
self:reload(dir, projects[toplevel] or {})
end end
end) end)
:recursor(function(n) :recursor(function(n)
@@ -458,7 +462,7 @@ function Explorer:reload_explorer()
end end
event_running = true event_running = true
local projects = git.reload() local projects = git.reload_all_projects()
self:refresh_nodes(projects) self:refresh_nodes(projects)
if view.is_visible() then if view.is_visible() then
self.renderer:draw() self.renderer:draw()
@@ -472,16 +476,67 @@ function Explorer:reload_git()
end end
event_running = true event_running = true
local projects = git.reload() local projects = git.reload_all_projects()
explorer_node.reload_node_status(self, projects) git.reload_node_status(self, projects)
self.renderer:draw() self.renderer:draw()
event_running = false event_running = false
end end
function Explorer.setup(opts) ---Cursor position as per vim.api.nvim_win_get_cursor
---nil on no explorer or invalid view win
---@return integer[]|nil
function Explorer:get_cursor_position()
local winnr = view.get_winnr()
if not winnr or not vim.api.nvim_win_is_valid(winnr) then
return
end
return vim.api.nvim_win_get_cursor(winnr)
end
---@return Node|nil
function Explorer:get_node_at_cursor()
local cursor = self:get_cursor_position()
if not cursor then
return
end
if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then
return self
end
return utils.get_nodes_by_line(self.nodes, core.get_nodes_starting_line())[cursor[1]]
end
function Explorer:place_cursor_on_node()
local ok, search = pcall(vim.fn.searchcount)
if ok and search and search.exact_match == 1 then
return
end
local node = self:get_node_at_cursor()
if not node or node.name == ".." then
return
end
node = node:get_parent_of_group() or node
local line = vim.api.nvim_get_current_line()
local cursor = vim.api.nvim_win_get_cursor(0)
local idx = vim.fn.stridx(line, node.name)
if idx >= 0 then
vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
end
end
---Api.tree.get_nodes
---@return nvim_tree.api.Node
function Explorer:get_nodes()
return self:clone()
end
function Explorer:setup(opts)
config = opts config = opts
require("nvim-tree.explorer.node").setup(opts)
require("nvim-tree.explorer.watch").setup(opts)
end end
return Explorer return Explorer

View File

@@ -1,29 +1,33 @@
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local Iterator = require("nvim-tree.iterators.node-iterator")
---@class LiveFilter local Class = require("nvim-tree.classic")
local Iterator = require("nvim-tree.iterators.node-iterator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) LiveFilter: Class
---@field explorer Explorer ---@field explorer Explorer
---@field prefix string ---@field prefix string
---@field always_show_folders boolean ---@field always_show_folders boolean
---@field filter string ---@field filter string
local LiveFilter = {} local LiveFilter = Class:extend()
---@param opts table ---@class LiveFilter
---@param explorer Explorer ---@overload fun(args: LiveFilterArgs): LiveFilter
function LiveFilter:new(opts, explorer)
local o = { ---@class (exact) LiveFilterArgs
explorer = explorer, ---@field explorer Explorer
prefix = opts.live_filter.prefix,
always_show_folders = opts.live_filter.always_show_folders, ---@protected
filter = nil, ---@param args LiveFilterArgs
} function LiveFilter:new(args)
setmetatable(o, self) self.explorer = args.explorer
self.__index = self self.prefix = self.explorer.opts.live_filter.prefix
return o self.always_show_folders = self.explorer.opts.live_filter.always_show_folders
self.filter = nil
end end
---@param node_ Node|nil ---@param node_ Node?
local function reset_filter(self, node_) local function reset_filter(self, node_)
node_ = node_ or self.explorer node_ = node_ or self.explorer
@@ -31,17 +35,19 @@ local function reset_filter(self, node_)
return return
end end
node_.hidden_stats = vim.tbl_deep_extend("force", node_.hidden_stats or {}, { local dir_ = node_:as(DirectoryNode)
live_filter = 0, if dir_ then
}) dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, })
end
Iterator.builder(node_.nodes) Iterator.builder(node_.nodes)
:hidden() :hidden()
:applier(function(node) :applier(function(node)
node.hidden = false node.hidden = false
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { local dir = node:as(DirectoryNode)
live_filter = 0, if dir then
}) dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
end
end) end)
:iterate() :iterate()
end end
@@ -76,7 +82,7 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
local function matches(self, node) local function matches(self, node)
if not self.explorer.filters.config.enable then if not self.explorer.filters.enabled then
return true return true
end end
@@ -85,14 +91,14 @@ local function matches(self, node)
return vim.regex(self.filter):match_str(name) ~= nil return vim.regex(self.filter):match_str(name) ~= nil
end end
---@param node_ Node|nil ---@param node_ DirectoryNode?
function LiveFilter:apply_filter(node_) function LiveFilter:apply_filter(node_)
if not self.filter or self.filter == "" then if not self.filter or self.filter == "" then
reset_filter(self, node_) reset_filter(self, node_)
return return
end end
-- TODO(kiyan): this iterator cannot yet be refactored with the Iterator module -- this iterator cannot yet be refactored with the Iterator module
-- since the node mapper is based on its children -- since the node mapper is based on its children
local function iterate(node) local function iterate(node)
local filtered_nodes = 0 local filtered_nodes = 0
@@ -163,21 +169,21 @@ local function create_overlay(self)
if view.View.float.enable then if view.View.float.enable then
-- don't close nvim-tree float when focus is changed to filter window -- don't close nvim-tree float when focus is changed to filter window
vim.api.nvim_clear_autocmds({ vim.api.nvim_clear_autocmds({
event = "WinLeave", event = "WinLeave",
pattern = "NvimTree_*", pattern = "NvimTree_*",
group = vim.api.nvim_create_augroup("NvimTree", { clear = false }), group = vim.api.nvim_create_augroup("NvimTree", { clear = false }),
}) })
end end
configure_buffer_overlay(self) configure_buffer_overlay(self)
overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, { overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, {
col = 1, col = 1,
row = 0, row = 0,
relative = "cursor", relative = "cursor",
width = calculate_overlay_win_width(self), width = calculate_overlay_win_width(self),
height = 1, height = 1,
border = "none", border = "none",
style = "minimal", style = "minimal",
}) })
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
@@ -192,7 +198,7 @@ local function create_overlay(self)
end end
function LiveFilter:start_filtering() function LiveFilter:start_filtering()
view.View.live_filter.prev_focused_node = require("nvim-tree.lib").get_node_at_cursor() view.View.live_filter.prev_focused_node = self.explorer:get_node_at_cursor()
self.filter = self.filter or "" self.filter = self.filter or ""
self.explorer.renderer:draw() self.explorer.renderer:draw()
@@ -206,7 +212,7 @@ function LiveFilter:start_filtering()
end end
function LiveFilter:clear_filter() function LiveFilter:clear_filter()
local node = require("nvim-tree.lib").get_node_at_cursor() local node = self.explorer:get_node_at_cursor()
local last_node = view.View.live_filter.prev_focused_node local last_node = view.View.live_filter.prev_focused_node
self.filter = nil self.filter = nil

View File

@@ -1,107 +0,0 @@
local utils = require("nvim-tree.utils")
local watch = require("nvim-tree.explorer.watch")
local M = {}
---@param parent Node
---@param absolute_path string
---@param name string
---@param fs_stat uv.fs_stat.result|nil
---@return Node
function M.folder(parent, absolute_path, name, fs_stat)
local handle = vim.loop.fs_scandir(absolute_path)
local has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil
local node = {
type = "directory",
absolute_path = absolute_path,
fs_stat = fs_stat,
group_next = nil, -- If node is grouped, this points to the next child dir/link node
has_children = has_children,
name = name,
nodes = {},
open = false,
parent = parent,
}
node.watcher = watch.create_watcher(node)
return node
end
--- path is an executable file or directory
---@param absolute_path string
---@return boolean|nil
function M.is_executable(absolute_path)
if utils.is_windows or utils.is_wsl then
--- executable detection on windows is buggy and not performant hence it is disabled
return false
else
return vim.loop.fs_access(absolute_path, "X")
end
end
---@param parent Node
---@param absolute_path string
---@param name string
---@param fs_stat uv.fs_stat.result|nil
---@return Node
function M.file(parent, absolute_path, name, fs_stat)
local ext = string.match(name, ".?[^.]+%.(.*)") or ""
return {
type = "file",
absolute_path = absolute_path,
executable = M.is_executable(absolute_path),
extension = ext,
fs_stat = fs_stat,
name = name,
parent = parent,
}
end
-- TODO-INFO: sometimes fs_realpath returns nil
-- I expect this be a bug in glibc, because it fails to retrieve the path for some
-- links (for instance libr2.so in /usr/lib) and thus even with a C program realpath fails
-- when it has no real reason to. Maybe there is a reason, but errno is definitely wrong.
-- So we need to check for link_to ~= nil when adding new links to the main tree
---@param parent Node
---@param absolute_path string
---@param name string
---@param fs_stat uv.fs_stat.result|nil
---@return Node
function M.link(parent, absolute_path, name, fs_stat)
--- I dont know if this is needed, because in my understanding, there isn't hard links in windows, but just to be sure i changed it.
local link_to = vim.loop.fs_realpath(absolute_path)
local open, nodes, has_children
local is_dir_link = (link_to ~= nil) and vim.loop.fs_stat(link_to).type == "directory"
if is_dir_link and link_to then
local handle = vim.loop.fs_scandir(link_to)
has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil
open = false
nodes = {}
end
local node = {
type = "link",
absolute_path = absolute_path,
fs_stat = fs_stat,
group_next = nil, -- If node is grouped, this points to the next child dir/link node
has_children = has_children,
link_to = link_to,
name = name,
nodes = nodes,
open = open,
parent = parent,
}
if is_dir_link then
node.watcher = watch.create_watcher(node)
end
return node
end
return M

View File

@@ -1,183 +0,0 @@
local git = {} -- circular dependencies
local M = {}
---@class GitStatus
---@field file string|nil
---@field dir table|nil
---@param parent_ignored boolean
---@param status table|nil
---@param absolute_path string
---@return GitStatus|nil
local function get_dir_git_status(parent_ignored, status, absolute_path)
if parent_ignored then
return { file = "!!" }
end
if status then
return {
file = status.files and status.files[absolute_path],
dir = status.dirs and {
direct = status.dirs.direct[absolute_path],
indirect = status.dirs.indirect[absolute_path],
},
}
end
end
---@param parent_ignored boolean
---@param status table
---@param absolute_path string
---@return GitStatus
local function get_git_status(parent_ignored, status, absolute_path)
local file_status = parent_ignored and "!!" or (status and status.files and status.files[absolute_path])
return { file = file_status }
end
---@param node Node
---@return boolean
function M.has_one_child_folder(node)
return #node.nodes == 1 and node.nodes[1].nodes and vim.loop.fs_access(node.nodes[1].absolute_path, "R") or false
end
---@param node Node
---@param parent_ignored boolean
---@param status table|nil
function M.update_git_status(node, parent_ignored, status)
local get_status
if node.nodes then
get_status = get_dir_git_status
else
get_status = get_git_status
end
-- status of the node's absolute path
node.git_status = get_status(parent_ignored, status, node.absolute_path)
-- status of the link target, if the link itself is not dirty
if node.link_to and not node.git_status then
node.git_status = get_status(parent_ignored, status, node.link_to)
end
end
---@param node Node
---@return GitStatus|nil
function M.get_git_status(node)
local git_status = node and node.git_status
if not git_status then
-- status doesn't exist
return nil
end
if not node.nodes then
-- file
return git_status.file and { git_status.file }
end
-- dir
if not M.config.git.show_on_dirs then
return nil
end
local status = {}
if not require("nvim-tree.lib").get_last_group_node(node).open or M.config.git.show_on_open_dirs then
-- dir is closed or we should show on open_dirs
if git_status.file ~= nil then
table.insert(status, git_status.file)
end
if git_status.dir ~= nil then
if git_status.dir.direct ~= nil then
for _, s in pairs(node.git_status.dir.direct) do
table.insert(status, s)
end
end
if git_status.dir.indirect ~= nil then
for _, s in pairs(node.git_status.dir.indirect) do
table.insert(status, s)
end
end
end
else
-- dir is open and we shouldn't show on open_dirs
if git_status.file ~= nil then
table.insert(status, git_status.file)
end
if git_status.dir ~= nil and git_status.dir.direct ~= nil then
local deleted = {
[" D"] = true,
["D "] = true,
["RD"] = true,
["DD"] = true,
}
for _, s in pairs(node.git_status.dir.direct) do
if deleted[s] then
table.insert(status, s)
end
end
end
end
if #status == 0 then
return nil
else
return status
end
end
---@param parent_node Node|nil
---@param projects table
function M.reload_node_status(parent_node, projects)
if parent_node == nil then
return
end
local toplevel = git.get_toplevel(parent_node.absolute_path)
local status = projects[toplevel] or {}
for _, node in ipairs(parent_node.nodes) do
M.update_git_status(node, M.is_git_ignored(parent_node), status)
if node.nodes and #node.nodes > 0 then
M.reload_node_status(node, projects)
end
end
end
---@param node Node
---@return boolean
function M.is_git_ignored(node)
return node and node.git_status ~= nil and node.git_status.file == "!!"
end
---@param node Node
---@return boolean
function M.is_dotfile(node)
if node == nil then
return false
end
if node.is_dot or (node.name and (node.name:sub(1, 1) == ".")) or M.is_dotfile(node.parent) then
node.is_dot = true
return true
end
return false
end
---@param node Node
function M.node_destroy(node)
if not node then
return
end
if node.watcher then
node.watcher:destroy()
node.watcher = nil
end
end
function M.setup(opts)
M.config = {
git = opts.git,
}
git = require("nvim-tree.git")
end
return M

View File

@@ -1,27 +1,25 @@
local C = {} local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory")
---@alias SorterType "name" | "case_sensitive" | "modification_time" | "extension" | "suffix" | "filetype"
---@alias SorterComparator fun(self: Sorter, a: Node, b: Node): boolean?
---@alias SorterUser fun(nodes: Node[]): SorterType?
---@class (exact) Sorter: Class
---@field private explorer Explorer
local Sorter = Class:extend()
---@class Sorter ---@class Sorter
local Sorter = {} ---@overload fun(args: SorterArgs): Sorter
function Sorter:new(opts) ---@class (exact) SorterArgs
local o = {} ---@field explorer Explorer
setmetatable(o, self)
self.__index = self
o.config = vim.deepcopy(opts.sort)
if type(o.config.sorter) == "function" then ---@protected
o.user = o.config.sorter ---@param args SorterArgs
end function Sorter:new(args)
return o self.explorer = args.explorer
end
--- Predefined comparator, defaulting to name
---@param sorter string as per options
---@return function
function Sorter:get_comparator(sorter)
return function(a, b)
return (C[sorter] or C.name)(a, b, self.config)
end
end end
---Create a shallow copy of a portion of a list. ---Create a shallow copy of a portion of a list.
@@ -38,30 +36,32 @@ local function tbl_slice(t, first, last)
return slice return slice
end end
---Evaluate `sort.folders_first` and `sort.files_first` ---Evaluate folders_first and sort.files_first returning nil when no order is necessary
---@param a Node ---@private
---@param b Node ---@type SorterComparator
---@return boolean|nil function Sorter:folders_or_files_first(a, b)
local function folders_or_files_first(a, b, cfg) if not (self.explorer.opts.sort.folders_first or self.explorer.opts.sort.files_first) then
if not (cfg.folders_first or cfg.files_first) then return nil
return
end end
if not a.nodes and b.nodes then if not a:is(DirectoryNode) and b:is(DirectoryNode) then
-- file <> folder -- file <> folder
return cfg.files_first return self.explorer.opts.sort.files_first
elseif a.nodes and not b.nodes then elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then
-- folder <> file -- folder <> file
return not cfg.files_first return not self.explorer.opts.sort.files_first
end end
return nil
end end
---@param t table ---@private
---@param t Node[]
---@param first number ---@param first number
---@param mid number ---@param mid number
---@param last number ---@param last number
---@param comparator fun(a: Node, b: Node): boolean ---@param comparator SorterComparator
local function merge(t, first, mid, last, comparator) function Sorter:merge(t, first, mid, last, comparator)
local n1 = mid - first + 1 local n1 = mid - first + 1
local n2 = last - mid local n2 = last - mid
local ls = tbl_slice(t, first, mid) local ls = tbl_slice(t, first, mid)
@@ -71,7 +71,7 @@ local function merge(t, first, mid, last, comparator)
local k = first local k = first
while i <= n1 and j <= n2 do while i <= n1 and j <= n2 do
if comparator(ls[i], rs[j]) then if comparator(self, ls[i], rs[j]) then
t[k] = ls[i] t[k] = ls[i]
i = i + 1 i = i + 1
else else
@@ -94,45 +94,49 @@ local function merge(t, first, mid, last, comparator)
end end
end end
---@param t table ---@private
---@param t Node[]
---@param first number ---@param first number
---@param last number ---@param last number
---@param comparator fun(a: Node, b: Node): boolean ---@param comparator SorterComparator
local function split_merge(t, first, last, comparator) function Sorter:split_merge(t, first, last, comparator)
if (last - first) < 1 then if (last - first) < 1 then
return return
end end
local mid = math.floor((first + last) / 2) local mid = math.floor((first + last) / 2)
split_merge(t, first, mid, comparator) self:split_merge(t, first, mid, comparator)
split_merge(t, mid + 1, last, comparator) self:split_merge(t, mid + 1, last, comparator)
merge(t, first, mid, last, comparator) self:merge(t, first, mid, last, comparator)
end end
---Perform a merge sort using sorter option. ---Perform a merge sort using sorter option.
---@param t table nodes ---@param t Node[]
function Sorter:sort(t) function Sorter:sort(t)
if self.user then if self[self.explorer.opts.sort.sorter] then
self:split_merge(t, 1, #t, self[self.explorer.opts.sort.sorter])
elseif type(self.explorer.opts.sort.sorter) == "function" then
local t_user = {} local t_user = {}
local origin_index = {} local origin_index = {}
for _, n in ipairs(t) do for _, n in ipairs(t) do
table.insert(t_user, { table.insert(t_user, {
absolute_path = n.absolute_path, absolute_path = n.absolute_path,
executable = n.executable, executable = n.executable,
extension = n.extension, extension = n.extension,
filetype = vim.filetype.match({ filename = n.name }), filetype = vim.filetype.match({ filename = n.name }),
link_to = n.link_to, link_to = n.link_to,
name = n.name, name = n.name,
type = n.type, type = n.type,
}) })
table.insert(origin_index, n) table.insert(origin_index, n)
end end
local predefined = self.user(t_user) -- user may return a SorterType
if predefined then local ret = self.explorer.opts.sort.sorter(t_user)
split_merge(t, 1, #t, self:get_comparator(predefined)) if self[ret] then
self:split_merge(t, 1, #t, self[ret])
return return
end end
@@ -145,7 +149,7 @@ function Sorter:sort(t)
end end
-- if missing value found, then using origin_index -- if missing value found, then using origin_index
local mini_comparator = function(a, b) local mini_comparator = function(_, a, b)
local a_index = user_index[a.absolute_path] or origin_index[a.absolute_path] local a_index = user_index[a.absolute_path] or origin_index[a.absolute_path]
local b_index = user_index[b.absolute_path] or origin_index[b.absolute_path] local b_index = user_index[b.absolute_path] or origin_index[b.absolute_path]
@@ -155,47 +159,52 @@ function Sorter:sort(t)
return (a_index or 0) <= (b_index or 0) return (a_index or 0) <= (b_index or 0)
end end
split_merge(t, 1, #t, mini_comparator) -- sort by user order self:split_merge(t, 1, #t, mini_comparator) -- sort by user order
else
split_merge(t, 1, #t, self:get_comparator(self.config.sorter))
end end
end end
---@private
---@param a Node ---@param a Node
---@param b Node ---@param b Node
---@param ignorecase boolean|nil ---@param ignore_case boolean
---@return boolean ---@return boolean
local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg) function Sorter:name_case(a, b, ignore_case)
if not (a and b) then if not (a and b) then
return true return true
end end
local early_return = folders_or_files_first(a, b, cfg) local early_return = self:folders_or_files_first(a, b)
if early_return ~= nil then if early_return ~= nil then
return early_return return early_return
end end
if ignorecase then if ignore_case then
return a.name:lower() <= b.name:lower() return a.name:lower() <= b.name:lower()
else else
return a.name <= b.name return a.name <= b.name
end end
end end
function C.case_sensitive(a, b, cfg) ---@private
return node_comparator_name_ignorecase_or_not(a, b, false, cfg) ---@type SorterComparator
function Sorter:case_sensitive(a, b)
return self:name_case(a, b, false)
end end
function C.name(a, b, cfg) ---@private
return node_comparator_name_ignorecase_or_not(a, b, true, cfg) ---@type SorterComparator
function Sorter:name(a, b)
return self:name_case(a, b, true)
end end
function C.modification_time(a, b, cfg) ---@private
---@type SorterComparator
function Sorter:modification_time(a, b)
if not (a and b) then if not (a and b) then
return true return true
end end
local early_return = folders_or_files_first(a, b, cfg) local early_return = self:folders_or_files_first(a, b)
if early_return ~= nil then if early_return ~= nil then
return early_return return early_return
end end
@@ -214,17 +223,19 @@ function C.modification_time(a, b, cfg)
return last_modified_b <= last_modified_a return last_modified_b <= last_modified_a
end end
function C.suffix(a, b, cfg) ---@private
---@type SorterComparator
function Sorter:suffix(a, b)
if not (a and b) then if not (a and b) then
return true return true
end end
-- directories go first -- directories go first
local early_return = folders_or_files_first(a, b, cfg) local early_return = self:folders_or_files_first(a, b)
if early_return ~= nil then if early_return ~= nil then
return early_return return early_return
elseif a.nodes and b.nodes then elseif a.nodes and b.nodes then
return C.name(a, b, cfg) return self:name(a, b)
end end
-- dotfiles go second -- dotfiles go second
@@ -233,7 +244,7 @@ function C.suffix(a, b, cfg)
elseif a.name:sub(1, 1) ~= "." and b.name:sub(1, 1) == "." then elseif a.name:sub(1, 1) ~= "." and b.name:sub(1, 1) == "." then
return false return false
elseif a.name:sub(1, 1) == "." and b.name:sub(1, 1) == "." then elseif a.name:sub(1, 1) == "." and b.name:sub(1, 1) == "." then
return C.name(a, b, cfg) return self:name(a, b)
end end
-- unsuffixed go third -- unsuffixed go third
@@ -245,7 +256,7 @@ function C.suffix(a, b, cfg)
elseif a_suffix_ndx and not b_suffix_ndx then elseif a_suffix_ndx and not b_suffix_ndx then
return false return false
elseif not (a_suffix_ndx and b_suffix_ndx) then elseif not (a_suffix_ndx and b_suffix_ndx) then
return C.name(a, b, cfg) return self:name(a, b)
end end
-- finally, compare by suffixes -- finally, compare by suffixes
@@ -257,18 +268,20 @@ function C.suffix(a, b, cfg)
elseif not a_suffix and b_suffix then elseif not a_suffix and b_suffix then
return false return false
elseif a_suffix:lower() == b_suffix:lower() then elseif a_suffix:lower() == b_suffix:lower() then
return C.name(a, b, cfg) return self:name(a, b)
end end
return a_suffix:lower() < b_suffix:lower() return a_suffix:lower() < b_suffix:lower()
end end
function C.extension(a, b, cfg) ---@private
---@type SorterComparator
function Sorter:extension(a, b)
if not (a and b) then if not (a and b) then
return true return true
end end
local early_return = folders_or_files_first(a, b, cfg) local early_return = self:folders_or_files_first(a, b)
if early_return ~= nil then if early_return ~= nil then
return early_return return early_return
end end
@@ -282,18 +295,20 @@ function C.extension(a, b, cfg)
local a_ext = (a.extension or ""):lower() local a_ext = (a.extension or ""):lower()
local b_ext = (b.extension or ""):lower() local b_ext = (b.extension or ""):lower()
if a_ext == b_ext then if a_ext == b_ext then
return C.name(a, b, cfg) return self:name(a, b)
end end
return a_ext < b_ext return a_ext < b_ext
end end
function C.filetype(a, b, cfg) ---@private
---@type SorterComparator
function Sorter:filetype(a, b)
local a_ft = vim.filetype.match({ filename = a.name }) local a_ft = vim.filetype.match({ filename = a.name })
local b_ft = vim.filetype.match({ filename = b.name }) local b_ft = vim.filetype.match({ filename = b.name })
-- directories first -- directories first
local early_return = folders_or_files_first(a, b, cfg) local early_return = self:folders_or_files_first(a, b)
if early_return ~= nil then if early_return ~= nil then
return early_return return early_return
end end
@@ -307,7 +322,7 @@ function C.filetype(a, b, cfg)
-- same filetype or both nil, sort by name -- same filetype or both nil, sort by name
if a_ft == b_ft then if a_ft == b_ft then
return C.name(a, b, cfg) return self:name(a, b)
end end
return a_ft < b_ft return a_ft < b_ft

View File

@@ -1,4 +1,5 @@
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local git = require("nvim-tree.git")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local Watcher = require("nvim-tree.watcher").Watcher local Watcher = require("nvim-tree.watcher").Watcher
@@ -53,7 +54,7 @@ local function is_folder_ignored(path)
return false return false
end end
---@param node Node ---@param node DirectoryNode
---@return Watcher|nil ---@return Watcher|nil
function M.create_watcher(node) function M.create_watcher(node)
if not M.config.filesystem_watchers.enable or type(node) ~= "table" then if not M.config.filesystem_watchers.enable or type(node) ~= "table" then
@@ -65,9 +66,10 @@ function M.create_watcher(node)
return nil return nil
end end
---@param watcher Watcher
local function callback(watcher) local function callback(watcher)
log.line("watcher", "node event scheduled refresh %s", watcher.context) log.line("watcher", "node event scheduled refresh %s", watcher.data.context)
utils.debounce(watcher.context, M.config.filesystem_watchers.debounce_delay, function() utils.debounce(watcher.data.context, M.config.filesystem_watchers.debounce_delay, function()
if watcher.destroyed then if watcher.destroyed then
return return
end end
@@ -76,18 +78,17 @@ function M.create_watcher(node)
else else
log.line("watcher", "node event executing refresh '%s'", node.absolute_path) log.line("watcher", "node event executing refresh '%s'", node.absolute_path)
end end
local explorer = require("nvim-tree.core").get_explorer() git.refresh_dir(node)
if explorer then
explorer:refresh_node(node, function()
explorer.renderer:draw()
end)
end
end) end)
end end
M.uid = M.uid + 1 M.uid = M.uid + 1
return Watcher:new(path, nil, callback, { return Watcher:create({
context = "explorer:watch:" .. path .. ":" .. M.uid, path = path,
callback = callback,
data = {
context = "explorer:watch:" .. path .. ":" .. M.uid
}
}) })
end end

View File

@@ -1,21 +1,48 @@
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local git_utils = require("nvim-tree.git.utils") local git_utils = require("nvim-tree.git.utils")
local Runner = require("nvim-tree.git.runner")
local GitRunner = require("nvim-tree.git.runner")
local Watcher = require("nvim-tree.watcher").Watcher local Watcher = require("nvim-tree.watcher").Watcher
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local explorer_node = require("nvim-tree.explorer.node") local DirectoryNode = require("nvim-tree.node.directory")
---Git short format status xy
---@alias GitXY string
-- Git short-format status
---@alias GitPathXY table<string, GitXY>
-- Git short-format statuses
---@alias GitPathXYs table<string, GitXY[]>
---Git short-format statuses for a single node
---@class (exact) GitNodeStatus
---@field file GitXY?
---@field dir table<"direct" | "indirect", GitXY[]>?
---Git state for an entire repo
---@class (exact) GitProject
---@field files GitProjectFiles?
---@field dirs GitProjectDirs?
---@field watcher Watcher?
---@alias GitProjectFiles GitPathXY
---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs>
local M = { local M = {
config = {}, config = {},
-- all projects keyed by toplevel ---all projects keyed by toplevel
---@type table<string, GitProject>
_projects_by_toplevel = {}, _projects_by_toplevel = {},
-- index of paths inside toplevels, false when not inside a project ---index of paths inside toplevels, false when not inside a project
---@type table<string, string|false>
_toplevels_by_path = {}, _toplevels_by_path = {},
-- git dirs by toplevel -- git dirs by toplevel
---@type table<string, string>
_git_dirs_by_toplevel = {}, _git_dirs_by_toplevel = {},
} }
@@ -31,35 +58,35 @@ local WATCHED_FILES = {
---@param toplevel string|nil ---@param toplevel string|nil
---@param path string|nil ---@param path string|nil
---@param project table ---@param project GitProject
---@param git_status table|nil ---@param project_files GitProjectFiles?
local function reload_git_status(toplevel, path, project, git_status) local function reload_git_project(toplevel, path, project, project_files)
if path then if path then
for p in pairs(project.files) do for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then if p:find(path, 1, true) == 1 then
project.files[p] = nil project.files[p] = nil
end end
end end
project.files = vim.tbl_deep_extend("force", project.files, git_status) project.files = vim.tbl_deep_extend("force", project.files, project_files)
else else
project.files = git_status project.files = project_files or {}
end end
project.dirs = git_utils.file_status_to_dir_status(project.files, toplevel) project.dirs = git_utils.project_files_to_project_dirs(project.files, toplevel)
end end
--- Is this path in a known ignored directory? --- Is this path in a known ignored directory?
---@param path string ---@param path string
---@param project table git status ---@param project GitProject
---@return boolean ---@return boolean
local function path_ignored_in_project(path, project) local function path_ignored_in_project(path, project)
if not path or not project then if not path or not project then
return false return false
end end
if project and project.files then if project.files then
for file, status in pairs(project.files) do for p, xy in pairs(project.files) do
if status == "!!" and vim.startswith(path, file) then if xy == "!!" and vim.startswith(path, p) then
return true return true
end end
end end
@@ -67,9 +94,8 @@ local function path_ignored_in_project(path, project)
return false return false
end end
--- Reload all projects ---@return GitProject[] maybe empty
---@return table projects maybe empty function M.reload_all_projects()
function M.reload()
if not M.config.git.enable then if not M.config.git.enable then
return {} return {}
end end
@@ -82,11 +108,12 @@ function M.reload()
end end
--- Reload one project. Does nothing when no project or path is ignored --- Reload one project. Does nothing when no project or path is ignored
---@param toplevel string|nil ---@param toplevel string?
---@param path string|nil optional path to update only ---@param path string? optional path to update only
---@param callback function|nil ---@param callback function?
function M.reload_project(toplevel, path, callback) function M.reload_project(toplevel, path, callback)
local project = M._projects_by_toplevel[toplevel] local project = M._projects_by_toplevel[toplevel] --[[@as GitProject]]
if not toplevel or not project or not M.config.git.enable then if not toplevel or not project or not M.config.git.enable then
if callback then if callback then
callback() callback()
@@ -101,29 +128,31 @@ function M.reload_project(toplevel, path, callback)
return return
end end
local opts = { ---@type GitRunnerArgs
toplevel = toplevel, local args = {
path = path, toplevel = toplevel,
path = path,
list_untracked = git_utils.should_show_untracked(toplevel), list_untracked = git_utils.should_show_untracked(toplevel),
list_ignored = true, list_ignored = true,
timeout = M.config.git.timeout, timeout = M.config.git.timeout,
} }
if callback then if callback then
Runner.run(opts, function(git_status) ---@param path_xy GitPathXY
reload_git_status(toplevel, path, project, git_status) args.callback = function(path_xy)
reload_git_project(toplevel, path, project, path_xy)
callback() callback()
end) end
GitRunner:run(args)
else else
-- TODO use callback once async/await is available -- TODO #1974 use callback once async/await is available
local git_status = Runner.run(opts) reload_git_project(toplevel, path, project, GitRunner:run(args))
reload_git_status(toplevel, path, project, git_status)
end end
end end
--- Retrieve a known project --- Retrieve a known project
---@param toplevel string|nil ---@param toplevel string?
---@return table|nil project ---@return GitProject? project
function M.get_project(toplevel) function M.get_project(toplevel)
return M._projects_by_toplevel[toplevel] return M._projects_by_toplevel[toplevel]
end end
@@ -144,11 +173,10 @@ function M.get_toplevel(path)
return nil return nil
end end
if M._toplevels_by_path[path] then local tl = M._toplevels_by_path[path]
return M._toplevels_by_path[path] if tl then
end return tl
elseif tl == false then
if M._toplevels_by_path[path] == false then
return nil return nil
end end
@@ -187,8 +215,15 @@ function M.get_toplevel(path)
end end
M._toplevels_by_path[path] = toplevel M._toplevels_by_path[path] = toplevel
M._git_dirs_by_toplevel[toplevel] = git_dir M._git_dirs_by_toplevel[toplevel] = git_dir
return M._toplevels_by_path[path]
toplevel = M._toplevels_by_path[path]
if toplevel == false then
return nil
else
return toplevel
end
end end
local function reload_tree_at(toplevel) local function reload_tree_at(toplevel)
@@ -203,31 +238,28 @@ local function reload_tree_at(toplevel)
end end
M.reload_project(toplevel, nil, function() M.reload_project(toplevel, nil, function()
local git_status = M.get_project(toplevel) local project = M.get_project(toplevel)
Iterator.builder(root_node.nodes) Iterator.builder(root_node.nodes)
:hidden() :hidden()
:applier(function(node) :applier(function(node)
local parent_ignored = explorer_node.is_git_ignored(node.parent) local parent_ignored = node.parent and node.parent:is_git_ignored() or false
explorer_node.update_git_status(node, parent_ignored, git_status) node:update_git_status(parent_ignored, project)
end) end)
:recursor(function(node) :recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes return node.nodes and #node.nodes > 0 and node.nodes
end) end)
:iterate() :iterate()
local explorer = require("nvim-tree.core").get_explorer() root_node.explorer.renderer:draw()
if explorer then
explorer.renderer:draw()
end
end) end)
end end
--- Load the project status for a path. Does nothing when no toplevel for path. --- Load the project status for a path. Does nothing when no toplevel for path.
--- Only fetches project status when unknown, otherwise returns existing. --- Only fetches project status when unknown, otherwise returns existing.
---@param path string absolute ---@param path string absolute
---@return table project maybe empty ---@return GitProject maybe empty
function M.load_project_status(path) function M.load_project(path)
if not M.config.git.enable then if not M.config.git.enable then
return {} return {}
end end
@@ -238,42 +270,48 @@ function M.load_project_status(path)
return {} return {}
end end
local status = M._projects_by_toplevel[toplevel] local project = M._projects_by_toplevel[toplevel]
if status then if project then
return status return project
end end
local git_status = Runner.run({ local path_xys = GitRunner:run({
toplevel = toplevel, toplevel = toplevel,
list_untracked = git_utils.should_show_untracked(toplevel), list_untracked = git_utils.should_show_untracked(toplevel),
list_ignored = true, list_ignored = true,
timeout = M.config.git.timeout, timeout = M.config.git.timeout,
}) })
local watcher = nil local watcher = nil
if M.config.filesystem_watchers.enable then if M.config.filesystem_watchers.enable then
log.line("watcher", "git start") log.line("watcher", "git start")
---@param w Watcher
local callback = function(w) local callback = function(w)
log.line("watcher", "git event scheduled '%s'", w.toplevel) log.line("watcher", "git event scheduled '%s'", w.data.toplevel)
utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function() utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function()
if w.destroyed then if w.destroyed then
return return
end end
reload_tree_at(w.toplevel) reload_tree_at(w.data.toplevel)
end) end)
end end
local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" }) local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" })
watcher = Watcher:new(git_dir, WATCHED_FILES, callback, { watcher = Watcher:create({
toplevel = toplevel, path = git_dir,
files = WATCHED_FILES,
callback = callback,
data = {
toplevel = toplevel,
}
}) })
end end
if git_status then if path_xys then
M._projects_by_toplevel[toplevel] = { M._projects_by_toplevel[toplevel] = {
files = git_status, files = path_xys,
dirs = git_utils.file_status_to_dir_status(git_status, toplevel), dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel),
watcher = watcher, watcher = watcher,
} }
return M._projects_by_toplevel[toplevel] return M._projects_by_toplevel[toplevel]
@@ -283,6 +321,71 @@ function M.load_project_status(path)
end end
end end
---@param dir DirectoryNode
---@param project GitProject?
---@param root string?
function M.update_parent_projects(dir, project, root)
while project and dir do
-- step up to the containing project
if dir.absolute_path == root then
-- stop at the top of the tree
if not dir.parent then
break
end
root = M.get_toplevel(dir.parent.absolute_path)
-- stop when no more projects
if not root then
break
end
-- update the containing project
project = M.get_project(root)
M.reload_project(root, dir.absolute_path, nil)
end
-- update status
dir:update_git_status(dir.parent and dir.parent:is_git_ignored() or false, project)
-- maybe parent
dir = dir.parent
end
end
---Refresh contents and git status for a single directory
---@param dir DirectoryNode
function M.refresh_dir(dir)
local node = dir:get_parent_of_group() or dir
local toplevel = M.get_toplevel(dir.absolute_path)
M.reload_project(toplevel, dir.absolute_path, function()
local project = M.get_project(toplevel) or {}
dir.explorer:reload(node, project)
M.update_parent_projects(dir, project, toplevel)
dir.explorer.renderer:draw()
end)
end
---@param dir DirectoryNode?
---@param projects GitProject[]
function M.reload_node_status(dir, projects)
dir = dir and dir:as(DirectoryNode)
if not dir or #dir.nodes == 0 then
return
end
local toplevel = M.get_toplevel(dir.absolute_path)
local project = projects[toplevel] or {}
for _, node in ipairs(dir.nodes) do
node:update_git_status(dir:is_git_ignored(), project)
M.reload_node_status(node:as(DirectoryNode), projects)
end
end
function M.purge_state() function M.purge_state()
log.line("git", "purge_state") log.line("git", "purge_state")

View File

@@ -2,17 +2,51 @@ local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
---@class Runner local Class = require("nvim-tree.classic")
local Runner = {}
Runner.__index = Runner ---@class (exact) GitRunner: Class
---@field private toplevel string absolute path
---@field private path string? absolute path
---@field private list_untracked boolean
---@field private list_ignored boolean
---@field private timeout integer
---@field private callback fun(path_xy: GitPathXY)?
---@field private path_xy GitPathXY
---@field private rc integer? -- -1 indicates timeout
local GitRunner = Class:extend()
---@class GitRunner
---@overload fun(args: GitRunnerArgs): GitRunner
---@class (exact) GitRunnerArgs
---@field toplevel string absolute path
---@field path string? absolute path
---@field list_untracked boolean
---@field list_ignored boolean
---@field timeout integer
---@field callback fun(path_xy: GitPathXY)?
local timeouts = 0 local timeouts = 0
local MAX_TIMEOUTS = 5 local MAX_TIMEOUTS = 5
---@protected
---@param args GitRunnerArgs
function GitRunner:new(args)
self.toplevel = args.toplevel
self.path = args.path
self.list_untracked = args.list_untracked
self.list_ignored = args.list_ignored
self.timeout = args.timeout
self.callback = args.callback
self.path_xy = {}
self.rc = nil
end
---@private ---@private
---@param status string ---@param status string
---@param path string|nil ---@param path string|nil
function Runner:_parse_status_output(status, path) function GitRunner:parse_status_output(status, path)
if not path then if not path then
return return
end end
@@ -22,7 +56,7 @@ function Runner:_parse_status_output(status, path)
path = path:gsub("/", "\\") path = path:gsub("/", "\\")
end end
if #status > 0 and #path > 0 then if #status > 0 and #path > 0 then
self.output[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status self.path_xy[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status
end end
end end
@@ -30,7 +64,7 @@ end
---@param prev_output string ---@param prev_output string
---@param incoming string ---@param incoming string
---@return string ---@return string
function Runner:_handle_incoming_data(prev_output, incoming) function GitRunner:handle_incoming_data(prev_output, incoming)
if incoming and utils.str_find(incoming, "\n") then if incoming and utils.str_find(incoming, "\n") then
local prev = prev_output .. incoming local prev = prev_output .. incoming
local i = 1 local i = 1
@@ -45,7 +79,7 @@ function Runner:_handle_incoming_data(prev_output, incoming)
-- skip next line if it is a rename entry -- skip next line if it is a rename entry
skip_next_line = true skip_next_line = true
end end
self:_parse_status_output(status, path) self:parse_status_output(status, path)
end end
i = i + #line i = i + #line
end end
@@ -58,35 +92,38 @@ function Runner:_handle_incoming_data(prev_output, incoming)
end end
for line in prev_output:gmatch("[^\n]*\n") do for line in prev_output:gmatch("[^\n]*\n") do
self:_parse_status_output(line) self:parse_status_output(line)
end end
return "" return ""
end end
---@private
---@param stdout_handle uv.uv_pipe_t ---@param stdout_handle uv.uv_pipe_t
---@param stderr_handle uv.uv_pipe_t ---@param stderr_handle uv.uv_pipe_t
---@return table ---@return uv.spawn.options
function Runner:_getopts(stdout_handle, stderr_handle) function GitRunner:get_spawn_options(stdout_handle, stderr_handle)
local untracked = self.list_untracked and "-u" or nil local untracked = self.list_untracked and "-u" or nil
local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no"
return { return {
args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path }, args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path },
cwd = self.toplevel, cwd = self.toplevel,
stdio = { nil, stdout_handle, stderr_handle }, stdio = { nil, stdout_handle, stderr_handle },
} }
end end
---@private
---@param output string ---@param output string
function Runner:_log_raw_output(output) function GitRunner:log_raw_output(output)
if log.enabled("git") and output and type(output) == "string" then if log.enabled("git") and output and type(output) == "string" then
log.raw("git", "%s", output) log.raw("git", "%s", output)
log.line("git", "done") log.line("git", "done")
end end
end end
---@private
---@param callback function|nil ---@param callback function|nil
function Runner:_run_git_job(callback) function GitRunner:run_git_job(callback)
local handle, pid local handle, pid
local stdout = vim.loop.new_pipe(false) local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false) local stderr = vim.loop.new_pipe(false)
@@ -123,13 +160,13 @@ function Runner:_run_git_job(callback)
end end
end end
local opts = self:_getopts(stdout, stderr) local spawn_options = self:get_spawn_options(stdout, stderr)
log.line("git", "running job with timeout %dms", self.timeout) log.line("git", "running job with timeout %dms", self.timeout)
log.line("git", "git %s", table.concat(utils.array_remove_nils(opts.args), " ")) log.line("git", "git %s", table.concat(utils.array_remove_nils(spawn_options.args), " "))
handle, pid = vim.loop.spawn( handle, pid = vim.loop.spawn(
"git", "git",
opts, spawn_options,
vim.schedule_wrap(function(rc) vim.schedule_wrap(function(rc)
on_finish(rc) on_finish(rc)
end) end)
@@ -151,19 +188,20 @@ function Runner:_run_git_job(callback)
if data then if data then
data = data:gsub("%z", "\n") data = data:gsub("%z", "\n")
end end
self:_log_raw_output(data) self:log_raw_output(data)
output_leftover = self:_handle_incoming_data(output_leftover, data) output_leftover = self:handle_incoming_data(output_leftover, data)
end end
local function manage_stderr(_, data) local function manage_stderr(_, data)
self:_log_raw_output(data) self:log_raw_output(data)
end end
vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout)) vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout))
vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr)) vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr))
end end
function Runner:_wait() ---@private
function GitRunner:wait()
local function is_done() local function is_done()
return self.rc ~= nil return self.rc ~= nil
end end
@@ -172,64 +210,64 @@ function Runner:_wait()
end end
end end
---@param opts table ---@private
function Runner:_finalise(opts) function GitRunner:finalise()
if self.rc == -1 then if self.rc == -1 then
log.line("git", "job timed out %s %s", opts.toplevel, opts.path) log.line("git", "job timed out %s %s", self.toplevel, self.path)
timeouts = timeouts + 1 timeouts = timeouts + 1
if timeouts == MAX_TIMEOUTS then if timeouts == MAX_TIMEOUTS then
notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, opts.timeout)) notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts,
self.timeout))
require("nvim-tree.git").disable_git_integration() require("nvim-tree.git").disable_git_integration()
end end
elseif self.rc ~= 0 then elseif self.rc ~= 0 then
log.line("git", "job fail rc %d %s %s", self.rc, opts.toplevel, opts.path) log.line("git", "job fail rc %d %s %s", self.rc, self.toplevel, self.path)
else else
log.line("git", "job success %s %s", opts.toplevel, opts.path) log.line("git", "job success %s %s", self.toplevel, self.path)
end end
end end
--- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms ---Return nil when callback present
---@param opts table ---@private
---@param callback function|nil executed passing return when complete ---@return GitPathXY?
---@return table|nil status by absolute path, nil if callback present function GitRunner:execute()
function Runner.run(opts, callback) local async = self.callback ~= nil
local self = setmetatable({ local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.toplevel, self.path)
toplevel = opts.toplevel,
path = opts.path,
list_untracked = opts.list_untracked,
list_ignored = opts.list_ignored,
timeout = opts.timeout or 400,
output = {},
rc = nil, -- -1 indicates timeout
}, Runner)
local async = callback ~= nil if async and self.callback then
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path)
if async and callback then
-- async, always call back -- async, always call back
self:_run_git_job(function() self:run_git_job(function()
log.profile_end(profile) log.profile_end(profile)
self:_finalise(opts) self:finalise()
callback(self.output) self.callback(self.path_xy)
end) end)
else else
-- sync, maybe call back -- sync, maybe call back
self:_run_git_job() self:run_git_job()
self:_wait() self:wait()
log.profile_end(profile) log.profile_end(profile)
self:_finalise(opts) self:finalise()
if callback then if self.callback then
callback(self.output) self.callback(self.path_xy)
else else
return self.output return self.path_xy
end end
end end
end end
return Runner ---Static method to run a git process, which will be killed if it takes more than timeout
---Return nil when callback present
---@param args GitRunnerArgs
---@return GitPathXY?
function GitRunner:run(args)
local runner = GitRunner(args)
return runner:execute()
end
return GitRunner

View File

@@ -58,10 +58,11 @@ function M.get_toplevel(cwd)
return toplevel, git_dir return toplevel, git_dir
end end
---@type table<string, boolean>
local untracked = {} local untracked = {}
---@param cwd string ---@param cwd string
---@return string|nil ---@return boolean
function M.should_show_untracked(cwd) function M.should_show_untracked(cwd)
if untracked[cwd] ~= nil then if untracked[cwd] ~= nil then
return untracked[cwd] return untracked[cwd]
@@ -81,8 +82,8 @@ function M.should_show_untracked(cwd)
return untracked[cwd] return untracked[cwd]
end end
---@param t table|nil ---@param t table<string|integer, boolean>?
---@param k string ---@param k string|integer
---@return table ---@return table
local function nil_insert(t, k) local function nil_insert(t, k)
t = t or {} t = t or {}
@@ -90,31 +91,33 @@ local function nil_insert(t, k)
return t return t
end end
---@param status table ---@param project_files GitProjectFiles
---@param cwd string|nil ---@param cwd string|nil
---@return table ---@return GitProjectDirs
function M.file_status_to_dir_status(status, cwd) function M.project_files_to_project_dirs(project_files, cwd)
local direct = {} ---@type GitProjectDirs
for p, s in pairs(status) do local project_dirs = {}
project_dirs.direct = {}
for p, s in pairs(project_files) do
if s ~= "!!" then if s ~= "!!" then
local modified = vim.fn.fnamemodify(p, ":h") local modified = vim.fn.fnamemodify(p, ":h")
direct[modified] = nil_insert(direct[modified], s) project_dirs.direct[modified] = nil_insert(project_dirs.direct[modified], s)
end end
end end
local indirect = {} project_dirs.indirect = {}
for dirname, statuses in pairs(direct) do for dirname, statuses in pairs(project_dirs.direct) do
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
local modified = dirname local modified = dirname
while modified ~= cwd and modified ~= "/" do while modified ~= cwd and modified ~= "/" do
modified = vim.fn.fnamemodify(modified, ":h") modified = vim.fn.fnamemodify(modified, ":h")
indirect[modified] = nil_insert(indirect[modified], s) project_dirs.indirect[modified] = nil_insert(project_dirs.indirect[modified], s)
end end
end end
end end
local r = { indirect = indirect, direct = direct } for _, d in pairs(project_dirs) do
for _, d in pairs(r) do
for dirname, statuses in pairs(d) do for dirname, statuses in pairs(d) do
local new_statuses = {} local new_statuses = {}
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
@@ -123,7 +126,60 @@ function M.file_status_to_dir_status(status, cwd)
d[dirname] = new_statuses d[dirname] = new_statuses
end end
end end
return r
return project_dirs
end
---Git file status for an absolute path
---@param parent_ignored boolean
---@param project GitProject?
---@param path string
---@param path_fallback string? alternative file path when no other file status
---@return GitNodeStatus
function M.git_status_file(parent_ignored, project, path, path_fallback)
---@type GitNodeStatus
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project and project.files then
ns = {
file = project.files[path] or project.files[path_fallback]
}
else
ns = {}
end
return ns
end
---Git file and directory status for an absolute path
---@param parent_ignored boolean
---@param project GitProject?
---@param path string
---@param path_fallback string? alternative file path when no other file status
---@return GitNodeStatus?
function M.git_status_dir(parent_ignored, project, path, path_fallback)
---@type GitNodeStatus?
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project then
ns = {
file = project.files and (project.files[path] or project.files[path_fallback]),
dir = project.dirs and {
direct = project.dirs.direct and project.dirs.direct[path],
indirect = project.dirs.indirect and project.dirs.indirect[path],
},
}
end
return ns
end end
function M.setup(opts) function M.setup(opts)

View File

@@ -53,6 +53,7 @@ end
--- sort vim command lhs roughly as per :help index --- sort vim command lhs roughly as per :help index
---@param a string ---@param a string
---@param b string ---@param b string
---@return boolean
local function sort_lhs(a, b) local function sort_lhs(a, b)
-- mouse first -- mouse first
if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then

View File

@@ -1,7 +1,7 @@
local M = {} local M = {}
--- Apply mappings to a scratch buffer and return buffer local mappings --- Apply mappings to a scratch buffer and return buffer local mappings
---@param fn function(bufnr) on_attach or default_on_attach ---@param fn fun(bufnr: integer) on_attach or default_on_attach
---@return table as per vim.api.nvim_buf_get_keymap ---@return table as per vim.api.nvim_buf_get_keymap
local function generate_keymap(fn) local function generate_keymap(fn)
-- create an unlisted scratch buffer -- create an unlisted scratch buffer
@@ -29,14 +29,6 @@ function M.get_keymap_default()
return generate_keymap(M.default_on_attach) return generate_keymap(M.default_on_attach)
end end
function M.setup(opts)
if type(opts.on_attach) ~= "function" then
M.on_attach = M.default_on_attach
else
M.on_attach = opts.on_attach
end
end
---@param bufnr integer ---@param bufnr integer
function M.default_on_attach(bufnr) function M.default_on_attach(bufnr)
local api = require("nvim-tree.api") local api = require("nvim-tree.api")
@@ -51,68 +43,74 @@ function M.default_on_attach(bufnr)
} }
end end
-- formatting cannot be re-enabled, hence this is at the end
---@format disable
-- BEGIN_DEFAULT_ON_ATTACH -- BEGIN_DEFAULT_ON_ATTACH
vim.keymap.set('n', '<C-]>', api.tree.change_root_to_node, opts('CD')) vim.keymap.set("n", "<C-]>", api.tree.change_root_to_node, opts("CD"))
vim.keymap.set('n', '<C-e>', api.node.open.replace_tree_buffer, opts('Open: In Place')) vim.keymap.set("n", "<C-e>", api.node.open.replace_tree_buffer, opts("Open: In Place"))
vim.keymap.set('n', '<C-k>', api.node.show_info_popup, opts('Info')) vim.keymap.set("n", "<C-k>", api.node.show_info_popup, opts("Info"))
vim.keymap.set('n', '<C-r>', api.fs.rename_sub, opts('Rename: Omit Filename')) vim.keymap.set("n", "<C-r>", api.fs.rename_sub, opts("Rename: Omit Filename"))
vim.keymap.set('n', '<C-t>', api.node.open.tab, opts('Open: New Tab')) vim.keymap.set("n", "<C-t>", api.node.open.tab, opts("Open: New Tab"))
vim.keymap.set('n', '<C-v>', api.node.open.vertical, opts('Open: Vertical Split')) vim.keymap.set("n", "<C-v>", api.node.open.vertical, opts("Open: Vertical Split"))
vim.keymap.set('n', '<C-x>', api.node.open.horizontal, opts('Open: Horizontal Split')) vim.keymap.set("n", "<C-x>", api.node.open.horizontal, opts("Open: Horizontal Split"))
vim.keymap.set('n', '<BS>', api.node.navigate.parent_close, opts('Close Directory')) vim.keymap.set("n", "<BS>", api.node.navigate.parent_close, opts("Close Directory"))
vim.keymap.set('n', '<CR>', api.node.open.edit, opts('Open')) vim.keymap.set("n", "<CR>", api.node.open.edit, opts("Open"))
vim.keymap.set('n', '<Tab>', api.node.open.preview, opts('Open Preview')) vim.keymap.set("n", "<Tab>", api.node.open.preview, opts("Open Preview"))
vim.keymap.set('n', '>', api.node.navigate.sibling.next, opts('Next Sibling')) vim.keymap.set("n", ">", api.node.navigate.sibling.next, opts("Next Sibling"))
vim.keymap.set('n', '<', api.node.navigate.sibling.prev, opts('Previous Sibling')) vim.keymap.set("n", "<", api.node.navigate.sibling.prev, opts("Previous Sibling"))
vim.keymap.set('n', '.', api.node.run.cmd, opts('Run Command')) vim.keymap.set("n", ".", api.node.run.cmd, opts("Run Command"))
vim.keymap.set('n', '-', api.tree.change_root_to_parent, opts('Up')) vim.keymap.set("n", "-", api.tree.change_root_to_parent, opts("Up"))
vim.keymap.set('n', 'a', api.fs.create, opts('Create File Or Directory')) vim.keymap.set("n", "a", api.fs.create, opts("Create File Or Directory"))
vim.keymap.set('n', 'bd', api.marks.bulk.delete, opts('Delete Bookmarked')) vim.keymap.set("n", "bd", api.marks.bulk.delete, opts("Delete Bookmarked"))
vim.keymap.set('n', 'bt', api.marks.bulk.trash, opts('Trash Bookmarked')) vim.keymap.set("n", "bt", api.marks.bulk.trash, opts("Trash Bookmarked"))
vim.keymap.set('n', 'bmv', api.marks.bulk.move, opts('Move Bookmarked')) vim.keymap.set("n", "bmv", api.marks.bulk.move, opts("Move Bookmarked"))
vim.keymap.set('n', 'B', api.tree.toggle_no_buffer_filter, opts('Toggle Filter: No Buffer')) vim.keymap.set("n", "B", api.tree.toggle_no_buffer_filter, opts("Toggle Filter: No Buffer"))
vim.keymap.set('n', 'c', api.fs.copy.node, opts('Copy')) vim.keymap.set("n", "c", api.fs.copy.node, opts("Copy"))
vim.keymap.set('n', 'C', api.tree.toggle_git_clean_filter, opts('Toggle Filter: Git Clean')) vim.keymap.set("n", "C", api.tree.toggle_git_clean_filter, opts("Toggle Filter: Git Clean"))
vim.keymap.set('n', '[c', api.node.navigate.git.prev, opts('Prev Git')) vim.keymap.set("n", "[c", api.node.navigate.git.prev, opts("Prev Git"))
vim.keymap.set('n', ']c', api.node.navigate.git.next, opts('Next Git')) vim.keymap.set("n", "]c", api.node.navigate.git.next, opts("Next Git"))
vim.keymap.set('n', 'd', api.fs.remove, opts('Delete')) vim.keymap.set("n", "d", api.fs.remove, opts("Delete"))
vim.keymap.set('n', 'D', api.fs.trash, opts('Trash')) vim.keymap.set("n", "D", api.fs.trash, opts("Trash"))
vim.keymap.set('n', 'E', api.tree.expand_all, opts('Expand All')) vim.keymap.set("n", "E", api.tree.expand_all, opts("Expand All"))
vim.keymap.set('n', 'e', api.fs.rename_basename, opts('Rename: Basename')) 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.next, opts("Next Diagnostic"))
vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic')) vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic"))
vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear')) 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", "f", api.live_filter.start, opts("Live Filter: Start"))
vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help')) 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", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set('n', 'ge', api.fs.copy.basename, opts('Copy Basename')) vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename"))
vim.keymap.set('n', 'H', api.tree.toggle_hidden_filter, opts('Toggle Filter: Dotfiles')) 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", "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", "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", "K", api.node.navigate.sibling.first, opts("First Sibling"))
vim.keymap.set('n', 'L', api.node.open.toggle_group_empty, opts('Toggle Group Empty')) vim.keymap.set("n", "L", api.node.open.toggle_group_empty, opts("Toggle Group Empty"))
vim.keymap.set('n', 'M', api.tree.toggle_no_bookmark_filter, opts('Toggle Filter: No Bookmark')) 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", "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.edit, opts("Open"))
vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker')) vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
vim.keymap.set('n', 'p', api.fs.paste, opts('Paste')) vim.keymap.set("n", "p", api.fs.paste, opts("Paste"))
vim.keymap.set('n', 'P', api.node.navigate.parent, opts('Parent Directory')) vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory"))
vim.keymap.set('n', 'q', api.tree.close, opts('Close')) vim.keymap.set("n", "q", api.tree.close, opts("Close"))
vim.keymap.set('n', 'r', api.fs.rename, opts('Rename')) vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
vim.keymap.set('n', 'R', api.tree.reload, opts('Refresh')) vim.keymap.set("n", "R", api.tree.reload, opts("Refresh"))
vim.keymap.set('n', 's', api.node.run.system, opts('Run System')) vim.keymap.set("n", "s", api.node.run.system, opts("Run System"))
vim.keymap.set('n', 'S', api.tree.search_node, opts('Search')) vim.keymap.set("n", "S", api.tree.search_node, opts("Search"))
vim.keymap.set('n', 'u', api.fs.rename_full, opts('Rename: Full Path')) vim.keymap.set("n", "u", api.fs.rename_full, opts("Rename: Full Path"))
vim.keymap.set('n', 'U', api.tree.toggle_custom_filter, opts('Toggle Filter: Hidden')) vim.keymap.set("n", "U", api.tree.toggle_custom_filter, opts("Toggle Filter: Hidden"))
vim.keymap.set('n', 'W', api.tree.collapse_all, opts('Collapse')) vim.keymap.set("n", "W", api.tree.collapse_all, opts("Collapse"))
vim.keymap.set('n', 'x', api.fs.cut, opts('Cut')) vim.keymap.set("n", "x", api.fs.cut, opts("Cut"))
vim.keymap.set('n', 'y', api.fs.copy.filename, opts('Copy Name')) vim.keymap.set("n", "y", api.fs.copy.filename, opts("Copy Name"))
vim.keymap.set('n', 'Y', api.fs.copy.relative_path, opts('Copy Relative Path')) vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path"))
vim.keymap.set('n', '<2-LeftMouse>', api.node.open.edit, opts('Open')) vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open"))
vim.keymap.set('n', '<2-RightMouse>', api.tree.change_root_to_node, opts('CD')) vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD"))
-- END_DEFAULT_ON_ATTACH -- END_DEFAULT_ON_ATTACH
end end
function M.setup(opts)
if type(opts.on_attach) ~= "function" then
M.on_attach = M.default_on_attach
else
M.on_attach = opts.on_attach
end
end
return M return M

View File

@@ -6,12 +6,12 @@ local M = {}
-- silently move, please add to help nvim-tree-legacy-opts -- silently move, please add to help nvim-tree-legacy-opts
local function refactored(opts) local function refactored(opts)
-- 2022/06/20 -- 2022/06/20
utils.move_missing_val(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root", true) utils.move_missing_val(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root", true)
utils.move_missing_val(opts, "", "update_cwd", opts, "", "sync_root_with_cwd", true) utils.move_missing_val(opts, "", "update_cwd", opts, "", "sync_root_with_cwd", true)
-- 2022/11/07 -- 2022/11/07
utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "open", false) utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "open", false)
utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "close", true) utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "close", true)
utils.move_missing_val(opts, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true) utils.move_missing_val(opts, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true)
-- 2022/11/22 -- 2022/11/22

View File

@@ -1,9 +1,7 @@
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local explorer_node = require("nvim-tree.explorer.node")
---@class LibOpenOpts ---@class LibOpenOpts
---@field path string|nil path ---@field path string|nil path
@@ -14,173 +12,6 @@ local M = {
target_winid = nil, target_winid = nil,
} }
---Cursor position as per vim.api.nvim_win_get_cursor
---@return integer[]|nil
function M.get_cursor_position()
if not core.get_explorer() then
return
end
local winnr = view.get_winnr()
if not winnr or not vim.api.nvim_win_is_valid(winnr) then
return
end
return vim.api.nvim_win_get_cursor(winnr)
end
---@return Node|nil
function M.get_node_at_cursor()
local cursor = M.get_cursor_position()
if not cursor then
return
end
if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then
return { name = ".." }
end
return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[cursor[1]]
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param node Node|nil
---@return Node|nil cloned node
local function clone_node(node)
if not node then
node = core.get_explorer()
if not node then
return nil
end
end
local n = {
absolute_path = node.absolute_path,
executable = node.executable,
extension = node.extension,
git_status = node.git_status,
has_children = node.has_children,
hidden = node.hidden,
link_to = node.link_to,
name = node.name,
open = node.open,
type = node.type,
fs_stat = node.fs_stat,
}
if type(node.nodes) == "table" then
n.nodes = {}
for _, child in ipairs(node.nodes) do
table.insert(n.nodes, clone_node(child))
end
end
return n
end
---Api.tree.get_nodes
---@return Node[]|nil
function M.get_nodes()
return clone_node(core.get_explorer())
end
-- If node is grouped, return the last node in the group. Otherwise, return the given node.
---@param node Node
---@return Node
function M.get_last_group_node(node)
while node and node.group_next do
node = node.group_next
end
return node ---@diagnostic disable-line: return-type-mismatch -- it can't be nil
end
---Group empty folders
-- Recursively group nodes
---@param node Node
---@return Node[]
function M.group_empty_folders(node)
local is_root = not node.parent
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
if M.group_empty and not is_root and child_folder_only then
node.group_next = child_folder_only
local ns = M.group_empty_folders(child_folder_only)
node.nodes = ns or {}
return ns
end
return node.nodes
end
---Ungroup empty folders
-- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil
---@param node Node
function M.ungroup_empty_folders(node)
local cur = node
while cur and cur.group_next do
cur.nodes = { cur.group_next }
cur.group_next = nil
cur = cur.nodes[1]
end
end
---@param node Node
---@return Node[]
function M.get_all_nodes_in_group(node)
local next_node = utils.get_parent_of_group(node)
local nodes = {}
while next_node do
table.insert(nodes, next_node)
next_node = next_node.group_next
end
return nodes
end
-- Toggle group empty folders
---@param head_node Node
local function toggle_group_folders(head_node)
local is_grouped = head_node.group_next ~= nil
if is_grouped then
M.ungroup_empty_folders(head_node)
else
M.group_empty_folders(head_node)
end
end
---@param node Node
function M.expand_or_collapse(node, toggle_group)
local explorer = core.get_explorer()
toggle_group = toggle_group or false
if node.has_children then
node.has_children = false
end
if #node.nodes == 0 and explorer then
explorer:expand(node)
end
local head_node = utils.get_parent_of_group(node)
if toggle_group then
toggle_group_folders(head_node)
end
local open = M.get_last_group_node(node).open
local next_open
if toggle_group then
next_open = open
else
next_open = not open
end
for _, n in ipairs(M.get_all_nodes_in_group(head_node)) do
n.open = next_open
end
if explorer then
explorer.renderer:draw()
end
end
function M.set_target_win() function M.set_target_win()
local id = vim.api.nvim_get_current_win() local id = vim.api.nvim_get_current_win()
local tree_id = view.get_winnr() local tree_id = view.get_winnr()
@@ -234,7 +65,7 @@ end
---@param items_short string[] ---@param items_short string[]
---@param items_long string[] ---@param items_long string[]
---@param kind string|nil ---@param kind string|nil
---@param callback fun(item_short: string) ---@param callback fun(item_short: string|nil)
function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback) function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback)
local function format_item(short) local function format_item(short)
for i, s in ipairs(items_short) do for i, s in ipairs(items_short) do

View File

@@ -1,7 +1,12 @@
local M = { ---@alias LogTypes "all" | "config" | "copy_paste" | "dev" | "diagnostics" | "git" | "profile" | "watcher"
config = nil,
path = nil, ---@type table<LogTypes, boolean>
} local types = {}
---@type string
local file_path
local M = {}
--- Write to log file --- Write to log file
---@param typ string as per log.types config ---@param typ string as per log.types config
@@ -13,7 +18,26 @@ function M.raw(typ, fmt, ...)
end end
local line = string.format(fmt, ...) local line = string.format(fmt, ...)
local file = io.open(M.path, "a") local file = io.open(file_path, "a")
if file then
io.output(file)
io.write(line)
io.close(file)
end
end
--- Write to a new file
---@param typ LogTypes as per log.types config
---@param path string absolute path
---@param fmt string for string.format
---@param ... any arguments for string.format
function M.file(typ, path, fmt, ...)
if not M.enabled(typ) then
return
end
local line = string.format(fmt, ...)
local file = io.open(path, "w")
if file then if file then
io.output(file) io.output(file)
io.write(line) io.write(line)
@@ -52,7 +76,7 @@ end
--- Write to log file --- Write to log file
--- time and typ are prefixed and a trailing newline is added --- time and typ are prefixed and a trailing newline is added
---@param typ string as per log.types config ---@param typ LogTypes as per log.types config
---@param fmt string for string.format ---@param fmt string for string.format
---@param ... any arguments for string.format ---@param ... any arguments for string.format
function M.line(typ, fmt, ...) function M.line(typ, fmt, ...)
@@ -69,33 +93,31 @@ function M.set_inspect_opts(opts)
end end
--- Write to log file the inspection of a node --- Write to log file the inspection of a node
--- defaults to the node under cursor if none is provided ---@param typ LogTypes as per log.types config
---@param typ string as per log.types config ---@param node Node node to be inspected
---@param node table|nil node to be inspected
---@param fmt string for string.format ---@param fmt string for string.format
---@vararg any arguments for string.format ---@param ... any arguments for string.format
function M.node(typ, node, fmt, ...) function M.node(typ, node, fmt, ...)
if M.enabled(typ) then if M.enabled(typ) then
node = node or require("nvim-tree.lib").get_node_at_cursor()
M.raw(typ, string.format("[%s] [%s] %s\n%s\n", os.date("%Y-%m-%d %H:%M:%S"), typ, (fmt or "???"), vim.inspect(node, inspect_opts)), ...) M.raw(typ, string.format("[%s] [%s] %s\n%s\n", os.date("%Y-%m-%d %H:%M:%S"), typ, (fmt or "???"), vim.inspect(node, inspect_opts)), ...)
end end
end end
--- Logging is enabled for typ or all --- Logging is enabled for typ or all
---@param typ string as per log.types config ---@param typ LogTypes as per log.types config
---@return boolean ---@return boolean
function M.enabled(typ) function M.enabled(typ)
return M.path ~= nil and (M.config.types[typ] or M.config.types.all) return file_path ~= nil and (types[typ] or types.all)
end end
function M.setup(opts) function M.setup(opts)
M.config = opts.log if opts.log and opts.log.enable and opts.log.types then
if M.config and M.config.enable and M.config.types then types = opts.log.types
M.path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER) file_path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER)
if M.config.truncate then if opts.log.truncate then
os.remove(M.path) os.remove(file_path)
end end
require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. M.path) require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. file_path)
end end
end end

View File

@@ -8,35 +8,33 @@ local rename_file = require("nvim-tree.actions.fs.rename-file")
local trash = require("nvim-tree.actions.fs.trash") local trash = require("nvim-tree.actions.fs.trash")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
---@class Marks local Class = require("nvim-tree.classic")
---@field config table hydrated user opts.filters local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) Marks: Class
---@field private explorer Explorer ---@field private explorer Explorer
---@field private marks table<string, Node> by absolute path ---@field private marks table<string, Node> by absolute path
local Marks = {} local Marks = Class:extend()
---@return Marks ---@class Marks
---@param opts table user options ---@overload fun(args: MarksArgs): Marks
---@param explorer Explorer
function Marks:new(opts, explorer)
local o = {
explorer = explorer,
config = {
ui = opts.ui,
filesystem_watchers = opts.filesystem_watchers,
},
marks = {},
}
setmetatable(o, self) ---@class (exact) MarksArgs
self.__index = self ---@field explorer Explorer
return o
---@protected
---@param args MarksArgs
function Marks:new(args)
self.explorer = args.explorer
self.marks = {}
end end
---Clear all marks and reload if watchers disabled ---Clear all marks and reload if watchers disabled
---@private ---@private
function Marks:clear_reload() function Marks:clear_reload()
self:clear() self:clear()
if not self.config.filesystem_watchers.enable then if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer() self.explorer:reload_explorer()
end end
end end
@@ -98,7 +96,7 @@ function Marks:bulk_delete()
self:clear_reload() self:clear_reload()
end end
if self.config.ui.confirm.remove then if self.explorer.opts.ui.confirm.remove then
local prompt_select = "Remove bookmarked ?" local prompt_select = "Remove bookmarked ?"
local prompt_input = prompt_select .. " y/N: " local prompt_input = prompt_select .. " y/N: "
lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_delete", function(item_short) lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_delete", function(item_short)
@@ -127,7 +125,7 @@ function Marks:bulk_trash()
self:clear_reload() self:clear_reload()
end end
if self.config.ui.confirm.trash then if self.explorer.opts.ui.confirm.trash then
local prompt_select = "Trash bookmarked ?" local prompt_select = "Trash bookmarked ?"
local prompt_input = prompt_select .. " y/N: " local prompt_input = prompt_select .. " y/N: "
lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short) lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short)
@@ -149,10 +147,10 @@ function Marks:bulk_move()
return return
end end
local node_at_cursor = lib.get_node_at_cursor() local node_at_cursor = self.explorer:get_node_at_cursor()
local default_path = core.get_cwd() local default_path = core.get_cwd()
if node_at_cursor and node_at_cursor.type == "directory" then if node_at_cursor and node_at_cursor:is(DirectoryNode) then
default_path = node_at_cursor.absolute_path default_path = node_at_cursor.absolute_path
elseif node_at_cursor and node_at_cursor.parent then elseif node_at_cursor and node_at_cursor.parent then
default_path = node_at_cursor.parent.absolute_path default_path = node_at_cursor.parent.absolute_path
@@ -188,7 +186,7 @@ end
---@private ---@private
---@param up boolean ---@param up boolean
function Marks:navigate(up) function Marks:navigate(up)
local node = lib.get_node_at_cursor() local node = self.explorer:get_node_at_cursor()
if not node then if not node then
return return
end end
@@ -198,7 +196,8 @@ function Marks:navigate(up)
Iterator.builder(self.explorer.nodes) Iterator.builder(self.explorer.nodes)
:recursor(function(n) :recursor(function(n)
return n.open and n.nodes local dir = n:as(DirectoryNode)
return dir and dir.open and dir.nodes
end) end)
:applier(function(n) :applier(function(n)
if n.absolute_path == node.absolute_path then if n.absolute_path == node.absolute_path then
@@ -261,7 +260,7 @@ function Marks:navigate_select()
return return
end end
local node = self.marks[choice] local node = self.marks[choice]
if node and not node.nodes and not utils.get_win_buf_from_path(node.absolute_path) then if node and not node:is(DirectoryNode) and not utils.get_win_buf_from_path(node.absolute_path) then
open_file.fn("edit", node.absolute_path) open_file.fn("edit", node.absolute_path)
elseif node then elseif node then
utils.focus_file(node.absolute_path) utils.focus_file(node.absolute_path)

View File

@@ -1,36 +0,0 @@
---@meta
---@class ParentNode
---@field name string
---@class BaseNode
---@field absolute_path string
---@field executable boolean
---@field fs_stat uv.fs_stat.result|nil
---@field git_status GitStatus|nil
---@field hidden boolean
---@field is_dot boolean
---@field name string
---@field parent DirNode
---@field type string
---@field watcher function|nil
---@field diag_status DiagStatus|nil
---@class DirNode: BaseNode
---@field has_children boolean
---@field group_next Node|nil
---@field nodes Node[]
---@field open boolean
---@field hidden_stats table -- Each field of this table is a key for source and value for count
---@class FileNode: BaseNode
---@field extension string
---@class SymlinkDirNode: DirNode
---@field link_to string
---@class SymlinkFileNode: FileNode
---@field link_to string
---@alias SymlinkNode SymlinkDirNode|SymlinkFileNode
---@alias Node ParentNode|DirNode|FileNode|SymlinkNode|Explorer

View File

@@ -0,0 +1,87 @@
local git_utils = require("nvim-tree.git.utils")
local utils = require("nvim-tree.utils")
local DirectoryNode = require("nvim-tree.node.directory")
local LinkNode = require("nvim-tree.node.link")
---@class (exact) DirectoryLinkNode: DirectoryNode, LinkNode
local DirectoryLinkNode = DirectoryNode:extend()
DirectoryLinkNode:implement(LinkNode)
---@class DirectoryLinkNode
---@overload fun(args: LinkNodeArgs): DirectoryLinkNode
---@protected
---@param args LinkNodeArgs
function DirectoryLinkNode:new(args)
LinkNode.new(self, args)
-- create DirectoryNode with watcher on link_to
local absolute_path = args.absolute_path
args.absolute_path = args.link_to
DirectoryLinkNode.super.new(self, args)
self.type = "link"
-- reset absolute path to the link itself
self.absolute_path = absolute_path
end
function DirectoryLinkNode:destroy()
DirectoryNode.destroy(self)
end
---Update the directory git_status of link target and the file status of the link itself
---@param parent_ignored boolean
---@param project GitProject?
function DirectoryLinkNode:update_git_status(parent_ignored, project)
self.git_status = git_utils.git_status_dir(parent_ignored, project, self.link_to, self.absolute_path)
end
---@return HighlightedString name
function DirectoryLinkNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.folder then
return self:highlighted_icon_empty()
end
local str, hl
if self.open then
str = self.explorer.opts.renderer.icons.glyphs.folder.symlink_open
hl = "NvimTreeOpenedFolderIcon"
else
str = self.explorer.opts.renderer.icons.glyphs.folder.symlink
hl = "NvimTreeClosedFolderIcon"
end
return { str = str, hl = { hl } }
end
---Maybe override name with arrow
---@return HighlightedString name
function DirectoryLinkNode:highlighted_name()
local name = DirectoryNode.highlighted_name(self)
if self.explorer.opts.renderer.symlink_destination then
local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
name.str = string.format("%s%s%s", name.str, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
name.hl = { "NvimTreeSymlinkFolderName" }
end
return name
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.DirectoryLinkNode cloned
function DirectoryLinkNode:clone(api_nodes)
local clone = DirectoryNode.clone(self, api_nodes) --[[@as nvim_tree.api.DirectoryLinkNode]]
clone.link_to = self.link_to
clone.fs_stat_target = self.fs_stat_target
return clone
end
return DirectoryLinkNode

View File

@@ -0,0 +1,293 @@
local git_utils = require("nvim-tree.git.utils")
local icons = require("nvim-tree.renderer.components.devicons")
local notify = require("nvim-tree.notify")
local Node = require("nvim-tree.node")
---@class (exact) DirectoryNode: Node
---@field has_children boolean
---@field group_next DirectoryNode? -- If node is grouped, this points to the next child dir/link node
---@field nodes Node[]
---@field open boolean
---@field hidden_stats table? -- Each field of this table is a key for source and value for count
---@field private watcher Watcher?
local DirectoryNode = Node:extend()
---@class DirectoryNode
---@overload fun(args: NodeArgs): DirectoryNode
---@protected
---@param args NodeArgs
function DirectoryNode:new(args)
DirectoryNode.super.new(self, args)
local handle = vim.loop.fs_scandir(args.absolute_path)
local has_children = handle and vim.loop.fs_scandir_next(handle) ~= nil or false
self.type = "directory"
self.has_children = has_children
self.group_next = nil
self.nodes = {}
self.open = false
self.hidden_stats = nil
self.watcher = require("nvim-tree.explorer.watch").create_watcher(self)
end
function DirectoryNode:destroy()
if self.watcher then
self.watcher:destroy()
self.watcher = nil
end
if self.nodes then
for _, node in pairs(self.nodes) do
node:destroy()
end
end
Node.destroy(self)
end
---Update the git_status of the directory
---@param parent_ignored boolean
---@param project GitProject?
function DirectoryNode:update_git_status(parent_ignored, project)
self.git_status = git_utils.git_status_dir(parent_ignored, project, self.absolute_path, nil)
end
---@return GitXY[]?
function DirectoryNode:get_git_xy()
if not self.git_status or not self.explorer.opts.git.show_on_dirs then
return nil
end
local xys = {}
if not self:last_group_node().open or self.explorer.opts.git.show_on_open_dirs then
-- dir is closed or we should show on open_dirs
if self.git_status.file ~= nil then
table.insert(xys, self.git_status.file)
end
if self.git_status.dir ~= nil then
if self.git_status.dir.direct ~= nil then
for _, s in pairs(self.git_status.dir.direct) do
table.insert(xys, s)
end
end
if self.git_status.dir.indirect ~= nil then
for _, s in pairs(self.git_status.dir.indirect) do
table.insert(xys, s)
end
end
end
else
-- dir is open and we shouldn't show on open_dirs
if self.git_status.file ~= nil then
table.insert(xys, self.git_status.file)
end
if self.git_status.dir ~= nil and self.git_status.dir.direct ~= nil then
local deleted = {
[" D"] = true,
["D "] = true,
["RD"] = true,
["DD"] = true,
}
for _, s in pairs(self.git_status.dir.direct) do
if deleted[s] then
table.insert(xys, s)
end
end
end
end
if #xys == 0 then
return nil
else
return xys
end
end
-- If node is grouped, return the last node in the group. Otherwise, return the given node.
---@return DirectoryNode
function DirectoryNode:last_group_node()
return self.group_next and self.group_next:last_group_node() or self
end
---Return the one and only one child directory
---@return DirectoryNode?
function DirectoryNode:single_child_directory()
if #self.nodes == 1 then
return self.nodes[1]:as(DirectoryNode)
end
end
---@private
-- Toggle group empty folders
function DirectoryNode:toggle_group_folders()
local is_grouped = self.group_next ~= nil
if is_grouped then
self:ungroup_empty_folders()
else
self:group_empty_folders()
end
end
---Group empty folders
-- Recursively group nodes
---@private
---@return Node[]
function DirectoryNode:group_empty_folders()
local single_child = self:single_child_directory()
if self.explorer.opts.renderer.group_empty and self.parent and single_child then
self.group_next = single_child
local ns = single_child:group_empty_folders()
self.nodes = ns or {}
return ns
end
return self.nodes
end
---Ungroup empty folders
-- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil
---@private
function DirectoryNode:ungroup_empty_folders()
if self.group_next then
self.group_next:ungroup_empty_folders()
self.nodes = { self.group_next }
self.group_next = nil
end
end
---@param toggle_group boolean?
function DirectoryNode:expand_or_collapse(toggle_group)
toggle_group = toggle_group or false
if self.has_children then
self.has_children = false
end
if #self.nodes == 0 then
self.explorer:expand(self)
end
local head_node = self:get_parent_of_group() or self
if toggle_group then
head_node:toggle_group_folders()
end
local open = self:last_group_node().open
local next_open
if toggle_group then
next_open = open
else
next_open = not open
end
local node = head_node
while node do
node.open = next_open
node = node.group_next
end
self.explorer.renderer:draw()
end
---@return HighlightedString icon
function DirectoryNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.folder then
return self:highlighted_icon_empty()
end
local str, hl
-- devicon if enabled and available
if self.explorer.opts.renderer.icons.web_devicons.folder.enable then
str, hl = icons.get_icon(self.name)
if not self.explorer.opts.renderer.icons.web_devicons.folder.color then
hl = nil
end
end
-- default icon from opts
if not str then
if #self.nodes ~= 0 or self.has_children then
if self.open then
str = self.explorer.opts.renderer.icons.glyphs.folder.open
else
str = self.explorer.opts.renderer.icons.glyphs.folder.default
end
else
if self.open then
str = self.explorer.opts.renderer.icons.glyphs.folder.empty_open
else
str = self.explorer.opts.renderer.icons.glyphs.folder.empty
end
end
end
-- default hl
if not hl then
if self.open then
hl = "NvimTreeOpenedFolderIcon"
else
hl = "NvimTreeClosedFolderIcon"
end
end
return { str = str, hl = { hl } }
end
---@return HighlightedString icon
function DirectoryNode:highlighted_name()
local str, hl
local name = self.name
local next = self.group_next
while next do
name = string.format("%s/%s", name, next.name)
next = next.group_next
end
if self.group_next and type(self.explorer.opts.renderer.group_empty) == "function" then
local new_name = self.explorer.opts.renderer.group_empty(name)
if type(new_name) == "string" then
name = new_name
else
notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
end
end
str = string.format("%s%s", name, self.explorer.opts.renderer.add_trailing and "/" or "")
hl = "NvimTreeFolderName"
if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
hl = "NvimTreeSpecialFolderName"
elseif self.open then
hl = "NvimTreeOpenedFolderName"
elseif #self.nodes == 0 and not self.has_children then
hl = "NvimTreeEmptyFolderName"
end
return { str = str, hl = { hl } }
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.DirectoryNode cloned
function DirectoryNode:clone(api_nodes)
local clone = Node.clone(self, api_nodes) --[[@as nvim_tree.api.DirectoryNode]]
clone.has_children = self.has_children
clone.nodes = {}
clone.open = self.open
local clone_child
for _, child in ipairs(self.nodes) do
clone_child = child:clone(api_nodes)
clone_child.parent = clone
table.insert(clone.nodes, clone_child)
end
return clone
end
return DirectoryNode

View File

@@ -0,0 +1,48 @@
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local FileLinkNode = require("nvim-tree.node.file-link")
local FileNode = require("nvim-tree.node.file")
local Watcher = require("nvim-tree.watcher")
local M = {}
---Factory function to create the appropriate Node
---nil on invalid stat or invalid link target stat
---@param args NodeArgs
---@return Node?
function M.create(args)
if not args.fs_stat then
return nil
end
if args.fs_stat.type == "directory" then
-- directory must be readable and enumerable
if vim.loop.fs_access(args.absolute_path, "R") and Watcher.is_fs_event_capable(args.absolute_path) then
return DirectoryNode(args)
end
elseif args.fs_stat.type == "file" then
return FileNode(args)
elseif args.fs_stat.type == "link" then
-- link target path and stat must resolve
local link_to = vim.loop.fs_realpath(args.absolute_path)
local link_to_stat = link_to and vim.loop.fs_stat(link_to)
if not link_to or not link_to_stat then
return
end
---@cast args LinkNodeArgs
args.link_to = link_to
args.fs_stat_target = link_to_stat
-- choose directory or file
if link_to_stat.type == "directory" then
return DirectoryLinkNode(args)
else
return FileLinkNode(args)
end
end
return nil
end
return M

View File

@@ -0,0 +1,72 @@
local git_utils = require("nvim-tree.git.utils")
local utils = require("nvim-tree.utils")
local FileNode = require("nvim-tree.node.file")
local LinkNode = require("nvim-tree.node.link")
---@class (exact) FileLinkNode: FileNode, LinkNode
local FileLinkNode = FileNode:extend()
FileLinkNode:implement(LinkNode)
---@class FileLinkNode
---@overload fun(args: LinkNodeArgs): FileLinkNode
---@protected
---@param args LinkNodeArgs
function FileLinkNode:new(args)
LinkNode.new(self, args)
FileLinkNode.super.new(self, args)
self.type = "link"
end
function FileLinkNode:destroy()
FileNode.destroy(self)
end
---Update the git_status of the target otherwise the link itself
---@param parent_ignored boolean
---@param project GitProject?
function FileLinkNode:update_git_status(parent_ignored, project)
self.git_status = git_utils.git_status_file(parent_ignored, project, self.link_to, self.absolute_path)
end
---@return HighlightedString icon
function FileLinkNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.file then
return self:highlighted_icon_empty()
end
local str, hl
-- default icon from opts
str = self.explorer.opts.renderer.icons.glyphs.symlink
hl = "NvimTreeSymlinkIcon"
return { str = str, hl = { hl } }
end
---@return HighlightedString name
function FileLinkNode:highlighted_name()
local str = self.name
if self.explorer.opts.renderer.symlink_destination then
local link_to = utils.path_relative(self.link_to, self.explorer.absolute_path)
str = string.format("%s%s%s", str, self.explorer.opts.renderer.icons.symlink_arrow, link_to)
end
return { str = str, hl = { "NvimTreeSymlink" } }
end
---Create a sanitized partial copy of a node
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.FileLinkNode cloned
function FileLinkNode:clone(api_nodes)
local clone = FileNode.clone(self, api_nodes) --[[@as nvim_tree.api.FileLinkNode]]
clone.link_to = self.link_to
clone.fs_stat_target = self.fs_stat_target
return clone
end
return FileLinkNode

107
lua/nvim-tree/node/file.lua Normal file
View File

@@ -0,0 +1,107 @@
local git_utils = require("nvim-tree.git.utils")
local icons = require("nvim-tree.renderer.components.devicons")
local utils = require("nvim-tree.utils")
local Node = require("nvim-tree.node")
local PICTURE_MAP = {
jpg = true,
jpeg = true,
png = true,
gif = true,
webp = true,
jxl = true,
}
---@class (exact) FileNode: Node
---@field extension string
local FileNode = Node:extend()
---@class FileNode
---@overload fun(args: NodeArgs): FileNode
---@protected
---@param args NodeArgs
function FileNode:new(args)
FileNode.super.new(self, args)
self.type = "file"
self.extension = string.match(args.name, ".?[^.]+%.(.*)") or ""
self.executable = utils.is_executable(args.absolute_path)
end
function FileNode:destroy()
Node.destroy(self)
end
---Update the GitStatus of the file
---@param parent_ignored boolean
---@param project GitProject?
function FileNode:update_git_status(parent_ignored, project)
self.git_status = git_utils.git_status_file(parent_ignored, project, self.absolute_path, nil)
end
---@return GitXY[]?
function FileNode:get_git_xy()
if not self.git_status then
return nil
end
return self.git_status.file and { self.git_status.file }
end
---@return HighlightedString icon
function FileNode:highlighted_icon()
if not self.explorer.opts.renderer.icons.show.file then
return self:highlighted_icon_empty()
end
local str, hl
-- devicon if enabled and available, fallback to default
if self.explorer.opts.renderer.icons.web_devicons.file.enable then
str, hl = icons.get_icon(self.name, nil, { default = true })
if not self.explorer.opts.renderer.icons.web_devicons.file.color then
hl = nil
end
end
-- default icon from opts
if not str then
str = self.explorer.opts.renderer.icons.glyphs.default
end
-- default hl
if not hl then
hl = "NvimTreeFileIcon"
end
return { str = str, hl = { hl } }
end
---@return HighlightedString name
function FileNode:highlighted_name()
local hl
if vim.tbl_contains(self.explorer.opts.renderer.special_files, self.absolute_path) or vim.tbl_contains(self.explorer.opts.renderer.special_files, self.name) then
hl = "NvimTreeSpecialFile"
elseif self.executable then
hl = "NvimTreeExecFile"
elseif PICTURE_MAP[self.extension] then
hl = "NvimTreeImageFile"
end
return { str = self.name, hl = { hl } }
end
---Create a sanitized partial copy of a node
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.FileNode cloned
function FileNode:clone(api_nodes)
local clone = Node.clone(self, api_nodes) --[[@as nvim_tree.api.FileNode]]
clone.extension = self.extension
return clone
end
return FileNode

147
lua/nvim-tree/node/init.lua Normal file
View File

@@ -0,0 +1,147 @@
local Class = require("nvim-tree.classic")
---Abstract Node class.
---@class (exact) Node: Class
---@field uid_node number vim.loop.hrtime() at construction time
---@field type "file" | "directory" | "link" uv.fs_stat.result.type
---@field explorer Explorer
---@field absolute_path string
---@field executable boolean
---@field fs_stat uv.fs_stat.result?
---@field git_status GitNodeStatus?
---@field hidden boolean
---@field name string
---@field parent DirectoryNode?
---@field diag_status DiagStatus?
---@field private is_dot boolean cached is_dotfile
local Node = Class:extend()
---@class (exact) NodeArgs
---@field explorer Explorer
---@field parent DirectoryNode?
---@field absolute_path string
---@field name string
---@field fs_stat uv.fs_stat.result?
---@protected
---@param args NodeArgs
function Node:new(args)
self.uid_node = vim.loop.hrtime()
self.explorer = args.explorer
self.absolute_path = args.absolute_path
self.executable = false
self.fs_stat = args.fs_stat
self.git_status = nil
self.hidden = false
self.name = args.name
self.parent = args.parent
self.diag_status = nil
self.is_dot = false
end
function Node:destroy()
end
---Update the git_status of the node
---Abstract
---@param parent_ignored boolean
---@param project GitProject?
function Node:update_git_status(parent_ignored, project)
self:nop(parent_ignored, project)
end
---Short-format statuses
---@return GitXY[]?
function Node:get_git_xy()
end
---@return boolean
function Node:is_git_ignored()
return self.git_status ~= nil and self.git_status.file == "!!"
end
---Node or one of its parents begins with a dot
---@return boolean
function Node:is_dotfile()
if
self.is_dot
or (self.name and (self.name:sub(1, 1) == "."))
or (self.parent and self.parent:is_dotfile())
then
self.is_dot = true
return true
end
return false
end
---Get the highest parent of grouped nodes, nil when not grouped
---@return DirectoryNode?
function Node:get_parent_of_group()
if not self.parent or not self.parent.group_next then
return nil
end
local node = self.parent
while node do
if node.parent and node.parent.group_next then
node = node.parent
else
return node
end
end
end
---Empty highlighted icon
---@protected
---@return HighlightedString icon
function Node:highlighted_icon_empty()
return { str = "", hl = {} }
end
---Highlighted icon for the node
---Empty for base Node
---@return HighlightedString icon
function Node:highlighted_icon()
return self:highlighted_icon_empty()
end
---Empty highlighted name
---@protected
---@return HighlightedString name
function Node:highlighted_name_empty()
return { str = "", hl = {} }
end
---Highlighted name for the node
---Empty for base Node
---@return HighlightedString name
function Node:highlighted_name()
return self:highlighted_name_empty()
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.Node cloned
function Node:clone(api_nodes)
---@type nvim_tree.api.Node
local clone = {
uid_node = self.uid_node,
type = self.type,
absolute_path = self.absolute_path,
executable = self.executable,
fs_stat = self.fs_stat,
git_status = self.git_status,
hidden = self.hidden,
name = self.name,
parent = nil,
diag_severity = self.diag_status and self.diag_status.value or nil,
}
if api_nodes then
api_nodes[self.uid_node] = clone
end
return clone
end
return Node

View File

@@ -0,0 +1,19 @@
local Class = require("nvim-tree.classic")
---@class (exact) LinkNode: Class
---@field link_to string
---@field fs_stat_target uv.fs_stat.result
local LinkNode = Class:extend()
---@class (exact) LinkNodeArgs: NodeArgs
---@field link_to string
---@field fs_stat_target uv.fs_stat.result
---@protected
---@param args LinkNodeArgs
function LinkNode:new(args)
self.link_to = args.link_to
self.fs_stat_target = args.fs_stat_target
end
return LinkNode

View File

@@ -0,0 +1,34 @@
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) RootNode: DirectoryNode
local RootNode = DirectoryNode:extend()
---@class RootNode
---@overload fun(args: NodeArgs): RootNode
---@protected
---@param args NodeArgs
function RootNode:new(args)
RootNode.super.new(self, args)
end
---Root is never a dotfile
---@return boolean
function RootNode:is_dotfile()
return false
end
function RootNode:destroy()
DirectoryNode.destroy(self)
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node to populate
---@return nvim_tree.api.RootNode cloned
function RootNode:clone(api_nodes)
local clone = DirectoryNode.clone(self, api_nodes) --[[@as nvim_tree.api.RootNode]]
return clone
end
return RootNode

View File

@@ -2,31 +2,37 @@ local notify = require("nvim-tree.notify")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks") local Class = require("nvim-tree.classic")
local DecoratorCopied = require("nvim-tree.renderer.decorator.copied")
local DecoratorCut = require("nvim-tree.renderer.decorator.cut") local DirectoryNode = require("nvim-tree.node.directory")
local DecoratorDiagnostics = require("nvim-tree.renderer.decorator.diagnostics")
local DecoratorGit = require("nvim-tree.renderer.decorator.git") local BookmarkDecorator = require("nvim-tree.renderer.decorator.bookmarks")
local DecoratorModified = require("nvim-tree.renderer.decorator.modified") local CopiedDecorator = require("nvim-tree.renderer.decorator.copied")
local DecoratorHidden = require("nvim-tree.renderer.decorator.hidden") local CutDecorator = require("nvim-tree.renderer.decorator.cut")
local DecoratorOpened = require("nvim-tree.renderer.decorator.opened") local DiagnosticsDecorator = require("nvim-tree.renderer.decorator.diagnostics")
local GitDecorator = require("nvim-tree.renderer.decorator.git")
local HiddenDecorator = require("nvim-tree.renderer.decorator.hidden")
local ModifiedDecorator = require("nvim-tree.renderer.decorator.modified")
local OpenDecorator = require("nvim-tree.renderer.decorator.opened")
local UserDecorator = require("nvim-tree.renderer.decorator.user")
local pad = require("nvim-tree.renderer.components.padding") local pad = require("nvim-tree.renderer.components.padding")
local icons = require("nvim-tree.renderer.components.icons")
local PICTURE_MAP = { ---@alias HighlightedString nvim_tree.api.HighlightedString
jpg = true,
jpeg = true, -- Builtin Decorators
png = true, ---@type table<nvim_tree.api.decorator.Name, Decorator>
gif = true, local BUILTIN_DECORATORS = {
webp = true, Git = GitDecorator,
jxl = true, Open = OpenDecorator,
Hidden = HiddenDecorator,
Modified = ModifiedDecorator,
Bookmark = BookmarkDecorator,
Diagnostics = DiagnosticsDecorator,
Copied = CopiedDecorator,
Cut = CutDecorator,
} }
---@class (exact) HighlightedString
---@field str string
---@field hl string[]
---@class (exact) AddHighlightArgs ---@class (exact) AddHighlightArgs
---@field group string[] ---@field group string[]
---@field line number ---@field line number
@@ -34,7 +40,6 @@ local PICTURE_MAP = {
---@field col_end number ---@field col_end number
---@class (exact) Builder ---@class (exact) Builder
---@field private __index? table
---@field lines string[] includes icons etc. ---@field lines string[] includes icons etc.
---@field hl_args AddHighlightArgs[] line highlights ---@field hl_args AddHighlightArgs[] line highlights
---@field signs string[] line signs ---@field signs string[] line signs
@@ -47,43 +52,52 @@ local PICTURE_MAP = {
---@field private markers boolean[] indent markers ---@field private markers boolean[] indent markers
---@field private decorators Decorator[] ---@field private decorators Decorator[]
---@field private hidden_display fun(node: Node): string|nil ---@field private hidden_display fun(node: Node): string|nil
local Builder = {} ---@field private api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node for user decorators
local Builder = Class:extend()
---@param opts table user options ---@class Builder
---@param explorer Explorer ---@overload fun(args: BuilderArgs): Builder
---@return Builder
function Builder:new(opts, explorer)
---@type Builder
local o = {
opts = opts,
explorer = explorer,
index = 0,
depth = 0,
hl_args = {},
combined_groups = {},
lines = {},
markers = {},
signs = {},
extmarks = {},
virtual_lines = {},
decorators = {
-- priority order
DecoratorCut:new(opts, explorer),
DecoratorCopied:new(opts, explorer),
DecoratorDiagnostics:new(opts, explorer),
DecoratorBookmarks:new(opts, explorer),
DecoratorModified:new(opts, explorer),
DecoratorHidden:new(opts, explorer),
DecoratorOpened:new(opts, explorer),
DecoratorGit:new(opts, explorer),
},
hidden_display = Builder:setup_hidden_display_function(opts),
}
setmetatable(o, self) ---@class (exact) BuilderArgs
self.__index = self ---@field explorer Explorer
return o ---@protected
---@param args BuilderArgs
function Builder:new(args)
self.explorer = args.explorer
self.index = 0
self.depth = 0
self.hl_args = {}
self.combined_groups = {}
self.lines = {}
self.markers = {}
self.signs = {}
self.extmarks = {}
self.virtual_lines = {}
self.decorators = {}
self.hidden_display = Builder:setup_hidden_display_function(self.explorer.opts)
-- instantiate all the builtin and user decorator instances
local builtin, user
for _, d in ipairs(self.explorer.opts.renderer.decorators) do
---@type Decorator
builtin = BUILTIN_DECORATORS[d]
---@type UserDecorator
user = type(d) == "table" and type(d.as) == "function" and d:as(UserDecorator)
if builtin then
table.insert(self.decorators, builtin({ explorer = self.explorer }))
elseif user then
table.insert(self.decorators, user())
-- clone user nodes once
if not self.api_nodes then
self.api_nodes = {}
self.explorer:clone(self.api_nodes)
end
end
end
end end
---Insert ranged highlight groups into self.highlights ---Insert ranged highlight groups into self.highlights
@@ -95,27 +109,6 @@ function Builder:insert_highlight(groups, start, end_)
table.insert(self.hl_args, { groups, self.index, start, end_ or -1 }) table.insert(self.hl_args, { groups, self.index, start, end_ or -1 })
end end
---@private
function Builder:get_folder_name(node)
local name = node.name
local next = node.group_next
while next do
name = string.format("%s/%s", name, next.name)
next = next.group_next
end
if node.group_next and type(self.opts.renderer.group_empty) == "function" then
local new_name = self.opts.renderer.group_empty(name)
if type(new_name) == "string" then
name = new_name
else
notify.warn(string.format("Invalid return type for field renderer.group_empty. Expected string, got %s", type(new_name)))
end
end
return string.format("%s%s", name, self.opts.renderer.add_trailing and "/" or "")
end
---@private ---@private
---@param highlighted_strings HighlightedString[] ---@param highlighted_strings HighlightedString[]
---@return string ---@return string
@@ -136,78 +129,6 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
return string return string
end end
---@private
---@param node table
---@return HighlightedString icon
---@return HighlightedString name
function Builder:build_folder(node)
local has_children = #node.nodes ~= 0 or node.has_children
local icon, icon_hl = icons.get_folder_icon(node, has_children)
local foldername = self:get_folder_name(node)
if #icon > 0 and icon_hl == nil then
if node.open then
icon_hl = "NvimTreeOpenedFolderIcon"
else
icon_hl = "NvimTreeClosedFolderIcon"
end
end
local foldername_hl = "NvimTreeFolderName"
if node.link_to and self.opts.renderer.symlink_destination then
local arrow = icons.i.symlink_arrow
local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path)
foldername = string.format("%s%s%s", foldername, arrow, link_to)
foldername_hl = "NvimTreeSymlinkFolderName"
elseif
vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name)
then
foldername_hl = "NvimTreeSpecialFolderName"
elseif node.open then
foldername_hl = "NvimTreeOpenedFolderName"
elseif not has_children then
foldername_hl = "NvimTreeEmptyFolderName"
end
return { str = icon, hl = { icon_hl } }, { str = foldername, hl = { foldername_hl } }
end
---@private
---@param node table
---@return HighlightedString icon
---@return HighlightedString name
function Builder:build_symlink(node)
local icon = icons.i.symlink
local arrow = icons.i.symlink_arrow
local symlink_formatted = node.name
if self.opts.renderer.symlink_destination then
local link_to = utils.path_relative(node.link_to, self.explorer.absolute_path)
symlink_formatted = string.format("%s%s%s", symlink_formatted, arrow, link_to)
end
return { str = icon, hl = { "NvimTreeSymlinkIcon" } }, { str = symlink_formatted, hl = { "NvimTreeSymlink" } }
end
---@private
---@param node table
---@return HighlightedString icon
---@return HighlightedString name
function Builder:build_file(node)
local hl
if
vim.tbl_contains(self.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(self.opts.renderer.special_files, node.name)
then
hl = "NvimTreeSpecialFile"
elseif node.executable then
hl = "NvimTreeExecFile"
elseif PICTURE_MAP[node.extension] then
hl = "NvimTreeImageFile"
end
local icon, hl_group = icons.get_file_icon(node.name, node.extension)
return { str = icon, hl = { hl_group } }, { str = node.name, hl = { hl } }
end
---@private ---@private
---@param indent_markers HighlightedString[] ---@param indent_markers HighlightedString[]
---@param arrows HighlightedString[]|nil ---@param arrows HighlightedString[]|nil
@@ -223,7 +144,7 @@ function Builder:format_line(indent_markers, arrows, icon, name, node)
end end
for _, v in ipairs(t2) do for _, v in ipairs(t2) do
if added_len > 0 then if added_len > 0 then
table.insert(t1, { str = self.opts.renderer.icons.padding }) table.insert(t1, { str = self.explorer.opts.renderer.icons.padding })
end end
table.insert(t1, v) table.insert(t1, v)
end end
@@ -236,22 +157,25 @@ function Builder:format_line(indent_markers, arrows, icon, name, node)
end end
end end
-- use the api node for user decorators
local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]]
local line = { indent_markers, arrows } local line = { indent_markers, arrows }
add_to_end(line, { icon }) add_to_end(line, { icon })
for i = #self.decorators, 1, -1 do for _, d in ipairs(self.decorators) do
add_to_end(line, self.decorators[i]:icons_before(node)) add_to_end(line, d:icons_before(not d:is(UserDecorator) and node or api_node))
end end
add_to_end(line, { name }) add_to_end(line, { name })
for i = #self.decorators, 1, -1 do for _, d in ipairs(self.decorators) do
add_to_end(line, self.decorators[i]:icons_after(node)) add_to_end(line, d:icons_after(not d:is(UserDecorator) and node or api_node))
end end
local rights = {} local rights = {}
for i = #self.decorators, 1, -1 do for _, d in ipairs(self.decorators) do
add_to_end(rights, self.decorators[i]:icons_right_align(node)) add_to_end(rights, d:icons_right_align(not d:is(UserDecorator) and node or api_node))
end end
if #rights > 0 then if #rights > 0 then
self.extmarks[self.index] = rights self.extmarks[self.index] = rights
@@ -263,10 +187,14 @@ end
---@private ---@private
---@param node Node ---@param node Node
function Builder:build_signs(node) function Builder:build_signs(node)
-- use the api node for user decorators
local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]]
-- first in priority order -- first in priority order
local sign_name local d, sign_name
for _, d in ipairs(self.decorators) do for i = #self.decorators, 1, -1 do
sign_name = d:sign_name(node) d = self.decorators[i]
sign_name = d:sign_name(not d:is(UserDecorator) and node or api_node)
if sign_name then if sign_name then
self.signs[self.index] = sign_name self.signs[self.index] = sign_name
break break
@@ -302,78 +230,78 @@ function Builder:create_combined_group(groups)
return combined_name return combined_name
end end
---Calculate highlight group for icon and name. A combined highlight group will be created ---Calculate decorated icon and name for a node.
---when there is more than one highlight. ---A combined highlight group will be created when there is more than one highlight.
---A highlight group is always calculated and upserted for the case of highlights changing. ---A highlight group is always calculated and upserted for the case of highlights changing.
---@private ---@private
---@param node Node ---@param node Node
---@return string|nil icon_hl_group ---@return HighlightedString icon
---@return string|nil name_hl_group ---@return HighlightedString name
function Builder:add_highlights(node) function Builder:icon_name_decorated(node)
-- result -- use the api node for user decorators
local icon_hl_group, name_hl_group local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]]
-- calculate all groups -- base case
local icon = node:highlighted_icon()
local name = node:highlighted_name()
-- calculate node icon and all decorated highlight groups
local icon_groups = {} local icon_groups = {}
local name_groups = {} local name_groups = {}
local d, icon, name local hl_icon, hl_name
for i = #self.decorators, 1, -1 do for _, d in ipairs(self.decorators) do
d = self.decorators[i] -- maybe overridde icon
icon, name = d:groups_icon_name(node) icon = d:icon_node((not d:is(UserDecorator) and node or api_node)) or icon
table.insert(icon_groups, icon)
table.insert(name_groups, name) hl_icon, hl_name = d:highlight_group_icon_name((not d:is(UserDecorator) and node or api_node))
table.insert(icon_groups, hl_icon)
table.insert(name_groups, hl_name)
end end
-- one or many icon groups -- add one or many icon groups
if #icon_groups > 1 then if #icon_groups > 1 then
icon_hl_group = self:create_combined_group(icon_groups) table.insert(icon.hl, self:create_combined_group(icon_groups))
else else
icon_hl_group = icon_groups[1] table.insert(icon.hl, icon_groups[1])
end end
-- one or many name groups -- add one or many name groups
if #name_groups > 1 then if #name_groups > 1 then
name_hl_group = self:create_combined_group(name_groups) table.insert(name.hl, self:create_combined_group(name_groups))
else else
name_hl_group = name_groups[1] table.insert(name.hl, name_groups[1])
end end
return icon_hl_group, name_hl_group return icon, name
end end
---Insert node line into self.lines, calling Builder:build_lines for each directory
---@private ---@private
---@param node Node
---@param idx integer line number starting at 1
---@param num_children integer of node
function Builder:build_line(node, idx, num_children) function Builder:build_line(node, idx, num_children)
-- various components -- various components
local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers) local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
local arrows = pad.get_arrows(node) local arrows = pad.get_arrows(node)
-- main components -- decorated node icon and name
local is_folder = node.nodes ~= nil local icon, name = self:icon_name_decorated(node)
local is_symlink = node.link_to ~= nil
local icon, name
if is_folder then
icon, name = self:build_folder(node)
elseif is_symlink then
icon, name = self:build_symlink(node)
else
icon, name = self:build_file(node)
end
-- highighting
local icon_hl_group, name_hl_group = self:add_highlights(node)
table.insert(icon.hl, icon_hl_group)
table.insert(name.hl, name_hl_group)
local line = self:format_line(indent_markers, arrows, icon, name, node) local line = self:format_line(indent_markers, arrows, icon, name, node)
table.insert(self.lines, self:unwrap_highlighted_strings(line)) table.insert(self.lines, self:unwrap_highlighted_strings(line))
self.index = self.index + 1 self.index = self.index + 1
node = require("nvim-tree.lib").get_last_group_node(node) local dir = node:as(DirectoryNode)
if node.open then if dir then
self.depth = self.depth + 1 dir = dir:last_group_node()
self:build_lines(node) if dir.open then
self.depth = self.depth - 1 self.depth = self.depth + 1
self:build_lines(dir)
self.depth = self.depth - 1
end
end end
end end
@@ -386,7 +314,7 @@ function Builder:add_hidden_count_string(node, idx, num_children)
local hidden_count_string = self.hidden_display(node.hidden_stats) local hidden_count_string = self.hidden_display(node.hidden_stats)
if hidden_count_string and hidden_count_string ~= "" then if hidden_count_string and hidden_count_string ~= "" then
local indent_markers = pad.get_indent_markers(self.depth, idx or 0, num_children or 0, node, self.markers, 1) local indent_markers = pad.get_indent_markers(self.depth, idx or 0, num_children or 0, node, self.markers, 1)
local indent_width = self.opts.renderer.indent_width local indent_width = self.explorer.opts.renderer.indent_width
local indent_padding = string.rep(" ", indent_width) local indent_padding = string.rep(" ", indent_width)
local indent_string = indent_padding .. indent_markers.str local indent_string = indent_padding .. indent_markers.str
@@ -403,8 +331,11 @@ function Builder:add_hidden_count_string(node, idx, num_children)
end end
end end
---Number of visible nodes
---@private ---@private
function Builder:get_nodes_number(nodes) ---@param nodes Node[]
---@return integer
function Builder:num_visible(nodes)
if not self.explorer.live_filter.filter then if not self.explorer.live_filter.filter then
return #nodes return #nodes
end end
@@ -423,7 +354,7 @@ function Builder:build_lines(node)
if not node then if not node then
node = self.explorer node = self.explorer
end end
local num_children = self:get_nodes_number(node.nodes) local num_children = self:num_visible(node.nodes)
local idx = 1 local idx = 1
for _, n in ipairs(node.nodes) do for _, n in ipairs(node.nodes) do
if not n.hidden then if not n.hidden then
@@ -453,18 +384,18 @@ end
---@private ---@private
function Builder:build_header() function Builder:build_header()
if view.is_root_folder_visible(self.explorer.absolute_path) then if view.is_root_folder_visible(self.explorer.absolute_path) then
local root_name = self:format_root_name(self.opts.renderer.root_folder_label) local root_name = self:format_root_name(self.explorer.opts.renderer.root_folder_label)
table.insert(self.lines, root_name) table.insert(self.lines, root_name)
self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(root_name)) self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(root_name))
self.index = 1 self.index = 1
end end
if self.explorer.live_filter.filter then if self.explorer.live_filter.filter then
local filter_line = string.format("%s/%s/", self.opts.live_filter.prefix, self.explorer.live_filter.filter) local filter_line = string.format("%s/%s/", self.explorer.opts.live_filter.prefix, self.explorer.live_filter.filter)
table.insert(self.lines, filter_line) table.insert(self.lines, filter_line)
local prefix_length = string.len(self.opts.live_filter.prefix) local prefix_length = string.len(self.explorer.opts.live_filter.prefix)
self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length) self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length)
self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line)) self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line))
self.index = self.index + 1 self.index = self.index + 1
end end
end end
@@ -487,7 +418,7 @@ function Builder:build()
return self return self
end end
---TODO refactor back to function; this was left here to reduce PR noise ---@private
---@param opts table ---@param opts table
---@return fun(node: Node): string|nil ---@return fun(node: Node): string|nil
function Builder:setup_hidden_display_function(opts) function Builder:setup_hidden_display_function(opts)
@@ -512,11 +443,11 @@ function Builder:setup_hidden_display_function(opts)
-- In case of missing field such as live_filter we zero it, otherwise keep field as is -- In case of missing field such as live_filter we zero it, otherwise keep field as is
hidden_stats = vim.tbl_deep_extend("force", { hidden_stats = vim.tbl_deep_extend("force", {
live_filter = 0, live_filter = 0,
git = 0, git = 0,
buf = 0, buf = 0,
dotfile = 0, dotfile = 0,
custom = 0, custom = 0,
bookmark = 0, bookmark = 0,
}, hidden_stats or {}) }, hidden_stats or {})
local ok, result = pcall(hidden_display, hidden_stats) local ok, result = pcall(hidden_display, hidden_stats)

View File

@@ -0,0 +1,35 @@
---@alias devicons_get_icon fun(name: string, ext: string?, opts: table?): string?, string?
---@alias devicons_setup fun(opts: table?)
---@class (strict) DevIcons?
---@field setup devicons_setup
---@field get_icon devicons_get_icon
local devicons
local M = {}
---Wrapper around nvim-web-devicons, nils if devicons not available
---@type devicons_get_icon
function M.get_icon(name, ext, opts)
if devicons then
return devicons.get_icon(name, ext, opts)
else
return nil, nil
end
end
---Attempt to use nvim-web-devicons if present and enabled for file or folder
---@param opts table
function M.setup(opts)
if opts.renderer.icons.show.file or opts.renderer.icons.show.folder then
local ok, di = pcall(require, "nvim-web-devicons")
if ok then
devicons = di --[[@as DevIcons]]
-- does nothing if already called i.e. doesn't clobber previous user setup
devicons.setup()
end
end
end
return M

View File

@@ -1,93 +0,0 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local diagnostics = require("nvim-tree.diagnostics")
local M = {
-- highlight strings for the icons
HS_ICON = {},
-- highlight groups for HL
HG_FILE = {},
HG_FOLDER = {},
-- position for HL
HL_POS = HL_POSITION.none,
}
---Diagnostics highlight group and position when highlight_diagnostics.
---@param node table
---@return HL_POSITION position none when no status
---@return string|nil group only when status
function M.get_highlight(node)
if not node or M.HL_POS == HL_POSITION.none then
return HL_POSITION.none, nil
end
local group
local diag_status = diagnostics.get_diag_status(node)
if node.nodes then
group = M.HS_FOLDER[diag_status and diag_status.value]
else
group = M.HS_FILE[diag_status and diag_status.value]
end
if group then
return M.HL_POS, group
else
return HL_POSITION.none, nil
end
end
---diagnostics icon if there is a status
---@param node table
---@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
local diag_status = diagnostics.get_diag_status(node)
return M.ICON[diag_status and diag_status.value]
end
end
function M.setup(opts)
M.config = {
diagnostics = opts.diagnostics,
renderer = opts.renderer,
}
if opts.diagnostics.enable and opts.renderer.highlight_diagnostics then
M.HL_POS = HL_POSITION[opts.renderer.highlight_diagnostics]
end
M.HG_FILE[vim.diagnostic.severity.ERROR] = "NvimTreeDiagnosticErrorFileHL"
M.HG_FILE[vim.diagnostic.severity.WARN] = "NvimTreeDiagnosticWarningFileHL"
M.HG_FILE[vim.diagnostic.severity.INFO] = "NvimTreeDiagnosticInfoFileHL"
M.HG_FILE[vim.diagnostic.severity.HINT] = "NvimTreeDiagnosticHintFileHL"
M.HG_FOLDER[vim.diagnostic.severity.ERROR] = "NvimTreeDiagnosticErrorFolderHL"
M.HG_FOLDER[vim.diagnostic.severity.WARN] = "NvimTreeDiagnosticWarningFolderHL"
M.HG_FOLDER[vim.diagnostic.severity.INFO] = "NvimTreeDiagnosticInfoFolderHL"
M.HG_FOLDER[vim.diagnostic.severity.HINT] = "NvimTreeDiagnosticHintFolderHL"
M.HS_ICON[vim.diagnostic.severity.ERROR] = {
str = M.config.diagnostics.icons.error,
hl = { "NvimTreeDiagnosticErrorIcon" },
}
M.HS_ICON[vim.diagnostic.severity.WARN] = {
str = M.config.diagnostics.icons.warning,
hl = { "NvimTreeDiagnosticWarningIcon" },
}
M.HS_ICON[vim.diagnostic.severity.INFO] = {
str = M.config.diagnostics.icons.info,
hl = { "NvimTreeDiagnosticInfoIcon" },
}
M.HS_ICON[vim.diagnostic.severity.HINT] = {
str = M.config.diagnostics.icons.hint,
hl = { "NvimTreeDiagnosticHintIcon" },
}
for _, i in ipairs(M.HS_ICON) do
vim.fn.sign_define(i.hl[1], { text = i.str, texthl = i.hl[1] })
end
end
return M

View File

@@ -57,13 +57,13 @@ local function show()
end end
M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, { M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, {
relative = "win", relative = "win",
row = 0, row = 0,
bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 }, bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 },
width = math.min(text_width, vim.o.columns - 2), width = math.min(text_width, vim.o.columns - 2),
height = 1, height = 1,
noautocmd = true, noautocmd = true,
style = "minimal", style = "minimal",
}) })
local ns_id = vim.api.nvim_get_namespaces()["NvimTreeHighlights"] local ns_id = vim.api.nvim_get_namespaces()["NvimTreeHighlights"]

View File

@@ -1,113 +0,0 @@
local M = { i = {} }
local function config_symlinks()
M.i.symlink = #M.config.glyphs.symlink > 0 and M.config.glyphs.symlink or ""
M.i.symlink_arrow = M.config.symlink_arrow
end
local function empty()
return ""
end
local function get_folder_icon_default(node, has_children)
local is_symlink = node.links_to ~= nil
local n
if is_symlink and node.open then
n = M.config.glyphs.folder.symlink_open
elseif is_symlink then
n = M.config.glyphs.folder.symlink
elseif node.open then
if has_children then
n = M.config.glyphs.folder.open
else
n = M.config.glyphs.folder.empty_open
end
else
if has_children then
n = M.config.glyphs.folder.default
else
n = M.config.glyphs.folder.empty
end
end
return n, nil
end
local function get_folder_icon_webdev(node, has_children)
local icon, hl_group = M.devicons.get_icon(node.name, node.extension)
if not M.config.web_devicons.folder.color then
hl_group = nil
end
if icon ~= nil then
return icon, hl_group
else
return get_folder_icon_default(node, has_children)
end
end
local function get_file_icon_default()
local hl_group = "NvimTreeFileIcon"
local icon = M.config.glyphs.default
if #icon > 0 then
return icon, hl_group
else
return ""
end
end
local function get_file_icon_webdev(fname, extension)
local icon, hl_group = M.devicons.get_icon(fname, extension)
if not M.config.web_devicons.file.color then
hl_group = "NvimTreeFileIcon"
end
if icon and hl_group ~= "DevIconDefault" then
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, "%.(.*)"))
else
local devicons_default = M.devicons.get_default_icon()
if devicons_default and type(devicons_default.icon) == "string" and type(devicons_default.name) == "string" then
return devicons_default.icon, "DevIcon" .. devicons_default.name
else
return get_file_icon_default()
end
end
end
local function config_file_icon()
if M.config.show.file then
if M.devicons and M.config.web_devicons.file.enable then
M.get_file_icon = get_file_icon_webdev
else
M.get_file_icon = get_file_icon_default
end
else
M.get_file_icon = empty
end
end
local function config_folder_icon()
if M.config.show.folder then
if M.devicons and M.config.web_devicons.folder.enable then
M.get_folder_icon = get_folder_icon_webdev
else
M.get_folder_icon = get_folder_icon_default
end
else
M.get_folder_icon = empty
end
end
function M.reset_config()
config_symlinks()
config_file_icon()
config_folder_icon()
end
function M.setup(opts)
M.config = opts.renderer.icons
M.devicons = pcall(require, "nvim-web-devicons") and require("nvim-web-devicons") or nil
end
return M

View File

@@ -1,14 +1,12 @@
local M = {} local M = {}
M.diagnostics = require("nvim-tree.renderer.components.diagnostics")
M.full_name = require("nvim-tree.renderer.components.full-name") M.full_name = require("nvim-tree.renderer.components.full-name")
M.icons = require("nvim-tree.renderer.components.icons") M.devicons = require("nvim-tree.renderer.components.devicons")
M.padding = require("nvim-tree.renderer.components.padding") M.padding = require("nvim-tree.renderer.components.padding")
function M.setup(opts) function M.setup(opts)
M.diagnostics.setup(opts)
M.full_name.setup(opts) M.full_name.setup(opts)
M.icons.setup(opts) M.devicons.setup(opts)
M.padding.setup(opts) M.padding.setup(opts)
end end

View File

@@ -1,3 +1,5 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
local function check_siblings_for_folder(node, with_arrows) local function check_siblings_for_folder(node, with_arrows)
@@ -59,9 +61,10 @@ end
---@param depth integer ---@param depth integer
---@param idx integer ---@param idx integer
---@param nodes_number integer ---@param nodes_number integer
---@param node table ---@param node Node
---@param markers table ---@param markers table
---@return HighlightedString[] ---@param early_stop integer?
---@return HighlightedString
function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop)
local str = "" local str = ""
@@ -79,7 +82,7 @@ function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_sto
return { str = str, hl = { "NvimTreeIndentMarker" } } return { str = str, hl = { "NvimTreeIndentMarker" } }
end end
---@param node table ---@param node Node
---@return HighlightedString[]|nil ---@return HighlightedString[]|nil
function M.get_arrows(node) function M.get_arrows(node)
if not M.config.icons.show.folder_arrow then if not M.config.icons.show.folder_arrow then
@@ -89,8 +92,9 @@ function M.get_arrows(node)
local str local str
local hl = "NvimTreeFolderArrowClosed" local hl = "NvimTreeFolderArrowClosed"
if node.nodes then local dir = node:as(DirectoryNode)
if node.open then if dir then
if dir.open then
str = M.config.icons.glyphs.folder["arrow_open"] .. " " str = M.config.icons.glyphs.folder["arrow_open"] .. " "
hl = "NvimTreeFolderArrowOpen" hl = "NvimTreeFolderArrowOpen"
else else

View File

@@ -1,39 +1,35 @@
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") local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorBookmarks: Decorator ---@class (exact) BookmarkDecorator: Decorator
---@field icon HighlightedString ---@field private explorer Explorer
local DecoratorBookmarks = Decorator:new() ---@field private icon HighlightedString?
local BookmarkDecorator = Decorator:extend()
---@param opts table ---@class BookmarkDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): BookmarkDecorator
---@return DecoratorBookmarks
function DecoratorBookmarks: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 DecoratorBookmarks
if opts.renderer.icons.show.bookmarks then ---@protected
o.icon = { ---@param args DecoratorArgs
str = opts.renderer.icons.glyphs.bookmark, 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 = {
str = self.explorer.opts.renderer.icons.glyphs.bookmark,
hl = { "NvimTreeBookmarkIcon" }, hl = { "NvimTreeBookmarkIcon" },
} }
o:define_sign(o.icon) self:define_sign(self.icon)
end end
return o
end end
---Bookmark icon: renderer.icons.show.bookmarks and node is marked ---Bookmark icon: renderer.icons.show.bookmarks and node is marked
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function DecoratorBookmarks:calculate_icons(node) function BookmarkDecorator:icons(node)
if self.explorer.marks:get(node) then if self.explorer.marks:get(node) then
return { self.icon } return { self.icon }
end end
@@ -41,11 +37,11 @@ end
---Bookmark highlight: renderer.highlight_bookmarks and node is marked ---Bookmark highlight: renderer.highlight_bookmarks and node is marked
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorBookmarks:calculate_highlight(node) function BookmarkDecorator:highlight_group(node)
if self.hl_pos ~= HL_POSITION.none and self.explorer.marks:get(node) then if self.highlight_range ~= "none" and self.explorer.marks:get(node) then
return "NvimTreeBookmarkHL" return "NvimTreeBookmarkHL"
end end
end end
return DecoratorBookmarks return BookmarkDecorator

View File

@@ -1,35 +1,29 @@
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") local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorCopied: Decorator ---@class (exact) CopiedDecorator: Decorator
---@field enabled boolean ---@field private explorer Explorer
---@field icon HighlightedString|nil local CopiedDecorator = Decorator:extend()
local DecoratorCopied = Decorator:new()
---@param opts table ---@class CopiedDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): CopiedDecorator
---@return DecoratorCopied
function DecoratorCopied:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = true,
hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none,
})
---@cast o DecoratorCopied
return o ---@protected
---@param args DecoratorArgs
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 end
---Copied highlight: renderer.highlight_clipboard and node is copied ---Copied highlight: renderer.highlight_clipboard and node is copied
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorCopied:calculate_highlight(node) function CopiedDecorator:highlight_group(node)
if self.hl_pos ~= HL_POSITION.none and self.explorer.clipboard:is_copied(node) then if self.highlight_range ~= "none" and self.explorer.clipboard:is_copied(node) then
return "NvimTreeCopiedHL" return "NvimTreeCopiedHL"
end end
end end
return DecoratorCopied return CopiedDecorator

View File

@@ -1,35 +1,29 @@
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") local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorCut: Decorator ---@class (exact) CutDecorator: Decorator
---@field enabled boolean ---@field private explorer Explorer
---@field icon HighlightedString|nil local CutDecorator = Decorator:extend()
local DecoratorCut = Decorator:new()
---@param opts table ---@class CutDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): CutDecorator
---@return DecoratorCut
function DecoratorCut:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = true,
hl_pos = HL_POSITION[opts.renderer.highlight_clipboard] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none,
})
---@cast o DecoratorCut
return o ---@protected
---@param args DecoratorArgs
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 end
---Cut highlight: renderer.highlight_clipboard and node is cut ---Cut highlight: renderer.highlight_clipboard and node is cut
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorCut:calculate_highlight(node) function CutDecorator:highlight_group(node)
if self.hl_pos ~= HL_POSITION.none and self.explorer.clipboard:is_cut(node) then if self.highlight_range ~= "none" and self.explorer.clipboard:is_cut(node) then
return "NvimTreeCutHL" return "NvimTreeCutHL"
end end
end end
return DecoratorCut return CutDecorator

View File

@@ -1,9 +1,7 @@
local diagnostics = require("nvim-tree.diagnostics") local diagnostics = require("nvim-tree.diagnostics")
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") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
-- highlight groups by severity -- highlight groups by severity
local HG_ICON = { local HG_ICON = {
@@ -32,59 +30,54 @@ local ICON_KEYS = {
["hint"] = vim.diagnostic.severity.HINT, ["hint"] = vim.diagnostic.severity.HINT,
} }
---@class (exact) DecoratorDiagnostics: Decorator ---@class (exact) DiagnosticsDecorator: Decorator
---@field icons HighlightedString[] ---@field private explorer Explorer
local DecoratorDiagnostics = Decorator:new() ---@field private diag_icons HighlightedString[]?
local DiagnosticsDecorator = Decorator:extend()
---@param opts table ---@class DiagnosticsDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): DiagnosticsDecorator
---@return DecoratorDiagnostics
function DecoratorDiagnostics:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = opts.diagnostics.enable,
hl_pos = HL_POSITION[opts.renderer.highlight_diagnostics] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT[opts.renderer.icons.diagnostics_placement] or ICON_PLACEMENT.none,
})
---@cast o DecoratorDiagnostics
if not o.enabled then ---@protected
return o ---@param args DecoratorArgs
end function DiagnosticsDecorator:new(args)
self.explorer = args.explorer
if opts.renderer.icons.show.diagnostics then self.enabled = true
o.icons = {} 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.diag_icons = {}
for name, sev in pairs(ICON_KEYS) do for name, sev in pairs(ICON_KEYS) do
o.icons[sev] = { self.diag_icons[sev] = {
str = opts.diagnostics.icons[name], str = self.explorer.opts.diagnostics.icons[name],
hl = { HG_ICON[sev] }, hl = { HG_ICON[sev] },
} }
o:define_sign(o.icons[sev]) self:define_sign(self.diag_icons[sev])
end end
end end
return o
end end
---Diagnostic icon: diagnostics.enable, renderer.icons.show.diagnostics and node has status ---Diagnostic icon: diagnostics.enable, renderer.icons.show.diagnostics and node has status
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function DecoratorDiagnostics:calculate_icons(node) function DiagnosticsDecorator:icons(node)
if node and self.enabled and self.icons then if node and self.diag_icons then
local diag_status = diagnostics.get_diag_status(node) local diag_status = diagnostics.get_diag_status(node)
local diag_value = diag_status and diag_status.value local diag_value = diag_status and diag_status.value
if diag_value then if diag_value then
return { self.icons[diag_value] } return { self.diag_icons[diag_value] }
end end
end end
end end
---Diagnostic highlight: diagnostics.enable, renderer.highlight_diagnostics and node has status ---Diagnostic highlight: diagnostics.enable, renderer.highlight_diagnostics and node has status
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorDiagnostics:calculate_highlight(node) function DiagnosticsDecorator:highlight_group(node)
if not node or not self.enabled or self.hl_pos == HL_POSITION.none then if self.highlight_range == "none" then
return nil return nil
end end
@@ -96,7 +89,7 @@ function DecoratorDiagnostics:calculate_highlight(node)
end end
local group local group
if node.nodes then if node:is(DirectoryNode) then
group = HG_FOLDER[diag_value] group = HG_FOLDER[diag_value]
else else
group = HG_FILE[diag_value] group = HG_FILE[diag_value]
@@ -109,4 +102,4 @@ function DecoratorDiagnostics:calculate_highlight(node)
end end
end end
return DecoratorDiagnostics return DiagnosticsDecorator

View File

@@ -1,68 +1,69 @@
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local explorer_node = require("nvim-tree.explorer.node")
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") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class HighlightedStringGit: HighlightedString ---@class (exact) GitHighlightedString: nvim_tree.api.HighlightedString
---@field ord number decreasing priority ---@field ord number decreasing priority
---@class (exact) DecoratorGit: Decorator ---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked"
---@field file_hl table<string, string> by porcelain status e.g. "AM"
---@field folder_hl table<string, string> by porcelain status
---@field icons_by_status HighlightedStringGit[] by human status
---@field icons_by_xy table<string, HighlightedStringGit[]> by porcelain status
local DecoratorGit = Decorator:new()
---@param opts table ---@alias GitIconsByStatus table<GitStatusStrings, GitHighlightedString> human status
---@param explorer Explorer ---@alias GitIconsByXY table<GitXY, GitHighlightedString[]> porcelain status
---@return DecoratorGit ---@alias GitGlyphsByStatus table<GitStatusStrings, string> from opts
function DecoratorGit:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = opts.git.enable,
hl_pos = HL_POSITION[opts.renderer.highlight_git] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT[opts.renderer.icons.git_placement] or ICON_PLACEMENT.none,
})
---@cast o DecoratorGit
if not o.enabled then ---@class (exact) GitDecorator: Decorator
return o ---@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 GitDecorator
---@overload fun(args: DecoratorArgs): GitDecorator
---@protected
---@param args DecoratorArgs
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 end
if o.hl_pos ~= HL_POSITION.none then if self.highlight_range ~= "none" then
o:build_hl_table() self:build_file_folder_hl_by_xy()
end end
if opts.renderer.icons.show.git then if self.explorer.opts.renderer.icons.show.git then
o:build_icons_by_status(opts.renderer.icons.glyphs.git) self:build_icons_by_status(self.explorer.opts.renderer.icons.glyphs.git)
o:build_icons_by_xy(o.icons_by_status) self:build_icons_by_xy(self.icons_by_status)
for _, icon in pairs(o.icons_by_status) do for _, icon in pairs(self.icons_by_status) do
self:define_sign(icon) self:define_sign(icon)
end end
end end
return o
end end
---@param glyphs table<string, string> user glyps ---@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.staged = { str = glyphs.staged, hl = { "NvimTreeGitStagedIcon" }, ord = 1 }
unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }, self.icons_by_status.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 }
renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }, self.icons_by_status.renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 }
deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }, self.icons_by_status.deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 }
unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }, self.icons_by_status.unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 }
untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }, self.icons_by_status.untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 }
ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }, self.icons_by_status.ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 }
}
end end
---@param icons HighlightedStringGit[] ---@param icons GitIconsByXY
function DecoratorGit:build_icons_by_xy(icons) function GitDecorator:build_icons_by_xy(icons)
self.icons_by_xy = { self.icons_by_xy = {
["M "] = { icons.staged }, ["M "] = { icons.staged },
[" M"] = { icons.unstaged }, [" M"] = { icons.unstaged },
@@ -95,12 +96,12 @@ function DecoratorGit:build_icons_by_xy(icons)
["DD"] = { icons.deleted }, ["DD"] = { icons.deleted },
["DU"] = { icons.deleted, icons.unmerged }, ["DU"] = { icons.deleted, icons.unmerged },
["!!"] = { icons.ignored }, ["!!"] = { icons.ignored },
dirty = { icons.unstaged }, dirty = { icons.unstaged },
} }
end end
function DecoratorGit:build_hl_table() function GitDecorator:build_file_folder_hl_by_xy()
self.file_hl = { self.file_hl_by_xy = {
["M "] = "NvimTreeGitFileStagedHL", ["M "] = "NvimTreeGitFileStagedHL",
["C "] = "NvimTreeGitFileStagedHL", ["C "] = "NvimTreeGitFileStagedHL",
["AA"] = "NvimTreeGitFileStagedHL", ["AA"] = "NvimTreeGitFileStagedHL",
@@ -114,7 +115,7 @@ function DecoratorGit:build_hl_table()
[" T"] = "NvimTreeGitFileDirtyHL", [" T"] = "NvimTreeGitFileDirtyHL",
["MM"] = "NvimTreeGitFileDirtyHL", ["MM"] = "NvimTreeGitFileDirtyHL",
["AM"] = "NvimTreeGitFileDirtyHL", ["AM"] = "NvimTreeGitFileDirtyHL",
dirty = "NvimTreeGitFileDirtyHL", dirty = "NvimTreeGitFileDirtyHL",
["A "] = "NvimTreeGitFileStagedHL", ["A "] = "NvimTreeGitFileStagedHL",
["??"] = "NvimTreeGitFileNewHL", ["??"] = "NvimTreeGitFileNewHL",
["AU"] = "NvimTreeGitFileMergeHL", ["AU"] = "NvimTreeGitFileMergeHL",
@@ -133,33 +134,33 @@ function DecoratorGit:build_hl_table()
[" A"] = "none", [" A"] = "none",
} }
self.folder_hl = {} self.folder_hl_by_xy = {}
for k, v in pairs(self.file_hl) do for k, v in pairs(self.file_hl_by_xy) do
self.folder_hl[k] = v:gsub("File", "Folder") self.folder_hl_by_xy[k] = v:gsub("File", "Folder")
end end
end end
---Git icons: git.enable, renderer.icons.show.git and node has status ---Git icons: git.enable, renderer.icons.show.git and node has status
---@param node Node ---@param node Node
---@return HighlightedString[]|nil modified icon ---@return HighlightedString[]? icons
function DecoratorGit:calculate_icons(node) function GitDecorator:icons(node)
if not node or not self.enabled or not self.icons_by_xy then if not self.icons_by_xy then
return nil return nil
end end
local git_status = explorer_node.get_git_status(node) local git_xy = node:get_git_xy()
if git_status == nil then if git_xy == nil then
return nil return nil
end end
local inserted = {} local inserted = {}
local iconss = {} local iconss = {}
for _, s in pairs(git_status) do for _, s in pairs(git_xy) do
local icons = self.icons_by_xy[s] local icons = self.icons_by_xy[s]
if not icons then if not icons then
if self.hl_pos == HL_POSITION.none then if self.highlight_range == "none" then
notify.warn(string.format("Unrecognized git state '%s'", git_status)) notify.warn(string.format("Unrecognized git state '%s'", git_xy))
end end
return nil return nil
end end
@@ -189,12 +190,12 @@ end
---Get the first icon as the sign if appropriate ---Get the first icon as the sign if appropriate
---@param node Node ---@param node Node
---@return string|nil name ---@return string|nil name
function DecoratorGit:sign_name(node) function GitDecorator:sign_name(node)
if self.icon_placement ~= ICON_PLACEMENT.signcolumn then if self.icon_placement ~= "signcolumn" then
return return
end end
local icons = self:calculate_icons(node) local icons = self:icons(node)
if icons and #icons > 0 then if icons and #icons > 0 then
return icons[1].hl[1] return icons[1].hl[1]
end end
@@ -202,22 +203,22 @@ end
---Git highlight: git.enable, renderer.highlight_git and node has status ---Git highlight: git.enable, renderer.highlight_git and node has status
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorGit:calculate_highlight(node) function GitDecorator:highlight_group(node)
if not node or not self.enabled or self.hl_pos == HL_POSITION.none then if self.highlight_range == "none" then
return nil return nil
end end
local git_status = explorer_node.get_git_status(node) local git_xy = node:get_git_xy()
if not git_status then if not git_xy then
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return self.folder_hl[git_status[1]] return self.folder_hl_by_xy[git_xy[1]]
else else
return self.file_hl[git_status[1]] return self.file_hl_by_xy[git_xy[1]]
end end
end end
return DecoratorGit return GitDecorator

View File

@@ -1,57 +1,54 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local explorer_node = require("nvim-tree.explorer.node")
local Decorator = require("nvim-tree.renderer.decorator") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorHidden: Decorator ---@class (exact) HiddenDecorator: Decorator
---@field icon HighlightedString|nil ---@field private explorer Explorer
local DecoratorHidden = Decorator:new() ---@field private icon HighlightedString?
local HiddenDecorator = Decorator:extend()
---@param opts table ---@class HiddenDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): HiddenDecorator
---@return DecoratorHidden
function DecoratorHidden:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = true,
hl_pos = HL_POSITION[opts.renderer.highlight_hidden] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT[opts.renderer.icons.hidden_placement] or ICON_PLACEMENT.none,
})
---@cast o DecoratorHidden
if opts.renderer.icons.show.hidden then ---@protected
o.icon = { ---@param args DecoratorArgs
str = opts.renderer.icons.glyphs.hidden, 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 = {
str = self.explorer.opts.renderer.icons.glyphs.hidden,
hl = { "NvimTreeHiddenIcon" }, hl = { "NvimTreeHiddenIcon" },
} }
o:define_sign(o.icon) self:define_sign(self.icon)
end end
return o
end end
---Hidden icon: renderer.icons.show.hidden and node starts with `.` (dotfile). ---Hidden icon: renderer.icons.show.hidden and node starts with `.` (dotfile).
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function DecoratorHidden:calculate_icons(node) function HiddenDecorator:icons(node)
if self.enabled and explorer_node.is_dotfile(node) then if node:is_dotfile() then
return { self.icon } return { self.icon }
end end
end end
---Hidden highlight: renderer.highlight_hidden and node starts with `.` (dotfile). ---Hidden highlight: renderer.highlight_hidden and node starts with `.` (dotfile).
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorHidden:calculate_highlight(node) function HiddenDecorator:highlight_group(node)
if not self.enabled or self.hl_pos == HL_POSITION.none or (not explorer_node.is_dotfile(node)) then if self.highlight_range == "none" or not node:is_dotfile() then
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return "NvimTreeHiddenFolderHL" return "NvimTreeHiddenFolderHL"
else else
return "NvimTreeHiddenFileHL" return "NvimTreeHiddenFileHL"
end end
end end
return DecoratorHidden return HiddenDecorator

View File

@@ -1,39 +1,52 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION local Class = require("nvim-tree.classic")
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
---@class (exact) Decorator ---Abstract Decorator
---@field private __index? table ---@class (exact) Decorator: Class
---@field protected explorer Explorer
---@field protected enabled boolean ---@field protected enabled boolean
---@field protected hl_pos HL_POSITION ---@field protected highlight_range nvim_tree.api.decorator.HighlightRange
---@field protected icon_placement ICON_PLACEMENT ---@field protected icon_placement nvim_tree.api.decorator.IconPlacement
local Decorator = {} local Decorator = Class:extend()
---@param o Decorator|nil ---@class (exact) DecoratorArgs
---@return Decorator ---@field explorer Explorer
function Decorator:new(o)
o = o or {}
setmetatable(o, self) ---Abstract icon override, optionally implemented
self.__index = self ---@param node Node
---@return HighlightedString? icon_node
return o function Decorator:icon_node(node)
return self:nop(node)
end end
---Maybe highlight groups ---Abstract icons, optionally implemented
---@protected
---@param node Node ---@param node Node
---@return string|nil icon highlight group ---@return HighlightedString[]? icons
---@return string|nil name highlight group function Decorator:icons(node)
function Decorator:groups_icon_name(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 local icon_hl, name_hl
if self.enabled and self.hl_pos ~= HL_POSITION.none then if self.enabled and self.highlight_range ~= "none" then
local hl = self:calculate_highlight(node) local hl = self:highlight_group(node)
if self.hl_pos == HL_POSITION.all or self.hl_pos == HL_POSITION.icon then if self.highlight_range == "all" or self.highlight_range == "icon" then
icon_hl = hl icon_hl = hl
end end
if self.hl_pos == HL_POSITION.all or self.hl_pos == HL_POSITION.name then if self.highlight_range == "all" or self.highlight_range == "name" then
name_hl = hl name_hl = hl
end end
end end
@@ -43,70 +56,54 @@ end
---Maybe icon sign ---Maybe icon sign
---@param node Node ---@param node Node
---@return string|nil name ---@return string? name
function Decorator:sign_name(node) function Decorator:sign_name(node)
if not self.enabled or self.icon_placement ~= ICON_PLACEMENT.signcolumn then if not self.enabled or self.icon_placement ~= "signcolumn" then
return return
end end
local icons = self:calculate_icons(node) local icons = self:icons(node)
if icons and #icons > 0 then if icons and #icons > 0 then
return icons[1].hl[1] return icons[1].hl[1]
end end
end end
---Icons when ICON_PLACEMENT.before ---Icons when "before"
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function Decorator:icons_before(node) function Decorator:icons_before(node)
if not self.enabled or self.icon_placement ~= ICON_PLACEMENT.before then if not self.enabled or self.icon_placement ~= "before" then
return return
end end
return self:calculate_icons(node) return self:icons(node)
end end
---Icons when ICON_PLACEMENT.after ---Icons when "after"
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function Decorator:icons_after(node) function Decorator:icons_after(node)
if not self.enabled or self.icon_placement ~= ICON_PLACEMENT.after then if not self.enabled or self.icon_placement ~= "after" then
return return
end end
return self:calculate_icons(node) return self:icons(node)
end end
---Icons when ICON_PLACEMENT.right_align ---Icons when "right_align"
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function Decorator:icons_right_align(node) function Decorator:icons_right_align(node)
if not self.enabled or self.icon_placement ~= ICON_PLACEMENT.right_align then if not self.enabled or self.icon_placement ~= "right_align" then
return return
end end
return self:calculate_icons(node) return self: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
end end
---Define a sign ---Define a sign
---@protected ---@protected
---@param icon HighlightedString|nil ---@param icon HighlightedString?
function Decorator:define_sign(icon) function Decorator:define_sign(icon)
if icon and #icon.hl > 0 then if icon and #icon.hl > 0 then
local name = icon.hl[1] local name = icon.hl[1]
@@ -117,7 +114,7 @@ function Decorator:define_sign(icon)
-- don't use sign if not defined -- don't use sign if not defined
if #icon.str < 1 then if #icon.str < 1 then
self.icon_placement = ICON_PLACEMENT.none self.icon_placement = "none"
return return
end end

View File

@@ -1,63 +1,56 @@
local buffers = require("nvim-tree.buffers") local buffers = require("nvim-tree.buffers")
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") local Decorator = require("nvim-tree.renderer.decorator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) DecoratorModified: Decorator ---@class (exact) ModifiedDecorator: Decorator
---@field icon HighlightedString|nil ---@field private explorer Explorer
local DecoratorModified = Decorator:new() ---@field private icon HighlightedString?
local ModifiedDecorator = Decorator:extend()
---@param opts table ---@class ModifiedDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): ModifiedDecorator
---@return DecoratorModified
function DecoratorModified:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = opts.modified.enable,
hl_pos = HL_POSITION[opts.renderer.highlight_modified] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT[opts.renderer.icons.modified_placement] or ICON_PLACEMENT.none,
})
---@cast o DecoratorModified
if not o.enabled then ---@protected
return o ---@param args DecoratorArgs
end function ModifiedDecorator:new(args)
self.explorer = args.explorer
if opts.renderer.icons.show.modified then self.enabled = true
o.icon = { self.highlight_range = self.explorer.opts.renderer.highlight_modified or "none"
str = opts.renderer.icons.glyphs.modified, self.icon_placement = self.explorer.opts.renderer.icons.modified_placement or "none"
if self.explorer.opts.renderer.icons.show.modified then
self.icon = {
str = self.explorer.opts.renderer.icons.glyphs.modified,
hl = { "NvimTreeModifiedIcon" }, hl = { "NvimTreeModifiedIcon" },
} }
o:define_sign(o.icon) self:define_sign(self.icon)
end end
return o
end end
---Modified icon: modified.enable, renderer.icons.show.modified and node is modified ---Modified icon: modified.enable, renderer.icons.show.modified and node is modified
---@param node Node ---@param node Node
---@return HighlightedString[]|nil icons ---@return HighlightedString[]? icons
function DecoratorModified:calculate_icons(node) function ModifiedDecorator:icons(node)
if self.enabled and buffers.is_modified(node) then if buffers.is_modified(node) then
return { self.icon } return { self.icon }
end end
end end
---Modified highlight: modified.enable, renderer.highlight_modified and node is modified ---Modified highlight: modified.enable, renderer.highlight_modified and node is modified
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorModified:calculate_highlight(node) function ModifiedDecorator:highlight_group(node)
if not self.enabled or self.hl_pos == HL_POSITION.none or not buffers.is_modified(node) then if self.highlight_range == "none" or not buffers.is_modified(node) then
return nil return nil
end end
if node.nodes then if node:is(DirectoryNode) then
return "NvimTreeModifiedFolderHL" return "NvimTreeModifiedFolderHL"
else else
return "NvimTreeModifiedFileHL" return "NvimTreeModifiedFileHL"
end end
end end
return DecoratorModified return ModifiedDecorator

View File

@@ -1,37 +1,32 @@
local buffers = require("nvim-tree.buffers") local buffers = require("nvim-tree.buffers")
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") local Decorator = require("nvim-tree.renderer.decorator")
---@class (exact) DecoratorOpened: Decorator ---@class (exact) OpenDecorator: Decorator
---@field enabled boolean ---@field private explorer Explorer
---@field icon HighlightedString|nil ---@field private icon HighlightedString|nil
local DecoratorOpened = Decorator:new() local OpenDecorator = Decorator:extend()
---@param opts table ---@class OpenDecorator
---@param explorer Explorer ---@overload fun(args: DecoratorArgs): OpenDecorator
---@return DecoratorOpened
function DecoratorOpened:new(opts, explorer)
local o = Decorator.new(self, {
explorer = explorer,
enabled = true,
hl_pos = HL_POSITION[opts.renderer.highlight_opened_files] or HL_POSITION.none,
icon_placement = ICON_PLACEMENT.none,
})
---@cast o DecoratorOpened
return o ---@protected
---@param args DecoratorArgs
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 end
---Opened highlight: renderer.highlight_opened_files and node has an open buffer ---Opened highlight: renderer.highlight_opened_files and node has an open buffer
---@param node Node ---@param node Node
---@return string|nil group ---@return string? highlight_group
function DecoratorOpened:calculate_highlight(node) function OpenDecorator:highlight_group(node)
if self.hl_pos ~= HL_POSITION.none and buffers.is_opened(node) then if self.highlight_range ~= "none" and buffers.is_opened(node) then
return "NvimTreeOpenedHL" return "NvimTreeOpenedHL"
end end
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

View File

@@ -2,8 +2,7 @@ local log = require("nvim-tree.log")
local view = require("nvim-tree.view") local view = require("nvim-tree.view")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local icon_component = require("nvim-tree.renderer.components.icons") local Class = require("nvim-tree.classic")
local Builder = require("nvim-tree.renderer.builder") local Builder = require("nvim-tree.renderer.builder")
local SIGN_GROUP = "NvimTreeRendererSigns" local SIGN_GROUP = "NvimTreeRendererSigns"
@@ -12,28 +11,20 @@ local namespace_highlights_id = vim.api.nvim_create_namespace("NvimTreeHighlight
local namespace_extmarks_id = vim.api.nvim_create_namespace("NvimTreeExtmarks") local namespace_extmarks_id = vim.api.nvim_create_namespace("NvimTreeExtmarks")
local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines") local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines")
---@class (exact) Renderer ---@class (exact) Renderer: Class
---@field private __index? table ---@field explorer Explorer
---@field private opts table user options local Renderer = Class:extend()
---@field private explorer Explorer
---@field private builder Builder
local Renderer = {}
---@param opts table user options ---@class Renderer
---@param explorer Explorer ---@overload fun(args: RendererArgs): Renderer
---@return Renderer
function Renderer:new(opts, explorer)
---@type Renderer
local o = {
opts = opts,
explorer = explorer,
builder = Builder:new(opts, explorer),
}
setmetatable(o, self) ---@class (exact) RendererArgs
self.__index = self ---@field explorer Explorer
return o ---@protected
---@param args RendererArgs
function Renderer:new(args)
self.explorer = args.explorer
end end
---@private ---@private
@@ -41,6 +32,8 @@ end
---@param lines string[] ---@param lines string[]
---@param hl_args AddHighlightArgs[] ---@param hl_args AddHighlightArgs[]
---@param signs string[] ---@param signs string[]
---@param extmarks table[] extra marks for right icon placement
---@param virtual_lines table[] virtual lines for hidden count display
function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines) function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr }) vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr })
@@ -66,9 +59,9 @@ function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
for i, extname in pairs(extmarks) do for i, extname in pairs(extmarks) do
for _, mark in ipairs(extname) do for _, mark in ipairs(extname) do
vim.api.nvim_buf_set_extmark(bufnr, namespace_extmarks_id, i, -1, { vim.api.nvim_buf_set_extmark(bufnr, namespace_extmarks_id, i, -1, {
virt_text = { { mark.str, mark.hl } }, virt_text = { { mark.str, mark.hl } },
virt_text_pos = "right_align", virt_text_pos = "right_align",
hl_mode = "combine", hl_mode = "combine",
}) })
end end
end end
@@ -76,8 +69,8 @@ function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1)
for line_nr, vlines in pairs(virtual_lines) do for line_nr, vlines in pairs(virtual_lines) do
vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, { vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, {
virt_lines = vlines, virt_lines = vlines,
virt_lines_above = false, virt_lines_above = false,
virt_lines_leftcol = true, virt_lines_leftcol = true,
}) })
end end
@@ -107,9 +100,8 @@ function Renderer:draw()
local profile = log.profile_start("draw") local profile = log.profile_start("draw")
local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0) local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0)
icon_component.reset_config()
local builder = Builder:new(self.opts, self.explorer):build() local builder = Builder(self.explorer):build()
self:_draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks, builder.virtual_lines) self:_draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks, builder.virtual_lines)

View File

@@ -59,6 +59,34 @@ function M.path_basename(path)
return path:sub(i + 1, #path) return path:sub(i + 1, #path)
end end
--- Check if there are parentheses before brackets, it causes problems for windows.
--- Refer to issue #2862 and #2961 for more details.
local function has_parentheses_and_brackets(path)
local _, i_parentheses = path:find("(", 1, true)
local _, i_brackets = path:find("[", 1, true)
if i_parentheses and i_brackets then
return true
end
return false
end
--- Path normalizations for windows only
local function win_norm_path(path)
if path == nil then
return path
end
local norm_path = path
-- Normalize for issue #2862 and #2961
if has_parentheses_and_brackets(norm_path) then
norm_path = norm_path:gsub("/", "\\")
end
-- Normalize the drive letter
norm_path = norm_path:gsub("^%l:", function(drive)
return drive:upper()
end)
return norm_path
end
--- Get a path relative to another path. --- Get a path relative to another path.
---@param path string ---@param path string
---@param relative_to string|nil ---@param relative_to string|nil
@@ -68,13 +96,18 @@ function M.path_relative(path, relative_to)
return path return path
end end
local _, r = path:find(M.path_add_trailing(relative_to), 1, true) local norm_path = path
local p = path if M.is_windows then
norm_path = win_norm_path(norm_path)
end
local _, r = norm_path:find(M.path_add_trailing(relative_to), 1, true)
local p = norm_path
if r then if r then
-- take the relative path starting after '/' -- take the relative path starting after '/'
-- if somehow given a completely matching path, -- if somehow given a completely matching path,
-- returns "" -- returns ""
p = path:sub(r + 1) p = norm_path:sub(r + 1)
end end
return p return p
end end
@@ -112,8 +145,7 @@ function M.find_node(nodes, fn)
end) end)
:iterate() :iterate()
i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1 i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
local explorer = require("nvim-tree.core").get_explorer() if node and node.explorer.live_filter.filter then
if explorer and explorer.live_filter.filter then
i = i + 1 i = i + 1
end end
return node, i return node, i
@@ -121,7 +153,7 @@ end
-- Find the line number of a node. -- Find the line number of a node.
-- Return -1 is node is nil or not found. -- Return -1 is node is nil or not found.
---@param node Node|nil ---@param node Node?
---@return integer ---@return integer
function M.find_node_line(node) function M.find_node_line(node)
if not node then if not node then
@@ -174,16 +206,6 @@ function M.get_node_from_path(path)
:iterate() :iterate()
end end
---Get the highest parent of grouped nodes
---@param node Node
---@return Node node or parent
function M.get_parent_of_group(node)
while node and node.parent and node.parent.group_next do
node = node.parent
end
return node
end
M.default_format_hidden_count = function(hidden_count, simple) M.default_format_hidden_count = function(hidden_count, simple)
local parts = {} local parts = {}
local total_count = 0 local total_count = 0
@@ -283,6 +305,14 @@ function M.canonical_path(path)
return path return path
end end
--- Escapes special characters in string for windows, refer to issue #2862 and #2961 for more details.
local function escape_special_char_for_windows(path)
if has_parentheses_and_brackets(path) then
return path:gsub("\\", "/"):gsub("/ ", "\\ ")
end
return path:gsub("%(", "\\("):gsub("%)", "\\)")
end
--- Escapes special characters in string if windows else returns unmodified string. --- Escapes special characters in string if windows else returns unmodified string.
---@param path string ---@param path string
---@return string|nil ---@return string|nil
@@ -290,7 +320,7 @@ function M.escape_special_chars(path)
if path == nil then if path == nil then
return path return path
end end
return M.is_windows and path:gsub("\\", "/") or path return M.is_windows and escape_special_char_for_windows(path) or path
end end
--- Create empty sub-tables if not present --- Create empty sub-tables if not present
@@ -473,7 +503,7 @@ end
---Focus node passed as parameter if visible, otherwise focus first visible parent. ---Focus node passed as parameter if visible, otherwise focus first visible parent.
---If none of the parents is visible focus root. ---If none of the parents is visible focus root.
---If node is nil do nothing. ---If node is nil do nothing.
---@param node Node|nil node to focus ---@param node Node? node to focus
function M.focus_node_or_parent(node) function M.focus_node_or_parent(node)
local explorer = require("nvim-tree.core").get_explorer() local explorer = require("nvim-tree.core").get_explorer()
@@ -549,14 +579,6 @@ function M.array_remove_nils(array)
end, array) end, array)
end end
---@param f fun(node: Node|nil)
---@return function
function M.inject_node(f)
return function()
f(require("nvim-tree.lib").get_node_at_cursor())
end
end
--- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file. --- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file.
--- This is cheap, as the readable test should only ever be needed when resuming a vim session. --- This is cheap, as the readable test should only ever be needed when resuming a vim session.
---@param bufnr number|nil may be 0 or nil for current ---@param bufnr number|nil may be 0 or nil for current
@@ -578,4 +600,44 @@ function M.is_nvim_tree_buf(bufnr)
return false return false
end end
--- path is an executable file or directory
---@param absolute_path string
---@return boolean
function M.is_executable(absolute_path)
if M.is_windows or M.is_wsl then
--- executable detection on windows is buggy and not performant hence it is disabled
return false
else
return vim.loop.fs_access(absolute_path, "X") or false
end
end
---List of all option info/values
---@param opts vim.api.keyset.option passed directly to vim.api.nvim_get_option_info2 and vim.api.nvim_get_option_value
---@param was_set boolean filter was_set
---@return { info: vim.api.keyset.get_option_info, val: any }[]
function M.enumerate_options(opts, was_set)
local res = {}
local infos = vim.tbl_filter(function(info)
if opts.buf and info.scope ~= "buf" then
return false
elseif opts.win and info.scope ~= "win" then
return false
else
return true
end
end, vim.api.nvim_get_all_options_info())
for _, info in vim.spairs(infos) do
local _, info2 = pcall(vim.api.nvim_get_option_info2, info.name, opts)
if not was_set or info2.was_set then
local val = pcall(vim.api.nvim_get_option_value, info.name, opts)
table.insert(res, { info = info2, val = val })
end
end
return res
end
return M return M

View File

@@ -15,31 +15,31 @@ local DEFAULT_MAX_WIDTH = -1
local DEFAULT_PADDING = 1 local DEFAULT_PADDING = 1
M.View = { M.View = {
adaptive_size = false, adaptive_size = false,
centralize_selection = false, centralize_selection = false,
tabpages = {}, tabpages = {},
cursors = {}, cursors = {},
hide_root_folder = false, hide_root_folder = false,
live_filter = { live_filter = {
prev_focused_node = nil, prev_focused_node = nil,
}, },
winopts = { winopts = {
relativenumber = false, relativenumber = false,
number = false, number = false,
list = false, list = false,
foldenable = false, foldenable = false,
winfixwidth = true, winfixwidth = true,
winfixheight = true, winfixheight = true,
spell = false, spell = false,
signcolumn = "yes", signcolumn = "yes",
foldmethod = "manual", foldmethod = "manual",
foldcolumn = "0", foldcolumn = "0",
cursorcolumn = false, cursorcolumn = false,
cursorline = true, cursorline = true,
cursorlineopt = "both", cursorlineopt = "both",
colorcolumn = "0", colorcolumn = "0",
wrap = false, wrap = false,
winhl = table.concat({ winhl = table.concat({
"EndOfBuffer:NvimTreeEndOfBuffer", "EndOfBuffer:NvimTreeEndOfBuffer",
"CursorLine:NvimTreeCursorLine", "CursorLine:NvimTreeCursorLine",
"CursorLineNr:NvimTreeCursorLineNr", "CursorLineNr:NvimTreeCursorLineNr",
@@ -65,13 +65,15 @@ local tabinitial = {
} }
local BUFNR_PER_TAB = {} local BUFNR_PER_TAB = {}
---@type { name: string, value: any }[]
local BUFFER_OPTIONS = { local BUFFER_OPTIONS = {
swapfile = false, { name = "bufhidden", value = "wipe" },
buftype = "nofile", { name = "buflisted", value = false },
modifiable = false, { name = "buftype", value = "nofile" },
filetype = "NvimTree", { name = "filetype", value = "NvimTree" },
bufhidden = "wipe", { name = "modifiable", value = false },
buflisted = false, { name = "swapfile", value = false },
} }
---@param bufnr integer ---@param bufnr integer
@@ -101,8 +103,9 @@ local function create_buffer(bufnr)
BUFNR_PER_TAB[tab] = bufnr or vim.api.nvim_create_buf(false, false) BUFNR_PER_TAB[tab] = bufnr or vim.api.nvim_create_buf(false, false)
vim.api.nvim_buf_set_name(M.get_bufnr(), "NvimTree_" .. tab) vim.api.nvim_buf_set_name(M.get_bufnr(), "NvimTree_" .. tab)
for option, value in pairs(BUFFER_OPTIONS) do bufnr = M.get_bufnr()
vim.bo[M.get_bufnr()][option] = value for _, option in ipairs(BUFFER_OPTIONS) do
vim.api.nvim_set_option_value(option.name, option.value, { buf = bufnr })
end end
require("nvim-tree.keymap").on_attach(M.get_bufnr()) require("nvim-tree.keymap").on_attach(M.get_bufnr())
@@ -124,6 +127,7 @@ local function get_size(size)
end end
---@param size (fun():integer)|integer|nil ---@param size (fun():integer)|integer|nil
---@return integer
local function get_width(size) local function get_width(size)
if size then if size then
return get_size(size) return get_size(size)
@@ -146,12 +150,28 @@ end
local function set_window_options_and_buffer() local function set_window_options_and_buffer()
pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr()) pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr())
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore = "all" if vim.fn.has("nvim-0.10") == 1 then
for k, v in pairs(M.View.winopts) do local eventignore = vim.api.nvim_get_option_value("eventignore", {})
vim.opt_local[k] = v vim.api.nvim_set_option_value("eventignore", "all", {})
for k, v in pairs(M.View.winopts) do
vim.api.nvim_set_option_value(k, v, { scope = "local" })
end
vim.api.nvim_set_option_value("eventignore", eventignore, {})
else
local eventignore = vim.api.nvim_get_option("eventignore") ---@diagnostic disable-line: deprecated
vim.api.nvim_set_option("eventignore", "all") ---@diagnostic disable-line: deprecated
-- #3009 vim.api.nvim_win_set_option does not set local scope without explicit winid.
-- Revert to opt_local instead of propagating it through for just the 0.10 path.
for k, v in pairs(M.View.winopts) do
vim.opt_local[k] = v
end
vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated
end end
vim.opt.eventignore = eventignore
end end
---@return table ---@return table
@@ -411,6 +431,7 @@ function M.abandon_all_windows()
end end
---@param opts table|nil ---@param opts table|nil
---@return boolean
function M.is_visible(opts) function M.is_visible(opts)
if opts and opts.tabpage then if opts and opts.tabpage then
if M.View.tabpages[opts.tabpage] == nil then if M.View.tabpages[opts.tabpage] == nil then
@@ -495,12 +516,6 @@ function M.get_bufnr()
return BUFNR_PER_TAB[vim.api.nvim_get_current_tabpage()] return BUFNR_PER_TAB[vim.api.nvim_get_current_tabpage()]
end end
---@param bufnr number
---@return boolean
function M.is_buf_valid(bufnr)
return bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
end
function M._prevent_buffer_override() function M._prevent_buffer_override()
local view_winnr = M.get_winnr() local view_winnr = M.get_winnr()
local view_bufnr = M.get_bufnr() local view_bufnr = M.get_bufnr()

View File

@@ -2,21 +2,7 @@ local notify = require("nvim-tree.notify")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local M = { local Class = require("nvim-tree.classic")
config = {},
}
---@class Event
local Event = {
_events = {},
}
Event.__index = Event
---@class Watcher
local Watcher = {
_watchers = {},
}
Watcher.__index = Watcher
local FS_EVENT_FLAGS = { local FS_EVENT_FLAGS = {
-- inotify or equivalent will be used; fallback to stat has not yet been implemented -- inotify or equivalent will be used; fallback to stat has not yet been implemented
@@ -25,20 +11,49 @@ local FS_EVENT_FLAGS = {
recursive = false, recursive = false,
} }
---@param path string local M = {
---@return Event|nil config = {},
function Event:new(path) }
log.line("watcher", "Event:new '%s'", path)
local e = setmetatable({ ---Registry of all events
_path = path, ---@type Event[]
_fs_event = nil, local events = {}
_listeners = {},
}, Event)
if e:start() then ---@class (exact) Event: Class
Event._events[path] = e ---@field destroyed boolean
return e ---@field private path string
---@field private fs_event uv.uv_fs_event_t?
---@field private listeners function[]
local Event = Class:extend()
---@class Event
---@overload fun(args: EventArgs): Event
---@class (exact) EventArgs
---@field path string
---@protected
---@param args EventArgs
function Event:new(args)
self.destroyed = false
self.path = args.path
self.fs_event = nil
self.listeners = {}
end
---Static factory method
---Creates and starts an Event
---nil on failure to start
---@param args EventArgs
---@return Event?
function Event:create(args)
log.line("watcher", "Event:create '%s'", args.path)
local event = Event(args)
if event:start() then
events[event.path] = event
return event
else else
return nil return nil
end end
@@ -46,21 +61,21 @@ end
---@return boolean ---@return boolean
function Event:start() function Event:start()
log.line("watcher", "Event:start '%s'", self._path) log.line("watcher", "Event:start '%s'", self.path)
local rc, _, name local rc, _, name
self._fs_event, _, name = vim.loop.new_fs_event() self.fs_event, _, name = vim.loop.new_fs_event()
if not self._fs_event then if not self.fs_event then
self._fs_event = nil self.fs_event = nil
notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name)) notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name))
return false return false
end end
local event_cb = vim.schedule_wrap(function(err, filename) local event_cb = vim.schedule_wrap(function(err, filename)
if err then if err then
log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self._path, filename, err) log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self.path, filename, err)
local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self._path) local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self.path)
if err == "EPERM" and (utils.is_windows or utils.is_wsl) then if err == "EPERM" and (utils.is_windows or utils.is_wsl) then
-- on directory removal windows will cascade the filesystem events out of order -- on directory removal windows will cascade the filesystem events out of order
log.line("watcher", message) log.line("watcher", message)
@@ -69,19 +84,19 @@ function Event:start()
self:destroy(message) self:destroy(message)
end end
else else
log.line("watcher", "event_cb '%s' '%s'", self._path, filename) log.line("watcher", "event_cb '%s' '%s'", self.path, filename)
for _, listener in ipairs(self._listeners) do for _, listener in ipairs(self.listeners) do
listener(filename) listener(filename)
end end
end end
end) end)
rc, _, name = self._fs_event:start(self._path, FS_EVENT_FLAGS, event_cb) rc, _, name = self.fs_event:start(self.path, FS_EVENT_FLAGS, event_cb)
if rc ~= 0 then if rc ~= 0 then
if name == "EMFILE" then if name == "EMFILE" then
M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting") M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting")
else else
notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self._path, name)) notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self.path, name))
end end
return false return false
end end
@@ -91,81 +106,114 @@ end
---@param listener function ---@param listener function
function Event:add(listener) function Event:add(listener)
table.insert(self._listeners, listener) table.insert(self.listeners, listener)
end end
---@param listener function ---@param listener function
function Event:remove(listener) function Event:remove(listener)
utils.array_remove(self._listeners, listener) utils.array_remove(self.listeners, listener)
if #self._listeners == 0 then if #self.listeners == 0 then
self:destroy() self:destroy()
end end
end end
---@param message string|nil ---@param message string|nil
function Event:destroy(message) function Event:destroy(message)
log.line("watcher", "Event:destroy '%s'", self._path) log.line("watcher", "Event:destroy '%s'", self.path)
if self._fs_event then if self.fs_event then
if message then if message then
notify.warn(message) notify.warn(message)
end end
local rc, _, name = self._fs_event:stop() local rc, _, name = self.fs_event:stop()
if rc ~= 0 then if rc ~= 0 then
notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self._path, name)) notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self.path, name))
end end
self._fs_event = nil self.fs_event = nil
end end
Event._events[self._path] = nil
self.destroyed = true self.destroyed = true
events[self.path] = nil
end end
---@param path string ---Registry of all watchers
---@param files string[]|nil ---@type Watcher[]
---@param callback function local watchers = {}
---@param data table
---@class (exact) Watcher: Class
---@field data table user data
---@field destroyed boolean
---@field private path string
---@field private callback fun(watcher: Watcher)
---@field private files string[]?
---@field private listener fun(filename: string)?
---@field private event Event
local Watcher = Class:extend()
---@class Watcher
---@overload fun(args: WatcherArgs): Watcher
---@class (exact) WatcherArgs
---@field path string
---@field files string[]|nil
---@field callback fun(watcher: Watcher)
---@field data table? user data
---@protected
---@param args WatcherArgs
function Watcher:new(args)
self.data = args.data
self.destroyed = false
self.path = args.path
self.callback = args.callback
self.files = args.files
self.listener = nil
end
---Static factory method
---Creates and starts a Watcher
---nil on failure to create Event
---@param args WatcherArgs
---@return Watcher|nil ---@return Watcher|nil
function Watcher:new(path, files, callback, data) function Watcher:create(args)
log.line("watcher", "Watcher:new '%s' %s", path, vim.inspect(files)) log.line("watcher", "Watcher:create '%s' %s", args.path, vim.inspect(args.files))
local w = setmetatable(data, Watcher) local event = events[args.path] or Event:create({ path = args.path })
if not event then
w._event = Event._events[path] or Event:new(path)
w._listener = nil
w._path = path
w._files = files
w._callback = callback
if not w._event then
return nil return nil
end end
w:start() local watcher = Watcher(args)
table.insert(Watcher._watchers, w) watcher.event = event
return w watcher:start()
table.insert(watchers, watcher)
return watcher
end end
function Watcher:start() function Watcher:start()
self._listener = function(filename) self.listener = function(filename)
if not self._files or vim.tbl_contains(self._files, filename) then if not self.files or vim.tbl_contains(self.files, filename) then
self._callback(self) self.callback(self)
end end
end end
self._event:add(self._listener) self.event:add(self.listener)
end end
function Watcher:destroy() function Watcher:destroy()
log.line("watcher", "Watcher:destroy '%s'", self._path) log.line("watcher", "Watcher:destroy '%s'", self.path)
self._event:remove(self._listener) self.event:remove(self.listener)
utils.array_remove(Watcher._watchers, self) utils.array_remove(
watchers,
self
)
self.destroyed = true self.destroyed = true
end end
@@ -183,11 +231,11 @@ end
function M.purge_watchers() function M.purge_watchers()
log.line("watcher", "purge_watchers") log.line("watcher", "purge_watchers")
for _, w in ipairs(utils.array_shallow_clone(Watcher._watchers)) do for _, w in ipairs(utils.array_shallow_clone(watchers)) do
w:destroy() w:destroy()
end end
for _, e in pairs(Event._events) do for _, e in pairs(events) do
e:destroy() e:destroy()
end end
end end

View File

@@ -70,7 +70,7 @@ sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_ON_ATTACH.lua
# help human # help human
echo > /tmp/DEFAULT_ON_ATTACH.help echo > /tmp/DEFAULT_ON_ATTACH.help
sed -E "s/^ *vim.keymap.set\('n', '(.*)',.*api(.*),.*opts\('(.*)'.*$/'\`\1\`' '\3' '|nvim-tree-api\2()|'/g sed -E "s/^ *vim.keymap.set\(\"n\", \"(.*)\",.*api(.*),.*opts\(\"(.*)\".*$/'\`\1\`' '\3' '|nvim-tree-api\2()|'/g
" /tmp/DEFAULT_ON_ATTACH.lua | while read -r line " /tmp/DEFAULT_ON_ATTACH.lua | while read -r line
do do
eval "printf '%-17.17s %-26.26s %s\n' ${line}" >> /tmp/DEFAULT_ON_ATTACH.help eval "printf '%-17.17s %-26.26s %s\n' ${line}" >> /tmp/DEFAULT_ON_ATTACH.help

View File

@@ -9,15 +9,20 @@ if [ -z "${VIMRUNTIME}" ]; then
export VIMRUNTIME="/usr/share/nvim/runtime" export VIMRUNTIME="/usr/share/nvim/runtime"
fi fi
DIR_SRC="lua" DIR_SRC="${PWD}/lua"
DIR_OUT="luals-out" DIR_OUT="${PWD}/luals-out"
FILE_LUARC="${DIR_OUT}/luarc.json"
# clear output # clear output
rm -rf "${DIR_OUT}" rm -rf "${DIR_OUT}"
mkdir "${DIR_OUT}" mkdir "${DIR_OUT}"
# Uncomment runtime.version for strict neovim baseline 5.1
# It is not set normally, to prevent luals loading 5.1 and 5.x, resulting in both versions being chosen on vim.lsp.buf.definition()
cat "${PWD}/.luarc.json" | sed -E 's/.luals-check-only//g' > "${FILE_LUARC}"
# execute inside lua to prevent luals itself from being checked # execute inside lua to prevent luals itself from being checked
OUT=$(lua-language-server --check="${DIR_SRC}" --configpath="${PWD}/.luarc.json" --checklevel=Information --logpath="${DIR_OUT}" --loglevel=error) OUT=$(lua-language-server --check="${DIR_SRC}" --configpath="${FILE_LUARC}" --checklevel=Information --logpath="${DIR_OUT}" --loglevel=error)
RC=$? RC=$?
echo "${OUT}" >&2 echo "${OUT}" >&2