Compare commits

...

103 Commits

Author SHA1 Message Date
Alexander Courtis
ec5d41f182 Merge branch 'master' into 2826-remove-view-globals 2025-07-28 11:30:20 +10:00
alexfinger21
10db6943cb fix(#3077): deleting a directory containing symlinked directory will delete the contents of the linked directory (#3168)
* fix(#3077) deleting a directory containing symlink file will delete all content inside the symlink

* fix(#3077): add diagnostic override TODO

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-07-28 09:42:38 +10:00
Alexander Courtis
5737649ca9 Merge branch 'master' into 2826-remove-view-globals 2025-07-28 08:26:33 +10:00
Tomasz N
543ed3cac2 fix(picker): exclude full_name window id from the choice (#3165)
Problem: `full_name` window from is considered as usable by picker
Solution: exclude its ID (also true for nil values)

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-07-21 13:22:56 +10:00
Alexander Courtis
2a386fe567 ci: luals_version 3.13.9 -> 3.15.0 (#3167) 2025-07-21 13:22:07 +10:00
Alexander Courtis
b0b49552c9 docs: polish example decorator (#3160) 2025-06-21 10:57:48 +10:00
Alexander Courtis
fee19f491d Merge branch 'master' into 2826-remove-view-globals 2025-06-20 13:48:22 +10:00
Alexander Courtis
8eb5e0bfd1 feat(#3157): add view.cursorlineopt (#3158)
fix(#3157): add view.cursorlineopt
2025-06-20 13:46:38 +10:00
Alexander Courtis
0830436cca refactor(#2826): consistency check returns new 2025-06-20 13:11:21 +10:00
Alexander Courtis
51b269dc71 refactor(#2826): move global CURSORS to view member 2025-06-20 12:52:58 +10:00
Alexander Courtis
82cc80ffa4 Revert "refactor(#2826): move global CURSORS to view member"
This reverts commit d84dfad1c3.
2025-06-20 12:47:04 +10:00
Alexander Courtis
d84dfad1c3 refactor(#2826): move global CURSORS to view member 2025-06-20 12:46:30 +10:00
Alexander Courtis
4d6c42356a refactor(#2826): more consistency checking 2025-06-20 12:36:09 +10:00
Alexander Courtis
d72f85f524 refactor(#2826): more consistency checking 2025-06-20 12:33:16 +10:00
Alexander Courtis
e875f15b32 refactor(#2826): consistent naming of tabid 2025-06-20 12:01:24 +10:00
Alexander Courtis
83fdff7c4a refactor(#2826): globals.TABPAGES -> WINID_PER_TAB 2025-06-20 11:18:38 +10:00
Alexander Courtis
09ec00c085 refactor(#2826): get_winid returns new after consistency check 2025-06-20 10:01:12 +10:00
Alexander Courtis
3615c7dffe refactor(#2826): temporarily reuse BUFNR_PER_TAB in view constructor 2025-06-20 09:33:07 +10:00
Alexander Courtis
5fbd6745eb refactor(#2826): remove unused view member height 2025-06-20 09:19:33 +10:00
Alexander Courtis
f1e9d5165c refactor(#2826): remove unused view members centralize_selection and preserve_window_proportions 2025-06-20 09:12:29 +10:00
Alexander Courtis
414e576bc2 refactor(#2826): remove unnecessary view members float, hide_root_folder; use explorer opts 2025-06-20 09:03:19 +10:00
Alexander Courtis
de2ae0b06f refactor(#2826): consistent use of buffer registry, tidy, add todos 2025-06-20 08:32:40 +10:00
Alexander Courtis
d6cd465462 refactor(#2826): winnr->winid consistently 2025-06-19 16:46:27 +10:00
Alexander Courtis
b7e9789850 refactor(#2826): winnr->winid in view/globals, remove redundant get_winid and get_bufnr calls 2025-06-19 16:44:19 +10:00
Alexander Courtis
6e7ce8771b refactor(#2826): fuller error messages 2025-06-19 16:25:32 +10:00
Alexander Courtis
0a06f65bf0 refactor(#2826): move view to instanced window class (#3153)
* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class, WIP

* refactor(#2826): singleton View class

* refactor(#2826): View is an Explorer member

* refactor(#2826): move autocmds to Explorer

* refactor(#2826): API uses Explorer's View

* refactor(#2826): move View into Explorer package

* refactor(#2826): retain necessary view globals

* refactor(#2826): move all winhl to appearance constants

* refactor(#2826): add lifecycle logging to all Explorer members

* refactor(#2826): fix bad cherry-pick

* refactor(#2826): better enumerate_options function

* refactor(#2826): add View.tab_line for debugging

* refactor(#2826): default lifecycle log off

* refactor(#2826): add experimental.multi_instance_debug, split globals out of view, move diagnostics to its own module

* refactor(#2826): instrument View:get_winnr

* refactor(#2826): instrument View:setup_tabpage

* refactor(#2826): instrument View:set_current_win, View:prevent_buffer_override

* refactor(#2826): instrument View:get_bufnr

* refactor(#2826): track member bufnr -> winid with global

* refactor(#2826): tidy experiment names and logs

* vim: nvim-tree: track bufnr via buffer-update channel

* vim: nvim-tree: more logging

* vim: nvim-tree: revert: track bufnr via buffer-update channel

* refactor(#2826): notify error on view winid and bufnr mismatches

* refactor(#2826): notify error on view winid and bufnr mismatches

* refactor(#2826): explorer init logging
2025-06-19 15:45:55 +10:00
Yavorski
d54a1875a9 fix: invalid window id for popup info window (#3147) 2025-06-17 16:59:28 +10:00
Garry Filakhtov
aa087788d7 docs: fix renderer.icons.bookmarks_placement parameter, misspelling (#3150)
Fix minor documentation issues

Add a missing double quotes around the default value for
`nvim-tree.renderer.icons.bookmarks_placement` config value and fix
spelling of `bookmarked`.
2025-06-17 10:23:34 +10:00
phanium
d87b41ca53 fix: window picker ignore hidden window (#3145) 2025-06-15 15:04:47 +10:00
github-actions[bot]
6b5b366596 chore(master): release nvim-tree 1.13.0 (#3120)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-14 17:30:22 +10:00
Rami Elwan
ae595611fb feat(#3132): add api.node.expand and api.node.collapse (#3133)
* feat: allow passing node to collapse all

* refactor: use snake case

* feat: handle api legacy calls and update signature

* refactor: make sure open is a boolean

* doc: collapse_all

* Revert "doc: collapse_all"

This reverts commit d243da3e14.

* add api.node.collapse

* add api.node.expand

* add api.node.expand

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-06-14 17:26:58 +10:00
Lucas Mendes
05d8172ebf fix(#3143): actions.open_file.window_picker.exclude applies when not using window picker (#3144)
* fix(#3143): ensure open.no_window_picker respects window_picker.exclude

* fix(#3143): doc

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-06-14 15:35:07 +10:00
Lorentz Lasson
1c733e8c19 chore: use portable shebangs consistently (#3141)
use portable shebangs consistently
2025-06-02 09:21:07 +10:00
Šimon Mandlík
ebcaccda1c fix(#3134): setting one glyph to "" no longer disables others (#3136)
fix: fixes #3134
2025-05-26 13:32:21 +10:00
Šimon Mandlík
cbc3165e08 fix(#2746): background and right aligned icons in floating windows (#3128)
* fix(#2746): fix cursorcolumn and right aligned icons in floating windows

* feat: remove right aligned icons from full name float, show float over right aligned icons

* refactoring: move `extmarks_length` to utils.lua

* fix: decrease `win_width` instead of increasing `text_width` when computing condition for full name float to show

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-05-24 13:19:19 +10:00
Arthur Roos
bd54d1d33c fix(#3117): windows: change file/dir case (#3135)
fix(#3117): allow changing filename's casing

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-05-24 12:52:25 +10:00
Christoph
25d16aab7d fix: "Invalid buffer id" on closing nvim-tree window (#3129)
fix: invalid buffer issue

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-05-18 04:35:59 +00:00
Ross W
e4cd856ebf fix(#3124): fix icon padding for "right_align" placements, notably for dotfiles (#3125)
fix(#3124): prevent empty icons_right_align response from breaking padding
2025-05-18 12:26:18 +10:00
Alexander Courtis
e7d1b7dadc fix(#3122): remove redundant vim.validate (#3123) 2025-05-09 10:00:28 +10:00
Spencer Chunn
ea5097a1e2 feat(#3113): add renderer.icons.folder_arrow_padding (#3114)
* Update padding.lua

* add folder_arrow_padding

* update help docs

* refactor: renderer.icons.padding

renderer.icons.padding -> renderer.icons.padding.icon
renderer.icons.folder_arrow_padding ->
renderer.icons.padding.folder_arrow

* refactor: renderer.icons.padding

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-05-05 11:51:29 +10:00
Alexander Courtis
582ae48c9e chore: fix incorrect @param (#3115) 2025-04-26 12:55:36 +10:00
github-actions[bot]
be5b788f2d chore(master): release nvim-tree 1.12.0 (#3099)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-04-21 12:04:41 +10:00
Alexander Courtis
64bb47f868 ci: simplify luarocks release tag pattern as it was not firing 2025-04-21 08:59:56 +10:00
Devansh Sharma
c24c0470d9 feat: add TreePreOpen event (#3105)
* feat: Add `TreePreOpen` and `TreePreClose` events

* docs: Update docs for `TreePreOpen` and `TreePreClose` events

* chore: remove `TreePreClose` event and update dispatch of `TreePreOpen`

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-04-21 08:39:48 +10:00
Devansh Sharma
3a63717d3d fix: reliably dispatch exactly one TreeOpen and TreeClose events (#3107)
* fix: correctly handle `TreeOpen` and `TreeClose` event dispatch

* fix: lint issues
2025-04-20 09:49:28 +10:00
Alexander Courtis
5bea2b3752 fix(#3101): when renderer.highlight_opened_files = "none" do not reload on BufUnload and BufReadPost (#3102)
* fix(#3101): fix bad reference to renderer.highlight_opened_files during BufUnload and BufReadPost

* fix(#3101): only redraw renderer.highlight_opened_files during BufUnload and BufReadPost

* fix(#3101): only redraw renderer.highlight_opened_files during BufUnload and BufReadPost

* fix(#3101): only redraw renderer.highlight_opened_files during BufUnload and BufReadPost
2025-04-11 12:48:34 +10:00
Šimon Mandlík
c3c1935942 fix: explicitly set border to "none" in full name float (#3094) 2025-04-04 17:29:38 +11:00
Alexander Courtis
44d9b58f11 chore: use builtin EmmyLuaCodeStyle for style checking (#3084)
* chore: sync EmmyLuaCodeStyle settings between .editorconfig and .luarc.json

* chore: lua-language-server 3.11.0 -> 3.13.9

* chore: fix incorrect definition of vim.loop.fs_lstat

* chore: add codestyle-check option to luals-check.sh

* chore: use luals for style check

* chore: use luals for style check

* Revert "chore: use luals for style check"

This reverts commit e5fde80fab.

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check

* chore: use luals for style check
2025-03-23 12:46:17 +11:00
dependabot[bot]
c09ff35de5 chore(deps): bump leafo/gh-actions-lua from 10 to 11 (#3069)
Bumps [leafo/gh-actions-lua](https://github.com/leafo/gh-actions-lua) from 10 to 11.
- [Release notes](https://github.com/leafo/gh-actions-lua/releases)
- [Commits](https://github.com/leafo/gh-actions-lua/compare/v10...v11)

---
updated-dependencies:
- dependency-name: leafo/gh-actions-lua
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-01 11:07:35 +11:00
github-actions[bot]
6709463b2d chore(master): release nvim-tree 1.11.0 (#3051)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-22 12:31:19 +11:00
Šimon Mandlík
b69914325a fix: window picker: hide fillchars: stl and stlnc (#3066)
fix: stl and stlnc fillchars are hidden in window picker

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-02-22 11:32:52 +11:00
Gabriel Crispino
3281f331f7 feat(#1984): add quit_on_open and focus opts to various api.node.open functions (#3054)
* feat: add quit_on_open opt to api.node.open.edit

* fix: fix missing @param annotation

* feat: add focus opt to api.node.open.edit

* fix: fix focus == false behaviour on api.node.open.tab command

* fix: add optional tabpage integer parameter to view.close

if tabpage is not nil, then the function closes the tabpage in this
specific tabpage

* fix: fix quit_on_open == true behaviour on api.node.open.tab command

* fix: add check to not use new opts for certain edit modes

* fix: add docs for new opts

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-02-22 11:09:49 +11:00
Hendrik Ziegler
80523101f0 fix: arithmetic on nil value error on first git project open (#3064)
* fixed error message when opening new git repo

* defensive nil + type check

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-02-10 08:06:02 +11:00
Alexander Courtis
70825f23db fix(#3059): test for presence of new 0.11 API vim.hl.range (#3060) 2025-02-03 15:42:22 +11:00
Alexander Courtis
d05881f65f docs: tidy readme contributing 2025-01-27 11:33:37 +11:00
Gabriel Crispino
fee1da8897 feat(#3037): add API node.buffer.delete, node.buffer.wipe (#3040)
* feat(mappings): add key map to close file buffer

* feat: implement Api.node.buffer.delete

* feat: implement Api.node.buffer.wipe

* refactor: add util fn for common delete ops on bufs

* fix: minor fixes

* refactor: fix lint issues

* fix: undo unintended ApiTreeToggleOpts change

* fix: change error message level to info

* fix: remove unused opts

* refactor: merge delete-buffer and wipe-buffer into single buffer file

* refactor: make wipe and delete fns take a node instead of a file path

* docs: update help with new API commands

* remove refactored utils.lua

* remove unused static setup

* tweak doc

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-01-25 12:47:34 +11:00
Alexander Courtis
db7403243d chore: resolve deprecated in 0.11 (#3053)
* chore: resolve deprecated in 0.11

* chore: resolve deprecated in 0.11

* chore: resolve deprecated in 0.11

* chore: resolve deprecated in 0.11

* chore: resolve deprecated in 0.11

* chore: resolve deprecated in 0.11
2025-01-24 11:57:18 +11:00
𝐍𝐆𝐏𝐎𝐍𝐆
fca0b67c0b fix(#3045): wipe scratch buffers for full name and show info popups (#3050) 2025-01-18 10:28:06 +11:00
github-actions[bot]
d529a99f88 chore(master): release nvim-tree 1.10.0 (#3021)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-13 15:41:35 +11:00
phanium
39bc630816 fix: hijack directory "BufEnter", "BufNewFile" events are nested (#3044)
Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-01-13 15:39:10 +11:00
Lev Yuvenskiy
aae01853dd fix(#3041): use vim.diagnostic.get for updating diagnostics (#3042)
* fix(#3041): use vim.diagnostic.get for updating diagnostics

* fix(#3041): remove unnecessary @type

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2025-01-13 15:15:48 +11:00
fdgdgerg
68fc4c20f5 feat(api): add node.open.vertical_no_picker, node.open.horizontal_no_picker (#3031)
* test

* add splits with no window pickers

removed the 1 buffer per file limitation

test

test2

* no-picker for splits

* help vertical/horizontal_no_picker

* revert whitespace changes

---------

Co-authored-by: JoeDaBu <joegbu@gmail.com>
Co-authored-by: Alexander Courtis <alex@courtis.org>
2024-12-22 09:35:48 +11:00
Šimon Mandlík
f7b76cd1a7 fix(#3015): dynamic width no longer truncates on right_align icons (#3022) 2024-12-14 08:54:18 +11:00
Alexander Courtis
c3d9b1779f docs: notify users with a fs.inotify.max_user_watches message on EMFILE event (#3028)
* docs: notify users with a fs.inotify.max_user_watches message on EMFILE event

* type safety for newly created vim.v.event type
2024-12-13 10:39:46 +11:00
ShyRobin
db8d7ac1f5 fix(#3018): error when focusing nvim-tree when in terminal mode (#3019)
fix: Can't re-enter normal mode from terminal mode

Co-authored-by: Alexander Courtis <alex@courtis.org>
2024-12-08 12:05:33 +11:00
Jie Liu
6b4be1dc0c fix: view.width functions may return strings (#3020)
* Fix get_size() function when size is a function return string

* update view.width help

---------

Co-authored-by: Alexander Courtis <alex@courtis.org>
2024-12-08 11:45:32 +11:00
github-actions[bot]
375e38673b chore(master): release nvim-tree 1.9.0 (#2999)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-07 16:04:32 +11:00
Alexander Courtis
7a4ff1a516 feat(#2948): add custom decorators, :help nvim-tree-decorators (#2996)
* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator, proof of concept

* feat(#2948): add UserDecorator

* feat(#2948): add UserDecorator

* feat(#2948): add UserDecorator

* feat(#2948): add Decorator node icon override

* feat(#2948): add nvim_tree.api.* node classes

* feat(#2948): extract _meta following nvim pattern

* feat(#2948): extract _meta following nvim pattern

* feat(#2948): add decorator registry and order

* feat(#2948): add decorator registry and order

* feat(#2948): tidy

* feat(#2948): document API

* feat(#2948): document API

* feat(#2948): document API

* feat(#2948): pass api nodes to user decorators

* feat(#2948): document API

* feat(#2948): use renderer.decorators to define order and register

* feat(#2948): tidy decorator args and complete documentation

* feat(#2948): decorator classes specified by prefix rather than suffix

* feat(#2948): improve doc

* feat(#2948): improve doc

* feat(#2948): improve doc

* feat(#2948): additional user decorator safety

* feat(#2948): create nvim_tree.api.decorator.UserDecorator class in API, add :extend

* feat(#2948): improve doc
2024-12-07 16:03:29 +11:00
Alexander Courtis
ca7c4c33ca fix(#3009): nvim < 0.10 apply view options locally (#3010) 2024-11-24 17:00:58 +11:00
Alexander Courtis
1f3ffd6af1 fix(#2954): more efficient LSP updates, increase diagnostics.debounce_delay from 50ms to 500ms (#3007)
* fix(#2954): use LSP diagnostic data deltas from events instead of a full query

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

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

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

* add mixins to classic

* typechecked optargs constructors for nodes

* typechecked optargs constructors for watcher and event

* luacheck

* typechecked optargs constructors for GitRunner

* typechecked optargs constructors for Sorter

* typechecked optargs constructors for decorators, WIP

* typechecked optargs constructors for decorators, WIP

* typechecked optargs constructors for decorators

* remove class

* replace enums with named maps

* Renderer and Builder use classic, tidy opts

* LiveFilter uses classic, tidy opts

* Filter uses classic, tidy opts

* add FilterTypes named map

* move toggles into filters

* Marks uses classic, tidy opts

* Sorter uses classic, tidy opts

* Clipboard uses classic, tidy opts

* use supers for node methods

* HighlightDisplay uses classic

* protected :new

* Watcher tidy

* Revert "use supers for node methods"

This reverts commit 9fc7a866ec.

* Watcher tidy

* format

* format

* Filters private methods

* format

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* Sorter type safety

* tidy Runner

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

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

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

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

move expand_or_collapse to DirectoryNode

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

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

* move lib.get_cursor_position to Explorer

* move lib.get_node_at_cursor to Explorer

* move lib.get_nodes to Explorer

* move place_cursor_on_node to Explorer

* resolve resource leak in purge_all_state

* move many autocommands into Explorer

* post merge tidy

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit be546ff18d41f28466b065c857e1e041659bd2c8.

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit e82db1c44d.

* chore: resolve undefined-field

* chore: class new is now generic

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* Revert "chore: resolve undefined-field"

This reverts commit 0e9b844d22.

* move icon builders into node classes

* move icon builders into node classes

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* chore: resolve undefined-field

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move folder specifics from icons to Directory

* move file specifics from icons to File

* clean up sorters

* chore: resolve undefined-field

* tidy hl icon name

* file devicon uses library to fall back

* file devicon uses library to fall back

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

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

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

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

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

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

* inject lua 5.1 check failure

* Revert "inject lua 5.1 check failure"

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

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

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

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

move expand_or_collapse to DirectoryNode

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

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

* move lib.get_cursor_position to Explorer

* move lib.get_node_at_cursor to Explorer

* move lib.get_nodes to Explorer

* move place_cursor_on_node to Explorer

* resolve resource leak in purge_all_state

* move many autocommands into Explorer

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

* chore: format nvt-min.lua

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: complete lua doc

* chore: enable incomplete-signature-doc

* chore: enable incomplete-signature-doc

* chore: complete lua doc

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

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

* extract DirectoryLinkNode and FileLinkNode, move Node methods to children

* temporarily move DirectoryNode methods into BaseNode for easier reviewing

* move mostly unchanged DirectoryNode methods back to BaseNode

* tidy

* git.git_status_file takes an array

* update git status of links

* luacheck hack

* safer git_status_dir

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

move expand_or_collapse to DirectoryNode

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

* move last_group_node to DirectoryNode

* move add BaseNode:as and more doc

* revert parameter name changes

* revert parameter name changes

* add Class

* move group methods into DN

* tidy group methods

* tidy group methods

* tidy group methods

* tidy group methods

* parent is DirectoryNode

* tidy expand all

* BaseNode -> Node

* move watcher to DirectoryNode

* last_group_node is DirectoryNode only

* simplify create-file

* simplify parent

* simplify collapse-all

* simplify live-filter

* style

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

This reverts commit 45a93d9979.

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

* remove debug messages

* remove unnecessary comments

* add is_windows feature flag when normalizing path

* add is_windows flag for filename change

* Revert "add is_windows flag for filename change"

This reverts commit ada77cb7e9.

---------

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

This reverts commit 50e919426a.

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

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

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

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* node classes and constructors

* node methods

* refactor(#2875): multi instance renderer

* node classes and constructors

* explorer is a directory node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from lib

* use .. name for root node for compatibility

* use node.explorer

* extract node factory, remove unused code

* factories for all nodes, add RootNode

* factories for all nodes, add RootNode

* use factory pattern for decorators

* note regression and commit

* fix dir git status regression

* destroy nodes, not explorer

* add BaseNode:is

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

* extract methods from explorer

* extract methods from explorer

* extract methods from explorer

* use Node everywhere in luadoc

* extract methods from lib

* extract methods from lib

* lint

* remove unused code

* don't call methods on fake root node

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

* remove unused inject_node

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* extract methods from lib

* node factory uses stat only

* temporary DirectoryNode casting until method extraction into child classes

* lua-language-server 3.10.5 -> 3.11.0

* explicitly call Explorer constructor

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

* chore: style: align_continuous_similar_call_args

* chore: style: align_continuous_similar_call_args

* chore: style: align_continuous_similar_call_args

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

View File

@@ -7,6 +7,9 @@ end_of_line = lf
[nvim-tree-lua.txt] [nvim-tree-lua.txt]
max_line_length = 78 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 max_line_length = 140
@@ -18,3 +21,4 @@ continuation_indent = 2
quote_style = double quote_style = double
call_arg_parentheses = always call_arg_parentheses = always
space_before_closure_open_parenthesis = false space_before_closure_open_parenthesis = false
align_continuous_similar_call_args = true

View File

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

View File

@@ -20,45 +20,25 @@ jobs:
strategy: strategy:
matrix: matrix:
lua_version: [ 5.1 ] lua_version: [ 5.1 ]
luacheck_version: [ 1.2.0 ]
steps: steps:
- uses: actions/checkout@v4 - name: checkout
uses: actions/checkout@v4
- uses: leafo/gh-actions-lua@v10 - name: install lua ${{ matrix.lua_version }}
uses: leafo/gh-actions-lua@v11
with: with:
luaVersion: ${{ matrix.lua_version }} luaVersion: ${{ matrix.lua_version }}
- uses: leafo/gh-actions-luarocks@v4 - name: install luarocks
uses: leafo/gh-actions-luarocks@v5
- run: luarocks install luacheck 1.1.1 - name: install luacheck ${{ matrix.luacheck_version }}
run: luarocks install luacheck ${{ matrix.luacheck_version }}
- run: make lint - run: make lint
style:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ matrix.emmy_lua_code_style_version }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
strategy:
matrix:
emmy_lua_code_style_version: [ 1.5.6 ]
steps:
- uses: actions/checkout@v4
- name: install emmy_lua_code_style
run: |
mkdir -p CodeFormat
curl -L "https://github.com/CppCXY/EmmyLuaCodeStyle/releases/download/${{ matrix.emmy_lua_code_style_version }}/linux-x64.tar.gz" | tar zx --directory CodeFormat
- run: echo "CodeFormat/linux-x64/bin" >> "$GITHUB_PATH"
- run: make style
- run: make style-doc
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -69,26 +49,31 @@ jobs:
strategy: strategy:
matrix: matrix:
nvim_version: [ stable, nightly ] nvim_version: [ stable, nightly ]
luals_version: [ 3.10.5 ] luals_version: [ 3.15.0 ]
env:
VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime
steps: steps:
- uses: actions/checkout@v4 - name: checkout
uses: actions/checkout@v4
- uses: rhysd/action-setup-vim@v1 - name: install nvim ${{ matrix.nvim_version }}
uses: rhysd/action-setup-vim@v1
with: with:
neovim: true neovim: true
version: ${{ matrix.nvim_version }} version: ${{ matrix.nvim_version }}
- name: install luals - name: install lua-language-server ${{ matrix.luals_version }}
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: echo "luals/bin" >> "$GITHUB_PATH" - run: make check
- 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,9 +1,11 @@
name: Luarocks Release name: Luarocks Release
on: on:
push: push:
tags: tags:
- 'v[0-9]+.[0-9]+.[0-9]+' - v*
workflow_dispatch: workflow_dispatch:
jobs: jobs:
luarocks-upload: luarocks-upload:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,114 @@
# Changelog # Changelog
## [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) ## [1.7.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.0...nvim-tree-v1.7.1) (2024-09-30)

View File

@@ -2,7 +2,7 @@
Thank you for contributing. Thank you for contributing.
See [Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools. See [wiki: Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools.
# Tools # Tools
@@ -12,9 +12,9 @@ Language server: [luals](https://luals.github.io)
Lint: [luacheck](https://github.com/lunarmodules/luacheck/) Lint: [luacheck](https://github.com/lunarmodules/luacheck/)
Style: [EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle): `CodeCheck` Style Fixing: [EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle): `CodeCheck`
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 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)
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`
@@ -36,14 +36,14 @@ make lint
## style ## style
1. Runs CodeCheck using `.editorconfig` settings 1. Runs lua language server `codestyle-check` only, using `.luarc.json` 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 `CodeCheck` issues via: You can automatically fix style issues using `CodeCheck`:
```sh ```sh
make style-fix make style-fix
@@ -90,6 +90,14 @@ Documentation for options should also be added to `nvim-tree-opts` in `doc/nvim-
When adding or changing API please update :help nvim-tree-api When adding or changing API please update :help nvim-tree-api
# Windows
Please note that nvim-tree team members do not have access to nor expertise with Windows.
You will need to be an active participant during development and raise a PR to resolve any issues that may arise.
Please ensure that windows specific features and fixes are behind the appropriate feature flag, see [wiki: OS Feature Flags](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development#os-feature-flags)
# Pull Request # Pull Request
Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge. Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge.

View File

@@ -13,11 +13,11 @@ check: luals
# subtasks # subtasks
# #
luacheck: luacheck:
luacheck -q lua luacheck --codes --quiet lua --exclude-files "**/_meta/**"
# --diagnosis-as-error does not function for workspace, hence we post-process the output # --diagnosis-as-error does not function for workspace, hence we post-process the output
style-check: style-check:
CodeFormat check --config .editorconfig --diagnosis-as-error --workspace lua @scripts/luals-check.sh codestyle-check
style-doc: style-doc:
scripts/doc-comments.sh scripts/doc-comments.sh

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](#Contributing) are always welcome. 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.
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 [wiki](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) to get started. PRs are always welcome. See [CONTRIBUTING](CONTRIBUTING.md) and [wiki: Development](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,7 +1,4 @@
local lib = require("nvim-tree.lib")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local appearance = require("nvim-tree.appearance")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local actions = require("nvim-tree.actions") local actions = require("nvim-tree.actions")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
@@ -76,7 +73,8 @@ 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 explorer and explorer.view:is_visible({ any_tabpage = true }, "nvim-tree.tab_enter") then
local bufname = vim.api.nvim_buf_get_name(0) local bufname = vim.api.nvim_buf_get_name(0)
local ft local ft
@@ -91,17 +89,15 @@ function M.tab_enter()
return return
end end
end end
view.open({ focus_tree = false }) explorer.view:open({ focus_tree = false })
local explorer = core.get_explorer() explorer.renderer:draw()
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()
local should_proceed = _config.hijack_directories.auto_open or explorer and explorer.view:is_visible(nil, "nvim-tree.open_on_directory")
if not should_proceed then if not should_proceed then
return return
end end
@@ -115,27 +111,6 @@ function M.open_on_directory()
actions.root.change_dir.force_dirchange(bufname, true) actions.root.change_dir.force_dirchange(bufname, true)
end end
function M.place_cursor_on_node()
local ok, search = pcall(vim.fn.searchcount)
if ok and search and search.exact_match == 1 then
return
end
local node = lib.get_node_at_cursor()
if not node or node.name == ".." then
return
end
node = utils.get_parent_of_group(node)
local line = vim.api.nvim_get_current_line()
local cursor = vim.api.nvim_win_get_cursor(0)
local idx = vim.fn.stridx(line, node.name)
if idx >= 0 then
vim.api.nvim_win_set_cursor(0, { cursor[1], idx })
end
end
---@return table ---@return table
function M.get_config() function M.get_config()
return M.config return M.config
@@ -173,104 +148,9 @@ local function setup_autocommands(opts)
vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts)) vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts))
end end
-- reset and draw (highlights) when colorscheme is changed
create_nvim_tree_autocmd("ColorScheme", {
callback = function()
appearance.setup()
view.reset_winhl()
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end,
})
-- prevent new opened file from opening in the same window as nvim-tree
create_nvim_tree_autocmd("BufWipeout", {
pattern = "NvimTree_*",
callback = function()
if not utils.is_nvim_tree_buf(0) then
return
end
if opts.actions.open_file.eject then
view._prevent_buffer_override()
else
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
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end
end,
})
create_nvim_tree_autocmd("BufReadPost", {
callback = function(data)
-- update opened file buffers
local explorer = core.get_explorer()
if not explorer then
return
end
if
(explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
explorer:reload_explorer()
end)
end
end,
})
create_nvim_tree_autocmd("BufUnload", {
callback = function(data)
-- update opened file buffers
local explorer = core.get_explorer()
if not explorer then
return
end
if
(explorer.filters.config.filter_no_buffer or explorer.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == ""
then
utils.debounce("Buf:filter_buffer", opts.view.debounce_delay, function()
explorer:reload_explorer()
end)
end
end,
})
create_nvim_tree_autocmd("User", {
pattern = { "FugitiveChanged", "NeogitStatusRefreshed" },
callback = function()
if not opts.filesystem_watchers.enable and opts.git.enable then
local explorer = core.get_explorer()
if explorer then
explorer:reload_git()
end
end
end,
})
if opts.tab.sync.open then if opts.tab.sync.open then
create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) }) create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
end end
if opts.hijack_cursor then
create_nvim_tree_autocmd("CursorMoved", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
M.place_cursor_on_node()
end
end,
})
end
if opts.sync_root_with_cwd then if opts.sync_root_with_cwd then
create_nvim_tree_autocmd("DirChanged", { create_nvim_tree_autocmd("DirChanged", {
callback = function() callback = function()
@@ -293,29 +173,19 @@ 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 }) create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory, nested = true })
end end
create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
if vim.fn.getcwd() ~= core.get_cwd() or (opts.reload_on_bufenter and not opts.filesystem_watchers.enable) then
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end
end
end,
})
if opts.view.centralize_selection then if opts.view.centralize_selection then
create_nvim_tree_autocmd("BufEnter", { create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*", pattern = "NvimTree_*",
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"
if is_term_mode then
return
end
vim.cmd([[norm! zz]]) vim.cmd([[norm! zz]])
end) end)
end) end)
@@ -325,44 +195,33 @@ local function setup_autocommands(opts)
if opts.diagnostics.enable then if opts.diagnostics.enable then
create_nvim_tree_autocmd("DiagnosticChanged", { create_nvim_tree_autocmd("DiagnosticChanged", {
callback = function() callback = function(ev)
log.line("diagnostics", "DiagnosticChanged") log.line("diagnostics", "DiagnosticChanged")
require("nvim-tree.diagnostics").update() require("nvim-tree.diagnostics").update_lsp(ev)
end, end,
}) })
create_nvim_tree_autocmd("User", { create_nvim_tree_autocmd("User", {
pattern = "CocDiagnosticChange", pattern = "CocDiagnosticChange",
callback = function() callback = function()
log.line("diagnostics", "CocDiagnosticChange") log.line("diagnostics", "CocDiagnosticChange")
require("nvim-tree.diagnostics").update() require("nvim-tree.diagnostics").update_coc()
end, end,
}) })
end end
if opts.view.float.enable and opts.view.float.quit_on_focus_loss then -- Handles event dispatch when tree is closed by `:q`
create_nvim_tree_autocmd("WinLeave", { create_nvim_tree_autocmd("WinClosed", {
pattern = "NvimTree_*", pattern = "*",
callback = function() ---@param ev vim.api.keyset.create_autocmd.callback_args
if utils.is_nvim_tree_buf(0) then callback = function(ev)
view.close() if not vim.api.nvim_buf_is_valid(ev.buf) then
end return
end, end
}) if vim.api.nvim_get_option_value("filetype", { buf = ev.buf }) == "NvimTree" then
end require("nvim-tree.events")._dispatch_on_tree_close()
end
if opts.modified.enable then end,
create_nvim_tree_autocmd({ "BufModifiedSet", "BufWritePost" }, { })
callback = function()
utils.debounce("Buf:modified", opts.view.debounce_delay, function()
require("nvim-tree.buffers").reload_modified()
local explorer = core.get_explorer()
if explorer then
explorer:reload_explorer()
end
end)
end,
})
end
end end
local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
@@ -386,6 +245,7 @@ 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,
@@ -415,6 +275,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" },
hidden_display = "none", hidden_display = "none",
symlink_destination = true, symlink_destination = true,
decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", },
highlight_git = "none", highlight_git = "none",
highlight_diagnostics = "none", highlight_diagnostics = "none",
highlight_opened_files = "none", highlight_opened_files = "none",
@@ -449,7 +310,10 @@ 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,
@@ -517,7 +381,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
enable = false, enable = false,
show_on_dirs = false, show_on_dirs = false,
show_on_open_dirs = true, show_on_open_dirs = true,
debounce_delay = 50, debounce_delay = 500,
severity = { severity = {
min = vim.diagnostic.severity.HINT, min = vim.diagnostic.severity.HINT,
max = vim.diagnostic.severity.ERROR, max = vim.diagnostic.severity.ERROR,
@@ -551,7 +415,12 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
filesystem_watchers = { filesystem_watchers = {
enable = true, enable = true,
debounce_delay = 50, debounce_delay = 50,
ignore_dirs = {}, ignore_dirs = {
"/.ccls-cache",
"/build",
"/node_modules",
"/target",
},
}, },
actions = { actions = {
use_system_clipboard = true, use_system_clipboard = true,
@@ -577,6 +446,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
quit_on_open = false, quit_on_open = false,
eject = true, eject = true,
resize_window = true, resize_window = true,
relative_path = true,
window_picker = { window_picker = {
enable = true, enable = true,
picker = "default", picker = "default",
@@ -616,11 +486,7 @@ local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS
}, },
}, },
experimental = { experimental = {
actions = { multi_instance = false,
open_file = {
relative_path = false,
},
},
}, },
log = { log = {
enable = false, enable = false,
@@ -800,13 +666,16 @@ local function localise_default_opts()
end end
function M.purge_all_state() function M.purge_all_state()
require("nvim-tree.watcher").purge_watchers() local explorer = core.get_explorer()
view.close_all_tabs() if explorer then
view.abandon_all_windows() explorer.view:close_all_tabs()
if core.get_explorer() ~= nil then explorer.view:abandon_all_windows()
require("nvim-tree.git").purge_state() require("nvim-tree.git").purge_state()
explorer:destroy()
core.reset_explorer() core.reset_explorer()
end end
-- purge orphaned that were not destroyed by their nodes
require("nvim-tree.watcher").purge_watchers()
end end
---@param conf table|nil ---@param conf table|nil
@@ -849,18 +718,16 @@ 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.components").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-tree.multi-instance-debug").setup(opts)
require("nvim-web-devicons").setup()
end
setup_autocommands(opts) setup_autocommands(opts)

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
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 core = require("nvim-tree.core") local core = require("nvim-tree.core")
local DirectoryNode = require("nvim-tree.node.directory")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local M = {} local M = {}
@@ -12,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(nil, "finders/find-file.fn1") then
return return
end end
@@ -58,25 +59,33 @@ function M.fn(path)
local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator) local link_match = node.link_to and vim.startswith(path_real, node.link_to .. utils.path_separator)
if abs_match or link_match then if abs_match or link_match then
if not node.group_next then local dir = node:as(DirectoryNode)
node.open = true if dir then
end if not dir.group_next then
if #node.nodes == 0 then dir.open = true
core.get_explorer():expand(node) end
if node.group_next and incremented_line then if #dir.nodes == 0 then
line = line - 1 core.get_explorer():expand(dir)
if dir.group_next and incremented_line then
line = line - 1
end
end end
end end
end end
end) end)
:recursor(function(node) :recursor(function(node)
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) node = node and node:as(DirectoryNode)
if node then
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
else
return nil
end
end) end)
:iterate() :iterate()
if found and view.is_visible() then if found and explorer.view:is_visible(nil, "finders/find-file.fn2") then
explorer.renderer:draw() explorer.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

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

View File

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

View File

@@ -1,20 +1,24 @@
local core = require("nvim-tree.core") 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 view = require("nvim-tree.view")
local lib = require("nvim-tree.lib") local lib = require("nvim-tree.lib")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryLinkNode = require("nvim-tree.node.directory-link")
local DirectoryNode = require("nvim-tree.node.directory")
local M = { local M = {
config = {}, config = {},
} }
---@param windows integer[] ---@param windows integer[]
local function close_windows(windows) local function close_windows(windows)
local explorer = core.get_explorer()
-- 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 and explorer.opts.view.float.enable and #vim.api.nvim_list_wins() < 3 then
return return
end end
@@ -27,16 +31,17 @@ end
---@param absolute_path string ---@param absolute_path string
local function clear_buffer(absolute_path) local function clear_buffer(absolute_path)
local explorer = core.get_explorer()
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
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 and explorer.opts.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 explorer and not explorer.opts.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
@@ -66,9 +71,16 @@ local function remove_dir(cwd)
-- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility -- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility
local stat = vim.loop.fs_stat(new_cwd) local stat = vim.loop.fs_stat(new_cwd)
local type = stat and stat.type or nil -- 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)
if type == "directory" then 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
@@ -89,7 +101,7 @@ end
---@param node Node ---@param node Node
function M.remove(node) function M.remove(node)
local notify_node = notify.render_path(node.absolute_path) local notify_node = notify.render_path(node.absolute_path)
if node.nodes ~= nil and not node.link_to then if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then
local success = remove_dir(node.absolute_path) local success = remove_dir(node.absolute_path)
if not success then if not success then
notify.error("Could not remove " .. notify_node) notify.error("Could not remove " .. notify_node)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -50,13 +50,16 @@ 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
vim.api.nvim_win_close(current_popup.winnr, true) if vim.api.nvim_win_is_valid(current_popup.winnr) then
vim.api.nvim_win_close(current_popup.winnr, true)
end
vim.cmd("augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END") vim.cmd("augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END")
current_popup = nil current_popup = nil

View File

@@ -4,6 +4,7 @@ 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

@@ -2,7 +2,8 @@
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 view = require("nvim-tree.view") local core = require("nvim-tree.core")
local full_name = require("nvim-tree.renderer.components.full-name")
local M = {} local M = {}
@@ -19,9 +20,10 @@ 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 = 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_winid(tabpage, "open-file.usable_win_ids")
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)
@@ -39,21 +41,15 @@ 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 and win_config.focusable and not win_config.external or false return id ~= tree_winid
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
@@ -75,10 +71,19 @@ local function pick_win_id()
end end
local i = 1 local i = 1
local win_opts = {} local win_opts_selectable = {}
local win_opts_unselectable = {}
local win_map = {} local win_map = {}
local laststatus = vim.o.laststatus local laststatus = vim.o.laststatus
vim.o.laststatus = 2 vim.o.laststatus = 2
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)
@@ -89,19 +94,16 @@ local function pick_win_id()
if laststatus == 3 then if laststatus == 3 then
for _, win_id in ipairs(not_selectable) do for _, win_id in ipairs(not_selectable) do
local ok_status, statusline, ok_hl, winhl local ok_status, statusline
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id }) ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id })
ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = win_id })
else else
ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, win_id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts[win_id] = { win_opts_unselectable[win_id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "",
} }
-- Clear statusline for windows not selectable -- Clear statusline for windows not selectable
@@ -126,18 +128,18 @@ local function pick_win_id()
ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated
end end
win_opts[id] = { win_opts_selectable[id] = {
statusline = ok_status and statusline or "", statusline = ok_status and statusline or "",
winhl = ok_hl and winhl or "", winhl = ok_hl and winhl or "",
} }
win_map[char] = id win_map[char] = id
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id }) vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id })
vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id }) vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id })
else else
vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated
vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated
end end
i = i + 1 i = i + 1
@@ -156,7 +158,7 @@ local function pick_win_id()
-- Restore window options -- Restore window options
for _, id in ipairs(selectable) do for _, id in ipairs(selectable) do
for opt, value in pairs(win_opts[id]) do for opt, value in pairs(win_opts_selectable[id]) do
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_set_option_value(opt, value, { win = id })
else else
@@ -169,7 +171,7 @@ local function pick_win_id()
for _, id in ipairs(not_selectable) do for _, id in ipairs(not_selectable) do
-- Ensure window still exists at this point -- Ensure window still exists at this point
if vim.api.nvim_win_is_valid(id) then if vim.api.nvim_win_is_valid(id) then
for opt, value in pairs(win_opts[id]) do for opt, value in pairs(win_opts_unselectable[id]) do
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value(opt, value, { win = id }) vim.api.nvim_set_option_value(opt, value, { win = id })
else else
@@ -181,6 +183,7 @@ local function pick_win_id()
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
@@ -191,7 +194,10 @@ 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 = core.get_explorer()
if explorer then
explorer.view:close(nil, "open-file.open_file_in_tab")
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())
@@ -201,7 +207,10 @@ 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 = core.get_explorer()
if explorer then
explorer.view:close(nil, "open-file.drop")
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())
@@ -211,7 +220,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 = core.get_explorer()
if explorer then
explorer.view:close(nil, "open-file.tab_drop")
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())
@@ -232,17 +244,24 @@ local function on_preview(buf_loaded)
once = true, once = true,
}) })
end end
view.focus() local explorer = 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 mode == "edit_no_picker" or mode == "preview_no_picker" then if not M.window_picker.enable or string.find(mode, "no_picker") then
target_winid = lib.target_winid target_winid = lib.target_winid
local usable_wins = usable_win_ids()
-- first available window -- first available usable window
if not vim.tbl_contains(vim.api.nvim_tabpage_list_wins(0), target_winid) then if not vim.tbl_contains(usable_wins, target_winid) then
target_winid = first_win_id() if #usable_wins > 0 then
target_winid = usable_wins[1]
else
target_winid = -1
end
end end
else else
-- pick a window -- pick a window
@@ -273,6 +292,8 @@ local function set_current_win_no_autocmd(winid, autocmd)
end end
local function open_in_new_window(filename, mode) local function open_in_new_window(filename, mode)
local explorer = core.get_explorer()
if type(mode) ~= "string" then if type(mode) ~= "string" then
mode = "" mode = ""
end end
@@ -282,6 +303,11 @@ 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)
@@ -290,7 +316,11 @@ 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 new_window_side = "belowright"
if explorer and (explorer.view.side == "right") then
new_window_side = "aboveleft"
end
-- 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
@@ -322,7 +352,7 @@ local function open_in_new_window(filename, mode)
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.opts.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
@@ -333,9 +363,9 @@ local function open_in_new_window(filename, mode)
local fname local fname
if M.relative_path then if M.relative_path then
fname = vim.fn.fnameescape(utils.path_relative(filename, vim.fn.getcwd())) fname = utils.escape_special_chars(vim.fn.fnameescape(utils.path_relative(filename, vim.fn.getcwd())))
else else
fname = vim.fn.fnameescape(filename) fname = utils.escape_special_chars(vim.fn.fnameescape(filename))
end end
local command local command
@@ -362,7 +392,12 @@ 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 = 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
@@ -371,29 +406,31 @@ end
---@param mode string ---@param mode string
---@param filename string ---@param filename string
---@return nil
function M.fn(mode, filename) function M.fn(mode, filename)
local fname = utils.escape_special_chars(filename) local explorer = core.get_explorer()
if type(mode) ~= "string" then if type(mode) ~= "string" then
mode = "" mode = ""
end end
if mode == "tabnew" then if mode == "tabnew" then
return open_file_in_tab(fname) return open_file_in_tab(filename)
end end
if mode == "drop" then if mode == "drop" then
return drop(fname) return drop(filename)
end end
if mode == "tab_drop" then if mode == "tab_drop" then
return tab_drop(fname) return tab_drop(filename)
end end
if mode == "edit_in_place" then if mode == "edit_in_place" then
return edit_in_current_buf(fname) return edit_in_current_buf(filename)
end end
local buf_loaded = is_already_loaded(fname) local buf_loaded = is_already_loaded(filename)
local found_win = utils.get_win_buf_from_path(filename) local found_win = utils.get_win_buf_from_path(filename)
if found_win and (mode == "preview" or mode == "preview_no_picker") then if found_win and (mode == "preview" or mode == "preview_no_picker") then
@@ -401,29 +438,29 @@ function M.fn(mode, filename)
end end
if not found_win then if not found_win then
open_in_new_window(fname, mode) open_in_new_window(filename, mode)
else else
vim.api.nvim_set_current_win(found_win) vim.api.nvim_set_current_win(found_win)
vim.bo.bufhidden = "" vim.bo.bufhidden = ""
end end
if M.resize_window then if M.resize_window and explorer then
view.resize() explorer.view:resize()
end end
if mode == "preview" or mode == "preview_no_picker" then if mode == "preview" or mode == "preview_no_picker" then
return on_preview(buf_loaded) return on_preview(buf_loaded)
end end
if M.quit_on_open then if M.quit_on_open and explorer then
view.close() explorer.view:close(nil, "open-file.fn")
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.experimental.actions.open_file.relative_path M.relative_path = opts.actions.open_file.relative_path
if opts.actions.open_file.window_picker.chars then if opts.actions.open_file.window_picker.chars then
opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper() opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper()
end end

View File

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

View File

@@ -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 local is_event_scope_window = vim.v.event.scope == "window" or vim.v.event.changed_window or false
return is_event_scope_window and new_tabpage == M.current_tab return is_event_scope_window and new_tabpage == M.current_tab
end end
@@ -85,7 +85,7 @@ M.force_dirchange = add_profiling_to(function(foldername, should_open_view)
if should_change_dir() then if should_change_dir() then
cd(M.options.global, foldername) cd(M.options.global, foldername)
end end
core.init(foldername) core.init(foldername, "change-dir")
end end
if should_open_view then if should_open_view then

View File

@@ -1,6 +1,5 @@
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,11 +40,12 @@ 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(nil, "tree/find-file.fn") 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

View File

@@ -1,52 +0,0 @@
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()
explorer.renderer:draw()
utils.focus_node_or_parent(node)
end
return M

View File

@@ -0,0 +1,81 @@
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()
utils.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

@@ -1,7 +1,9 @@
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local lib = require("nvim-tree.lib")
local FileNode = require("nvim-tree.node.file")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
@@ -16,9 +18,9 @@ local function to_lookup_table(list)
return table return table
end end
---@param node Node ---@param node DirectoryNode
local function expand(node) local function expand(node)
node = lib.get_last_group_node(node) node = node:last_group_node()
node.open = true node.open = true
if #node.nodes == 0 then if #node.nodes == 0 then
core.get_explorer():expand(node) core.get_explorer():expand(node)
@@ -29,9 +31,13 @@ end
---@param node Node ---@param node Node
---@return boolean ---@return boolean
local function should_expand(expansion_count, node) local function should_expand(expansion_count, node)
local dir = node:as(DirectoryNode)
if not dir then
return false
end
local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY
local should_exclude = M.EXCLUDE[node.name] local should_exclude = M.EXCLUDE[dir.name]
return not should_halt and node.nodes and not node.open and not should_exclude return not should_halt and not dir.open and not should_exclude
end end
local function gen_iterator() local function gen_iterator()
@@ -48,7 +54,10 @@ local function gen_iterator()
:applier(function(node) :applier(function(node)
if should_expand(expansion_count, node) then if should_expand(expansion_count, node) then
expansion_count = expansion_count + 1 expansion_count = expansion_count + 1
expand(node) node = node:as(DirectoryNode)
if node then
expand(node)
end
end end
end) end)
:recursor(function(node) :recursor(function(node)
@@ -62,18 +71,38 @@ local function gen_iterator()
end end
end end
---@param base_node table ---@param node Node?
function M.fn(base_node) local function expand_node(node)
local explorer = core.get_explorer() if not node then
local node = base_node.nodes and base_node or explorer return
end
if gen_iterator()(node) then if gen_iterator()(node) then
notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders") notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders")
end end
local explorer = core.get_explorer()
if explorer then if explorer then
explorer.renderer:draw() explorer.renderer:draw()
end end
end end
---Expand the directory node or the root
---@param node Node
function M.all(node)
expand_node(node and node:as(DirectoryNode) or core.get_explorer())
end
---Expand the directory node or parent node
---@param node Node
function M.node(node)
if not node then
return
end
expand_node(node:is(FileNode) and node.parent or node:as(DirectoryNode))
end
function M.setup(opts) function M.setup(opts)
M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery
M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude) M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 = {}
@@ -23,10 +23,12 @@ function M.fn(opts)
opts.path = nil opts.path = nil
end end
if view.is_visible() then local explorer = core.get_explorer()
if explorer and explorer.view:is_visible(nil, "open.fn") then
-- focus -- focus
lib.set_target_win() lib.set_target_win()
view.focus() explorer.view:focus()
else else
-- open -- open
lib.open({ lib.open({

View File

@@ -1,14 +1,19 @@
local view = require("nvim-tree.view") local core = require("nvim-tree.core")
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 = 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 +21,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 +44,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,5 +1,5 @@
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 = {}
@@ -10,6 +10,8 @@ local M = {}
---@param cwd boolean|nil legacy -> opts.path ---@param cwd boolean|nil legacy -> opts.path
---@param bang boolean|nil legacy -> opts.update_root ---@param bang boolean|nil legacy -> opts.update_root
function M.fn(opts, no_focus, cwd, bang) function M.fn(opts, no_focus, cwd, bang)
local explorer = core.get_explorer()
-- legacy arguments -- legacy arguments
if type(opts) == "boolean" then if type(opts) == "boolean" then
opts = { opts = {
@@ -40,9 +42,9 @@ function M.fn(opts, no_focus, cwd, bang)
opts.path = nil opts.path = nil
end end
if view.is_visible() then if explorer and explorer.view:is_visible(nil, "toggle.fn") then
-- close -- close
view.close() explorer.view:close(nil, "toggle.fn")
else else
-- open -- open
lib.open({ lib.open({

View File

@@ -1,14 +1,17 @@
local lib = require("nvim-tree.lib")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local actions = require("nvim-tree.actions") local actions = require("nvim-tree.actions")
local appearance_diagnostics = require("nvim-tree.appearance.diagnostics") local appearance_hi_test = require("nvim-tree.appearance.hi-test")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local help = require("nvim-tree.help") local help = require("nvim-tree.help")
local keymap = require("nvim-tree.keymap") local keymap = require("nvim-tree.keymap")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local DirectoryNode = require("nvim-tree.node.directory")
local FileLinkNode = require("nvim-tree.node.file-link")
local RootNode = require("nvim-tree.node.root")
local UserDecorator = require("nvim-tree.renderer.decorator.user")
local Api = { local Api = {
tree = {}, tree = {},
node = { node = {
@@ -20,6 +23,7 @@ local Api = {
}, },
run = {}, run = {},
open = {}, open = {},
buffer = {},
}, },
events = {}, events = {},
marks = { marks = {
@@ -36,46 +40,26 @@ local Api = {
}, },
commands = {}, commands = {},
diagnostics = {}, diagnostics = {},
decorator = {},
} }
--- Print error when setup not called. ---Print error when setup not called.
--- f function to invoke ---@param fn fun(...): any
---@param f function ---@return fun(...): any
---@return fun(...) : any local function wrap(fn)
local function wrap(f)
return function(...) return function(...)
if vim.g.NvimTreeSetup == 1 then if vim.g.NvimTreeSetup == 1 then
return f(...) return fn(...)
else else
notify.error("nvim-tree setup not called") notify.error("nvim-tree setup not called")
end end
end end
end end
---Inject the node as the first argument if present otherwise do nothing.
---@param fn function function to invoke
local function wrap_node(fn)
return function(node, ...)
node = node or lib.get_node_at_cursor()
if node then
return fn(node, ...)
end
end
end
---Inject the node or nil as the first argument if absent.
---@param fn function function to invoke
local function wrap_node_or_nil(fn)
return function(node, ...)
node = node or lib.get_node_at_cursor()
return fn(node, ...)
end
end
---Invoke a method on the singleton explorer. ---Invoke a method on the singleton explorer.
---Print error when setup not called. ---Print error when setup not called.
---@param explorer_method string explorer method name ---@param explorer_method string explorer method name
---@return fun(...) : any ---@return fun(...): any
local function wrap_explorer(explorer_method) local function wrap_explorer(explorer_method)
return wrap(function(...) return wrap(function(...)
local explorer = core.get_explorer() local explorer = core.get_explorer()
@@ -85,11 +69,49 @@ local function wrap_explorer(explorer_method)
end) end)
end end
---Inject the node as the first argument if present otherwise do nothing.
---@param fn fun(node: Node, ...): any
---@return fun(node: Node?, ...): any
local function wrap_node(fn)
return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")()
if node then
return fn(node, ...)
end
end
end
---Inject the node or nil as the first argument if absent.
---@param fn fun(node: Node?, ...): any
---@return fun(node: Node?, ...): any
local function wrap_node_or_nil(fn)
return function(node, ...)
node = node or wrap_explorer("get_node_at_cursor")()
return fn(node, ...)
end
end
---Invoke a member's method on the singleton explorer. ---Invoke a member's method on the singleton explorer.
---Print error when setup not called. ---Print error when setup not called.
---@param explorer_member string explorer member name ---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member ---@param member_method string method name to invoke on member
---@return fun(...) : any ---@param ... any passed to method
---@return fun(...): any
local function wrap_explorer_member_args(explorer_member, member_method, ...)
local method_args = ...
return wrap(function(...)
local explorer = core.get_explorer()
if explorer then
return explorer[explorer_member][member_method](explorer[explorer_member], method_args, ...)
end
end)
end
---Invoke a member's method on the singleton explorer.
---Print error when setup not called.
---@param explorer_member string explorer member name
---@param member_method string method name to invoke on member
---@return fun(...): any
local function wrap_explorer_member(explorer_member, member_method) local function wrap_explorer_member(explorer_member, member_method)
return wrap(function(...) return wrap(function(...)
local explorer = core.get_explorer() local explorer = core.get_explorer()
@@ -118,9 +140,9 @@ 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_explorer("reload_explorer")
---@class ApiTreeResizeOpts ---@class ApiTreeResizeOpts
@@ -135,16 +157,19 @@ Api.tree.change_root = wrap(function(...)
end) end)
Api.tree.change_root_to_node = wrap_node(function(node) Api.tree.change_root_to_node = wrap_node(function(node)
if node.name == ".." then if node.name == ".." or node:is(RootNode) then
actions.root.change_dir.fn("..") actions.root.change_dir.fn("..")
elseif node.nodes ~= nil then else
actions.root.change_dir.fn(lib.get_last_group_node(node).absolute_path) node = node:as(DirectoryNode)
if node then
actions.root.change_dir.fn(node:last_group_node().absolute_path)
end
end end
end) end)
Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn) Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn)
Api.tree.get_node_under_cursor = wrap(lib.get_node_at_cursor) Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor")
Api.tree.get_nodes = wrap(lib.get_nodes) Api.tree.get_nodes = wrap_explorer("get_nodes")
---@class ApiTreeFindFileOpts ---@class ApiTreeFindFileOpts
---@field buf string|number|nil ---@field buf string|number|nil
@@ -156,15 +181,19 @@ Api.tree.get_nodes = wrap(lib.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)
Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand_all.fn) ---@class ApiCollapseOpts
Api.tree.toggle_enable_filters = wrap(actions.tree.modifiers.toggles.enable) ---@field keep_buffers boolean|nil default false
Api.tree.toggle_gitignore_filter = wrap(actions.tree.modifiers.toggles.git_ignored)
Api.tree.toggle_git_clean_filter = wrap(actions.tree.modifiers.toggles.git_clean) Api.tree.collapse_all = wrap(actions.tree.modifiers.collapse.all)
Api.tree.toggle_no_buffer_filter = wrap(actions.tree.modifiers.toggles.no_buffer) Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand.all)
Api.tree.toggle_custom_filter = wrap(actions.tree.modifiers.toggles.custom) Api.tree.toggle_enable_filters = wrap_explorer_member("filters", "toggle")
Api.tree.toggle_hidden_filter = wrap(actions.tree.modifiers.toggles.dotfiles) Api.tree.toggle_gitignore_filter = wrap_explorer_member_args("filters", "toggle", "git_ignored")
Api.tree.toggle_no_bookmark_filter = wrap(actions.tree.modifiers.toggles.no_bookmark) 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)
@@ -172,12 +201,12 @@ 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", "api_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)
@@ -196,27 +225,61 @@ Api.fs.copy.absolute_path = wrap_node(wrap_explorer_member("clipboard", "copy_ab
Api.fs.copy.filename = wrap_node(wrap_explorer_member("clipboard", "copy_filename")) Api.fs.copy.filename = wrap_node(wrap_explorer_member("clipboard", "copy_filename"))
Api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basename")) Api.fs.copy.basename = wrap_node(wrap_explorer_member("clipboard", "copy_basename"))
Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path")) Api.fs.copy.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path"))
---
---@class NodeEditOpts
---@field quit_on_open boolean|nil default false
---@field focus boolean|nil default true
---@param mode string ---@param mode string
---@param node table ---@param node Node
local function edit(mode, node) ---@param edit_opts NodeEditOpts?
local path = node.absolute_path local function edit(mode, node, edit_opts)
if node.link_to and not node.nodes then local file_link = node:as(FileLinkNode)
path = node.link_to local path = file_link and file_link.link_to or node.absolute_path
end local cur_tabpage = vim.api.nvim_get_current_tabpage()
local explorer = core.get_explorer()
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
if explorer then
explorer.view:close(cur_tabpage, "api.edit " .. mode)
end
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
if explorer then
explorer.view:focus()
end
end
end end
---@param mode string ---@param mode string
---@return fun(node: table) ---@param toggle_group boolean?
---@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)
return function(node) ---@param node Node
if node.name == ".." then ---@param edit_opts NodeEditOpts?
return function(node, edit_opts)
local root = node:as(RootNode)
local dir = node:as(DirectoryNode)
if root or node.name == ".." then
actions.root.change_dir.fn("..") actions.root.change_dir.fn("..")
elseif node.nodes then elseif dir then
lib.expand_or_collapse(node, toggle_group) dir:expand_or_collapse(toggle_group)
elseif not toggle_group then elseif not toggle_group then
edit(mode, node) edit(mode, node, edit_opts)
end end
end end
end end
@@ -227,7 +290,9 @@ 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.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.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"))
@@ -256,6 +321,19 @@ Api.node.navigate.diagnostics.prev_recursive = wrap_node(actions.moves.item.fn({
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.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.git.reload = wrap_explorer("reload_git")
Api.events.subscribe = events.subscribe Api.events.subscribe = events.subscribe
@@ -279,10 +357,15 @@ Api.config.mappings.get_keymap = wrap(keymap.get_keymap)
Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default) Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default)
Api.config.mappings.default_on_attach = keymap.default_on_attach Api.config.mappings.default_on_attach = keymap.default_on_attach
Api.diagnostics.hi_test = wrap(appearance_diagnostics.hi_test) Api.diagnostics.hi_test = wrap(appearance_hi_test)
Api.commands.get = wrap(function() Api.commands.get = wrap(function()
return require("nvim-tree.commands").get() return require("nvim-tree.commands").get()
end) end)
---Create a decorator class by calling :extend()
---See :help nvim-tree-decorators
---@type nvim_tree.api.decorator.UserDecorator
Api.decorator.UserDecorator = UserDecorator --[[@as nvim_tree.api.decorator.UserDecorator]]
return Api return Api

View File

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

View File

@@ -134,6 +134,29 @@ M.HIGHLIGHT_GROUPS = {
{ group = "NvimTreeDiagnosticHintFolderHL", link = "NvimTreeDiagnosticHintFileHL" }, { group = "NvimTreeDiagnosticHintFolderHL", link = "NvimTreeDiagnosticHintFileHL" },
} }
-- winhighlight for most cases
M.WIN_HL = table.concat({
"EndOfBuffer:NvimTreeEndOfBuffer",
"CursorLine:NvimTreeCursorLine",
"CursorLineNr:NvimTreeCursorLineNr",
"LineNr:NvimTreeLineNr",
"WinSeparator:NvimTreeWinSeparator",
"StatusLine:NvimTreeStatusLine",
"StatusLineNC:NvimTreeStatuslineNC",
"SignColumn:NvimTreeSignColumn",
"Normal:NvimTreeNormal",
"NormalNC:NvimTreeNormalNC",
"NormalFloat:NvimTreeNormalFloat",
"FloatBorder:NvimTreeNormalFloatBorder",
}, ",")
-- winhighlight for help
M.WIN_HL_HELP = table.concat({
"NormalFloat:NvimTreeNormalFloat",
"WinSeparator:NvimTreeWinSeparator",
"CursorLine:NvimTreeCursorLine",
}, ",")
-- nvim-tree highlight groups to legacy -- nvim-tree highlight groups to legacy
M.LEGACY_LINKS = { M.LEGACY_LINKS = {
NvimTreeModifiedIcon = "NvimTreeModifiedFile", NvimTreeModifiedIcon = "NvimTreeModifiedFile",

View File

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

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

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

View File

@@ -1,5 +1,5 @@
local api = require("nvim-tree.api") local api = require("nvim-tree.api")
local view = require("nvim-tree.view") local core = require("nvim-tree.core")
local M = {} local M = {}
@@ -111,7 +111,10 @@ local CMDS = {
bar = true, bar = true,
}, },
command = function(c) command = function(c)
view.resize(c.args) local explorer = core.get_explorer()
if explorer then
explorer.view:resize(c.args)
end
end, end,
}, },
{ {

View File

@@ -1,5 +1,5 @@
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local view = require("nvim-tree.view") local notify = require("nvim-tree.notify")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local M = {} local M = {}
@@ -9,13 +9,30 @@ local TreeExplorer = nil
local first_init_done = false local first_init_done = false
---@param foldername string ---@param foldername string
function M.init(foldername) ---@param callsite string
function M.init(foldername, callsite)
local profile = log.profile_start("core init %s", foldername) local profile = log.profile_start("core init %s", foldername)
log.line("dev", "core.init(%s, %s)", foldername, callsite)
if TreeExplorer then if TreeExplorer then
TreeExplorer:destroy() TreeExplorer:destroy()
end end
TreeExplorer = require("nvim-tree.explorer"):new(foldername)
local err, path
if foldername then
path, err = vim.loop.fs_realpath(foldername)
else
path, err = vim.loop.cwd()
end
if path then
TreeExplorer = require("nvim-tree.explorer")({ path = path })
else
notify.error(err)
TreeExplorer = nil
end
if not first_init_done then if not first_init_done then
events._dispatch_ready() events._dispatch_ready()
first_init_done = true first_init_done = true
@@ -40,7 +57,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,8 +1,9 @@
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local view = require("nvim-tree.view")
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local DirectoryNode = require("nvim-tree.node.directory")
local M = {} local M = {}
---COC severity level strings to LSP severity levels ---COC severity level strings to LSP severity levels
@@ -15,7 +16,7 @@ local COC_SEVERITY_LEVELS = {
} }
---Absolute Node path to LSP severity level ---Absolute Node path to LSP severity level
---@alias NodeSeverities table<string, lsp.DiagnosticSeverity> ---@alias NodeSeverities table<string, vim.diagnostic.Severity>
---@class DiagStatus ---@class DiagStatus
---@field value lsp.DiagnosticSeverity|nil ---@field value lsp.DiagnosticSeverity|nil
@@ -35,33 +36,6 @@ local function uniformize_path(path)
return utils.canonical_path(path:gsub("\\", "/")) return utils.canonical_path(path:gsub("\\", "/"))
end end
---Marshal severities from LSP. Does nothing when LSP disabled.
---@return NodeSeverities
local function from_nvim_lsp()
local buffer_severity = {}
-- is_enabled is not present in all 0.10 builds/releases, see #2781
local is_enabled = false
if vim.fn.has("nvim-0.10") == 1 and type(vim.diagnostic.is_enabled) == "function" then
is_enabled = vim.diagnostic.is_enabled()
elseif type(vim.diagnostic.is_disabled) == "function" then ---@diagnostic disable-line: deprecated
is_enabled = not vim.diagnostic.is_disabled() ---@diagnostic disable-line: deprecated
end
if is_enabled then
for _, diagnostic in ipairs(vim.diagnostic.get(nil, { severity = M.severity })) do
if diagnostic.severity and diagnostic.bufnr and vim.api.nvim_buf_is_valid(diagnostic.bufnr) then
local bufname = uniformize_path(vim.api.nvim_buf_get_name(diagnostic.bufnr))
if not buffer_severity[bufname] or diagnostic.severity < buffer_severity[bufname] then
buffer_severity[bufname] = diagnostic.severity
end
end
end
end
return buffer_severity
end
---Severity is within diagnostics.severity.min, diagnostics.severity.max ---Severity is within diagnostics.severity.min, diagnostics.severity.max
---@param severity lsp.DiagnosticSeverity ---@param severity lsp.DiagnosticSeverity
---@param config table ---@param config table
@@ -125,7 +99,7 @@ end
local function from_cache(node) local function from_cache(node)
local nodepath = uniformize_path(node.absolute_path) local nodepath = uniformize_path(node.absolute_path)
local max_severity = nil local max_severity = nil
if not node.nodes then if not node:is(DirectoryNode) then
-- direct cache hit for files -- direct cache hit for files
max_severity = NODE_SEVERITIES[nodepath] max_severity = NODE_SEVERITIES[nodepath]
else else
@@ -133,11 +107,8 @@ local function from_cache(node)
for bufname, severity in pairs(NODE_SEVERITIES) do for bufname, severity in pairs(NODE_SEVERITIES) do
local node_contains_buf = vim.startswith(bufname, nodepath .. "/") local node_contains_buf = vim.startswith(bufname, nodepath .. "/")
if node_contains_buf then if node_contains_buf then
if severity == M.severity.max then if not max_severity or severity < max_severity then
max_severity = severity max_severity = severity
break
else
max_severity = math.min(max_severity or severity, severity)
end end
end end
end end
@@ -145,28 +116,80 @@ local function from_cache(node)
return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION } return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION }
end end
---Fired on DiagnosticChanged and CocDiagnosticChanged events: ---Fired on DiagnosticChanged for a single buffer.
---This will be called on set and reset of diagnostics.
---On disabling LSP, a reset event will be sent for all buffers.
---@param ev table standard event with data.diagnostics populated
function M.update_lsp(ev)
if not M.enable or not ev or not ev.data or not ev.data.diagnostics then
return
end
local profile_event = log.profile_start("DiagnosticChanged event")
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() function M.update_coc()
if not M.enable then if not M.enable then
return return
end end
utils.debounce("diagnostics", M.debounce_delay, function() utils.debounce("CocDiagnosticChanged update", M.debounce_delay, function()
local profile = log.profile_start("diagnostics update") local profile = log.profile_start("CocDiagnosticChanged update")
if is_using_coc() then NODE_SEVERITIES = from_coc()
NODE_SEVERITIES = from_coc()
else
NODE_SEVERITIES = from_nvim_lsp()
end
NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1
if log.enabled("diagnostics") then if log.enabled("diagnostics") then
for bufname, severity in pairs(NODE_SEVERITIES) do for bufname, severity in pairs(NODE_SEVERITIES) do
log.line("diagnostics", "Indexing bufname '%s' with severity %d", bufname, severity) log.line("diagnostics", "COC Indexing bufname '%s' with severity %d", bufname, severity)
end end
end end
log.profile_end(profile) log.profile_end(profile)
if view.is_buf_valid(view.get_bufnr()) then
local explorer = core.get_explorer() local explorer = core.get_explorer()
local bufnr
if explorer then
bufnr = explorer.view:get_bufnr("diagnostics.update_coc")
end
local should_draw = bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
if should_draw then
if explorer then if explorer then
explorer.renderer:draw() explorer.renderer:draw()
end end
@@ -184,7 +207,7 @@ function M.get_diag_status(node)
end end
-- dir but we shouldn't show on dirs at all -- dir but we shouldn't show on dirs at all
if node.nodes ~= nil and not M.show_on_dirs then if node:is(DirectoryNode) and not M.show_on_dirs then
return nil return nil
end end
@@ -195,13 +218,15 @@ function M.get_diag_status(node)
node.diag_status = from_cache(node) node.diag_status = from_cache(node)
end end
local dir = node:as(DirectoryNode)
-- file -- file
if not node.nodes then if not dir then
return node.diag_status return node.diag_status
end end
-- dir is closed or we should show on open_dirs -- dir is closed or we should show on open_dirs
if not node.open or M.show_on_open_dirs then if not dir.open or M.show_on_open_dirs then
return node.diag_status return node.diag_status
end end
return nil return nil

View File

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

View File

@@ -8,6 +8,7 @@ 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",
@@ -91,6 +92,11 @@ 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,811 @@
local appearance = require("nvim-tree.appearance")
local events = require("nvim-tree.events")
local utils = require("nvim-tree.utils")
local log = require("nvim-tree.log")
local notify = require("nvim-tree.notify")
local globals = require("nvim-tree.globals")
local Class = require("nvim-tree.classic")
---Window and buffer related settings and operations
---@class (exact) View: Class
---@field live_filter table
---@field side string
---@field private explorer Explorer
---@field private adaptive_size boolean
---@field private winopts table
---@field private initial_width integer
---@field private width (fun():integer)|integer|string
---@field private max_width integer
---@field private padding integer
-- TODO multi-instance remove or replace with single member
---@field private bufnr_by_tabid table<integer, integer>
-- TODO multi-instance change to single member
---@field private cursors_by_tabid table<integer, integer[]> as per vim.api.nvim_win_get_cursor
local View = Class:extend()
---@class View
---@overload fun(args: ViewArgs): View
---@class (exact) ViewArgs
---@field explorer Explorer
---@protected
---@param args ViewArgs
function View:new(args)
args.explorer:log_new("View")
self.explorer = args.explorer
self.adaptive_size = false
self.side = (self.explorer.opts.view.side == "right") and "right" or "left"
self.live_filter = { prev_focused_node = nil, }
self.bufnr_by_tabid = {}
self.cursors_by_tabid = {}
self.winopts = {
relativenumber = self.explorer.opts.view.relativenumber,
number = self.explorer.opts.view.number,
list = false,
foldenable = false,
winfixwidth = true,
winfixheight = true,
spell = false,
signcolumn = self.explorer.opts.view.signcolumn,
foldmethod = "manual",
foldcolumn = "0",
cursorcolumn = false,
cursorline = self.explorer.opts.view.cursorline,
cursorlineopt = self.explorer.opts.view.cursorlineopt,
colorcolumn = "0",
wrap = false,
winhl = appearance.WIN_HL,
}
self:configure_width(self.explorer.opts.view.width)
self.initial_width = self:get_width()
-- TODO multi-instance remove this; delete buffers rather than retaining them
local tabid = vim.api.nvim_get_current_tabpage()
self.bufnr_by_tabid[tabid] = globals.BUFNR_BY_TABID[tabid]
end
function View:destroy()
self.explorer:log_destroy("View")
end
---@type { name: string, value: any }[]
local BUFFER_OPTIONS = {
{ name = "bufhidden", value = "wipe" },
{ name = "buflisted", value = false },
{ name = "buftype", value = "nofile" },
{ name = "filetype", value = "NvimTree" },
{ name = "modifiable", value = false },
{ name = "swapfile", value = false },
}
-- TODO multi-instance remove this; delete buffers rather than retaining them
---@private
---@param bufnr integer
---@return boolean
function View:matches_bufnr(bufnr)
for _, b in pairs(globals.BUFNR_BY_TABID) do
if b == bufnr then
return true
end
end
return false
end
-- TODO multi-instance remove this; delete buffers rather than retaining them
---@private
function View:wipe_rogue_buffer()
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if not self:matches_bufnr(bufnr) and utils.is_nvim_tree_buf(bufnr) then
pcall(vim.api.nvim_buf_delete, bufnr, { force = true })
end
end
end
---@private
---@param bufnr integer|false|nil
function View:create_buffer(bufnr)
self:wipe_rogue_buffer()
local tabid = vim.api.nvim_get_current_tabpage()
bufnr = bufnr or vim.api.nvim_create_buf(false, false)
-- set both bufnr registries
globals.BUFNR_BY_TABID[tabid] = bufnr
self.bufnr_by_tabid[tabid] = bufnr
vim.api.nvim_buf_set_name(bufnr, "NvimTree_" .. tabid)
for _, option in ipairs(BUFFER_OPTIONS) do
vim.api.nvim_set_option_value(option.name, option.value, { buf = bufnr })
end
require("nvim-tree.keymap").on_attach(bufnr)
events._dispatch_tree_attached_post(bufnr)
end
---@private
---@param size (fun():integer)|integer|string
---@return integer
function View:get_size(size)
if type(size) == "number" then
return size
elseif type(size) == "function" then
return self:get_size(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
---@return integer
function View:get_width(size)
if size then
return self:get_size(size)
else
return self:get_size(self.width)
end
end
local move_tbl = {
left = "H",
right = "L",
}
---@private
function View:set_window_options_and_buffer()
pcall(vim.api.nvim_command, "buffer " .. self:get_bufnr("View:set_window_options_and_buffer"))
if vim.fn.has("nvim-0.10") == 1 then
local eventignore = vim.api.nvim_get_option_value("eventignore", {})
vim.api.nvim_set_option_value("eventignore", "all", {})
for k, v in pairs(self.winopts) do
vim.api.nvim_set_option_value(k, v, { scope = "local" })
end
vim.api.nvim_set_option_value("eventignore", eventignore, {})
else
local eventignore = vim.api.nvim_get_option("eventignore") ---@diagnostic disable-line: deprecated
vim.api.nvim_set_option("eventignore", "all") ---@diagnostic disable-line: deprecated
-- #3009 vim.api.nvim_win_set_option does not set local scope without explicit winid.
-- Revert to opt_local instead of propagating it through for just the 0.10 path.
for k, v in pairs(self.winopts) do
vim.opt_local[k] = v
end
vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated
end
end
---@private
---@return table
function View:open_win_config()
if type(self.explorer.opts.view.float.open_win_config) == "function" then
return self.explorer.opts.view.float.open_win_config()
else
return self.explorer.opts.view.float.open_win_config
end
end
---@private
function View:open_window()
if self.explorer.opts.view.float.enable then
vim.api.nvim_open_win(0, true, self:open_win_config())
else
vim.api.nvim_command("vsp")
self:reposition_window()
end
globals.WINID_BY_TABID[vim.api.nvim_get_current_tabpage()] = vim.api.nvim_get_current_win()
self:set_window_options_and_buffer()
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 any state that should be preserved on reopening
---@private
---@param tabid integer
function View:save_state(tabid)
tabid = tabid or vim.api.nvim_get_current_tabpage()
self.cursors_by_tabid[tabid] = vim.api.nvim_win_get_cursor(self:get_winid(tabid, "View:save_tab_state") or 0)
end
---@private
---@param tabid integer
function View:close_internal(tabid)
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:close_internal(t%s)", tabid)
end
--- END multi-instance FF
if not self:is_visible({ tabpage = tabid }, "View:close_internal") then
return
end
self:save_state(tabid)
switch_buf_if_last_buf()
local tree_win = self:get_winid(tabid, "View:close_internal")
local current_win = vim.api.nvim_get_current_win()
for _, win in pairs(vim.api.nvim_tabpage_list_wins(tabid)) 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
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:close_internal(t%s) w%s", tabid, tree_win)
end
--- END multi-instance FF
---
local success, error = pcall(vim.api.nvim_win_close, tree_win or 0, true)
if not success then
notify.debug("Failed to close window: " .. error)
return
end
end
return
end
end
end
function View:close_this_tab_only()
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:close_this_tab_only()")
end
--- END multi-instance FF
self:close_internal(vim.api.nvim_get_current_tabpage())
end
-- TODO this is broken at 1.13.0 - current tab does not close when tab.sync.close is set
function View:close_all_tabs()
log.line("dev", "View:close_all_tabs() globals.WINID_BY_TABID=%s", vim.inspect(globals.WINID_BY_TABID))
for tabid, _ in pairs(globals.WINID_BY_TABID) do
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:close_all_tabs()")
end
--- END multi-instance FF
self:close_internal(tabid)
end
end
---@param tabid integer|nil
---@param callsite string
function View:close(tabid, callsite)
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:close(t%s, %s)", tabid, callsite)
end
--- END multi-instance FF
if self.explorer.opts.tab.sync.close then
self:close_all_tabs()
elseif tabid then
self:close_internal(tabid)
else
self:close_this_tab_only()
end
end
---@param options table|nil
function View:open(options)
if self:is_visible(nil, "View:open") then
return
end
local profile = log.profile_start("view open")
events._dispatch_on_tree_pre_open()
self:create_buffer()
self:open_window()
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
---@private
function View:grow()
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("View:grow1"), starts_at, -1, false)
-- number of columns of right-padding to indicate end of path
local padding = self:get_size(self.padding)
-- account for sign/number columns etc.
local wininfo = vim.fn.getwininfo(self:get_winid(nil, "View:grow"))
if type(wininfo) == "table" and type(wininfo[1]) == "table" then
padding = padding + wininfo[1].textoff
end
local resizing_width = self.initial_width - padding
local max_width
-- maybe bound max
if self.max_width == -1 then
max_width = -1
else
max_width = self:get_width(self.max_width) - padding
end
local ns_id = vim.api.nvim_get_namespaces()["NvimTreeExtmarks"]
for line_nr, l in pairs(lines) do
local count = vim.fn.strchars(l)
-- also add space for right-aligned icons
local extmarks = vim.api.nvim_buf_get_extmarks(self:get_bufnr("View:grow2"), ns_id, { line_nr, 0 }, { line_nr, -1 }, { details = true })
count = count + utils.extmarks_length(extmarks)
if resizing_width < count then
resizing_width = count
end
if self.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 View:grow_from_content()
if self.adaptive_size then
self:grow()
end
end
---@param size string|number|nil
function View:resize(size)
if self.explorer.opts.view.float.enable and not self.adaptive_size then
-- if the floating windows's adaptive size is not desired, then the
-- float size should be defined in self.explorer.opts.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.width + size
end
end
if type(size) == "number" and size <= 0 then
return
end
if size then
self.width = size
end
if not self:is_visible(nil, "View:resize") then
return
end
local winid = self:get_winid(nil, "View:resize") or 0
local new_size = self:get_width()
if new_size ~= vim.api.nvim_win_get_width(winid) then
vim.api.nvim_win_set_width(winid, new_size)
if not self.explorer.opts.view.preserve_window_proportions then
vim.cmd(":wincmd =")
end
end
events._dispatch_on_tree_resize(new_size)
end
---@private
function View:reposition_window()
local move_to = move_tbl[self.side]
vim.api.nvim_command("wincmd " .. move_to)
self:resize()
end
---@private
function View:set_current_win()
local current_tab = vim.api.nvim_get_current_tabpage()
globals.WINID_BY_TABID[current_tab] = vim.api.nvim_get_current_win()
end
---@class OpenInWinOpts
---@field hijack_current_buf boolean|nil default true
---@field resize boolean|nil default true
---@field winid number|nil 0 or nil for current
---Open the tree in the a window
---@param opts OpenInWinOpts|nil
function View:open_in_win(opts)
opts = opts or { hijack_current_buf = true, resize = true }
events._dispatch_on_tree_pre_open()
if opts.winid and vim.api.nvim_win_is_valid(opts.winid) then
vim.api.nvim_set_current_win(opts.winid)
end
self:create_buffer(opts.hijack_current_buf and vim.api.nvim_get_current_buf())
globals.WINID_BY_TABID[vim.api.nvim_get_current_tabpage()] = vim.api.nvim_get_current_win()
self:set_current_win()
self:set_window_options_and_buffer()
if opts.resize then
self:reposition_window()
self:resize()
end
events._dispatch_on_tree_open()
end
function View:abandon_current_window()
local tab = vim.api.nvim_get_current_tabpage()
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
log.line("dev", "View:abandon_current_window() t%d w%s b%s member b%s %s",
tab,
globals.WINID_BY_TABID[tab],
globals.BUFNR_BY_TABID[tab],
self.bufnr_by_tabid[tab],
(globals.BUFNR_BY_TABID[tab] == self.bufnr_by_tabid[tab]) and "" or "MISMATCH")
end
--- END multi-instance FF
-- reset both bufnr registries
globals.BUFNR_BY_TABID[tab] = nil
self.bufnr_by_tabid[tab] = nil
globals.WINID_BY_TABID[tab] = nil
end
function View:abandon_all_windows()
for tab, _ in pairs(vim.api.nvim_list_tabpages()) do
globals.BUFNR_BY_TABID[tab] = nil
globals.WINID_BY_TABID[tab] = nil
end
end
---@param opts table|nil
---@param callsite string
---@return boolean
function View:is_visible(opts, callsite)
local msg
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
msg = string.format("View:is_visible(%s, %-20.20s)", vim.inspect(opts, { newline = "" }), callsite)
end
--- END multi-instance FF
if opts and opts.tabpage then
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
local winid = self:winid(opts.tabpage)
local winid_by_tabid = opts.tabpage and globals.WINID_BY_TABID[opts.tabpage] or nil
msg = string.format("%s globals.WINID_BY_TABID[%s]=w%s view.winid(%s)=w%s",
msg,
opts.tabpage, winid_by_tabid,
opts.tabpage, winid
)
if winid ~= winid_by_tabid then
msg = string.format("%s MISMATCH", msg)
notify.error(msg)
end
log.line("dev", "%s", msg)
return winid and vim.api.nvim_win_is_valid(winid) or false
--- END multi-instance FF
else
local winid = globals.WINID_BY_TABID[opts.tabpage]
return winid and vim.api.nvim_win_is_valid(winid)
end
end
if opts and opts.any_tabpage then
for tabid, winid_by_tabid in pairs(globals.WINID_BY_TABID) do
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
local winid = self:winid(tabid)
msg = string.format("%s globals.WINID_BY_TABID[%s]=w%s view.winid(%s)=w%s",
msg,
tabid, winid_by_tabid,
tabid, winid
)
if winid ~= winid_by_tabid then
msg = string.format("%s MISMATCH", msg)
notify.error(msg)
end
log.line("dev", "%s", msg)
if winid and vim.api.nvim_win_is_valid(winid) then
return true
end
--- END multi-instance FF
else
if winid_by_tabid and vim.api.nvim_win_is_valid(winid_by_tabid) then
return true
end
end
end
return false
end
local winid = self:get_winid(nil, "View:is_visible")
return winid ~= nil and vim.api.nvim_win_is_valid(winid or 0)
end
---@param opts table|nil
function View:set_cursor(opts)
if self:is_visible(nil, "View:set_cursor") then
pcall(vim.api.nvim_win_set_cursor, self:get_winid(nil, "View:set_cursor"), opts)
end
end
---@param winid number|nil
---@param open_if_closed boolean|nil
function View:focus(winid, open_if_closed)
local wid = winid or self:get_winid(nil, "View:focus1")
if vim.api.nvim_win_get_tabpage(wid or 0) ~= vim.api.nvim_win_get_tabpage(0) then
self:close(nil, "View:focus")
self:open()
wid = self:get_winid(nil, "View:focus2")
elseif open_if_closed and not self:is_visible(nil, "View:focus") then
self:open()
end
if wid then
vim.api.nvim_set_current_win(wid)
end
end
--- Retrieve the winid of the open tree.
---@param opts ApiTreeWinIdOpts|nil
---@return number|nil winid unlike get_winid(), this returns nil if the nvim-tree window is not visible
function View:api_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 }, "View:api_winid") then
return self:get_winid(tabpage, "View:winid")
else
return nil
end
end
---restore any state from last close
function View:restore_state()
self:set_cursor(self.cursors_by_tabid[vim.api.nvim_get_current_tabpage()])
end
--- winid containing the buffer
---@param tabid number|nil (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return integer? winid
function View:winid(tabid)
local bufnr = self.bufnr_by_tabid[tabid]
if bufnr then
for _, winid in pairs(vim.api.nvim_tabpage_list_wins(tabid or 0)) do
if vim.api.nvim_win_get_buf(winid) == bufnr then
return winid
end
end
end
end
--- Returns the window number for nvim-tree within the tabpage specified
---@param tabid number|nil (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@param callsite string
---@return number|nil
function View:get_winid(tabid, callsite)
local tabid_param = tabid
tabid = tabid or vim.api.nvim_get_current_tabpage()
local global_winid = nil
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
local msg_fault = ""
if not globals.WINID_BY_TABID[tabid] then
msg_fault = "no WINID_BY_TABID"
elseif not vim.api.nvim_win_is_valid(globals.WINID_BY_TABID[tabid]) then
msg_fault = string.format("invalid globals.WINID_BY_TABID[tabid] %d", globals.WINID_BY_TABID[tabid])
else
global_winid = globals.WINID_BY_TABID[tabid]
end
local winid = self:winid(tabid)
if winid ~= global_winid then
msg_fault = "MISMATCH"
end
local msg = string.format("View:get_winid(%3s, %-20.20s) globals.WINID_BY_TABID[%s]=w%s view.winid(%s)=w%s %s",
tabid_param,
callsite,
tabid, global_winid,
tabid, winid,
msg_fault
)
log.line("dev", "%s", msg)
if winid ~= global_winid then
notify.error(msg)
end
return winid
end
--- END multi-instance FF
-- legacy codepath
if global_winid and vim.api.nvim_win_is_valid(global_winid) then
return global_winid
end
end
--- Returns the current nvim tree bufnr
---@param callsite string
---@return number
function View:get_bufnr(callsite)
local tab = vim.api.nvim_get_current_tabpage()
--- BEGIN multi-instance FF
if self.explorer.opts.experimental.multi_instance then
local msg = string.format("View:get_bufnr(%-20.20s) globals.BUFNR_BY_TABID[%s]=b%s view.bufnr_by_tab[%s]=b%s %s",
callsite,
tab, globals.BUFNR_BY_TABID[tab],
tab, self.bufnr_by_tabid[tab],
(globals.BUFNR_BY_TABID[tab] == self.bufnr_by_tabid[tab]) and "" or "MISMATCH"
)
if globals.BUFNR_BY_TABID[tab] ~= self.bufnr_by_tabid[tab] then
notify.error(msg)
end
log.line("dev", msg)
return self.bufnr_by_tabid[tab]
end
--- END multi-instance FF
return globals.BUFNR_BY_TABID[tab]
end
function View:prevent_buffer_override()
local view_winid = self:get_winid(nil, "View:prevent_buffer_override")
local view_bufnr = self:get_bufnr("View:prevent_buffer_override")
-- 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, winid in ipairs(globals.WINID_BY_TABID) do
if winid == view_winid then
globals.WINID_BY_TABID[i] = nil
break
end
end
end
if curwin ~= view_winid 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")
self:open({ focus_tree = false })
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
explorer.renderer:draw()
end
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 View:is_root_folder_visible(cwd)
return cwd ~= "/" and self.explorer.opts.renderer.root_folder_label ~= false
end
-- used on ColorScheme event
function View:reset_winhl()
local winid = self:get_winid(nil, "View:reset_winhl")
if winid and vim.api.nvim_win_is_valid(winid) then
vim.wo[winid].winhl = appearance.WIN_HL
end
end
---Check if width determined or calculated on-fly
---@return boolean
function View:is_width_determined()
return type(self.width) ~= "function"
end
-- These are needed as they are populated only by the user, not configuration
local DEFAULT_MIN_WIDTH = 30
local DEFAULT_MAX_WIDTH = -1
local DEFAULT_PADDING = 1
---Configure width-related config
---@param width string|function|number|table|nil
function View:configure_width(width)
if type(width) == "table" then
self.adaptive_size = true
self.width = width.min or DEFAULT_MIN_WIDTH
self.max_width = width.max or DEFAULT_MAX_WIDTH
self.padding = width.padding or DEFAULT_PADDING
elseif width == nil then
if self.explorer.opts.view.width ~= nil then
-- if we had input config - fallback to it
self:configure_width(self.explorer.opts.view.width)
else
-- otherwise - restore initial width
self.width = self.initial_width
end
else
self.adaptive_size = false
self.width = width
end
end
return View

View File

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

View File

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

View File

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

View File

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

10
lua/nvim-tree/globals.lua Normal file
View File

@@ -0,0 +1,10 @@
-- global state, to be refactored away during multi-instance
local M = {
-- from View
WINID_BY_TABID = {},
BUFNR_BY_TABID = {},
CURSORS = {},
}
return M

View File

@@ -1,3 +1,4 @@
local appearance = require("nvim-tree.appearance")
local keymap = require("nvim-tree.keymap") local keymap = require("nvim-tree.keymap")
local api = {} -- circular dependency local api = {} -- circular dependency
@@ -5,11 +6,7 @@ local PAT_MOUSE = "^<.*Mouse"
local PAT_CTRL = "^<C%-" local PAT_CTRL = "^<C%-"
local PAT_SPECIAL = "^<.+" local PAT_SPECIAL = "^<.+"
local WIN_HL = table.concat({ local namespace_help_id = vim.api.nvim_create_namespace("NvimTreeHelp")
"NormalFloat:NvimTreeNormalFloat",
"WinSeparator:NvimTreeWinSeparator",
"CursorLine:NvimTreeCursorLine",
}, ",")
local M = { local M = {
config = {}, config = {},
@@ -53,6 +50,7 @@ end
--- sort vim command lhs roughly as per :help index --- sort vim command lhs roughly as per :help index
---@param a string ---@param a string
---@param b string ---@param b string
---@return boolean
local function sort_lhs(a, b) local function sort_lhs(a, b)
-- mouse first -- mouse first
if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then
@@ -81,8 +79,8 @@ end
--- Compute all lines for the buffer --- Compute all lines for the buffer
---@param map table keymap.get_keymap ---@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(map)
local head_lhs = "nvim-tree mappings" local head_lhs = "nvim-tree mappings"
@@ -129,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 = { local hl_range_args = {
{ "NvimTreeFolderName", 0, 0, #head_lhs }, { higroup = "NvimTreeFolderName", start = { 0, 0, }, finish = { 0, #head_lhs, }, },
{ "NvimTreeFolderName", 0, width - 1, width }, { higroup = "NvimTreeFolderName", start = { 0, width - 1, }, finish = { 0, width, }, },
{ "NvimTreeFolderName", 1, width - 1, width }, { higroup = "NvimTreeFolderName", start = { 1, width - 1, }, finish = { 1, width, }, },
} }
-- mappings, left padded 1 -- mappings, left padded 1
@@ -144,10 +142,10 @@ local function compute(map)
width = math.max(#line, width) width = math.max(#line, width)
-- highlight lhs -- highlight lhs
table.insert(hl, { "NvimTreeFolderName", i + 1, 1, #l.lhs + 1 }) table.insert(hl_range_args, { higroup = "NvimTreeFolderName", start = { i + 1, 1, }, finish = { i + 1, #l.lhs + 1, }, })
end end
return lines, hl, width return lines, hl_range_args, width
end end
--- close the window and delete the buffer, if they exist --- close the window and delete the buffer, if they exist
@@ -171,7 +169,7 @@ local function open()
local map = keymap.get_keymap() local map = keymap.get_keymap()
-- text and highlight -- text and highlight
local lines, hl, width = compute(map) local lines, hl_range_args, width = compute(map)
-- create the buffer -- create the buffer
M.bufnr = vim.api.nvim_create_buf(false, true) M.bufnr = vim.api.nvim_create_buf(false, true)
@@ -186,8 +184,12 @@ local function open()
end end
-- highlight it -- highlight it
for _, h in ipairs(hl) do for _, args in ipairs(hl_range_args) do
vim.api.nvim_buf_add_highlight(M.bufnr, -1, h[1], h[2], h[3], h[4]) if vim.fn.has("nvim-0.11") == 1 and vim.hl and vim.hl.range then
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
@@ -203,7 +205,7 @@ local function open()
}) })
-- style it a bit like the tree -- style it a bit like the tree
vim.wo[M.winnr].winhl = WIN_HL vim.wo[M.winnr].winhl = appearance.WIN_HL_HELP
vim.wo[M.winnr].cursorline = M.config.cursorline vim.wo[M.winnr].cursorline = M.config.cursorline
local function toggle_sort() local function toggle_sort()

View File

@@ -48,7 +48,7 @@ function NodeIterator:recursor(f)
end end
---@return Node|nil ---@return Node|nil
---@return number|nil ---@return number
function NodeIterator:iterate() function NodeIterator:iterate()
local iteration_count = 0 local iteration_count = 0
local function iter(nodes) local function iter(nodes)

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
local view = require("nvim-tree.view")
local core = require("nvim-tree.core") local core = require("nvim-tree.core")
local utils = require("nvim-tree.utils")
local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify") local notify = require("nvim-tree.notify")
local explorer_node = require("nvim-tree.explorer.node")
---@class LibOpenOpts ---@class LibOpenOpts
---@field path string|nil path ---@field path string|nil path
@@ -14,177 +10,12 @@ local M = {
target_winid = nil, target_winid = nil,
} }
---Cursor position as per vim.api.nvim_win_get_cursor function M.set_target_win()
---@return integer[]|nil
function M.get_cursor_position()
if not core.get_explorer() then
return
end
local winnr = view.get_winnr()
if not winnr or not vim.api.nvim_win_is_valid(winnr) then
return
end
return vim.api.nvim_win_get_cursor(winnr)
end
---@return Node|nil
function M.get_node_at_cursor()
local cursor = M.get_cursor_position()
if not cursor then
return
end
if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then
return { name = ".." }
end
return utils.get_nodes_by_line(core.get_explorer().nodes, core.get_nodes_starting_line())[cursor[1]]
end
---Create a sanitized partial copy of a node, populating children recursively.
---@param node Node|nil
---@return Node|nil cloned node
local function clone_node(node)
if not node then
node = core.get_explorer()
if not node then
return nil
end
end
local n = {
absolute_path = node.absolute_path,
executable = node.executable,
extension = node.extension,
git_status = node.git_status,
has_children = node.has_children,
hidden = node.hidden,
link_to = node.link_to,
name = node.name,
open = node.open,
type = node.type,
fs_stat = node.fs_stat,
}
if type(node.nodes) == "table" then
n.nodes = {}
for _, child in ipairs(node.nodes) do
table.insert(n.nodes, clone_node(child))
end
end
return n
end
---Api.tree.get_nodes
---@return Node[]|nil
function M.get_nodes()
return clone_node(core.get_explorer())
end
-- If node is grouped, return the last node in the group. Otherwise, return the given node.
---@param node Node
---@return Node
function M.get_last_group_node(node)
while node and node.group_next do
node = node.group_next
end
return node ---@diagnostic disable-line: return-type-mismatch -- it can't be nil
end
---Group empty folders
-- Recursively group nodes
---@param node Node
---@return Node[]
function M.group_empty_folders(node)
local is_root = not node.parent
local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
if M.group_empty and not is_root and child_folder_only then
node.group_next = child_folder_only
local ns = M.group_empty_folders(child_folder_only)
node.nodes = ns or {}
return ns
end
return node.nodes
end
---Ungroup empty folders
-- If a node is grouped, ungroup it: put node.group_next to the node.nodes and set node.group_next to nil
---@param node Node
function M.ungroup_empty_folders(node)
local cur = node
while cur and cur.group_next do
cur.nodes = { cur.group_next }
cur.group_next = nil
cur = cur.nodes[1]
end
end
---@param node Node
---@return Node[]
function M.get_all_nodes_in_group(node)
local next_node = utils.get_parent_of_group(node)
local nodes = {}
while next_node do
table.insert(nodes, next_node)
next_node = next_node.group_next
end
return nodes
end
-- Toggle group empty folders
---@param head_node Node
local function toggle_group_folders(head_node)
local is_grouped = head_node.group_next ~= nil
if is_grouped then
M.ungroup_empty_folders(head_node)
else
M.group_empty_folders(head_node)
end
end
---@param node Node
function M.expand_or_collapse(node, toggle_group)
local explorer = core.get_explorer() local explorer = core.get_explorer()
toggle_group = toggle_group or false
if node.has_children then
node.has_children = false
end
if #node.nodes == 0 and explorer then
explorer:expand(node)
end
local head_node = utils.get_parent_of_group(node)
if toggle_group then
toggle_group_folders(head_node)
end
local open = M.get_last_group_node(node).open
local next_open
if toggle_group then
next_open = open
else
next_open = not open
end
for _, n in ipairs(M.get_all_nodes_in_group(head_node)) do
n.open = next_open
end
if explorer then
explorer.renderer:draw()
end
end
function M.set_target_win()
local id = vim.api.nvim_get_current_win() local id = vim.api.nvim_get_current_win()
local tree_id = view.get_winnr()
if tree_id and id == tree_id then if explorer and id == explorer.view:get_winid(nil, "lib.set_target_win") then
M.target_winid = 0 M.target_winid = 0
return return
end end
@@ -200,11 +31,16 @@ local function handle_buf_cwd(cwd)
end end
local function open_view_and_draw() local function open_view_and_draw()
local explorer = core.get_explorer()
local cwd = vim.fn.getcwd() local cwd = vim.fn.getcwd()
view.open()
if explorer then
explorer.view:open()
end
handle_buf_cwd(cwd) handle_buf_cwd(cwd)
local explorer = core.get_explorer()
if explorer then if explorer then
explorer.renderer:draw() explorer.renderer:draw()
end end
@@ -234,7 +70,7 @@ end
---@param items_short string[] ---@param items_short string[]
---@param items_long string[] ---@param items_long string[]
---@param kind string|nil ---@param kind string|nil
---@param callback fun(item_short: string) ---@param callback fun(item_short: string|nil)
function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback) function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback)
local function format_item(short) local function format_item(short)
for i, s in ipairs(items_short) do for i, s in ipairs(items_short) do
@@ -266,40 +102,42 @@ function M.open(opts)
M.set_target_win() M.set_target_win()
if not core.get_explorer() or opts.path then if not core.get_explorer() or opts.path then
if opts.path then if opts.path then
core.init(opts.path) core.init(opts.path, "lib.open - opts.path")
else else
local cwd, err = vim.loop.cwd() local cwd, err = vim.loop.cwd()
if not cwd then if not cwd then
notify.error(string.format("current working directory unavailable: %s", err)) notify.error(string.format("current working directory unavailable: %s", err))
return return
end end
core.init(cwd) core.init(cwd, "lib.open - cwd")
end end
end end
local explorer = core.get_explorer() local explorer = core.get_explorer()
if should_hijack_current_buf() then if should_hijack_current_buf() then
view.close_this_tab_only()
view.open_in_win()
if explorer then if explorer then
explorer.view:close_this_tab_only()
explorer.view:open_in_win()
explorer.renderer:draw() explorer.renderer:draw()
end end
elseif opts.winid then elseif opts.winid then
view.open_in_win({ hijack_current_buf = false, resize = false, winid = opts.winid })
if explorer then if explorer then
explorer.view:open_in_win({ hijack_current_buf = false, resize = false, winid = opts.winid })
explorer.renderer:draw() explorer.renderer:draw()
end end
elseif opts.current_window then elseif opts.current_window then
view.open_in_win({ hijack_current_buf = false, resize = false })
if explorer then if explorer then
explorer.view:open_in_win({ hijack_current_buf = false, resize = false })
explorer.renderer:draw() explorer.renderer:draw()
end end
else else
open_view_and_draw() open_view_and_draw()
end end
view.restore_tab_state()
events._dispatch_on_tree_open() if explorer then
explorer.view:restore_state()
end
end end
function M.setup(opts) function M.setup(opts)

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
local globals = require("nvim-tree.globals")
local M = {}
--- Debugging only.
--- Tabs show WINID_BY_TABID winid and BUFNR_BY_TABID bufnr for the tab.
--- Orphans for inexistent tab_ids are shown at the right.
--- lib.target_winid is always shown at the right next to a close button.
--- Enable with:
--- vim.opt.tabline = "%!v:lua.require('nvim-tree.explorer.view').tab_line()"
--- vim.opt.showtabline = 2
---@return string
function M.tab_line()
local tabids = vim.api.nvim_list_tabpages()
local tabid_cur = vim.api.nvim_get_current_tabpage()
local bufnr_by_tabid = vim.deepcopy(globals.BUFNR_BY_TABID)
local winid_by_tabid = vim.deepcopy(globals.WINID_BY_TABID)
local tl = "%#TabLine#"
for i, tabid in ipairs(tabids) do
-- click to select
tl = tl .. "%" .. i .. "T"
-- style
if tabid == tabid_cur then
tl = tl .. "%#StatusLine#|"
else
tl = tl .. "|%#TabLine#"
end
-- tab_id itself
tl = tl .. " t" .. tabid
-- winid, if present
local tp = globals.WINID_BY_TABID[tabid]
if tp then
tl = tl .. " w" .. (tp or "nil")
else
tl = tl .. " "
end
-- bufnr, if present
local bpt = globals.BUFNR_BY_TABID[tabid]
if bpt then
tl = tl .. " b" .. bpt
else
tl = tl .. " "
end
tl = tl .. " "
-- remove actively mapped
bufnr_by_tabid[tabid] = nil
winid_by_tabid[tabid] = nil
end
-- close last and reset
tl = tl .. "|%#CursorLine#%T"
-- collect orphans
local orphans = {}
for tab_id, bufnr in pairs(bufnr_by_tabid) do
orphans[tab_id] = orphans[tab_id] or {}
orphans[tab_id].bufnr = bufnr
end
for tab_id, tp in pairs(winid_by_tabid) do
orphans[tab_id] = orphans[tab_id] or {}
orphans[tab_id].winid = tp
end
-- right-align
tl = tl .. "%=%#TabLine#"
-- print orphans
for tab_id, orphan in pairs(orphans) do
-- inexistent tab
tl = tl .. "%#error#| t" .. tab_id
-- maybe winid
if orphan.winid then
tl = tl .. " w" .. (orphan.winid or "nil")
else
tl = tl .. " "
end
-- maybe bufnr
if orphan.bufnr then
tl = tl .. " b" .. orphan.bufnr
else
tl = tl .. " "
end
tl = tl .. " "
end
-- target win id and close button
tl = tl .. "|%#TabLine# twi" .. (require("nvim-tree.lib").target_winid or "?") .. " %999X| X |"
return tl
end
function M.setup(opts)
if not opts.experimental.multi_instance then
return
end
vim.opt.tabline = "%!v:lua.require('nvim-tree.multi-instance-debug').tab_line()"
vim.opt.showtabline = 2
end
return M

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
local M = {} local appearance = require("nvim-tree.appearance")
local utils = require("nvim-tree.utils") local utils = require("nvim-tree.utils")
local M = {}
local function hide(win) local function hide(win)
if win then if win then
if vim.api.nvim_win_is_valid(win) then if vim.api.nvim_win_is_valid(win) then
@@ -32,7 +33,7 @@ local function effective_win_width()
return win_width - win_info[1].textoff return win_width - win_info[1].textoff
end end
local function show() local function show(opts)
local line_nr = vim.api.nvim_win_get_cursor(0)[1] local line_nr = vim.api.nvim_win_get_cursor(0)[1]
if vim.wo.wrap then if vim.wo.wrap then
return return
@@ -52,19 +53,26 @@ local function show()
local text_width = vim.fn.strdisplaywidth(vim.fn.substitute(line, "[^[:print:]]*$", "", "g")) local text_width = vim.fn.strdisplaywidth(vim.fn.substitute(line, "[^[:print:]]*$", "", "g"))
local win_width = effective_win_width() local win_width = effective_win_width()
-- windows width reduced by right aligned icons
local icon_ns_id = vim.api.nvim_get_namespaces()["NvimTreeExtmarks"]
local icon_extmarks = vim.api.nvim_buf_get_extmarks(0, icon_ns_id, { line_nr - 1, 0 }, { line_nr - 1, -1 }, { details = true })
win_width = win_width - utils.extmarks_length(icon_extmarks)
if text_width < win_width then if text_width < win_width then
return return
end end
M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, { M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, {
relative = "win", relative = "win",
row = 0, row = 0,
bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 }, bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 },
width = math.min(text_width, vim.o.columns - 2), width = math.min(text_width, vim.o.columns - 2),
height = 1, height = 1,
noautocmd = true, noautocmd = true,
style = "minimal", style = "minimal",
border = "none"
}) })
vim.wo[M.popup_win].winhl = appearance.WIN_HL
local ns_id = vim.api.nvim_get_namespaces()["NvimTreeHighlights"] local ns_id = vim.api.nvim_get_namespaces()["NvimTreeHighlights"]
local extmarks = vim.api.nvim_buf_get_extmarks(0, ns_id, { line_nr - 1, 0 }, { line_nr - 1, -1 }, { details = true }) local extmarks = vim.api.nvim_buf_get_extmarks(0, ns_id, { line_nr - 1, 0 }, { line_nr - 1, -1 }, { details = true })
@@ -79,9 +87,18 @@ local function show()
---@type vim.api.keyset.extmark_details ---@type vim.api.keyset.extmark_details
local details = extmark[4] local details = extmark[4]
vim.api.nvim_buf_add_highlight(0, ns_id, details.hl_group, 0, col, details.end_col) if type(details) == "table" then
if vim.fn.has("nvim-0.11") == 1 and vim.hl and vim.hl.range then
vim.hl.range(0, ns_id, details.hl_group, { 0, col }, { 0, details.end_col, }, {})
else
vim.api.nvim_buf_add_highlight(0, ns_id, details.hl_group, 0, col, details.end_col) ---@diagnostic disable-line: deprecated
end
end
end
vim.cmd([[ setlocal nowrap noswapfile nobuflisted buftype=nofile bufhidden=wipe ]])
if opts.view.cursorline then
vim.cmd([[ setlocal cursorline cursorlineopt=both ]])
end end
vim.cmd([[ setlocal nowrap cursorline noswapfile nobuflisted buftype=nofile bufhidden=hide ]])
end) end)
end end
@@ -107,7 +124,7 @@ M.setup = function(opts)
pattern = { "NvimTree_*" }, pattern = { "NvimTree_*" },
callback = function() callback = function()
if utils.is_nvim_tree_buf(0) then if utils.is_nvim_tree_buf(0) then
show() show(opts)
end end
end, end,
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
local Decorator = require("nvim-tree.renderer.decorator")
---Exposed as nvim_tree.api.decorator.UserDecorator
---@class (exact) UserDecorator: Decorator
local UserDecorator = Decorator:extend()
return UserDecorator

View File

@@ -1,9 +1,7 @@
local log = require("nvim-tree.log") local log = require("nvim-tree.log")
local view = require("nvim-tree.view")
local events = require("nvim-tree.events") local events = require("nvim-tree.events")
local icon_component = require("nvim-tree.renderer.components.icons") local Class = require("nvim-tree.classic")
local Builder = require("nvim-tree.renderer.builder") local Builder = require("nvim-tree.renderer.builder")
local SIGN_GROUP = "NvimTreeRendererSigns" local SIGN_GROUP = "NvimTreeRendererSigns"
@@ -12,36 +10,38 @@ local namespace_highlights_id = vim.api.nvim_create_namespace("NvimTreeHighlight
local namespace_extmarks_id = vim.api.nvim_create_namespace("NvimTreeExtmarks") local namespace_extmarks_id = vim.api.nvim_create_namespace("NvimTreeExtmarks")
local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines") local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines")
---@class (exact) Renderer ---@alias HighlightRangeArgs { higroup:string, start:integer[], finish:integer[] } named arguments for vim.hl.range
---@field private __index? table
---@field private opts table user options
---@field private explorer Explorer
---@field private builder Builder
local Renderer = {}
---@param opts table user options ---@class (exact) Renderer: Class
---@param explorer Explorer ---@field explorer Explorer
---@return Renderer local Renderer = Class:extend()
function Renderer:new(opts, explorer)
---@type Renderer
local o = {
opts = opts,
explorer = explorer,
builder = Builder:new(opts, explorer),
}
setmetatable(o, self) ---@class Renderer
self.__index = self ---@overload fun(args: RendererArgs): Renderer
return o ---@class (exact) RendererArgs
---@field explorer Explorer
---@protected
---@param args RendererArgs
function Renderer:new(args)
args.explorer:log_new("Renderer")
self.explorer = args.explorer
end
function Renderer:destroy()
self.explorer:log_destroy("Renderer")
end end
---@private ---@private
---@param bufnr number ---@param bufnr number
---@param lines string[] ---@param lines string[]
---@param hl_args AddHighlightArgs[] ---@param hl_range_args HighlightRangeArgs[]
---@param signs string[] ---@param signs string[]
function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines) ---@param extmarks table[] extra marks for right icon placement
---@param virtual_lines table[] virtual lines for hidden count display
function Renderer:_draw(bufnr, lines, hl_range_args, signs, extmarks, virtual_lines)
if vim.fn.has("nvim-0.10") == 1 then if vim.fn.has("nvim-0.10") == 1 then
vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr }) vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr })
else else
@@ -49,7 +49,7 @@ function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
end end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
self:render_hl(bufnr, hl_args) self:render_hl(bufnr, hl_range_args)
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 })
@@ -66,9 +66,9 @@ function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
for i, extname in pairs(extmarks) do for i, extname in pairs(extmarks) do
for _, mark in ipairs(extname) do for _, mark in ipairs(extname) do
vim.api.nvim_buf_set_extmark(bufnr, namespace_extmarks_id, i, -1, { vim.api.nvim_buf_set_extmark(bufnr, namespace_extmarks_id, i, -1, {
virt_text = { { mark.str, mark.hl } }, virt_text = { { mark.str, mark.hl } },
virt_text_pos = "right_align", virt_text_pos = "right_align",
hl_mode = "combine", hl_mode = "combine",
}) })
end end
end end
@@ -76,52 +76,54 @@ function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines)
vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1)
for line_nr, vlines in pairs(virtual_lines) do for line_nr, vlines in pairs(virtual_lines) do
vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, { vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, {
virt_lines = vlines, virt_lines = vlines,
virt_lines_above = false, virt_lines_above = false,
virt_lines_leftcol = true, virt_lines_leftcol = true,
}) })
end end
end end
---@private ---@private
function Renderer:render_hl(bufnr, hl) ---@param bufnr integer
---@param hl_range_args HighlightRangeArgs[]
function Renderer:render_hl(bufnr, hl_range_args)
if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then
return return
end end
vim.api.nvim_buf_clear_namespace(bufnr, namespace_highlights_id, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, namespace_highlights_id, 0, -1)
for _, data in ipairs(hl) do for _, args in ipairs(hl_range_args) do
if type(data[1]) == "table" then if vim.fn.has("nvim-0.11") == 1 and vim.hl and vim.hl.range then
for _, group in ipairs(data[1]) do vim.hl.range(bufnr, namespace_highlights_id, args.higroup, args.start, args.finish, {})
vim.api.nvim_buf_add_highlight(bufnr, namespace_highlights_id, group, data[2], data[3], data[4]) else
end vim.api.nvim_buf_add_highlight(bufnr, namespace_highlights_id, args.higroup, args.start[1], args.start[2], args.finish[2]) ---@diagnostic disable-line: deprecated
end end
end end
end end
function Renderer:draw() function Renderer:draw()
local bufnr = view.get_bufnr() local bufnr = self.explorer.view:get_bufnr("Renderer:draw")
if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then
return return
end end
local winid = self.explorer.view:get_winid(nil, "Renderer:draw")
local profile = log.profile_start("draw") local profile = log.profile_start("draw")
local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0) local cursor = vim.api.nvim_win_get_cursor(winid or 0)
icon_component.reset_config()
local builder = Builder:new(self.opts, self.explorer):build() local builder = Builder(self.explorer):build()
self:_draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks, builder.virtual_lines) self:_draw(bufnr, builder.lines, builder.hl_range_args, builder.signs, builder.extmarks, builder.virtual_lines)
if cursor and #builder.lines >= cursor[1] then if cursor and #builder.lines >= cursor[1] then
vim.api.nvim_win_set_cursor(view.get_winnr() or 0, cursor) vim.api.nvim_win_set_cursor(winid or 0, cursor)
end end
view.grow_from_content() self.explorer.view:grow_from_content()
log.profile_end(profile) log.profile_end(profile)
events._dispatch_on_tree_rendered(bufnr, view.get_winnr()) events._dispatch_on_tree_rendered(bufnr, winid)
end end
return Renderer return Renderer

View File

@@ -1,5 +1,4 @@
local Iterator = require("nvim-tree.iterators.node-iterator") local Iterator = require("nvim-tree.iterators.node-iterator")
local notify = require("nvim-tree.notify")
local M = { local M = {
debouncers = {}, debouncers = {},
@@ -59,6 +58,34 @@ function M.path_basename(path)
return path:sub(i + 1, #path) return path:sub(i + 1, #path)
end end
--- Check if there are parentheses before brackets, it causes problems for windows.
--- Refer to issue #2862 and #2961 for more details.
local function has_parentheses_and_brackets(path)
local _, i_parentheses = path:find("(", 1, true)
local _, i_brackets = path:find("[", 1, true)
if i_parentheses and i_brackets then
return true
end
return false
end
--- Path normalizations for windows only
local function win_norm_path(path)
if path == nil then
return path
end
local norm_path = path
-- Normalize for issue #2862 and #2961
if has_parentheses_and_brackets(norm_path) then
norm_path = norm_path:gsub("/", "\\")
end
-- Normalize the drive letter
norm_path = norm_path:gsub("^%l:", function(drive)
return drive:upper()
end)
return norm_path
end
--- Get a path relative to another path. --- Get a path relative to another path.
---@param path string ---@param path string
---@param relative_to string|nil ---@param relative_to string|nil
@@ -68,13 +95,18 @@ function M.path_relative(path, relative_to)
return path return path
end end
local _, r = path:find(M.path_add_trailing(relative_to), 1, true) local norm_path = path
local p = path if M.is_windows then
norm_path = win_norm_path(norm_path)
end
local _, r = norm_path:find(M.path_add_trailing(relative_to), 1, true)
local p = norm_path
if r then if r then
-- take the relative path starting after '/' -- take the relative path starting after '/'
-- if somehow given a completely matching path, -- if somehow given a completely matching path,
-- returns "" -- returns ""
p = path:sub(r + 1) p = norm_path:sub(r + 1)
end end
return p return p
end end
@@ -111,17 +143,22 @@ function M.find_node(nodes, fn)
return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes)
end) end)
:iterate() :iterate()
i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1
local explorer = require("nvim-tree.core").get_explorer() if node then
if explorer and explorer.live_filter.filter then if not node.explorer.view:is_root_folder_visible() then
i = i + 1 i = i - 1
end
if node.explorer.live_filter.filter then
i = i + 1
end
end end
return node, i return node, i
end end
-- Find the line number of a node. -- Find the line number of a node.
-- Return -1 is node is nil or not found. -- Return -1 is node is nil or not found.
---@param node Node|nil ---@param node Node?
---@return integer ---@return integer
function M.find_node_line(node) function M.find_node_line(node)
if not node then if not node then
@@ -141,6 +178,21 @@ function M.find_node_line(node)
return -1 return -1
end end
---@param extmarks vim.api.keyset.get_extmark_item[] as per vim.api.nvim_buf_get_extmarks
---@return number
function M.extmarks_length(extmarks)
local length = 0
for _, extmark in ipairs(extmarks) do
local details = extmark[4]
if details and details.virt_text then
for _, text in ipairs(details.virt_text) do
length = length + vim.fn.strchars(text[1])
end
end
end
return length
end
-- get the node in the tree state depending on the absolute path of the node -- get the node in the tree state depending on the absolute path of the node
-- (grouped or hidden too) -- (grouped or hidden too)
---@param path string ---@param path string
@@ -174,16 +226,6 @@ function M.get_node_from_path(path)
:iterate() :iterate()
end end
---Get the highest parent of grouped nodes
---@param node Node
---@return Node node or parent
function M.get_parent_of_group(node)
while node and node.parent and node.parent.group_next do
node = node.parent
end
return node
end
M.default_format_hidden_count = function(hidden_count, simple) M.default_format_hidden_count = function(hidden_count, simple)
local parts = {} local parts = {}
local total_count = 0 local total_count = 0
@@ -267,11 +309,51 @@ function M.rename_loaded_buffers(old_path, new_path)
end end
end end
local is_windows_drive = function(path)
return (M.is_windows) and (path:match("^%a:\\$") ~= nil)
end
---@param path string path to file or directory ---@param path string path to file or directory
---@return boolean ---@return boolean
function M.file_exists(path) function M.file_exists(path)
local _, error = vim.loop.fs_stat(path) if not (M.is_windows or M.is_wsl) then
return error == nil local _, error = vim.loop.fs_stat(path)
return error == nil
end
-- Windows is case-insensetive, but case-preserving
-- If a file's name is being changed into itself
-- with different casing, windows will falsely
-- report that file is already existing, so a hand-rolled
-- implementation of checking for existance is needed.
-- Same holds for WSL, since it can sometimes
-- access Windows files directly.
-- For more details see (#3117).
if is_windows_drive(path) then
return vim.fn.isdirectory(path) == 1
end
local parent = vim.fn.fnamemodify(path, ":h")
local filename = vim.fn.fnamemodify(path, ":t")
local handle = vim.loop.fs_scandir(parent)
if not handle then
-- File can not exist if its parent directory does not exist
return false
end
while true do
local name, _ = vim.loop.fs_scandir_next(handle)
if not name then
break
end
if name == filename then
return true
end
end
return false
end end
---@param path string ---@param path string
@@ -283,6 +365,14 @@ function M.canonical_path(path)
return path return path
end end
--- Escapes special characters in string for windows, refer to issue #2862 and #2961 for more details.
local function escape_special_char_for_windows(path)
if has_parentheses_and_brackets(path) then
return path:gsub("\\", "/"):gsub("/ ", "\\ ")
end
return path:gsub("%(", "\\("):gsub("%)", "\\)")
end
--- Escapes special characters in string if windows else returns unmodified string. --- Escapes special characters in string if windows else returns unmodified string.
---@param path string ---@param path string
---@return string|nil ---@return string|nil
@@ -290,7 +380,7 @@ function M.escape_special_chars(path)
if path == nil then if path == nil then
return path return path
end end
return M.is_windows and path:gsub("\\", "/") or path return M.is_windows and escape_special_char_for_windows(path) or path
end end
--- Create empty sub-tables if not present --- Create empty sub-tables if not present
@@ -319,20 +409,6 @@ end
---@param dst_pos string value pos ---@param dst_pos string value pos
---@param remove boolean ---@param remove boolean
function M.move_missing_val(src, src_path, src_pos, dst, dst_path, dst_pos, remove) function M.move_missing_val(src, src_path, src_pos, dst, dst_path, dst_pos, remove)
local ok, err = pcall(vim.validate, {
src = { src, "table" },
src_path = { src_path, "string" },
src_pos = { src_pos, "string" },
dst = { dst, "table" },
dst_path = { dst_path, "string" },
dst_pos = { dst_pos, "string" },
remove = { remove, "boolean" },
})
if not ok then
notify.warn("move_missing_val: " .. (err or "invalid arguments"))
return
end
for pos in string.gmatch(src_path, "([^%.]+)%.*") do for pos in string.gmatch(src_path, "([^%.]+)%.*") do
if src[pos] and type(src[pos]) == "table" then if src[pos] and type(src[pos]) == "table" then
src = src[pos] src = src[pos]
@@ -467,13 +543,16 @@ function M.focus_file(path)
local _, i = M.find_node(require("nvim-tree.core").get_explorer().nodes, function(node) local _, i = M.find_node(require("nvim-tree.core").get_explorer().nodes, function(node)
return node.absolute_path == path return node.absolute_path == path
end) end)
require("nvim-tree.view").set_cursor({ i + 1, 1 }) local explorer = require("nvim-tree.core").get_explorer()
if explorer then
explorer.view:set_cursor({ i + 1, 1 })
end
end end
---Focus node passed as parameter if visible, otherwise focus first visible parent. ---Focus node passed as parameter if visible, otherwise focus first visible parent.
---If none of the parents is visible focus root. ---If none of the parents is visible focus root.
---If node is nil do nothing. ---If node is nil do nothing.
---@param node Node|nil node to focus ---@param node Node? node to focus
function M.focus_node_or_parent(node) function M.focus_node_or_parent(node)
local explorer = require("nvim-tree.core").get_explorer() local explorer = require("nvim-tree.core").get_explorer()
@@ -487,7 +566,7 @@ function M.focus_node_or_parent(node)
end) end)
if found_node or node.parent == nil then if found_node or node.parent == nil then
require("nvim-tree.view").set_cursor({ i + 1, 1 }) explorer.view:set_cursor({ i + 1, 1 })
break break
end end
@@ -549,14 +628,6 @@ function M.array_remove_nils(array)
end, array) end, array)
end end
---@param f fun(node: Node|nil)
---@return function
function M.inject_node(f)
return function()
f(require("nvim-tree.lib").get_node_at_cursor())
end
end
--- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file. --- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file.
--- This is cheap, as the readable test should only ever be needed when resuming a vim session. --- This is cheap, as the readable test should only ever be needed when resuming a vim session.
---@param bufnr number|nil may be 0 or nil for current ---@param bufnr number|nil may be 0 or nil for current
@@ -578,4 +649,41 @@ function M.is_nvim_tree_buf(bufnr)
return false return false
end end
--- path is an executable file or directory
---@param absolute_path string
---@return boolean
function M.is_executable(absolute_path)
if M.is_windows or M.is_wsl then
--- executable detection on windows is buggy and not performant hence it is disabled
return false
else
return vim.loop.fs_access(absolute_path, "X") or false
end
end
---@class UtilEnumerateOptionsOpts
---@field keyset_opts vim.api.keyset.option
---@field was_set boolean? as per vim.api.keyset.get_option_info
---Option name/values
---@param opts UtilEnumerateOptionsOpts
---@return table<string, any>
function M.enumerate_options(opts)
-- enumerate all options, limiting buf and win scopes
return vim.tbl_map(function(info)
if opts.keyset_opts.buf and info.scope ~= "buf" then
return nil
elseif opts.keyset_opts.win and info.scope ~= "win" then
return nil
else
-- optional, lazy was_set check
if not opts.was_set or vim.api.nvim_get_option_info2(info.name, opts.keyset_opts).was_set then
return vim.api.nvim_get_option_value(info.name, opts.keyset_opts)
else
return nil
end
end
end, vim.api.nvim_get_all_options_info())
end
return M return M

View File

@@ -1,615 +0,0 @@
local events = require("nvim-tree.events")
local utils = require("nvim-tree.utils")
local log = require("nvim-tree.log")
local notify = require("nvim-tree.notify")
---@class OpenInWinOpts
---@field hijack_current_buf boolean|nil default true
---@field resize boolean|nil default true
---@field winid number|nil 0 or nil for current
local M = {}
local DEFAULT_MIN_WIDTH = 30
local DEFAULT_MAX_WIDTH = -1
local DEFAULT_PADDING = 1
M.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",
"FloatBorder:NvimTreeNormalFloatBorder",
}, ","),
},
}
-- 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(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(M.get_bufnr(), "NvimTree_" .. tab)
for option, value in pairs(BUFFER_OPTIONS) do
vim.bo[M.get_bufnr()][option] = value
end
require("nvim-tree.keymap").on_attach(M.get_bufnr())
events._dispatch_tree_attached_post(M.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
local function get_width(size)
if size then
return get_size(size)
else
return get_size(M.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(tabpage)
local winnr = vim.api.nvim_get_current_win()
M.View.tabpages[tabpage] = vim.tbl_extend("force", M.View.tabpages[tabpage] or tabinitial, { winnr = winnr })
end
local function set_window_options_and_buffer()
pcall(vim.api.nvim_command, "buffer " .. M.get_bufnr())
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore = "all"
for k, v in pairs(M.View.winopts) do
vim.opt_local[k] = v
end
vim.opt.eventignore = eventignore
end
---@return table
local function open_win_config()
if type(M.View.float.open_win_config) == "function" then
return M.View.float.open_win_config()
else
return M.View.float.open_win_config
end
end
local function open_window()
if M.View.float.enable then
vim.api.nvim_open_win(0, true, open_win_config())
else
vim.api.nvim_command("vsp")
M.reposition_window()
end
setup_tabpage(vim.api.nvim_get_current_tabpage())
set_window_options_and_buffer()
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(tabnr)
local tabpage = tabnr or vim.api.nvim_get_current_tabpage()
M.View.cursors[tabpage] = vim.api.nvim_win_get_cursor(M.get_winnr(tabpage) or 0)
end
---@param tabpage integer
local function close(tabpage)
if not M.is_visible({ tabpage = tabpage }) then
return
end
save_tab_state(tabpage)
switch_buf_if_last_buf()
local tree_win = M.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
local success, error = pcall(vim.api.nvim_win_close, tree_win or 0, true)
if not success then
notify.debug("Failed to close window: " .. error)
return
end
end
events._dispatch_on_tree_close()
return
end
end
end
function M.close_this_tab_only()
close(vim.api.nvim_get_current_tabpage())
end
function M.close_all_tabs()
for tabpage, _ in pairs(M.View.tabpages) do
close(tabpage)
end
end
function M.close()
if M.View.tab.sync.close then
M.close_all_tabs()
else
M.close_this_tab_only()
end
end
---@param options table|nil
function M.open(options)
if M.is_visible() then
return
end
local profile = log.profile_start("view open")
create_buffer()
open_window()
M.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()
local starts_at = M.is_root_folder_visible(require("nvim-tree.core").get_cwd()) and 1 or 0
local lines = vim.api.nvim_buf_get_lines(M.get_bufnr(), starts_at, -1, false)
-- number of columns of right-padding to indicate end of path
local padding = get_size(M.View.padding)
-- account for sign/number columns etc.
local wininfo = vim.fn.getwininfo(M.get_winnr())
if type(wininfo) == "table" and type(wininfo[1]) == "table" then
padding = padding + wininfo[1].textoff
end
local resizing_width = M.View.initial_width - padding
local max_width
-- maybe bound max
if M.View.max_width == -1 then
max_width = -1
else
max_width = get_width(M.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 M.View.adaptive_size and max_width >= 0 and resizing_width >= max_width then
resizing_width = max_width
break
end
end
M.resize(resizing_width + padding)
end
function M.grow_from_content()
if M.View.adaptive_size then
grow()
end
end
---@param size string|number|nil
function M.resize(size)
if M.View.float.enable and not M.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 = M.View.width + size
end
end
if type(size) == "number" and size <= 0 then
return
end
if size then
M.View.width = size
M.View.height = size
end
if not M.is_visible() then
return
end
local winnr = M.get_winnr() or 0
local new_size = get_width()
if new_size ~= vim.api.nvim_win_get_width(winnr) then
vim.api.nvim_win_set_width(winnr, new_size)
if not M.View.preserve_window_proportions then
vim.cmd(":wincmd =")
end
end
events._dispatch_on_tree_resize(new_size)
end
function M.reposition_window()
local move_to = move_tbl[M.View.side]
vim.api.nvim_command("wincmd " .. move_to)
M.resize()
end
local function set_current_win()
local current_tab = vim.api.nvim_get_current_tabpage()
M.View.tabpages[current_tab].winnr = vim.api.nvim_get_current_win()
end
---Open the tree in the a window
---@param opts OpenInWinOpts|nil
function M.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(opts.hijack_current_buf and vim.api.nvim_get_current_buf())
setup_tabpage(vim.api.nvim_get_current_tabpage())
set_current_win()
set_window_options_and_buffer()
if opts.resize then
M.reposition_window()
M.resize()
end
end
function M.abandon_current_window()
local tab = vim.api.nvim_get_current_tabpage()
BUFNR_PER_TAB[tab] = nil
if M.View.tabpages[tab] then
M.View.tabpages[tab].winnr = nil
end
end
function M.abandon_all_windows()
for tab, _ in pairs(vim.api.nvim_list_tabpages()) do
BUFNR_PER_TAB[tab] = nil
if M.View.tabpages[tab] then
M.View.tabpages[tab].winnr = nil
end
end
end
---@param opts table|nil
function M.is_visible(opts)
if opts and opts.tabpage then
if M.View.tabpages[opts.tabpage] == nil then
return false
end
local winnr = M.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(M.View.tabpages) do
if v.winnr and vim.api.nvim_win_is_valid(v.winnr) then
return true
end
end
return false
end
return M.get_winnr() ~= nil and vim.api.nvim_win_is_valid(M.get_winnr() or 0)
end
---@param opts table|nil
function M.set_cursor(opts)
if M.is_visible() then
pcall(vim.api.nvim_win_set_cursor, M.get_winnr(), opts)
end
end
---@param winnr number|nil
---@param open_if_closed boolean|nil
function M.focus(winnr, open_if_closed)
local wnr = winnr or M.get_winnr()
if vim.api.nvim_win_get_tabpage(wnr or 0) ~= vim.api.nvim_win_get_tabpage(0) then
M.close()
M.open()
wnr = M.get_winnr()
elseif open_if_closed and not M.is_visible() then
M.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 M.winid(opts)
local tabpage = opts and opts.tabpage
if tabpage == 0 then
tabpage = vim.api.nvim_get_current_tabpage()
end
if M.is_visible({ tabpage = tabpage }) then
return M.get_winnr(tabpage)
else
return nil
end
end
--- Restores the state of a NvimTree window if it was initialized before.
function M.restore_tab_state()
local tabpage = vim.api.nvim_get_current_tabpage()
M.set_cursor(M.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 M.get_winnr(tabpage)
tabpage = tabpage or vim.api.nvim_get_current_tabpage()
local tabinfo = M.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 M.get_bufnr()
return BUFNR_PER_TAB[vim.api.nvim_get_current_tabpage()]
end
---@param bufnr number
---@return boolean
function M.is_buf_valid(bufnr)
return bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
end
function M._prevent_buffer_override()
local view_winnr = M.get_winnr()
local view_bufnr = M.get_bufnr()
-- 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(M.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 })
local explorer = require("nvim-tree.core").get_explorer()
if explorer then
explorer.renderer:draw()
end
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 M.is_root_folder_visible(cwd)
return cwd ~= "/" and not M.View.hide_root_folder
end
-- used on ColorScheme event
function M.reset_winhl()
local winnr = M.get_winnr()
if winnr and vim.api.nvim_win_is_valid(winnr) then
vim.wo[M.get_winnr()].winhl = M.View.winopts.winhl
end
end
---Check if width determined or calculated on-fly
---@return boolean
function M.is_width_determined()
return type(M.View.width) ~= "function"
end
---Configure width-related config
---@param width string|function|number|table|nil
function M.configure_width(width)
if type(width) == "table" then
M.View.adaptive_size = true
M.View.width = width.min or DEFAULT_MIN_WIDTH
M.View.max_width = width.max or DEFAULT_MAX_WIDTH
M.View.padding = width.padding or DEFAULT_PADDING
elseif width == nil then
if M.config.width ~= nil then
-- if we had input config - fallback to it
M.configure_width(M.config.width)
else
-- otherwise - restore initial width
M.View.width = M.View.initial_width
end
else
M.View.adaptive_size = false
M.View.width = width
end
end
function M.setup(opts)
local options = opts.view or {}
M.View.centralize_selection = options.centralize_selection
M.View.side = (options.side == "right") and "right" or "left"
M.View.height = options.height
M.View.hide_root_folder = opts.renderer.root_folder_label == false
M.View.tab = opts.tab
M.View.preserve_window_proportions = options.preserve_window_proportions
M.View.winopts.cursorline = options.cursorline
M.View.winopts.number = options.number
M.View.winopts.relativenumber = options.relativenumber
M.View.winopts.signcolumn = options.signcolumn
M.View.float = options.float
M.on_attach = opts.on_attach
M.config = options
M.configure_width(options.width)
M.View.initial_width = get_width()
end
return M

View File

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

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