Compare commits

...

31 Commits
v1.7.1 ... v1.8

Author SHA1 Message Date
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
79 changed files with 3129 additions and 2680 deletions

View File

@@ -18,3 +18,4 @@ continuation_indent = 2
quote_style = double
call_arg_parentheses = always
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_netrwPlugin = 1
vim.cmd [[set runtimepath=$VIMRUNTIME]]
vim.cmd [[set packpath=/tmp/nvt-min/site]]
vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.cmd([[set packpath=/tmp/nvt-min/site]])
local package_root = "/tmp/nvt-min/site/pack"
local install_path = package_root .. "/packer/start/packer.nvim"
local function load_plugins()
require("packer").startup {
require("packer").startup({
{
"wbthomason/packer.nvim",
"nvim-tree/nvim-tree.lua",
@@ -18,21 +18,21 @@ local function load_plugins()
compile_path = install_path .. "/plugin/packer_compiled.lua",
display = { non_interactive = true },
},
}
})
end
if vim.fn.isdirectory(install_path) == 0 then
print "Installing nvim-tree and dependencies."
vim.fn.system { "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path }
print("Installing nvim-tree and dependencies.")
vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path })
end
load_plugins()
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.cursorline = true
-- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE
_G.setup = function()
require("nvim-tree").setup {}
require("nvim-tree").setup({})
end
-- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate.
@@ -41,7 +41,11 @@ end
vim.api.nvim_create_autocmd("FileType", {
pattern = "lua",
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,
})
]]

View File

@@ -69,7 +69,7 @@ jobs:
strategy:
matrix:
nvim_version: [ stable, nightly ]
luals_version: [ 3.10.5 ]
luals_version: [ 3.11.0 ]
steps:
- 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.
self = false
M.self = false
ignore = {
M.ignore = {
"631", -- max_line_length
}
-- Global objects defined by the C code
globals = {
M.globals = {
"vim",
"TreeExplorer"
}
return M

View File

@@ -1,6 +1,6 @@
{
"$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": {
"library": [
"$VIMRUNTIME/lua/vim",
@@ -33,13 +33,13 @@
"empty-block": "Any",
"global-element": "Any",
"global-in-nil-env": "Any",
"incomplete-signature-doc": "None",
"incomplete-signature-doc": "Any",
"inject-field": "Any",
"invisible": "Any",
"lowercase-global": "Any",
"missing-fields": "Any",
"missing-global-doc": "Any",
"missing-local-export-doc": "None",
"missing-local-export-doc": "Any",
"missing-parameter": "Any",
"missing-return": "Any",
"missing-return-value": "Any",

View File

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

View File

@@ -1,5 +1,28 @@
# Changelog
## [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)

View File

@@ -2,7 +2,7 @@
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
@@ -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
# 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
Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge.

View File

@@ -117,7 +117,7 @@ Disabling |netrw| is strongly advised, see |nvim-tree-netrw|
==============================================================================
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
vim.g.loaded_netrw = 1
@@ -215,7 +215,7 @@ Show the mappings: `g?`
2.3 QUICKSTART: CUSTOM MAPPINGS *nvim-tree-quickstart-custom-mappings*
|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 api = require "nvim-tree.api"
@@ -228,8 +228,8 @@ via |nvim-tree.on_attach| e.g. >
api.config.mappings.default_on_attach(bufnr)
-- custom mappings
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", "<C-t>", api.tree.change_root_to_parent, opts("Up"))
vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help"))
end
-- pass to setup along with your other options
@@ -245,7 +245,7 @@ via |nvim-tree.on_attach| e.g. >
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
applied at runtime. e.g. >
applied at runtime. e.g. >lua
vim.cmd([[
:hi NvimTreeExecFile guifg=#ffa0a0
@@ -366,15 +366,15 @@ again to apply a change in configuration without restarting nvim.
setup() function takes one optional argument: configuration table. If omitted
nvim-tree will be initialised with default configuration.
>
The first setup() call is cheap: it does nothing more than validate / apply
the configuration. Nothing happens until the tree is first opened.
Subsequent setup() calls are expensive as they tear down the world before
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
on_attach = "default",
hijack_cursor = false,
@@ -561,7 +561,12 @@ Following is the default configuration. See |nvim-tree-opts| for details.
filesystem_watchers = {
enable = true,
debounce_delay = 50,
ignore_dirs = {},
ignore_dirs = {
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
},
actions = {
use_system_clipboard = true,
@@ -587,6 +592,7 @@ Following is the default configuration. See |nvim-tree-opts| for details.
quit_on_open = false,
eject = true,
resize_window = true,
relative_path = true,
window_picker = {
enable = true,
picker = "default",
@@ -626,11 +632,6 @@ Following is the default configuration. See |nvim-tree-opts| for details.
},
},
experimental = {
actions = {
open_file = {
relative_path = false,
},
},
},
log = {
enable = false,
@@ -672,7 +673,7 @@ Completely disable netrw
It is strongly advised to eagerly disable netrw, due to race conditions at vim
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_netrwPlugin = 1
<
@@ -734,7 +735,7 @@ Can be one of `"name"`, `"case_sensitive"`, `"modification_time"`, `"extension"`
- `name`: `string`
- `type`: `"directory"` | `"file"` | `"link"`
Example: sort by name length: >
Example: sort by name length: >lua
local sorter = function(nodes)
table.sort(nodes, function(a, b)
return #a.name < #b.name
@@ -866,7 +867,7 @@ Set to `false` to hide the root folder.
Type: `string` or `boolean` or `function(root_cwd)`, Default: `":~:s?$?/..?"`
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)
return ".../" .. vim.fn.fnamemodify(path, ":t")
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.
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 = {
bookmark = <num>,
buf = <num>,
@@ -908,7 +909,7 @@ Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay
live_filter = <num>,
}
<
Example of function that can be passed: >
Example of function that can be passed: >lua
function(hidden_stats)
local total_count = 0
for reason, count in pairs(hidden_stats) do
@@ -983,7 +984,7 @@ Configuration options for tree indent markers.
*nvim-tree.renderer.indent_markers.icons*
Icons shown before the file/directory. Length 1.
Type: `table`, Default: >
Type: `table`, Default: >lua
{
corner = "└",
edge = "│",
@@ -1295,7 +1296,7 @@ Severity for which the diagnostics will be displayed. See |diagnostic-severity|
*nvim-tree.diagnostics.icons*
Icons for diagnostic severity.
Type: `table`, Default: >
Type: `table`, Default: >lua
{
hint = "",
info = "",
@@ -1416,8 +1417,14 @@ function returning whether a path should be ignored.
Strings must be backslash escaped e.g. `"my-proj/\\.build$"`. See |string-match|.
Function is passed an absolute path.
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*
@@ -1436,8 +1443,6 @@ vim |current-directory| behaviour.
*nvim-tree.actions.change_dir.global*
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`
*nvim-tree.actions.change_dir.restrict_above_cwd*
@@ -1488,6 +1493,11 @@ Configuration options for opening a file from nvim-tree.
Resizes the tree when opening a file.
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*
Window picker configuration.
@@ -1502,10 +1512,10 @@ Configuration options for opening a file from nvim-tree.
or `nil` if an invalid window is picked or user cancelled the action.
The picker may create a new window.
Type: `string` | `function`, Default: `"default"`
e.g. s1n7ax/nvim-window-picker plugin: >
e.g. s1n7ax/nvim-window-picker plugin: >lua
window_picker = {
enable = true,
picker = require('window-picker').pick_window,
picker = require("window-picker").pick_window,
<
*nvim-tree.actions.open_file.window_picker.chars*
A string of chars used as identifiers by the window picker.
@@ -1515,7 +1525,7 @@ Configuration options for opening a file from nvim-tree.
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
selectable.
Type: `table`, Default: >
Type: `table`, Default: >lua
{
filetype = {
"notify",
@@ -1616,12 +1626,6 @@ Confirmation prompts.
Experimental features that may become default or optional functionality.
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*
@@ -1674,9 +1678,7 @@ Specify which information to log.
==============================================================================
6. API *nvim-tree-api*
Nvim-tree's public API can be used to access features.
>
e.g. >
Nvim-tree's public API can be used to access features. e.g. >lua
local api = require("nvim-tree.api")
api.tree.toggle()
<
@@ -2275,30 +2277,30 @@ Active mappings may be viewed via HELP, default `g?`. The mapping's description
is used when displaying HELP.
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 api = require('nvim-tree.api')
local api = require("nvim-tree.api")
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
-- 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-e>', api.node.open.replace_tree_buffer, opts('Open: In Place'))
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"))
---
-- OR use all default mappings
api.config.mappings.default_on_attach(bufnr)
-- remove a default
vim.keymap.del('n', '<C-]>', { buffer = bufnr })
vim.keymap.del("n", "<C-]>", { buffer = bufnr })
-- 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
vim.keymap.set('n', '?', api.tree.toggle_help, opts('Help'))
vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help"))
---
end
@@ -2315,16 +2317,16 @@ Single left mouse mappings can be achieved via `<LeftRelease>`.
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
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 api = require('nvim-tree.api')
local api = require("nvim-tree.api")
local node = api.tree.get_node_under_cursor()
print(node.absolute_path)
end
-- 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*
@@ -2332,83 +2334,83 @@ define your own function to map complex functionality e.g. >
In the absence of an |nvim-tree.on_attach| function, the following defaults
will be applied.
You are encouraged to copy these to your own |nvim-tree.on_attach| function.
>
local api = require('nvim-tree.api')
You are encouraged to copy these to your own |nvim-tree.on_attach| function. >lua
local api = require("nvim-tree.api")
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
-- BEGIN_DEFAULT_ON_ATTACH
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-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-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-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', '<CR>', api.node.open.edit, opts('Open'))
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.prev, opts('Previous Sibling'))
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', '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', 'bt', api.marks.bulk.trash, opts('Trash 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', '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.node.navigate.git.prev, opts('Prev 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.trash, opts('Trash'))
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.node.navigate.diagnostics.next, opts('Next Diagnostic'))
vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic'))
vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear'))
vim.keymap.set('n', 'f', api.live_filter.start, opts('Live Filter: Start'))
vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help'))
vim.keymap.set('n', 'gy', api.fs.copy.absolute_path, opts('Copy Absolute Path'))
vim.keymap.set('n', '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', 'I', api.tree.toggle_gitignore_filter, opts('Toggle Filter: Git Ignore'))
vim.keymap.set('n', 'J', api.node.navigate.sibling.last, opts('Last Sibling'))
vim.keymap.set('n', 'K', api.node.navigate.sibling.first, opts('First Sibling'))
vim.keymap.set('n', '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.marks.toggle, opts('Toggle Bookmark'))
vim.keymap.set('n', 'o', api.node.open.edit, opts('Open'))
vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker'))
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', 'q', api.tree.close, opts('Close'))
vim.keymap.set('n', 'r', api.fs.rename, opts('Rename'))
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.tree.search_node, opts('Search'))
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', 'W', api.tree.collapse_all, opts('Collapse'))
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.relative_path, opts('Copy Relative Path'))
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", "<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-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-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-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", "<CR>", api.node.open.edit, opts("Open"))
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.prev, opts("Previous Sibling"))
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", "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", "bt", api.marks.bulk.trash, opts("Trash 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", "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.node.navigate.git.prev, opts("Prev 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.trash, opts("Trash"))
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.node.navigate.diagnostics.next, opts("Next Diagnostic"))
vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic"))
vim.keymap.set("n", "F", api.live_filter.clear, opts("Live Filter: Clear"))
vim.keymap.set("n", "f", api.live_filter.start, opts("Live Filter: Start"))
vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help"))
vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set("n", "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", "I", api.tree.toggle_gitignore_filter, opts("Toggle Filter: Git Ignore"))
vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling"))
vim.keymap.set("n", "K", api.node.navigate.sibling.first, opts("First Sibling"))
vim.keymap.set("n", "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.marks.toggle, opts("Toggle Bookmark"))
vim.keymap.set("n", "o", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
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", "q", api.tree.close, opts("Close"))
vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
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.tree.search_node, opts("Search"))
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", "W", api.tree.collapse_all, opts("Collapse"))
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.relative_path, opts("Copy Relative Path"))
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"))
-- END_DEFAULT_ON_ATTACH
<
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 api = require('nvim-tree.api')
local api = require("nvim-tree.api")
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
api.config.mappings.default_on_attach(bufnr)
@@ -2423,10 +2425,10 @@ All the following highlight groups can be configured by hand. Aside from
`NvimTreeWindowPicker`, it is not advised to colorize the background of these
groups.
Example |:highlight| >
Example |:highlight| >vim
: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.
To view the nvim-tree highlight groups run |:NvimTreeHiTest|
@@ -2437,16 +2439,18 @@ as per |:highlight|
The `*HL` groups are additive as per |nvim-tree-opts-renderer| precedence.
Only present attributes will clobber each other.
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 NvimTreeModifiedFileHL guisp=cyan gui=undercurl
<
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*
@@ -2601,7 +2605,7 @@ See |nvim-tree-legacy-highlight| for old highlight group compatibility.
- NvimTreeSpecialFile PreProc -> 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([[
:hi NvimTreeExecFile gui=bold guifg=#ffa0a0
@@ -2628,7 +2632,8 @@ to |nvim_tree_registering_handlers| for more information.
Handlers are registered by calling |nvim-tree-api.events.subscribe()|
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 Event = api.events.Event
@@ -2715,7 +2720,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
value of 1.
Example subscription: >
Example subscription: >lua
vim.api.nvim_create_autocmd("User", {
pattern = "NvimTreeRequired",
callback = function(data)
@@ -2756,6 +2762,9 @@ Windows WSL and PowerShell
- Executable file detection is disabled as this is non-performant and can
freeze nvim
- 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*
@@ -2767,7 +2776,7 @@ It interferes with nvim-tree and the intended user experience is nvim-tree
replacing the |netrw| browser.
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_netrwPlugin = 1

View File

@@ -1,6 +1,4 @@
local lib = require("nvim-tree.lib")
local log = require("nvim-tree.log")
local appearance = require("nvim-tree.appearance")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils")
local actions = require("nvim-tree.actions")
@@ -115,27 +113,6 @@ function M.open_on_directory()
actions.root.change_dir.force_dirchange(bufname, true)
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
function M.get_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))
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
create_nvim_tree_autocmd("BufWipeout", {
pattern = "NvimTree_*",
@@ -201,76 +165,9 @@ local function setup_autocommands(opts)
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
create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
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
create_nvim_tree_autocmd("DirChanged", {
callback = function()
@@ -296,20 +193,6 @@ local function setup_autocommands(opts)
create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory })
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
create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*",
@@ -349,20 +232,6 @@ local function setup_autocommands(opts)
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
local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
@@ -551,7 +420,12 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
filesystem_watchers = {
enable = true,
debounce_delay = 50,
ignore_dirs = {},
ignore_dirs = {
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
},
actions = {
use_system_clipboard = true,
@@ -577,6 +451,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
quit_on_open = false,
eject = true,
resize_window = true,
relative_path = true,
window_picker = {
enable = true,
picker = "default",
@@ -616,11 +491,6 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
},
},
experimental = {
actions = {
open_file = {
relative_path = false,
},
},
},
log = {
enable = false,
@@ -800,13 +670,16 @@ local function localise_default_opts()
end
function M.purge_all_state()
require("nvim-tree.watcher").purge_watchers()
view.close_all_tabs()
view.abandon_all_windows()
if core.get_explorer() ~= nil then
local explorer = core.get_explorer()
if explorer then
require("nvim-tree.git").purge_state()
explorer:destroy()
core.reset_explorer()
end
-- purge orphaned that were not destroyed by their nodes
require("nvim-tree.watcher").purge_watchers()
end
---@param conf table|nil
@@ -849,7 +722,8 @@ function M.setup(conf)
require("nvim-tree.keymap").setup(opts)
require("nvim-tree.appearance").setup()
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.utils").setup(opts)
require("nvim-tree.view").setup(opts)
@@ -858,9 +732,6 @@ function M.setup(conf)
require("nvim-tree.buffers").setup(opts)
require("nvim-tree.help").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)

View File

@@ -2,6 +2,8 @@ local log = require("nvim-tree.log")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local DirectoryNode = require("nvim-tree.node.directory")
local Iterator = require("nvim-tree.iterators.node-iterator")
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)
if abs_match or link_match then
if not node.group_next then
node.open = true
end
if #node.nodes == 0 then
core.get_explorer():expand(node)
if node.group_next and incremented_line then
line = line - 1
local dir = node:as(DirectoryNode)
if dir then
if not dir.group_next then
dir.open = true
end
if #dir.nodes == 0 then
core.get_explorer():expand(dir)
if dir.group_next and incremented_line then
line = line - 1
end
end
end
end
end)
: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)
:iterate()

View File

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

View File

@@ -1,11 +1,13 @@
local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events")
local lib = require("nvim-tree.lib")
local core = require("nvim-tree.core")
local notify = require("nvim-tree.notify")
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 = {}
---@param file string
@@ -30,34 +32,21 @@ local function get_num_nodes(iter)
return i
end
---@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
---@param node Node?
function M.fn(node)
local cwd = core.get_cwd()
if cwd == nil then
node = node or core.get_explorer()
if not node then
return
end
node = node and lib.get_last_group_node(node)
if not node or node.name == ".." then
node = {
absolute_path = cwd,
name = "",
nodes = core.get_explorer().nodes,
open = true,
}
local dir = node:is(FileNode) and node.parent or node:as(DirectoryNode)
if not dir then
return
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 = {
prompt = "Create file ",

View File

@@ -5,6 +5,9 @@ local view = require("nvim-tree.view")
local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {
config = {},
}
@@ -89,7 +92,7 @@ end
---@param node Node
function M.remove(node)
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)
if not success then
notify.error("Could not remove " .. notify_node)

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,8 @@ local function pick_win_id()
end
local i = 1
local win_opts = {}
local win_opts_selectable = {}
local win_opts_unselectable = {}
local win_map = {}
local laststatus = vim.o.laststatus
vim.o.laststatus = 2
@@ -89,19 +90,16 @@ local function pick_win_id()
if laststatus == 3 then
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
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
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
win_opts[win_id] = {
win_opts_unselectable[win_id] = {
statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "",
}
-- 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
end
win_opts[id] = {
win_opts_selectable[id] = {
statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "",
}
win_map[char] = id
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("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { 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 })
else
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
i = i + 1
@@ -156,7 +154,7 @@ local function pick_win_id()
-- Restore window options
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
vim.api.nvim_set_option_value(opt, value, { win = id })
else
@@ -169,7 +167,7 @@ local function pick_win_id()
for _, id in ipairs(not_selectable) do
-- Ensure window still exists at this point
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
vim.api.nvim_set_option_value(opt, value, { win = id })
else
@@ -333,9 +331,9 @@ local function open_in_new_window(filename, mode)
local fname
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
fname = vim.fn.fnameescape(filename)
fname = utils.escape_special_chars(vim.fn.fnameescape(filename))
end
local command
@@ -371,29 +369,29 @@ end
---@param mode string
---@param filename string
---@return nil
function M.fn(mode, filename)
local fname = utils.escape_special_chars(filename)
if type(mode) ~= "string" then
mode = ""
end
if mode == "tabnew" then
return open_file_in_tab(fname)
return open_file_in_tab(filename)
end
if mode == "drop" then
return drop(fname)
return drop(filename)
end
if mode == "tab_drop" then
return tab_drop(fname)
return tab_drop(filename)
end
if mode == "edit_in_place" then
return edit_in_current_buf(fname)
return edit_in_current_buf(filename)
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)
if found_win and (mode == "preview" or mode == "preview_no_picker") then
@@ -401,7 +399,7 @@ function M.fn(mode, filename)
end
if not found_win then
open_in_new_window(fname, mode)
open_in_new_window(filename, mode)
else
vim.api.nvim_set_current_win(found_win)
vim.bo.bufhidden = ""
@@ -423,7 +421,7 @@ end
function M.setup(opts)
M.quit_on_open = opts.actions.open_file.quit_on_open
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
opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper()
end

View File

@@ -64,7 +64,7 @@ function M.fn(node)
M.open(node)
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)
M.config = {}
M.config.system_open = opts.system_open or {}

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ local M = {}
M.collapse_all = require("nvim-tree.actions.tree.modifiers.collapse-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)
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,17 @@
local lib = require("nvim-tree.lib")
local core = require("nvim-tree.core")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils")
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 help = require("nvim-tree.help")
local keymap = require("nvim-tree.keymap")
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 Api = {
tree = {},
node = {
@@ -38,44 +41,23 @@ local Api = {
diagnostics = {},
}
--- Print error when setup not called.
--- f function to invoke
---@param f function
---@return fun(...) : any
local function wrap(f)
---Print error when setup not called.
---@param fn fun(...): any
---@return fun(...): any
local function wrap(fn)
return function(...)
if vim.g.NvimTreeSetup == 1 then
return f(...)
return fn(...)
else
notify.error("nvim-tree setup not called")
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.
---Print error when setup not called.
---@param explorer_method string explorer method name
---@return fun(...) : any
---@return fun(...): any
local function wrap_explorer(explorer_method)
return wrap(function(...)
local explorer = core.get_explorer()
@@ -85,11 +67,49 @@ local function wrap_explorer(explorer_method)
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.
---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
---@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)
return wrap(function(...)
local explorer = core.get_explorer()
@@ -135,16 +155,19 @@ Api.tree.change_root = wrap(function(...)
end)
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("..")
elseif node.nodes ~= nil then
actions.root.change_dir.fn(lib.get_last_group_node(node).absolute_path)
else
node = node:as(DirectoryNode)
if node then
actions.root.change_dir.fn(node:last_group_node().absolute_path)
end
end
end)
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_nodes = wrap(lib.get_nodes)
Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor")
Api.tree.get_nodes = wrap_explorer("get_nodes")
---@class ApiTreeFindFileOpts
---@field buf string|number|nil
@@ -158,13 +181,13 @@ Api.tree.find_file = wrap(actions.tree.find_file.fn)
Api.tree.search_node = wrap(actions.finders.search_node.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.toggle_enable_filters = wrap(actions.tree.modifiers.toggles.enable)
Api.tree.toggle_gitignore_filter = wrap(actions.tree.modifiers.toggles.git_ignored)
Api.tree.toggle_git_clean_filter = wrap(actions.tree.modifiers.toggles.git_clean)
Api.tree.toggle_no_buffer_filter = wrap(actions.tree.modifiers.toggles.no_buffer)
Api.tree.toggle_custom_filter = wrap(actions.tree.modifiers.toggles.custom)
Api.tree.toggle_hidden_filter = wrap(actions.tree.modifiers.toggles.dotfiles)
Api.tree.toggle_no_bookmark_filter = wrap(actions.tree.modifiers.toggles.no_bookmark)
Api.tree.toggle_enable_filters = wrap_explorer_member("filters", "toggle")
Api.tree.toggle_gitignore_filter = wrap_explorer_member_args("filters", "toggle", "git_ignored")
Api.tree.toggle_git_clean_filter = wrap_explorer_member_args("filters", "toggle", "git_clean")
Api.tree.toggle_no_buffer_filter = wrap_explorer_member_args("filters", "toggle", "no_buffer")
Api.tree.toggle_custom_filter = wrap_explorer_member_args("filters", "toggle", "custom")
Api.tree.toggle_hidden_filter = wrap_explorer_member_args("filters", "toggle", "dotfiles")
Api.tree.toggle_no_bookmark_filter = wrap_explorer_member_args("filters", "toggle", "no_bookmark")
Api.tree.toggle_help = wrap(help.toggle)
Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf)
@@ -198,23 +221,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"))
---@param mode string
---@param node table
---@param node Node
local function edit(mode, node)
local path = node.absolute_path
if node.link_to and not node.nodes then
path = node.link_to
end
local file_link = node:as(FileLinkNode)
local path = file_link and file_link.link_to or node.absolute_path
actions.node.open_file.fn(mode, path)
end
---@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)
---@param node 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("..")
elseif node.nodes then
lib.expand_or_collapse(node, toggle_group)
elseif dir then
dir:expand_or_collapse(toggle_group)
elseif not toggle_group then
edit(mode, node)
end
@@ -279,7 +305,7 @@ Api.config.mappings.get_keymap = wrap(keymap.get_keymap)
Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default)
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()
return require("nvim-tree.commands").get()

View File

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

View File

@@ -1,3 +1,5 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {}
---@type table<string, boolean> record of which file is modified
@@ -21,18 +23,33 @@ function M.reload_modified()
end
end
---@param node table
---@param node Node
---@return boolean
function M.is_modified(node)
return node
and M.config.modified.enable
and M._modified[node.absolute_path]
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.config.modified.enable then
return false
end
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
---A buffer exists for the node's absolute path
---@param node table
---@param node Node
---@return boolean
function M.is_opened(node)
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 notify = require("nvim-tree.notify")
local view = require("nvim-tree.view")
local log = require("nvim-tree.log")
@@ -15,7 +16,21 @@ function M.init(foldername)
if TreeExplorer then
TreeExplorer:destroy()
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
events._dispatch_ready()
first_init_done = true

View File

@@ -3,6 +3,8 @@ local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view")
local log = require("nvim-tree.log")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {}
---COC severity level strings to LSP severity levels
@@ -125,7 +127,7 @@ end
local function from_cache(node)
local nodepath = uniformize_path(node.absolute_path)
local max_severity = nil
if not node.nodes then
if not node:is(DirectoryNode) then
-- direct cache hit for files
max_severity = NODE_SEVERITIES[nodepath]
else
@@ -165,7 +167,13 @@ function M.update()
end
end
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)
and vim.api.nvim_get_option_value("buflisted", { buf = bufnr })
if should_draw then
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
@@ -184,7 +192,7 @@ function M.get_diag_status(node)
end
-- 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
end
@@ -195,13 +203,15 @@ function M.get_diag_status(node)
node.diag_status = from_cache(node)
end
local dir = node:as(DirectoryNode)
-- file
if not node.nodes then
if not dir then
return node.diag_status
end
-- dir is closed or we should show on open_dirs
if not node.open or M.show_on_open_dirs then
if not dir.open or M.show_on_open_dirs then
return node.diag_status
end
return nil

View File

@@ -1,24 +1,5 @@
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
---@enum FILTER_REASON
M.FILTER_REASON = {

View File

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

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 log = require("nvim-tree.log")
local notify = require("nvim-tree.notify")
local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view")
local watch = require("nvim-tree.explorer.watch")
local explorer_node = require("nvim-tree.explorer.node")
local node_factory = require("nvim-tree.node.factory")
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 NodeIterator = require("nvim-tree.iterators.node-iterator")
local Watcher = require("nvim-tree.watcher")
local Filters = require("nvim-tree.explorer.filters")
local Marks = require("nvim-tree.marks")
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 Renderer = require("nvim-tree.renderer")
@@ -22,78 +25,158 @@ local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
local config
---@class Explorer
---@class (exact) Explorer: RootNode
---@field uid_explorer number vim.loop.hrtime() at construction time
---@field opts table user options
---@field absolute_path string
---@field nodes Node[]
---@field open boolean
---@field watcher Watcher|nil
---@field augroup_id integer
---@field renderer Renderer
---@field filters Filters
---@field live_filter LiveFilter
---@field sorters Sorter
---@field marks Marks
---@field clipboard Clipboard
local Explorer = {}
local Explorer = RootNode:extend()
---@param path string|nil
---@return Explorer|nil
function Explorer:new(path)
local err
---@class Explorer
---@overload fun(args: ExplorerArgs): Explorer
if path then
path, err = vim.loop.fs_realpath(path)
else
path, err = vim.loop.cwd()
end
if not path then
notify.error(err)
return
end
---@class (exact) ExplorerArgs
---@field path string
local o = {
opts = config,
absolute_path = path,
nodes = {},
open = true,
sorters = Sorters:new(config),
}
---@protected
---@param args ExplorerArgs
function Explorer:new(args)
Explorer.super.new(self, {
explorer = self,
absolute_path = args.path,
name = "..",
})
setmetatable(o, self)
self.__index = self
self.uid_explorer = vim.loop.hrtime()
self.augroup_id = vim.api.nvim_create_augroup("NvimTree_Explorer_" .. self.uid_explorer, {})
o.watcher = watch.create_watcher(o)
o.renderer = Renderer:new(config, o)
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)
self.open = true
self.opts = config
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
---@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)
self:_load(node)
end
function Explorer:destroy()
local function iterate(node)
explorer_node.node_destroy(node)
if node.nodes then
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)
---@param node DirectoryNode
---@param project GitProject?
---@return Node[]?
function Explorer:reload(node, project)
local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd)
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 filter_status = self.filters:prepare(git_status)
local filter_status = self.filters:prepare(project)
if node.group_next then
node.nodes = { node.group_next }
@@ -111,16 +194,16 @@ function Explorer:reload(node, git_status)
local remain_childs = {}
local node_ignored = explorer_node.is_git_ignored(node)
local node_ignored = node:is_git_ignored()
---@type table<string, Node>
local nodes_by_path = utils.key_by(node.nodes, "absolute_path")
-- To reset we must 'zero' everything that we use
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
})
@@ -138,32 +221,25 @@ function Explorer:reload(node, git_status)
if filter_reason == FILTER_REASON.none then
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.
if nodes_by_path[abs] then
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)
explorer_node.node_destroy(n)
n:destroy()
nodes_by_path[abs] = nil
end
end
if not nodes_by_path[abs] then
local new_child = nil
if t == "directory" and vim.loop.fs_access(abs, "R") and Watcher.is_fs_event_capable(abs) then
new_child = builders.folder(node, abs, name, stat)
elseif t == "file" then
new_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
new_child = link
end
end
local new_child = node_factory.create({
explorer = self,
parent = node,
absolute_path = abs,
name = name,
fs_stat = stat
})
if new_child then
table.insert(node.nodes, new_child)
nodes_by_path[abs] = new_child
@@ -171,7 +247,7 @@ function Explorer:reload(node, git_status)
else
local n = nodes_by_path[abs]
if n then
n.executable = builders.is_executable(abs) or false
n.executable = utils.is_executable(abs) or false
n.fs_stat = stat
end
end
@@ -185,22 +261,21 @@ function Explorer:reload(node, git_status)
end
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)
if remain_childs[n.absolute_path] then
return remain_childs[n.absolute_path]
else
explorer_node.node_destroy(n)
n:destroy()
return false
end
end, node.nodes)
)
local is_root = not node.parent
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
if config.renderer.group_empty and not is_root and child_folder_only then
node.group_next = child_folder_only
local ns = self:reload(child_folder_only, git_status)
local single_child = node:single_child_directory()
if config.renderer.group_empty and node.parent and single_child then
node.group_next = single_child
local ns = self:reload(single_child, project)
node.nodes = ns or {}
log.profile_end(profile)
return ns
@@ -212,26 +287,6 @@ function Explorer:reload(node, git_status)
return node.nodes
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.
---Groups will be expanded if needed.
---@param path string absolute path
@@ -259,97 +314,51 @@ function Explorer:refresh_parent_nodes_for_path(path)
local project = git.get_project(toplevel) or {}
self:reload(node, project)
self:update_parent_statuses(node, project, toplevel)
git.update_parent_projects(node, project, toplevel)
end
log.profile_end(profile)
end
---@private
---@param node Node
---@param node DirectoryNode
function Explorer:_load(node)
local cwd = node.link_to or node.absolute_path
local git_status = git.load_project_status(cwd)
self:explore(node, git_status, self)
local project = git.load_project(cwd)
self:explore(node, project, self)
end
---@private
---@param nodes_by_path table
---@param nodes_by_path Node[]
---@param node_ignored boolean
---@param status table|nil
---@return fun(node: Node): table
function Explorer:update_status(nodes_by_path, node_ignored, status)
---@param project GitProject?
---@return fun(node: Node): Node
function Explorer:update_git_statuses(nodes_by_path, node_ignored, project)
return function(node)
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
return node
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
---@param handle uv.uv_fs_t
---@param cwd string
---@param node Node
---@param git_status table
---@param node DirectoryNode
---@param project GitProject
---@param parent Explorer
function Explorer:populate_children(handle, cwd, node, git_status, parent)
local node_ignored = explorer_node.is_git_ignored(node)
function Explorer:populate_children(handle, cwd, node, project, parent)
local node_ignored = node:is_git_ignored()
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 {}, {
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
git = 0,
buf = 0,
dotfile = 0,
custom = 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 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
-- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility
local t = stat and stat.type or nil
local child = nil
if t == "directory" and vim.loop.fs_access(abs, "R") then
child = builders.folder(node, abs, name, stat)
elseif t == "file" then
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
local child = node_factory.create({
explorer = self,
parent = node,
absolute_path = abs,
name = name,
fs_stat = stat
})
if child then
table.insert(node.nodes, child)
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
else
for reason, value in pairs(FILTER_REASON) do
@@ -400,11 +403,11 @@ function Explorer:populate_children(handle, cwd, node, git_status, parent)
end
---@private
---@param node Node
---@param status table
---@param node DirectoryNode
---@param project GitProject
---@param parent Explorer
---@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 handle = vim.loop.fs_scandir(cwd)
if not handle then
@@ -413,15 +416,15 @@ function Explorer:explore(node, status, parent)
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 child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
if config.renderer.group_empty and not is_root and child_folder_only then
local child_cwd = child_folder_only.link_to or child_folder_only.absolute_path
local child_status = git.load_project_status(child_cwd)
node.group_next = child_folder_only
local ns = self:explore(child_folder_only, child_status, parent)
local single_child = node:single_child_directory()
if config.renderer.group_empty and not is_root and single_child then
local child_cwd = single_child.link_to or single_child.absolute_path
local child_project = git.load_project(child_cwd)
node.group_next = single_child
local ns = self:explore(single_child, child_project, parent)
node.nodes = ns or {}
log.profile_end(profile)
@@ -436,13 +439,14 @@ function Explorer:explore(node, status, parent)
end
---@private
---@param projects table
---@param projects GitProject[]
function Explorer:refresh_nodes(projects)
Iterator.builder({ self })
:applier(function(n)
if n.nodes then
local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path)
self:reload(n, projects[toplevel] or {})
local dir = n:as(DirectoryNode)
if dir then
local toplevel = git.get_toplevel(dir.cwd or dir.link_to or dir.absolute_path)
self:reload(dir, projects[toplevel] or {})
end
end)
:recursor(function(n)
@@ -458,7 +462,7 @@ function Explorer:reload_explorer()
end
event_running = true
local projects = git.reload()
local projects = git.reload_all_projects()
self:refresh_nodes(projects)
if view.is_visible() then
self.renderer:draw()
@@ -472,16 +476,67 @@ function Explorer:reload_git()
end
event_running = true
local projects = git.reload()
explorer_node.reload_node_status(self, projects)
local projects = git.reload_all_projects()
git.reload_node_status(self, projects)
self.renderer:draw()
event_running = false
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 Node
function Explorer:get_nodes()
return self:clone()
end
function Explorer:setup(opts)
config = opts
require("nvim-tree.explorer.node").setup(opts)
require("nvim-tree.explorer.watch").setup(opts)
end
return Explorer

View File

@@ -1,29 +1,33 @@
local view = require("nvim-tree.view")
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 prefix string
---@field always_show_folders boolean
---@field filter string
local LiveFilter = {}
local LiveFilter = Class:extend()
---@param opts table
---@param explorer Explorer
function LiveFilter:new(opts, explorer)
local o = {
explorer = explorer,
prefix = opts.live_filter.prefix,
always_show_folders = opts.live_filter.always_show_folders,
filter = nil,
}
setmetatable(o, self)
self.__index = self
return o
---@class LiveFilter
---@overload fun(args: LiveFilterArgs): LiveFilter
---@class (exact) LiveFilterArgs
---@field explorer Explorer
---@protected
---@param args LiveFilterArgs
function LiveFilter:new(args)
self.explorer = args.explorer
self.prefix = self.explorer.opts.live_filter.prefix
self.always_show_folders = self.explorer.opts.live_filter.always_show_folders
self.filter = nil
end
---@param node_ Node|nil
---@param node_ Node?
local function reset_filter(self, node_)
node_ = node_ or self.explorer
@@ -31,17 +35,19 @@ local function reset_filter(self, node_)
return
end
node_.hidden_stats = vim.tbl_deep_extend("force", node_.hidden_stats or {}, {
live_filter = 0,
})
local dir_ = node_:as(DirectoryNode)
if dir_ then
dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, })
end
Iterator.builder(node_.nodes)
:hidden()
:applier(function(node)
node.hidden = false
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
live_filter = 0,
})
local dir = node:as(DirectoryNode)
if dir then
dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, })
end
end)
:iterate()
end
@@ -76,7 +82,7 @@ end
---@param node Node
---@return boolean
local function matches(self, node)
if not self.explorer.filters.config.enable then
if not self.explorer.filters.enabled then
return true
end
@@ -85,14 +91,14 @@ local function matches(self, node)
return vim.regex(self.filter):match_str(name) ~= nil
end
---@param node_ Node|nil
---@param node_ DirectoryNode?
function LiveFilter:apply_filter(node_)
if not self.filter or self.filter == "" then
reset_filter(self, node_)
return
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
local function iterate(node)
local filtered_nodes = 0
@@ -163,21 +169,21 @@ local function create_overlay(self)
if view.View.float.enable then
-- don't close nvim-tree float when focus is changed to filter window
vim.api.nvim_clear_autocmds({
event = "WinLeave",
event = "WinLeave",
pattern = "NvimTree_*",
group = vim.api.nvim_create_augroup("NvimTree", { clear = false }),
group = vim.api.nvim_create_augroup("NvimTree", { clear = false }),
})
end
configure_buffer_overlay(self)
overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, {
col = 1,
row = 0,
col = 1,
row = 0,
relative = "cursor",
width = calculate_overlay_win_width(self),
height = 1,
border = "none",
style = "minimal",
width = calculate_overlay_win_width(self),
height = 1,
border = "none",
style = "minimal",
})
if vim.fn.has("nvim-0.10") == 1 then
@@ -192,7 +198,7 @@ local function create_overlay(self)
end
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.explorer.renderer:draw()
@@ -206,7 +212,7 @@ function LiveFilter:start_filtering()
end
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
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
local Sorter = {}
---@overload fun(args: SorterArgs): Sorter
function Sorter:new(opts)
local o = {}
setmetatable(o, self)
self.__index = self
o.config = vim.deepcopy(opts.sort)
---@class (exact) SorterArgs
---@field explorer Explorer
if type(o.config.sorter) == "function" then
o.user = o.config.sorter
end
return o
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
---@protected
---@param args SorterArgs
function Sorter:new(args)
self.explorer = args.explorer
end
---Create a shallow copy of a portion of a list.
@@ -38,30 +36,32 @@ local function tbl_slice(t, first, last)
return slice
end
---Evaluate `sort.folders_first` and `sort.files_first`
---@param a Node
---@param b Node
---@return boolean|nil
local function folders_or_files_first(a, b, cfg)
if not (cfg.folders_first or cfg.files_first) then
return
---Evaluate folders_first and sort.files_first returning nil when no order is necessary
---@private
---@type SorterComparator
function Sorter:folders_or_files_first(a, b)
if not (self.explorer.opts.sort.folders_first or self.explorer.opts.sort.files_first) then
return nil
end
if not a.nodes and b.nodes then
if not a:is(DirectoryNode) and b:is(DirectoryNode) then
-- file <> folder
return cfg.files_first
elseif a.nodes and not b.nodes then
return self.explorer.opts.sort.files_first
elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then
-- folder <> file
return not cfg.files_first
return not self.explorer.opts.sort.files_first
end
return nil
end
---@param t table
---@private
---@param t Node[]
---@param first number
---@param mid number
---@param last number
---@param comparator fun(a: Node, b: Node): boolean
local function merge(t, first, mid, last, comparator)
---@param comparator SorterComparator
function Sorter:merge(t, first, mid, last, comparator)
local n1 = mid - first + 1
local n2 = last - mid
local ls = tbl_slice(t, first, mid)
@@ -71,7 +71,7 @@ local function merge(t, first, mid, last, comparator)
local k = first
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]
i = i + 1
else
@@ -94,45 +94,49 @@ local function merge(t, first, mid, last, comparator)
end
end
---@param t table
---@private
---@param t Node[]
---@param first number
---@param last number
---@param comparator fun(a: Node, b: Node): boolean
local function split_merge(t, first, last, comparator)
---@param comparator SorterComparator
function Sorter:split_merge(t, first, last, comparator)
if (last - first) < 1 then
return
end
local mid = math.floor((first + last) / 2)
split_merge(t, first, mid, comparator)
split_merge(t, mid + 1, last, comparator)
merge(t, first, mid, last, comparator)
self:split_merge(t, first, mid, comparator)
self:split_merge(t, mid + 1, last, comparator)
self:merge(t, first, mid, last, comparator)
end
---Perform a merge sort using sorter option.
---@param t table nodes
---@param t Node[]
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 origin_index = {}
for _, n in ipairs(t) do
table.insert(t_user, {
absolute_path = n.absolute_path,
executable = n.executable,
extension = n.extension,
filetype = vim.filetype.match({ filename = n.name }),
link_to = n.link_to,
name = n.name,
type = n.type,
executable = n.executable,
extension = n.extension,
filetype = vim.filetype.match({ filename = n.name }),
link_to = n.link_to,
name = n.name,
type = n.type,
})
table.insert(origin_index, n)
end
local predefined = self.user(t_user)
if predefined then
split_merge(t, 1, #t, self:get_comparator(predefined))
-- user may return a SorterType
local ret = self.explorer.opts.sort.sorter(t_user)
if self[ret] then
self:split_merge(t, 1, #t, self[ret])
return
end
@@ -145,7 +149,7 @@ function Sorter:sort(t)
end
-- 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 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)
end
split_merge(t, 1, #t, mini_comparator) -- sort by user order
else
split_merge(t, 1, #t, self:get_comparator(self.config.sorter))
self:split_merge(t, 1, #t, mini_comparator) -- sort by user order
end
end
---@private
---@param a Node
---@param b Node
---@param ignorecase boolean|nil
---@param ignore_case 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
return true
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
return early_return
end
if ignorecase then
if ignore_case then
return a.name:lower() <= b.name:lower()
else
return a.name <= b.name
end
end
function C.case_sensitive(a, b, cfg)
return node_comparator_name_ignorecase_or_not(a, b, false, cfg)
---@private
---@type SorterComparator
function Sorter:case_sensitive(a, b)
return self:name_case(a, b, false)
end
function C.name(a, b, cfg)
return node_comparator_name_ignorecase_or_not(a, b, true, cfg)
---@private
---@type SorterComparator
function Sorter:name(a, b)
return self:name_case(a, b, true)
end
function C.modification_time(a, b, cfg)
---@private
---@type SorterComparator
function Sorter:modification_time(a, b)
if not (a and b) then
return true
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
return early_return
end
@@ -214,17 +223,19 @@ function C.modification_time(a, b, cfg)
return last_modified_b <= last_modified_a
end
function C.suffix(a, b, cfg)
---@private
---@type SorterComparator
function Sorter:suffix(a, b)
if not (a and b) then
return true
end
-- 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
return early_return
elseif a.nodes and b.nodes then
return C.name(a, b, cfg)
return self:name(a, b)
end
-- 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
return false
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
-- unsuffixed go third
@@ -245,7 +256,7 @@ function C.suffix(a, b, cfg)
elseif a_suffix_ndx and not b_suffix_ndx then
return false
elseif not (a_suffix_ndx and b_suffix_ndx) then
return C.name(a, b, cfg)
return self:name(a, b)
end
-- finally, compare by suffixes
@@ -257,18 +268,20 @@ function C.suffix(a, b, cfg)
elseif not a_suffix and b_suffix then
return false
elseif a_suffix:lower() == b_suffix:lower() then
return C.name(a, b, cfg)
return self:name(a, b)
end
return a_suffix:lower() < b_suffix:lower()
end
function C.extension(a, b, cfg)
---@private
---@type SorterComparator
function Sorter:extension(a, b)
if not (a and b) then
return true
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
return early_return
end
@@ -282,18 +295,20 @@ function C.extension(a, b, cfg)
local a_ext = (a.extension or ""):lower()
local b_ext = (b.extension or ""):lower()
if a_ext == b_ext then
return C.name(a, b, cfg)
return self:name(a, b)
end
return a_ext < b_ext
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 b_ft = vim.filetype.match({ filename = b.name })
-- 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
return early_return
end
@@ -307,7 +322,7 @@ function C.filetype(a, b, cfg)
-- same filetype or both nil, sort by name
if a_ft == b_ft then
return C.name(a, b, cfg)
return self:name(a, b)
end
return a_ft < b_ft

View File

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

View File

@@ -1,21 +1,48 @@
local log = require("nvim-tree.log")
local utils = require("nvim-tree.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 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 = {
config = {},
-- all projects keyed by toplevel
---all projects keyed by toplevel
---@type table<string, GitProject>
_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 = {},
-- git dirs by toplevel
---@type table<string, string>
_git_dirs_by_toplevel = {},
}
@@ -31,35 +58,35 @@ local WATCHED_FILES = {
---@param toplevel string|nil
---@param path string|nil
---@param project table
---@param git_status table|nil
local function reload_git_status(toplevel, path, project, git_status)
---@param project GitProject
---@param project_files GitProjectFiles?
local function reload_git_project(toplevel, path, project, project_files)
if path then
for p in pairs(project.files) do
if p:find(path, 1, true) == 1 then
project.files[p] = nil
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
project.files = git_status
project.files = project_files or {}
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
--- Is this path in a known ignored directory?
---@param path string
---@param project table git status
---@param project GitProject
---@return boolean
local function path_ignored_in_project(path, project)
if not path or not project then
return false
end
if project and project.files then
for file, status in pairs(project.files) do
if status == "!!" and vim.startswith(path, file) then
if project.files then
for p, xy in pairs(project.files) do
if xy == "!!" and vim.startswith(path, p) then
return true
end
end
@@ -67,9 +94,8 @@ local function path_ignored_in_project(path, project)
return false
end
--- Reload all projects
---@return table projects maybe empty
function M.reload()
---@return GitProject[] maybe empty
function M.reload_all_projects()
if not M.config.git.enable then
return {}
end
@@ -82,11 +108,12 @@ function M.reload()
end
--- Reload one project. Does nothing when no project or path is ignored
---@param toplevel string|nil
---@param path string|nil optional path to update only
---@param callback function|nil
---@param toplevel string?
---@param path string? optional path to update only
---@param callback function?
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 callback then
callback()
@@ -101,29 +128,31 @@ function M.reload_project(toplevel, path, callback)
return
end
local opts = {
toplevel = toplevel,
path = path,
---@type GitRunnerArgs
local args = {
toplevel = toplevel,
path = path,
list_untracked = git_utils.should_show_untracked(toplevel),
list_ignored = true,
timeout = M.config.git.timeout,
list_ignored = true,
timeout = M.config.git.timeout,
}
if callback then
Runner.run(opts, function(git_status)
reload_git_status(toplevel, path, project, git_status)
---@param path_xy GitPathXY
args.callback = function(path_xy)
reload_git_project(toplevel, path, project, path_xy)
callback()
end)
end
GitRunner:run(args)
else
-- TODO use callback once async/await is available
local git_status = Runner.run(opts)
reload_git_status(toplevel, path, project, git_status)
-- TODO #1974 use callback once async/await is available
reload_git_project(toplevel, path, project, GitRunner:run(args))
end
end
--- Retrieve a known project
---@param toplevel string|nil
---@return table|nil project
---@param toplevel string?
---@return GitProject? project
function M.get_project(toplevel)
return M._projects_by_toplevel[toplevel]
end
@@ -144,11 +173,10 @@ function M.get_toplevel(path)
return nil
end
if M._toplevels_by_path[path] then
return M._toplevels_by_path[path]
end
if M._toplevels_by_path[path] == false then
local tl = M._toplevels_by_path[path]
if tl then
return tl
elseif tl == false then
return nil
end
@@ -187,8 +215,15 @@ function M.get_toplevel(path)
end
M._toplevels_by_path[path] = toplevel
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
local function reload_tree_at(toplevel)
@@ -203,31 +238,28 @@ local function reload_tree_at(toplevel)
end
M.reload_project(toplevel, nil, function()
local git_status = M.get_project(toplevel)
local project = M.get_project(toplevel)
Iterator.builder(root_node.nodes)
:hidden()
:applier(function(node)
local parent_ignored = explorer_node.is_git_ignored(node.parent)
explorer_node.update_git_status(node, parent_ignored, git_status)
local parent_ignored = node.parent and node.parent:is_git_ignored() or false
node:update_git_status(parent_ignored, project)
end)
:recursor(function(node)
return node.nodes and #node.nodes > 0 and node.nodes
end)
:iterate()
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
explorer.renderer:draw()
end
root_node.explorer.renderer:draw()
end)
end
--- Load the project status for a path. Does nothing when no toplevel for path.
--- Only fetches project status when unknown, otherwise returns existing.
---@param path string absolute
---@return table project maybe empty
function M.load_project_status(path)
---@return GitProject maybe empty
function M.load_project(path)
if not M.config.git.enable then
return {}
end
@@ -238,42 +270,48 @@ function M.load_project_status(path)
return {}
end
local status = M._projects_by_toplevel[toplevel]
if status then
return status
local project = M._projects_by_toplevel[toplevel]
if project then
return project
end
local git_status = Runner.run({
toplevel = toplevel,
local path_xys = GitRunner:run({
toplevel = toplevel,
list_untracked = git_utils.should_show_untracked(toplevel),
list_ignored = true,
timeout = M.config.git.timeout,
list_ignored = true,
timeout = M.config.git.timeout,
})
local watcher = nil
if M.config.filesystem_watchers.enable then
log.line("watcher", "git start")
---@param w Watcher
local callback = function(w)
log.line("watcher", "git event scheduled '%s'", w.toplevel)
utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function()
log.line("watcher", "git event scheduled '%s'", w.data.toplevel)
utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function()
if w.destroyed then
return
end
reload_tree_at(w.toplevel)
reload_tree_at(w.data.toplevel)
end)
end
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, {
toplevel = toplevel,
watcher = Watcher:create({
path = git_dir,
files = WATCHED_FILES,
callback = callback,
data = {
toplevel = toplevel,
}
})
end
if git_status then
if path_xys then
M._projects_by_toplevel[toplevel] = {
files = git_status,
dirs = git_utils.file_status_to_dir_status(git_status, toplevel),
files = path_xys,
dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel),
watcher = watcher,
}
return M._projects_by_toplevel[toplevel]
@@ -283,6 +321,71 @@ function M.load_project_status(path)
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()
log.line("git", "purge_state")

View File

@@ -2,17 +2,51 @@ local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils")
local notify = require("nvim-tree.notify")
---@class Runner
local Runner = {}
Runner.__index = Runner
local Class = require("nvim-tree.classic")
---@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 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
---@param status string
---@param path string|nil
function Runner:_parse_status_output(status, path)
function GitRunner:parse_status_output(status, path)
if not path then
return
end
@@ -22,7 +56,7 @@ function Runner:_parse_status_output(status, path)
path = path:gsub("/", "\\")
end
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
@@ -30,7 +64,7 @@ end
---@param prev_output string
---@param incoming 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
local prev = prev_output .. incoming
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 = true
end
self:_parse_status_output(status, path)
self:parse_status_output(status, path)
end
i = i + #line
end
@@ -58,35 +92,38 @@ function Runner:_handle_incoming_data(prev_output, incoming)
end
for line in prev_output:gmatch("[^\n]*\n") do
self:_parse_status_output(line)
self:parse_status_output(line)
end
return ""
end
---@private
---@param stdout_handle uv.uv_pipe_t
---@param stderr_handle uv.uv_pipe_t
---@return table
function Runner:_getopts(stdout_handle, stderr_handle)
---@return uv.spawn.options
function GitRunner:get_spawn_options(stdout_handle, stderr_handle)
local untracked = self.list_untracked and "-u" or nil
local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no"
return {
args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path },
cwd = self.toplevel,
args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path },
cwd = self.toplevel,
stdio = { nil, stdout_handle, stderr_handle },
}
end
---@private
---@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
log.raw("git", "%s", output)
log.line("git", "done")
end
end
---@private
---@param callback function|nil
function Runner:_run_git_job(callback)
function GitRunner:run_git_job(callback)
local handle, pid
local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false)
@@ -123,13 +160,13 @@ function Runner:_run_git_job(callback)
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", "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(
"git",
opts,
spawn_options,
vim.schedule_wrap(function(rc)
on_finish(rc)
end)
@@ -151,19 +188,20 @@ function Runner:_run_git_job(callback)
if data then
data = data:gsub("%z", "\n")
end
self:_log_raw_output(data)
output_leftover = self:_handle_incoming_data(output_leftover, data)
self:log_raw_output(data)
output_leftover = self:handle_incoming_data(output_leftover, data)
end
local function manage_stderr(_, data)
self:_log_raw_output(data)
self:log_raw_output(data)
end
vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout))
vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr))
end
function Runner:_wait()
---@private
function GitRunner:wait()
local function is_done()
return self.rc ~= nil
end
@@ -172,64 +210,64 @@ function Runner:_wait()
end
end
---@param opts table
function Runner:_finalise(opts)
---@private
function GitRunner:finalise()
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
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()
end
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
log.line("git", "job success %s %s", opts.toplevel, opts.path)
log.line("git", "job success %s %s", self.toplevel, self.path)
end
end
--- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
---@param opts table
---@param callback function|nil executed passing return when complete
---@return table|nil status by absolute path, nil if callback present
function Runner.run(opts, callback)
local self = setmetatable({
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)
---Return nil when callback present
---@private
---@return GitPathXY?
function GitRunner:execute()
local async = self.callback ~= nil
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.toplevel, self.path)
local async = callback ~= nil
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path)
if async and callback then
if async and self.callback then
-- async, always call back
self:_run_git_job(function()
self:run_git_job(function()
log.profile_end(profile)
self:_finalise(opts)
self:finalise()
callback(self.output)
self.callback(self.path_xy)
end)
else
-- sync, maybe call back
self:_run_git_job()
self:_wait()
self:run_git_job()
self:wait()
log.profile_end(profile)
self:_finalise(opts)
self:finalise()
if callback then
callback(self.output)
if self.callback then
self.callback(self.path_xy)
else
return self.output
return self.path_xy
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
end
---@type table<string, boolean>
local untracked = {}
---@param cwd string
---@return string|nil
---@return boolean
function M.should_show_untracked(cwd)
if untracked[cwd] ~= nil then
return untracked[cwd]
@@ -81,8 +82,8 @@ function M.should_show_untracked(cwd)
return untracked[cwd]
end
---@param t table|nil
---@param k string
---@param t table<string|integer, boolean>?
---@param k string|integer
---@return table
local function nil_insert(t, k)
t = t or {}
@@ -90,31 +91,33 @@ local function nil_insert(t, k)
return t
end
---@param status table
---@param project_files GitProjectFiles
---@param cwd string|nil
---@return table
function M.file_status_to_dir_status(status, cwd)
local direct = {}
for p, s in pairs(status) do
---@return GitProjectDirs
function M.project_files_to_project_dirs(project_files, cwd)
---@type GitProjectDirs
local project_dirs = {}
project_dirs.direct = {}
for p, s in pairs(project_files) do
if s ~= "!!" then
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
local indirect = {}
for dirname, statuses in pairs(direct) do
project_dirs.indirect = {}
for dirname, statuses in pairs(project_dirs.direct) do
for s, _ in pairs(statuses) do
local modified = dirname
while modified ~= cwd and modified ~= "/" do
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
local r = { indirect = indirect, direct = direct }
for _, d in pairs(r) do
for _, d in pairs(project_dirs) do
for dirname, statuses in pairs(d) do
local new_statuses = {}
for s, _ in pairs(statuses) do
@@ -123,7 +126,60 @@ function M.file_status_to_dir_status(status, cwd)
d[dirname] = new_statuses
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
function M.setup(opts)

View File

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

View File

@@ -1,7 +1,7 @@
local M = {}
--- 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
local function generate_keymap(fn)
-- create an unlisted scratch buffer
@@ -29,14 +29,6 @@ function M.get_keymap_default()
return generate_keymap(M.default_on_attach)
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
function M.default_on_attach(bufnr)
local api = require("nvim-tree.api")
@@ -51,68 +43,74 @@ function M.default_on_attach(bufnr)
}
end
-- formatting cannot be re-enabled, hence this is at the end
---@format disable
-- BEGIN_DEFAULT_ON_ATTACH
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-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-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-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', '<CR>', api.node.open.edit, opts('Open'))
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.prev, opts('Previous Sibling'))
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', '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', 'bt', api.marks.bulk.trash, opts('Trash 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', '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.node.navigate.git.prev, opts('Prev 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.trash, opts('Trash'))
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.node.navigate.diagnostics.next, opts('Next Diagnostic'))
vim.keymap.set('n', '[e', api.node.navigate.diagnostics.prev, opts('Prev Diagnostic'))
vim.keymap.set('n', 'F', api.live_filter.clear, opts('Live Filter: Clear'))
vim.keymap.set('n', 'f', api.live_filter.start, opts('Live Filter: Start'))
vim.keymap.set('n', 'g?', api.tree.toggle_help, opts('Help'))
vim.keymap.set('n', 'gy', api.fs.copy.absolute_path, opts('Copy Absolute Path'))
vim.keymap.set('n', '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', 'I', api.tree.toggle_gitignore_filter, opts('Toggle Filter: Git Ignore'))
vim.keymap.set('n', 'J', api.node.navigate.sibling.last, opts('Last Sibling'))
vim.keymap.set('n', 'K', api.node.navigate.sibling.first, opts('First Sibling'))
vim.keymap.set('n', '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.marks.toggle, opts('Toggle Bookmark'))
vim.keymap.set('n', 'o', api.node.open.edit, opts('Open'))
vim.keymap.set('n', 'O', api.node.open.no_window_picker, opts('Open: No Window Picker'))
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', 'q', api.tree.close, opts('Close'))
vim.keymap.set('n', 'r', api.fs.rename, opts('Rename'))
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.tree.search_node, opts('Search'))
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', 'W', api.tree.collapse_all, opts('Collapse'))
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.relative_path, opts('Copy Relative Path'))
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", "<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-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-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-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", "<CR>", api.node.open.edit, opts("Open"))
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.prev, opts("Previous Sibling"))
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", "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", "bt", api.marks.bulk.trash, opts("Trash 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", "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.node.navigate.git.prev, opts("Prev 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.trash, opts("Trash"))
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.node.navigate.diagnostics.next, opts("Next Diagnostic"))
vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic"))
vim.keymap.set("n", "F", api.live_filter.clear, opts("Live Filter: Clear"))
vim.keymap.set("n", "f", api.live_filter.start, opts("Live Filter: Start"))
vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help"))
vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path"))
vim.keymap.set("n", "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", "I", api.tree.toggle_gitignore_filter, opts("Toggle Filter: Git Ignore"))
vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling"))
vim.keymap.set("n", "K", api.node.navigate.sibling.first, opts("First Sibling"))
vim.keymap.set("n", "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.marks.toggle, opts("Toggle Bookmark"))
vim.keymap.set("n", "o", api.node.open.edit, opts("Open"))
vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker"))
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", "q", api.tree.close, opts("Close"))
vim.keymap.set("n", "r", api.fs.rename, opts("Rename"))
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.tree.search_node, opts("Search"))
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", "W", api.tree.collapse_all, opts("Collapse"))
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.relative_path, opts("Copy Relative Path"))
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"))
-- END_DEFAULT_ON_ATTACH
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

View File

@@ -6,12 +6,12 @@ local M = {}
-- silently move, please add to help nvim-tree-legacy-opts
local function refactored(opts)
-- 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_cwd", opts, "", "sync_root_with_cwd", 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)
-- 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", "close", true)
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, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true)
-- 2022/11/22

View File

@@ -1,9 +1,7 @@
local view = require("nvim-tree.view")
local core = require("nvim-tree.core")
local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify")
local explorer_node = require("nvim-tree.explorer.node")
---@class LibOpenOpts
---@field path string|nil path
@@ -14,173 +12,6 @@ local M = {
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()
local id = vim.api.nvim_get_current_win()
local tree_id = view.get_winnr()
@@ -234,7 +65,7 @@ end
---@param items_short string[]
---@param items_long string[]
---@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)
local function format_item(short)
for i, s in ipairs(items_short) do

View File

@@ -1,7 +1,12 @@
local M = {
config = nil,
path = nil,
}
---@alias LogTypes "all" | "config" | "copy_paste" | "dev" | "diagnostics" | "git" | "profile" | "watcher"
---@type table<LogTypes, boolean>
local types = {}
---@type string
local file_path
local M = {}
--- Write to log file
---@param typ string as per log.types config
@@ -13,7 +18,26 @@ function M.raw(typ, fmt, ...)
end
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
io.output(file)
io.write(line)
@@ -52,7 +76,7 @@ end
--- Write to log file
--- 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 ... any arguments for string.format
function M.line(typ, fmt, ...)
@@ -69,33 +93,31 @@ function M.set_inspect_opts(opts)
end
--- Write to log file the inspection of a node
--- defaults to the node under cursor if none is provided
---@param typ string as per log.types config
---@param node table|nil node to be inspected
---@param typ LogTypes as per log.types config
---@param node Node node to be inspected
---@param fmt string for string.format
---@vararg any arguments for string.format
---@param ... any arguments for string.format
function M.node(typ, node, fmt, ...)
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)), ...)
end
end
--- 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
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
function M.setup(opts)
M.config = opts.log
if M.config and M.config.enable and M.config.types then
M.path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER)
if M.config.truncate then
os.remove(M.path)
if opts.log and opts.log.enable and opts.log.types then
types = opts.log.types
file_path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER)
if opts.log.truncate then
os.remove(file_path)
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

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 utils = require("nvim-tree.utils")
---@class Marks
---@field config table hydrated user opts.filters
local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) Marks: Class
---@field private explorer Explorer
---@field private marks table<string, Node> by absolute path
local Marks = {}
local Marks = Class:extend()
---@return Marks
---@param opts table user options
---@param explorer Explorer
function Marks:new(opts, explorer)
local o = {
explorer = explorer,
config = {
ui = opts.ui,
filesystem_watchers = opts.filesystem_watchers,
},
marks = {},
}
---@class Marks
---@overload fun(args: MarksArgs): Marks
setmetatable(o, self)
self.__index = self
return o
---@class (exact) MarksArgs
---@field explorer Explorer
---@protected
---@param args MarksArgs
function Marks:new(args)
self.explorer = args.explorer
self.marks = {}
end
---Clear all marks and reload if watchers disabled
---@private
function Marks:clear_reload()
self:clear()
if not self.config.filesystem_watchers.enable then
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
end
end
@@ -98,7 +96,7 @@ function Marks:bulk_delete()
self:clear_reload()
end
if self.config.ui.confirm.remove then
if self.explorer.opts.ui.confirm.remove then
local prompt_select = "Remove bookmarked ?"
local prompt_input = prompt_select .. " y/N: "
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()
end
if self.config.ui.confirm.trash then
if self.explorer.opts.ui.confirm.trash then
local prompt_select = "Trash bookmarked ?"
local prompt_input = prompt_select .. " y/N: "
lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short)
@@ -149,10 +147,10 @@ function Marks:bulk_move()
return
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()
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
elseif node_at_cursor and node_at_cursor.parent then
default_path = node_at_cursor.parent.absolute_path
@@ -188,7 +186,7 @@ end
---@private
---@param up boolean
function Marks:navigate(up)
local node = lib.get_node_at_cursor()
local node = self.explorer:get_node_at_cursor()
if not node then
return
end
@@ -198,7 +196,8 @@ function Marks:navigate(up)
Iterator.builder(self.explorer.nodes)
:recursor(function(n)
return n.open and n.nodes
local dir = n:as(DirectoryNode)
return dir and dir.open and dir.nodes
end)
:applier(function(n)
if n.absolute_path == node.absolute_path then
@@ -261,7 +260,7 @@ function Marks:navigate_select()
return
end
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)
elseif node then
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,86 @@
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.
---@return DirectoryLinkNode cloned
function DirectoryLinkNode:clone()
local clone = DirectoryNode.clone(self) --[[@as 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,291 @@
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.
---@return DirectoryNode cloned
function DirectoryNode:clone()
local clone = Node.clone(self) --[[@as DirectoryNode]]
clone.has_children = self.has_children
clone.group_next = nil
clone.nodes = {}
clone.open = self.open
clone.hidden_stats = nil
for _, child in ipairs(self.nodes) do
table.insert(clone.nodes, child:clone())
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,71 @@
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
---@return FileLinkNode cloned
function FileLinkNode:clone()
local clone = FileNode.clone(self) --[[@as FileLinkNode]]
clone.link_to = self.link_to
clone.fs_stat_target = self.fs_stat_target
return clone
end
return FileLinkNode

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

@@ -0,0 +1,106 @@
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
---@return FileNode cloned
function FileNode:clone()
local clone = Node.clone(self) --[[@as FileNode]]
clone.extension = self.extension
return clone
end
return FileNode

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

@@ -0,0 +1,144 @@
local Class = require("nvim-tree.classic")
---Abstract Node class.
---@class (exact) Node: Class
---@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.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 icon
function Node:highlighted_name()
return self:highlighted_name_empty()
end
---Create a sanitized partial copy of a node, populating children recursively.
---@return Node cloned
function Node:clone()
---@type Explorer
local explorer_placeholder = nil
---@type Node
local clone = {
type = self.type,
explorer = explorer_placeholder,
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_status = nil,
is_dot = self.is_dot,
}
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 protected 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,25 @@
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
return RootNode

View File

@@ -2,6 +2,9 @@ local notify = require("nvim-tree.notify")
local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view")
local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory")
local DecoratorBookmarks = require("nvim-tree.renderer.decorator.bookmarks")
local DecoratorCopied = require("nvim-tree.renderer.decorator.copied")
local DecoratorCut = require("nvim-tree.renderer.decorator.cut")
@@ -12,16 +15,6 @@ local DecoratorHidden = require("nvim-tree.renderer.decorator.hidden")
local DecoratorOpened = require("nvim-tree.renderer.decorator.opened")
local pad = require("nvim-tree.renderer.components.padding")
local icons = require("nvim-tree.renderer.components.icons")
local PICTURE_MAP = {
jpg = true,
jpeg = true,
png = true,
gif = true,
webp = true,
jxl = true,
}
---@class (exact) HighlightedString
---@field str string
@@ -34,7 +27,6 @@ local PICTURE_MAP = {
---@field col_end number
---@class (exact) Builder
---@field private __index? table
---@field lines string[] includes icons etc.
---@field hl_args AddHighlightArgs[] line highlights
---@field signs string[] line signs
@@ -47,43 +39,39 @@ local PICTURE_MAP = {
---@field private markers boolean[] indent markers
---@field private decorators Decorator[]
---@field private hidden_display fun(node: Node): string|nil
local Builder = {}
local Builder = Class:extend()
---@param opts table user options
---@param explorer Explorer
---@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),
---@class Builder
---@overload fun(args: BuilderArgs): Builder
---@class (exact) BuilderArgs
---@field explorer Explorer
---@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 = {
-- priority order
DecoratorCut({ explorer = args.explorer }),
DecoratorCopied({ explorer = args.explorer }),
DecoratorDiagnostics({ explorer = args.explorer }),
DecoratorBookmarks({ explorer = args.explorer }),
DecoratorModified({ explorer = args.explorer }),
DecoratorHidden({ explorer = args.explorer }),
DecoratorOpened({ explorer = args.explorer }),
DecoratorGit({ explorer = args.explorer })
}
setmetatable(o, self)
self.__index = self
return o
self.hidden_display = Builder:setup_hidden_display_function(self.explorer.opts)
end
---Insert ranged highlight groups into self.highlights
@@ -95,27 +83,6 @@ function Builder:insert_highlight(groups, start, end_)
table.insert(self.hl_args, { groups, self.index, start, end_ or -1 })
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
---@param highlighted_strings HighlightedString[]
---@return string
@@ -136,78 +103,6 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
return string
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
---@param indent_markers HighlightedString[]
---@param arrows HighlightedString[]|nil
@@ -223,7 +118,7 @@ function Builder:format_line(indent_markers, arrows, icon, name, node)
end
for _, v in ipairs(t2) do
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
table.insert(t1, v)
end
@@ -341,23 +236,18 @@ function Builder:add_highlights(node)
return icon_hl_group, name_hl_group
end
---Insert node line into self.lines, calling Builder:build_lines for each directory
---@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)
-- various components
local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
local arrows = pad.get_arrows(node)
-- main components
local is_folder = node.nodes ~= nil
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
local icon, name = node:highlighted_icon(), node:highlighted_name()
-- highighting
local icon_hl_group, name_hl_group = self:add_highlights(node)
@@ -369,11 +259,14 @@ function Builder:build_line(node, idx, num_children)
self.index = self.index + 1
node = require("nvim-tree.lib").get_last_group_node(node)
if node.open then
self.depth = self.depth + 1
self:build_lines(node)
self.depth = self.depth - 1
local dir = node:as(DirectoryNode)
if dir then
dir = dir:last_group_node()
if dir.open then
self.depth = self.depth + 1
self:build_lines(dir)
self.depth = self.depth - 1
end
end
end
@@ -386,7 +279,7 @@ function Builder:add_hidden_count_string(node, idx, num_children)
local hidden_count_string = self.hidden_display(node.hidden_stats)
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_width = self.opts.renderer.indent_width
local indent_width = self.explorer.opts.renderer.indent_width
local indent_padding = string.rep(" ", indent_width)
local indent_string = indent_padding .. indent_markers.str
@@ -403,8 +296,11 @@ function Builder:add_hidden_count_string(node, idx, num_children)
end
end
---Number of visible nodes
---@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
return #nodes
end
@@ -423,7 +319,7 @@ function Builder:build_lines(node)
if not node then
node = self.explorer
end
local num_children = self:get_nodes_number(node.nodes)
local num_children = self:num_visible(node.nodes)
local idx = 1
for _, n in ipairs(node.nodes) do
if not n.hidden then
@@ -453,18 +349,18 @@ end
---@private
function Builder:build_header()
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)
self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(root_name))
self.index = 1
end
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)
local prefix_length = string.len(self.opts.live_filter.prefix)
self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length)
self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line))
local prefix_length = string.len(self.explorer.opts.live_filter.prefix)
self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length)
self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line))
self.index = self.index + 1
end
end
@@ -487,7 +383,7 @@ function Builder:build()
return self
end
---TODO refactor back to function; this was left here to reduce PR noise
---@private
---@param opts table
---@return fun(node: Node): string|nil
function Builder:setup_hidden_display_function(opts)
@@ -512,11 +408,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
hidden_stats = vim.tbl_deep_extend("force", {
live_filter = 0,
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
}, hidden_stats or {})
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
M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, {
relative = "win",
row = 0,
bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 },
width = math.min(text_width, vim.o.columns - 2),
height = 1,
relative = "win",
row = 0,
bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 },
width = math.min(text_width, vim.o.columns - 2),
height = 1,
noautocmd = true,
style = "minimal",
style = "minimal",
})
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 = {}
M.diagnostics = require("nvim-tree.renderer.components.diagnostics")
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")
function M.setup(opts)
M.diagnostics.setup(opts)
M.full_name.setup(opts)
M.icons.setup(opts)
M.devicons.setup(opts)
M.padding.setup(opts)
end

View File

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

View File

@@ -1,33 +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")
---@class (exact) DecoratorBookmarks: Decorator
---@field icon HighlightedString
local DecoratorBookmarks = Decorator:new()
---@field icon HighlightedString?
local DecoratorBookmarks = Decorator:extend()
---@param opts table
---@param explorer Explorer
---@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,
---@class DecoratorBookmarks
---@overload fun(explorer: DecoratorArgs): DecoratorBookmarks
---@protected
---@param args DecoratorArgs
function DecoratorBookmarks:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_bookmarks or "none",
icon_placement = args.explorer.opts.renderer.icons.bookmarks_placement or "none",
})
---@cast o DecoratorBookmarks
if opts.renderer.icons.show.bookmarks then
o.icon = {
str = opts.renderer.icons.glyphs.bookmark,
if self.explorer.opts.renderer.icons.show.bookmarks then
self.icon = {
str = self.explorer.opts.renderer.icons.glyphs.bookmark,
hl = { "NvimTreeBookmarkIcon" },
}
o:define_sign(o.icon)
self:define_sign(self.icon)
end
return o
end
---Bookmark icon: renderer.icons.show.bookmarks and node is marked
@@ -43,7 +39,7 @@ end
---@param node Node
---@return string|nil group
function DecoratorBookmarks:calculate_highlight(node)
if self.hl_pos ~= HL_POSITION.none and self.explorer.marks:get(node) then
if self.range ~= "none" and self.explorer.marks:get(node) then
return "NvimTreeBookmarkHL"
end
end

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
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 DirectoryNode = require("nvim-tree.node.directory")
-- highlight groups by severity
local HG_ICON = {
@@ -33,37 +31,36 @@ local ICON_KEYS = {
}
---@class (exact) DecoratorDiagnostics: Decorator
---@field icons HighlightedString[]
local DecoratorDiagnostics = Decorator:new()
---@field icons HighlightedString[]?
local DecoratorDiagnostics = Decorator:extend()
---@param opts table
---@param explorer Explorer
---@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,
---@class DecoratorDiagnostics
---@overload fun(explorer: DecoratorArgs): DecoratorDiagnostics
---@protected
---@param args DecoratorArgs
function DecoratorDiagnostics:new(args)
Decorator.new(self, {
explorer = args.explorer,
enabled = true,
hl_pos = args.explorer.opts.renderer.highlight_diagnostics or "none",
icon_placement = args.explorer.opts.renderer.icons.diagnostics_placement or "none",
})
---@cast o DecoratorDiagnostics
if not o.enabled then
return o
if not self.enabled then
return
end
if opts.renderer.icons.show.diagnostics then
o.icons = {}
if self.explorer.opts.renderer.icons.show.diagnostics then
self.icons = {}
for name, sev in pairs(ICON_KEYS) do
o.icons[sev] = {
str = opts.diagnostics.icons[name],
self.icons[sev] = {
str = self.explorer.opts.diagnostics.icons[name],
hl = { HG_ICON[sev] },
}
o:define_sign(o.icons[sev])
self:define_sign(self.icons[sev])
end
end
return o
end
---Diagnostic icon: diagnostics.enable, renderer.icons.show.diagnostics and node has status
@@ -84,7 +81,7 @@ end
---@param node Node
---@return string|nil group
function DecoratorDiagnostics:calculate_highlight(node)
if not node or not self.enabled or self.hl_pos == HL_POSITION.none then
if not node or not self.enabled or self.range == "none" then
return nil
end
@@ -96,7 +93,7 @@ function DecoratorDiagnostics:calculate_highlight(node)
end
local group
if node.nodes then
if node:is(DirectoryNode) then
group = HG_FOLDER[diag_value]
else
group = HG_FILE[diag_value]

View File

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

View File

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

View File

@@ -1,23 +1,32 @@
local HL_POSITION = require("nvim-tree.enum").HL_POSITION
local ICON_PLACEMENT = require("nvim-tree.enum").ICON_PLACEMENT
local Class = require("nvim-tree.classic")
---@class (exact) Decorator
---@field private __index? table
---@alias DecoratorRange "none" | "icon" | "name" | "all"
---@alias DecoratorIconPlacement "none" | "before" | "after" | "signcolumn" | "right_align"
---Abstract Decorator
---Uses the factory pattern to instantiate child instances.
---@class (exact) Decorator: Class
---@field protected explorer Explorer
---@field protected enabled boolean
---@field protected hl_pos HL_POSITION
---@field protected icon_placement ICON_PLACEMENT
local Decorator = {}
---@field protected range DecoratorRange
---@field protected icon_placement DecoratorIconPlacement
local Decorator = Class:extend()
---@param o Decorator|nil
---@return Decorator
function Decorator:new(o)
o = o or {}
---@class (exact) DecoratorArgs
---@field explorer Explorer
setmetatable(o, self)
self.__index = self
---@class (exact) AbstractDecoratorArgs: DecoratorArgs
---@field enabled boolean
---@field hl_pos DecoratorRange
---@field icon_placement DecoratorIconPlacement
return o
---@protected
---@param args AbstractDecoratorArgs
function Decorator:new(args)
self.explorer = args.explorer
self.enabled = args.enabled
self.range = args.hl_pos
self.icon_placement = args.icon_placement
end
---Maybe highlight groups
@@ -27,13 +36,13 @@ end
function Decorator:groups_icon_name(node)
local icon_hl, name_hl
if self.enabled and self.hl_pos ~= HL_POSITION.none then
if self.enabled and self.range ~= "none" then
local hl = self:calculate_highlight(node)
if self.hl_pos == HL_POSITION.all or self.hl_pos == HL_POSITION.icon then
if self.range == "all" or self.range == "icon" then
icon_hl = hl
end
if self.hl_pos == HL_POSITION.all or self.hl_pos == HL_POSITION.name then
if self.range == "all" or self.range == "name" then
name_hl = hl
end
end
@@ -45,7 +54,7 @@ end
---@param node Node
---@return string|nil name
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
end
@@ -55,33 +64,33 @@ function Decorator:sign_name(node)
end
end
---Icons when ICON_PLACEMENT.before
---Icons when "before"
---@param node Node
---@return HighlightedString[]|nil icons
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
end
return self:calculate_icons(node)
end
---Icons when ICON_PLACEMENT.after
---Icons when "after"
---@param node Node
---@return HighlightedString[]|nil icons
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
end
return self:calculate_icons(node)
end
---Icons when ICON_PLACEMENT.right_align
---Icons when "right_align"
---@param node Node
---@return HighlightedString[]|nil icons
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
end
@@ -117,7 +126,7 @@ function Decorator:define_sign(icon)
-- don't use sign if not defined
if #icon.str < 1 then
self.icon_placement = ICON_PLACEMENT.none
self.icon_placement = "none"
return
end

View File

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

View File

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

View File

@@ -2,8 +2,7 @@ local log = require("nvim-tree.log")
local view = require("nvim-tree.view")
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 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_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines")
---@class (exact) Renderer
---@field private __index? table
---@field private opts table user options
---@field private explorer Explorer
---@field private builder Builder
local Renderer = {}
---@class (exact) Renderer: Class
---@field explorer Explorer
local Renderer = Class:extend()
---@param opts table user options
---@param explorer Explorer
---@return Renderer
function Renderer:new(opts, explorer)
---@type Renderer
local o = {
opts = opts,
explorer = explorer,
builder = Builder:new(opts, explorer),
}
---@class Renderer
---@overload fun(args: RendererArgs): Renderer
setmetatable(o, self)
self.__index = self
---@class (exact) RendererArgs
---@field explorer Explorer
return o
---@protected
---@param args RendererArgs
function Renderer:new(args)
self.explorer = args.explorer
end
---@private
@@ -41,6 +32,8 @@ end
---@param lines string[]
---@param hl_args AddHighlightArgs[]
---@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)
if vim.fn.has("nvim-0.10") == 1 then
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 _, mark in ipairs(extname) do
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",
hl_mode = "combine",
hl_mode = "combine",
})
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)
for line_nr, vlines in pairs(virtual_lines) do
vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, {
virt_lines = vlines,
virt_lines_above = false,
virt_lines = vlines,
virt_lines_above = false,
virt_lines_leftcol = true,
})
end
@@ -107,9 +100,8 @@ function Renderer:draw()
local profile = log.profile_start("draw")
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)

View File

@@ -59,6 +59,34 @@ function M.path_basename(path)
return path:sub(i + 1, #path)
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.
---@param path string
---@param relative_to string|nil
@@ -68,13 +96,18 @@ function M.path_relative(path, relative_to)
return path
end
local _, r = path:find(M.path_add_trailing(relative_to), 1, true)
local p = path
local norm_path = 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
-- take the relative path starting after '/'
-- if somehow given a completely matching path,
-- returns ""
p = path:sub(r + 1)
p = norm_path:sub(r + 1)
end
return p
end
@@ -112,8 +145,7 @@ function M.find_node(nodes, fn)
end)
:iterate()
i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
local explorer = require("nvim-tree.core").get_explorer()
if explorer and explorer.live_filter.filter then
if node and node.explorer.live_filter.filter then
i = i + 1
end
return node, i
@@ -121,7 +153,7 @@ end
-- Find the line number of a node.
-- Return -1 is node is nil or not found.
---@param node Node|nil
---@param node Node?
---@return integer
function M.find_node_line(node)
if not node then
@@ -174,16 +206,6 @@ function M.get_node_from_path(path)
:iterate()
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)
local parts = {}
local total_count = 0
@@ -283,6 +305,14 @@ function M.canonical_path(path)
return path
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.
---@param path string
---@return string|nil
@@ -290,7 +320,7 @@ function M.escape_special_chars(path)
if path == nil then
return path
end
return M.is_windows and path:gsub("\\", "/") or path
return M.is_windows and escape_special_char_for_windows(path) or path
end
--- Create empty sub-tables if not present
@@ -473,7 +503,7 @@ end
---Focus node passed as parameter if visible, otherwise focus first visible parent.
---If none of the parents is visible focus root.
---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)
local explorer = require("nvim-tree.core").get_explorer()
@@ -549,14 +579,6 @@ function M.array_remove_nils(array)
end, array)
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.
--- 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
@@ -578,4 +600,44 @@ function M.is_nvim_tree_buf(bufnr)
return false
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

View File

@@ -15,31 +15,31 @@ local DEFAULT_MAX_WIDTH = -1
local DEFAULT_PADDING = 1
M.View = {
adaptive_size = false,
adaptive_size = false,
centralize_selection = false,
tabpages = {},
cursors = {},
hide_root_folder = false,
live_filter = {
tabpages = {},
cursors = {},
hide_root_folder = false,
live_filter = {
prev_focused_node = nil,
},
winopts = {
winopts = {
relativenumber = false,
number = false,
list = false,
foldenable = false,
winfixwidth = true,
winfixheight = true,
spell = false,
signcolumn = "yes",
foldmethod = "manual",
foldcolumn = "0",
cursorcolumn = false,
cursorline = true,
cursorlineopt = "both",
colorcolumn = "0",
wrap = false,
winhl = table.concat({
number = false,
list = false,
foldenable = false,
winfixwidth = true,
winfixheight = true,
spell = false,
signcolumn = "yes",
foldmethod = "manual",
foldcolumn = "0",
cursorcolumn = false,
cursorline = true,
cursorlineopt = "both",
colorcolumn = "0",
wrap = false,
winhl = table.concat({
"EndOfBuffer:NvimTreeEndOfBuffer",
"CursorLine:NvimTreeCursorLine",
"CursorLineNr:NvimTreeCursorLineNr",
@@ -65,13 +65,15 @@ local tabinitial = {
}
local BUFNR_PER_TAB = {}
---@type { name: string, value: any }[]
local BUFFER_OPTIONS = {
swapfile = false,
buftype = "nofile",
modifiable = false,
filetype = "NvimTree",
bufhidden = "wipe",
buflisted = false,
{ name = "bufhidden", value = "wipe" },
{ name = "buflisted", value = false },
{ name = "buftype", value = "nofile" },
{ name = "filetype", value = "NvimTree" },
{ name = "modifiable", value = false },
{ name = "swapfile", value = false },
}
---@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)
vim.api.nvim_buf_set_name(M.get_bufnr(), "NvimTree_" .. tab)
for option, value in pairs(BUFFER_OPTIONS) do
vim.bo[M.get_bufnr()][option] = value
bufnr = M.get_bufnr()
for _, option in ipairs(BUFFER_OPTIONS) do
vim.api.nvim_set_option_value(option.name, option.value, { buf = bufnr })
end
require("nvim-tree.keymap").on_attach(M.get_bufnr())
@@ -124,6 +127,7 @@ local function get_size(size)
end
---@param size (fun():integer)|integer|nil
---@return integer
local function get_width(size)
if size then
return get_size(size)
@@ -146,12 +150,26 @@ end
local function set_window_options_and_buffer()
pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr())
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore = "all"
for k, v in pairs(M.View.winopts) do
vim.opt_local[k] = v
if vim.fn.has("nvim-0.10") == 1 then
local eventignore = vim.api.nvim_get_option_value("eventignore", {})
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
for k, v in pairs(M.View.winopts) do
vim.api.nvim_win_set_option(0, k, v) ---@diagnostic disable-line: deprecated
end
vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated
end
vim.opt.eventignore = eventignore
end
---@return table
@@ -411,6 +429,7 @@ function M.abandon_all_windows()
end
---@param opts table|nil
---@return boolean
function M.is_visible(opts)
if opts and opts.tabpage then
if M.View.tabpages[opts.tabpage] == nil then
@@ -495,12 +514,6 @@ function M.get_bufnr()
return BUFNR_PER_TAB[vim.api.nvim_get_current_tabpage()]
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()
local view_winnr = M.get_winnr()
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 utils = require("nvim-tree.utils")
local M = {
config = {},
}
---@class Event
local Event = {
_events = {},
}
Event.__index = Event
---@class Watcher
local Watcher = {
_watchers = {},
}
Watcher.__index = Watcher
local Class = require("nvim-tree.classic")
local FS_EVENT_FLAGS = {
-- inotify or equivalent will be used; fallback to stat has not yet been implemented
@@ -25,20 +11,49 @@ local FS_EVENT_FLAGS = {
recursive = false,
}
---@param path string
---@return Event|nil
function Event:new(path)
log.line("watcher", "Event:new '%s'", path)
local M = {
config = {},
}
local e = setmetatable({
_path = path,
_fs_event = nil,
_listeners = {},
}, Event)
---Registry of all events
---@type Event[]
local events = {}
if e:start() then
Event._events[path] = e
return e
---@class (exact) Event: Class
---@field destroyed boolean
---@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
return nil
end
@@ -46,21 +61,21 @@ end
---@return boolean
function Event:start()
log.line("watcher", "Event:start '%s'", self._path)
log.line("watcher", "Event:start '%s'", self.path)
local rc, _, name
self._fs_event, _, name = vim.loop.new_fs_event()
if not self._fs_event then
self._fs_event = nil
notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self._path, name))
self.fs_event, _, name = vim.loop.new_fs_event()
if not self.fs_event then
self.fs_event = nil
notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name))
return false
end
local event_cb = vim.schedule_wrap(function(err, filename)
if err then
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)
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)
if err == "EPERM" and (utils.is_windows or utils.is_wsl) then
-- on directory removal windows will cascade the filesystem events out of order
log.line("watcher", message)
@@ -69,19 +84,19 @@ function Event:start()
self:destroy(message)
end
else
log.line("watcher", "event_cb '%s' '%s'", self._path, filename)
for _, listener in ipairs(self._listeners) do
log.line("watcher", "event_cb '%s' '%s'", self.path, filename)
for _, listener in ipairs(self.listeners) do
listener(filename)
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 name == "EMFILE" then
M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting")
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
return false
end
@@ -91,81 +106,114 @@ end
---@param listener function
function Event:add(listener)
table.insert(self._listeners, listener)
table.insert(self.listeners, listener)
end
---@param listener function
function Event:remove(listener)
utils.array_remove(self._listeners, listener)
if #self._listeners == 0 then
utils.array_remove(self.listeners, listener)
if #self.listeners == 0 then
self:destroy()
end
end
---@param message string|nil
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
notify.warn(message)
end
local rc, _, name = self._fs_event:stop()
local rc, _, name = self.fs_event:stop()
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
self._fs_event = nil
self.fs_event = nil
end
Event._events[self._path] = nil
self.destroyed = true
events[self.path] = nil
end
---@param path string
---@param files string[]|nil
---@param callback function
---@param data table
---Registry of all watchers
---@type Watcher[]
local watchers = {}
---@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
function Watcher:new(path, files, callback, data)
log.line("watcher", "Watcher:new '%s' %s", path, vim.inspect(files))
function Watcher:create(args)
log.line("watcher", "Watcher:create '%s' %s", args.path, vim.inspect(args.files))
local w = setmetatable(data, Watcher)
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
local event = events[args.path] or Event:create({ path = args.path })
if not event then
return nil
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
function Watcher:start()
self._listener = function(filename)
if not self._files or vim.tbl_contains(self._files, filename) then
self._callback(self)
self.listener = function(filename)
if not self.files or vim.tbl_contains(self.files, filename) then
self.callback(self)
end
end
self._event:add(self._listener)
self.event:add(self.listener)
end
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
end
@@ -183,11 +231,11 @@ end
function M.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()
end
for _, e in pairs(Event._events) do
for _, e in pairs(events) do
e:destroy()
end
end

View File

@@ -70,7 +70,7 @@ sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_ON_ATTACH.lua
# help human
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
do
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"
fi
DIR_SRC="lua"
DIR_OUT="luals-out"
DIR_SRC="${PWD}/lua"
DIR_OUT="${PWD}/luals-out"
FILE_LUARC="${DIR_OUT}/luarc.json"
# clear output
rm -rf "${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
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=$?
echo "${OUT}" >&2