Compare commits

..

11 Commits

Author SHA1 Message Date
Mateusz Russak
fa051cf990
refactor(#2826): multi instance nvim-tree.view 2024-08-04 13:31:11 +02:00
Mateusz Russak
0f2cda6ce0
docs: add missing live filter luadocs 2024-08-04 11:26:14 +02:00
Mateusz Russak
e6374abc7d
Merge branch 'live-filter-multiinstace' of github.com:nvim-tree/nvim-tree.lua into live-filter-multiinstace 2024-08-04 11:20:04 +02:00
Mateusz Russak
32314fd3ee
Update lua/nvim-tree/api.lua
Co-authored-by: Alexander Courtis <alex@courtis.org>
2024-08-04 11:19:31 +02:00
Mateusz Russak
e2853ee4fb
Merge branch 'master' into live-filter-multiinstace 2024-08-04 11:19:06 +02:00
Mateusz Russak
85fba095cd
Merge branch 'master' into live-filter-multiinstace 2024-07-28 11:28:26 +02:00
Mateusz Russak
d7504b3963 fix: style 2024-07-28 11:26:59 +02:00
Mateusz Russak
a634a1bb4d fix: api and filtration 2024-07-28 11:19:37 +02:00
Mateusz Russak
9deac32a40 refactor: all usages going through the explorer 2024-07-28 10:16:02 +02:00
Mateusz Russak
c6ae2431bc Merge branch 'master' into live-filter-multiinstace 2024-07-27 13:26:26 +02:00
Mateusz Russak
cc2d8c7475 feat(#2827): Multi Instance: Refactor: nvim-tree.live-filter 2024-07-23 18:53:16 +02:00
122 changed files with 5323 additions and 7154 deletions

View File

@ -4,21 +4,9 @@ root = true
insert_final_newline = true insert_final_newline = true
end_of_line = lf end_of_line = lf
[nvim-tree-lua.txt]
max_line_length = 78
# keep these in sync with .luarc.json
# .editorconfig is used within nvim, overriding .luarc.json
# .luarc.json is used by style check
[*.lua] [*.lua]
indent_style = space indent_style = space
max_line_length = 140
indent_size = 2 indent_size = 2
# EmmyLuaCodeStyle specific, see [nvim-tree-lua.txt]
# https://github.com/CppCXY/EmmyLuaCodeStyle/blob/master/lua.template.editorconfig max_line_length = 78
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_netrw = 1
vim.g.loaded_netrwPlugin = 1 vim.g.loaded_netrwPlugin = 1
vim.cmd([[set runtimepath=$VIMRUNTIME]]) vim.cmd [[set runtimepath=$VIMRUNTIME]]
vim.cmd([[set packpath=/tmp/nvt-min/site]]) vim.cmd [[set packpath=/tmp/nvt-min/site]]
local package_root = "/tmp/nvt-min/site/pack" local package_root = "/tmp/nvt-min/site/pack"
local install_path = package_root .. "/packer/start/packer.nvim" local install_path = package_root .. "/packer/start/packer.nvim"
local function load_plugins() local function load_plugins()
require("packer").startup({ require("packer").startup {
{ {
"wbthomason/packer.nvim", "wbthomason/packer.nvim",
"nvim-tree/nvim-tree.lua", "nvim-tree/nvim-tree.lua",
@ -18,21 +18,21 @@ local function load_plugins()
compile_path = install_path .. "/plugin/packer_compiled.lua", compile_path = install_path .. "/plugin/packer_compiled.lua",
display = { non_interactive = true }, display = { non_interactive = true },
}, },
}) }
end end
if vim.fn.isdirectory(install_path) == 0 then if vim.fn.isdirectory(install_path) == 0 then
print("Installing nvim-tree and dependencies.") print "Installing nvim-tree and dependencies."
vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path }) vim.fn.system { "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path }
end end
load_plugins() load_plugins()
require("packer").sync() require("packer").sync()
vim.cmd([[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]]) vim.cmd [[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]]
vim.opt.termguicolors = true vim.opt.termguicolors = true
vim.opt.cursorline = true vim.opt.cursorline = true
-- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE -- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE
_G.setup = function() _G.setup = function()
require("nvim-tree").setup({}) require("nvim-tree").setup {}
end end
-- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate. -- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate.
@ -41,11 +41,7 @@ end
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = "lua", pattern = "lua",
callback = function() callback = function()
vim.lsp.start { vim.lsp.start { cmd = { "lua-language-server" } }
name = "my-luals",
cmd = { "lua-language-server" },
root_dir = vim.loop.cwd(),
}
end, end,
}) })
]] ]]

View File

@ -20,25 +20,43 @@ jobs:
strategy: strategy:
matrix: matrix:
lua_version: [ 5.1 ] lua_version: [ 5.1 ]
luacheck_version: [ 1.2.0 ]
steps: steps:
- name: checkout - uses: actions/checkout@v4
uses: actions/checkout@v5
- name: install lua ${{ matrix.lua_version }} - uses: leafo/gh-actions-lua@v10
uses: leafo/gh-actions-lua@v12
with: with:
luaVersion: ${{ matrix.lua_version }} luaVersion: ${{ matrix.lua_version }}
- name: install luarocks - uses: leafo/gh-actions-luarocks@v4
uses: leafo/gh-actions-luarocks@v6
- name: install luacheck ${{ matrix.luacheck_version }} - run: luarocks install luacheck 1.1.1
run: luarocks install luacheck ${{ matrix.luacheck_version }}
- run: make lint - run: make lint
style:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ matrix.stylua_version }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
strategy:
matrix:
stylua_version: [ 0.19.1 ]
steps:
- uses: actions/checkout@v4
- name: stylua
uses: JohnnyMorganz/stylua-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ matrix.stylua_version }}
args: --check lua
- run: make style-doc
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -49,31 +67,26 @@ jobs:
strategy: strategy:
matrix: matrix:
nvim_version: [ stable, nightly ] nvim_version: [ stable, nightly ]
luals_version: [ 3.15.0 ] luals_version: [ 3.9.1 ]
env:
VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime
steps: steps:
- name: checkout - uses: actions/checkout@v4
uses: actions/checkout@v5
- name: install nvim ${{ matrix.nvim_version }} - uses: rhysd/action-setup-vim@v1
uses: rhysd/action-setup-vim@v1
with: with:
neovim: true neovim: true
version: ${{ matrix.nvim_version }} version: ${{ matrix.nvim_version }}
- name: install lua-language-server ${{ matrix.luals_version }} - name: install luals
run: | run: |
mkdir -p luals mkdir -p luals
curl -L "https://github.com/LuaLS/lua-language-server/releases/download/${{ matrix.luals_version }}/lua-language-server-${{ matrix.luals_version }}-linux-x64.tar.gz" | tar zx --directory luals curl -L "https://github.com/LuaLS/lua-language-server/releases/download/${{ matrix.luals_version }}/lua-language-server-${{ matrix.luals_version }}-linux-x64.tar.gz" | tar zx --directory luals
echo "luals/bin" >> "$GITHUB_PATH"
- run: make check - run: echo "luals/bin" >> "$GITHUB_PATH"
- name: make check
env:
VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime
run: make check
- run: make help-check - run: make help-check
- run: make style
- run: make style-doc

View File

@ -1,16 +1,14 @@
name: Luarocks Release name: Luarocks Release
on: on:
push: push:
tags: tags:
- v* - 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
luarocks-upload: luarocks-upload:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: LuaRocks Upload - name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v7 uses: nvim-neorocks/luarocks-tag-release@v7
env: env:

View File

@ -16,7 +16,7 @@ jobs:
steps: steps:
- uses: google-github-actions/release-please-action@v4 - uses: google-github-actions/release-please-action@v4
id: release id: release
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: tag major and minor versions - name: tag major and minor versions
if: ${{ steps.release.outputs.release_created }} if: ${{ steps.release.outputs.release_created }}
run: | run: |

View File

@ -14,6 +14,6 @@ jobs:
semantic-pr-subject: semantic-pr-subject:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v6.1.1 - uses: amannn/action-semantic-pull-request@v5.5.3
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,3 +1,3 @@
#!/usr/bin/env sh #!/bin/sh
make make

View File

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

View File

@ -1,23 +1,12 @@
{ {
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"runtime.version": "Lua 5.1",
"workspace": { "workspace": {
"library": [ "library": [
"$VIMRUNTIME/lua/vim", "$VIMRUNTIME/lua/vim",
"${3rd}/luv/library" "${3rd}/luv/library"
] ]
}, },
"format": {
"defaultConfig": {
"indent_style": "space",
"max_line_length": "140",
"indent_size": "2",
"continuation_indent": "2",
"quote_style": "double",
"call_arg_parentheses": "always",
"space_before_closure_open_parenthesis": "false",
"align_continuous_similar_call_args": "true"
}
},
"diagnostics": { "diagnostics": {
"libraryFiles": "Disable", "libraryFiles": "Disable",
"globals": [], "globals": [],
@ -44,13 +33,13 @@
"empty-block": "Any", "empty-block": "Any",
"global-element": "Any", "global-element": "Any",
"global-in-nil-env": "Any", "global-in-nil-env": "Any",
"incomplete-signature-doc": "Any", "incomplete-signature-doc": "None",
"inject-field": "Any", "inject-field": "Any",
"invisible": "Any", "invisible": "Any",
"lowercase-global": "Any", "lowercase-global": "Any",
"missing-fields": "Any", "missing-fields": "Any",
"missing-global-doc": "Any", "missing-global-doc": "Any",
"missing-local-export-doc": "Any", "missing-local-export-doc": "None",
"missing-parameter": "Any", "missing-parameter": "Any",
"missing-return": "Any", "missing-return": "Any",
"missing-return-value": "Any", "missing-return-value": "Any",

View File

@ -1,3 +1,3 @@
{ {
".": "1.14.0" ".": "1.5.0"
} }

6
.stylua.toml Normal file
View File

@ -0,0 +1,6 @@
column_width = 140
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "None"

View File

@ -1,203 +1,5 @@
# Changelog # Changelog
## [1.14.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.13.0...nvim-tree-v1.14.0) (2025-08-12)
### Features
* **#2685:** highlight git new tracked with NvimTreeGitFileNewHL ([#3176](https://github.com/nvim-tree/nvim-tree.lua/issues/3176)) ([0a52012](https://github.com/nvim-tree/nvim-tree.lua/commit/0a52012d611f3c1492b8d2aba363fabf734de91d))
* **#2789:** add optional function expand_until to api.tree.expand_all and api.node.expand ([#3166](https://github.com/nvim-tree/nvim-tree.lua/issues/3166)) ([1b876db](https://github.com/nvim-tree/nvim-tree.lua/commit/1b876db04903b93c78c97fd3f3dd85d59eeef5ff))
* **#2826:** allow only one window with nvim-tree buffer per tab ([#3174](https://github.com/nvim-tree/nvim-tree.lua/issues/3174)) ([dd2364d](https://github.com/nvim-tree/nvim-tree.lua/commit/dd2364d6802f7f57a98acb8b545ed484c6697626))
* **#3157:** add view.cursorlineopt ([#3158](https://github.com/nvim-tree/nvim-tree.lua/issues/3158)) ([8eb5e0b](https://github.com/nvim-tree/nvim-tree.lua/commit/8eb5e0bfd1c4da6efc03ab0c1ccf463dbaae831e))
### Bug Fixes
* **#3077:** deleting a directory containing symlinked directory will delete the contents of the linked directory ([#3168](https://github.com/nvim-tree/nvim-tree.lua/issues/3168)) ([10db694](https://github.com/nvim-tree/nvim-tree.lua/commit/10db6943cb40625941a35235eeb385ffdfbf827a))
* **#3157:** add view.cursorlineopt ([8eb5e0b](https://github.com/nvim-tree/nvim-tree.lua/commit/8eb5e0bfd1c4da6efc03ab0c1ccf463dbaae831e))
* **#3172:** live filter exception ([#3173](https://github.com/nvim-tree/nvim-tree.lua/issues/3173)) ([0a7fcdf](https://github.com/nvim-tree/nvim-tree.lua/commit/0a7fcdf3f8ba208f4260988a198c77ec11748339))
* invalid window id for popup info window ([#3147](https://github.com/nvim-tree/nvim-tree.lua/issues/3147)) ([d54a187](https://github.com/nvim-tree/nvim-tree.lua/commit/d54a1875a91e1a705795ea26074795210b92ce7f))
* **picker:** exclude full_name window id from the choice ([#3165](https://github.com/nvim-tree/nvim-tree.lua/issues/3165)) ([543ed3c](https://github.com/nvim-tree/nvim-tree.lua/commit/543ed3cac212dc3993ef9f042f6c0812e34ddd43))
* window picker ignore hidden window ([#3145](https://github.com/nvim-tree/nvim-tree.lua/issues/3145)) ([d87b41c](https://github.com/nvim-tree/nvim-tree.lua/commit/d87b41ca537e2131622d48a6c25ccf2fbe0e5d62))
### Performance Improvements
* **#3171:** cache toplevel for untracked ([#3185](https://github.com/nvim-tree/nvim-tree.lua/issues/3185)) ([4425136](https://github.com/nvim-tree/nvim-tree.lua/commit/442513648c6936e754c3308a1c58591a399493e5))
* **#3171:** use vim.system() instead of vim.fn.system() to execute git toplevel ([#3175](https://github.com/nvim-tree/nvim-tree.lua/issues/3175)) ([9a05b9e](https://github.com/nvim-tree/nvim-tree.lua/commit/9a05b9e9f928856ca23dbf876fab372003180c3f))
### Reverts
* **#3180, #3177:** invalid group or tabpage ([#3181](https://github.com/nvim-tree/nvim-tree.lua/issues/3181)) ([9b289ab](https://github.com/nvim-tree/nvim-tree.lua/commit/9b289abd6998e30fd24cbc9919e0b0cbed6364ce))
* **#3180, #3177:** resolve live filter failures ([#3183](https://github.com/nvim-tree/nvim-tree.lua/issues/3183)) ([a4699c0](https://github.com/nvim-tree/nvim-tree.lua/commit/a4699c0904103e7767334f6da05f5c2ea5514845))
## [1.13.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.12.0...nvim-tree-v1.13.0) (2025-06-14)
### Features
* **#3113:** add renderer.icons.folder_arrow_padding ([#3114](https://github.com/nvim-tree/nvim-tree.lua/issues/3114)) ([ea5097a](https://github.com/nvim-tree/nvim-tree.lua/commit/ea5097a1e2702b4827cb7380e7fa0bd6da87699c))
* **#3132:** add api.node.expand and api.node.collapse ([#3133](https://github.com/nvim-tree/nvim-tree.lua/issues/3133)) ([ae59561](https://github.com/nvim-tree/nvim-tree.lua/commit/ae595611fb2225f2041996c042aa4e4b8663b41e))
### Bug Fixes
* "Invalid buffer id" on closing nvim-tree window ([#3129](https://github.com/nvim-tree/nvim-tree.lua/issues/3129)) ([25d16aa](https://github.com/nvim-tree/nvim-tree.lua/commit/25d16aab7d29ca940a9feb92e6bb734697417009))
* **#2746:** background and right aligned icons in floating windows ([#3128](https://github.com/nvim-tree/nvim-tree.lua/issues/3128)) ([cbc3165](https://github.com/nvim-tree/nvim-tree.lua/commit/cbc3165e08893bb499da035c6f6f9d1512b57664))
* **#3117:** allow changing filename's casing ([bd54d1d](https://github.com/nvim-tree/nvim-tree.lua/commit/bd54d1d33c20d8630703b9842480291588dbad07))
* **#3117:** windows: change file/dir case ([#3135](https://github.com/nvim-tree/nvim-tree.lua/issues/3135)) ([bd54d1d](https://github.com/nvim-tree/nvim-tree.lua/commit/bd54d1d33c20d8630703b9842480291588dbad07))
* **#3122:** remove redundant vim.validate ([#3123](https://github.com/nvim-tree/nvim-tree.lua/issues/3123)) ([e7d1b7d](https://github.com/nvim-tree/nvim-tree.lua/commit/e7d1b7dadc62fe2eccc17d814354b0a5688621ce))
* **#3124:** fix icon padding for "right_align" placements, notably for dotfiles ([#3125](https://github.com/nvim-tree/nvim-tree.lua/issues/3125)) ([e4cd856](https://github.com/nvim-tree/nvim-tree.lua/commit/e4cd856ebf4fec51db10c69d63e43224b701cbce))
* **#3124:** prevent empty icons_right_align response from breaking padding ([e4cd856](https://github.com/nvim-tree/nvim-tree.lua/commit/e4cd856ebf4fec51db10c69d63e43224b701cbce))
* **#3134:** setting one glyph to "" no longer disables others ([#3136](https://github.com/nvim-tree/nvim-tree.lua/issues/3136)) ([ebcaccd](https://github.com/nvim-tree/nvim-tree.lua/commit/ebcaccda1c575fa19a8087445276e6671e2b9b37))
* **#3143:** actions.open_file.window_picker.exclude applies when not using window picker ([#3144](https://github.com/nvim-tree/nvim-tree.lua/issues/3144)) ([05d8172](https://github.com/nvim-tree/nvim-tree.lua/commit/05d8172ebf9cdb2d140cf25b75625374fbc3df7f))
* fixes [#3134](https://github.com/nvim-tree/nvim-tree.lua/issues/3134) ([ebcaccd](https://github.com/nvim-tree/nvim-tree.lua/commit/ebcaccda1c575fa19a8087445276e6671e2b9b37))
* invalid buffer issue ([25d16aa](https://github.com/nvim-tree/nvim-tree.lua/commit/25d16aab7d29ca940a9feb92e6bb734697417009))
## [1.12.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.11.0...nvim-tree-v1.12.0) (2025-04-20)
### Features
* add TreePreOpen event ([#3105](https://github.com/nvim-tree/nvim-tree.lua/issues/3105)) ([c24c047](https://github.com/nvim-tree/nvim-tree.lua/commit/c24c0470d9de277fbebecd718f33561ed7c90298))
### Bug Fixes
* **#3101:** when renderer.highlight_opened_files = "none" do not reload on BufUnload and BufReadPost ([#3102](https://github.com/nvim-tree/nvim-tree.lua/issues/3102)) ([5bea2b3](https://github.com/nvim-tree/nvim-tree.lua/commit/5bea2b37523a31288e0fcab42f3be5c1bd4516bb))
* explicitly set `border` to `"none"` in full name float ([#3094](https://github.com/nvim-tree/nvim-tree.lua/issues/3094)) ([c3c1935](https://github.com/nvim-tree/nvim-tree.lua/commit/c3c193594213c5e2f89ec5d7729cad805f76b256))
* reliably dispatch exactly one TreeOpen and TreeClose events ([#3107](https://github.com/nvim-tree/nvim-tree.lua/issues/3107)) ([3a63717](https://github.com/nvim-tree/nvim-tree.lua/commit/3a63717d3d332d8f39aaf65be7a0e4c2265af021))
## [1.11.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.10.0...nvim-tree-v1.11.0) (2025-02-22)
### Features
* **#1984:** add quit_on_open and focus opts to various api.node.open functions ([#3054](https://github.com/nvim-tree/nvim-tree.lua/issues/3054)) ([3281f33](https://github.com/nvim-tree/nvim-tree.lua/commit/3281f331f7f0bef13eb00fb2d5a9d28b2f6155a2))
* **#3037:** add API node.buffer.delete, node.buffer.wipe ([#3040](https://github.com/nvim-tree/nvim-tree.lua/issues/3040)) ([fee1da8](https://github.com/nvim-tree/nvim-tree.lua/commit/fee1da88972f5972a8296813f6c00d7598325ebd))
### Bug Fixes
* **#3045:** wipe scratch buffers for full name and show info popups ([#3050](https://github.com/nvim-tree/nvim-tree.lua/issues/3050)) ([fca0b67](https://github.com/nvim-tree/nvim-tree.lua/commit/fca0b67c0b5a31727fb33addc4d9c100736a2894))
* **#3059:** test for presence of new 0.11 API vim.hl.range ([#3060](https://github.com/nvim-tree/nvim-tree.lua/issues/3060)) ([70825f2](https://github.com/nvim-tree/nvim-tree.lua/commit/70825f23db61ecd900c4cfea169bffe931926a9d))
* arithmetic on nil value error on first git project open ([#3064](https://github.com/nvim-tree/nvim-tree.lua/issues/3064)) ([8052310](https://github.com/nvim-tree/nvim-tree.lua/commit/80523101f0ae48b7f1990e907b685a3d79776c01))
* stl and stlnc fillchars are hidden in window picker ([b699143](https://github.com/nvim-tree/nvim-tree.lua/commit/b69914325a945ee5157f0d21047210b42af5776e))
* window picker: hide fillchars: stl and stlnc ([#3066](https://github.com/nvim-tree/nvim-tree.lua/issues/3066)) ([b699143](https://github.com/nvim-tree/nvim-tree.lua/commit/b69914325a945ee5157f0d21047210b42af5776e))
## [1.10.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.9.0...nvim-tree-v1.10.0) (2025-01-13)
### Features
* **api:** add node.open.vertical_no_picker, node.open.horizontal_no_picker ([#3031](https://github.com/nvim-tree/nvim-tree.lua/issues/3031)) ([68fc4c2](https://github.com/nvim-tree/nvim-tree.lua/commit/68fc4c20f5803444277022c681785c5edd11916d))
### Bug Fixes
* **#3015:** dynamic width no longer truncates on right_align icons ([#3022](https://github.com/nvim-tree/nvim-tree.lua/issues/3022)) ([f7b76cd](https://github.com/nvim-tree/nvim-tree.lua/commit/f7b76cd1a75615c8d6254fc58bedd2a7304eb7d8))
* **#3018:** error when focusing nvim-tree when in terminal mode ([#3019](https://github.com/nvim-tree/nvim-tree.lua/issues/3019)) ([db8d7ac](https://github.com/nvim-tree/nvim-tree.lua/commit/db8d7ac1f524fc6f808764b29fa695c51e014aa6))
* **#3041:** use vim.diagnostic.get for updating diagnostics ([#3042](https://github.com/nvim-tree/nvim-tree.lua/issues/3042)) ([aae0185](https://github.com/nvim-tree/nvim-tree.lua/commit/aae01853ddbd790d1efd6ff04ff96cf38c02c95f))
* Can't re-enter normal mode from terminal mode ([db8d7ac](https://github.com/nvim-tree/nvim-tree.lua/commit/db8d7ac1f524fc6f808764b29fa695c51e014aa6))
* hijack directory "BufEnter", "BufNewFile" events are nested ([#3044](https://github.com/nvim-tree/nvim-tree.lua/issues/3044)) ([39bc630](https://github.com/nvim-tree/nvim-tree.lua/commit/39bc63081605c1d4b974131ebecaea11e8a8595f))
* view.width functions may return strings ([#3020](https://github.com/nvim-tree/nvim-tree.lua/issues/3020)) ([6b4be1d](https://github.com/nvim-tree/nvim-tree.lua/commit/6b4be1dc0cd4d5d5b8e8b56b510a75016e99746f))
## [1.9.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.8.0...nvim-tree-v1.9.0) (2024-12-07)
### Features
* **#2948:** add custom decorators, :help nvim-tree-decorators ([#2996](https://github.com/nvim-tree/nvim-tree.lua/issues/2996)) ([7a4ff1a](https://github.com/nvim-tree/nvim-tree.lua/commit/7a4ff1a516fe92a5ed6b79d7ce31ea4d8f341a72))
### Bug Fixes
* **#2954:** more efficient LSP updates, increase diagnostics.debounce_delay from 50ms to 500ms ([#3007](https://github.com/nvim-tree/nvim-tree.lua/issues/3007)) ([1f3ffd6](https://github.com/nvim-tree/nvim-tree.lua/commit/1f3ffd6af145af2a4930a61c50f763264922c3fe))
* **#2990:** Do not check if buffer is buflisted in diagnostics.update() ([#2998](https://github.com/nvim-tree/nvim-tree.lua/issues/2998)) ([28eac28](https://github.com/nvim-tree/nvim-tree.lua/commit/28eac2801b201f301449e976d7a9e8cfde053ba3))
* **#3009:** nvim &lt; 0.10 apply view options locally ([#3010](https://github.com/nvim-tree/nvim-tree.lua/issues/3010)) ([ca7c4c3](https://github.com/nvim-tree/nvim-tree.lua/commit/ca7c4c33cac2ad66ec69d45e465379716ef0cc97))
* **api:** correct argument types in `wrap_node` and `wrap_node_or_nil` ([#3006](https://github.com/nvim-tree/nvim-tree.lua/issues/3006)) ([f7c65e1](https://github.com/nvim-tree/nvim-tree.lua/commit/f7c65e11d695a084ca10b93df659bb7e68b71f9f))
## [1.8.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.1...nvim-tree-v1.8.0) (2024-11-09)
### Features
* **#2819:** add actions.open_file.relative_path, default enabled, following successful experiment ([#2995](https://github.com/nvim-tree/nvim-tree.lua/issues/2995)) ([2ee1c5e](https://github.com/nvim-tree/nvim-tree.lua/commit/2ee1c5e17fdfbf5013af31b1410e4a5f28f4cadd))
* **#2938:** add default filesystem_watchers.ignore_dirs = { "/.ccls-cache", "/build", "/node_modules", "/target", } ([#2940](https://github.com/nvim-tree/nvim-tree.lua/issues/2940)) ([010ae03](https://github.com/nvim-tree/nvim-tree.lua/commit/010ae0365aafd6275c478d932515d2e8e897b7bb))
### Bug Fixes
* **#2945:** stack overflow on api.git.reload or fugitive event with watchers disabled ([#2949](https://github.com/nvim-tree/nvim-tree.lua/issues/2949)) ([5ad8762](https://github.com/nvim-tree/nvim-tree.lua/commit/5ad87620ec9d1190d15c88171a3f0122bc16b0fe))
* **#2947:** root is never a dotfile, so that it doesn't propagate to children ([#2958](https://github.com/nvim-tree/nvim-tree.lua/issues/2958)) ([f5f6789](https://github.com/nvim-tree/nvim-tree.lua/commit/f5f67892996b280ae78b1b0a2d07c4fa29ae0905))
* **#2951:** highlights incorrect following cancelled pick ([#2952](https://github.com/nvim-tree/nvim-tree.lua/issues/2952)) ([1c9553a](https://github.com/nvim-tree/nvim-tree.lua/commit/1c9553a19f70df3dcb171546a3d5e034531ef093))
* **#2954:** resolve occasional tree flashing on diagnostics, set tree buffer options in deterministic order ([#2980](https://github.com/nvim-tree/nvim-tree.lua/issues/2980)) ([82ab19e](https://github.com/nvim-tree/nvim-tree.lua/commit/82ab19ebf79c1839d7351f2fed213d1af13a598e))
* **#2961:** windows: escape brackets and parentheses when opening file ([#2962](https://github.com/nvim-tree/nvim-tree.lua/issues/2962)) ([63c7ad9](https://github.com/nvim-tree/nvim-tree.lua/commit/63c7ad9037fb7334682dd0b3a177cee25c5c8a0f))
* **#2969:** After a rename, the node loses selection ([#2974](https://github.com/nvim-tree/nvim-tree.lua/issues/2974)) ([1403933](https://github.com/nvim-tree/nvim-tree.lua/commit/14039337a563f4efd72831888f332a15585f0ea1))
* **#2972:** error on :colorscheme ([#2973](https://github.com/nvim-tree/nvim-tree.lua/issues/2973)) ([6e5a204](https://github.com/nvim-tree/nvim-tree.lua/commit/6e5a204ca659bb8f2a564df75df2739edec03cb0))
* **#2976:** use vim.loop to preserve neovim 0.9 compatibility ([#2977](https://github.com/nvim-tree/nvim-tree.lua/issues/2977)) ([00dff48](https://github.com/nvim-tree/nvim-tree.lua/commit/00dff482f9a8fb806a54fd980359adc6cd45d435))
* **#2978:** grouped folder not showing closed icon ([#2979](https://github.com/nvim-tree/nvim-tree.lua/issues/2979)) ([120ba58](https://github.com/nvim-tree/nvim-tree.lua/commit/120ba58254835d412bbc91cffe847e9be835fadd))
* **#2981:** windows: root changed when navigating with LSP ([#2982](https://github.com/nvim-tree/nvim-tree.lua/issues/2982)) ([c22124b](https://github.com/nvim-tree/nvim-tree.lua/commit/c22124b37409bee6d1a0da77f4f3a1526f7a204d))
* symlink file icons rendered when renderer.icons.show.file = false, folder.symlink* was incorrectly rendered as folder.default|open ([#2983](https://github.com/nvim-tree/nvim-tree.lua/issues/2983)) ([2156bc0](https://github.com/nvim-tree/nvim-tree.lua/commit/2156bc08c982d3c4b4cfc2b8fd7faeff58a88e10))
## [1.7.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.0...nvim-tree-v1.7.1) (2024-09-30)
### Bug Fixes
* **#2794:** sshfs compatibility ([#2922](https://github.com/nvim-tree/nvim-tree.lua/issues/2922)) ([9650e73](https://github.com/nvim-tree/nvim-tree.lua/commit/9650e735baad0d39505f4cb4867a60f02858536a))
* **#2928:** nil explorer in parent move action ([#2929](https://github.com/nvim-tree/nvim-tree.lua/issues/2929)) ([0429f28](https://github.com/nvim-tree/nvim-tree.lua/commit/0429f286b350c65118d66b646775bf187936fa47))
* **#2930:** empty groups expanded on reload ([#2935](https://github.com/nvim-tree/nvim-tree.lua/issues/2935)) ([4520c03](https://github.com/nvim-tree/nvim-tree.lua/commit/4520c0355cc561830ee2cf90dc37a2a75abf7995))
* invalid explorer on open ([#2927](https://github.com/nvim-tree/nvim-tree.lua/issues/2927)) ([59a8a6a](https://github.com/nvim-tree/nvim-tree.lua/commit/59a8a6ae5e9d3eae99d08ab655d12fd51d5d17f3))
### Reverts
* **#2794:** sshfs compatibility ([#2920](https://github.com/nvim-tree/nvim-tree.lua/issues/2920)) ([8405ecf](https://github.com/nvim-tree/nvim-tree.lua/commit/8405ecfbd6bb08a94ffc9c68fef211eea56e8a3b))
## [1.7.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.6.1...nvim-tree-v1.7.0) (2024-09-21)
### Features
* **#2430:** use vim.ui.open as default system_open, for neovim 0.10+ ([#2912](https://github.com/nvim-tree/nvim-tree.lua/issues/2912)) ([03f737e](https://github.com/nvim-tree/nvim-tree.lua/commit/03f737e5744a2b3ebb4b086f7636a3399224ec0c))
* help closes on &lt;Esc&gt; and api.tree.toggle_help mappings ([#2909](https://github.com/nvim-tree/nvim-tree.lua/issues/2909)) ([b652dbd](https://github.com/nvim-tree/nvim-tree.lua/commit/b652dbd0e0489c5fbb81fbededf0d99029cd2f38))
### Bug Fixes
* **#2862:** windows path replaces backslashes with forward slashes ([#2903](https://github.com/nvim-tree/nvim-tree.lua/issues/2903)) ([45a93d9](https://github.com/nvim-tree/nvim-tree.lua/commit/45a93d99794fff3064141d5b3a50db98ce352697))
* **#2906:** resource leak on populate children ([#2907](https://github.com/nvim-tree/nvim-tree.lua/issues/2907)) ([a4dd5ad](https://github.com/nvim-tree/nvim-tree.lua/commit/a4dd5ad5c8f9349142291d24e0e6466995594b9a))
* **#2917:** fix root copy paths: Y, ge, gy, y ([#2918](https://github.com/nvim-tree/nvim-tree.lua/issues/2918)) ([b18ce8b](https://github.com/nvim-tree/nvim-tree.lua/commit/b18ce8be8f162eee0bc37addcfe17d7d019fcec7))
* safely close last tree window ([#2913](https://github.com/nvim-tree/nvim-tree.lua/issues/2913)) ([bd48816](https://github.com/nvim-tree/nvim-tree.lua/commit/bd4881660bf0ddfa6acb21259f856ba3dcb26a93))
* safely close tree window with pcall and debug logging ([bd48816](https://github.com/nvim-tree/nvim-tree.lua/commit/bd4881660bf0ddfa6acb21259f856ba3dcb26a93))
## [1.6.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.6.0...nvim-tree-v1.6.1) (2024-09-09)
### Bug Fixes
* **#2794:** sshfs compatibility ([#2893](https://github.com/nvim-tree/nvim-tree.lua/issues/2893)) ([2d6e64d](https://github.com/nvim-tree/nvim-tree.lua/commit/2d6e64dd8c45a86f312552b7a47eef2c8623a25c))
* **#2868:** windows: do not visit unenumerable directories such as Application Data ([#2874](https://github.com/nvim-tree/nvim-tree.lua/issues/2874)) ([2104786](https://github.com/nvim-tree/nvim-tree.lua/commit/210478677cb9d672c4265deb0e9b59d58b675bd4))
* **#2878:** nowrapscan prevents move from root ([#2880](https://github.com/nvim-tree/nvim-tree.lua/issues/2880)) ([4234095](https://github.com/nvim-tree/nvim-tree.lua/commit/42340952af598a08ab80579d067b6da72a9e6d29))
* **#2879:** remove unnecessary tree window width setting to prevent unnecessary :wincmd = ([#2881](https://github.com/nvim-tree/nvim-tree.lua/issues/2881)) ([d43ab67](https://github.com/nvim-tree/nvim-tree.lua/commit/d43ab67d0eb4317961c5e9d15fffe908519debe0))
## [1.6.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.5.0...nvim-tree-v1.6.0) (2024-08-10)
### Features
* **#2225:** add renderer.hidden_display to show a summary of hidden files below the tree ([#2856](https://github.com/nvim-tree/nvim-tree.lua/issues/2856)) ([e25eb7f](https://github.com/nvim-tree/nvim-tree.lua/commit/e25eb7fa83f7614bb23d762e91d2de44fcd7103b))
* **#2349:** add "right_align" option for renderer.icons.*_placement ([#2839](https://github.com/nvim-tree/nvim-tree.lua/issues/2839)) ([1d629a5](https://github.com/nvim-tree/nvim-tree.lua/commit/1d629a5d3f7d83d516494c221a2cfc079f43bc47))
* **#2349:** add "right_align" option for renderer.icons.*_placement ([#2846](https://github.com/nvim-tree/nvim-tree.lua/issues/2846)) ([48d0e82](https://github.com/nvim-tree/nvim-tree.lua/commit/48d0e82f9434691cc50d970898142a8c084a49d6))
* add renderer.highlight_hidden, renderer.icons.show.hidden and renderer.icons.hidden_placement for dotfile icons/highlights ([#2840](https://github.com/nvim-tree/nvim-tree.lua/issues/2840)) ([48a9290](https://github.com/nvim-tree/nvim-tree.lua/commit/48a92907575df1dbd7242975a04e98169cb3a115))
### Bug Fixes
* **#2859:** make sure window still exists when restoring options ([#2863](https://github.com/nvim-tree/nvim-tree.lua/issues/2863)) ([466fbed](https://github.com/nvim-tree/nvim-tree.lua/commit/466fbed3e4b61fcc23a48fe99de7bfa264a9fee8))
## [1.5.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.4.0...nvim-tree-v1.5.0) (2024-07-11) ## [1.5.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.4.0...nvim-tree-v1.5.0) (2024-07-11)

View File

@ -2,43 +2,17 @@
Thank you for contributing. Thank you for contributing.
See [wiki: Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools. See [Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools.
<!--
https://github.com/jonschlinkert/markdown-toc
markdown-toc --maxdepth=2 -i CONTRIBUTING.md
-->
<!-- toc -->
- [Tools](#tools)
- [Quality](#quality)
* [lint](#lint)
* [style](#style)
* [check](#check)
- [Diagnostics](#diagnostics)
- [Backwards Compatibility](#backwards-compatibility)
- [Adding New Actions](#adding-new-actions)
- [Documentation](#documentation)
* [Opts](#opts)
* [API](#api)
- [Windows](#windows)
- [Pull Request](#pull-request)
* [Subject](#subject)
<!-- tocstop -->
# Tools # Tools
Following are used during CI and strongly recommended during local development. Following are used during CI and strongly recommended during local development.
Language server: [luals](https://luals.github.io)
Lint: [luacheck](https://github.com/lunarmodules/luacheck/) Lint: [luacheck](https://github.com/lunarmodules/luacheck/)
Style Fixing: [EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle): `CodeCheck` Style: [StyLua](https://github.com/JohnnyMorganz/StyLua)
nvim-tree.lua migrated from stylua to EmmyLuaCodeStyle ~2024/10. `vim.lsp.buf.format()` may be used as it is the default formatter for luals, using an embedded [EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle) Language server: [luals](https://luals.github.io)
You can install them via you OS package manager e.g. `pacman`, `brew` or other via other package managers such as `cargo` or `luarocks` You can install them via you OS package manager e.g. `pacman`, `brew` or other via other package managers such as `cargo` or `luarocks`
@ -60,14 +34,14 @@ make lint
## style ## style
1. Runs lua language server `codestyle-check` only, using `.luarc.json` settings 1. Runs stylua using `.stylua.toml` settings
1. Runs `scripts/doc-comments.sh` to validate annotated documentation 1. Runs `scripts/doc-comments.sh` to validate annotated documentation
```sh ```sh
make style make style
``` ```
You can automatically fix style issues using `CodeCheck`: You can automatically fix stylua issues via:
```sh ```sh
make style-fix make style-fix
@ -96,30 +70,6 @@ curl -L "https://github.com/LuaLS/lua-language-server/releases/download/3.9.1/lu
PATH="luals/bin:${PATH}" make check PATH="luals/bin:${PATH}" make check
``` ```
# Diagnostics
Diagnostics issues may not be suppressed. See [luals](https://luals.github.io) documentation for details on how to structure the code and comments.
Suppressions are permitted only in the following cases:
- Backwards compatibility shims
- neovim API metadata incorrect, awaiting upstream fix
- classic class framework
# Backwards Compatibility
Whenever new neovim API is introduced, please ensure that it is available in older versions. See `:help deprecated.txt` and `$VIMRUNTIME/lua/vim/_meta/api.lua`
See `nvim-tree.setup` for the oldest supported version of neovim. If the API is not availble in that version, a backwards compatibility shim must be used e.g.
```lua
if vim.fn.has("nvim-0.10") == 1 then
modified = vim.api.nvim_get_option_value("modified", { buf = target_bufid })
else
modified = vim.api.nvim_buf_get_option(target_bufid, "modified") ---@diagnostic disable-line: deprecated
end
```
# Adding New Actions # Adding New Actions
To add a new action, add a file in `actions/name-of-the-action.lua`. You should export a `setup` function if some configuration is needed. To add a new action, add a file in `actions/name-of-the-action.lua`. You should export a `setup` function if some configuration is needed.
@ -138,14 +88,6 @@ Documentation for options should also be added to `nvim-tree-opts` in `doc/nvim-
When adding or changing API please update :help nvim-tree-api When adding or changing API please update :help nvim-tree-api
# Windows
Please note that nvim-tree team members do not have access to nor expertise with Windows.
You will need to be an active participant during development and raise a PR to resolve any issues that may arise.
Please ensure that windows specific features and fixes are behind the appropriate feature flag, see [wiki: OS Feature Flags](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development#os-feature-flags)
# Pull Request # Pull Request
Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge. Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge.

View File

@ -5,7 +5,7 @@ all: lint style check
# #
lint: luacheck lint: luacheck
style: style-check style-doc style: stylua style-doc
check: luals check: luals
@ -13,11 +13,10 @@ check: luals
# subtasks # subtasks
# #
luacheck: luacheck:
luacheck --codes --quiet lua --exclude-files "**/_meta/**" luacheck -q lua
# --diagnosis-as-error does not function for workspace, hence we post-process the output stylua:
style-check: stylua lua --check
@scripts/luals-check.sh codestyle-check
style-doc: style-doc:
scripts/doc-comments.sh scripts/doc-comments.sh
@ -29,7 +28,7 @@ luals:
# fixes # fixes
# #
style-fix: style-fix:
CodeFormat format --config .editorconfig --workspace lua stylua lua
# #
# utility # utility
@ -44,5 +43,5 @@ help-check: help-update
git diff --exit-code doc/nvim-tree-lua.txt git diff --exit-code doc/nvim-tree-lua.txt
.PHONY: all lint style check luacheck style-check style-doc luals style-fix help-update help-check .PHONY: all lint style check luacheck stylua style-doc luals style-fix help-update help-check

View File

@ -162,13 +162,13 @@ nvim-tree exposes a public API. This is non breaking, with additions made as nec
See wiki [Recipes](https://github.com/nvim-tree/nvim-tree.lua/wiki/Recipes) and [Tips](https://github.com/nvim-tree/nvim-tree.lua/wiki/Tips) for ideas and inspiration. See wiki [Recipes](https://github.com/nvim-tree/nvim-tree.lua/wiki/Recipes) and [Tips](https://github.com/nvim-tree/nvim-tree.lua/wiki/Tips) for ideas and inspiration.
Please raise a [feature request](https://github.com/nvim-tree/nvim-tree.lua/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=) if the API is insufficient for your needs. Contributions are always welcome, see below. Please raise a [feature request](https://github.com/nvim-tree/nvim-tree.lua/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=) if the API is insufficient for your needs. [Contributions](#Contributing) are always welcome.
You may also subscribe to events that nvim-tree will dispatch in a variety of situations, see [:help nvim-tree-events](doc/nvim-tree-lua.txt) You may also subscribe to events that nvim-tree will dispatch in a variety of situations, see [:help nvim-tree-events](doc/nvim-tree-lua.txt)
## Contributing ## Contributing
PRs are always welcome. See [CONTRIBUTING](CONTRIBUTING.md) and [wiki: Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) to get started. PRs are always welcome. See [wiki](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) to get started.
See [bug](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and [PR Please](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aopen+is%3Aissue+label%3A%22PR+please%22) issues if you are looking for some work to get you started. See [bug](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and [PR Please](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aopen+is%3Aissue+label%3A%22PR+please%22) issues if you are looking for some work to get you started.

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,15 @@
local log = require("nvim-tree.log") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local log = require "nvim-tree.log"
local utils = require("nvim-tree.utils") local appearance = require "nvim-tree.appearance"
local actions = require("nvim-tree.actions") local renderer = require "nvim-tree.renderer"
local core = require("nvim-tree.core") local commands = require "nvim-tree.commands"
local notify = require("nvim-tree.notify") local utils = require "nvim-tree.utils"
local actions = require "nvim-tree.actions"
local legacy = require "nvim-tree.legacy"
local core = require "nvim-tree.core"
local git = require "nvim-tree.git"
local buffers = require "nvim-tree.buffers"
local notify = require "nvim-tree.notify"
local _config = {} local _config = {}
@ -19,7 +25,7 @@ function M.change_root(path, bufnr)
if type(bufnr) == "number" then if type(bufnr) == "number" then
local ft local ft
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or "" ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or ""
else else
ft = vim.api.nvim_buf_get_option(bufnr, "filetype") or "" ---@diagnostic disable-line: deprecated ft = vim.api.nvim_buf_get_option(bufnr, "filetype") or "" ---@diagnostic disable-line: deprecated
@ -74,11 +80,15 @@ function M.change_root(path, bufnr)
end end
function M.tab_enter() function M.tab_enter()
if view.is_visible({ any_tabpage = true }) then local explorer = core.get_explorer();
if not explorer then
return
end
if explorer.view:is_visible { any_tabpage = true } then
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local ft local ft
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) or "" ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) or ""
else else
ft = vim.api.nvim_buf_get_option(0, "ft") ---@diagnostic disable-line: deprecated ft = vim.api.nvim_buf_get_option(0, "ft") ---@diagnostic disable-line: deprecated
@ -89,17 +99,17 @@ function M.tab_enter()
return return
end end
end end
view.open({ focus_tree = false }) explorer.view:open { focus_tree = false }
renderer.draw()
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end end
end end
function M.open_on_directory() function M.open_on_directory()
local should_proceed = _config.hijack_directories.auto_open or view.is_visible() local explorer = core.get_explorer();
if not explorer then
return
end
local should_proceed = _config.hijack_directories.auto_open or explorer.view:is_visible()
if not should_proceed then if not should_proceed then
return return
end end
@ -113,6 +123,27 @@ function M.open_on_directory()
actions.root.change_dir.force_dirchange(bufname, true) actions.root.change_dir.force_dirchange(bufname, true)
end end
function M.place_cursor_on_node()
local ok, search = pcall(vim.fn.searchcount)
if ok and search and search.exact_match == 1 then
return
end
local node = lib.get_node_at_cursor()
if not node or node.name == ".." then
return
end
node = utils.get_parent_of_group(node)
local line = vim.api.nvim_get_current_line()
local cursor = vim.api.nvim_win_get_cursor(0)
local idx = vim.fn.stridx(line, node.name)
if idx >= 0 then
vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
end
end
---@return table ---@return table
function M.get_config() function M.get_config()
return M.config return M.config
@ -122,8 +153,8 @@ end
---@param hijack_netrw boolean ---@param hijack_netrw boolean
local function manage_netrw(disable_netrw, hijack_netrw) local function manage_netrw(disable_netrw, hijack_netrw)
if hijack_netrw then if hijack_netrw then
vim.cmd("silent! autocmd! FileExplorer *") vim.cmd "silent! autocmd! FileExplorer *"
vim.cmd("autocmd VimEnter * ++once silent! autocmd! FileExplorer *") vim.cmd "autocmd VimEnter * ++once silent! autocmd! FileExplorer *"
end end
if disable_netrw then if disable_netrw then
vim.g.loaded_netrw = 1 vim.g.loaded_netrw = 1
@ -150,6 +181,19 @@ local function setup_autocommands(opts)
vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts)) vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts))
end end
-- reset and draw (highlights) when colorscheme is changed
create_nvim_tree_autocmd("ColorScheme", {
callback = function()
local explorer = core.get_explorer();
if not explorer then
return
end
appearance.setup()
explorer.view:reset_winhl()
renderer.draw()
end,
})
-- prevent new opened file from opening in the same window as nvim-tree -- prevent new opened file from opening in the same window as nvim-tree
create_nvim_tree_autocmd("BufWipeout", { create_nvim_tree_autocmd("BufWipeout", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
@ -157,10 +201,65 @@ local function setup_autocommands(opts)
if not utils.is_nvim_tree_buf(0) then if not utils.is_nvim_tree_buf(0) then
return return
end end
local explorer = core.get_explorer();
if not explorer then
return
end
if opts.actions.open_file.eject then if opts.actions.open_file.eject then
view._prevent_buffer_override() explorer.view:_prevent_buffer_override()
else else
view.abandon_current_window() explorer.view:abandon_current_window()
end
end,
})
create_nvim_tree_autocmd("BufWritePost", {
callback = function()
if opts.auto_reload_on_write and not opts.filesystem_watchers.enable then
actions.reloaders.reload_explorer()
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 renderer.config.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
actions.reloaders.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 renderer.config.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
actions.reloaders.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
actions.reloaders.reload_git()
end end
end, end,
}) })
@ -168,6 +267,16 @@ local function setup_autocommands(opts)
if opts.tab.sync.open then if opts.tab.sync.open then
create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) }) create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
end end
if opts.hijack_cursor then
create_nvim_tree_autocmd("CursorMoved", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
M.place_cursor_on_node()
end
end,
})
end
if opts.sync_root_with_cwd then if opts.sync_root_with_cwd then
create_nvim_tree_autocmd("DirChanged", { create_nvim_tree_autocmd("DirChanged", {
callback = function() callback = function()
@ -190,20 +299,27 @@ local function setup_autocommands(opts)
end end
if opts.hijack_directories.enable then if opts.hijack_directories.enable then
create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory, nested = true }) create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory })
end end
create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
if vim.fn.getcwd() ~= core.get_cwd() or (opts.reload_on_bufenter and not opts.filesystem_watchers.enable) then
actions.reloaders.reload_explorer()
end
end
end,
})
if opts.view.centralize_selection then if opts.view.centralize_selection then
create_nvim_tree_autocmd("BufEnter", { create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
callback = function() callback = function()
vim.schedule(function() vim.schedule(function()
vim.api.nvim_buf_call(0, function() vim.api.nvim_buf_call(0, function()
local is_term_mode = vim.api.nvim_get_mode().mode == "t" vim.cmd [[norm! zz]]
if is_term_mode then
return
end
vim.cmd([[norm! zz]])
end) end)
end) end)
end, end,
@ -212,16 +328,16 @@ local function setup_autocommands(opts)
if opts.diagnostics.enable then if opts.diagnostics.enable then
create_nvim_tree_autocmd("DiagnosticChanged", { create_nvim_tree_autocmd("DiagnosticChanged", {
callback = function(ev) callback = function()
log.line("diagnostics", "DiagnosticChanged") log.line("diagnostics", "DiagnosticChanged")
require("nvim-tree.diagnostics").update_lsp(ev) require("nvim-tree.diagnostics").update()
end, end,
}) })
create_nvim_tree_autocmd("User", { create_nvim_tree_autocmd("User", {
pattern = "CocDiagnosticChange", pattern = "CocDiagnosticChange",
callback = function() callback = function()
log.line("diagnostics", "CocDiagnosticChange") log.line("diagnostics", "CocDiagnosticChange")
require("nvim-tree.diagnostics").update_coc() require("nvim-tree.diagnostics").update()
end, end,
}) })
end end
@ -230,26 +346,27 @@ local function setup_autocommands(opts)
create_nvim_tree_autocmd("WinLeave", { create_nvim_tree_autocmd("WinLeave", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
callback = function() callback = function()
local explorer = core.get_explorer()
if not explorer then
return
end
if utils.is_nvim_tree_buf(0) then if utils.is_nvim_tree_buf(0) then
view.close() explorer.view:close()
end end
end, end,
}) })
end end
-- Handles event dispatch when tree is closed by `:q` if opts.modified.enable then
create_nvim_tree_autocmd("WinClosed", { create_nvim_tree_autocmd({ "BufModifiedSet", "BufWritePost" }, {
pattern = "*", callback = function()
---@param ev vim.api.keyset.create_autocmd.callback_args utils.debounce("Buf:modified", opts.view.debounce_delay, function()
callback = function(ev) buffers.reload_modified()
if not vim.api.nvim_buf_is_valid(ev.buf) then actions.reloaders.reload_explorer()
return end)
end end,
if vim.api.nvim_get_option_value("filetype", { buf = ev.buf }) == "NvimTree" then })
require("nvim-tree.events")._dispatch_on_tree_close() end
end
end,
})
end end
local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
@ -273,7 +390,6 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
view = { view = {
centralize_selection = false, centralize_selection = false,
cursorline = true, cursorline = true,
cursorlineopt = "both",
debounce_delay = 15, debounce_delay = 15,
side = "left", side = "left",
preserve_window_proportions = false, preserve_window_proportions = false,
@ -301,9 +417,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
root_folder_label = ":~:s?$?/..?", root_folder_label = ":~:s?$?/..?",
indent_width = 2, indent_width = 2,
special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" },
hidden_display = "none",
symlink_destination = true, symlink_destination = true,
decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", },
highlight_git = "none", highlight_git = "none",
highlight_diagnostics = "none", highlight_diagnostics = "none",
highlight_opened_files = "none", highlight_opened_files = "none",
@ -338,10 +452,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
hidden_placement = "after", hidden_placement = "after",
diagnostics_placement = "signcolumn", diagnostics_placement = "signcolumn",
bookmarks_placement = "signcolumn", bookmarks_placement = "signcolumn",
padding = { padding = " ",
icon = " ",
folder_arrow = " ",
},
symlink_arrow = "", symlink_arrow = "",
show = { show = {
file = true, file = true,
@ -409,7 +520,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
show_on_open_dirs = true, show_on_open_dirs = true,
debounce_delay = 500, debounce_delay = 50,
severity = { severity = {
min = vim.diagnostic.severity.HINT, min = vim.diagnostic.severity.HINT,
max = vim.diagnostic.severity.ERROR, max = vim.diagnostic.severity.ERROR,
@ -420,7 +531,6 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
warning = "", warning = "",
error = "", error = "",
}, },
diagnostic_opts = false,
}, },
modified = { modified = {
enable = false, enable = false,
@ -444,12 +554,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
filesystem_watchers = { filesystem_watchers = {
enable = true, enable = true,
debounce_delay = 50, debounce_delay = 50,
ignore_dirs = { ignore_dirs = {},
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
}, },
actions = { actions = {
use_system_clipboard = true, use_system_clipboard = true,
@ -475,7 +580,6 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
quit_on_open = false, quit_on_open = false,
eject = true, eject = true,
resize_window = true, resize_window = true,
relative_path = true,
window_picker = { window_picker = {
enable = true, enable = true,
picker = "default", picker = "default",
@ -515,6 +619,11 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
}, },
}, },
experimental = { experimental = {
actions = {
open_file = {
relative_path = false,
},
},
}, },
log = { log = {
enable = false, enable = false,
@ -557,7 +666,6 @@ local ACCEPTED_TYPES = {
}, },
}, },
renderer = { renderer = {
hidden_display = { "function", "string" },
group_empty = { "boolean", "function" }, group_empty = { "boolean", "function" },
root_folder_label = { "function", "string", "boolean" }, root_folder_label = { "function", "string", "boolean" },
}, },
@ -591,7 +699,6 @@ local ACCEPTED_STRINGS = {
signcolumn = { "yes", "no", "auto" }, signcolumn = { "yes", "no", "auto" },
}, },
renderer = { renderer = {
hidden_display = { "none", "simple", "all" },
highlight_git = { "none", "icon", "name", "all" }, highlight_git = { "none", "icon", "name", "all" },
highlight_opened_files = { "none", "icon", "name", "all" }, highlight_opened_files = { "none", "icon", "name", "all" },
highlight_modified = { "none", "icon", "name", "all" }, highlight_modified = { "none", "icon", "name", "all" },
@ -694,22 +801,23 @@ local function localise_default_opts()
end end
function M.purge_all_state() function M.purge_all_state()
view.close_all_tabs() require("nvim-tree.watcher").purge_watchers()
view.abandon_all_windows()
local explorer = core.get_explorer() local explorer = core.get_explorer()
if explorer then if not explorer then
require("nvim-tree.git").purge_state() return
explorer:destroy() end
explorer.view:close_all_tabs()
explorer.view:abandon_all_windows()
if core.get_explorer() ~= nil then
git.purge_state()
core.reset_explorer() core.reset_explorer()
end end
-- purge orphaned that were not destroyed by their nodes
require("nvim-tree.watcher").purge_watchers()
end end
---@param conf table|nil ---@param conf table|nil
function M.setup(conf) function M.setup(conf)
if vim.fn.has("nvim-0.9") == 0 then if vim.fn.has "nvim-0.9" == 0 then
notify.warn("nvim-tree.lua requires Neovim 0.9 or higher") notify.warn "nvim-tree.lua requires Neovim 0.9 or higher"
return return
end end
@ -717,7 +825,7 @@ function M.setup(conf)
localise_default_opts() localise_default_opts()
require("nvim-tree.legacy").migrate_legacy_options(conf or {}) legacy.migrate_legacy_options(conf or {})
validate_options(conf) validate_options(conf)
@ -737,7 +845,7 @@ function M.setup(conf)
require("nvim-tree.notify").setup(opts) require("nvim-tree.notify").setup(opts)
require("nvim-tree.log").setup(opts) require("nvim-tree.log").setup(opts)
if log.enabled("config") then if log.enabled "config" then
log.line("config", "default config + user") log.line("config", "default config + user")
log.raw("config", "%s\n", vim.inspect(opts)) log.raw("config", "%s\n", vim.inspect(opts))
end end
@ -746,22 +854,24 @@ function M.setup(conf)
require("nvim-tree.keymap").setup(opts) require("nvim-tree.keymap").setup(opts)
require("nvim-tree.appearance").setup() require("nvim-tree.appearance").setup()
require("nvim-tree.diagnostics").setup(opts) require("nvim-tree.diagnostics").setup(opts)
require("nvim-tree.explorer"):setup(opts) require("nvim-tree.explorer").setup(opts)
require("nvim-tree.explorer.watch").setup(opts)
require("nvim-tree.git").setup(opts) require("nvim-tree.git").setup(opts)
require("nvim-tree.git.utils").setup(opts) require("nvim-tree.git.utils").setup(opts)
require("nvim-tree.view").setup(opts)
require("nvim-tree.lib").setup(opts) require("nvim-tree.lib").setup(opts)
require("nvim-tree.renderer.components").setup(opts) require("nvim-tree.renderer").setup(opts)
require("nvim-tree.marks").setup(opts)
require("nvim-tree.buffers").setup(opts) require("nvim-tree.buffers").setup(opts)
require("nvim-tree.help").setup(opts) require("nvim-tree.help").setup(opts)
require("nvim-tree.watcher").setup(opts) require("nvim-tree.watcher").setup(opts)
if M.config.renderer.icons.show.file and pcall(require, "nvim-web-devicons") then
require("nvim-web-devicons").setup()
end
setup_autocommands(opts) setup_autocommands(opts)
if vim.g.NvimTreeSetup ~= 1 then if vim.g.NvimTreeSetup ~= 1 then
-- first call to setup -- first call to setup
require("nvim-tree.commands").setup() commands.setup()
else else
-- subsequent calls to setup -- subsequent calls to setup
M.purge_all_state() M.purge_all_state()

View File

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

View File

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

View File

@ -1,10 +1,9 @@
local log = require("nvim-tree.log") local log = require "nvim-tree.log"
local view = require("nvim-tree.view") local utils = require "nvim-tree.utils"
local utils = require("nvim-tree.utils") local renderer = require "nvim-tree.renderer"
local core = require("nvim-tree.core") local reload = require "nvim-tree.explorer.reload"
local core = require "nvim-tree.core"
local DirectoryNode = require("nvim-tree.node.directory") local Iterator = require "nvim-tree.iterators.node-iterator"
local Iterator = require("nvim-tree.iterators.node-iterator")
local M = {} local M = {}
@ -14,7 +13,7 @@ local running = {}
---@param path string relative or absolute ---@param path string relative or absolute
function M.fn(path) function M.fn(path)
local explorer = core.get_explorer() local explorer = core.get_explorer()
if not explorer or not view.is_visible() then if not explorer or not explorer.view:is_visible() then
return return
end end
@ -32,8 +31,8 @@ function M.fn(path)
local profile = log.profile_start("find file %s", path_real) local profile = log.profile_start("find file %s", path_real)
-- refresh the contents of all parents, expanding groups as needed -- refresh the contents of all parents, expanding groups as needed
if explorer:get_node_from_path(path_real) == nil then if utils.get_node_from_path(path_real) == nil then
explorer:refresh_parent_nodes_for_path(vim.fn.fnamemodify(path_real, ":h")) reload.refresh_parent_nodes_for_path(vim.fn.fnamemodify(path_real, ":h"))
end end
local line = core.get_nodes_starting_line() local line = core.get_nodes_starting_line()
@ -60,33 +59,25 @@ function M.fn(path)
local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator) local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator)
if abs_match or link_match then if abs_match or link_match then
local dir = node:as(DirectoryNode) if not node.group_next then
if dir then node.open = true
if not dir.group_next then end
dir.open = true if #node.nodes == 0 then
end core.get_explorer():expand(node)
if #dir.nodes == 0 then if node.group_next and incremented_line then
core.get_explorer():expand(dir) line = line - 1
if dir.group_next and incremented_line then
line = line - 1
end
end end
end end
end end
end) end)
:recursor(function(node) :recursor(function(node)
node = node and node:as(DirectoryNode) return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
if node then
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
else
return nil
end
end) end)
:iterate() :iterate()
if found and view.is_visible() then if found and explorer.view:is_visible() then
explorer.renderer:draw() renderer.draw()
view.set_cursor({ line, 0 }) explorer.view:set_cursor { line, 0 }
end end
running[path_real] = false running[path_real] = false

View File

@ -1,6 +1,6 @@
local M = {} local M = {}
M.find_file = require("nvim-tree.actions.finders.find-file") M.find_file = require "nvim-tree.actions.finders.find-file"
M.search_node = require("nvim-tree.actions.finders.search-node") M.search_node = require "nvim-tree.actions.finders.search-node"
return M return M

View File

@ -1,4 +1,4 @@
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
local M = {} local M = {}
@ -75,7 +75,7 @@ function M.fn()
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local path_existed, path_opt local path_existed, path_opt
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
path_existed, path_opt = pcall(vim.api.nvim_get_option_value, "path", { buf = bufnr }) path_existed, path_opt = pcall(vim.api.nvim_get_option_value, "path", { buf = bufnr })
vim.api.nvim_set_option_value("path", core.get_cwd() .. "/**", { buf = bufnr }) vim.api.nvim_set_option_value("path", core.get_cwd() .. "/**", { buf = bufnr })
else else
@ -89,13 +89,13 @@ function M.fn()
end end
-- reset &path -- reset &path
if path_existed then if path_existed then
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("path", path_opt, { buf = bufnr }) vim.api.nvim_set_option_value("path", path_opt, { buf = bufnr })
else else
vim.api.nvim_buf_set_option(bufnr, "path", path_opt) ---@diagnostic disable-line: deprecated vim.api.nvim_buf_set_option(bufnr, "path", path_opt) ---@diagnostic disable-line: deprecated
end end
else else
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("path", nil, { buf = bufnr }) vim.api.nvim_set_option_value("path", nil, { buf = bufnr })
else else
vim.api.nvim_buf_set_option(bufnr, "path", nil) ---@diagnostic disable-line: deprecated vim.api.nvim_buf_set_option(bufnr, "path", nil) ---@diagnostic disable-line: deprecated

View File

@ -1,391 +0,0 @@
local lib = require("nvim-tree.lib")
local log = require("nvim-tree.log")
local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify")
local find_file = require("nvim-tree.actions.finders.find-file").fn
local Class = require("nvim-tree.classic")
local DirectoryNode = require("nvim-tree.node.directory")
---@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 ClipboardData
---@field private clipboard_name string
---@field private reg string
local Clipboard = Class:extend()
---@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 = {},
}
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
---@param destination string
---@return boolean
---@return string|nil
local function do_copy(source, destination)
local source_stats, err = vim.loop.fs_stat(source)
if not source_stats then
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)
if source == destination then
log.line("copy_paste", "do_copy source and destination are the same, exiting early")
return true
end
if source_stats.type == "file" then
local success
success, err = vim.loop.fs_copyfile(source, destination)
if not success then
log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err)
return false, err
end
return true
elseif source_stats.type == "directory" then
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, err)
return false, err
end
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, err)
return false, err
end
while true do
local name, _ = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local new_name = utils.path_join({ source, name })
local new_destination = utils.path_join({ destination, name })
success, err = do_copy(new_name, new_destination)
if not success then
return false, err
end
end
else
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
end
---@param source string
---@param 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 notify_source = notify.render_path(source)
log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest)
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()
local success, error = action_fn(source, dest)
if not success then
notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???"))
return false, error
end
find_file(utils.path_remove_trailing(dest))
end
if dest_stats then
local input_opts = {
prompt = "Rename to ",
default = dest,
completion = "dir",
}
if source == dest then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action, action_fn)
end
end)
else
local prompt_select = "Overwrite " .. dest .. " ?"
local prompt_input = prompt_select .. " R(ename)/y/n: "
lib.prompt(prompt_input, prompt_select, { "", "y", "n" }, { "Rename", "Yes", "No" }, "nvimtree_overwrite_rename", function(item_short)
utils.clear_prompt()
if item_short == "y" then
on_process()
elseif item_short == "" or item_short == "r" then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action, action_fn)
end
end)
end
end)
end
else
on_process()
end
end
---@param node Node
---@param clip ClipboardData
local function toggle(node, clip)
if node.name == ".." then
return
end
local notify_node = notify.render_path(node.absolute_path)
if utils.array_remove(clip, node) then
notify.info(notify_node .. " removed from clipboard.")
return
end
table.insert(clip, node)
notify.info(notify_node .. " added to clipboard.")
end
---Clear copied and cut
function Clipboard:clear_clipboard()
self.data.copy = {}
self.data.cut = {}
notify.info("Clipboard has been emptied.")
self.explorer.renderer:draw()
end
---Copy one node
---@param node Node
function Clipboard:copy(node)
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.copy, node)
toggle(node, self.data.cut)
self.explorer.renderer:draw()
end
---Paste cut or cop
---@private
---@param node Node
---@param action ClipboardAction
---@param action_fn ClipboardActionFn
function Clipboard:do_paste(node, action, action_fn)
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
return
end
local destination = node.absolute_path
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"
if not is_dir then
destination = vim.fn.fnamemodify(destination, ":p:h")
end
for _, _node in ipairs(clip) do
local dest = utils.path_join({ destination, _node.name })
do_single_paste(_node.absolute_path, dest, action, action_fn)
end
self.data[action] = {}
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
end
end
---@param source string
---@param destination string
---@return boolean
---@return string?
local function do_cut(source, destination)
log.line("copy_paste", "do_cut '%s' -> '%s'", source, destination)
if source == destination then
log.line("copy_paste", "do_cut source and destination are the same, exiting early")
return true
end
events._dispatch_will_rename_node(source, destination)
local success, errmsg = vim.loop.fs_rename(source, destination)
if not success then
log.line("copy_paste", "do_cut fs_rename failed '%s'", errmsg)
return false, errmsg
end
utils.rename_loaded_buffers(source, destination)
events._dispatch_node_renamed(source, destination)
return true
end
---Paste cut (if present) or copy (if present)
---@param node Node
function Clipboard:paste(node)
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.cut > 0 then
table.insert(content, "Cut")
for _, node in pairs(self.data.cut) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end
end
if #self.data.copy > 0 then
table.insert(content, "Copy")
for _, node in pairs(self.data.copy) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end
end
notify.info(table.concat(content, "\n") .. "\n")
end
---@param content string
function Clipboard:copy_to_reg(content)
-- 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$', self.reg))
end)
vim.api.nvim_buf_delete(temp_buf, {})
notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name))
end
---@param node Node
function Clipboard:copy_filename(node)
if node.name == ".." then
-- root
self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t"))
else
-- node
self:copy_to_reg(node.name)
end
end
---@param node Node
function Clipboard:copy_basename(node)
if node.name == ".." then
-- root
self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r"))
else
-- node
self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r"))
end
end
---@param node Node
function Clipboard:copy_path(node)
if node.name == ".." then
-- root
self:copy_to_reg(utils.path_add_trailing(""))
else
-- node
local absolute_path = node.absolute_path
local cwd = core.get_cwd()
if cwd == nil then
return
end
local relative_path = utils.path_relative(absolute_path, cwd)
if node:is(DirectoryNode) then
self:copy_to_reg(utils.path_add_trailing(relative_path))
else
self:copy_to_reg(relative_path)
end
end
end
---@param node Node
function Clipboard:copy_absolute_path(node)
if node.name == ".." then
node = self.explorer
end
local absolute_path = node.absolute_path
local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
self:copy_to_reg(content)
end
---Node is cut. Will not be copied.
---@param node Node
---@return boolean
function Clipboard:is_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.copy, node)
end
return Clipboard

View File

@ -0,0 +1,350 @@
local lib = require "nvim-tree.lib"
local log = require "nvim-tree.log"
local utils = require "nvim-tree.utils"
local core = require "nvim-tree.core"
local events = require "nvim-tree.events"
local notify = require "nvim-tree.notify"
local renderer = require "nvim-tree.renderer"
local reloaders = require "nvim-tree.actions.reloaders"
local find_file = require("nvim-tree.actions.finders.find-file").fn
local M = {
config = {},
}
local clipboard = {
cut = {},
copy = {},
}
---@param source string
---@param destination string
---@return boolean
---@return string|nil
local function do_copy(source, destination)
local source_stats, handle
local success, errmsg
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
end
log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination)
if source == destination then
log.line("copy_paste", "do_copy source and destination are the same, exiting early")
return true
end
if source_stats.type == "file" then
success, errmsg = vim.loop.fs_copyfile(source, destination)
if not success then
log.line("copy_paste", "do_copy fs_copyfile failed '%s'", errmsg)
return false, errmsg
end
return true
elseif source_stats.type == "directory" then
handle, errmsg = 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
end
success, errmsg = 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
end
while true do
local name, _ = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local new_name = utils.path_join { source, name }
local new_destination = utils.path_join { destination, name }
success, errmsg = do_copy(new_name, new_destination)
if not success then
return false, errmsg
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
end
return true
end
---@param source string
---@param dest string
---@param action_type string
---@param action_fn fun(source: string, dest: string)
---@return boolean|nil -- success
---@return string|nil -- error message
local function do_single_paste(source, dest, action_type, 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_type .. " " .. notify_source .. " - " .. (errmsg or "???"))
return false, errmsg
end
local function on_process()
success, errmsg = action_fn(source, dest)
if not success then
notify.error("Could not " .. action_type .. " " .. notify_source .. " - " .. (errmsg or "???"))
return false, errmsg
end
find_file(utils.path_remove_trailing(dest))
end
if dest_stats then
local input_opts = {
prompt = "Rename to ",
default = dest,
completion = "dir",
}
if source == dest then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action_type, action_fn)
end
end)
else
local prompt_select = "Overwrite " .. dest .. " ?"
local prompt_input = prompt_select .. " R(ename)/y/n: "
lib.prompt(prompt_input, prompt_select, { "", "y", "n" }, { "Rename", "Yes", "No" }, "nvimtree_overwrite_rename", function(item_short)
utils.clear_prompt()
if item_short == "y" then
on_process()
elseif item_short == "" or item_short == "r" then
vim.ui.input(input_opts, function(new_dest)
utils.clear_prompt()
if new_dest then
do_single_paste(source, new_dest, action_type, action_fn)
end
end)
end
end)
end
else
on_process()
end
end
---@param node Node
---@param clip table
local function toggle(node, clip)
if node.name == ".." then
return
end
local notify_node = notify.render_path(node.absolute_path)
if utils.array_remove(clip, node) then
notify.info(notify_node .. " removed from clipboard.")
return
end
table.insert(clip, node)
notify.info(notify_node .. " added to clipboard.")
end
function M.clear_clipboard()
clipboard.cut = {}
clipboard.copy = {}
notify.info "Clipboard has been emptied."
renderer.draw()
end
---@param node Node
function M.copy(node)
utils.array_remove(clipboard.cut, node)
toggle(node, clipboard.copy)
renderer.draw()
end
---@param node Node
function M.cut(node)
utils.array_remove(clipboard.copy, node)
toggle(node, clipboard.cut)
renderer.draw()
end
---@param node Node
---@param action_type string
---@param action_fn fun(source: string, dest: string)
local function do_paste(node, action_type, action_fn)
node = lib.get_last_group_node(node)
local explorer = core.get_explorer()
if node.name == ".." and explorer then
node = explorer
end
local clip = clipboard[action_type]
if #clip == 0 then
return
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_type .. " " .. notify.render_path(destination) .. " - " .. (errmsg or "???"))
return
end
local is_dir = stats and stats.type == "directory"
if not is_dir then
destination = vim.fn.fnamemodify(destination, ":p:h")
end
for _, _node in ipairs(clip) do
local dest = utils.path_join { destination, _node.name }
do_single_paste(_node.absolute_path, dest, action_type, action_fn)
end
clipboard[action_type] = {}
if not M.config.filesystem_watchers.enable then
reloaders.reload_explorer()
end
end
---@param source string
---@param destination string
---@return boolean
---@return string?
local function do_cut(source, destination)
log.line("copy_paste", "do_cut '%s' -> '%s'", source, destination)
if source == destination then
log.line("copy_paste", "do_cut source and destination are the same, exiting early")
return true
end
events._dispatch_will_rename_node(source, destination)
local success, errmsg = vim.loop.fs_rename(source, destination)
if not success then
log.line("copy_paste", "do_cut fs_rename failed '%s'", errmsg)
return false, errmsg
end
utils.rename_loaded_buffers(source, destination)
events._dispatch_node_renamed(source, destination)
return true
end
---@param node Node
function M.paste(node)
if clipboard.cut[1] ~= nil then
do_paste(node, "cut", do_cut)
else
do_paste(node, "copy", do_copy)
end
end
function M.print_clipboard()
local content = {}
if #clipboard.cut > 0 then
table.insert(content, "Cut")
for _, node in pairs(clipboard.cut) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end
end
if #clipboard.copy > 0 then
table.insert(content, "Copy")
for _, node in pairs(clipboard.copy) do
table.insert(content, " * " .. (notify.render_path(node.absolute_path)))
end
end
notify.info(table.concat(content, "\n") .. "\n")
end
---@param content string
local function copy_to_clipboard(content)
local clipboard_name
local reg
if M.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))
end)
vim.api.nvim_buf_delete(temp_buf, {})
notify.info(string.format("Copied %s to %s clipboard!", content, clipboard_name))
end
---@param node Node
function M.copy_filename(node)
copy_to_clipboard(node.name)
end
---@param node Node
function M.copy_basename(node)
local basename = vim.fn.fnamemodify(node.name, ":r")
copy_to_clipboard(basename)
end
---@param node Node
function M.copy_path(node)
local absolute_path = node.absolute_path
local cwd = core.get_cwd()
if cwd == nil then
return
end
local relative_path = utils.path_relative(absolute_path, cwd)
local content = node.nodes ~= nil and utils.path_add_trailing(relative_path) or relative_path
copy_to_clipboard(content)
end
---@param node Node
function M.copy_absolute_path(node)
local absolute_path = node.absolute_path
local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path
copy_to_clipboard(content)
end
---Node is cut. Will not be copied.
---@param node Node
---@return boolean
function M.is_cut(node)
return vim.tbl_contains(clipboard.cut, node)
end
---Node is copied. Will not be cut.
---@param node Node
---@return boolean
function M.is_copied(node)
return vim.tbl_contains(clipboard.copy, node)
end
function M.setup(opts)
M.config.filesystem_watchers = opts.filesystem_watchers
M.config.actions = opts.actions
end
return M

View File

@ -1,13 +1,11 @@
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local events = require("nvim-tree.events") local events = require "nvim-tree.events"
local core = require("nvim-tree.core") local lib = require "nvim-tree.lib"
local notify = require("nvim-tree.notify") local core = require "nvim-tree.core"
local notify = require "nvim-tree.notify"
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@param file string ---@param file string
@ -32,21 +30,34 @@ local function get_num_nodes(iter)
return i return i
end end
---@param node Node? ---@param node Node
---@return string
local function get_containing_folder(node)
if node.nodes ~= nil then
return utils.path_add_trailing(node.absolute_path)
end
local node_name_size = #(node.name or "")
return node.absolute_path:sub(0, -node_name_size - 1)
end
---@param node Node|nil
function M.fn(node) function M.fn(node)
node = node or core.get_explorer() local cwd = core.get_cwd()
if not node then if cwd == nil then
return return
end end
local dir = node:is(FileNode) and node.parent or node:as(DirectoryNode) node = node and lib.get_last_group_node(node)
if not dir then if not node or node.name == ".." then
return node = {
absolute_path = cwd,
name = "",
nodes = core.get_explorer().nodes,
open = true,
}
end end
dir = dir:last_group_node() local containing_folder = get_containing_folder(node)
local containing_folder = utils.path_add_trailing(dir.absolute_path)
local input_opts = { local input_opts = {
prompt = "Create file ", prompt = "Create file ",
@ -61,7 +72,7 @@ function M.fn(node)
end end
if utils.file_exists(new_file_path) then if utils.file_exists(new_file_path) then
notify.warn("Cannot create: file already exists") notify.warn "Cannot create: file already exists"
return return
end end
@ -76,10 +87,10 @@ function M.fn(node)
for path in utils.path_split(new_file_path) do for path in utils.path_split(new_file_path) do
idx = idx + 1 idx = idx + 1
local p = utils.path_remove_trailing(path) local p = utils.path_remove_trailing(path)
if #path_to_create == 0 and vim.fn.has("win32") == 1 then if #path_to_create == 0 and vim.fn.has "win32" == 1 then
path_to_create = utils.path_join({ p, path_to_create }) path_to_create = utils.path_join { p, path_to_create }
else else
path_to_create = utils.path_join({ path_to_create, p }) path_to_create = utils.path_join { path_to_create, p }
end end
if is_last_path_file and idx == num_nodes then if is_last_path_file and idx == num_nodes then
create_and_notify(path_to_create) create_and_notify(path_to_create)

View File

@ -1,11 +1,13 @@
local M = {} local M = {}
M.create_file = require("nvim-tree.actions.fs.create-file") M.copy_paste = require "nvim-tree.actions.fs.copy-paste"
M.remove_file = require("nvim-tree.actions.fs.remove-file") M.create_file = require "nvim-tree.actions.fs.create-file"
M.rename_file = require("nvim-tree.actions.fs.rename-file") M.remove_file = require "nvim-tree.actions.fs.remove-file"
M.trash = require("nvim-tree.actions.fs.trash") M.rename_file = require "nvim-tree.actions.fs.rename-file"
M.trash = require "nvim-tree.actions.fs.trash"
function M.setup(opts) function M.setup(opts)
M.copy_paste.setup(opts)
M.remove_file.setup(opts) M.remove_file.setup(opts)
M.rename_file.setup(opts) M.rename_file.setup(opts)
M.trash.setup(opts) M.trash.setup(opts)

View File

@ -1,12 +1,7 @@
local core = require("nvim-tree.core") local utils = require "nvim-tree.utils"
local utils = require("nvim-tree.utils") local events = require "nvim-tree.events"
local events = require("nvim-tree.events") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local notify = require "nvim-tree.notify"
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 = { local M = {
config = {}, config = {},
@ -14,10 +9,14 @@ local M = {
---@param windows integer[] ---@param windows integer[]
local function close_windows(windows) local function close_windows(windows)
local explorer = require "nvim-tree.core".get_explorer()
if not explorer then
return
end
-- Prevent from closing when the win count equals 1 or 2, -- Prevent from closing when the win count equals 1 or 2,
-- where the win to remove could be the last opened. -- where the win to remove could be the last opened.
-- For details see #2503. -- For details see #2503.
if view.View.float.enable and #vim.api.nvim_list_wins() < 3 then if explorer.view.View.float.enable and #vim.api.nvim_list_wins() < 3 then
return return
end end
@ -30,16 +29,20 @@ end
---@param absolute_path string ---@param absolute_path string
local function clear_buffer(absolute_path) local function clear_buffer(absolute_path)
local bufs = vim.fn.getbufinfo({ bufloaded = 1, buflisted = 1 }) local explorer = require "nvim-tree.core".get_explorer()
if not explorer then
return
end
local bufs = vim.fn.getbufinfo { bufloaded = 1, buflisted = 1 }
for _, buf in pairs(bufs) do for _, buf in pairs(bufs) do
if buf.name == absolute_path then if buf.name == absolute_path then
local tree_winnr = vim.api.nvim_get_current_win() local tree_winnr = vim.api.nvim_get_current_win()
if buf.hidden == 0 and (#bufs > 1 or view.View.float.enable) then if buf.hidden == 0 and (#bufs > 1 or explorer.view.View.float.enable) then
vim.api.nvim_set_current_win(buf.windows[1]) vim.api.nvim_set_current_win(buf.windows[1])
vim.cmd(":bn") vim.cmd ":bn"
end end
vim.api.nvim_buf_delete(buf.bufnr, { force = true }) vim.api.nvim_buf_delete(buf.bufnr, { force = true })
if not view.View.float.quit_on_focus_loss then if not explorer.view.View.float.quit_on_focus_loss then
vim.api.nvim_set_current_win(tree_winnr) vim.api.nvim_set_current_win(tree_winnr)
end end
if M.config.actions.remove_file.close_window then if M.config.actions.remove_file.close_window then
@ -60,25 +63,13 @@ local function remove_dir(cwd)
end end
while true do while true do
local name, _ = vim.loop.fs_scandir_next(handle) local name, t = vim.loop.fs_scandir_next(handle)
if not name then if not name then
break break
end end
local new_cwd = utils.path_join({ cwd, name }) local new_cwd = utils.path_join { cwd, name }
if t == "directory" then
-- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility
local stat = vim.loop.fs_stat(new_cwd)
-- TODO remove once 0.12 is the minimum neovim version
-- path incorrectly specified as an integer, fixed upstream for neovim 0.12 https://github.com/neovim/neovim/pull/33872
---@diagnostic disable-next-line: param-type-mismatch
local lstat = vim.loop.fs_lstat(new_cwd)
local type = stat and stat.type or nil
-- Checks if file is a link file to ensure deletion of the symlink instead of the file it points to
local ltype = lstat and lstat.type or nil
if type == "directory" and ltype ~= "link" then
local success = remove_dir(new_cwd) local success = remove_dir(new_cwd)
if not success then if not success then
return false return false
@ -99,7 +90,7 @@ end
---@param node Node ---@param node Node
function M.remove(node) function M.remove(node)
local notify_node = notify.render_path(node.absolute_path) local notify_node = notify.render_path(node.absolute_path)
if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then if node.nodes ~= nil and not node.link_to then
local success = remove_dir(node.absolute_path) local success = remove_dir(node.absolute_path)
if not success then if not success then
notify.error("Could not remove " .. notify_node) notify.error("Could not remove " .. notify_node)
@ -127,9 +118,8 @@ function M.fn(node)
local function do_remove() local function do_remove()
M.remove(node) M.remove(node)
local explorer = core.get_explorer() if not M.config.filesystem_watchers.enable then
if not M.config.filesystem_watchers.enable and explorer then require("nvim-tree.actions.reloaders").reload_explorer()
explorer:reload_explorer()
end end
end end

View File

@ -1,12 +1,10 @@
local core = require("nvim-tree.core") local lib = require "nvim-tree.lib"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local events = require("nvim-tree.events") local events = require "nvim-tree.events"
local notify = require("nvim-tree.notify") local notify = require "nvim-tree.notify"
local find_file = require("nvim-tree.actions.finders.find-file").fn local find_file = require("nvim-tree.actions.finders.find-file").fn
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
@ -65,10 +63,10 @@ function M.rename(node, to)
idx = idx + 1 idx = idx + 1
local p = utils.path_remove_trailing(path) local p = utils.path_remove_trailing(path)
if #path_to_create == 0 and vim.fn.has("win32") == 1 then if #path_to_create == 0 and vim.fn.has "win32" == 1 then
path_to_create = utils.path_join({ p, path_to_create }) path_to_create = utils.path_join { p, path_to_create }
else else
path_to_create = utils.path_join({ path_to_create, p }) path_to_create = utils.path_join { path_to_create, p }
end end
if idx == num_nodes then if idx == num_nodes then
@ -103,15 +101,11 @@ function M.fn(default_modifier)
default_modifier = default_modifier or ":t" default_modifier = default_modifier or ":t"
return function(node, modifier) return function(node, modifier)
local explorer = core.get_explorer() if type(node) ~= "table" then
if not explorer then node = lib.get_node_at_cursor()
return
end end
if type(node) ~= "table" then if node == nil then
node = explorer:get_node_at_cursor()
end
if not node then
return return
end end
@ -125,10 +119,7 @@ function M.fn(default_modifier)
return return
end end
local dir = node:as(DirectoryNode) node = lib.get_last_group_node(node)
if dir then
node = dir:last_group_node()
end
if node.name == ".." then if node.name == ".." then
return return
end end
@ -162,14 +153,12 @@ function M.fn(default_modifier)
return return
end end
local full_new_path = prepend .. new_file_path .. append M.rename(node, prepend .. new_file_path .. append)
M.rename(node, full_new_path)
if not M.config.filesystem_watchers.enable then if not M.config.filesystem_watchers.enable then
explorer:reload_explorer() require("nvim-tree.actions.reloaders").reload_explorer()
end end
find_file(utils.path_remove_trailing(full_new_path)) find_file(utils.path_remove_trailing(new_file_path))
end) end)
end end
end end

View File

@ -1,26 +1,23 @@
local core = require("nvim-tree.core") local lib = require "nvim-tree.lib"
local lib = require("nvim-tree.lib") local notify = require "nvim-tree.notify"
local notify = require("nvim-tree.notify") local reloaders = require "nvim-tree.actions.reloaders"
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local events = require("nvim-tree.events") local events = require "nvim-tree.events"
---@param absolute_path string ---@param absolute_path string
local function clear_buffer(absolute_path) local function clear_buffer(absolute_path)
local bufs = vim.fn.getbufinfo({ bufloaded = 1, buflisted = 1 }) local bufs = vim.fn.getbufinfo { bufloaded = 1, buflisted = 1 }
for _, buf in pairs(bufs) do for _, buf in pairs(bufs) do
if buf.name == absolute_path then if buf.name == absolute_path then
if buf.hidden == 0 and #bufs > 1 then if buf.hidden == 0 and #bufs > 1 then
local winnr = vim.api.nvim_get_current_win() local winnr = vim.api.nvim_get_current_win()
vim.api.nvim_set_current_win(buf.windows[1]) vim.api.nvim_set_current_win(buf.windows[1])
vim.cmd(":bn") vim.cmd ":bn"
vim.api.nvim_set_current_win(winnr) vim.api.nvim_set_current_win(winnr)
end end
vim.api.nvim_buf_delete(buf.bufnr, {}) vim.api.nvim_buf_delete(buf.bufnr, {})
@ -51,21 +48,19 @@ function M.remove(node)
on_stderr = on_stderr, on_stderr = on_stderr,
}) })
if need_sync_wait then if need_sync_wait then
vim.fn.jobwait({ job }) vim.fn.jobwait { job }
end end
end end
local explorer = core.get_explorer() if node.nodes ~= nil and not node.link_to then
if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
trash_path(function(_, rc) trash_path(function(_, rc)
if rc ~= 0 then if rc ~= 0 then
notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash")
return return
end end
events._dispatch_folder_removed(node.absolute_path) events._dispatch_folder_removed(node.absolute_path)
if not M.config.filesystem_watchers.enable and explorer then if not M.config.filesystem_watchers.enable then
explorer:reload_explorer() reloaders.reload_explorer()
end end
end) end)
else else
@ -77,8 +72,8 @@ function M.remove(node)
end end
events._dispatch_file_removed(node.absolute_path) events._dispatch_file_removed(node.absolute_path)
clear_buffer(node.absolute_path) clear_buffer(node.absolute_path)
if not M.config.filesystem_watchers.enable and explorer then if not M.config.filesystem_watchers.enable then
explorer:reload_explorer() reloaders.reload_explorer()
end end
end) end)
end end

View File

@ -1,11 +1,12 @@
local M = {} local M = {}
M.finders = require("nvim-tree.actions.finders") M.finders = require "nvim-tree.actions.finders"
M.fs = require("nvim-tree.actions.fs") M.fs = require "nvim-tree.actions.fs"
M.moves = require("nvim-tree.actions.moves") M.moves = require "nvim-tree.actions.moves"
M.node = require("nvim-tree.actions.node") M.node = require "nvim-tree.actions.node"
M.root = require("nvim-tree.actions.root") M.reloaders = require "nvim-tree.actions.reloaders"
M.tree = require("nvim-tree.actions.tree") M.root = require "nvim-tree.actions.root"
M.tree = require "nvim-tree.actions.tree"
function M.setup(opts) function M.setup(opts)
M.fs.setup(opts) M.fs.setup(opts)

View File

@ -1,7 +1,7 @@
local M = {} local M = {}
M.item = require("nvim-tree.actions.moves.item") M.item = require "nvim-tree.actions.moves.item"
M.parent = require("nvim-tree.actions.moves.parent") M.parent = require "nvim-tree.actions.moves.parent"
M.sibling = require("nvim-tree.actions.moves.sibling") M.sibling = require "nvim-tree.actions.moves.sibling"
return M return M

View File

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

View File

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

View File

@ -1,5 +1,6 @@
local core = require("nvim-tree.core") local utils = require "nvim-tree.utils"
local Iterator = require("nvim-tree.iterators.node-iterator") local core = require "nvim-tree.core"
local Iterator = require "nvim-tree.iterators.node-iterator"
local M = {} local M = {}
@ -11,15 +12,10 @@ function M.fn(direction)
return return
end end
local explorer = core.get_explorer()
if not explorer then
return
end
local first, last, next, prev = nil, nil, nil, nil local first, last, next, prev = nil, nil, nil, nil
local found = false local found = false
local parent = node.parent or explorer local parent = node.parent or core.get_explorer()
Iterator.builder(parent and parent.nodes or {}) Iterator.builder(parent.nodes)
:recursor(function() :recursor(function()
return nil return nil
end) end)
@ -49,7 +45,7 @@ function M.fn(direction)
end end
if target_node then if target_node then
explorer:focus_node_or_parent(target_node) utils.focus_file(target_node.absolute_path)
end end
end end
end end

View File

@ -1,58 +0,0 @@
-- Copyright 2019 Yazdani Kiyan under MIT License
local notify = require("nvim-tree.notify")
local M = {}
---@param node Node
---@param opts ApiNodeDeleteWipeBufferOpts|nil
---@return nil
function M.delete(node, opts)
M.delete_buffer("delete", node.absolute_path, opts)
end
---@param node Node
---@param opts ApiNodeDeleteWipeBufferOpts|nil
---@return nil
function M.wipe(node, opts)
M.delete_buffer("wipe", node.absolute_path, opts)
end
---@alias ApiNodeDeleteWipeBufferMode '"delete"'|'"wipe"'
---@param mode ApiNodeDeleteWipeBufferMode
---@param filename string
---@param opts ApiNodeDeleteWipeBufferOpts|nil
---@return nil
function M.delete_buffer(mode, filename, opts)
if type(mode) ~= "string" then
mode = "delete"
end
local buf_fn = vim.cmd.bdelete
if mode == "wipe" then
buf_fn = vim.cmd.bwipe
end
opts = opts or { force = false }
local notify_node = notify.render_path(filename)
-- check if buffer for file at cursor exists and if it is loaded
local bufnr_at_filename = vim.fn.bufnr(filename)
if bufnr_at_filename == -1 or vim.fn.getbufinfo(bufnr_at_filename)[1].loaded == 0 then
notify.info("No loaded buffer coincides with " .. notify_node)
return
end
local force = opts.force
-- check if buffer is modified
local buf_modified = vim.fn.getbufinfo(bufnr_at_filename)[1].changed
if not force and buf_modified == 1 then
notify.error("Buffer for file " .. notify_node .. " is modified")
return
end
buf_fn({ filename, bang = force })
end
return M

View File

@ -1,4 +1,4 @@
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local M = {} local M = {}
@ -50,17 +50,14 @@ local function setup_window(node)
file_path = node.absolute_path, file_path = node.absolute_path,
} }
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = "wipe"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.api.nvim_win_set_buf(winnr, bufnr) vim.api.nvim_win_set_buf(winnr, bufnr)
end end
function M.close_popup() function M.close_popup()
if current_popup ~= nil then if current_popup ~= nil then
if vim.api.nvim_win_is_valid(current_popup.winnr) then vim.api.nvim_win_close(current_popup.winnr, true)
vim.api.nvim_win_close(current_popup.winnr, true) vim.cmd "augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END"
end
vim.cmd("augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END")
current_popup = nil current_popup = nil
end end

View File

@ -1,10 +1,9 @@
local M = {} local M = {}
M.file_popup = require("nvim-tree.actions.node.file-popup") M.file_popup = require "nvim-tree.actions.node.file-popup"
M.open_file = require("nvim-tree.actions.node.open-file") M.open_file = require "nvim-tree.actions.node.open-file"
M.run_command = require("nvim-tree.actions.node.run-command") M.run_command = require "nvim-tree.actions.node.run-command"
M.system_open = require("nvim-tree.actions.node.system-open") M.system_open = require "nvim-tree.actions.node.system-open"
M.buffer = require("nvim-tree.actions.node.buffer")
function M.setup(opts) function M.setup(opts)
require("nvim-tree.actions.node.system-open").setup(opts) require("nvim-tree.actions.node.system-open").setup(opts)

View File

@ -1,9 +1,7 @@
-- Copyright 2019 Yazdani Kiyan under MIT License -- Copyright 2019 Yazdani Kiyan under MIT License
local lib = require("nvim-tree.lib") local lib = require "nvim-tree.lib"
local notify = require("nvim-tree.notify") local notify = require "nvim-tree.notify"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local full_name = require("nvim-tree.renderer.components.full-name")
local view = require("nvim-tree.view")
local M = {} local M = {}
@ -20,15 +18,17 @@ end
---Get all windows in the current tabpage that aren't NvimTree. ---Get all windows in the current tabpage that aren't NvimTree.
---@return table with valid win_ids ---@return table with valid win_ids
local function usable_win_ids() local function usable_win_ids()
local explorer = require "nvim-tree.core".get_explorer()
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
local win_ids = vim.api.nvim_tabpage_list_wins(tabpage) local win_ids = vim.api.nvim_tabpage_list_wins(tabpage)
local tree_winid = view.get_winnr(tabpage) local tree_winid = explorer and explorer.view:get_winnr(tabpage)
return vim.tbl_filter(function(id) return vim.tbl_filter(function(id)
local bufid = vim.api.nvim_win_get_buf(id) local bufid = vim.api.nvim_win_get_buf(id)
for option, v in pairs(M.window_picker.exclude) do for option, v in pairs(M.window_picker.exclude) do
local ok, option_value local ok, option_value
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
ok, option_value = pcall(vim.api.nvim_get_option_value, option, { buf = bufid }) ok, option_value = pcall(vim.api.nvim_get_option_value, option, { buf = bufid })
else else
ok, option_value = pcall(vim.api.nvim_buf_get_option, bufid, option) ---@diagnostic disable-line: deprecated ok, option_value = pcall(vim.api.nvim_buf_get_option, bufid, option) ---@diagnostic disable-line: deprecated
@ -40,15 +40,21 @@ local function usable_win_ids()
end end
local win_config = vim.api.nvim_win_get_config(id) local win_config = vim.api.nvim_win_get_config(id)
return id ~= tree_winid return id ~= tree_winid and win_config.focusable and not win_config.external or false
and id ~= full_name.popup_win
and win_config.focusable
and not win_config.hide
and not win_config.external
or false
end, win_ids) end, win_ids)
end end
---Find the first window in the tab that is not NvimTree.
---@return integer -1 if none available
local function first_win_id()
local selectable = usable_win_ids()
if #selectable > 0 then
return selectable[1]
else
return -1
end
end
---Get user to pick a window in the tab that is not NvimTree. ---Get user to pick a window in the tab that is not NvimTree.
---@return integer|nil -- If a valid window was picked, return its id. If an ---@return integer|nil -- If a valid window was picked, return its id. If an
--- invalid window was picked / user canceled, return nil. If there are --- invalid window was picked / user canceled, return nil. If there are
@ -70,19 +76,10 @@ local function pick_win_id()
end end
local i = 1 local i = 1
local win_opts_selectable = {} local win_opts = {}
local win_opts_unselectable = {}
local win_map = {} local win_map = {}
local laststatus = vim.o.laststatus local laststatus = vim.o.laststatus
vim.o.laststatus = 2 vim.o.laststatus = 2
local fillchars = vim.opt.fillchars:get()
local stl = fillchars.stl
local stlnc = fillchars.stlnc
fillchars.stl = nil
fillchars.stlnc = nil
vim.opt.fillchars = fillchars
fillchars.stl = stl
fillchars.stlnc = stlnc
local tabpage = vim.api.nvim_get_current_tabpage() local tabpage = vim.api.nvim_get_current_tabpage()
local win_ids = vim.api.nvim_tabpage_list_wins(tabpage) local win_ids = vim.api.nvim_tabpage_list_wins(tabpage)
@ -93,20 +90,23 @@ local function pick_win_id()
if laststatus == 3 then if laststatus == 3 then
for _, win_id in ipairs(not_selectable) do for _, win_id in ipairs(not_selectable) do
local ok_status, statusline local ok_status, statusline, ok_hl, winhl
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id }) ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id })
ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = win_id })
else else
ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, win_id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts_unselectable[win_id] = { win_opts[win_id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "",
} }
-- Clear statusline for windows not selectable -- Clear statusline for windows not selectable
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("statusline", " ", { win = win_id }) vim.api.nvim_set_option_value("statusline", " ", { win = win_id })
else else
vim.api.nvim_win_set_option(win_id, "statusline", " ") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(win_id, "statusline", " ") ---@diagnostic disable-line: deprecated
@ -119,7 +119,7 @@ local function pick_win_id()
local char = M.window_picker.chars:sub(i, i) local char = M.window_picker.chars:sub(i, i)
local ok_status, statusline, ok_hl, winhl local ok_status, statusline, ok_hl, winhl
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = id }) ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = id })
ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = id }) ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = id })
else else
@ -127,18 +127,18 @@ local function pick_win_id()
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts_selectable[id] = { win_opts[id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "", winhl = ok_hl and winhl or "",
} }
win_map[char] = id win_map[char] = id
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id }) vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id })
vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id }) vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id })
else else
vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated
vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated
end end
i = i + 1 i = i + 1
@ -147,9 +147,9 @@ local function pick_win_id()
end end
end end
vim.cmd("redraw") vim.cmd "redraw"
if vim.opt.cmdheight._value ~= 0 then if vim.opt.cmdheight._value ~= 0 then
print("Pick window: ") print "Pick window: "
end end
local _, resp = pcall(get_user_input_char) local _, resp = pcall(get_user_input_char)
resp = (resp or ""):upper() resp = (resp or ""):upper()
@ -157,8 +157,8 @@ local function pick_win_id()
-- Restore window options -- Restore window options
for _, id in ipairs(selectable) do for _, id in ipairs(selectable) do
for opt, value in pairs(win_opts_selectable[id]) do for opt, value in pairs(win_opts[id]) do
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_set_option_value(opt, value, { win = id })
else else
vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated
@ -168,21 +168,17 @@ local function pick_win_id()
if laststatus == 3 then if laststatus == 3 then
for _, id in ipairs(not_selectable) do for _, id in ipairs(not_selectable) do
-- Ensure window still exists at this point for opt, value in pairs(win_opts[id]) do
if vim.api.nvim_win_is_valid(id) then if vim.fn.has "nvim-0.10" == 1 then
for opt, value in pairs(win_opts_unselectable[id]) do vim.api.nvim_set_option_value(opt, value, { win = id })
if vim.fn.has("nvim-0.10") == 1 then else
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated
else
vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated
end
end end
end end
end end
end end
vim.o.laststatus = laststatus vim.o.laststatus = laststatus
vim.opt.fillchars = fillchars
if not vim.tbl_contains(vim.split(M.window_picker.chars, ""), resp) then if not vim.tbl_contains(vim.split(M.window_picker.chars, ""), resp) then
return return
@ -193,25 +189,23 @@ end
local function open_file_in_tab(filename) local function open_file_in_tab(filename)
if M.quit_on_open then if M.quit_on_open then
view.close() local explorer = require "nvim-tree.core".get_explorer()
if explorer then
explorer.view:close()
end
end end
if M.relative_path then if M.relative_path then
filename = utils.path_relative(filename, vim.fn.getcwd()) filename = utils.path_relative(filename, vim.fn.getcwd())
end end
vim.cmd.tabnew() vim.cmd("tabe " .. vim.fn.fnameescape(filename))
vim.bo.bufhidden = "wipe"
-- Following vim.fn.tabnew the # buffer may be set to the tree buffer. There is no way to clear the # buffer via vim.fn.setreg as it requires a valid buffer. Clear # by setting it to a new temporary scratch buffer.
if utils.is_nvim_tree_buf(vim.fn.bufnr("#")) then
local tmpbuf = vim.api.nvim_create_buf(false, true)
vim.fn.setreg("#", tmpbuf)
vim.api.nvim_buf_delete(tmpbuf, { force = true })
end
vim.cmd.edit(vim.fn.fnameescape(filename))
end end
local function drop(filename) local function drop(filename)
if M.quit_on_open then if M.quit_on_open then
view.close() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:close()
end
end end
if M.relative_path then if M.relative_path then
filename = utils.path_relative(filename, vim.fn.getcwd()) filename = utils.path_relative(filename, vim.fn.getcwd())
@ -221,7 +215,10 @@ end
local function tab_drop(filename) local function tab_drop(filename)
if M.quit_on_open then if M.quit_on_open then
view.close() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:close()
end
end end
if M.relative_path then if M.relative_path then
filename = utils.path_relative(filename, vim.fn.getcwd()) filename = utils.path_relative(filename, vim.fn.getcwd())
@ -242,21 +239,20 @@ local function on_preview(buf_loaded)
once = true, once = true,
}) })
end end
view.focus() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:focus()
end
end end
local function get_target_winid(mode) local function get_target_winid(mode)
local target_winid local target_winid
if not M.window_picker.enable or string.find(mode, "no_picker") then if not M.window_picker.enable or mode == "edit_no_picker" or mode == "preview_no_picker" then
target_winid = lib.target_winid target_winid = lib.target_winid
local usable_wins = usable_win_ids()
-- first available usable window -- first available window
if not vim.tbl_contains(usable_wins, target_winid) then if not vim.tbl_contains(vim.api.nvim_tabpage_list_wins(0), target_winid) then
if #usable_wins > 0 then target_winid = first_win_id()
target_winid = usable_wins[1]
else
target_winid = -1
end
end end
else else
-- pick a window -- pick a window
@ -296,11 +292,6 @@ local function open_in_new_window(filename, mode)
return return
end end
local position = string.find(mode, "no_picker")
if position then
mode = string.sub(mode, 0, position - 2)
end
-- non-floating, non-nvim-tree windows -- non-floating, non-nvim-tree windows
local win_ids = vim.tbl_filter(function(id) local win_ids = vim.tbl_filter(function(id)
local config = vim.api.nvim_win_get_config(id) local config = vim.api.nvim_win_get_config(id)
@ -309,7 +300,8 @@ local function open_in_new_window(filename, mode)
end, vim.api.nvim_list_wins()) end, vim.api.nvim_list_wins())
local create_new_window = #win_ids == 1 -- This implies that the nvim-tree window is the only one local create_new_window = #win_ids == 1 -- This implies that the nvim-tree window is the only one
local new_window_side = (view.View.side == "right") and "aboveleft" or "belowright" local explorer = require"nvim-tree.core".get_explorer()
local new_window_side = (explorer and view.View.side == "right") and "aboveleft" or "belowright"
-- Target is invalid: create new window -- Target is invalid: create new window
if not vim.tbl_contains(win_ids, target_winid) then if not vim.tbl_contains(win_ids, target_winid) then
@ -319,7 +311,7 @@ local function open_in_new_window(filename, mode)
-- No need to split, as we created a new window. -- No need to split, as we created a new window.
create_new_window = false create_new_window = false
if mode:match("split$") then if mode:match "split$" then
mode = "edit" mode = "edit"
end end
elseif not vim.o.hidden then elseif not vim.o.hidden then
@ -328,20 +320,20 @@ local function open_in_new_window(filename, mode)
local target_bufid = vim.api.nvim_win_get_buf(target_winid) local target_bufid = vim.api.nvim_win_get_buf(target_winid)
local modified local modified
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
modified = vim.api.nvim_get_option_value("modified", { buf = target_bufid }) modified = vim.api.nvim_get_option_value("modified", { buf = target_bufid })
else else
modified = vim.api.nvim_buf_get_option(target_bufid, "modified") ---@diagnostic disable-line: deprecated modified = vim.api.nvim_buf_get_option(target_bufid, "modified") ---@diagnostic disable-line: deprecated
end end
if modified then if modified then
if not mode:match("split$") then if not mode:match "split$" then
mode = "vsplit" mode = "vsplit"
end end
end end
end end
if (mode == "preview" or mode == "preview_no_picker") and view.View.float.enable then if (mode == "preview" or mode == "preview_no_picker") and explorer and explorer.view.View.float.enable then
-- ignore "WinLeave" autocmd on preview -- ignore "WinLeave" autocmd on preview
-- because the registered "WinLeave" -- because the registered "WinLeave"
-- will kill the floating window immediately -- will kill the floating window immediately
@ -361,7 +353,7 @@ local function open_in_new_window(filename, mode)
if create_new_window then if create_new_window then
-- generated from vim.api.nvim_parse_cmd("belowright vsplit foo", {}) -- generated from vim.api.nvim_parse_cmd("belowright vsplit foo", {})
command = { cmd = "vsplit", mods = { split = new_window_side }, args = { fname } } command = { cmd = "vsplit", mods = { split = new_window_side }, args = { fname } }
elseif mode:match("split$") then elseif mode:match "split$" then
command = { cmd = mode, args = { fname } } command = { cmd = mode, args = { fname } }
else else
command = { cmd = "edit", args = { fname } } command = { cmd = "edit", args = { fname } }
@ -381,7 +373,10 @@ local function is_already_loaded(filename)
end end
local function edit_in_current_buf(filename) local function edit_in_current_buf(filename)
require("nvim-tree.view").abandon_current_window() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:abandon_current_window()
end
if M.relative_path then if M.relative_path then
filename = utils.path_relative(filename, vim.fn.getcwd()) filename = utils.path_relative(filename, vim.fn.getcwd())
end end
@ -390,7 +385,6 @@ end
---@param mode string ---@param mode string
---@param filename string ---@param filename string
---@return nil
function M.fn(mode, filename) function M.fn(mode, filename)
if type(mode) ~= "string" then if type(mode) ~= "string" then
mode = "" mode = ""
@ -427,7 +421,10 @@ function M.fn(mode, filename)
end end
if M.resize_window then if M.resize_window then
view.resize() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:resize()
end
end end
if mode == "preview" or mode == "preview_no_picker" then if mode == "preview" or mode == "preview_no_picker" then
@ -435,14 +432,17 @@ function M.fn(mode, filename)
end end
if M.quit_on_open then if M.quit_on_open then
view.close() local explorer = require"nvim-tree.core".get_explorer()
if explorer then
explorer.view:close()
end
end end
end end
function M.setup(opts) function M.setup(opts)
M.quit_on_open = opts.actions.open_file.quit_on_open M.quit_on_open = opts.actions.open_file.quit_on_open
M.resize_window = opts.actions.open_file.resize_window M.resize_window = opts.actions.open_file.resize_window
M.relative_path = opts.actions.open_file.relative_path M.relative_path = opts.experimental.actions.open_file.relative_path
if opts.actions.open_file.window_picker.chars then if opts.actions.open_file.window_picker.chars then
opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper() opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper()
end end

View File

@ -1,5 +1,5 @@
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local M = {} local M = {}

View File

@ -1,12 +1,12 @@
local notify = require("nvim-tree.notify") local notify = require "nvim-tree.notify"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local M = {} local M = {}
---@param node Node ---@param node Node
local function user(node) function M.fn(node)
if #M.config.system_open.cmd == 0 then if #M.config.system_open.cmd == 0 then
require("nvim-tree.utils").notify.warn("Cannot open file with system application. Unrecognized platform.") require("nvim-tree.utils").notify.warn "Cannot open file with system application. Unrecognized platform."
return return
end end
@ -49,41 +49,20 @@ local function user(node)
vim.loop.unref(process.handle) vim.loop.unref(process.handle)
end end
---@param node Node
local function native(node)
local _, err = vim.ui.open(node.link_to or node.absolute_path)
-- err only provided on opener executable not found hence logging path is not useful
if err then
notify.warn(err)
end
end
---@param node Node
function M.fn(node)
M.open(node)
end
-- TODO #2430 always use native once 0.10 is the minimum neovim version
function M.setup(opts) function M.setup(opts)
M.config = {} M.config = {}
M.config.system_open = opts.system_open or {} M.config.system_open = opts.system_open or {}
if vim.fn.has("nvim-0.10") == 1 and #M.config.system_open.cmd == 0 then if #M.config.system_open.cmd == 0 then
M.open = native if utils.is_windows then
else M.config.system_open = {
M.open = user cmd = "cmd",
if #M.config.system_open.cmd == 0 then args = { "/c", "start", '""' },
if utils.is_windows then }
M.config.system_open = { elseif utils.is_macos then
cmd = "cmd", M.config.system_open.cmd = "open"
args = { "/c", "start", '""' }, elseif utils.is_unix then
} M.config.system_open.cmd = "xdg-open"
elseif utils.is_macos then
M.config.system_open.cmd = "open"
elseif utils.is_unix then
M.config.system_open.cmd = "xdg-open"
end
end end
end end
end end

View File

@ -0,0 +1,71 @@
local git = require "nvim-tree.git"
local renderer = require "nvim-tree.renderer"
local explorer_module = require "nvim-tree.explorer"
local core = require "nvim-tree.core"
local explorer_node = require "nvim-tree.explorer.node"
local Iterator = require "nvim-tree.iterators.node-iterator"
local M = {}
---@param node Explorer|nil
---@param projects table
local function refresh_nodes(node, projects)
Iterator.builder({ node })
:applier(function(n)
if n.nodes then
local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path)
explorer_module.reload(n, projects[toplevel] or {})
end
end)
:recursor(function(n)
return n.group_next and { n.group_next } or (n.open and n.nodes)
end)
:iterate()
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
explorer_node.update_git_status(node, explorer_node.is_git_ignored(parent_node), status)
if node.nodes and #node.nodes > 0 then
M.reload_node_status(node, projects)
end
end
end
local event_running = false
function M.reload_explorer()
local explorer = core.get_explorer()
if event_running or not explorer or vim.v.exiting ~= vim.NIL then
return
end
event_running = true
local projects = git.reload()
refresh_nodes(core.get_explorer(), projects)
if explorer.view:is_visible() then
renderer.draw()
end
event_running = false
end
function M.reload_git()
if not core.get_explorer() or not git.config.git.enable or event_running then
return
end
event_running = true
local projects = git.reload()
M.reload_node_status(core.get_explorer(), projects)
renderer.draw()
event_running = false
end
return M

View File

@ -1,6 +1,6 @@
local log = require("nvim-tree.log") local log = require "nvim-tree.log"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local M = { local M = {
current_tab = vim.api.nvim_get_current_tabpage(), current_tab = vim.api.nvim_get_current_tabpage(),
@ -25,7 +25,7 @@ end
---@param new_tabpage integer ---@param new_tabpage integer
---@return boolean ---@return boolean
local function is_window_event(new_tabpage) local function is_window_event(new_tabpage)
local is_event_scope_window = vim.v.event.scope == "window" or vim.v.event.changed_window or false local is_event_scope_window = vim.v.event.scope == "window" or vim.v.event.changed_window
return is_event_scope_window and new_tabpage == M.current_tab return is_event_scope_window and new_tabpage == M.current_tab
end end
@ -91,10 +91,7 @@ M.force_dirchange = add_profiling_to(function(foldername, should_open_view)
if should_open_view then if should_open_view then
require("nvim-tree.lib").open() require("nvim-tree.lib").open()
else else
local explorer = core.get_explorer() require("nvim-tree.renderer").draw()
if explorer then
explorer.renderer:draw()
end
end end
end) end)

View File

@ -1,12 +1,12 @@
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local M = {} local M = {}
---@param node Node ---@param node Node
function M.fn(node) function M.fn(node)
if not node or node.name == ".." then if not node or node.name == ".." then
require("nvim-tree.actions.root.change-dir").fn("..") require("nvim-tree.actions.root.change-dir").fn ".."
else else
local cwd = core.get_cwd() local cwd = core.get_cwd()
if cwd == nil then if cwd == nil then

View File

@ -1,7 +1,7 @@
local M = {} local M = {}
M.change_dir = require("nvim-tree.actions.root.change-dir") M.change_dir = require "nvim-tree.actions.root.change-dir"
M.dir_up = require("nvim-tree.actions.root.dir-up") M.dir_up = require "nvim-tree.actions.root.dir-up"
function M.setup(opts) function M.setup(opts)
M.change_dir.setup(opts) M.change_dir.setup(opts)

View File

@ -1,7 +1,6 @@
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local lib = require("nvim-tree.lib") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local finders_find_file = require "nvim-tree.actions.finders.find-file"
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local M = {} local M = {}
@ -41,17 +40,18 @@ function M.fn(opts)
return return
end end
if view.is_visible() then local explorer = core.get_explorer()
if explorer and explorer.view:is_visible() then
-- focus -- focus
if opts.focus then if opts.focus then
lib.set_target_win() lib.set_target_win()
view.focus() explorer.view:focus()
end end
elseif opts.open then elseif opts.open then
-- open -- open
lib.open({ current_window = opts.current_window, winid = opts.winid }) lib.open { current_window = opts.current_window, winid = opts.winid }
if not opts.focus then if not opts.focus then
vim.cmd("noautocmd wincmd p") vim.cmd "noautocmd wincmd p"
end end
end end

View File

@ -1,10 +1,10 @@
local M = {} local M = {}
M.find_file = require("nvim-tree.actions.tree.find-file") M.find_file = require "nvim-tree.actions.tree.find-file"
M.modifiers = require("nvim-tree.actions.tree.modifiers") M.modifiers = require "nvim-tree.actions.tree.modifiers"
M.open = require("nvim-tree.actions.tree.open") M.open = require "nvim-tree.actions.tree.open"
M.toggle = require("nvim-tree.actions.tree.toggle") M.toggle = require "nvim-tree.actions.tree.toggle"
M.resize = require("nvim-tree.actions.tree.resize") M.resize = require "nvim-tree.actions.tree.resize"
function M.setup(opts) function M.setup(opts)
M.find_file.setup(opts) M.find_file.setup(opts)

View File

@ -0,0 +1,53 @@
local renderer = require "nvim-tree.renderer"
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 M = {}
---@return fun(path: string): boolean
local function buf_match()
local buffer_paths = vim.tbl_map(function(buffer)
return vim.api.nvim_buf_get_name(buffer)
end, vim.api.nvim_list_bufs())
return function(path)
for _, buffer_path in ipairs(buffer_paths) do
local matches = utils.str_find(buffer_path, path)
if matches then
return true
end
end
return false
end
end
---@param keep_buffers boolean
function M.fn(keep_buffers)
local node = lib.get_node_at_cursor()
local explorer = core.get_explorer()
if explorer == nil then
return
end
local matches = buf_match()
Iterator.builder(explorer.nodes)
:hidden()
:applier(function(n)
if n.nodes ~= nil then
n.open = keep_buffers == true and matches(n.absolute_path)
end
end)
:recursor(function(n)
return n.group_next and { n.group_next } or n.nodes
end)
:iterate()
renderer.draw()
utils.focus_node_or_parent(node)
end
return M

View File

@ -1,81 +0,0 @@
local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local Iterator = require("nvim-tree.iterators.node-iterator")
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {}
---@return fun(path: string): boolean
local function buf_match()
local buffer_paths = vim.tbl_map(function(buffer)
return vim.api.nvim_buf_get_name(buffer)
end, vim.api.nvim_list_bufs())
return function(path)
for _, buffer_path in ipairs(buffer_paths) do
local matches = utils.str_find(buffer_path, path)
if matches then
return true
end
end
return false
end
end
---Collapse a node, root if nil
---@param node Node?
---@param opts ApiCollapseOpts
local function collapse(node, opts)
local explorer = core.get_explorer()
if not explorer then
return
end
node = node or explorer
local node_at_cursor = explorer:get_node_at_cursor()
if not node_at_cursor then
return
end
local matches = buf_match()
Iterator.builder({ node:is(FileNode) and node.parent or node:as(DirectoryNode) })
:hidden()
:applier(function(n)
local dir = n:as(DirectoryNode)
if dir then
dir.open = opts.keep_buffers == true and matches(dir.absolute_path)
end
end)
:recursor(function(n)
return n.group_next and { n.group_next } or n.nodes
end)
:iterate()
explorer.renderer:draw()
explorer:focus_node_or_parent(node_at_cursor)
end
---@param opts ApiCollapseOpts|boolean|nil legacy -> opts.keep_buffers
function M.all(opts)
-- legacy arguments
if type(opts) == "boolean" then
opts = {
keep_buffers = opts,
}
end
collapse(nil, opts or {})
end
---@param node Node
---@param opts ApiCollapseOpts?
function M.node(node, opts)
collapse(node, opts or {})
end
return M

View File

@ -0,0 +1,80 @@
local core = require "nvim-tree.core"
local renderer = require "nvim-tree.renderer"
local Iterator = require "nvim-tree.iterators.node-iterator"
local notify = require "nvim-tree.notify"
local lib = require "nvim-tree.lib"
local M = {}
---@param list string[]
---@return table
local function to_lookup_table(list)
local table = {}
for _, element in ipairs(list) do
table[element] = true
end
return table
end
---@param node Node
local function expand(node)
node = lib.get_last_group_node(node)
node.open = true
if #node.nodes == 0 then
core.get_explorer():expand(node)
end
end
---@param expansion_count integer
---@param node Node
---@return boolean
local function should_expand(expansion_count, node)
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
end
local function gen_iterator()
local expansion_count = 0
return function(parent)
if parent.parent and parent.nodes and not parent.open then
expansion_count = expansion_count + 1
expand(parent)
end
Iterator.builder(parent.nodes)
:hidden()
:applier(function(node)
if should_expand(expansion_count, node) then
expansion_count = expansion_count + 1
expand(node)
end
end)
:recursor(function(node)
return expansion_count < M.MAX_FOLDER_DISCOVERY and (node.group_next and { node.group_next } or (node.open and node.nodes))
end)
:iterate()
if expansion_count >= M.MAX_FOLDER_DISCOVERY then
return true
end
end
end
---@param base_node table
function M.fn(base_node)
local node = base_node.nodes and base_node or core.get_explorer()
if gen_iterator()(node) then
notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders")
end
renderer.draw()
end
function M.setup(opts)
M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery
M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude)
end
return M

View File

@ -1,166 +0,0 @@
local core = require("nvim-tree.core")
local Iterator = require("nvim-tree.iterators.node-iterator")
local notify = require("nvim-tree.notify")
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {}
---@param list string[]
---@return table
local function to_lookup_table(list)
local table = {}
for _, element in ipairs(list) do
table[element] = true
end
return table
end
---@param node DirectoryNode
local function expand(node)
node = node:last_group_node()
node.open = true
if #node.nodes == 0 then
core.get_explorer():expand(node)
end
end
---@param should_descend fun(expansion_count: integer, node: Node): boolean
---@return fun(expansion_count: integer, node: Node): boolean
local function limit_folder_discovery(should_descend)
return function(expansion_count, node)
local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY
if should_halt then
notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders")
return false
end
return should_descend(expansion_count, node)
end
end
---@param _ integer expansion_count
---@param node Node
---@return boolean
local function descend_until_empty(_, node)
local dir = node:as(DirectoryNode)
if not dir then
return false
end
local should_exclude = M.EXCLUDE[dir.name]
return not should_exclude
end
---@param expansion_count integer
---@param node Node
---@param should_descend fun(expansion_count: integer, node: Node): boolean
---@return boolean
local function should_expand(expansion_count, node, should_descend)
local dir = node:as(DirectoryNode)
if not dir then
return false
end
if not dir.open and should_descend(expansion_count, node) then
if #node.nodes == 0 then
core.get_explorer():expand(dir) -- populate node.group_next
end
if dir.group_next then
local expand_next = should_expand(expansion_count, dir.group_next, should_descend)
if expand_next then
dir.open = true
end
return expand_next
else
return true
end
end
return false
end
---@param should_descend fun(expansion_count: integer, node: Node): boolean
---@return fun(node): any
local function gen_iterator(should_descend)
local expansion_count = 0
return function(parent)
if parent.parent and parent.nodes and not parent.open then
expansion_count = expansion_count + 1
expand(parent)
end
Iterator.builder(parent.nodes)
:hidden()
:applier(function(node)
if should_expand(expansion_count, node, should_descend) then
expansion_count = expansion_count + 1
node = node:as(DirectoryNode)
if node then
expand(node)
end
end
end)
:recursor(function(node)
if not should_descend(expansion_count, node) then
return nil
end
if node.group_next then
return { node.group_next }
end
if node.open and node.nodes then
return node.nodes
end
return nil
end)
:iterate()
end
end
---@param node Node?
---@param expand_opts ApiTreeExpandOpts?
local function expand_node(node, expand_opts)
if not node then
return
end
local descend_until = limit_folder_discovery((expand_opts and expand_opts.expand_until) or descend_until_empty)
gen_iterator(descend_until)(node)
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end
---Expand the directory node or the root
---@param node Node
---@param expand_opts ApiTreeExpandOpts?
function M.all(node, expand_opts)
expand_node(node and node:as(DirectoryNode) or core.get_explorer(), expand_opts)
end
---Expand the directory node or parent node
---@param node Node
---@param expand_opts ApiTreeExpandOpts?
function M.node(node, expand_opts)
if not node then
return
end
expand_node(node:is(FileNode) and node.parent or node:as(DirectoryNode), expand_opts)
end
function M.setup(opts)
M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery
M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude)
end
return M

View File

@ -1,10 +1,11 @@
local M = {} local M = {}
M.collapse = require("nvim-tree.actions.tree.modifiers.collapse") M.collapse_all = require "nvim-tree.actions.tree.modifiers.collapse-all"
M.expand = require("nvim-tree.actions.tree.modifiers.expand") M.expand_all = require "nvim-tree.actions.tree.modifiers.expand-all"
M.toggles = require "nvim-tree.actions.tree.modifiers.toggles"
function M.setup(opts) function M.setup(opts)
M.expand.setup(opts) M.expand_all.setup(opts)
end end
return M return M

View File

@ -0,0 +1,65 @@
local lib = require "nvim-tree.lib"
local utils = require "nvim-tree.utils"
local reloaders = require "nvim-tree.actions.reloaders"
local core = require "nvim-tree.core"
local M = {}
local function reload()
local node = lib.get_node_at_cursor()
reloaders.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
local function custom(explorer)
explorer.filters.config.filter_custom = not explorer.filters.config.filter_custom
reload()
end
local function git_ignored(explorer)
explorer.filters.config.filter_git_ignored = not explorer.filters.config.filter_git_ignored
reload()
end
local function git_clean(explorer)
explorer.filters.config.filter_git_clean = not explorer.filters.config.filter_git_clean
reload()
end
local function no_buffer(explorer)
explorer.filters.config.filter_no_buffer = not explorer.filters.config.filter_no_buffer
reload()
end
local function no_bookmark(explorer)
explorer.filters.config.filter_no_bookmark = not explorer.filters.config.filter_no_bookmark
reload()
end
local function dotfiles(explorer)
explorer.filters.config.filter_dotfiles = not explorer.filters.config.filter_dotfiles
reload()
end
local function enable(explorer)
explorer.filters.config.enable = not explorer.filters.config.enable
reload()
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,6 +1,5 @@
local lib = require("nvim-tree.lib") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local finders_find_file = require "nvim-tree.actions.finders.find-file"
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local M = {} local M = {}
@ -23,17 +22,18 @@ function M.fn(opts)
opts.path = nil opts.path = nil
end end
if view.is_visible() then local explorer = require"nvim-tree.core".get_explorer()
if explorer and explorer.view:is_visible() then
-- focus -- focus
lib.set_target_win() lib.set_target_win()
view.focus() explorer.view:focus()
else else
-- open -- open
lib.open({ lib.open {
path = opts.path, path = opts.path,
current_window = opts.current_window, current_window = opts.current_window,
winid = opts.winid, winid = opts.winid,
}) }
end end
-- find file -- find file

View File

@ -1,14 +1,18 @@
local view = require("nvim-tree.view")
local M = {} local M = {}
---Resize the tree, persisting the new size. ---Resize the tree, persisting the new size.
---@param opts ApiTreeResizeOpts|nil ---@param opts ApiTreeResizeOpts|nil
function M.fn(opts) function M.fn(opts)
local explorer = require"nvim-tree.core".get_explorer()
if not explorer then
return
end
if opts == nil then if opts == nil then
-- reset to config values -- reset to config values
view.configure_width() explorer.view:configure_width()
view.resize() explorer.view:resize()
return return
end end
@ -16,19 +20,19 @@ function M.fn(opts)
local width_cfg = options.width local width_cfg = options.width
if width_cfg ~= nil then if width_cfg ~= nil then
view.configure_width(width_cfg) explorer.view:configure_width(width_cfg)
view.resize() explorer.view:resize()
return return
end end
if not view.is_width_determined() then if not explorer.view:is_width_determined() then
-- {absolute} and {relative} do nothing when {width} is a function. -- {absolute} and {relative} do nothing when {width} is a function.
return return
end end
local absolute = options.absolute local absolute = options.absolute
if type(absolute) == "number" then if type(absolute) == "number" then
view.resize(absolute) explorer.view:resize(absolute)
return return
end end
@ -39,7 +43,7 @@ function M.fn(opts)
relative_size = "+" .. relative_size relative_size = "+" .. relative_size
end end
view.resize(relative_size) explorer.view:resize(relative_size)
return return
end end
end end

View File

@ -1,6 +1,5 @@
local lib = require("nvim-tree.lib") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local finders_find_file = require "nvim-tree.actions.finders.find-file"
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local M = {} local M = {}
@ -40,16 +39,21 @@ function M.fn(opts, no_focus, cwd, bang)
opts.path = nil opts.path = nil
end end
if view.is_visible() then local explorer = require"nvim-tree.core".get_explorer()
if not explorer then
return
end
if explorer.view:is_visible() then
-- close -- close
view.close() explorer.view:close()
else else
-- open -- open
lib.open({ lib.open {
path = opts.path, path = opts.path,
current_window = opts.current_window, current_window = opts.current_window,
winid = opts.winid, winid = opts.winid,
}) }
-- find file -- find file
if M.config.update_focused_file.enable or opts.find_file then if M.config.update_focused_file.enable or opts.find_file then
@ -64,7 +68,7 @@ function M.fn(opts, no_focus, cwd, bang)
-- restore focus -- restore focus
if not opts.focus then if not opts.focus then
vim.cmd("noautocmd wincmd p") vim.cmd "noautocmd wincmd p"
end end
end end
end end

View File

@ -1,17 +1,16 @@
local core = require("nvim-tree.core") local lib = require "nvim-tree.lib"
local view = require("nvim-tree.view") local core = require "nvim-tree.core"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local actions = require("nvim-tree.actions") local actions = require "nvim-tree.actions"
local appearance_hi_test = require("nvim-tree.appearance.hi-test") local appearance_diagnostics = require "nvim-tree.appearance.diagnostics"
local events = require("nvim-tree.events") local events = require "nvim-tree.events"
local help = require("nvim-tree.help") local help = require "nvim-tree.help"
local keymap = require("nvim-tree.keymap") local marks_navigation = require "nvim-tree.marks.navigation"
local notify = require("nvim-tree.notify") local marks_bulk_delete = require "nvim-tree.marks.bulk-delete"
local marks_bulk_trash = require "nvim-tree.marks.bulk-trash"
local DirectoryNode = require("nvim-tree.node.directory") local marks_bulk_move = require "nvim-tree.marks.bulk-move"
local FileLinkNode = require("nvim-tree.node.file-link") local keymap = require "nvim-tree.keymap"
local RootNode = require("nvim-tree.node.root") local notify = require "nvim-tree.notify"
local UserDecorator = require("nvim-tree.renderer.decorator.user")
local Api = { local Api = {
tree = {}, tree = {},
@ -24,7 +23,6 @@ local Api = {
}, },
run = {}, run = {},
open = {}, open = {},
buffer = {},
}, },
events = {}, events = {},
marks = { marks = {
@ -41,41 +39,27 @@ local Api = {
}, },
commands = {}, commands = {},
diagnostics = {}, diagnostics = {},
decorator = {},
} }
---Print error when setup not called. --- Print error when setup not called.
---@param fn fun(...): any --- f function to invoke
---@return fun(...): any ---@param f function
local function wrap(fn) ---@return fun(...) : any
local function wrap(f)
return function(...) return function(...)
if vim.g.NvimTreeSetup == 1 then if vim.g.NvimTreeSetup == 1 then
return fn(...) return f(...)
else else
notify.error("nvim-tree setup not called") notify.error "nvim-tree setup not called"
end end
end end
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
local function wrap_explorer(explorer_method)
return wrap(function(...)
local explorer = core.get_explorer()
if explorer then
return explorer[explorer_method](explorer, ...)
end
end)
end
---Inject the node as the first argument if present otherwise do nothing. ---Inject the node as the first argument if present otherwise do nothing.
---@param fn fun(node: Node, ...): any ---@param fn function function to invoke
---@return fun(node: Node?, ...): any
local function wrap_node(fn) local function wrap_node(fn)
return function(node, ...) return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")() node = node or lib.get_node_at_cursor()
if node then if node then
return fn(node, ...) return fn(node, ...)
end end
@ -83,36 +67,31 @@ local function wrap_node(fn)
end end
---Inject the node or nil as the first argument if absent. ---Inject the node or nil as the first argument if absent.
---@param fn fun(node: Node?, ...): any ---@param fn function function to invoke
---@return fun(node: Node?, ...): any
local function wrap_node_or_nil(fn) local function wrap_node_or_nil(fn)
return function(node, ...) return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")() node = node or lib.get_node_at_cursor()
return fn(node, ...) return fn(node, ...)
end end
end end
---Inject the explorer as the first argument if present otherwise do nothing.
---@param fn function function to invoke
---@return fun(...) : any
local function wrap_explorer(fn)
return function(...)
local explorer = core.get_explorer()
if explorer then
return fn(explorer, ...)
end
end
end
---Invoke a member's method on the singleton explorer. ---Invoke a member's method on the singleton explorer.
---Print error when setup not called. ---Print error when setup not called.
---@param explorer_member string explorer member name ---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member ---@param member_method string method name to invoke on member
---@param ... any passed to method ---@return fun(...) : any
---@return fun(...): any
local function wrap_explorer_member_args(explorer_member, member_method, ...)
local method_args = ...
return wrap(function(...)
local explorer = core.get_explorer()
if explorer then
return explorer[explorer_member][member_method](explorer[explorer_member], method_args, ...)
end
end)
end
---Invoke a member's method on the singleton explorer.
---Print error when setup not called.
---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member
---@return fun(...): any
local function wrap_explorer_member(explorer_member, member_method) local function wrap_explorer_member(explorer_member, member_method)
return wrap(function(...) return wrap(function(...)
local explorer = core.get_explorer() local explorer = core.get_explorer()
@ -141,10 +120,10 @@ Api.tree.focus = Api.tree.open
---@field focus boolean|nil default true ---@field focus boolean|nil default true
Api.tree.toggle = wrap(actions.tree.toggle.fn) Api.tree.toggle = wrap(actions.tree.toggle.fn)
Api.tree.close = wrap(view.close) Api.tree.close = wrap_explorer_member("view", "close")
Api.tree.close_in_this_tab = wrap(view.close_this_tab_only) Api.tree.close_in_this_tab = wrap_explorer_member("view", "close_this_tab_only")
Api.tree.close_in_all_tabs = wrap(view.close_all_tabs) Api.tree.close_in_all_tabs = wrap_explorer_member("view", "close_all_tabs")
Api.tree.reload = wrap_explorer("reload_explorer") Api.tree.reload = wrap(actions.reloaders.reload_explorer)
---@class ApiTreeResizeOpts ---@class ApiTreeResizeOpts
---@field width string|function|number|table|nil ---@field width string|function|number|table|nil
@ -158,19 +137,16 @@ Api.tree.change_root = wrap(function(...)
end) end)
Api.tree.change_root_to_node = wrap_node(function(node) Api.tree.change_root_to_node = wrap_node(function(node)
if node.name == ".." or node:is(RootNode) then if node.name == ".." then
actions.root.change_dir.fn("..") actions.root.change_dir.fn ".."
else elseif node.nodes ~= nil then
node = node:as(DirectoryNode) actions.root.change_dir.fn(lib.get_last_group_node(node).absolute_path)
if node then
actions.root.change_dir.fn(node:last_group_node().absolute_path)
end
end end
end) end)
Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn) Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn)
Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor") Api.tree.get_node_under_cursor = wrap(lib.get_node_at_cursor)
Api.tree.get_nodes = wrap_explorer("get_nodes") Api.tree.get_nodes = wrap(lib.get_nodes)
---@class ApiTreeFindFileOpts ---@class ApiTreeFindFileOpts
---@field buf string|number|nil ---@field buf string|number|nil
@ -182,23 +158,15 @@ Api.tree.get_nodes = wrap_explorer("get_nodes")
Api.tree.find_file = wrap(actions.tree.find_file.fn) Api.tree.find_file = wrap(actions.tree.find_file.fn)
Api.tree.search_node = wrap(actions.finders.search_node.fn) Api.tree.search_node = wrap(actions.finders.search_node.fn)
Api.tree.collapse_all = wrap(actions.tree.modifiers.collapse_all.fn)
---@class ApiCollapseOpts Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand_all.fn)
---@field keep_buffers boolean|nil default false 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.collapse_all = wrap(actions.tree.modifiers.collapse.all) 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)
---@class ApiTreeExpandOpts Api.tree.toggle_custom_filter = wrap(actions.tree.modifiers.toggles.custom)
---@field expand_until (fun(expansion_count: integer, node: Node): boolean)|nil 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.expand_all = wrap_node(actions.tree.modifiers.expand.all)
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.toggle_help = wrap(help.toggle)
Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf) Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf)
@ -206,134 +174,91 @@ Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf)
---@field tabpage number|nil ---@field tabpage number|nil
---@field any_tabpage boolean|nil default false ---@field any_tabpage boolean|nil default false
Api.tree.is_visible = wrap(view.is_visible) Api.tree.is_visible = wrap_explorer_member("view", "is_visible")
---@class ApiTreeWinIdOpts ---@class ApiTreeWinIdOpts
---@field tabpage number|nil default nil ---@field tabpage number|nil default nil
Api.tree.winid = wrap(view.winid) Api.tree.winid = wrap_explorer_member("view", "winid")
Api.fs.create = wrap_node_or_nil(actions.fs.create_file.fn) Api.fs.create = wrap_node_or_nil(actions.fs.create_file.fn)
Api.fs.remove = wrap_node(actions.fs.remove_file.fn) Api.fs.remove = wrap_node(actions.fs.remove_file.fn)
Api.fs.trash = wrap_node(actions.fs.trash.fn) Api.fs.trash = wrap_node(actions.fs.trash.fn)
Api.fs.rename_node = wrap_node(actions.fs.rename_file.fn(":t")) Api.fs.rename_node = wrap_node(actions.fs.rename_file.fn ":t")
Api.fs.rename = wrap_node(actions.fs.rename_file.fn(":t")) Api.fs.rename = wrap_node(actions.fs.rename_file.fn ":t")
Api.fs.rename_sub = wrap_node(actions.fs.rename_file.fn(":p:h")) Api.fs.rename_sub = wrap_node(actions.fs.rename_file.fn ":p:h")
Api.fs.rename_basename = wrap_node(actions.fs.rename_file.fn(":t:r")) Api.fs.rename_basename = wrap_node(actions.fs.rename_file.fn ":t:r")
Api.fs.rename_full = wrap_node(actions.fs.rename_file.fn(":p")) Api.fs.rename_full = wrap_node(actions.fs.rename_file.fn ":p")
Api.fs.cut = wrap_node(wrap_explorer_member("clipboard", "cut")) Api.fs.cut = wrap_node(actions.fs.copy_paste.cut)
Api.fs.paste = wrap_node(wrap_explorer_member("clipboard", "paste")) Api.fs.paste = wrap_node(actions.fs.copy_paste.paste)
Api.fs.clear_clipboard = wrap_explorer_member("clipboard", "clear_clipboard") Api.fs.clear_clipboard = wrap(actions.fs.copy_paste.clear_clipboard)
Api.fs.print_clipboard = wrap_explorer_member("clipboard", "print_clipboard") Api.fs.print_clipboard = wrap(actions.fs.copy_paste.print_clipboard)
Api.fs.copy.node = wrap_node(wrap_explorer_member("clipboard", "copy")) Api.fs.copy.node = wrap_node(actions.fs.copy_paste.copy)
Api.fs.copy.absolute_path = wrap_node(wrap_explorer_member("clipboard", "copy_absolute_path")) Api.fs.copy.absolute_path = wrap_node(actions.fs.copy_paste.copy_absolute_path)
Api.fs.copy.filename = wrap_node(wrap_explorer_member("clipboard", "copy_filename")) Api.fs.copy.filename = wrap_node(actions.fs.copy_paste.copy_filename)
Api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basename")) Api.fs.copy.basename = wrap_node(actions.fs.copy_paste.copy_basename)
Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path")) Api.fs.copy.relative_path = wrap_node(actions.fs.copy_paste.copy_path)
---
---@class NodeEditOpts
---@field quit_on_open boolean|nil default false
---@field focus boolean|nil default true
---@param mode string ---@param mode string
---@param node Node ---@param node table
---@param edit_opts NodeEditOpts? local function edit(mode, node)
local function edit(mode, node, edit_opts) local path = node.absolute_path
local file_link = node:as(FileLinkNode) if node.link_to and not node.nodes then
local path = file_link and file_link.link_to or node.absolute_path path = node.link_to
local cur_tabpage = vim.api.nvim_get_current_tabpage() end
actions.node.open_file.fn(mode, path) actions.node.open_file.fn(mode, path)
edit_opts = edit_opts or {}
local mode_unsupported_quit_on_open = mode == "drop" or mode == "tab_drop" or mode == "edit_in_place"
if not mode_unsupported_quit_on_open and edit_opts.quit_on_open then
view.close(cur_tabpage)
end
local mode_unsupported_focus = mode == "drop" or mode == "tab_drop" or mode == "edit_in_place"
local focus = edit_opts.focus == nil or edit_opts.focus == true
if not mode_unsupported_focus and not focus then
-- if mode == "tabnew" a new tab will be opened and we need to focus back to the previous tab
if mode == "tabnew" then
vim.cmd(":tabprev")
end
view.focus()
end
end end
---@param mode string ---@param mode string
---@param toggle_group boolean? ---@return fun(node: table)
---@return fun(node: Node, edit_opts: NodeEditOpts?)
local function open_or_expand_or_dir_up(mode, toggle_group) local function open_or_expand_or_dir_up(mode, toggle_group)
---@param node Node return function(node)
---@param edit_opts NodeEditOpts? if node.name == ".." then
return function(node, edit_opts) actions.root.change_dir.fn ".."
local root = node:as(RootNode) elseif node.nodes then
local dir = node:as(DirectoryNode) lib.expand_or_collapse(node, toggle_group)
if root or node.name == ".." then
actions.root.change_dir.fn("..")
elseif dir then
dir:expand_or_collapse(toggle_group)
elseif not toggle_group then elseif not toggle_group then
edit(mode, node, edit_opts) edit(mode, node)
end end
end end
end end
Api.node.open.edit = wrap_node(open_or_expand_or_dir_up("edit")) Api.node.open.edit = wrap_node(open_or_expand_or_dir_up "edit")
Api.node.open.drop = wrap_node(open_or_expand_or_dir_up("drop")) Api.node.open.drop = wrap_node(open_or_expand_or_dir_up "drop")
Api.node.open.tab_drop = wrap_node(open_or_expand_or_dir_up("tab_drop")) Api.node.open.tab_drop = wrap_node(open_or_expand_or_dir_up "tab_drop")
Api.node.open.replace_tree_buffer = wrap_node(open_or_expand_or_dir_up("edit_in_place")) Api.node.open.replace_tree_buffer = wrap_node(open_or_expand_or_dir_up "edit_in_place")
Api.node.open.no_window_picker = wrap_node(open_or_expand_or_dir_up("edit_no_picker")) Api.node.open.no_window_picker = wrap_node(open_or_expand_or_dir_up "edit_no_picker")
Api.node.open.vertical = wrap_node(open_or_expand_or_dir_up("vsplit")) Api.node.open.vertical = wrap_node(open_or_expand_or_dir_up "vsplit")
Api.node.open.vertical_no_picker = wrap_node(open_or_expand_or_dir_up("vsplit_no_picker")) Api.node.open.horizontal = wrap_node(open_or_expand_or_dir_up "split")
Api.node.open.horizontal = wrap_node(open_or_expand_or_dir_up("split")) Api.node.open.tab = wrap_node(open_or_expand_or_dir_up "tabnew")
Api.node.open.horizontal_no_picker = wrap_node(open_or_expand_or_dir_up("split_no_picker"))
Api.node.open.tab = wrap_node(open_or_expand_or_dir_up("tabnew"))
Api.node.open.toggle_group_empty = wrap_node(open_or_expand_or_dir_up("toggle_group_empty", true)) Api.node.open.toggle_group_empty = wrap_node(open_or_expand_or_dir_up("toggle_group_empty", true))
Api.node.open.preview = wrap_node(open_or_expand_or_dir_up("preview")) Api.node.open.preview = wrap_node(open_or_expand_or_dir_up "preview")
Api.node.open.preview_no_picker = wrap_node(open_or_expand_or_dir_up("preview_no_picker")) Api.node.open.preview_no_picker = wrap_node(open_or_expand_or_dir_up "preview_no_picker")
Api.node.show_info_popup = wrap_node(actions.node.file_popup.toggle_file_info) Api.node.show_info_popup = wrap_node(actions.node.file_popup.toggle_file_info)
Api.node.run.cmd = wrap_node(actions.node.run_command.run_file_command) Api.node.run.cmd = wrap_node(actions.node.run_command.run_file_command)
Api.node.run.system = wrap_node(actions.node.system_open.fn) Api.node.run.system = wrap_node(actions.node.system_open.fn)
Api.node.navigate.sibling.next = wrap_node(actions.moves.sibling.fn("next")) Api.node.navigate.sibling.next = wrap_node(actions.moves.sibling.fn "next")
Api.node.navigate.sibling.prev = wrap_node(actions.moves.sibling.fn("prev")) Api.node.navigate.sibling.prev = wrap_node(actions.moves.sibling.fn "prev")
Api.node.navigate.sibling.first = wrap_node(actions.moves.sibling.fn("first")) Api.node.navigate.sibling.first = wrap_node(actions.moves.sibling.fn "first")
Api.node.navigate.sibling.last = wrap_node(actions.moves.sibling.fn("last")) Api.node.navigate.sibling.last = wrap_node(actions.moves.sibling.fn "last")
Api.node.navigate.parent = wrap_node(actions.moves.parent.fn(false)) Api.node.navigate.parent = wrap_node(actions.moves.parent.fn(false))
Api.node.navigate.parent_close = wrap_node(actions.moves.parent.fn(true)) Api.node.navigate.parent_close = wrap_node(actions.moves.parent.fn(true))
Api.node.navigate.git.next = wrap_node(actions.moves.item.fn({ where = "next", what = "git" })) Api.node.navigate.git.next = wrap_node(actions.moves.item.fn { where = "next", what = "git" })
Api.node.navigate.git.next_skip_gitignored = wrap_node(actions.moves.item.fn({ where = "next", what = "git", skip_gitignored = true })) Api.node.navigate.git.next_skip_gitignored = wrap_node(actions.moves.item.fn { where = "next", what = "git", skip_gitignored = true })
Api.node.navigate.git.next_recursive = wrap_node(actions.moves.item.fn({ where = "next", what = "git", recurse = true })) Api.node.navigate.git.next_recursive = wrap_node(actions.moves.item.fn { where = "next", what = "git", recurse = true })
Api.node.navigate.git.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "git" })) Api.node.navigate.git.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "git" })
Api.node.navigate.git.prev_skip_gitignored = wrap_node(actions.moves.item.fn({ where = "prev", what = "git", skip_gitignored = true })) Api.node.navigate.git.prev_skip_gitignored = wrap_node(actions.moves.item.fn { where = "prev", what = "git", skip_gitignored = true })
Api.node.navigate.git.prev_recursive = wrap_node(actions.moves.item.fn({ where = "prev", what = "git", recurse = true })) Api.node.navigate.git.prev_recursive = wrap_node(actions.moves.item.fn { where = "prev", what = "git", recurse = true })
Api.node.navigate.diagnostics.next = wrap_node(actions.moves.item.fn({ where = "next", what = "diag" })) Api.node.navigate.diagnostics.next = wrap_node(actions.moves.item.fn { where = "next", what = "diag" })
Api.node.navigate.diagnostics.next_recursive = wrap_node(actions.moves.item.fn({ where = "next", what = "diag", recurse = true })) Api.node.navigate.diagnostics.next_recursive = wrap_node(actions.moves.item.fn { where = "next", what = "diag", recurse = true })
Api.node.navigate.diagnostics.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "diag" })) Api.node.navigate.diagnostics.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "diag" })
Api.node.navigate.diagnostics.prev_recursive = wrap_node(actions.moves.item.fn({ where = "prev", what = "diag", recurse = true })) Api.node.navigate.diagnostics.prev_recursive = wrap_node(actions.moves.item.fn { where = "prev", what = "diag", recurse = true })
Api.node.navigate.opened.next = wrap_node(actions.moves.item.fn({ where = "next", what = "opened" })) Api.node.navigate.opened.next = wrap_node(actions.moves.item.fn { where = "next", what = "opened" })
Api.node.navigate.opened.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "opened" })) Api.node.navigate.opened.prev = wrap_node(actions.moves.item.fn { where = "prev", what = "opened" })
Api.node.expand = wrap_node(actions.tree.modifiers.expand.node) Api.git.reload = wrap(actions.reloaders.reload_git)
Api.node.collapse = wrap_node(actions.tree.modifiers.collapse.node)
---@class ApiNodeDeleteWipeBufferOpts
---@field force boolean|nil default false
Api.node.buffer.delete = wrap_node(function(node, opts)
actions.node.buffer.delete(node, opts)
end)
Api.node.buffer.wipe = wrap_node(function(node, opts)
actions.node.buffer.wipe(node, opts)
end)
Api.git.reload = wrap_explorer("reload_git")
Api.events.subscribe = events.subscribe Api.events.subscribe = events.subscribe
Api.events.Event = events.Event Api.events.Event = events.Event
@ -341,30 +266,25 @@ Api.events.Event = events.Event
Api.live_filter.start = wrap_explorer_member("live_filter", "start_filtering") Api.live_filter.start = wrap_explorer_member("live_filter", "start_filtering")
Api.live_filter.clear = wrap_explorer_member("live_filter", "clear_filter") Api.live_filter.clear = wrap_explorer_member("live_filter", "clear_filter")
Api.marks.get = wrap_node(wrap_explorer_member("marks", "get")) Api.marks.get = wrap_node(wrap_explorer_member("marks", "get_mark"))
Api.marks.list = wrap_explorer_member("marks", "list") Api.marks.list = wrap_explorer_member("marks", "get_marks")
Api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle")) Api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle_mark"))
Api.marks.clear = wrap_explorer_member("marks", "clear") Api.marks.clear = wrap_explorer_member("marks", "clear_marks")
Api.marks.bulk.delete = wrap_explorer_member("marks", "bulk_delete") Api.marks.bulk.delete = wrap_explorer(marks_bulk_delete.bulk_delete)
Api.marks.bulk.trash = wrap_explorer_member("marks", "bulk_trash") Api.marks.bulk.trash = wrap_explorer(marks_bulk_trash.bulk_trash)
Api.marks.bulk.move = wrap_explorer_member("marks", "bulk_move") Api.marks.bulk.move = wrap_explorer(marks_bulk_move.bulk_move)
Api.marks.navigate.next = wrap_explorer_member("marks", "navigate_next") Api.marks.navigate.next = wrap(marks_navigation.next)
Api.marks.navigate.prev = wrap_explorer_member("marks", "navigate_prev") Api.marks.navigate.prev = wrap(marks_navigation.prev)
Api.marks.navigate.select = wrap_explorer_member("marks", "navigate_select") Api.marks.navigate.select = wrap(marks_navigation.select)
Api.config.mappings.get_keymap = wrap(keymap.get_keymap) Api.config.mappings.get_keymap = wrap(keymap.get_keymap)
Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default) Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default)
Api.config.mappings.default_on_attach = keymap.default_on_attach Api.config.mappings.default_on_attach = keymap.default_on_attach
Api.diagnostics.hi_test = wrap(appearance_hi_test) Api.diagnostics.hi_test = wrap(appearance_diagnostics.hi_test)
Api.commands.get = wrap(function() Api.commands.get = wrap(function()
return require("nvim-tree.commands").get() return require("nvim-tree.commands").get()
end) end)
---Create a decorator class by calling :extend()
---See :help nvim-tree-decorators
---@type nvim_tree.api.decorator.UserDecorator
Api.decorator.UserDecorator = UserDecorator --[[@as nvim_tree.api.decorator.UserDecorator]]
return Api return Api

View File

@ -1,48 +1,45 @@
local appearance = require("nvim-tree.appearance") local appearance = require "nvim-tree.appearance"
local Class = require("nvim-tree.classic")
-- others with name and links less than this arbitrary value are short -- others with name and links less than this arbitrary value are short
local SHORT_LEN = 50 local SHORT_LEN = 50
local namespace_hi_test_id = vim.api.nvim_create_namespace("NvimTreeHiTest") local M = {}
---@class (exact) HighlightDisplay: Class for :NvimTreeHiTest ---@class HighlightDisplay for :NvimTreeHiTest
---@field group string nvim-tree highlight group name ---@field group string nvim-tree highlight group name
---@field links string link chain to a concretely defined group ---@field links string link chain to a concretely defined group
---@field def string :hi concrete definition after following any links ---@field def string :hi concrete definition after following any links
local HighlightDisplay = Class:extend() local HighlightDisplay = {}
---@class HighlightDisplay ---@param group string nvim-tree highlight group name
---@overload fun(args: HighlightDisplayArgs): HighlightDisplay ---@return HighlightDisplay
function HighlightDisplay:new(group)
local o = {}
setmetatable(o, self)
self.__index = self
---@class (exact) HighlightDisplayArgs o.group = group
---@field group string nvim-tree highlight group name local concrete = o.group
---@protected
---@param args HighlightDisplayArgs
function HighlightDisplay:new(args)
self.group = args.group
local concrete = self.group
-- maybe follow links -- maybe follow links
local links = {} local links = {}
local link = vim.api.nvim_get_hl(0, { name = self.group }).link local link = vim.api.nvim_get_hl(0, { name = o.group }).link
while link do while link do
table.insert(links, link) table.insert(links, link)
concrete = link concrete = link
link = vim.api.nvim_get_hl(0, { name = link }).link link = vim.api.nvim_get_hl(0, { name = link }).link
end end
self.links = table.concat(links, " ") o.links = table.concat(links, " ")
-- concrete definition -- concrete definition
local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { concrete } }, { output = true }) local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { concrete } }, { output = true })
if ok and type(res) == "string" then if ok and type(res) == "string" then
self.def = res:gsub(".*xxx *", "") o.def = res:gsub(".*xxx *", "")
else else
self.def = "" o.def = ""
end end
return o
end end
---Render one group. ---Render one group.
@ -54,12 +51,7 @@ function HighlightDisplay:render(bufnr, fmt, l)
local text = string.format(fmt, self.group, self.links, self.def) local text = string.format(fmt, self.group, self.links, self.def)
vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { text }) vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { text })
vim.api.nvim_buf_add_highlight(bufnr, -1, self.group, l, 0, #self.group)
if vim.fn.has("nvim-0.11") == 1 and vim.hl and vim.hl.range then
vim.hl.range(bufnr, namespace_hi_test_id, self.group, { l, 0 }, { l, #self.group, }, {})
else
vim.api.nvim_buf_add_highlight(bufnr, -1, self.group, l, 0, #self.group) ---@diagnostic disable-line: deprecated
end
return l + 1 return l + 1
end end
@ -95,7 +87,7 @@ end
---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim ---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim
---Display all nvim-tree and neovim highlight groups, their link chain and actual definition ---Display all nvim-tree and neovim highlight groups, their link chain and actual definition
return function() function M.hi_test()
-- create a buffer -- create a buffer
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
@ -104,7 +96,7 @@ return function()
-- nvim-tree groups, ordered -- nvim-tree groups, ordered
local displays = {} local displays = {}
for _, highlight_group in ipairs(appearance.HIGHLIGHT_GROUPS) do for _, highlight_group in ipairs(appearance.HIGHLIGHT_GROUPS) do
local display = HighlightDisplay({ group = highlight_group.group }) local display = HighlightDisplay:new(highlight_group.group)
table.insert(displays, display) table.insert(displays, display)
end end
l = render_displays("nvim-tree", displays, bufnr, l) l = render_displays("nvim-tree", displays, bufnr, l)
@ -118,7 +110,7 @@ return function()
if ok then if ok then
for group in string.gmatch(out, "(%w*)%s+xxx") do for group in string.gmatch(out, "(%w*)%s+xxx") do
if group:find("NvimTree", 1, true) ~= 1 then if group:find("NvimTree", 1, true) ~= 1 then
local display = HighlightDisplay({ group = group }) local display = HighlightDisplay:new(group)
if #display.group + #display.links > SHORT_LEN then if #display.group + #display.links > SHORT_LEN then
table.insert(displays_long, display) table.insert(displays_long, display)
else else
@ -137,7 +129,7 @@ return function()
render_displays("other, long", displays_long, bufnr, l) render_displays("other, long", displays_long, bufnr, l)
-- finalise and focus the buffer -- finalise and focus the buffer
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr }) vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr })
else else
vim.api.nvim_buf_set_option(bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated vim.api.nvim_buf_set_option(bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated
@ -145,3 +137,5 @@ return function()
vim.cmd.buffer(bufnr) vim.cmd.buffer(bufnr)
end end
return M

View File

@ -12,126 +12,122 @@ local M = {}
M.HIGHLIGHT_GROUPS = { M.HIGHLIGHT_GROUPS = {
-- Standard -- Standard
{ group = "NvimTreeNormal", link = "Normal" }, { group = "NvimTreeNormal", link = "Normal" },
{ group = "NvimTreeNormalFloat", link = "NormalFloat" }, { group = "NvimTreeNormalFloat", link = "NormalFloat" },
{ group = "NvimTreeNormalFloatBorder", link = "FloatBorder" }, { group = "NvimTreeNormalNC", link = "NvimTreeNormal" },
{ group = "NvimTreeNormalNC", link = "NvimTreeNormal" },
{ group = "NvimTreeLineNr", link = "LineNr" }, { group = "NvimTreeLineNr", link = "LineNr" },
{ group = "NvimTreeWinSeparator", link = "WinSeparator" }, { group = "NvimTreeWinSeparator", link = "WinSeparator" },
{ group = "NvimTreeEndOfBuffer", link = "EndOfBuffer" }, { group = "NvimTreeEndOfBuffer", link = "EndOfBuffer" },
{ group = "NvimTreePopup", link = "Normal" }, { group = "NvimTreePopup", link = "Normal" },
{ group = "NvimTreeSignColumn", link = "NvimTreeNormal" }, { group = "NvimTreeSignColumn", link = "NvimTreeNormal" },
{ group = "NvimTreeCursorColumn", link = "CursorColumn" }, { group = "NvimTreeCursorColumn", link = "CursorColumn" },
{ group = "NvimTreeCursorLine", link = "CursorLine" }, { group = "NvimTreeCursorLine", link = "CursorLine" },
{ group = "NvimTreeCursorLineNr", link = "CursorLineNr" }, { group = "NvimTreeCursorLineNr", link = "CursorLineNr" },
{ group = "NvimTreeStatusLine", link = "StatusLine" }, { group = "NvimTreeStatusLine", link = "StatusLine" },
{ group = "NvimTreeStatusLineNC", link = "StatusLineNC" }, { group = "NvimTreeStatusLineNC", link = "StatusLineNC" },
-- File Text -- File Text
{ group = "NvimTreeExecFile", link = "Question" }, { group = "NvimTreeExecFile", link = "Question" },
{ group = "NvimTreeImageFile", link = "Question" }, { group = "NvimTreeImageFile", link = "Question" },
{ group = "NvimTreeSpecialFile", link = "Title" }, { group = "NvimTreeSpecialFile", link = "Title" },
{ group = "NvimTreeSymlink", link = "Underlined" }, { group = "NvimTreeSymlink", link = "Underlined" },
-- Folder Text -- Folder Text
{ group = "NvimTreeRootFolder", link = "Title" }, { group = "NvimTreeRootFolder", link = "Title" },
{ group = "NvimTreeFolderName", link = "Directory" }, { group = "NvimTreeFolderName", link = "Directory" },
{ group = "NvimTreeEmptyFolderName", link = "Directory" }, { group = "NvimTreeEmptyFolderName", link = "Directory" },
{ group = "NvimTreeOpenedFolderName", link = "Directory" }, { group = "NvimTreeOpenedFolderName", link = "Directory" },
{ group = "NvimTreeSymlinkFolderName", link = "Directory" }, { group = "NvimTreeSymlinkFolderName", link = "Directory" },
-- File Icons -- File Icons
{ group = "NvimTreeFileIcon", link = "NvimTreeNormal" }, { group = "NvimTreeFileIcon", link = "NvimTreeNormal" },
{ group = "NvimTreeSymlinkIcon", link = "NvimTreeNormal" }, { group = "NvimTreeSymlinkIcon", link = "NvimTreeNormal" },
-- Folder Icons -- Folder Icons
{ group = "NvimTreeFolderIcon", def = "guifg=#8094b4 ctermfg=Blue" }, { group = "NvimTreeFolderIcon", def = "guifg=#8094b4 ctermfg=Blue" },
{ group = "NvimTreeOpenedFolderIcon", link = "NvimTreeFolderIcon" }, { group = "NvimTreeOpenedFolderIcon", link = "NvimTreeFolderIcon" },
{ group = "NvimTreeClosedFolderIcon", link = "NvimTreeFolderIcon" }, { group = "NvimTreeClosedFolderIcon", link = "NvimTreeFolderIcon" },
{ group = "NvimTreeFolderArrowClosed", link = "NvimTreeIndentMarker" }, { group = "NvimTreeFolderArrowClosed", link = "NvimTreeIndentMarker" },
{ group = "NvimTreeFolderArrowOpen", link = "NvimTreeIndentMarker" }, { group = "NvimTreeFolderArrowOpen", link = "NvimTreeIndentMarker" },
-- Indent -- Indent
{ group = "NvimTreeIndentMarker", link = "NvimTreeFolderIcon" }, { group = "NvimTreeIndentMarker", link = "NvimTreeFolderIcon" },
-- Picker -- Picker
{ group = "NvimTreeWindowPicker", def = "guifg=#ededed guibg=#4493c8 gui=bold ctermfg=White ctermbg=DarkBlue" }, { group = "NvimTreeWindowPicker", def = "guifg=#ededed guibg=#4493c8 gui=bold ctermfg=White ctermbg=DarkBlue" },
-- LiveFilter -- LiveFilter
{ group = "NvimTreeLiveFilterPrefix", link = "PreProc" }, { group = "NvimTreeLiveFilterPrefix", link = "PreProc" },
{ group = "NvimTreeLiveFilterValue", link = "ModeMsg" }, { group = "NvimTreeLiveFilterValue", link = "ModeMsg" },
-- Clipboard -- Clipboard
{ group = "NvimTreeCutHL", link = "SpellBad" }, { group = "NvimTreeCutHL", link = "SpellBad" },
{ group = "NvimTreeCopiedHL", link = "SpellRare" }, { group = "NvimTreeCopiedHL", link = "SpellRare" },
-- Bookmark -- Bookmark
{ group = "NvimTreeBookmarkIcon", link = "NvimTreeFolderIcon" }, { group = "NvimTreeBookmarkIcon", link = "NvimTreeFolderIcon" },
{ group = "NvimTreeBookmarkHL", link = "SpellLocal" }, { group = "NvimTreeBookmarkHL", link = "SpellLocal" },
-- Modified -- Modified
{ group = "NvimTreeModifiedIcon", link = "Type" }, { group = "NvimTreeModifiedIcon", link = "Type" },
{ group = "NvimTreeModifiedFileHL", link = "NvimTreeModifiedIcon" }, { group = "NvimTreeModifiedFileHL", link = "NvimTreeModifiedIcon" },
{ group = "NvimTreeModifiedFolderHL", link = "NvimTreeModifiedFileHL" }, { group = "NvimTreeModifiedFolderHL", link = "NvimTreeModifiedFileHL" },
-- Hidden -- Hidden
{ group = "NvimTreeHiddenIcon", link = "Conceal" }, { group = "NvimTreeHiddenIcon", link = "Conceal" },
{ group = "NvimTreeHiddenFileHL", link = "NvimTreeHiddenIcon" }, { group = "NvimTreeHiddenFileHL", link = "NvimTreeHiddenIcon" },
{ group = "NvimTreeHiddenFolderHL", link = "NvimTreeHiddenFileHL" }, { group = "NvimTreeHiddenFolderHL", link = "NvimTreeHiddenFileHL" },
-- Hidden Display
{ group = "NvimTreeHiddenDisplay", link = "Conceal" },
-- Opened -- Opened
{ group = "NvimTreeOpenedHL", link = "Special" }, { group = "NvimTreeOpenedHL", link = "Special" },
-- Git Icon -- Git Icon
{ group = "NvimTreeGitDeletedIcon", link = "Statement" }, { group = "NvimTreeGitDeletedIcon", link = "Statement" },
{ group = "NvimTreeGitDirtyIcon", link = "Statement" }, { group = "NvimTreeGitDirtyIcon", link = "Statement" },
{ group = "NvimTreeGitIgnoredIcon", link = "Comment" }, { group = "NvimTreeGitIgnoredIcon", link = "Comment" },
{ group = "NvimTreeGitMergeIcon", link = "Constant" }, { group = "NvimTreeGitMergeIcon", link = "Constant" },
{ group = "NvimTreeGitNewIcon", link = "PreProc" }, { group = "NvimTreeGitNewIcon", link = "PreProc" },
{ group = "NvimTreeGitRenamedIcon", link = "PreProc" }, { group = "NvimTreeGitRenamedIcon", link = "PreProc" },
{ group = "NvimTreeGitStagedIcon", link = "Constant" }, { group = "NvimTreeGitStagedIcon", link = "Constant" },
-- Git File Highlight -- Git File Highlight
{ group = "NvimTreeGitFileDeletedHL", link = "NvimTreeGitDeletedIcon" }, { group = "NvimTreeGitFileDeletedHL", link = "NvimTreeGitDeletedIcon" },
{ group = "NvimTreeGitFileDirtyHL", link = "NvimTreeGitDirtyIcon" }, { group = "NvimTreeGitFileDirtyHL", link = "NvimTreeGitDirtyIcon" },
{ group = "NvimTreeGitFileIgnoredHL", link = "NvimTreeGitIgnoredIcon" }, { group = "NvimTreeGitFileIgnoredHL", link = "NvimTreeGitIgnoredIcon" },
{ group = "NvimTreeGitFileMergeHL", link = "NvimTreeGitMergeIcon" }, { group = "NvimTreeGitFileMergeHL", link = "NvimTreeGitMergeIcon" },
{ group = "NvimTreeGitFileNewHL", link = "NvimTreeGitNewIcon" }, { group = "NvimTreeGitFileNewHL", link = "NvimTreeGitNewIcon" },
{ group = "NvimTreeGitFileRenamedHL", link = "NvimTreeGitRenamedIcon" }, { group = "NvimTreeGitFileRenamedHL", link = "NvimTreeGitRenamedIcon" },
{ group = "NvimTreeGitFileStagedHL", link = "NvimTreeGitStagedIcon" }, { group = "NvimTreeGitFileStagedHL", link = "NvimTreeGitStagedIcon" },
-- Git Folder Highlight -- Git Folder Highlight
{ group = "NvimTreeGitFolderDeletedHL", link = "NvimTreeGitFileDeletedHL" }, { group = "NvimTreeGitFolderDeletedHL", link = "NvimTreeGitFileDeletedHL" },
{ group = "NvimTreeGitFolderDirtyHL", link = "NvimTreeGitFileDirtyHL" }, { group = "NvimTreeGitFolderDirtyHL", link = "NvimTreeGitFileDirtyHL" },
{ group = "NvimTreeGitFolderIgnoredHL", link = "NvimTreeGitFileIgnoredHL" }, { group = "NvimTreeGitFolderIgnoredHL", link = "NvimTreeGitFileIgnoredHL" },
{ group = "NvimTreeGitFolderMergeHL", link = "NvimTreeGitFileMergeHL" }, { group = "NvimTreeGitFolderMergeHL", link = "NvimTreeGitFileMergeHL" },
{ group = "NvimTreeGitFolderNewHL", link = "NvimTreeGitFileNewHL" }, { group = "NvimTreeGitFolderNewHL", link = "NvimTreeGitFileNewHL" },
{ group = "NvimTreeGitFolderRenamedHL", link = "NvimTreeGitFileRenamedHL" }, { group = "NvimTreeGitFolderRenamedHL", link = "NvimTreeGitFileRenamedHL" },
{ group = "NvimTreeGitFolderStagedHL", link = "NvimTreeGitFileStagedHL" }, { group = "NvimTreeGitFolderStagedHL", link = "NvimTreeGitFileStagedHL" },
-- Diagnostics Icon -- Diagnostics Icon
{ group = "NvimTreeDiagnosticErrorIcon", link = "DiagnosticError" }, { group = "NvimTreeDiagnosticErrorIcon", link = "DiagnosticError" },
{ group = "NvimTreeDiagnosticWarnIcon", link = "DiagnosticWarn" }, { group = "NvimTreeDiagnosticWarnIcon", link = "DiagnosticWarn" },
{ group = "NvimTreeDiagnosticInfoIcon", link = "DiagnosticInfo" }, { group = "NvimTreeDiagnosticInfoIcon", link = "DiagnosticInfo" },
{ group = "NvimTreeDiagnosticHintIcon", link = "DiagnosticHint" }, { group = "NvimTreeDiagnosticHintIcon", link = "DiagnosticHint" },
-- Diagnostics File Highlight -- Diagnostics File Highlight
{ group = "NvimTreeDiagnosticErrorFileHL", link = "DiagnosticUnderlineError" }, { group = "NvimTreeDiagnosticErrorFileHL", link = "DiagnosticUnderlineError" },
{ group = "NvimTreeDiagnosticWarnFileHL", link = "DiagnosticUnderlineWarn" }, { group = "NvimTreeDiagnosticWarnFileHL", link = "DiagnosticUnderlineWarn" },
{ group = "NvimTreeDiagnosticInfoFileHL", link = "DiagnosticUnderlineInfo" }, { group = "NvimTreeDiagnosticInfoFileHL", link = "DiagnosticUnderlineInfo" },
{ group = "NvimTreeDiagnosticHintFileHL", link = "DiagnosticUnderlineHint" }, { group = "NvimTreeDiagnosticHintFileHL", link = "DiagnosticUnderlineHint" },
-- Diagnostics Folder Highlight -- Diagnostics Folder Highlight
{ group = "NvimTreeDiagnosticErrorFolderHL", link = "NvimTreeDiagnosticErrorFileHL" }, { group = "NvimTreeDiagnosticErrorFolderHL", link = "NvimTreeDiagnosticErrorFileHL" },
{ group = "NvimTreeDiagnosticWarnFolderHL", link = "NvimTreeDiagnosticWarnFileHL" }, { group = "NvimTreeDiagnosticWarnFolderHL", link = "NvimTreeDiagnosticWarnFileHL" },
{ group = "NvimTreeDiagnosticInfoFolderHL", link = "NvimTreeDiagnosticInfoFileHL" }, { group = "NvimTreeDiagnosticInfoFolderHL", link = "NvimTreeDiagnosticInfoFileHL" },
{ group = "NvimTreeDiagnosticHintFolderHL", link = "NvimTreeDiagnosticHintFileHL" }, { group = "NvimTreeDiagnosticHintFolderHL", link = "NvimTreeDiagnosticHintFileHL" },
} }
-- nvim-tree highlight groups to legacy -- nvim-tree highlight groups to legacy

View File

@ -1,5 +1,3 @@
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---@type table<string, boolean> record of which file is modified ---@type table<string, boolean> record of which file is modified
@ -8,14 +6,16 @@ M._modified = {}
---refresh M._modified ---refresh M._modified
function M.reload_modified() function M.reload_modified()
M._modified = {} M._modified = {}
local bufs = vim.fn.getbufinfo({ bufmodified = 1, buflisted = 1 }) local bufs = vim.fn.getbufinfo { bufmodified = 1, buflisted = 1 }
for _, buf in pairs(bufs) do for _, buf in pairs(bufs) do
local path = buf.name local path = buf.name
if path ~= "" then -- not a [No Name] buffer if path ~= "" then -- not a [No Name] buffer
-- mark all the parent as modified as well -- mark all the parent as modified as well
while M._modified[path] ~= true do while
M._modified[path] ~= true
-- no need to keep going if already recorded -- no need to keep going if already recorded
-- This also prevents an infinite loop -- This also prevents an infinite loop
do
M._modified[path] = true M._modified[path] = true
path = vim.fn.fnamemodify(path, ":h") path = vim.fn.fnamemodify(path, ":h")
end end
@ -23,33 +23,18 @@ function M.reload_modified()
end end
end end
---@param node Node ---@param node table
---@return boolean ---@return boolean
function M.is_modified(node) function M.is_modified(node)
if not M.config.modified.enable then return node
return false and M.config.modified.enable
end and M._modified[node.absolute_path]
and (not node.nodes or M.config.modified.show_on_dirs)
if not M._modified[node.absolute_path] then and (not node.open or M.config.modified.show_on_open_dirs)
return false
end
local dir = node:as(DirectoryNode)
if dir then
if not M.config.modified.show_on_dirs then
return false
end
if dir.open and not M.config.modified.show_on_open_dirs then
return false
end
end
return true
end end
---A buffer exists for the node's absolute path ---A buffer exists for the node's absolute path
---@param node Node ---@param node table
---@return boolean ---@return boolean
function M.is_opened(node) function M.is_opened(node)
return node and vim.fn.bufloaded(node.absolute_path) > 0 return node and vim.fn.bufloaded(node.absolute_path) > 0

View File

@ -1,91 +0,0 @@
--
-- 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,5 +1,4 @@
local api = require("nvim-tree.api") local api = require "nvim-tree.api"
local view = require("nvim-tree.view")
local M = {} local M = {}
@ -12,7 +11,7 @@ local CMDS = {
complete = "dir", complete = "dir",
}, },
command = function(c) command = function(c)
api.tree.open({ path = c.args }) api.tree.open { path = c.args }
end, end,
}, },
{ {
@ -33,12 +32,12 @@ local CMDS = {
complete = "dir", complete = "dir",
}, },
command = function(c) command = function(c)
api.tree.toggle({ api.tree.toggle {
find_file = false, find_file = false,
focus = true, focus = true,
path = c.args, path = c.args,
update_root = false, update_root = false,
}) }
end, end,
}, },
{ {
@ -79,11 +78,11 @@ local CMDS = {
bar = true, bar = true,
}, },
command = function(c) command = function(c)
api.tree.find_file({ api.tree.find_file {
open = true, open = true,
focus = true, focus = true,
update_root = c.bang, update_root = c.bang,
}) }
end, end,
}, },
{ {
@ -95,12 +94,12 @@ local CMDS = {
complete = "dir", complete = "dir",
}, },
command = function(c) command = function(c)
api.tree.toggle({ api.tree.toggle {
find_file = true, find_file = true,
focus = true, focus = true,
path = c.args, path = c.args,
update_root = c.bang, update_root = c.bang,
}) }
end, end,
}, },
{ {
@ -111,7 +110,11 @@ local CMDS = {
bar = true, bar = true,
}, },
command = function(c) command = function(c)
view.resize(c.args) local explorer = require "nvim-tree.core".get_explorer();
if not explorer then
return
end
explorer.view:resize(c.args)
end, end,
}, },
{ {

View File

@ -1,7 +1,6 @@
local events = require("nvim-tree.events") local events = require "nvim-tree.events"
local notify = require("nvim-tree.notify") local explorer = require "nvim-tree.explorer"
local view = require("nvim-tree.view") local log = require "nvim-tree.log"
local log = require("nvim-tree.log")
local M = {} local M = {}
@ -16,21 +15,7 @@ function M.init(foldername)
if TreeExplorer then if TreeExplorer then
TreeExplorer:destroy() TreeExplorer:destroy()
end end
TreeExplorer = explorer.Explorer.new(foldername)
local err, path
if foldername then
path, err = vim.loop.fs_realpath(foldername)
else
path, err = vim.loop.cwd()
end
if path then
TreeExplorer = require("nvim-tree.explorer")({ path = path })
else
notify.error(err)
TreeExplorer = nil
end
if not first_init_done then if not first_init_done then
events._dispatch_ready() events._dispatch_ready()
first_init_done = true first_init_done = true
@ -55,7 +40,7 @@ end
---@return integer ---@return integer
function M.get_nodes_starting_line() function M.get_nodes_starting_line()
local offset = 1 local offset = 1
if view.is_root_folder_visible(M.get_cwd()) then if TreeExplorer and TreeExplorer.view:is_root_folder_visible(M.get_cwd()) then
offset = offset + 1 offset = offset + 1
end end
if TreeExplorer and TreeExplorer.live_filter.filter then if TreeExplorer and TreeExplorer.live_filter.filter then

View File

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

View File

@ -1,14 +1,22 @@
local M = {} local M = {}
---Reason for filter in filter.lua ---Setup options for "highlight_*"
---@enum FILTER_REASON ---@enum HL_POSITION
M.FILTER_REASON = { M.HL_POSITION = {
none = 0, -- It's not filtered none = 0,
git = 1, icon = 1,
buf = 2, name = 2,
dotfile = 4, all = 4,
custom = 8, }
bookmark = 16,
---Setup options for "*_placement"
---@enum ICON_PLACEMENT
M.ICON_PLACEMENT = {
none = 0,
signcolumn = 1,
before = 2,
after = 3,
right_align = 4,
} }
return M return M

View File

@ -1,4 +1,4 @@
local notify = require("nvim-tree.notify") local notify = require "nvim-tree.notify"
local M = {} local M = {}
@ -8,7 +8,6 @@ M.Event = {
Ready = "Ready", Ready = "Ready",
WillRenameNode = "WillRenameNode", WillRenameNode = "WillRenameNode",
NodeRenamed = "NodeRenamed", NodeRenamed = "NodeRenamed",
TreePreOpen = "TreePreOpen",
TreeOpen = "TreeOpen", TreeOpen = "TreeOpen",
TreeClose = "TreeClose", TreeClose = "TreeClose",
WillCreateFile = "WillCreateFile", WillCreateFile = "WillCreateFile",
@ -92,11 +91,6 @@ function M._dispatch_folder_removed(folder_name)
dispatch(M.Event.FolderRemoved, { folder_name = folder_name }) dispatch(M.Event.FolderRemoved, { folder_name = folder_name })
end end
--@private
function M._dispatch_on_tree_pre_open()
dispatch(M.Event.TreePreOpen, nil)
end
--@private --@private
function M._dispatch_on_tree_open() function M._dispatch_on_tree_open()
dispatch(M.Event.TreeOpen, nil) dispatch(M.Event.TreeOpen, nil)

View File

@ -0,0 +1,94 @@
local utils = require "nvim-tree.utils"
local builders = require "nvim-tree.explorer.node-builders"
local explorer_node = require "nvim-tree.explorer.node"
local git = require "nvim-tree.git"
local log = require "nvim-tree.log"
local Watcher = require "nvim-tree.watcher"
local M = {}
---@param handle uv.uv_fs_t
---@param cwd string
---@param node Node
---@param git_status table
---@param parent Explorer
local function populate_children(handle, cwd, node, git_status, parent)
local node_ignored = explorer_node.is_git_ignored(node)
local nodes_by_path = utils.bool_record(node.nodes, "absolute_path")
local filter_status = parent.filters:prepare(git_status)
while true do
local name, t = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local abs = utils.path_join { cwd, name }
local profile = log.profile_start("explore populate_children %s", abs)
---@type uv.fs_stat.result|nil
local stat = vim.loop.fs_stat(abs)
if not parent.filters:should_filter(abs, stat, filter_status) and not nodes_by_path[abs] and Watcher.is_fs_event_capable(abs) then
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
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)
end
end
log.profile_end(profile)
end
end
---@param node Node
---@param status table
---@param parent Explorer
---@return Node[]|nil
function M.explore(node, status, parent)
local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd)
if not handle then
return
end
local profile = log.profile_start("explore init %s", node.absolute_path)
populate_children(handle, cwd, node, status, parent)
local is_root = not node.parent
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
if M.config.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 = M.explore(child_folder_only, child_status, parent)
node.nodes = ns or {}
log.profile_end(profile)
return ns
end
parent.sorters:sort(node.nodes)
parent.live_filter:apply_filter(node)
log.profile_end(profile)
return node.nodes
end
function M.setup(opts)
M.config = opts.renderer
end
return M

View File

@ -1,59 +1,51 @@
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
local Class = require("nvim-tree.classic") ---@class Filters to handle all opts.filters and related API
---@field config table hydrated user opts.filters
---@alias FilterType "custom" | "dotfiles" | "git_ignored" | "git_clean" | "no_buffer" | "no_bookmark"
---@class (exact) Filters: Class
---@field enabled boolean
---@field state table<FilterType, boolean>
---@field private explorer Explorer ---@field private explorer Explorer
---@field private exclude_list string[] filters.exclude ---@field private exclude_list string[] filters.exclude
---@field private ignore_list table<string, boolean> filters.custom string table ---@field private ignore_list string[] filters.custom string table
---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function ---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function
local Filters = Class:extend() local Filters = {}
---@class Filters ---@param opts table user options
---@overload fun(args: FiltersArgs): Filters ---@param explorer Explorer
---@return Filters
---@class (exact) FiltersArgs function Filters:new(opts, explorer)
---@field explorer Explorer local o = {
explorer = explorer,
---@protected ignore_list = {},
---@param args FiltersArgs exclude_list = opts.filters.exclude,
function Filters:new(args) custom_function = nil,
self.explorer = args.explorer config = {
self.ignore_list = {} enable = opts.filters.enable,
self.exclude_list = self.explorer.opts.filters.exclude filter_custom = true,
self.custom_function = nil filter_dotfiles = opts.filters.dotfiles,
filter_git_ignored = opts.filters.git_ignored,
self.enabled = self.explorer.opts.filters.enable filter_git_clean = opts.filters.git_clean,
self.state = { filter_no_buffer = opts.filters.no_buffer,
custom = true, filter_no_bookmark = opts.filters.no_bookmark,
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 = self.explorer.opts.filters.custom local custom_filter = opts.filters.custom
if type(custom_filter) == "function" then if type(custom_filter) == "function" then
self.custom_function = custom_filter o.custom_function = custom_filter
else else
if custom_filter and #custom_filter > 0 then if custom_filter and #custom_filter > 0 then
for _, filter_name in pairs(custom_filter) do for _, filter_name in pairs(custom_filter) do
self.ignore_list[filter_name] = true o.ignore_list[filter_name] = true
end end
end end
end end
setmetatable(o, self)
self.__index = self
return o
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
function Filters:is_excluded(path) local function is_excluded(self, path)
for _, node in ipairs(self.exclude_list) do for _, node in ipairs(self.exclude_list) do
if path:match(node) then if path:match(node) then
return true return true
@ -63,27 +55,26 @@ function Filters:is_excluded(path)
end end
---Check if the given path is git clean/ignored ---Check if the given path is git clean/ignored
---@private
---@param path string Absolute path ---@param path string Absolute path
---@param project GitProject from prepare ---@param git_status table from prepare
---@return boolean ---@return boolean
function Filters:git(path, project) local function git(self, path, git_status)
if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then if type(git_status) ~= "table" or type(git_status.files) ~= "table" or type(git_status.dirs) ~= "table" then
return false return false
end end
-- default status to clean -- default status to clean
local xy = project.files[path] local status = git_status.files[path]
xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1] status = status or git_status.dirs.direct[path] and git_status.dirs.direct[path][1]
xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1] status = status or git_status.dirs.indirect[path] and git_status.dirs.indirect[path][1]
-- filter ignored; overrides clean as they are effectively dirty -- filter ignored; overrides clean as they are effectively dirty
if self.state.git_ignored and xy == "!!" then if self.config.filter_git_ignored and status == "!!" then
return true return true
end end
-- filter clean -- filter clean
if self.state.git_clean and not xy then if self.config.filter_git_clean and not status then
return true return true
end end
@ -91,12 +82,11 @@ function Filters:git(path, project)
end end
---Check if the given path has no listed buffer ---Check if the given path has no listed buffer
---@private
---@param path string Absolute path ---@param path string Absolute path
---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 } ---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 }
---@return boolean ---@return boolean
function Filters:buf(path, bufinfo) local function buf(self, path, bufinfo)
if not self.state.no_buffer or type(bufinfo) ~= "table" then if not self.config.filter_no_buffer or type(bufinfo) ~= "table" then
return false return false
end end
@ -110,21 +100,17 @@ function Filters:buf(path, bufinfo)
return true return true
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
function Filters:dotfile(path) local function dotfile(self, path)
return self.state.dotfiles and utils.path_basename(path):sub(1, 1) == "." return self.config.filter_dotfiles and utils.path_basename(path):sub(1, 1) == "."
end end
---Bookmark is present
---@private
---@param path string ---@param path string
---@param path_type string|nil filetype of path ---@param path_type string|nil filetype of path
---@param bookmarks table<string, string|nil> path, filetype table of bookmarked files ---@param bookmarks table<string, string|nil> path, filetype table of bookmarked files
---@return boolean local function bookmark(self, path, path_type, bookmarks)
function Filters:bookmark(path, path_type, bookmarks) if not self.config.filter_no_bookmark then
if not self.state.no_bookmark then
return false return false
end end
-- if bookmark is empty, we should see a empty filetree -- if bookmark is empty, we should see a empty filetree
@ -156,11 +142,10 @@ function Filters:bookmark(path, path_type, bookmarks)
return true return true
end end
---@private
---@param path string ---@param path string
---@return boolean ---@return boolean
function Filters:custom(path) local function custom(self, path)
if not self.state.custom then if not self.config.filter_custom then
return false return false
end end
@ -179,7 +164,7 @@ function Filters:custom(path)
end end
end end
local idx = path:match(".+()%.[^.]+$") local idx = path:match ".+()%.[^.]+$"
if idx then if idx then
if self.ignore_list["*" .. string.sub(path, idx)] == true then if self.ignore_list["*" .. string.sub(path, idx)] == true then
return true return true
@ -190,25 +175,25 @@ function Filters:custom(path)
end end
---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons. ---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons.
---@param project GitProject? optional results of git.load_projects(...) ---@param git_status table|nil optional results of git.load_project_status(...)
---@return table ---@return table
--- project: reference --- git_status: reference
--- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } --- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 }
--- bookmarks: absolute paths to boolean --- bookmarks: absolute paths to boolean
function Filters:prepare(project) function Filters:prepare(git_status)
local status = { local status = {
project = project or {}, git_status = git_status or {},
bufinfo = {}, bufinfo = {},
bookmarks = {}, bookmarks = {},
} }
if self.state.no_buffer then if self.config.filter_no_buffer then
status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 }) status.bufinfo = vim.fn.getbufinfo { buflisted = 1 }
end end
local explorer = require("nvim-tree.core").get_explorer() local explorer = require("nvim-tree.core").get_explorer()
if explorer then if explorer then
for _, node in pairs(explorer.marks:list()) do for _, node in pairs(explorer.marks:get_marks()) do
status.bookmarks[node.absolute_path] = node.type status.bookmarks[node.absolute_path] = node.type
end end
end end
@ -222,66 +207,20 @@ end
---@param status table from prepare ---@param status table from prepare
---@return boolean ---@return boolean
function Filters:should_filter(path, fs_stat, status) function Filters:should_filter(path, fs_stat, status)
if not self.enabled then if not self.config.enable then
return false return false
end end
-- exclusions override all filters -- exclusions override all filters
if self:is_excluded(path) then if is_excluded(self, path) then
return false return false
end end
return self:git(path, status.project) return git(self, path, status.git_status)
or self:buf(path, status.bufinfo) or buf(self, path, status.bufinfo)
or self:dotfile(path) or dotfile(self, path)
or self:custom(path) or custom(self, path)
or self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks) or bookmark(self, 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
---@param path string Absolute path
---@param fs_stat uv.fs_stat.result|nil fs_stat of file
---@param status table from prepare
---@return FILTER_REASON
function Filters:should_filter_as_reason(path, fs_stat, status)
if not self.enabled then
return FILTER_REASON.none
end
if self:is_excluded(path) then
return FILTER_REASON.none
end
if self:git(path, status.project) then
return FILTER_REASON.git
elseif self:buf(path, status.bufinfo) then
return FILTER_REASON.buf
elseif self:dotfile(path) then
return FILTER_REASON.dotfile
elseif self:custom(path) then
return FILTER_REASON.custom
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
self.explorer:focus_node_or_parent(node)
end
end end
return Filters return Filters

View File

@ -1,672 +1,94 @@
local appearance = require("nvim-tree.appearance") local git = require "nvim-tree.git"
local buffers = require("nvim-tree.buffers") local notify = require "nvim-tree.notify"
local core = require("nvim-tree.core") local watch = require "nvim-tree.explorer.watch"
local git = require("nvim-tree.git") local explorer_node = require "nvim-tree.explorer.node"
local log = require("nvim-tree.log") local Filters = require "nvim-tree.explorer.filters"
local utils = require("nvim-tree.utils") local Marks = require "nvim-tree.marks"
local view = require("nvim-tree.view") local LiveFilter = require "nvim-tree.explorer.live-filter"
local node_factory = require("nvim-tree.node.factory") local Sorters = require "nvim-tree.explorer.sorters"
local View = require "nvim-tree.explorer.view"
local DirectoryNode = require("nvim-tree.node.directory") local M = {}
local RootNode = require("nvim-tree.node.root")
local Watcher = require("nvim-tree.watcher")
local Iterator = require("nvim-tree.iterators.node-iterator") M.explore = require("nvim-tree.explorer.explore").explore
local NodeIterator = require("nvim-tree.iterators.node-iterator") M.reload = require("nvim-tree.explorer.reload").reload
local Filters = require("nvim-tree.explorer.filters") ---@class Explorer
local Marks = require("nvim-tree.marks") ---@field absolute_path string
local LiveFilter = require("nvim-tree.explorer.live-filter") ---@field nodes Node[]
local Sorter = require("nvim-tree.explorer.sorter") ---@field open boolean
local Clipboard = require("nvim-tree.actions.fs.clipboard")
local Renderer = require("nvim-tree.renderer")
local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON
local config
---@class (exact) Explorer: RootNode
---@field uid_explorer number vim.loop.hrtime() at construction time
---@field opts table user options
---@field augroup_id integer
---@field renderer Renderer
---@field filters Filters ---@field filters Filters
---@field live_filter LiveFilter ---@field live_filter LiveFilter
---@field sorters Sorter ---@field sorters Sorter
---@field marks Marks ---@field marks Marks
---@field clipboard Clipboard
local Explorer = RootNode:extend()
---@class Explorer local Explorer = {}
---@overload fun(args: ExplorerArgs): Explorer Explorer.__index = Explorer
---@class (exact) ExplorerArgs ---@param path string|nil
---@field path string ---@return Explorer|nil
function Explorer.new(path)
local err
---@protected if path then
---@param args ExplorerArgs path, err = vim.loop.fs_realpath(path)
function Explorer:new(args) else
Explorer.super.new(self, { path, err = vim.loop.cwd()
explorer = self, end
absolute_path = args.path, if not path then
name = "..", notify.error(err)
}) return
self.uid_explorer = vim.loop.hrtime()
self.augroup_id = vim.api.nvim_create_augroup("NvimTree_Explorer_" .. self.uid_explorer, {})
self.open = true
self.opts = config
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 })
self:create_autocmds()
self:_load(self)
end
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)
-- only handle normal files
if vim.bo[data.buf].buftype ~= "" then
return
end
if self.filters.state.no_buffer then
-- full reload is required to update the filter state
utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
self:reload_explorer()
end)
elseif self.opts.renderer.highlight_opened_files ~= "none" then
-- draw to update opened highlight
self.renderer:draw()
end
end,
})
-- update opened file buffers
vim.api.nvim_create_autocmd("BufUnload", {
group = self.augroup_id,
callback = function(data)
-- only handle normal files
if vim.bo[data.buf].buftype ~= "" then
return
end
if self.filters.state.no_buffer then
-- full reload is required to update the filter state
utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function()
self:reload_explorer()
end)
elseif self.opts.renderer.highlight_opened_files ~= "none" then
-- draw to update opened highlight; must be delayed as the buffer is still loaded during BufUnload
vim.schedule(function()
self.renderer:draw()
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 end
if self.opts.modified.enable then ---@class Explorer
vim.api.nvim_create_autocmd({ "BufModifiedSet", "BufWritePost" }, { local explorer = setmetatable({
group = self.augroup_id, absolute_path = path,
callback = function() nodes = {},
utils.debounce("Buf:modified_" .. self.uid_explorer, self.opts.view.debounce_delay, function() open = true,
buffers.reload_modified() marks = Marks:new(),
self:reload_explorer() sorters = Sorters:new(M.config),
end) view = View:new(M.config),
end, }, Explorer)
}) explorer.watcher = watch.create_watcher(explorer)
end explorer.filters = Filters:new(M.config, explorer)
explorer.live_filter = LiveFilter:new(M.config, explorer)
explorer:_load(explorer)
return explorer
end end
---@param node DirectoryNode ---@private
---@param node Node
function Explorer:_load(node)
local cwd = node.link_to or node.absolute_path
local git_status = git.load_project_status(cwd)
M.explore(node, git_status, self)
end
---@param node Node
function Explorer:expand(node) function Explorer:expand(node)
self:_load(node) self:_load(node)
end end
---@param node DirectoryNode function Explorer:destroy()
---@param project GitProject? local function iterate(node)
---@return Node[]? explorer_node.node_destroy(node)
function Explorer:reload(node, project) if node.nodes then
local cwd = node.link_to or node.absolute_path for _, child in pairs(node.nodes) do
local handle = vim.loop.fs_scandir(cwd) iterate(child)
if not handle then
return
end
local profile = log.profile_start("reload %s", node.absolute_path)
local filter_status = self.filters:prepare(project)
if node.group_next then
node.nodes = { node.group_next }
node.group_next = nil
end
local remain_childs = {}
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,
bookmark = 0,
})
while true do
local name, _ = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local abs = utils.path_join({ cwd, name })
-- path incorrectly specified as an integer
local stat = vim.loop.fs_lstat(abs) ---@diagnostic disable-line param-type-mismatch
local filter_reason = self.filters:should_filter_as_reason(abs, stat, filter_status)
if filter_reason == FILTER_REASON.none then
remain_childs[abs] = true
-- Recreate node if type changes.
if nodes_by_path[abs] then
local n = nodes_by_path[abs]
if not stat or n.type ~= stat.type then
utils.array_remove(node.nodes, n)
n:destroy()
nodes_by_path[abs] = nil
end
end
if not nodes_by_path[abs] then
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
end
else
local n = nodes_by_path[abs]
if n then
n.executable = utils.is_executable(abs) or false
n.fs_stat = stat
end
end
else
for reason, value in pairs(FILTER_REASON) do
if filter_reason == value then
node.hidden_stats[reason] = node.hidden_stats[reason] + 1
end
end end
end end
end end
iterate(self)
node.nodes = vim.tbl_map(
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
n:destroy()
return false
end
end, node.nodes)
)
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
end
self.sorters:sort(node.nodes)
self.live_filter:apply_filter(node)
log.profile_end(profile)
return node.nodes
end end
---Refresh contents of all nodes to a path: actual directory and links. function M.setup(opts)
---Groups will be expanded if needed. M.config = opts
---@param path string absolute path require("nvim-tree.explorer.node").setup(opts)
function Explorer:refresh_parent_nodes_for_path(path) require("nvim-tree.explorer.explore").setup(opts)
local profile = log.profile_start("refresh_parent_nodes_for_path %s", path) require("nvim-tree.explorer.reload").setup(opts)
require("nvim-tree.explorer.watch").setup(opts)
-- collect parent nodes from the top down
local parent_nodes = {}
NodeIterator.builder({ self })
:recursor(function(node)
return node.nodes
end)
:applier(function(node)
local abs_contains = node.absolute_path and path:find(node.absolute_path, 1, true) == 1
local link_contains = node.link_to and path:find(node.link_to, 1, true) == 1
if abs_contains or link_contains then
table.insert(parent_nodes, node)
end
end)
:iterate()
-- refresh in order; this will expand groups as needed
for _, node in ipairs(parent_nodes) do
local toplevel = git.get_toplevel(node.absolute_path)
local project = git.get_project(toplevel) or {}
self:reload(node, project)
git.update_parent_projects(node, project, toplevel)
end
log.profile_end(profile)
end end
---@private M.Explorer = Explorer
---@param node DirectoryNode
function Explorer:_load(node)
local cwd = node.link_to or node.absolute_path
local project = git.load_project(cwd)
self:explore(node, project, self)
end
---@private return M
---@param nodes_by_path Node[]
---@param node_ignored boolean
---@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
node:update_git_status(node_ignored, project)
end
return node
end
end
---@private
---@param handle uv.uv_fs_t
---@param cwd string
---@param node DirectoryNode
---@param project GitProject
---@param parent Explorer
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(project)
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
git = 0,
buf = 0,
dotfile = 0,
custom = 0,
bookmark = 0,
})
while true do
local name, _ = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local abs = utils.path_join({ cwd, name })
if Watcher.is_fs_event_capable(abs) then
local profile = log.profile_start("populate_children %s", abs)
-- path incorrectly specified as an integer
local stat = vim.loop.fs_lstat(abs) ---@diagnostic disable-line param-type-mismatch
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
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
child:update_git_status(node_ignored, project)
end
elseif node.hidden_stats then
for reason, value in pairs(FILTER_REASON) do
if filter_reason == value and type(node.hidden_stats[reason]) == "number" then
node.hidden_stats[reason] = node.hidden_stats[reason] + 1
end
end
end
log.profile_end(profile)
end
end
end
---@private
---@param node DirectoryNode
---@param project GitProject
---@param parent Explorer
---@return Node[]|nil
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
return
end
local profile = log.profile_start("explore %s", node.absolute_path)
self:populate_children(handle, cwd, node, project, parent)
local is_root = not node.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)
return ns
end
parent.sorters:sort(node.nodes)
parent.live_filter:apply_filter(node)
log.profile_end(profile)
return node.nodes
end
---@private
---@param projects GitProject[]
function Explorer:refresh_nodes(projects)
Iterator.builder({ self })
:applier(function(n)
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)
return n.group_next and { n.group_next } or (n.open and n.nodes)
end)
:iterate()
end
local event_running = false
function Explorer:reload_explorer()
if event_running or vim.v.exiting ~= vim.NIL then
return
end
event_running = true
local projects = git.reload_all_projects()
self:refresh_nodes(projects)
if view.is_visible() then
self.renderer:draw()
end
event_running = false
end
function Explorer:reload_git()
if not git.config.git.enable or event_running then
return
end
event_running = true
local projects = git.reload_all_projects()
git.reload_node_status(self, projects)
self.renderer:draw()
event_running = false
end
---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 self:get_nodes_by_line(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
-- Find the line number of a node.
---@param node Node?
---@return integer -1 not found
function Explorer:find_node_line(node)
if not node then
return -1
end
local first_node_line = core.get_nodes_starting_line()
local nodes_by_line = self:get_nodes_by_line(first_node_line)
local iter_start, iter_end = first_node_line, #nodes_by_line
for line = iter_start, iter_end, 1 do
if nodes_by_line[line] == node then
return line
end
end
return -1
end
-- get the node in the tree state depending on the absolute path of the node
-- (grouped or hidden too)
---@param path string
---@return Node|nil
---@return number|nil
function Explorer:get_node_from_path(path)
if self.absolute_path == path then
return self
end
return Iterator.builder(self.nodes)
:hidden()
:matcher(function(node)
return node.absolute_path == path or node.link_to == path
end)
:recursor(function(node)
if node.group_next then
return { node.group_next }
end
if node.nodes then
return node.nodes
end
end)
:iterate()
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? node to focus
function Explorer:focus_node_or_parent(node)
while node do
local found_node, i = self:find_node(function(node_)
return node_.absolute_path == node.absolute_path
end)
if found_node or node.parent == nil then
view.set_cursor({ i + 1, 1 })
break
end
node = node.parent
end
end
--- Get the node and index of the node from the tree that matches the predicate.
--- The explored nodes are those displayed on the view.
---@param fn fun(node: Node): boolean
---@return table|nil
---@return number
function Explorer:find_node(fn)
local node, i = Iterator.builder(self.nodes)
:matcher(fn)
:recursor(function(node)
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
end)
:iterate()
i = view.is_root_folder_visible() and i or i - 1
if node and node.explorer.live_filter.filter then
i = i + 1
end
return node, i
end
--- Return visible nodes indexed by line
---@param line_start number
---@return table
function Explorer:get_nodes_by_line(line_start)
local nodes_by_line = {}
local line = line_start
Iterator.builder(self.nodes)
:applier(function(node)
if node.group_next then
return
end
nodes_by_line[line] = node
line = line + 1
end)
:recursor(function(node)
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
end)
:iterate()
return nodes_by_line
end
---Api.tree.get_nodes
---@return nvim_tree.api.Node
function Explorer:get_nodes()
return self:clone()
end
function Explorer:setup(opts)
config = opts
end
return Explorer

View File

@ -1,33 +1,32 @@
local view = require("nvim-tree.view") local utils = require "nvim-tree.utils"
local utils = require("nvim-tree.utils") local Iterator = require "nvim-tree.iterators.node-iterator"
local Class = require("nvim-tree.classic") ---@class LiveFilter
local Iterator = require("nvim-tree.iterators.node-iterator")
local DirectoryNode = require("nvim-tree.node.directory")
---@class (exact) LiveFilter: Class
---@field explorer Explorer ---@field explorer Explorer
---@field prefix string ---@field prefix string
---@field always_show_folders boolean ---@field always_show_folders boolean
---@field filter string ---@field filter string
local LiveFilter = Class:extend() local LiveFilter = {}
---@class LiveFilter ---@param opts table
---@overload fun(args: LiveFilterArgs): LiveFilter ---@param explorer Explorer
function LiveFilter:new(opts, explorer)
---@class (exact) LiveFilterArgs local o = {
---@field explorer Explorer explorer = explorer,
prefix = opts.live_filter.prefix,
---@protected always_show_folders = opts.live_filter.always_show_folders,
---@param args LiveFilterArgs filter = nil,
function LiveFilter:new(args) }
self.explorer = args.explorer setmetatable(o, self)
self.prefix = self.explorer.opts.live_filter.prefix self.__index = self
self.always_show_folders = self.explorer.opts.live_filter.always_show_folders return o
self.filter = nil
end end
---@param node_ Node? local function redraw()
require("nvim-tree.renderer").draw()
end
---@param node_ Node|nil
local function reset_filter(self, node_) local function reset_filter(self, node_)
node_ = node_ or self.explorer node_ = node_ or self.explorer
@ -35,19 +34,10 @@ local function reset_filter(self, node_)
return return
end end
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) Iterator.builder(node_.nodes)
:hidden() :hidden()
:applier(function(node) :applier(function(node)
node.hidden = false node.hidden = false
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) end)
:iterate() :iterate()
end end
@ -56,14 +46,14 @@ local overlay_bufnr = 0
local overlay_winnr = 0 local overlay_winnr = 0
local function remove_overlay(self) local function remove_overlay(self)
if view.View.float.enable and view.View.float.quit_on_focus_loss then if self.explorer.view.View.float.enable and self.explorer.view.View.float.quit_on_focus_loss then
-- return to normal nvim-tree float behaviour when filter window is closed -- return to normal nvim-tree float behaviour when filter window is closed
vim.api.nvim_create_autocmd("WinLeave", { vim.api.nvim_create_autocmd("WinLeave", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
group = vim.api.nvim_create_augroup("NvimTree", { clear = false }), group = vim.api.nvim_create_augroup("NvimTree", { clear = false }),
callback = function() callback = function()
if utils.is_nvim_tree_buf(0) then if utils.is_nvim_tree_buf(0) then
view.close() self.explorer.view:close()
end end
end, end,
}) })
@ -82,7 +72,7 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
local function matches(self, node) local function matches(self, node)
if not self.explorer.filters.enabled then if not self.explorer.filters.config.enable then
return true return true
end end
@ -91,23 +81,19 @@ local function matches(self, node)
return vim.regex(self.filter):match_str(name) ~= nil return vim.regex(self.filter):match_str(name) ~= nil
end end
---@param node_ DirectoryNode? ---@param node_ Node|nil
function LiveFilter:apply_filter(node_) function LiveFilter:apply_filter(node_)
if not self.filter or self.filter == "" then if not self.filter or self.filter == "" then
reset_filter(self, node_) reset_filter(self, node_)
return return
end end
-- this iterator cannot yet be refactored with the Iterator module -- TODO(kiyan): this iterator cannot yet be refactored with the Iterator module
-- since the node mapper is based on its children -- since the node mapper is based on its children
local function iterate(node) local function iterate(node)
local filtered_nodes = 0 local filtered_nodes = 0
local nodes = node.group_next and { node.group_next } or node.nodes local nodes = node.group_next and { node.group_next } or node.nodes
node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, {
live_filter = 0,
})
if nodes then if nodes then
for _, n in pairs(nodes) do for _, n in pairs(nodes) do
iterate(n) iterate(n)
@ -117,8 +103,6 @@ function LiveFilter:apply_filter(node_)
end end
end end
node.hidden_stats.live_filter = filtered_nodes
local has_nodes = nodes and (self.always_show_folders or #nodes > filtered_nodes) local has_nodes = nodes and (self.always_show_folders or #nodes > filtered_nodes)
local ok, is_match = pcall(matches, self, node) local ok, is_match = pcall(matches, self, node)
node.hidden = not (has_nodes or (ok and is_match)) node.hidden = not (has_nodes or (ok and is_match))
@ -131,7 +115,7 @@ local function record_char(self)
vim.schedule(function() vim.schedule(function()
self.filter = vim.api.nvim_buf_get_lines(overlay_bufnr, 0, -1, false)[1] self.filter = vim.api.nvim_buf_get_lines(overlay_bufnr, 0, -1, false)[1]
self:apply_filter() self:apply_filter()
self.explorer.renderer:draw() redraw()
end) end)
end end
@ -156,7 +140,7 @@ end
---@return integer ---@return integer
local function calculate_overlay_win_width(self) local function calculate_overlay_win_width(self)
local wininfo = vim.fn.getwininfo(view.get_winnr())[1] local wininfo = vim.fn.getwininfo(self.explorer.view:get_winnr())[1]
if wininfo then if wininfo then
return wininfo.width - wininfo.textoff - #self.prefix return wininfo.width - wininfo.textoff - #self.prefix
@ -166,47 +150,45 @@ local function calculate_overlay_win_width(self)
end end
local function create_overlay(self) local function create_overlay(self)
if view.View.float.enable then if self.explorer.view.View.float.enable then
-- don't close nvim-tree float when focus is changed to filter window -- don't close nvim-tree float when focus is changed to filter window
vim.api.nvim_clear_autocmds({ vim.api.nvim_clear_autocmds {
event = "WinLeave", event = "WinLeave",
pattern = "NvimTree_*", pattern = "NvimTree_*",
group = vim.api.nvim_create_augroup("NvimTree", { clear = false }), group = vim.api.nvim_create_augroup("NvimTree", { clear = false }),
}) }
end end
configure_buffer_overlay(self) configure_buffer_overlay(self)
overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, { overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, {
col = 1, col = 1,
row = 0, row = 0,
relative = "cursor", relative = "cursor",
width = calculate_overlay_win_width(self), width = calculate_overlay_win_width(self),
height = 1, height = 1,
border = "none", border = "none",
style = "minimal", style = "minimal",
}) })
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("modifiable", true, { buf = overlay_bufnr }) vim.api.nvim_set_option_value("modifiable", true, { buf = overlay_bufnr })
vim.api.nvim_set_option_value("filetype", "NvimTreeFilter", { buf = overlay_bufnr })
else else
vim.api.nvim_buf_set_option(overlay_bufnr, "modifiable", true) ---@diagnostic disable-line: deprecated vim.api.nvim_buf_set_option(overlay_bufnr, "modifiable", true) ---@diagnostic disable-line: deprecated
vim.api.nvim_buf_set_option(overlay_bufnr, "filetype", "NvimTreeFilter") ---@diagnostic disable-line: deprecated
end end
vim.api.nvim_buf_set_lines(overlay_bufnr, 0, -1, false, { self.filter }) vim.api.nvim_buf_set_lines(overlay_bufnr, 0, -1, false, { self.filter })
vim.cmd("startinsert") vim.cmd "startinsert"
vim.api.nvim_win_set_cursor(overlay_winnr, { 1, #self.filter + 1 }) vim.api.nvim_win_set_cursor(overlay_winnr, { 1, #self.filter + 1 })
end end
function LiveFilter:start_filtering() function LiveFilter:start_filtering()
view.View.live_filter.prev_focused_node = self.explorer:get_node_at_cursor() self.explorer.view.View.live_filter.prev_focused_node = require("nvim-tree.lib").get_node_at_cursor()
self.filter = self.filter or "" self.filter = self.filter or ""
self.explorer.renderer:draw() redraw()
local row = require("nvim-tree.core").get_nodes_starting_line() - 1 local row = require("nvim-tree.core").get_nodes_starting_line() - 1
local col = #self.prefix > 0 and #self.prefix - 1 or 1 local col = #self.prefix > 0 and #self.prefix - 1 or 1
view.set_cursor({ row, col }) self.explorer.view:set_cursor { row, col }
-- needs scheduling to let the cursor move before initializing the window -- needs scheduling to let the cursor move before initializing the window
vim.schedule(function() vim.schedule(function()
return create_overlay(self) return create_overlay(self)
@ -214,17 +196,17 @@ function LiveFilter:start_filtering()
end end
function LiveFilter:clear_filter() function LiveFilter:clear_filter()
local node = self.explorer:get_node_at_cursor() local node = require("nvim-tree.lib").get_node_at_cursor()
local last_node = view.View.live_filter.prev_focused_node local last_node = self.explorer.view.View.live_filter.prev_focused_node
self.filter = nil self.filter = nil
reset_filter(self) reset_filter(self)
self.explorer.renderer:draw() redraw()
if node then if node then
self.explorer:focus_node_or_parent(node) utils.focus_file(node.absolute_path)
elseif last_node then elseif last_node then
self.explorer:focus_node_or_parent(last_node) utils.focus_file(last_node.absolute_path)
end end
end end

View File

@ -0,0 +1,107 @@
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

@ -0,0 +1,162 @@
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 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,
}
end
return M

View File

@ -0,0 +1,233 @@
local utils = require "nvim-tree.utils"
local builders = require "nvim-tree.explorer.node-builders"
local explorer_node = require "nvim-tree.explorer.node"
local git = require "nvim-tree.git"
local log = require "nvim-tree.log"
local NodeIterator = require "nvim-tree.iterators.node-iterator"
local Watcher = require "nvim-tree.watcher"
local M = {}
---@param nodes_by_path table
---@param node_ignored boolean
---@param status table
---@return fun(node: Node): table
local function update_status(nodes_by_path, node_ignored, status)
return function(node)
if nodes_by_path[node.absolute_path] then
explorer_node.update_git_status(node, node_ignored, status)
end
return node
end
end
---@param path string
---@param callback fun(toplevel: string|nil, project: table|nil)
local function 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
---@param node Node
---@param project table|nil
---@param root string|nil
local function 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
---@param node Node
---@param git_status table
function M.reload(node, git_status)
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then
return
end
local cwd = node.link_to or node.absolute_path
local handle = vim.loop.fs_scandir(cwd)
if not handle then
return
end
local profile = log.profile_start("reload %s", node.absolute_path)
local filter_status = explorer.filters:prepare(git_status)
if node.group_next then
node.nodes = { node.group_next }
node.group_next = nil
end
local remain_childs = {}
local node_ignored = explorer_node.is_git_ignored(node)
---@type table<string, Node>
local nodes_by_path = utils.key_by(node.nodes, "absolute_path")
while true do
local name, t = vim.loop.fs_scandir_next(handle)
if not name then
break
end
local abs = utils.path_join { cwd, name }
---@type uv.fs_stat.result|nil
local stat = vim.loop.fs_stat(abs)
if not explorer.filters:should_filter(abs, stat, filter_status) then
remain_childs[abs] = true
-- Recreate node if type changes.
if nodes_by_path[abs] then
local n = nodes_by_path[abs]
if n.type ~= t then
utils.array_remove(node.nodes, n)
explorer_node.node_destroy(n)
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
if new_child then
table.insert(node.nodes, new_child)
nodes_by_path[abs] = new_child
end
else
local n = nodes_by_path[abs]
if n then
n.executable = builders.is_executable(abs) or false
n.fs_stat = stat
end
end
end
end
node.nodes = vim.tbl_map(
update_status(nodes_by_path, node_ignored, git_status),
vim.tbl_filter(function(n)
if remain_childs[n.absolute_path] then
return remain_childs[n.absolute_path]
else
explorer_node.node_destroy(n)
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 M.config.group_empty and not is_root and child_folder_only then
node.group_next = child_folder_only
local ns = M.reload(child_folder_only, git_status)
node.nodes = ns or {}
log.profile_end(profile)
return ns
end
explorer.sorters:sort(node.nodes)
explorer.live_filter:apply_filter(node)
log.profile_end(profile)
return node.nodes
end
---Refresh contents and git status for a single node
---@param node Node
---@param callback function
function M.refresh_node(node, callback)
if type(node) ~= "table" then
callback()
end
local parent_node = utils.get_parent_of_group(node)
reload_and_get_git_project(node.absolute_path, function(toplevel, project)
require("nvim-tree.explorer.reload").reload(parent_node, project)
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
function M.refresh_parent_nodes_for_path(path)
local explorer = require("nvim-tree.core").get_explorer()
if not explorer then
return
end
local profile = log.profile_start("refresh_parent_nodes_for_path %s", path)
-- collect parent nodes from the top down
local parent_nodes = {}
NodeIterator.builder({ explorer })
:recursor(function(node)
return node.nodes
end)
:applier(function(node)
local abs_contains = node.absolute_path and path:find(node.absolute_path, 1, true) == 1
local link_contains = node.link_to and path:find(node.link_to, 1, true) == 1
if abs_contains or link_contains then
table.insert(parent_nodes, node)
end
end)
:iterate()
-- refresh in order; this will expand groups as needed
for _, node in ipairs(parent_nodes) do
local toplevel = git.get_toplevel(node.absolute_path)
local project = git.get_project(toplevel) or {}
M.reload(node, project)
update_parent_statuses(node, project, toplevel)
end
log.profile_end(profile)
end
function M.setup(opts)
M.config = opts.renderer
end
return M

View File

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

View File

@ -0,0 +1,599 @@
local events = require "nvim-tree.events"
local utils = require "nvim-tree.utils"
local log = require "nvim-tree.log"
local ExplorerView = {}
local DEFAULT_MIN_WIDTH = 30
local DEFAULT_MAX_WIDTH = -1
local DEFAULT_PADDING = 1
function ExplorerView:new(opts)
local o = {
View = {
adaptive_size = false,
centralize_selection = false,
tabpages = {},
cursors = {},
hide_root_folder = false,
live_filter = {
prev_focused_node = nil,
},
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({
"EndOfBuffer:NvimTreeEndOfBuffer",
"CursorLine:NvimTreeCursorLine",
"CursorLineNr:NvimTreeCursorLineNr",
"LineNr:NvimTreeLineNr",
"WinSeparator:NvimTreeWinSeparator",
"StatusLine:NvimTreeStatusLine",
"StatusLineNC:NvimTreeStatuslineNC",
"SignColumn:NvimTreeSignColumn",
"Normal:NvimTreeNormal",
"NormalNC:NvimTreeNormalNC",
"NormalFloat:NvimTreeNormalFloat",
}, ","),
},
}
}
local options = opts.view or {}
o.View.centralize_selection = options.centralize_selection
o.View.side = (options.side == "right") and "right" or "left"
o.View.height = options.height
o.View.hide_root_folder = opts.renderer.root_folder_label == false
o.View.tab = opts.tab
o.View.preserve_window_proportions = options.preserve_window_proportions
o.View.winopts.cursorline = options.cursorline
o.View.winopts.number = options.number
o.View.winopts.relativenumber = options.relativenumber
o.View.winopts.signcolumn = options.signcolumn
o.View.float = options.float
o.on_attach = opts.on_attach
o.config = vim.deepcopy(options)
setmetatable(o, self)
self.__index = self
o:configure_width(options.width)
o.View.initial_width = o:get_width()
end
-- The initial state of a tab
local tabinitial = {
-- The position of the cursor { line, column }
cursor = { 0, 0 },
-- The NvimTree window number
winnr = nil,
}
local BUFNR_PER_TAB = {}
local BUFFER_OPTIONS = {
swapfile = false,
buftype = "nofile",
modifiable = false,
filetype = "NvimTree",
bufhidden = "wipe",
buflisted = false,
}
---@param bufnr integer
---@return boolean
local function matches_bufnr(bufnr)
for _, b in pairs(BUFNR_PER_TAB) do
if b == bufnr then
return true
end
end
return false
end
local function wipe_rogue_buffer()
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if not matches_bufnr(bufnr) and utils.is_nvim_tree_buf(bufnr) then
pcall(vim.api.nvim_buf_delete, bufnr, { force = true })
end
end
end
---@param bufnr integer|boolean|nil
local function create_buffer(self, bufnr)
wipe_rogue_buffer()
local tab = vim.api.nvim_get_current_tabpage()
BUFNR_PER_TAB[tab] = bufnr or vim.api.nvim_create_buf(false, false)
vim.api.nvim_buf_set_name(self:get_bufnr(), "NvimTree_" .. tab)
for option, value in pairs(BUFFER_OPTIONS) do
vim.bo[self:get_bufnr()][option] = value
end
require("nvim-tree.keymap").on_attach(self:get_bufnr())
events._dispatch_tree_attached_post(self:get_bufnr())
end
---@param size (fun():integer)|integer|string
---@return integer
local function get_size(size)
if type(size) == "number" then
return size
elseif type(size) == "function" then
return size()
end
local size_as_number = tonumber(size:sub(0, -2))
local percent_as_decimal = size_as_number / 100
return math.floor(vim.o.columns * percent_as_decimal)
end
---@param size (fun():integer)|integer|nil
function ExplorerView:get_width(size)
if size then
return get_size(size)
else
return get_size(self.View.width)
end
end
local move_tbl = {
left = "H",
right = "L",
}
-- setup_tabpage sets up the initial state of a tab
---@param tabpage integer
local function setup_tabpage(self, tabpage)
local winnr = vim.api.nvim_get_current_win()
self.View.tabpages[tabpage] = vim.tbl_extend("force", self.View.tabpages[tabpage] or tabinitial, { winnr = winnr })
end
local function set_window_options_and_buffer(self)
pcall(vim.api.nvim_command, "buffer " .. self:get_bufnr())
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore = "all"
for k, v in pairs(self.View.winopts) do
vim.opt_local[k] = v
end
vim.opt.eventignore = eventignore
end
---@return table
local function open_win_config(self)
if type(self.View.float.open_win_config) == "function" then
return self.View.float.open_win_config(self)
else
return self.View.float.open_win_config
end
end
local function open_window(self)
if self.View.float.enable then
vim.api.nvim_open_win(0, true, open_win_config(self))
else
vim.api.nvim_command "vsp"
self:reposition_window()
end
setup_tabpage(self, vim.api.nvim_get_current_tabpage())
set_window_options_and_buffer(self)
end
---@param buf integer
---@return boolean
local function is_buf_displayed(buf)
return vim.api.nvim_buf_is_valid(buf) and vim.fn.buflisted(buf) == 1
end
---@return number|nil
local function get_alt_or_next_buf()
local alt_buf = vim.fn.bufnr "#"
if is_buf_displayed(alt_buf) then
return alt_buf
end
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if is_buf_displayed(buf) then
return buf
end
end
end
local function switch_buf_if_last_buf()
if #vim.api.nvim_list_wins() == 1 then
local buf = get_alt_or_next_buf()
if buf then
vim.cmd("sb" .. buf)
else
vim.cmd "new"
end
end
end
-- save_tab_state saves any state that should be preserved across redraws.
---@param tabnr integer
local function save_tab_state(self, tabnr)
local tabpage = tabnr or vim.api.nvim_get_current_tabpage()
self.View.cursors[tabpage] = vim.api.nvim_win_get_cursor(self:get_winnr(tabpage) or 0)
end
---@param tabpage integer
local function close(self, tabpage)
if not self:is_visible { tabpage = tabpage } then
return
end
save_tab_state(self, tabpage)
switch_buf_if_last_buf()
local tree_win = self:get_winnr(tabpage)
local current_win = vim.api.nvim_get_current_win()
for _, win in pairs(vim.api.nvim_tabpage_list_wins(tabpage)) do
if vim.api.nvim_win_get_config(win).relative == "" then
local prev_win = vim.fn.winnr "#" -- this tab only
if tree_win == current_win and prev_win > 0 then
vim.api.nvim_set_current_win(vim.fn.win_getid(prev_win))
end
if vim.api.nvim_win_is_valid(tree_win or 0) then
vim.api.nvim_win_close(tree_win or 0, true)
end
events._dispatch_on_tree_close()
return
end
end
end
function ExplorerView:close_this_tab_only()
close(self, vim.api.nvim_get_current_tabpage())
end
function ExplorerView:close_all_tabs()
for tabpage, _ in pairs(self.View.tabpages) do
close(self, tabpage)
end
end
function ExplorerView:close()
if self.View.tab.sync.close then
self:close_all_tabs()
else
self:close_this_tab_only()
end
end
---@param options table|nil
function ExplorerView:open(options)
if self:is_visible() then
return
end
local profile = log.profile_start "view open"
create_buffer(self)
open_window(self)
self:resize()
local opts = options or { focus_tree = true }
if not opts.focus_tree then
vim.cmd "wincmd p"
end
events._dispatch_on_tree_open()
log.profile_end(profile)
end
local function grow(self)
local starts_at = self:is_root_folder_visible(require("nvim-tree.core").get_cwd()) and 1 or 0
local lines = vim.api.nvim_buf_get_lines(self:get_bufnr(), starts_at, -1, false)
-- number of columns of right-padding to indicate end of path
local padding = get_size(self.View.padding)
-- account for sign/number columns etc.
local wininfo = vim.fn.getwininfo(self:get_winnr())
if type(wininfo) == "table" and type(wininfo[1]) == "table" then
padding = padding + wininfo[1].textoff
end
local resizing_width = self.View.initial_width - padding
local max_width
-- maybe bound max
if self.View.max_width == -1 then
max_width = -1
else
max_width = self:get_width(self.View.max_width) - padding
end
for _, l in pairs(lines) do
local count = vim.fn.strchars(l)
if resizing_width < count then
resizing_width = count
end
if self.View.adaptive_size and max_width >= 0 and resizing_width >= max_width then
resizing_width = max_width
break
end
end
self:resize(resizing_width + padding)
end
function ExplorerView:grow_from_content()
if self.View.adaptive_size then
grow(self)
end
end
---@param size string|number|nil
function ExplorerView:resize(size)
if self.View.float.enable and not self.View.adaptive_size then
-- if the floating windows's adaptive size is not desired, then the
-- float size should be defined in view.float.open_win_config
return
end
if type(size) == "string" then
size = vim.trim(size)
local first_char = size:sub(1, 1)
size = tonumber(size)
if first_char == "+" or first_char == "-" then
size = self.View.width + size
end
end
if type(size) == "number" and size <= 0 then
return
end
if size then
self.View.width = size
self.View.height = size
end
if not self:is_visible() then
return
end
local new_size = self:get_width()
vim.api.nvim_win_set_width(self:get_winnr() or 0, new_size)
events._dispatch_on_tree_resize(new_size)
if not self.View.preserve_window_proportions then
vim.cmd ":wincmd ="
end
end
function ExplorerView:reposition_window()
local move_to = move_tbl[self.View.side]
vim.api.nvim_command("wincmd " .. move_to)
self:resize()
end
local function set_current_win(self)
local current_tab = vim.api.nvim_get_current_tabpage()
self.View.tabpages[current_tab].winnr = vim.api.nvim_get_current_win()
end
---Open the tree in the a window
---@param opts OpenInWinOpts|nil
function ExplorerView:open_in_win(opts)
opts = opts or { hijack_current_buf = true, resize = true }
if opts.winid and vim.api.nvim_win_is_valid(opts.winid) then
vim.api.nvim_set_current_win(opts.winid)
end
create_buffer(self, opts.hijack_current_buf and vim.api.nvim_get_current_buf())
setup_tabpage(self, vim.api.nvim_get_current_tabpage())
set_current_win(self)
set_window_options_and_buffer(self)
if opts.resize then
self:reposition_window()
self:resize()
end
end
function ExplorerView:abandon_current_window()
local tab = vim.api.nvim_get_current_tabpage()
BUFNR_PER_TAB[tab] = nil
if self.View.tabpages[tab] then
self.View.tabpages[tab].winnr = nil
end
end
function ExplorerView:abandon_all_windows()
for tab, _ in pairs(vim.api.nvim_list_tabpages()) do
BUFNR_PER_TAB[tab] = nil
if self.View.tabpages[tab] then
self.View.tabpages[tab].winnr = nil
end
end
end
---@param opts table|nil
function ExplorerView:is_visible(opts)
if opts and opts.tabpage then
if self.View.tabpages[opts.tabpage] == nil then
return false
end
local winnr = self.View.tabpages[opts.tabpage].winnr
return winnr and vim.api.nvim_win_is_valid(winnr)
end
if opts and opts.any_tabpage then
for _, v in pairs(self.View.tabpages) do
if v.winnr and vim.api.nvim_win_is_valid(v.winnr) then
return true
end
end
return false
end
return self:get_winnr() ~= nil and vim.api.nvim_win_is_valid(self:get_winnr() or 0)
end
---@param opts table|nil
function ExplorerView:set_cursor(opts)
if self:is_visible() then
pcall(vim.api.nvim_win_set_cursor, self:get_winnr(), opts)
end
end
---@param winnr number|nil
---@param open_if_closed boolean|nil
function ExplorerView:focus(winnr, open_if_closed)
local wnr = winnr or self:get_winnr()
if vim.api.nvim_win_get_tabpage(wnr or 0) ~= vim.api.nvim_win_get_tabpage(0) then
self:close()
self:open()
wnr = self:get_winnr()
elseif open_if_closed and not self:is_visible() then
self:open()
end
if wnr then
vim.api.nvim_set_current_win(wnr)
end
end
--- Retrieve the winid of the open tree.
---@param opts ApiTreeWinIdOpts|nil
---@return number|nil winid unlike get_winnr(), this returns nil if the nvim-tree window is not visible
function ExplorerView:winid(opts)
local tabpage = opts and opts.tabpage
if tabpage == 0 then
tabpage = vim.api.nvim_get_current_tabpage()
end
if self:is_visible { tabpage = tabpage } then
return self:get_winnr(tabpage)
else
return nil
end
end
--- Restores the state of a NvimTree window if it was initialized before.
function ExplorerView:restore_tab_state()
local tabpage = vim.api.nvim_get_current_tabpage()
self:set_cursor(self.View.cursors[tabpage])
end
--- Returns the window number for nvim-tree within the tabpage specified
---@param tabpage number|nil (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return number|nil
function ExplorerView:get_winnr(tabpage)
tabpage = tabpage or vim.api.nvim_get_current_tabpage()
local tabinfo = self.View.tabpages[tabpage]
if tabinfo and tabinfo.winnr and vim.api.nvim_win_is_valid(tabinfo.winnr) then
return tabinfo.winnr
end
end
--- Returns the current nvim tree bufnr
---@return number
function ExplorerView:get_bufnr()
return BUFNR_PER_TAB[vim.api.nvim_get_current_tabpage()]
end
---@param bufnr number
---@return boolean
function ExplorerView:is_buf_valid(bufnr)
return bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
end
function ExplorerView:_prevent_buffer_override()
local view_winnr = self:get_winnr()
local view_bufnr = self:get_bufnr()
-- need to schedule to let the new buffer populate the window
-- because this event needs to be run on bufWipeout.
-- Otherwise the curwin/curbuf would match the view buffer and the view window.
vim.schedule(function()
local curwin = vim.api.nvim_get_current_win()
local curwinconfig = vim.api.nvim_win_get_config(curwin)
local curbuf = vim.api.nvim_win_get_buf(curwin)
local bufname = vim.api.nvim_buf_get_name(curbuf)
if not bufname:match "NvimTree" then
for i, tabpage in ipairs(self.View.tabpages) do
if tabpage.winnr == view_winnr then
M.View.tabpages[i] = nil
break
end
end
end
if curwin ~= view_winnr or bufname == "" or curbuf == view_bufnr then
return
end
-- patch to avoid the overriding window to be fixed in size
-- might need a better patch
vim.cmd "setlocal nowinfixwidth"
vim.cmd "setlocal nowinfixheight"
M.open { focus_tree = false }
require("nvim-tree.renderer").draw()
pcall(vim.api.nvim_win_close, curwin, { force = true })
-- to handle opening a file using :e when nvim-tree is on floating mode
-- falling back to the current window instead of creating a new one
if curwinconfig.relative ~= "" then
require("nvim-tree.actions.node.open-file").fn("edit_in_place", bufname)
else
require("nvim-tree.actions.node.open-file").fn("edit", bufname)
end
end)
end
---@param cwd string|nil
---@return boolean
function ExplorerView:is_root_folder_visible(cwd)
return cwd ~= "/" and not self.View.hide_root_folder
end
-- used on ColorScheme event
function ExplorerView:reset_winhl()
local winnr = self:get_winnr()
if winnr and vim.api.nvim_win_is_valid(winnr) then
vim.wo[self:get_winnr()].winhl = self.View.winopts.winhl
end
end
---Check if width determined or calculated on-fly
---@return boolean
function ExplorerView:is_width_determined()
return type(self.View.width) ~= "function"
end
---Configure width-related config
---@param width string|function|number|table|nil
function ExplorerView:configure_width(width)
if type(width) == "table" then
self.View.adaptive_size = true
self.View.width = width.min or DEFAULT_MIN_WIDTH
self.View.max_width = width.max or DEFAULT_MAX_WIDTH
self.View.padding = width.padding or DEFAULT_PADDING
elseif width == nil then
if self.config.width ~= nil then
-- if we had input config - fallback to it
self.configure_width(self.config.width)
else
-- otherwise - restore initial width
self.View.width = self.View.initial_width
end
else
self.View.adaptive_size = false
self.View.width = width
end
end
return ExplorerView

View File

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

View File

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

View File

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

@ -1,23 +1,10 @@
local log = require("nvim-tree.log") local log = require "nvim-tree.log"
local utils = require("nvim-tree.utils") local utils = require "nvim-tree.utils"
local M = { local M = {
use_cygpath = false, use_cygpath = false,
} }
--- Execute system command
---@param cmd string[]
---@return string stdout
---@return integer exit code
local function system(cmd)
if vim.fn.has("nvim-0.10") == 1 then
local obj = vim.system(cmd):wait()
return obj.stdout or "", obj.code
else
return vim.fn.system(cmd), vim.v.shell_error
end
end
--- Retrieve the git toplevel directory --- Retrieve the git toplevel directory
---@param cwd string path ---@param cwd string path
---@return string|nil toplevel absolute path ---@return string|nil toplevel absolute path
@ -29,35 +16,35 @@ function M.get_toplevel(cwd)
local cmd = { "git", "-C", cwd, "rev-parse", "--show-toplevel", "--absolute-git-dir" } local cmd = { "git", "-C", cwd, "rev-parse", "--show-toplevel", "--absolute-git-dir" }
log.line("git", "%s", table.concat(cmd, " ")) log.line("git", "%s", table.concat(cmd, " "))
local out, exitCode = system(cmd) local out = vim.fn.system(cmd)
log.raw("git", out) log.raw("git", out)
log.profile_end(profile) log.profile_end(profile)
if exitCode ~= 0 or not out or #out == 0 or out:match("fatal") then if vim.v.shell_error ~= 0 or not out or #out == 0 or out:match "fatal" then
return nil, nil return nil, nil
end end
local toplevel, git_dir = out:match("([^\n]+)\n+([^\n]+)") local toplevel, git_dir = out:match "([^\n]+)\n+([^\n]+)"
if not toplevel then if not toplevel then
return nil, nil return nil, nil
end end
if not git_dir then if not git_dir then
git_dir = utils.path_join({ toplevel, ".git" }) git_dir = utils.path_join { toplevel, ".git" }
end end
-- git always returns path with forward slashes -- git always returns path with forward slashes
if vim.fn.has("win32") == 1 then if vim.fn.has "win32" == 1 then
-- msys2 git support -- msys2 git support
-- cygpath calls must in array format to avoid shell compatibility issues -- cygpath calls must in array format to avoid shell compatibility issues
if M.use_cygpath then if M.use_cygpath then
toplevel = vim.fn.system({ "cygpath", "-w", toplevel }) toplevel = vim.fn.system { "cygpath", "-w", toplevel }
if vim.v.shell_error ~= 0 then if vim.v.shell_error ~= 0 then
return nil, nil return nil, nil
end end
-- remove trailing newline(\n) character added by vim.fn.system -- remove trailing newline(\n) character added by vim.fn.system
toplevel = toplevel:gsub("\n", "") toplevel = toplevel:gsub("\n", "")
git_dir = vim.fn.system({ "cygpath", "-w", git_dir }) git_dir = vim.fn.system { "cygpath", "-w", git_dir }
if vim.v.shell_error ~= 0 then if vim.v.shell_error ~= 0 then
return nil, nil return nil, nil
end end
@ -71,11 +58,10 @@ function M.get_toplevel(cwd)
return toplevel, git_dir return toplevel, git_dir
end end
---@type table<string, boolean>
local untracked = {} local untracked = {}
---@param cwd string ---@param cwd string
---@return boolean ---@return string|nil
function M.should_show_untracked(cwd) function M.should_show_untracked(cwd)
if untracked[cwd] ~= nil then if untracked[cwd] ~= nil then
return untracked[cwd] return untracked[cwd]
@ -86,7 +72,7 @@ function M.should_show_untracked(cwd)
local cmd = { "git", "-C", cwd, "config", "status.showUntrackedFiles" } local cmd = { "git", "-C", cwd, "config", "status.showUntrackedFiles" }
log.line("git", table.concat(cmd, " ")) log.line("git", table.concat(cmd, " "))
local has_untracked = system(cmd) local has_untracked = vim.fn.system(cmd)
log.raw("git", has_untracked) log.raw("git", has_untracked)
log.profile_end(profile) log.profile_end(profile)
@ -95,8 +81,8 @@ function M.should_show_untracked(cwd)
return untracked[cwd] return untracked[cwd]
end end
---@param t table<string|integer, boolean>? ---@param t table|nil
---@param k string|integer ---@param k string
---@return table ---@return table
local function nil_insert(t, k) local function nil_insert(t, k)
t = t or {} t = t or {}
@ -104,33 +90,31 @@ local function nil_insert(t, k)
return t return t
end end
---@param project_files GitProjectFiles ---@param status table
---@param cwd string|nil ---@param cwd string|nil
---@return GitProjectDirs ---@return table
function M.project_files_to_project_dirs(project_files, cwd) function M.file_status_to_dir_status(status, cwd)
---@type GitProjectDirs local direct = {}
local project_dirs = {} for p, s in pairs(status) do
project_dirs.direct = {}
for p, s in pairs(project_files) do
if s ~= "!!" then if s ~= "!!" then
local modified = vim.fn.fnamemodify(p, ":h") local modified = vim.fn.fnamemodify(p, ":h")
project_dirs.direct[modified] = nil_insert(project_dirs.direct[modified], s) direct[modified] = nil_insert(direct[modified], s)
end end
end end
project_dirs.indirect = {} local indirect = {}
for dirname, statuses in pairs(project_dirs.direct) do for dirname, statuses in pairs(direct) do
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
local modified = dirname local modified = dirname
while modified ~= cwd and modified ~= "/" do while modified ~= cwd and modified ~= "/" do
modified = vim.fn.fnamemodify(modified, ":h") modified = vim.fn.fnamemodify(modified, ":h")
project_dirs.indirect[modified] = nil_insert(project_dirs.indirect[modified], s) indirect[modified] = nil_insert(indirect[modified], s)
end end
end end
end end
for _, d in pairs(project_dirs) do local r = { indirect = indirect, direct = direct }
for _, d in pairs(r) do
for dirname, statuses in pairs(d) do for dirname, statuses in pairs(d) do
local new_statuses = {} local new_statuses = {}
for s, _ in pairs(statuses) do for s, _ in pairs(statuses) do
@ -139,65 +123,12 @@ function M.project_files_to_project_dirs(project_files, cwd)
d[dirname] = new_statuses d[dirname] = new_statuses
end end
end end
return r
return project_dirs
end
---Git file status for an absolute path
---@param parent_ignored boolean
---@param project GitProject?
---@param path string
---@param path_fallback string? alternative file path when no other file status
---@return GitNodeStatus
function M.git_status_file(parent_ignored, project, path, path_fallback)
---@type GitNodeStatus
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project and project.files then
ns = {
file = project.files[path] or project.files[path_fallback]
}
else
ns = {}
end
return ns
end
---Git file and directory status for an absolute path
---@param parent_ignored boolean
---@param project GitProject?
---@param path string
---@param path_fallback string? alternative file path when no other file status
---@return GitNodeStatus?
function M.git_status_dir(parent_ignored, project, path, path_fallback)
---@type GitNodeStatus?
local ns
if parent_ignored then
ns = {
file = "!!"
}
elseif project then
ns = {
file = project.files and (project.files[path] or project.files[path_fallback]),
dir = project.dirs and {
direct = project.dirs.direct and project.dirs.direct[path],
indirect = project.dirs.indirect and project.dirs.indirect[path],
},
}
end
return ns
end end
function M.setup(opts) function M.setup(opts)
if opts.git.cygwin_support then if opts.git.cygwin_support then
M.use_cygpath = vim.fn.executable("cygpath") == 1 M.use_cygpath = vim.fn.executable "cygpath" == 1
end end
end end

View File

@ -1,5 +1,4 @@
local keymap = require("nvim-tree.keymap") local keymap = require "nvim-tree.keymap"
local api = {} -- circular dependency
local PAT_MOUSE = "^<.*Mouse" local PAT_MOUSE = "^<.*Mouse"
local PAT_CTRL = "^<C%-" local PAT_CTRL = "^<C%-"
@ -11,8 +10,6 @@ local WIN_HL = table.concat({
"CursorLine:NvimTreeCursorLine", "CursorLine:NvimTreeCursorLine",
}, ",") }, ",")
local namespace_help_id = vim.api.nvim_create_namespace("NvimTreeHelp")
local M = { local M = {
config = {}, config = {},
@ -29,12 +26,12 @@ local function tidy_lhs(lhs)
lhs = lhs:gsub("^<lt>", "<") lhs = lhs:gsub("^<lt>", "<")
-- shorten ctrls -- shorten ctrls
if lhs:lower():match("^<ctrl%-") then if lhs:lower():match "^<ctrl%-" then
lhs = lhs:lower():gsub("^<ctrl%-", "<C%-") lhs = lhs:lower():gsub("^<ctrl%-", "<C%-")
end end
-- uppercase ctrls -- uppercase ctrls
if lhs:lower():match("^<c%-") then if lhs:lower():match "^<c%-" then
lhs = lhs:upper() lhs = lhs:upper()
end end
@ -55,7 +52,6 @@ end
--- sort vim command lhs roughly as per :help index --- sort vim command lhs roughly as per :help index
---@param a string ---@param a string
---@param b string ---@param b string
---@return boolean
local function sort_lhs(a, b) local function sort_lhs(a, b)
-- mouse first -- mouse first
if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then
@ -83,19 +79,18 @@ local function sort_lhs(a, b)
end end
--- Compute all lines for the buffer --- Compute all lines for the buffer
---@param map table keymap.get_keymap ---@return table strings of text
---@return string[] lines of text ---@return table arrays of arguments 3-6 for nvim_buf_add_highlight()
---@return HighlightRangeArgs[] hl_range_args for lines
---@return number maximum length of text ---@return number maximum length of text
local function compute(map) local function compute()
local head_lhs = "nvim-tree mappings" local head_lhs = "nvim-tree mappings"
local head_rhs1 = "exit: q" local head_rhs1 = "exit: q"
local head_rhs2 = string.format("sort by %s: s", M.config.sort_by == "key" and "description" or "keymap") local head_rhs2 = string.format("sort by %s: s", M.config.sort_by == "key" and "description" or "keymap")
-- formatted lhs and desc from active keymap -- formatted lhs and desc from active keymap
local mappings = vim.tbl_map(function(m) local mappings = vim.tbl_map(function(map)
return { lhs = tidy_lhs(m.lhs), desc = tidy_desc(m.desc) } return { lhs = tidy_lhs(map.lhs), desc = tidy_desc(map.desc) }
end, map) end, keymap.get_keymap())
-- sorter function for mappings -- sorter function for mappings
local sort_fn local sort_fn
@ -132,10 +127,10 @@ local function compute(map)
local width = #lines[1] local width = #lines[1]
-- header highlight, assume one character keys -- header highlight, assume one character keys
local hl_range_args = { local hl = {
{ higroup = "NvimTreeFolderName", start = { 0, 0, }, finish = { 0, #head_lhs, }, }, { "NvimTreeFolderName", 0, 0, #head_lhs },
{ higroup = "NvimTreeFolderName", start = { 0, width - 1, }, finish = { 0, width, }, }, { "NvimTreeFolderName", 0, width - 1, width },
{ higroup = "NvimTreeFolderName", start = { 1, width - 1, }, finish = { 1, width, }, }, { "NvimTreeFolderName", 1, width - 1, width },
} }
-- mappings, left padded 1 -- mappings, left padded 1
@ -147,10 +142,10 @@ local function compute(map)
width = math.max(#line, width) width = math.max(#line, width)
-- highlight lhs -- highlight lhs
table.insert(hl_range_args, { higroup = "NvimTreeFolderName", start = { i + 1, 1, }, finish = { i + 1, #l.lhs + 1, }, }) table.insert(hl, { "NvimTreeFolderName", i + 1, 1, #l.lhs + 1 })
end end
return lines, hl_range_args, width return lines, hl, width
end end
--- close the window and delete the buffer, if they exist --- close the window and delete the buffer, if they exist
@ -170,11 +165,8 @@ local function open()
-- close existing, shouldn't be necessary -- close existing, shouldn't be necessary
close() close()
-- fetch all mappings
local map = keymap.get_keymap()
-- text and highlight -- text and highlight
local lines, hl_range_args, width = compute(map) local lines, hl, width = compute()
-- create the buffer -- create the buffer
M.bufnr = vim.api.nvim_create_buf(false, true) M.bufnr = vim.api.nvim_create_buf(false, true)
@ -182,19 +174,15 @@ local function open()
-- populate it -- populate it
vim.api.nvim_buf_set_lines(M.bufnr, 0, -1, false, lines) vim.api.nvim_buf_set_lines(M.bufnr, 0, -1, false, lines)
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
vim.api.nvim_set_option_value("modifiable", false, { buf = M.bufnr }) vim.api.nvim_set_option_value("modifiable", false, { buf = M.bufnr })
else else
vim.api.nvim_buf_set_option(M.bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated vim.api.nvim_buf_set_option(M.bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated
end end
-- highlight it -- highlight it
for _, args in ipairs(hl_range_args) do for _, h in ipairs(hl) do
if vim.fn.has("nvim-0.11") == 1 and vim.hl and vim.hl.range then vim.api.nvim_buf_add_highlight(M.bufnr, -1, h[1], h[2], h[3], h[4])
vim.hl.range(M.bufnr, namespace_help_id, args.higroup, args.start, args.finish, {})
else
vim.api.nvim_buf_add_highlight(M.bufnr, -1, args.higroup, args.start[1], args.start[2], args.finish[2]) ---@diagnostic disable-line: deprecated
end
end end
-- open a very restricted window -- open a very restricted window
@ -218,21 +206,12 @@ local function open()
open() open()
end end
-- hardcoded local keymaps = {
local help_keymaps = {
q = { fn = close, desc = "nvim-tree: exit help" }, q = { fn = close, desc = "nvim-tree: exit help" },
["<Esc>"] = { fn = close, desc = "nvim-tree: exit help" }, -- hidden
s = { fn = toggle_sort, desc = "nvim-tree: toggle sorting method" }, s = { fn = toggle_sort, desc = "nvim-tree: toggle sorting method" },
} }
-- api help binding closes for k, v in pairs(keymaps) do
for _, m in ipairs(map) do
if m.callback == api.tree.toggle_help then
help_keymaps[m.lhs] = { fn = close, desc = "nvim-tree: exit help" }
end
end
for k, v in pairs(help_keymaps) do
vim.keymap.set("n", k, v.fn, { vim.keymap.set("n", k, v.fn, {
desc = v.desc, desc = v.desc,
buffer = M.bufnr, buffer = M.bufnr,
@ -261,8 +240,6 @@ end
function M.setup(opts) function M.setup(opts)
M.config.cursorline = opts.view.cursorline M.config.cursorline = opts.view.cursorline
M.config.sort_by = opts.help.sort_by M.config.sort_by = opts.help.sort_by
api = require("nvim-tree.api")
end end
return M return M

View File

@ -1,7 +1,7 @@
local M = {} local M = {}
--- Apply mappings to a scratch buffer and return buffer local mappings --- Apply mappings to a scratch buffer and return buffer local mappings
---@param fn fun(bufnr: integer) on_attach or default_on_attach ---@param fn function(bufnr) on_attach or default_on_attach
---@return table as per vim.api.nvim_buf_get_keymap ---@return table as per vim.api.nvim_buf_get_keymap
local function generate_keymap(fn) local function generate_keymap(fn)
-- create an unlisted scratch buffer -- create an unlisted scratch buffer
@ -19,6 +19,84 @@ local function generate_keymap(fn)
return keymap return keymap
end end
-- stylua: ignore start
---@param bufnr integer
function M.default_on_attach(bufnr)
local api = require('nvim-tree.api')
local function opts(desc)
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'))
-- END_DEFAULT_ON_ATTACH
end
-- stylua: ignore end
---@return table ---@return table
function M.get_keymap() function M.get_keymap()
return generate_keymap(M.on_attach) return generate_keymap(M.on_attach)
@ -29,82 +107,6 @@ function M.get_keymap_default()
return generate_keymap(M.default_on_attach) return generate_keymap(M.default_on_attach)
end end
---@param bufnr integer
function M.default_on_attach(bufnr)
local api = require("nvim-tree.api")
local function opts(desc)
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 All"))
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) function M.setup(opts)
if type(opts.on_attach) ~= "function" then if type(opts.on_attach) ~= "function" then
M.on_attach = M.default_on_attach M.on_attach = M.default_on_attach

View File

@ -1,74 +1,27 @@
local notify = require("nvim-tree.notify") local utils = require "nvim-tree.utils"
local notify = require "nvim-tree.notify"
local M = {} local M = {}
--- Create empty sub-tables if not present
---@param tbl table to create empty inside of
---@param path string dot separated string of sub-tables
---@return table deepest sub-table
local function create(tbl, path)
local t = tbl
for s in string.gmatch(path, "([^%.]+)%.*") do
if t[s] == nil then
t[s] = {}
end
t = t[s]
end
return t
end
--- Move a value from src to dst if value is nil on dst.
--- Remove value from src
---@param src table to copy from
---@param src_path string dot separated string of sub-tables
---@param src_pos string value pos
---@param dst table to copy to
---@param dst_path string dot separated string of sub-tables, created when missing
---@param dst_pos string value pos
---@param remove boolean
local function move(src, src_path, src_pos, dst, dst_path, dst_pos, remove)
for pos in string.gmatch(src_path, "([^%.]+)%.*") do
if src[pos] and type(src[pos]) == "table" then
src = src[pos]
else
return
end
end
local src_val = src[src_pos]
if src_val == nil then
return
end
dst = create(dst, dst_path)
if dst[dst_pos] == nil then
dst[dst_pos] = src_val
end
if remove then
src[src_pos] = nil
end
end
-- silently move, please add to help nvim-tree-legacy-opts -- silently move, please add to help nvim-tree-legacy-opts
local function refactored(opts) local function refactored(opts)
-- 2022/06/20 -- 2022/06/20
move(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root", true) utils.move_missing_val(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root", true)
move(opts, "", "update_cwd", opts, "", "sync_root_with_cwd", true) utils.move_missing_val(opts, "", "update_cwd", opts, "", "sync_root_with_cwd", true)
-- 2022/11/07 -- 2022/11/07
move(opts, "", "open_on_tab", opts, "tab.sync", "open", false) utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "open", false)
move(opts, "", "open_on_tab", opts, "tab.sync", "close", true) utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "close", true)
move(opts, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true) utils.move_missing_val(opts, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true)
-- 2022/11/22 -- 2022/11/22
move(opts, "renderer", "root_folder_modifier", opts, "renderer", "root_folder_label", true) utils.move_missing_val(opts, "renderer", "root_folder_modifier", opts, "renderer", "root_folder_label", true)
-- 2023/01/01 -- 2023/01/01
move(opts, "update_focused_file", "debounce_delay", opts, "view", "debounce_delay", true) utils.move_missing_val(opts, "update_focused_file", "debounce_delay", opts, "view", "debounce_delay", true)
-- 2023/01/08 -- 2023/01/08
move(opts, "trash", "require_confirm", opts, "ui.confirm", "trash", true) utils.move_missing_val(opts, "trash", "require_confirm", opts, "ui.confirm", "trash", true)
-- 2023/01/15 -- 2023/01/15
if type(opts.view) == "table" and opts.view.adaptive_size ~= nil then if type(opts.view) == "table" and opts.view.adaptive_size ~= nil then
@ -82,13 +35,13 @@ local function refactored(opts)
end end
-- 2023/07/15 -- 2023/07/15
move(opts, "", "sort_by", opts, "sort", "sorter", true) utils.move_missing_val(opts, "", "sort_by", opts, "sort", "sorter", true)
-- 2023/07/16 -- 2023/07/16
move(opts, "git", "ignore", opts, "filters", "git_ignored", true) utils.move_missing_val(opts, "git", "ignore", opts, "filters", "git_ignored", true)
-- 2023/08/26 -- 2023/08/26
move(opts, "renderer.icons", "webdev_colors", opts, "renderer.icons.web_devicons.file", "color", true) utils.move_missing_val(opts, "renderer.icons", "webdev_colors", opts, "renderer.icons.web_devicons.file", "color", true)
-- 2023/10/08 -- 2023/10/08
if type(opts.renderer) == "table" and type(opts.renderer.highlight_diagnostics) == "boolean" then if type(opts.renderer) == "table" and type(opts.renderer.highlight_diagnostics) == "boolean" then
@ -106,36 +59,28 @@ local function refactored(opts)
opts.update_focused_file.update_root = { enable = opts.update_focused_file.update_root } opts.update_focused_file.update_root = { enable = opts.update_focused_file.update_root }
end end
end end
move(opts, "update_focused_file", "ignore_list", opts, "update_focused_file.update_root", "ignore_list", true) utils.move_missing_val(opts, "update_focused_file", "ignore_list", opts, "update_focused_file.update_root", "ignore_list", true)
-- 2025/04/30
if opts.renderer and opts.renderer.icons and type(opts.renderer.icons.padding) == "string" then
local icons_padding = opts.renderer.icons.padding
opts.renderer.icons.padding = {}
opts.renderer.icons.padding.icon = icons_padding
end
end end
local function deprecated(opts) local function deprecated(opts)
if type(opts.view) == "table" and opts.view.hide_root_folder then if type(opts.view) == "table" and opts.view.hide_root_folder then
notify.info("view.hide_root_folder is deprecated, please set renderer.root_folder_label = false") notify.info "view.hide_root_folder is deprecated, please set renderer.root_folder_label = false"
end end
end end
local function removed(opts) local function removed(opts)
if opts.auto_close then if opts.auto_close then
notify.warn("auto close feature has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Auto-Close") notify.warn "auto close feature has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Auto-Close"
opts.auto_close = nil opts.auto_close = nil
end end
if opts.focus_empty_on_setup then if opts.focus_empty_on_setup then
notify.warn("focus_empty_on_setup has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Open-At-Startup") notify.warn "focus_empty_on_setup has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Open-At-Startup"
opts.focus_empty_on_setup = nil opts.focus_empty_on_setup = nil
end end
if opts.create_in_closed_folder then if opts.create_in_closed_folder then
notify.warn( notify.warn "create_in_closed_folder has been removed and is now the default behaviour. You may use api.fs.create to add a file under your desired node."
"create_in_closed_folder has been removed and is now the default behaviour. You may use api.fs.create to add a file under your desired node.")
end end
opts.create_in_closed_folder = nil opts.create_in_closed_folder = nil
end end

View File

@ -1,6 +1,9 @@
local view = require("nvim-tree.view") local renderer = require "nvim-tree.renderer"
local core = require("nvim-tree.core") local core = require "nvim-tree.core"
local notify = require("nvim-tree.notify") 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 ---@class LibOpenOpts
---@field path string|nil path ---@field path string|nil path
@ -11,6 +14,162 @@ local M = {
target_winid = nil, target_winid = nil,
} }
---@return Node|nil
function M.get_node_at_cursor()
local explorer = core.get_explorer()
if not explorer then
return
end
local winnr = explorer.view:get_winnr()
if not winnr then
return
end
local cursor = vim.api.nvim_win_get_cursor(winnr)
local line = cursor[1]
if line == 1 and explorer.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())[line]
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)
toggle_group = toggle_group or false
if node.has_children then
node.has_children = false
end
if #node.nodes == 0 then
core.get_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
renderer.draw()
end
function M.set_target_win() function M.set_target_win()
local id = vim.api.nvim_get_current_win() local id = vim.api.nvim_get_current_win()
local tree_id = view.get_winnr() local tree_id = view.get_winnr()
@ -30,14 +189,14 @@ local function handle_buf_cwd(cwd)
end end
local function open_view_and_draw() local function open_view_and_draw()
local cwd = vim.fn.getcwd()
view.open()
handle_buf_cwd(cwd)
local explorer = core.get_explorer() local explorer = core.get_explorer()
if explorer then if not explorer then
explorer.renderer:draw() return
end end
local cwd = vim.fn.getcwd()
explorer.view:open()
handle_buf_cwd(cwd)
renderer.draw()
end end
local function should_hijack_current_buf() local function should_hijack_current_buf()
@ -45,7 +204,7 @@ local function should_hijack_current_buf()
local bufname = vim.api.nvim_buf_get_name(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr)
local bufmodified, ft local bufmodified, ft
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has "nvim-0.10" == 1 then
bufmodified = vim.api.nvim_get_option_value("modified", { buf = bufnr }) bufmodified = vim.api.nvim_get_option_value("modified", { buf = bufnr })
ft = vim.api.nvim_get_option_value("ft", { buf = bufnr }) ft = vim.api.nvim_get_option_value("ft", { buf = bufnr })
else else
@ -64,7 +223,7 @@ end
---@param items_short string[] ---@param items_short string[]
---@param items_long string[] ---@param items_long string[]
---@param kind string|nil ---@param kind string|nil
---@param callback fun(item_short: string|nil) ---@param callback fun(item_short: string)
function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback) function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback)
local function format_item(short) local function format_item(short)
for i, s in ipairs(items_short) do for i, s in ipairs(items_short) do
@ -106,29 +265,25 @@ function M.open(opts)
core.init(cwd) core.init(cwd)
end end
end end
local explorer = core.get_explorer() local explorer = core.get_explorer()
if not explorer then
return
end
if should_hijack_current_buf() then if should_hijack_current_buf() then
view.close_this_tab_only() explorer.view:close_this_tab_only()
view.open_in_win() explorer.view:open_in_win()
if explorer then renderer.draw()
explorer.renderer:draw()
end
elseif opts.winid then elseif opts.winid then
view.open_in_win({ hijack_current_buf = false, resize = false, winid = opts.winid }) explorer.view:open_in_win { hijack_current_buf = false, resize = false, winid = opts.winid }
if explorer then renderer.draw()
explorer.renderer:draw()
end
elseif opts.current_window then elseif opts.current_window then
view.open_in_win({ hijack_current_buf = false, resize = false }) explorer.view:open_in_win { hijack_current_buf = false, resize = false }
if explorer then renderer.draw()
explorer.renderer:draw()
end
else else
open_view_and_draw() open_view_and_draw()
end end
view.restore_tab_state() explorer.view:restore_tab_state()
events._dispatch_on_tree_open()
end end
function M.setup(opts) function M.setup(opts)

View File

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

View File

@ -0,0 +1,59 @@
local utils = require "nvim-tree.utils"
local remove_file = require "nvim-tree.actions.fs.remove-file"
local notify = require "nvim-tree.notify"
local lib = require "nvim-tree.lib"
local M = {
config = {},
}
--- Delete nodes; each removal will be optionally notified
---@param nodes Node[]
---@param marks Marks
local function do_delete(marks, nodes)
for _, node in pairs(nodes) do
remove_file.remove(node)
end
marks:clear_marks()
if not M.config.filesystem_watchers.enable then
require("nvim-tree.actions.reloaders").reload_explorer()
end
end
--- Delete marked nodes, optionally prompting
---@param explorer Explorer
function M.bulk_delete(explorer)
if not explorer then
return
end
local marks = explorer.marks
local nodes = marks:get_marks()
if not nodes or #nodes == 0 then
notify.warn "No bookmarksed to delete."
return
end
if M.config.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)
utils.clear_prompt()
if item_short == "y" then
do_delete(marks, nodes)
end
end)
else
do_delete(marks, nodes)
end
end
function M.setup(opts)
M.config.ui = opts.ui
M.config.filesystem_watchers = opts.filesystem_watchers
end
return M

View File

@ -0,0 +1,67 @@
local core = require "nvim-tree.core"
local utils = require "nvim-tree.utils"
local rename_file = require "nvim-tree.actions.fs.rename-file"
local notify = require "nvim-tree.notify"
local lib = require "nvim-tree.lib"
local M = {
config = {},
}
---@param explorer Explorer
function M.bulk_move(explorer)
if not explorer then
return
end
local marks = explorer.marks
if #marks:get_marks() == 0 then
notify.warn "No bookmarks to move."
return
end
local node_at_cursor = lib.get_node_at_cursor()
local default_path = core.get_cwd()
if node_at_cursor and node_at_cursor.type == "directory" 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
end
local input_opts = {
prompt = "Move to: ",
default = default_path,
completion = "dir",
}
vim.ui.input(input_opts, function(location)
utils.clear_prompt()
if not location or location == "" then
return
end
if vim.fn.filewritable(location) ~= 2 then
notify.warn(location .. " is not writable, cannot move.")
return
end
local nodes = marks:get_marks()
for _, node in pairs(nodes) do
local head = vim.fn.fnamemodify(node.absolute_path, ":t")
local to = utils.path_join { location, head }
rename_file.rename(node, to)
end
marks:clear_marks()
if not M.config.filesystem_watchers.enable then
require("nvim-tree.actions.reloaders").reload_explorer()
end
end)
end
function M.setup(opts)
M.config.filesystem_watchers = opts.filesystem_watchers
end
return M

View File

@ -0,0 +1,53 @@
local utils = require "nvim-tree.utils"
local remove_file = require "nvim-tree.actions.fs.trash"
local notify = require "nvim-tree.notify"
local lib = require "nvim-tree.lib"
local M = {
config = {},
}
--- Delete nodes; each removal will be optionally notified
---@param nodes Node[]
local function do_trash(nodes)
for _, node in pairs(nodes) do
remove_file.remove(node)
end
end
---@param explorer Explorer
function M.bulk_trash(explorer)
if not explorer then
return
end
local marks = explorer.marks
local nodes = marks:get_marks()
if not nodes or #nodes == 0 then
notify.warn "No bookmarks to trash."
return
end
if M.config.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)
utils.clear_prompt()
if item_short == "y" then
do_trash(nodes)
marks:clear_marks()
end
end)
else
do_trash(nodes)
marks:clear_marks()
end
end
function M.setup(opts)
M.config.ui = opts.ui
M.config.filesystem_watchers = opts.filesystem_watchers
end
return M

View File

@ -1,79 +1,65 @@
local Iterator = require("nvim-tree.iterators.node-iterator") local renderer = {} -- circular dependency
local core = require("nvim-tree.core")
local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify")
local open_file = require("nvim-tree.actions.node.open-file")
local remove_file = require("nvim-tree.actions.fs.remove-file")
local rename_file = require("nvim-tree.actions.fs.rename-file")
local trash = require("nvim-tree.actions.fs.trash")
local utils = require("nvim-tree.utils")
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 = Class:extend()
---@class Marks ---@class Marks
---@overload fun(args: MarksArgs): Marks ---@field private marks Node[]
local Marks = {}
---@class (exact) MarksArgs ---@return Marks
---@field explorer Explorer function Marks:new()
local o = {}
setmetatable(o, self)
self.__index = self
---@protected o.marks = {}
---@param args MarksArgs
function Marks:new(args)
self.explorer = args.explorer
self.marks = {} return o
end end
---Clear all marks and reload if watchers disabled
---@private ---@private
function Marks:clear_reload()
self:clear()
if not self.explorer.opts.filesystem_watchers.enable then
self.explorer:reload_explorer()
end
end
---Clear all marks and redraw
---@public
function Marks:clear()
self.marks = {}
self.explorer.renderer:draw()
end
---@public
---@param node Node ---@param node Node
function Marks:toggle(node) function Marks:add_mark(node)
self.marks[node.absolute_path] = node
renderer.draw()
end
---@private
---@param node Node
function Marks:remove_mark(node)
self.marks[node.absolute_path] = nil
renderer.draw()
end
---@param node Node
function Marks:toggle_mark(node)
if node.absolute_path == nil then if node.absolute_path == nil then
return return
end end
if self:get(node) then if self:get_mark(node) then
self.marks[node.absolute_path] = nil self:remove_mark(node)
else else
self.marks[node.absolute_path] = node self:add_mark(node)
end end
self.explorer.renderer:draw() renderer.draw()
end
function Marks:clear_marks()
self.marks = {}
renderer.draw()
end end
---Return node if marked
---@public
---@param node Node ---@param node Node
---@return Node|nil ---@return Node|nil
function Marks:get(node) function Marks:get_mark(node)
return node and self.marks[node.absolute_path] return node and self.marks[node.absolute_path]
end end
---List marked nodes
---@public
---@return Node[] ---@return Node[]
function Marks:list() function Marks:get_marks()
local list = {} local list = {}
for _, node in pairs(self.marks) do for _, node in pairs(self.marks) do
table.insert(list, node) table.insert(list, node)
@ -81,191 +67,12 @@ function Marks:list()
return list return list
end end
---Delete marked; each removal will be optionally notified function Marks.setup(opts)
---@public renderer = require "nvim-tree.renderer"
function Marks:bulk_delete()
if not next(self.marks) then
notify.warn("No bookmarks to delete.")
return
end
local function execute() require("nvim-tree.marks.bulk-delete").setup(opts)
for _, node in pairs(self.marks) do require("nvim-tree.marks.bulk-trash").setup(opts)
remove_file.remove(node) require("nvim-tree.marks.bulk-move").setup(opts)
end
self:clear_reload()
end
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)
utils.clear_prompt()
if item_short == "y" then
execute()
end
end)
else
execute()
end
end
---Trash marked; each removal will be optionally notified
---@public
function Marks:bulk_trash()
if not next(self.marks) then
notify.warn("No bookmarks to trash.")
return
end
local function execute()
for _, node in pairs(self.marks) do
trash.remove(node)
end
self:clear_reload()
end
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)
utils.clear_prompt()
if item_short == "y" then
execute()
end
end)
else
execute()
end
end
---Move marked
---@public
function Marks:bulk_move()
if not next(self.marks) then
notify.warn("No bookmarks to move.")
return
end
local node_at_cursor = self.explorer:get_node_at_cursor()
local default_path = core.get_cwd()
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
end
local input_opts = {
prompt = "Move to: ",
default = default_path,
completion = "dir",
}
vim.ui.input(input_opts, function(location)
utils.clear_prompt()
if not location or location == "" then
return
end
if vim.fn.filewritable(location) ~= 2 then
notify.warn(location .. " is not writable, cannot move.")
return
end
for _, node in pairs(self.marks) do
local head = vim.fn.fnamemodify(node.absolute_path, ":t")
local to = utils.path_join({ location, head })
rename_file.rename(node, to)
end
self:clear_reload()
end)
end
---Focus nearest marked node in direction.
---@private
---@param up boolean
function Marks:navigate(up)
local node = self.explorer:get_node_at_cursor()
if not node then
return
end
local first, prev, next, last = nil, nil, nil, nil
local found = false
Iterator.builder(self.explorer.nodes)
:recursor(function(n)
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
found = true
return
end
if not self:get(n) then
return
end
last = n
first = first or n
if found and not next then
next = n
end
if not found then
prev = n
end
end)
:iterate()
if not found then
return
end
if up then
self.explorer:focus_node_or_parent(prev or last)
else
self.explorer:focus_node_or_parent(next or first)
end
end
---@public
function Marks:navigate_prev()
self:navigate(true)
end
---@public
function Marks:navigate_next()
self:navigate(false)
end
---Prompts for selection of a marked node, sorted by absolute paths.
---A folder will be focused, a file will be opened.
---@public
function Marks:navigate_select()
local list = vim.tbl_map(function(n)
return n.absolute_path
end, self:list())
table.sort(list)
vim.ui.select(list, {
prompt = "Select a file to open or a folder to focus",
}, function(choice)
if not choice or choice == "" then
return
end
local node = self.marks[choice]
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
self.explorer:focus_node_or_parent(node)
end
end)
end end
return Marks return Marks

View File

@ -0,0 +1,111 @@
local Iterator = require "nvim-tree.iterators.node-iterator"
local core = require "nvim-tree.core"
local open_file = require "nvim-tree.actions.node.open-file"
local utils = require "nvim-tree.utils"
local lib = require "nvim-tree.lib"
---@param node table
---@param where string
---@return Node|nil
local function get_nearest(node, where)
local explorer = core.get_explorer()
if not explorer then
return
end
local first, prev, next, last = nil, nil, nil, nil
local found = false
Iterator.builder(explorer.nodes)
:recursor(function(n)
return n.open and n.nodes
end)
:applier(function(n)
if n.absolute_path == node.absolute_path then
found = true
return
end
if not explorer.marks:get_mark(n) then
return
end
last = n
first = first or n
if found and not next then
next = n
end
if not found then
prev = n
end
end)
:iterate()
if not found then
return
end
if where == "next" then
return next or first
else
return prev or last
end
end
---@param where string
---@param node table|nil
---@return Node|nil
local function get(where, node)
if node then
return get_nearest(node, where)
end
end
---@param node table|nil
local function open_or_focus(node)
if node and not node.nodes 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)
end
end
---@param where string
---@return function
local function navigate_to(where)
return function()
local node = lib.get_node_at_cursor()
local next = get(where, node)
open_or_focus(next)
end
end
local M = {}
M.next = navigate_to "next"
M.prev = navigate_to "prev"
function M.select()
local explorer = core.get_explorer()
if not explorer then
return
end
local list = vim.tbl_map(function(n)
return n.absolute_path
end, explorer.marks:get_marks())
vim.ui.select(list, {
prompt = "Select a file to open or a folder to focus",
}, function(choice)
if not choice or choice == "" then
return
end
local node = explorer.marks:get_mark { absolute_path = choice }
open_or_focus(node)
end)
end
return M

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

@ -0,0 +1,35 @@
---@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
---@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

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

View File

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

View File

@ -1,48 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,9 +9,9 @@ local title_support
---@return boolean ---@return boolean
function M.supports_title() function M.supports_title()
if title_support == nil then if title_support == nil then
title_support = (package.loaded.notify and (vim.notify == require("notify") or vim.notify == require("notify").notify)) title_support = (package.loaded.notify and (vim.notify == require "notify" or vim.notify == require("notify").notify))
or (package.loaded.noice and (vim.notify == require("noice").notify or vim.notify == require("noice.source.notify").notify)) or (package.loaded.noice and (vim.notify == require("noice").notify or vim.notify == require("noice.source.notify").notify))
or (package.loaded.notifier and require("notifier.config").has_component("nvim")) or (package.loaded.notifier and require("notifier.config").has_component "nvim")
or false or false
end end
@ -21,8 +21,8 @@ end
local modes = { local modes = {
{ name = "trace", level = vim.log.levels.TRACE }, { name = "trace", level = vim.log.levels.TRACE },
{ name = "debug", level = vim.log.levels.DEBUG }, { name = "debug", level = vim.log.levels.DEBUG },
{ name = "info", level = vim.log.levels.INFO }, { name = "info", level = vim.log.levels.INFO },
{ name = "warn", level = vim.log.levels.WARN }, { name = "warn", level = vim.log.levels.WARN },
{ name = "error", level = vim.log.levels.ERROR }, { name = "error", level = vim.log.levels.ERROR },
} }
@ -35,7 +35,7 @@ do
vim.schedule(function() vim.schedule(function()
if not M.supports_title() then if not M.supports_title() then
-- add title to the message, with a newline if the message is multiline -- add title to the message, with a newline if the message is multiline
msg = string.format("[NvimTree]%s%s", (msg:match("\n") and "\n" or " "), msg) msg = string.format("[NvimTree]%s%s", (msg:match "\n" and "\n" or " "), msg)
end end
vim.notify(msg, level, { title = "NvimTree" }) vim.notify(msg, level, { title = "NvimTree" })

View File

@ -1,97 +1,70 @@
local notify = require("nvim-tree.notify") local core = require "nvim-tree.core"
local utils = require("nvim-tree.utils") local notify = require "nvim-tree.notify"
local view = require("nvim-tree.view") local utils = require "nvim-tree.utils"
local Class = require("nvim-tree.classic") local DecoratorBookmarks = require "nvim-tree.renderer.decorator.bookmarks"
local DecoratorCopied = require "nvim-tree.renderer.decorator.copied"
local DecoratorCut = require "nvim-tree.renderer.decorator.cut"
local DecoratorDiagnostics = require "nvim-tree.renderer.decorator.diagnostics"
local DecoratorGit = require "nvim-tree.renderer.decorator.git"
local DecoratorModified = require "nvim-tree.renderer.decorator.modified"
local DecoratorHidden = require "nvim-tree.renderer.decorator.hidden"
local DecoratorOpened = require "nvim-tree.renderer.decorator.opened"
local DirectoryNode = require("nvim-tree.node.directory") local pad = require "nvim-tree.renderer.components.padding"
local icons = require "nvim-tree.renderer.components.icons"
local BookmarkDecorator = require("nvim-tree.renderer.decorator.bookmarks") local M = {
local CopiedDecorator = require("nvim-tree.renderer.decorator.copied") opts = {},
local CutDecorator = require("nvim-tree.renderer.decorator.cut") decorators = {},
local DiagnosticsDecorator = require("nvim-tree.renderer.decorator.diagnostics") picture_map = {
local GitDecorator = require("nvim-tree.renderer.decorator.git") jpg = true,
local HiddenDecorator = require("nvim-tree.renderer.decorator.hidden") jpeg = true,
local ModifiedDecorator = require("nvim-tree.renderer.decorator.modified") png = true,
local OpenDecorator = require("nvim-tree.renderer.decorator.opened") gif = true,
local UserDecorator = require("nvim-tree.renderer.decorator.user") webp = true,
jxl = true,
local pad = require("nvim-tree.renderer.components.padding") },
---@alias HighlightedString nvim_tree.api.HighlightedString
-- Builtin Decorators
---@type table<nvim_tree.api.decorator.Name, Decorator>
local BUILTIN_DECORATORS = {
Git = GitDecorator,
Open = OpenDecorator,
Hidden = HiddenDecorator,
Modified = ModifiedDecorator,
Bookmark = BookmarkDecorator,
Diagnostics = DiagnosticsDecorator,
Copied = CopiedDecorator,
Cut = CutDecorator,
} }
---@class (exact) Builder ---@class HighlightedString
---@field str string
---@field hl string[]
---@class AddHighlightArgs
---@field group string[]
---@field line number
---@field col_start number
---@field col_end number
---@class Builder
---@field lines string[] includes icons etc. ---@field lines string[] includes icons etc.
---@field hl_range_args HighlightRangeArgs[] highlights for lines ---@field hl_args AddHighlightArgs[] line highlights
---@field signs string[] line signs ---@field signs string[] line signs
---@field extmarks table[] extra marks for right icon placement ---@field private root_cwd string absolute path
---@field virtual_lines table[] virtual lines for hidden count display
---@field private explorer Explorer
---@field private index number ---@field private index number
---@field private depth number ---@field private depth number
---@field private combined_groups table<string, boolean> combined group names ---@field private combined_groups table<string, boolean> combined group names
---@field private markers boolean[] indent markers ---@field private markers boolean[] indent markers
---@field private decorators Decorator[] local Builder = {}
---@field private hidden_display fun(node: Node): string|nil
---@field private api_nodes table<number, nvim_tree.api.Node>? optional map of uids to api node for user decorators
local Builder = Class:extend()
---@class Builder ---@return Builder
---@overload fun(args: BuilderArgs): Builder function Builder:new()
local o = {
root_cwd = core.get_cwd(),
index = 0,
depth = 0,
hl_args = {},
combined_groups = {},
lines = {},
markers = {},
signs = {},
extmarks = {},
}
setmetatable(o, self)
self.__index = self
---@class (exact) BuilderArgs return o
---@field explorer Explorer
---@protected
---@param args BuilderArgs
function Builder:new(args)
self.explorer = args.explorer
self.index = 0
self.depth = 0
self.hl_range_args = {}
self.combined_groups = {}
self.lines = {}
self.markers = {}
self.signs = {}
self.extmarks = {}
self.virtual_lines = {}
self.decorators = {}
self.hidden_display = Builder:setup_hidden_display_function(self.explorer.opts)
-- instantiate all the builtin and user decorator instances
local builtin, user
for _, d in ipairs(self.explorer.opts.renderer.decorators) do
---@type Decorator
builtin = BUILTIN_DECORATORS[d]
---@type UserDecorator
user = type(d) == "table" and type(d.as) == "function" and d:as(UserDecorator)
if builtin then
table.insert(self.decorators, builtin({ explorer = self.explorer }))
elseif user then
table.insert(self.decorators, user())
-- clone user nodes once
if not self.api_nodes then
self.api_nodes = {}
self.explorer:clone(self.api_nodes)
end
end
end
end end
---Insert ranged highlight groups into self.highlights ---Insert ranged highlight groups into self.highlights
@ -100,9 +73,28 @@ end
---@param start number ---@param start number
---@param end_ number|nil ---@param end_ number|nil
function Builder:insert_highlight(groups, start, end_) function Builder:insert_highlight(groups, start, end_)
for _, higroup in ipairs(groups) do table.insert(self.hl_args, { groups, self.index, start, end_ or -1 })
table.insert(self.hl_range_args, { higroup = higroup, start = { self.index, start }, finish = { self.index, 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 end
if node.group_next and type(M.opts.renderer.group_empty) == "function" then
local new_name = M.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, M.opts.renderer.add_trailing and "/" or "")
end end
---@private ---@private
@ -125,6 +117,76 @@ function Builder:unwrap_highlighted_strings(highlighted_strings)
return string return string
end end
---@private
---@param node table
---@return HighlightedString icon
---@return HighlightedString name
function Builder:build_folder(node)
local has_children = #node.nodes ~= 0 or node.has_children
local icon, icon_hl = icons.get_folder_icon(node, has_children)
local foldername = self:get_folder_name(node)
if #icon > 0 and icon_hl == nil then
if node.open then
icon_hl = "NvimTreeOpenedFolderIcon"
else
icon_hl = "NvimTreeClosedFolderIcon"
end
end
local foldername_hl = "NvimTreeFolderName"
if node.link_to and M.opts.renderer.symlink_destination then
local arrow = icons.i.symlink_arrow
local link_to = utils.path_relative(node.link_to, self.root_cwd)
foldername = string.format("%s%s%s", foldername, arrow, link_to)
foldername_hl = "NvimTreeSymlinkFolderName"
elseif
vim.tbl_contains(M.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(M.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 M.opts.renderer.symlink_destination then
local link_to = utils.path_relative(node.link_to, self.root_cwd)
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(M.opts.renderer.special_files, node.absolute_path) or vim.tbl_contains(M.opts.renderer.special_files, node.name) then
hl = "NvimTreeSpecialFile"
elseif node.executable then
hl = "NvimTreeExecFile"
elseif M.picture_map[node.extension] then
hl = "NvimTreeImageFile"
end
local icon, hl_group = icons.get_file_icon(node.name, node.extension)
return { str = icon, hl = { hl_group } }, { str = node.name, hl = { hl } }
end
---@private ---@private
---@param indent_markers HighlightedString[] ---@param indent_markers HighlightedString[]
---@param arrows HighlightedString[]|nil ---@param arrows HighlightedString[]|nil
@ -135,12 +197,12 @@ end
function Builder:format_line(indent_markers, arrows, icon, name, node) function Builder:format_line(indent_markers, arrows, icon, name, node)
local added_len = 0 local added_len = 0
local function add_to_end(t1, t2) local function add_to_end(t1, t2)
if not t2 or vim.tbl_isempty(t2) then if not t2 then
return return
end end
for _, v in ipairs(t2) do for _, v in ipairs(t2) do
if added_len > 0 then if added_len > 0 then
table.insert(t1, { str = self.explorer.opts.renderer.icons.padding.icon }) table.insert(t1, { str = M.opts.renderer.icons.padding })
end end
table.insert(t1, v) table.insert(t1, v)
end end
@ -153,25 +215,22 @@ function Builder:format_line(indent_markers, arrows, icon, name, node)
end end
end end
-- use the api node for user decorators
local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]]
local line = { indent_markers, arrows } local line = { indent_markers, arrows }
add_to_end(line, { icon }) add_to_end(line, { icon })
for _, d in ipairs(self.decorators) do for i = #M.decorators, 1, -1 do
add_to_end(line, d:icons_before(not d:is(UserDecorator) and node or api_node)) add_to_end(line, M.decorators[i]:icons_before(node))
end end
add_to_end(line, { name }) add_to_end(line, { name })
for _, d in ipairs(self.decorators) do for i = #M.decorators, 1, -1 do
add_to_end(line, d:icons_after(not d:is(UserDecorator) and node or api_node)) add_to_end(line, M.decorators[i]:icons_after(node))
end end
local rights = {} local rights = {}
for _, d in ipairs(self.decorators) do for i = #M.decorators, 1, -1 do
add_to_end(rights, d:icons_right_align(not d:is(UserDecorator) and node or api_node)) add_to_end(rights, M.decorators[i]:icons_right_align(node))
end end
if #rights > 0 then if #rights > 0 then
self.extmarks[self.index] = rights self.extmarks[self.index] = rights
@ -183,14 +242,10 @@ end
---@private ---@private
---@param node Node ---@param node Node
function Builder:build_signs(node) function Builder:build_signs(node)
-- use the api node for user decorators
local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]]
-- first in priority order -- first in priority order
local d, sign_name local sign_name
for i = #self.decorators, 1, -1 do for _, d in ipairs(M.decorators) do
d = self.decorators[i] sign_name = d:sign_name(node)
sign_name = d:sign_name(not d:is(UserDecorator) and node or api_node)
if sign_name then if sign_name then
self.signs[self.index] = sign_name self.signs[self.index] = sign_name
break break
@ -226,113 +281,86 @@ function Builder:create_combined_group(groups)
return combined_name return combined_name
end end
---Calculate decorated icon and name for a node. ---Calculate highlight group for icon and name. A combined highlight group will be created
---A combined highlight group will be created when there is more than one highlight. ---when there is more than one highlight.
---A highlight group is always calculated and upserted for the case of highlights changing. ---A highlight group is always calculated and upserted for the case of highlights changing.
---@private ---@private
---@param node Node ---@param node Node
---@return HighlightedString icon ---@return string|nil icon_hl_group
---@return HighlightedString name ---@return string|nil name_hl_group
function Builder:icon_name_decorated(node) function Builder:add_highlights(node)
-- use the api node for user decorators -- result
local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]] local icon_hl_group, name_hl_group
-- base case -- calculate all groups
local icon = node:highlighted_icon()
local name = node:highlighted_name()
-- calculate node icon and all decorated highlight groups
local icon_groups = {} local icon_groups = {}
local name_groups = {} local name_groups = {}
local hl_icon, hl_name local d, icon, name
for _, d in ipairs(self.decorators) do for i = #M.decorators, 1, -1 do
-- maybe overridde icon d = M.decorators[i]
icon = d:icon_node((not d:is(UserDecorator) and node or api_node)) or icon icon, name = d:groups_icon_name(node)
table.insert(icon_groups, icon)
hl_icon, hl_name = d:highlight_group_icon_name((not d:is(UserDecorator) and node or api_node)) table.insert(name_groups, name)
table.insert(icon_groups, hl_icon)
table.insert(name_groups, hl_name)
end end
-- add one or many icon groups -- one or many icon groups
if #icon_groups > 1 then if #icon_groups > 1 then
table.insert(icon.hl, self:create_combined_group(icon_groups)) icon_hl_group = self:create_combined_group(icon_groups)
else else
table.insert(icon.hl, icon_groups[1]) icon_hl_group = icon_groups[1]
end end
-- add one or many name groups -- one or many name groups
if #name_groups > 1 then if #name_groups > 1 then
table.insert(name.hl, self:create_combined_group(name_groups)) name_hl_group = self:create_combined_group(name_groups)
else else
table.insert(name.hl, name_groups[1]) name_hl_group = name_groups[1]
end end
return icon, name return icon_hl_group, name_hl_group
end end
---Insert node line into self.lines, calling Builder:build_lines for each directory
---@private ---@private
---@param node Node
---@param idx integer line number starting at 1
---@param num_children integer of node
function Builder:build_line(node, idx, num_children) function Builder:build_line(node, idx, num_children)
-- various components -- various components
local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers) local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers)
local arrows = pad.get_arrows(node) local arrows = pad.get_arrows(node)
-- decorated node icon and name -- main components
local icon, name = self:icon_name_decorated(node) 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
-- highighting
local icon_hl_group, name_hl_group = self:add_highlights(node)
table.insert(icon.hl, icon_hl_group)
table.insert(name.hl, name_hl_group)
local line = self:format_line(indent_markers, arrows, icon, name, node) local line = self:format_line(indent_markers, arrows, icon, name, node)
table.insert(self.lines, self:unwrap_highlighted_strings(line)) table.insert(self.lines, self:unwrap_highlighted_strings(line))
self.index = self.index + 1 self.index = self.index + 1
local dir = node:as(DirectoryNode) node = require("nvim-tree.lib").get_last_group_node(node)
if dir then
dir = dir:last_group_node() if node.open then
if dir.open then self.depth = self.depth + 1
self.depth = self.depth + 1 self:build_lines(node)
self:build_lines(dir) self.depth = self.depth - 1
self.depth = self.depth - 1
end
end end
end end
---Add virtual lines for rendering hidden count information per node
---@private ---@private
function Builder:add_hidden_count_string(node, idx, num_children) function Builder:get_nodes_number(nodes)
if not node.open then local explorer = core.get_explorer()
return if not explorer or not explorer.live_filter.filter then
end
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.explorer.opts.renderer.indent_width
local indent_padding = string.rep(" ", indent_width)
local indent_string = indent_padding .. indent_markers.str
local line_nr = #self.lines - 1
self.virtual_lines[line_nr] = self.virtual_lines[line_nr] or {}
-- NOTE: We are inserting in depth order because of current traversal
-- if we change the traversal, we might need to sort by depth before rendering `self.virtual_lines`
-- to maintain proper ordering of parent and child folder hidden count info.
table.insert(self.virtual_lines[line_nr], {
{ indent_string, indent_markers.hl },
{ string.rep(indent_padding, (node.parent == nil and 0 or 1)) .. hidden_count_string, "NvimTreeHiddenDisplay" },
})
end
end
---Number of visible nodes
---@private
---@param nodes Node[]
---@return integer
function Builder:num_visible(nodes)
if not self.explorer.live_filter.filter then
return #nodes return #nodes
end end
@ -348,9 +376,9 @@ end
---@private ---@private
function Builder:build_lines(node) function Builder:build_lines(node)
if not node then if not node then
node = self.explorer node = core.get_explorer()
end end
local num_children = self:num_visible(node.nodes) local num_children = self:get_nodes_number(node.nodes)
local idx = 1 local idx = 1
for _, n in ipairs(node.nodes) do for _, n in ipairs(node.nodes) do
if not n.hidden then if not n.hidden then
@ -359,7 +387,6 @@ function Builder:build_lines(node)
idx = idx + 1 idx = idx + 1
end end
end end
self:add_hidden_count_string(node)
end end
---@private ---@private
@ -367,54 +394,39 @@ end
---@return string ---@return string
function Builder:format_root_name(root_label) function Builder:format_root_name(root_label)
if type(root_label) == "function" then if type(root_label) == "function" then
local label = root_label(self.explorer.absolute_path) local label = root_label(self.root_cwd)
if type(label) == "string" then if type(label) == "string" then
return label return label
end end
elseif type(root_label) == "string" then elseif type(root_label) == "string" then
return utils.path_remove_trailing(vim.fn.fnamemodify(self.explorer.absolute_path, root_label)) return utils.path_remove_trailing(vim.fn.fnamemodify(self.root_cwd, root_label))
end end
return "???" return "???"
end end
---@private ---@private
function Builder:build_header() function Builder:build_header()
if view.is_root_folder_visible(self.explorer.absolute_path) then local explorer = core.get_explorer()
local root_name = self:format_root_name(self.explorer.opts.renderer.root_folder_label) if not explorer then
return
-- Pad to window width so the highlight spans the whole row. end
local win = view.get_winnr() if explorer.view:is_root_folder_visible(core.get_cwd()) then
local width = 0 local root_name = self:format_root_name(M.opts.renderer.root_folder_label)
if win and vim.api.nvim_win_is_valid(win) then table.insert(self.lines, root_name)
width = vim.api.nvim_win_get_width(win) self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(root_name))
end
-- Use display width for proper padding with Nerd Font / wide glyphs.
local name_display_w = vim.fn.strdisplaywidth(root_name)
local pad = 0
if width and width > name_display_w then
pad = width - name_display_w
end
local padded_root = pad > 0 and (root_name .. string.rep(" ", pad)) or root_name
table.insert(self.lines, padded_root)
-- Highlight the entire padded string (covers the full visible row)
self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(padded_root))
-- Keep original indexing behavior
self.index = 1 self.index = 1
end end
if self.explorer.live_filter.filter then if explorer.live_filter.filter then
local filter_line = string.format("%s/%s/", self.explorer.opts.live_filter.prefix, self.explorer.live_filter.filter) local filter_line = string.format("%s/%s/", M.opts.live_filter.prefix, explorer.live_filter.filter)
table.insert(self.lines, filter_line) table.insert(self.lines, filter_line)
local prefix_length = string.len(self.explorer.opts.live_filter.prefix) local prefix_length = string.len(M.opts.live_filter.prefix)
self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length) self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length)
self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line)) self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line))
self.index = self.index + 1 self.index = self.index + 1
end end
end end
---Sanitize lines for rendering. ---Sanitize lines for rendering.
---Replace newlines with literal \n ---Replace newlines with literal \n
---@private ---@private
@ -433,48 +445,20 @@ function Builder:build()
return self return self
end end
---@private function Builder.setup(opts)
---@param opts table M.opts = opts
---@return fun(node: Node): string|nil
function Builder:setup_hidden_display_function(opts)
local hidden_display = opts.renderer.hidden_display
-- options are already validated, so ´hidden_display´ can ONLY be `string` or `function` if type(hidden_display) == "string" then
if type(hidden_display) == "string" then
if hidden_display == "none" then
return function()
return nil
end
elseif hidden_display == "simple" then
return function(hidden_stats)
return utils.default_format_hidden_count(hidden_stats, true)
end
else -- "all"
return function(hidden_stats)
return utils.default_format_hidden_count(hidden_stats, false)
end
end
else -- "function
return function(hidden_stats)
-- 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,
}, hidden_stats or {})
local ok, result = pcall(hidden_display, hidden_stats) -- priority order
if not ok then M.decorators = {
notify.warn( DecoratorCut:new(opts),
"Problem occurred in the function ``opts.renderer.hidden_display`` see nvim-tree.renderer.hidden_display on :h nvim-tree" DecoratorCopied:new(opts),
) DecoratorDiagnostics:new(opts),
return nil DecoratorBookmarks:new(opts),
end DecoratorModified:new(opts),
return result DecoratorHidden:new(opts),
end DecoratorOpened:new(opts),
end DecoratorGit:new(opts),
}
end end
return Builder return Builder

View File

@ -1,35 +0,0 @@
---@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

Some files were not shown because too many files have changed in this diff Show More