diff --git a/init.lua b/init.lua index b633ba5..e432091 100644 --- a/init.lua +++ b/init.lua @@ -2,3 +2,4 @@ vim.cmd("runtime vimrc") require('ant_tabcomplete') require('ant_lsp') +require('ant_nvimtree') diff --git a/lua/ant_nvimtree.lua b/lua/ant_nvimtree.lua new file mode 100644 index 0000000..5223aa0 --- /dev/null +++ b/lua/ant_nvimtree.lua @@ -0,0 +1,9 @@ +-- disable netrw at the very start of your init.lua +vim.g.loaded_netrw = 1 +vim.g.loaded_netrwPlugin = 1 + +-- optionally enable 24-bit colour +vim.opt.termguicolors = true + +-- empty setup using defaults +require("nvim-tree").setup() diff --git a/pack/ant/start/nvim-tree.lua/.editorconfig b/pack/ant/start/nvim-tree.lua/.editorconfig new file mode 100644 index 0000000..577eb1d --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +insert_final_newline = true +end_of_line = lf + +[nvim-tree-lua.txt] +max_line_length = 78 + +[*.lua] +indent_style = space +max_line_length = 140 +indent_size = 2 + +# EmmyLuaCodeStyle specific, see +# https://github.com/CppCXY/EmmyLuaCodeStyle/blob/master/lua.template.editorconfig +continuation_indent = 2 +quote_style = double +call_arg_parentheses = always +space_before_closure_open_parenthesis = false +align_continuous_similar_call_args = true diff --git a/pack/ant/start/nvim-tree.lua/.github/FUNDING.yml b/pack/ant/start/nvim-tree.lua/.github/FUNDING.yml new file mode 100644 index 0000000..1b07162 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: kyazdani42 diff --git a/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/bug_report.yml b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..26dfa3a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,86 @@ +name: Bug report +description: Report a problem with nvim-tree +labels: [bug] +body: + - type: markdown + attributes: + value: | + Is this a question? + * Please start a new [Q&A discussion](https://github.com/nvim-tree/nvim-tree.lua/discussions/new) instead of raising a bug. + + Before reporting: + * search [existing issues](https://github.com/nvim-tree/nvim-tree.lua/issues) + * ensure that nvim-tree is updated to the latest version + + If you are experiencing performance issues, please [enable profiling](https://github.com/nvim-tree/nvim-tree.lua#performance-issues) and attach the logs. + + 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 resolution. + - type: textarea + attributes: + label: "Description" + description: "A short description of the problem you are reporting." + validations: + required: true + - type: textarea + attributes: + label: "Neovim version" + description: "Output of `nvim --version`. Please see nvim-tree.lua [minimum required version](https://github.com/nvim-tree/nvim-tree.lua#notice)." + placeholder: | + NVIM v0.6.1 + Build type: Release + LuaJIT 2.1.0-beta3 + render: text + validations: + required: true + - type: input + attributes: + label: "Operating system and version" + placeholder: "Linux 5.16.11-arch1-1, macOS 11.5, Windows 10" + validations: + required: true + - type: input + attributes: + label: "Windows variant" + placeholder: "WSL, PowerShell, cygwin, msys" + validations: + required: false + - type: input + attributes: + label: "nvim-tree version" + description: "`cd /nvim-tree.lua ; git log --format='%h' -n 1`" + placeholder: | + nvim-tree branch, commit or tag number + validations: + required: true + - type: textarea + attributes: + label: "Clean room replication" + description: "Minimal(!) configuration necessary to reproduce the issue. + + If not provided it is very unlikely that the nvim-tree team will be able to address your issue. + + See [wiki: Clean Room Replication](https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting#clean-room-replication) for instructions and paste the contents of your `/tmp/nvt-min.lua` here. + + Please do NOT post a configuration that uses other plugin managers such as lazy, see [wiki: Lazy Loading](https://github.com/nvim-tree/nvim-tree.lua/wiki/Installation#lazy-loading)" + render: lua + validations: + required: true + - type: textarea + attributes: + label: "Steps to reproduce" + description: "Steps to reproduce using the minimal config provided below." + placeholder: | + 1. nvim -nu /tmp/nvt-min.lua + 2. :NvimTreeOpen + 3. ... + validations: + required: true + - type: textarea + attributes: + label: "Expected behavior" + description: "A description of the behavior you expected:" + - type: textarea + attributes: + label: "Actual behavior" + description: "Observed behavior (may optionally include images, videos or a screencast)." + diff --git a/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/feature_request.md b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..35429c9 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature request +assignees: '' + +--- +**Is this a question?** +Please start a new [Q&A discussion](https://github.com/nvim-tree/nvim-tree.lua/discussions/new) instead of raising a feature request. + +**Can this functionality be implemented utilising API?** +nvim-tree exposes extensive API (see `:h nvim-tree-api`). Can it be used to achieve your goal? Is there a missing API that would make it possible? +Given stable status of nvim-tree it's preferred to add new API than new functionality. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/nvt-min.lua b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/nvt-min.lua new file mode 100644 index 0000000..858768c --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/ISSUE_TEMPLATE/nvt-min.lua @@ -0,0 +1,51 @@ +vim.g.loaded_netrw = 1 +vim.g.loaded_netrwPlugin = 1 + +vim.cmd([[set runtimepath=$VIMRUNTIME]]) +vim.cmd([[set packpath=/tmp/nvt-min/site]]) +local package_root = "/tmp/nvt-min/site/pack" +local install_path = package_root .. "/packer/start/packer.nvim" +local function load_plugins() + require("packer").startup({ + { + "wbthomason/packer.nvim", + "nvim-tree/nvim-tree.lua", + "nvim-tree/nvim-web-devicons", + -- ADD PLUGINS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE + }, + config = { + package_root = package_root, + compile_path = install_path .. "/plugin/packer_compiled.lua", + display = { non_interactive = true }, + }, + }) +end +if vim.fn.isdirectory(install_path) == 0 then + print("Installing nvim-tree and dependencies.") + vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path }) +end +load_plugins() +require("packer").sync() +vim.cmd([[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]]) +vim.opt.termguicolors = true +vim.opt.cursorline = true + +-- MODIFY NVIM-TREE SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE +_G.setup = function() + require("nvim-tree").setup({}) +end + +-- UNCOMMENT this block for diagnostics issues, substituting pattern and cmd as appropriate. +-- Requires diagnostics.enable = true in setup. +--[[ +vim.api.nvim_create_autocmd("FileType", { + pattern = "lua", + callback = function() + vim.lsp.start { + name = "my-luals", + cmd = { "lua-language-server" }, + root_dir = vim.loop.cwd(), + } + end, +}) +]] diff --git a/pack/ant/start/nvim-tree.lua/.github/dependabot.yml b/pack/ant/start/nvim-tree.lua/.github/dependabot.yml new file mode 100644 index 0000000..a50f2b3 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + reviewers: + - "gegoune" diff --git a/pack/ant/start/nvim-tree.lua/.github/workflows/ci.yml b/pack/ant/start/nvim-tree.lua/.github/workflows/ci.yml new file mode 100644 index 0000000..d788554 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ matrix.lua_version }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + + strategy: + matrix: + lua_version: [ 5.1 ] + + steps: + - uses: actions/checkout@v4 + + - uses: leafo/gh-actions-lua@v10 + with: + luaVersion: ${{ matrix.lua_version }} + + - uses: leafo/gh-actions-luarocks@v4 + + - run: luarocks install luacheck 1.1.1 + + - 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: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ matrix.nvim_version }}-${{ matrix.luals_version }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + + strategy: + matrix: + nvim_version: [ stable, nightly ] + luals_version: [ 3.11.0 ] + + steps: + - uses: actions/checkout@v4 + + - uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: ${{ matrix.nvim_version }} + + - name: install luals + run: | + 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 + + - run: echo "luals/bin" >> "$GITHUB_PATH" + + - name: make check + env: + VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime + run: make check + + - run: make help-check diff --git a/pack/ant/start/nvim-tree.lua/.github/workflows/luarocks-release.yml b/pack/ant/start/nvim-tree.lua/.github/workflows/luarocks-release.yml new file mode 100644 index 0000000..a4e5d13 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/workflows/luarocks-release.yml @@ -0,0 +1,37 @@ +name: Luarocks Release +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: +jobs: + luarocks-upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: LuaRocks Upload + uses: nvim-neorocks/luarocks-tag-release@v7 + env: + LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} + with: + summary: A File Explorer For Neovim + detailed_description: | + Automatic updates + + File type icons + + Git integration + + Diagnostics integration - LSP and COC + + (Live) filtering + + Cut, copy, paste, rename, delete, create etc. + + Highly customisable + + Rich API + license: "GPL-3.0" + labels: neovim + dependencies: | + nvim-web-devicons diff --git a/pack/ant/start/nvim-tree.lua/.github/workflows/release-please.yml b/pack/ant/start/nvim-tree.lua/.github/workflows/release-please.yml new file mode 100644 index 0000000..5de026a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/workflows/release-please.yml @@ -0,0 +1,37 @@ +on: + push: + branches: + - master + workflow_dispatch: +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true +name: release-please +permissions: + contents: write + pull-requests: write +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v4 + id: release + - uses: actions/checkout@v4 + - name: tag major and minor versions + if: ${{ steps.release.outputs.release_created }} + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git remote add gh-token "https://${{ secrets.GITHUB_TOKEN }}@github.com/google-github-actions/release-please-action.git" + git tag -d v${{ steps.release.outputs.major }} || true + git tag -d v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} || true + git tag -d v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} || true + git push origin :v${{ steps.release.outputs.major }} || true + git push origin :v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} || true + git push origin :v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} || true + git tag -a v${{ steps.release.outputs.major }} -m "Release v${{ steps.release.outputs.major }}" + git tag -a v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} -m "Release v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}" + git tag -a v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} -m "Release v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}" + git push origin v${{ steps.release.outputs.major }} + git push origin v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }} + git push origin v${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }} diff --git a/pack/ant/start/nvim-tree.lua/.github/workflows/semantic-pr-subject.yml b/pack/ant/start/nvim-tree.lua/.github/workflows/semantic-pr-subject.yml new file mode 100644 index 0000000..66e3419 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.github/workflows/semantic-pr-subject.yml @@ -0,0 +1,19 @@ +name: Semantic Pull Request Subject +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true +jobs: + semantic-pr-subject: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5.5.3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pack/ant/start/nvim-tree.lua/.gitignore b/pack/ant/start/nvim-tree.lua/.gitignore new file mode 100644 index 0000000..faee0c4 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.gitignore @@ -0,0 +1,4 @@ +/luals-out/ +/luals/ +# backup vim files +*~ diff --git a/pack/ant/start/nvim-tree.lua/.hooks/pre-commit.sh b/pack/ant/start/nvim-tree.lua/.hooks/pre-commit.sh new file mode 100755 index 0000000..5b139ec --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.hooks/pre-commit.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +make diff --git a/pack/ant/start/nvim-tree.lua/.luacheckrc b/pack/ant/start/nvim-tree.lua/.luacheckrc new file mode 100644 index 0000000..267d0ce --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.luacheckrc @@ -0,0 +1,15 @@ +local M = {} + +-- Don't report unused self arguments of methods. +M.self = false + +M.ignore = { + "631", -- max_line_length +} + +-- Global objects defined by the C code +M.globals = { + "vim", +} + +return M diff --git a/pack/ant/start/nvim-tree.lua/.luarc.json b/pack/ant/start/nvim-tree.lua/.luarc.json new file mode 100644 index 0000000..bb0a84b --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.luarc.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "runtime.version.luals-check-only": "Lua 5.1", + "workspace": { + "library": [ + "$VIMRUNTIME/lua/vim", + "${3rd}/luv/library" + ] + }, + "diagnostics": { + "libraryFiles": "Disable", + "globals": [], + "neededFileStatus": { + "ambiguity-1": "Any", + "assign-type-mismatch": "Any", + "await-in-sync": "Any", + "cast-local-type": "Any", + "cast-type-mismatch": "Any", + "circle-doc-class": "Any", + "close-non-object": "Any", + "code-after-break": "Any", + "codestyle-check": "None", + "count-down-loop": "Any", + "deprecated": "Any", + "different-requires": "Any", + "discard-returns": "Any", + "doc-field-no-class": "Any", + "duplicate-doc-alias": "Any", + "duplicate-doc-field": "Any", + "duplicate-doc-param": "Any", + "duplicate-index": "Any", + "duplicate-set-field": "Any", + "empty-block": "Any", + "global-element": "Any", + "global-in-nil-env": "Any", + "incomplete-signature-doc": "Any", + "inject-field": "Any", + "invisible": "Any", + "lowercase-global": "Any", + "missing-fields": "Any", + "missing-global-doc": "Any", + "missing-local-export-doc": "Any", + "missing-parameter": "Any", + "missing-return": "Any", + "missing-return-value": "Any", + "name-style-check": "None", + "need-check-nil": "Any", + "newfield-call": "Any", + "newline-call": "Any", + "no-unknown": "None", + "not-yieldable": "Any", + "param-type-mismatch": "Any", + "redefined-local": "Any", + "redundant-parameter": "Any", + "redundant-return": "Any", + "redundant-return-value": "Any", + "redundant-value": "Any", + "return-type-mismatch": "Any", + "spell-check": "None", + "trailing-space": "Any", + "unbalanced-assignments": "Any", + "undefined-doc-class": "Any", + "undefined-doc-name": "Any", + "undefined-doc-param": "Any", + "undefined-env-child": "Any", + "undefined-field": "None", + "undefined-global": "Any", + "unknown-cast-variable": "Any", + "unknown-diag-code": "Any", + "unknown-operator": "Any", + "unreachable-code": "Any", + "unused-function": "Any", + "unused-label": "Any", + "unused-local": "Any", + "unused-vararg": "Any" + } + } +} diff --git a/pack/ant/start/nvim-tree.lua/.release-please-manifest.json b/pack/ant/start/nvim-tree.lua/.release-please-manifest.json new file mode 100644 index 0000000..4fcfdf7 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.9.0" +} diff --git a/pack/ant/start/nvim-tree.lua/CHANGELOG.md b/pack/ant/start/nvim-tree.lua/CHANGELOG.md new file mode 100644 index 0000000..7674333 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/CHANGELOG.md @@ -0,0 +1,248 @@ +# Changelog + +## [1.9.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.8.0...nvim-tree-v1.9.0) (2024-12-07) + + +### Features + +* **#2948:** add custom decorators, :help nvim-tree-decorators ([#2996](https://github.com/nvim-tree/nvim-tree.lua/issues/2996)) ([7a4ff1a](https://github.com/nvim-tree/nvim-tree.lua/commit/7a4ff1a516fe92a5ed6b79d7ce31ea4d8f341a72)) + + +### Bug Fixes + +* **#2954:** more efficient LSP updates, increase diagnostics.debounce_delay from 50ms to 500ms ([#3007](https://github.com/nvim-tree/nvim-tree.lua/issues/3007)) ([1f3ffd6](https://github.com/nvim-tree/nvim-tree.lua/commit/1f3ffd6af145af2a4930a61c50f763264922c3fe)) +* **#2990:** Do not check if buffer is buflisted in diagnostics.update() ([#2998](https://github.com/nvim-tree/nvim-tree.lua/issues/2998)) ([28eac28](https://github.com/nvim-tree/nvim-tree.lua/commit/28eac2801b201f301449e976d7a9e8cfde053ba3)) +* **#3009:** nvim < 0.10 apply view options locally ([#3010](https://github.com/nvim-tree/nvim-tree.lua/issues/3010)) ([ca7c4c3](https://github.com/nvim-tree/nvim-tree.lua/commit/ca7c4c33cac2ad66ec69d45e465379716ef0cc97)) +* **api:** correct argument types in `wrap_node` and `wrap_node_or_nil` ([#3006](https://github.com/nvim-tree/nvim-tree.lua/issues/3006)) ([f7c65e1](https://github.com/nvim-tree/nvim-tree.lua/commit/f7c65e11d695a084ca10b93df659bb7e68b71f9f)) + +## [1.8.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.1...nvim-tree-v1.8.0) (2024-11-09) + + +### Features + +* **#2819:** add actions.open_file.relative_path, default enabled, following successful experiment ([#2995](https://github.com/nvim-tree/nvim-tree.lua/issues/2995)) ([2ee1c5e](https://github.com/nvim-tree/nvim-tree.lua/commit/2ee1c5e17fdfbf5013af31b1410e4a5f28f4cadd)) +* **#2938:** add default filesystem_watchers.ignore_dirs = { "/.ccls-cache", "/build", "/node_modules", "/target", } ([#2940](https://github.com/nvim-tree/nvim-tree.lua/issues/2940)) ([010ae03](https://github.com/nvim-tree/nvim-tree.lua/commit/010ae0365aafd6275c478d932515d2e8e897b7bb)) + + +### Bug Fixes + +* **#2945:** stack overflow on api.git.reload or fugitive event with watchers disabled ([#2949](https://github.com/nvim-tree/nvim-tree.lua/issues/2949)) ([5ad8762](https://github.com/nvim-tree/nvim-tree.lua/commit/5ad87620ec9d1190d15c88171a3f0122bc16b0fe)) +* **#2947:** root is never a dotfile, so that it doesn't propagate to children ([#2958](https://github.com/nvim-tree/nvim-tree.lua/issues/2958)) ([f5f6789](https://github.com/nvim-tree/nvim-tree.lua/commit/f5f67892996b280ae78b1b0a2d07c4fa29ae0905)) +* **#2951:** highlights incorrect following cancelled pick ([#2952](https://github.com/nvim-tree/nvim-tree.lua/issues/2952)) ([1c9553a](https://github.com/nvim-tree/nvim-tree.lua/commit/1c9553a19f70df3dcb171546a3d5e034531ef093)) +* **#2954:** resolve occasional tree flashing on diagnostics, set tree buffer options in deterministic order ([#2980](https://github.com/nvim-tree/nvim-tree.lua/issues/2980)) ([82ab19e](https://github.com/nvim-tree/nvim-tree.lua/commit/82ab19ebf79c1839d7351f2fed213d1af13a598e)) +* **#2961:** windows: escape brackets and parentheses when opening file ([#2962](https://github.com/nvim-tree/nvim-tree.lua/issues/2962)) ([63c7ad9](https://github.com/nvim-tree/nvim-tree.lua/commit/63c7ad9037fb7334682dd0b3a177cee25c5c8a0f)) +* **#2969:** After a rename, the node loses selection ([#2974](https://github.com/nvim-tree/nvim-tree.lua/issues/2974)) ([1403933](https://github.com/nvim-tree/nvim-tree.lua/commit/14039337a563f4efd72831888f332a15585f0ea1)) +* **#2972:** error on :colorscheme ([#2973](https://github.com/nvim-tree/nvim-tree.lua/issues/2973)) ([6e5a204](https://github.com/nvim-tree/nvim-tree.lua/commit/6e5a204ca659bb8f2a564df75df2739edec03cb0)) +* **#2976:** use vim.loop to preserve neovim 0.9 compatibility ([#2977](https://github.com/nvim-tree/nvim-tree.lua/issues/2977)) ([00dff48](https://github.com/nvim-tree/nvim-tree.lua/commit/00dff482f9a8fb806a54fd980359adc6cd45d435)) +* **#2978:** grouped folder not showing closed icon ([#2979](https://github.com/nvim-tree/nvim-tree.lua/issues/2979)) ([120ba58](https://github.com/nvim-tree/nvim-tree.lua/commit/120ba58254835d412bbc91cffe847e9be835fadd)) +* **#2981:** windows: root changed when navigating with LSP ([#2982](https://github.com/nvim-tree/nvim-tree.lua/issues/2982)) ([c22124b](https://github.com/nvim-tree/nvim-tree.lua/commit/c22124b37409bee6d1a0da77f4f3a1526f7a204d)) +* symlink file icons rendered when renderer.icons.show.file = false, folder.symlink* was incorrectly rendered as folder.default|open ([#2983](https://github.com/nvim-tree/nvim-tree.lua/issues/2983)) ([2156bc0](https://github.com/nvim-tree/nvim-tree.lua/commit/2156bc08c982d3c4b4cfc2b8fd7faeff58a88e10)) + +## [1.7.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.7.0...nvim-tree-v1.7.1) (2024-09-30) + + +### Bug Fixes + +* **#2794:** sshfs compatibility ([#2922](https://github.com/nvim-tree/nvim-tree.lua/issues/2922)) ([9650e73](https://github.com/nvim-tree/nvim-tree.lua/commit/9650e735baad0d39505f4cb4867a60f02858536a)) +* **#2928:** nil explorer in parent move action ([#2929](https://github.com/nvim-tree/nvim-tree.lua/issues/2929)) ([0429f28](https://github.com/nvim-tree/nvim-tree.lua/commit/0429f286b350c65118d66b646775bf187936fa47)) +* **#2930:** empty groups expanded on reload ([#2935](https://github.com/nvim-tree/nvim-tree.lua/issues/2935)) ([4520c03](https://github.com/nvim-tree/nvim-tree.lua/commit/4520c0355cc561830ee2cf90dc37a2a75abf7995)) +* invalid explorer on open ([#2927](https://github.com/nvim-tree/nvim-tree.lua/issues/2927)) ([59a8a6a](https://github.com/nvim-tree/nvim-tree.lua/commit/59a8a6ae5e9d3eae99d08ab655d12fd51d5d17f3)) + + +### Reverts + +* **#2794:** sshfs compatibility ([#2920](https://github.com/nvim-tree/nvim-tree.lua/issues/2920)) ([8405ecf](https://github.com/nvim-tree/nvim-tree.lua/commit/8405ecfbd6bb08a94ffc9c68fef211eea56e8a3b)) + +## [1.7.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.6.1...nvim-tree-v1.7.0) (2024-09-21) + + +### Features + +* **#2430:** use vim.ui.open as default system_open, for neovim 0.10+ ([#2912](https://github.com/nvim-tree/nvim-tree.lua/issues/2912)) ([03f737e](https://github.com/nvim-tree/nvim-tree.lua/commit/03f737e5744a2b3ebb4b086f7636a3399224ec0c)) +* help closes on <Esc> and api.tree.toggle_help mappings ([#2909](https://github.com/nvim-tree/nvim-tree.lua/issues/2909)) ([b652dbd](https://github.com/nvim-tree/nvim-tree.lua/commit/b652dbd0e0489c5fbb81fbededf0d99029cd2f38)) + + +### Bug Fixes + +* **#2862:** windows path replaces backslashes with forward slashes ([#2903](https://github.com/nvim-tree/nvim-tree.lua/issues/2903)) ([45a93d9](https://github.com/nvim-tree/nvim-tree.lua/commit/45a93d99794fff3064141d5b3a50db98ce352697)) +* **#2906:** resource leak on populate children ([#2907](https://github.com/nvim-tree/nvim-tree.lua/issues/2907)) ([a4dd5ad](https://github.com/nvim-tree/nvim-tree.lua/commit/a4dd5ad5c8f9349142291d24e0e6466995594b9a)) +* **#2917:** fix root copy paths: Y, ge, gy, y ([#2918](https://github.com/nvim-tree/nvim-tree.lua/issues/2918)) ([b18ce8b](https://github.com/nvim-tree/nvim-tree.lua/commit/b18ce8be8f162eee0bc37addcfe17d7d019fcec7)) +* safely close last tree window ([#2913](https://github.com/nvim-tree/nvim-tree.lua/issues/2913)) ([bd48816](https://github.com/nvim-tree/nvim-tree.lua/commit/bd4881660bf0ddfa6acb21259f856ba3dcb26a93)) +* safely close tree window with pcall and debug logging ([bd48816](https://github.com/nvim-tree/nvim-tree.lua/commit/bd4881660bf0ddfa6acb21259f856ba3dcb26a93)) + +## [1.6.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.6.0...nvim-tree-v1.6.1) (2024-09-09) + + +### Bug Fixes + +* **#2794:** sshfs compatibility ([#2893](https://github.com/nvim-tree/nvim-tree.lua/issues/2893)) ([2d6e64d](https://github.com/nvim-tree/nvim-tree.lua/commit/2d6e64dd8c45a86f312552b7a47eef2c8623a25c)) +* **#2868:** windows: do not visit unenumerable directories such as Application Data ([#2874](https://github.com/nvim-tree/nvim-tree.lua/issues/2874)) ([2104786](https://github.com/nvim-tree/nvim-tree.lua/commit/210478677cb9d672c4265deb0e9b59d58b675bd4)) +* **#2878:** nowrapscan prevents move from root ([#2880](https://github.com/nvim-tree/nvim-tree.lua/issues/2880)) ([4234095](https://github.com/nvim-tree/nvim-tree.lua/commit/42340952af598a08ab80579d067b6da72a9e6d29)) +* **#2879:** remove unnecessary tree window width setting to prevent unnecessary :wincmd = ([#2881](https://github.com/nvim-tree/nvim-tree.lua/issues/2881)) ([d43ab67](https://github.com/nvim-tree/nvim-tree.lua/commit/d43ab67d0eb4317961c5e9d15fffe908519debe0)) + +## [1.6.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.5.0...nvim-tree-v1.6.0) (2024-08-10) + + +### Features + +* **#2225:** add renderer.hidden_display to show a summary of hidden files below the tree ([#2856](https://github.com/nvim-tree/nvim-tree.lua/issues/2856)) ([e25eb7f](https://github.com/nvim-tree/nvim-tree.lua/commit/e25eb7fa83f7614bb23d762e91d2de44fcd7103b)) +* **#2349:** add "right_align" option for renderer.icons.*_placement ([#2839](https://github.com/nvim-tree/nvim-tree.lua/issues/2839)) ([1d629a5](https://github.com/nvim-tree/nvim-tree.lua/commit/1d629a5d3f7d83d516494c221a2cfc079f43bc47)) +* **#2349:** add "right_align" option for renderer.icons.*_placement ([#2846](https://github.com/nvim-tree/nvim-tree.lua/issues/2846)) ([48d0e82](https://github.com/nvim-tree/nvim-tree.lua/commit/48d0e82f9434691cc50d970898142a8c084a49d6)) +* add renderer.highlight_hidden, renderer.icons.show.hidden and renderer.icons.hidden_placement for dotfile icons/highlights ([#2840](https://github.com/nvim-tree/nvim-tree.lua/issues/2840)) ([48a9290](https://github.com/nvim-tree/nvim-tree.lua/commit/48a92907575df1dbd7242975a04e98169cb3a115)) + + +### Bug Fixes + +* **#2859:** make sure window still exists when restoring options ([#2863](https://github.com/nvim-tree/nvim-tree.lua/issues/2863)) ([466fbed](https://github.com/nvim-tree/nvim-tree.lua/commit/466fbed3e4b61fcc23a48fe99de7bfa264a9fee8)) + +## [1.5.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.4.0...nvim-tree-v1.5.0) (2024-07-11) + + +### Features + +* **#2127:** add experimental.actions.open_file.relative_path to open files with a relative path rather than absolute ([#2805](https://github.com/nvim-tree/nvim-tree.lua/issues/2805)) ([869c064](https://github.com/nvim-tree/nvim-tree.lua/commit/869c064721a6c2091f22c3541e8f0ff958361771)) +* **#2598:** add api.tree.resize ([#2811](https://github.com/nvim-tree/nvim-tree.lua/issues/2811)) ([2ede0de](https://github.com/nvim-tree/nvim-tree.lua/commit/2ede0de67b47e89e2b4cb488ea3f58b8f5a8c90a)) +* **#2799:** `filesystem_watchers.ignore_dirs` and `git.disable_for_dirs` may be functions ([#2800](https://github.com/nvim-tree/nvim-tree.lua/issues/2800)) ([8b2c5c6](https://github.com/nvim-tree/nvim-tree.lua/commit/8b2c5c678be4b49dff6a2df794877000113fd77b)) +* **#2799:** filesystem_watchers.ignore_dirs and git.disable_for_dirs may be functions ([8b2c5c6](https://github.com/nvim-tree/nvim-tree.lua/commit/8b2c5c678be4b49dff6a2df794877000113fd77b)) + + +### Bug Fixes + +* **#2813:** macos: enable file renaming with changed capitalization ([#2814](https://github.com/nvim-tree/nvim-tree.lua/issues/2814)) ([abfd1d1](https://github.com/nvim-tree/nvim-tree.lua/commit/abfd1d1b6772540364743531cc0331e08a0027a9)) +* **#2819:** experimental.actions.open_file.relative_path issue following change directory ([#2820](https://github.com/nvim-tree/nvim-tree.lua/issues/2820)) ([12a9a99](https://github.com/nvim-tree/nvim-tree.lua/commit/12a9a995a455d2c2466e47140663275365a5d2fc)) + +## [1.4.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.3.3...nvim-tree-v1.4.0) (2024-06-09) + +### Notice + +* Neovim 0.9 is now the minimum supported version; please upgrade to neovim release version 0.9 or 0.10. + +### Reverts + +* **#2781:** "refactor: replace deprecated use of vim.diagnostic.is_disabled()" ([#2784](https://github.com/nvim-tree/nvim-tree.lua/issues/2784)) ([517e4fb](https://github.com/nvim-tree/nvim-tree.lua/commit/517e4fbb9ef3c0986da7047f44b4b91a2400f93c)) + + +### Miscellaneous Chores + +* release 1.4.0 ([1cac800](https://github.com/nvim-tree/nvim-tree.lua/commit/1cac8005df6da484c97499247754afa59fef92db)) + +## [1.3.3](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.3.2...nvim-tree-v1.3.3) (2024-05-14) + + +### Bug Fixes + +* nil access exception with git integration when changing branches ([#2774](https://github.com/nvim-tree/nvim-tree.lua/issues/2774)) ([340d3a9](https://github.com/nvim-tree/nvim-tree.lua/commit/340d3a9795e06bdd1814228de398cd510f9bfbb0)) + +## [1.3.2](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.3.1...nvim-tree-v1.3.2) (2024-05-12) + + +### Bug Fixes + +* **#2758:** use nvim-webdevicons default file icon, not renderer.icons.glyphs.default, as per :help ([#2759](https://github.com/nvim-tree/nvim-tree.lua/issues/2759)) ([347e1eb](https://github.com/nvim-tree/nvim-tree.lua/commit/347e1eb35264677f66a79466bb5e3d111968e12c)) +* **#2758:** use nvim-webdevicons default for default files ([347e1eb](https://github.com/nvim-tree/nvim-tree.lua/commit/347e1eb35264677f66a79466bb5e3d111968e12c)) +* **#925:** handle newlines in file names ([#2754](https://github.com/nvim-tree/nvim-tree.lua/issues/2754)) ([64f61e4](https://github.com/nvim-tree/nvim-tree.lua/commit/64f61e4c913047a045ff90bd188dd3b54ee443cf)) + +## [1.3.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.3.0...nvim-tree-v1.3.1) (2024-04-25) + + +### Bug Fixes + +* **#2535:** TextYankPost event sends vim.v.event ([#2734](https://github.com/nvim-tree/nvim-tree.lua/issues/2734)) ([d8d3a15](https://github.com/nvim-tree/nvim-tree.lua/commit/d8d3a1590a05b2d8b5eb26e2ed1c6052b1b47a77)) +* **#2733:** escape trash path ([#2735](https://github.com/nvim-tree/nvim-tree.lua/issues/2735)) ([81eb8d5](https://github.com/nvim-tree/nvim-tree.lua/commit/81eb8d519233c105f30dc0a278607e62b20502fd)) + +## [1.3.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.2.0...nvim-tree-v1.3.0) (2024-03-30) + + +### Features + +* add update_focused_file.exclude ([#2673](https://github.com/nvim-tree/nvim-tree.lua/issues/2673)) ([e20966a](https://github.com/nvim-tree/nvim-tree.lua/commit/e20966ae558524f8d6f93dc37f5d2a8605f893e2)) + + +### Bug Fixes + +* **#2658:** change SpellCap groups to reduce confusion: ExecFile->Question, ImageFile->Question, SpecialFile->Title, Symlink->Underlined; add all other highlight groups to :NvimTreeHiTest ([#2732](https://github.com/nvim-tree/nvim-tree.lua/issues/2732)) ([0aca092](https://github.com/nvim-tree/nvim-tree.lua/commit/0aca0920f44b12a8383134bcb52da9faec123608)) +* bookmark filter shows marked directory children ([#2719](https://github.com/nvim-tree/nvim-tree.lua/issues/2719)) ([2d97059](https://github.com/nvim-tree/nvim-tree.lua/commit/2d97059661c83787372c8c003e743c984ba3ac50)) + +## [1.2.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.1.1...nvim-tree-v1.2.0) (2024-03-24) + + +### Features + +* add api.tree.toggle_enable_filters ([#2706](https://github.com/nvim-tree/nvim-tree.lua/issues/2706)) ([f7c09bd](https://github.com/nvim-tree/nvim-tree.lua/commit/f7c09bd72e50e1795bd3afb9e2a2b157b4bfb3c3)) + +## [1.1.1](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.1.0...nvim-tree-v1.1.1) (2024-03-15) + + +### Bug Fixes + +* **#2395:** marks.bulk.move defaults to directory at cursor ([#2688](https://github.com/nvim-tree/nvim-tree.lua/issues/2688)) ([cfea5bd](https://github.com/nvim-tree/nvim-tree.lua/commit/cfea5bd0806aab41bef6014c6cf5a510910ddbdb)) +* **#2705:** change NvimTreeWindowPicker cterm background from Cyan to more visible DarkBlue ([#2708](https://github.com/nvim-tree/nvim-tree.lua/issues/2708)) ([1fd9c98](https://github.com/nvim-tree/nvim-tree.lua/commit/1fd9c98960463d2d5d400916c0633b2df016941d)) +* bookmark filter should include parent directory ([#2704](https://github.com/nvim-tree/nvim-tree.lua/issues/2704)) ([76b9810](https://github.com/nvim-tree/nvim-tree.lua/commit/76b98109f62caa12b2f1dff472060b2233ea2e90)) + +## [1.1.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v1.0.0...nvim-tree-v1.1.0) (2024-03-14) + + +### Features + +* **#2630:** file renames can now create directories ([#2657](https://github.com/nvim-tree/nvim-tree.lua/issues/2657)) ([efafd73](https://github.com/nvim-tree/nvim-tree.lua/commit/efafd73efa9bc8c26282aed563ba0f01c7465b06)) +* add api.fs.copy.basename, default mapping ge ([#2698](https://github.com/nvim-tree/nvim-tree.lua/issues/2698)) ([8f2a50f](https://github.com/nvim-tree/nvim-tree.lua/commit/8f2a50f1cd0c64003042364cf317c8788eaa6c8c)) + + +### Bug Fixes + +* **#2695:** git toplevel guard against missing paths ([#2696](https://github.com/nvim-tree/nvim-tree.lua/issues/2696)) ([3c4267e](https://github.com/nvim-tree/nvim-tree.lua/commit/3c4267eb5045fa86b67fe40c0c63d31efc801e77)) +* searchcount exception on invalid search regex ([#2693](https://github.com/nvim-tree/nvim-tree.lua/issues/2693)) ([041dbd1](https://github.com/nvim-tree/nvim-tree.lua/commit/041dbd18f440207ad161503a384e7c82d575db66)) + +## [1.0.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v0.100.0...nvim-tree-v1.0.0) (2024-02-18) + + +### Features + +* **#2654:** filters.custom may be a function ([#2655](https://github.com/nvim-tree/nvim-tree.lua/issues/2655)) ([4a87b8b](https://github.com/nvim-tree/nvim-tree.lua/commit/4a87b8b46b4a30107971871df3cb7f4c30fdd5d0)) + + +### Miscellaneous Chores + +* release 1.0.0 ([#2678](https://github.com/nvim-tree/nvim-tree.lua/issues/2678)) ([d16246a](https://github.com/nvim-tree/nvim-tree.lua/commit/d16246a7575538f77e9246520449b99333c469f7)) + +## [0.100.0](https://github.com/nvim-tree/nvim-tree.lua/compare/nvim-tree-v0.99.0...nvim-tree-v0.100.0) (2024-02-11) + + +### Features + +* **#1389:** api: recursive node navigation for git and diagnostics ([#2525](https://github.com/nvim-tree/nvim-tree.lua/issues/2525)) ([5d13cc8](https://github.com/nvim-tree/nvim-tree.lua/commit/5d13cc8205bce4963866f73c50f6fdc18a515ffe)) +* **#2415:** add :NvimTreeHiTest ([#2664](https://github.com/nvim-tree/nvim-tree.lua/issues/2664)) ([b278fc2](https://github.com/nvim-tree/nvim-tree.lua/commit/b278fc25ae0fc95e4808eb5618f07fc2522fd2b3)) +* **#2415:** colour and highlight overhaul, see :help nvim-tree-highlight-overhaul ([#2455](https://github.com/nvim-tree/nvim-tree.lua/issues/2455)) ([e9c5abe](https://github.com/nvim-tree/nvim-tree.lua/commit/e9c5abe073a973f54d3ca10bfe30f253569f4405)) +* add node.open.toggle_group_empty, default mapping L ([#2647](https://github.com/nvim-tree/nvim-tree.lua/issues/2647)) ([8cbb1db](https://github.com/nvim-tree/nvim-tree.lua/commit/8cbb1db8e90b62fc56f379992e622e9f919792ce)) + + +### Bug Fixes + +* **#2415:** disambiguate highlight groups, see :help nvim-tree-highlight-overhaul ([#2639](https://github.com/nvim-tree/nvim-tree.lua/issues/2639)) ([d9cb432](https://github.com/nvim-tree/nvim-tree.lua/commit/d9cb432d2c8d8fa9267ddbd7535d76fe4df89360)) +* **#2415:** fix NvimTreeIndentMarker highlight group: FileIcon->FolderIcon ([e9ac136](https://github.com/nvim-tree/nvim-tree.lua/commit/e9ac136a3ab996aa8e4253253521dcf2cb66b81b)) +* **#2415:** highlight help header and mappings ([#2669](https://github.com/nvim-tree/nvim-tree.lua/issues/2669)) ([39e6fef](https://github.com/nvim-tree/nvim-tree.lua/commit/39e6fef85ac3bb29532b877aa7c9c34911c661af)) +* **#2415:** nvim 0.8 highlight overhaul support, limited to only show highest highlight precedence ([#2642](https://github.com/nvim-tree/nvim-tree.lua/issues/2642)) ([f39f7b6](https://github.com/nvim-tree/nvim-tree.lua/commit/f39f7b6fcd3865ac2146de4cb4045286308f2935)) +* **#2415:** NvimTreeIndentMarker highlight group: FileIcon->FolderIcon ([#2656](https://github.com/nvim-tree/nvim-tree.lua/issues/2656)) ([e9ac136](https://github.com/nvim-tree/nvim-tree.lua/commit/e9ac136a3ab996aa8e4253253521dcf2cb66b81b)) +* **#2624:** open file from docked floating window ([#2627](https://github.com/nvim-tree/nvim-tree.lua/issues/2627)) ([f24afa2](https://github.com/nvim-tree/nvim-tree.lua/commit/f24afa2cef551122b8bd53bb2e4a7df42343ce2e)) +* **#2632:** occasional error stack when locating nvim-tree window ([#2633](https://github.com/nvim-tree/nvim-tree.lua/issues/2633)) ([48b1d86](https://github.com/nvim-tree/nvim-tree.lua/commit/48b1d8638fa3726236ae22e0e48a74ac8ea6592a)) +* **#2637:** show buffer modified icons and highlights ([#2638](https://github.com/nvim-tree/nvim-tree.lua/issues/2638)) ([7bdb220](https://github.com/nvim-tree/nvim-tree.lua/commit/7bdb220d0fe604a77361e92cdbc7af1b8a412126)) +* **#2643:** correctly apply linked highlight groups in tree window ([#2653](https://github.com/nvim-tree/nvim-tree.lua/issues/2653)) ([fbee8a6](https://github.com/nvim-tree/nvim-tree.lua/commit/fbee8a69a46f558d29ab84e96301425b0501c668)) +* allow highlight overrides for DEFAULT_DEFS: NvimTreeFolderIcon, NvimTreeWindowPicker ([#2636](https://github.com/nvim-tree/nvim-tree.lua/issues/2636)) ([74525ac](https://github.com/nvim-tree/nvim-tree.lua/commit/74525ac04760bf0d9fec2bf51474d2b05f36048e)) +* bad column offset when using full_name ([#2629](https://github.com/nvim-tree/nvim-tree.lua/issues/2629)) ([75ff64e](https://github.com/nvim-tree/nvim-tree.lua/commit/75ff64e6663fc3b23c72dca32b2f838acefe7c8a)) +* passing nil as window handle in view.get_winnr ([48b1d86](https://github.com/nvim-tree/nvim-tree.lua/commit/48b1d8638fa3726236ae22e0e48a74ac8ea6592a)) + +## 0.99.0 (2024-01-01) + + +### Features + +* **#1850:** add "no bookmark" filter ([#2571](https://github.com/nvim-tree/nvim-tree.lua/issues/2571)) ([8f92e1e](https://github.com/nvim-tree/nvim-tree.lua/commit/8f92e1edd399f839a23776dcc6eee4ba18030370)) +* add kind param to vim.ui.select function calls ([#2602](https://github.com/nvim-tree/nvim-tree.lua/issues/2602)) ([dc839a7](https://github.com/nvim-tree/nvim-tree.lua/commit/dc839a72a6496ce22ebd3dd959115cf97c1b20a0)) +* add option to skip gitignored files on git navigation ([#2583](https://github.com/nvim-tree/nvim-tree.lua/issues/2583)) ([50f30bc](https://github.com/nvim-tree/nvim-tree.lua/commit/50f30bcd8c62ac4a83d133d738f268279f2c2ce2)) + + +### Bug Fixes + +* **#2519:** Diagnostics Not Updated When Tree Not Visible ([#2597](https://github.com/nvim-tree/nvim-tree.lua/issues/2597)) ([96a783f](https://github.com/nvim-tree/nvim-tree.lua/commit/96a783fbd606a458bcce2ef8041240a8b94510ce)) +* **#2609:** help toggle ([#2611](https://github.com/nvim-tree/nvim-tree.lua/issues/2611)) ([fac4900](https://github.com/nvim-tree/nvim-tree.lua/commit/fac4900bd18a9fa15be3d104645d9bdef7b3dcec)) +* hijack_cursor on update focused file and vim search ([#2600](https://github.com/nvim-tree/nvim-tree.lua/issues/2600)) ([02ae523](https://github.com/nvim-tree/nvim-tree.lua/commit/02ae52357ba4da77a4c120390791584a81d15340)) diff --git a/pack/ant/start/nvim-tree.lua/CONTRIBUTING.md b/pack/ant/start/nvim-tree.lua/CONTRIBUTING.md new file mode 100644 index 0000000..742d4bc --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/CONTRIBUTING.md @@ -0,0 +1,130 @@ +# Contributing to `nvim-tree.lua` + +Thank you for contributing. + +See [wiki: Development](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) for environment setup, tips and tools. + +# Tools + +Following are used during CI and strongly recommended during local development. + +Language server: [luals](https://luals.github.io) + +Lint: [luacheck](https://github.com/lunarmodules/luacheck/) + +Style: [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 + +You can install them via you OS package manager e.g. `pacman`, `brew` or other via other package managers such as `cargo` or `luarocks` + +# Quality + +The following quality checks are mandatory and are performed during CI. They run on the entire `lua` directory and return 1 on any failure. + +You can run them all via `make` or `make all` + +You can setup git hooks to run all checks by running `scripts/setup-hooks.sh` + +## lint + +1. Runs luacheck quietly using `.luacheck` settings + +```sh +make lint +``` + +## style + +1. Runs CodeCheck using `.editorconfig` settings +1. Runs `scripts/doc-comments.sh` to validate annotated documentation + +```sh +make style +``` + +You can automatically fix `CodeCheck` issues via: + +```sh +make style-fix +``` + +## check + +1. Runs the checks that the LSP lua language server runs inside nvim using `.luarc.json` via `scripts/luals-check.sh` + +```sh +make check +``` + +Assumes `$VIMRUNTIME` is `/usr/share/nvim/runtime`. Adjust as necessary e.g. + +```sh +VIMRUNTIME="/my/path/to/runtime" make check +``` + +If `lua-language-server` is not available or `--check` doesn't function (e.g. Arch Linux 3.9.1-1) you can manually install it as per `ci.yml` e.g. + +```sh +mkdir luals +curl -L "https://github.com/LuaLS/lua-language-server/releases/download/3.9.1/lua-language-server-3.9.1-linux-x64.tar.gz" | tar zx --directory luals + +PATH="luals/bin:${PATH}" make check +``` + +# Adding New Actions + +To add a new action, add a file in `actions/name-of-the-action.lua`. You should export a `setup` function if some configuration is needed. + +Once you did, you should run `make help-update` + +# Documentation + +## Opts + +When adding new options, you should declare the defaults in the main `nvim-tree.lua` file. + +Documentation for options should also be added to `nvim-tree-opts` in `doc/nvim-tree-lua.txt` + +## 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 + +Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge. + +Please check "allow edits by maintainers" to allow nvim-tree developers to make small changes such as documentation tweaks. + +## Subject + +The merge commit message will be the subject of the PR. + +A [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) subject will be validated by the Semantic Pull Request Subject CI job. Reference the issue to be used in the release notes e.g. + +`fix(#2395): marks.bulk.move defaults to directory at cursor` + +Available types: +* feat: A new feature +* fix: A bug fix +* docs: Documentation only changes +* style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +* refactor: A code change that neither fixes a bug nor adds a feature +* perf: A code change that improves performance +* test: Adding missing tests or correcting existing tests +* build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +* ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) +* chore: Other changes that don't modify src or test files +* revert: Reverts a previous commit + +If in doubt, look at previous commits. + +See also [The Conventional Commits ultimate cheatsheet](https://gist.github.com/gabrielecanepa/fa6cca1a8ae96f77896fe70ddee65527) diff --git a/pack/ant/start/nvim-tree.lua/LICENSE b/pack/ant/start/nvim-tree.lua/LICENSE new file mode 100644 index 0000000..b6a9b48 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/LICENSE @@ -0,0 +1,15 @@ +nvim-tree.lua is a file explorer / filesystem tree view plugin for neovim +Copyright © 2019 Yazdani Kiyan + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/pack/ant/start/nvim-tree.lua/Makefile b/pack/ant/start/nvim-tree.lua/Makefile new file mode 100644 index 0000000..b5e829d --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/Makefile @@ -0,0 +1,48 @@ +all: lint style check + +# +# mandatory checks +# +lint: luacheck + +style: style-check style-doc + +check: luals + +# +# subtasks +# +luacheck: + luacheck --codes --quiet lua --exclude-files "**/_meta/**" + +# --diagnosis-as-error does not function for workspace, hence we post-process the output +style-check: + CodeFormat check --config .editorconfig --diagnosis-as-error --workspace lua + +style-doc: + scripts/doc-comments.sh + +luals: + @scripts/luals-check.sh + +# +# fixes +# +style-fix: + CodeFormat format --config .editorconfig --workspace lua + +# +# utility +# +help-update: + scripts/help-update.sh + +# +# CI +# +help-check: help-update + git diff --exit-code doc/nvim-tree-lua.txt + + +.PHONY: all lint style check luacheck style-check style-doc luals style-fix help-update help-check + diff --git a/pack/ant/start/nvim-tree.lua/README.md b/pack/ant/start/nvim-tree.lua/README.md new file mode 100644 index 0000000..8124809 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/README.md @@ -0,0 +1,186 @@ +# A File Explorer For Neovim Written In Lua + +[![CI](https://github.com/nvim-tree/nvim-tree.lua/actions/workflows/ci.yml/badge.svg)](https://github.com/nvim-tree/nvim-tree.lua/actions/workflows/ci.yml) + + + + + Automatic updates + + File type icons + + Git integration + + Diagnostics integration: LSP and COC + + (Live) filtering + + Cut, copy, paste, rename, delete, create + + Highly customisable + +
+
+ +Take a look at the [wiki](https://github.com/nvim-tree/nvim-tree.lua/wiki) for Showcases, Tips, Recipes and more. + +Questions and general support: [Discussions](https://github.com/nvim-tree/nvim-tree.lua/discussions) + +## Requirements + +[neovim >=0.9.0](https://github.com/neovim/neovim/wiki/Installing-Neovim) + +[nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) is optional and used to display file icons. It requires a [patched font](https://www.nerdfonts.com/). Your terminal emulator must be configured to use that font, usually "Hack Nerd Font" + +## Install + +Please install via your preferred package manager. See [Installation](https://github.com/nvim-tree/nvim-tree.lua/wiki/Installation) for specific package manager instructions. + +`nvim-tree/nvim-tree.lua` + +Major or minor versions may be specified via tags: `v` e.g. `v1` or `v.` e.g. `v1.23` + +`nvim-tree/nvim-web-devicons` optional, for file icons + +Disabling [netrw](https://neovim.io/doc/user/pi_netrw.html) is strongly advised, see [:help nvim-tree-netrw](doc/nvim-tree-lua.txt) + +## Quick Start + +### Setup + +Setup the plugin in your `init.lua` + +```lua +-- disable netrw at the very start of your init.lua +vim.g.loaded_netrw = 1 +vim.g.loaded_netrwPlugin = 1 + +-- optionally enable 24-bit colour +vim.opt.termguicolors = true + +-- empty setup using defaults +require("nvim-tree").setup() + +-- OR setup with some options +require("nvim-tree").setup({ + sort = { + sorter = "case_sensitive", + }, + view = { + width = 30, + }, + renderer = { + group_empty = true, + }, + filters = { + dotfiles = true, + }, +}) +``` + +### Help + +Open the tree: `:NvimTreeOpen` + +Show the mappings: `g?` + +### Custom Mappings + +[:help nvim-tree-mappings-default](doc/nvim-tree-lua.txt) are applied by default however you may customise via |nvim-tree.on_attach| e.g. + +```lua +local function my_on_attach(bufnr) + local api = require "nvim-tree.api" + + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + -- default mappings + api.config.mappings.default_on_attach(bufnr) + + -- custom mappings + vim.keymap.set('n', '', api.tree.change_root_to_parent, opts('Up')) + vim.keymap.set('n', '?', api.tree.toggle_help, opts('Help')) +end + +-- pass to setup along with your other options +require("nvim-tree").setup { + --- + on_attach = my_on_attach, + --- +} +``` + +### Highlight + +Run `:NvimTreeHiTest` to show all the highlights that nvim-tree uses. + +They can be customised before or after setup is called and will be immediately +applied at runtime. e.g. + +```lua +vim.cmd([[ + :hi NvimTreeExecFile guifg=#ffa0a0 + :hi NvimTreeSpecialFile guifg=#ff80ff gui=underline + :hi NvimTreeSymlink guifg=Yellow gui=italic + :hi link NvimTreeImageFile Title +]]) +``` +See [:help nvim-tree-highlight](doc/nvim-tree-lua.txt) for details. + +## Commands + +See [:help nvim-tree-commands](doc/nvim-tree-lua.txt) + +Basic commands: + +`:NvimTreeToggle` Open or close the tree. Takes an optional path argument. + +`:NvimTreeFocus` Open the tree if it is closed, and then focus on the tree. + +`:NvimTreeFindFile` Move the cursor in the tree for the current buffer, opening folders if needed. + +`:NvimTreeCollapse` Collapses the nvim-tree recursively. + +## Roadmap + +nvim-tree is stable and new major features will not be added. The focus is on existing user experience. + +Users are encouraged to add their own custom features via the public [API](#api). + +Development is focused on: +* Bug fixes +* Performance +* Quality of Life improvements +* API / Events +* Enhancements to existing features + +## API + +nvim-tree exposes a public API. This is non breaking, with additions made as necessary. See [:help nvim-tree-api](doc/nvim-tree-lua.txt) + +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. + +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 + +PRs are always welcome. See [wiki](https://github.com/nvim-tree/nvim-tree.lua/wiki/Development) to get started. + +See [bug](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and [PR Please](https://github.com/nvim-tree/nvim-tree.lua/issues?q=is%3Aopen+is%3Aissue+label%3A%22PR+please%22) issues if you are looking for some work to get you started. + +## Screenshots + +See [Showcases](https://github.com/nvim-tree/nvim-tree.lua/wiki/Showcases) wiki page for examples of user's configurations with sources. + +Please add your own! + +## Team + +* [@alex-courtis](https://github.com/alex-courtis) Arch Linux +* [@gegoune](https://github.com/gegoune) macOS +* [@Akmadan23](https://github.com/Akmadan23) Linux +* [@dependabot[bot]](https://github.com/apps/dependabot) Ubuntu Linux diff --git a/pack/ant/start/nvim-tree.lua/doc/.gitignore b/pack/ant/start/nvim-tree.lua/doc/.gitignore new file mode 100644 index 0000000..6e92f57 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/doc/.gitignore @@ -0,0 +1 @@ +tags diff --git a/pack/ant/start/nvim-tree.lua/doc/nvim-tree-lua.txt b/pack/ant/start/nvim-tree.lua/doc/nvim-tree-lua.txt new file mode 100644 index 0000000..7ab41be --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/doc/nvim-tree-lua.txt @@ -0,0 +1,3234 @@ +*nvim-tree.lua* A File Explorer For Neovim Written In Lua + +Author: Yazdani Kiyan + +============================================================================== +CONTENTS *nvim-tree* + + 1. Introduction |nvim-tree-introduction| + 2. Quickstart |nvim-tree-quickstart| + 2.1 Quickstart: Setup |nvim-tree-quickstart-setup| + 2.2 Quickstart: Help |nvim-tree-quickstart-help| + 2.3 Quickstart: Custom Mappings |nvim-tree-quickstart-custom-mappings| + 2.4 Quickstart: Highlight |nvim-tree-quickstart-highlight| + 3. Commands |nvim-tree-commands| + 4. Setup |nvim-tree-setup| + 5. Opts |nvim-tree-opts| + 5.1 Opts: Sort |nvim-tree-opts-sort| + 5.2 Opts: View |nvim-tree-opts-view| + 5.3 Opts: Renderer |nvim-tree-opts-renderer| + 5.4 Opts: Hijack Directories |nvim-tree-opts-hijack-directories| + 5.5 Opts: Update Focused File |nvim-tree-opts-update-focused-file| + 5.6 Opts: System Open |nvim-tree-opts-system-open| + 5.7 Opts: Git |nvim-tree-opts-git| + 5.8 Opts: Diagnostics |nvim-tree-opts-diagnostics| + 5.9 Opts: Modified |nvim-tree-opts-modified| + 5.10 Opts: Filters |nvim-tree-opts-filters| + 5.11 Opts: Live Filter |nvim-tree-opts-live-filter| + 5.12 Opts: Filesystem Watchers |nvim-tree-opts-filesystem-watchers| + 5.13 Opts: Actions |nvim-tree-opts-actions| + 5.14 Opts: Trash |nvim-tree-opts-trash| + 5.15 Opts: Tab |nvim-tree-opts-tab| + 5.16 Opts: Notify |nvim-tree-opts-notify| + 5.17 Opts: Help |nvim-tree-opts-help| + 5.18 Opts: UI |nvim-tree-opts-ui| + 5.19 Opts: Experimental |nvim-tree-opts-experimental| + 5.20 Opts: Log |nvim-tree-opts-log| + 6. API |nvim-tree-api| + 6.1 API Tree |nvim-tree-api.tree| + 6.2 API File System |nvim-tree-api.fs| + 6.3 API Node |nvim-tree-api.node| + 6.4 API Git |nvim-tree-api.git| + 6.5 API Events |nvim-tree-api.events| + 6.6 API Live Filter |nvim-tree-api.live_filter| + 6.7 API Marks |nvim-tree-api.marks| + 6.8 API Config |nvim-tree-api.config| + 6.9 API Commands |nvim-tree-api.commands| + 6.10 API Diagnostics |nvim-tree-api.diagnostics| + 7. Mappings |nvim-tree-mappings| + 7.1 Mappings: Default |nvim-tree-mappings-default| + 8. Highlight |nvim-tree-highlight| + 8.1 Highlight: Default |nvim-tree-highlight-default| + 8.2 Highlight: Overhaul |nvim-tree-highlight-overhaul| + 9. Events |nvim-tree-events| + 10. Prompts |nvim-tree-prompts| + 11. Decorators |nvim-tree-decorators| + 11.1 Decorator Example |nvim-tree-decorator-example| + 12. OS Specific Restrictions |nvim-tree-os-specific| + 13. Netrw |nvim-tree-netrw| + 14. Legacy |nvim-tree-legacy| + 14.1 Legacy: Opts |nvim-tree-legacy-opts| + 14.2 Legacy: Highlight |nvim-tree-legacy-highlight| + 15. Index |nvim-tree-index| + 15.1 Index: Opts |nvim-tree-index-opts| + 15.2 Index: API |nvim-tree-index-api| + +============================================================================== + 1. INTRODUCTION *nvim-tree-introduction* + +Features + + - Automatic updates + - File type icons + - Git integration + - Diagnostics integration: LSP and COC + - (Live) filtering + - Cut, copy, paste, rename, delete, create + - Highly customisable + +File Icons + + https://github.com/nvim-tree/nvim-web-devicons is optional and used to display file icons. + It requires a patched font: https://www.nerdfonts.com + Your terminal emulator must be configured to use that font, usually "Hack Nerd Font" + +  should look like an open folder. + + To disable the display of icons see |renderer.icons.show| + +Colours + + Syntax highlighting uses g:terminal_color_ from colorschemes, falls back to + ugly colors otherwise. + +Git Integration + + One or two icons for git status. When two are shown, the left is staged. + + ✗ unstaged + ✓ staged +  unmerged + ➜ renamed + ★ untracked +  deleted + ◌ ignored + +Requirements + + This file explorer requires `neovim >= 0.9.0` + +============================================================================== + 2. QUICKSTART *nvim-tree-quickstart* + +Install the plugins via your package manager: + `"nvim-tree/nvim-tree.lua"` + `"nvim-tree/nvim-web-devicons"` + +Disabling |netrw| is strongly advised, see |nvim-tree-netrw| + +============================================================================== + 2.1 QUICKSTART: SETUP *nvim-tree-quickstart-setup* + +Setup the plugin in your `init.lua` e.g. >lua + + -- disable netrw at the very start of your init.lua + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 + + -- optionally enable 24-bit colour + vim.opt.termguicolors = true + + -- empty setup using defaults + require("nvim-tree").setup() + + -- OR setup with some options + require("nvim-tree").setup({ + sort = { + sorter = "case_sensitive", + }, + view = { + width = 30, + }, + renderer = { + group_empty = true, + }, + filters = { + dotfiles = true, + }, + }) +< +============================================================================== + 2.2 QUICKSTART: HELP *nvim-tree-quickstart-help* + +Open the tree: `:NvimTreeOpen` + +Show the mappings: `g?` + +`` CD |nvim-tree-api.tree.change_root_to_node()| +`` Open: In Place |nvim-tree-api.node.open.replace_tree_buffer()| +`` Info |nvim-tree-api.node.show_info_popup()| +`` Rename: Omit Filename |nvim-tree-api.fs.rename_sub()| +`` Open: New Tab |nvim-tree-api.node.open.tab()| +`` Open: Vertical Split |nvim-tree-api.node.open.vertical()| +`` Open: Horizontal Split |nvim-tree-api.node.open.horizontal()| +`` Close Directory |nvim-tree-api.node.navigate.parent_close()| +`` Open |nvim-tree-api.node.open.edit()| +`` Open Preview |nvim-tree-api.node.open.preview()| +`>` Next Sibling |nvim-tree-api.node.navigate.sibling.next()| +`<` Previous Sibling |nvim-tree-api.node.navigate.sibling.prev()| +`.` Run Command |nvim-tree-api.node.run.cmd()| +`-` Up |nvim-tree-api.tree.change_root_to_parent()| +`a` Create File Or Directory |nvim-tree-api.fs.create()| +`bd` Delete Bookmarked |nvim-tree-api.marks.bulk.delete()| +`bt` Trash Bookmarked |nvim-tree-api.marks.bulk.trash()| +`bmv` Move Bookmarked |nvim-tree-api.marks.bulk.move()| +`B` Toggle Filter: No Buffer |nvim-tree-api.tree.toggle_no_buffer_filter()| +`c` Copy |nvim-tree-api.fs.copy.node()| +`C` Toggle Filter: Git Clean |nvim-tree-api.tree.toggle_git_clean_filter()| +`[c` Prev Git |nvim-tree-api.node.navigate.git.prev()| +`]c` Next Git |nvim-tree-api.node.navigate.git.next()| +`d` Delete |nvim-tree-api.fs.remove()| +`D` Trash |nvim-tree-api.fs.trash()| +`E` Expand All |nvim-tree-api.tree.expand_all()| +`e` Rename: Basename |nvim-tree-api.fs.rename_basename()| +`]e` Next Diagnostic |nvim-tree-api.node.navigate.diagnostics.next()| +`[e` Prev Diagnostic |nvim-tree-api.node.navigate.diagnostics.prev()| +`F` Live Filter: Clear |nvim-tree-api.live_filter.clear()| +`f` Live Filter: Start |nvim-tree-api.live_filter.start()| +`g?` Help |nvim-tree-api.tree.toggle_help()| +`gy` Copy Absolute Path |nvim-tree-api.fs.copy.absolute_path()| +`ge` Copy Basename |nvim-tree-api.fs.copy.basename()| +`H` Toggle Filter: Dotfiles |nvim-tree-api.tree.toggle_hidden_filter()| +`I` Toggle Filter: Git Ignore |nvim-tree-api.tree.toggle_gitignore_filter()| +`J` Last Sibling |nvim-tree-api.node.navigate.sibling.last()| +`K` First Sibling |nvim-tree-api.node.navigate.sibling.first()| +`L` Toggle Group Empty |nvim-tree-api.node.open.toggle_group_empty()| +`M` Toggle Filter: No Bookmark |nvim-tree-api.tree.toggle_no_bookmark_filter()| +`m` Toggle Bookmark |nvim-tree-api.marks.toggle()| +`o` Open |nvim-tree-api.node.open.edit()| +`O` Open: No Window Picker |nvim-tree-api.node.open.no_window_picker()| +`p` Paste |nvim-tree-api.fs.paste()| +`P` Parent Directory |nvim-tree-api.node.navigate.parent()| +`q` Close |nvim-tree-api.tree.close()| +`r` Rename |nvim-tree-api.fs.rename()| +`R` Refresh |nvim-tree-api.tree.reload()| +`s` Run System |nvim-tree-api.node.run.system()| +`S` Search |nvim-tree-api.tree.search_node()| +`u` Rename: Full Path |nvim-tree-api.fs.rename_full()| +`U` Toggle Filter: Hidden |nvim-tree-api.tree.toggle_custom_filter()| +`W` Collapse |nvim-tree-api.tree.collapse_all()| +`x` Cut |nvim-tree-api.fs.cut()| +`y` Copy Name |nvim-tree-api.fs.copy.filename()| +`Y` Copy Relative Path |nvim-tree-api.fs.copy.relative_path()| +`<2-LeftMouse>` Open |nvim-tree-api.node.open.edit()| +`<2-RightMouse>` CD |nvim-tree-api.tree.change_root_to_node()| + +============================================================================== + 2.3 QUICKSTART: CUSTOM MAPPINGS *nvim-tree-quickstart-custom-mappings* + +|nvim-tree-mappings-default| are applied by default however you may customise +via |nvim-tree.on_attach| e.g. >lua + + local function my_on_attach(bufnr) + local api = require "nvim-tree.api" + + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + -- default mappings + api.config.mappings.default_on_attach(bufnr) + + -- custom mappings + vim.keymap.set("n", "", api.tree.change_root_to_parent, opts("Up")) + vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help")) + end + + -- pass to setup along with your other options + require("nvim-tree").setup { + --- + on_attach = my_on_attach, + --- + } +< +============================================================================== + 2.4 QUICKSTART: HIGHLIGHT *nvim-tree-quickstart-highlight* + +Run |:NvimTreeHiTest| to show all the highlights that nvim-tree uses. + +They can be customised before or after setup is called and will be immediately +applied at runtime. e.g. >lua + + vim.cmd([[ + :hi NvimTreeExecFile guifg=#ffa0a0 + :hi NvimTreeSpecialFile guifg=#ff80ff gui=underline + :hi NvimTreeSymlink guifg=Yellow gui=italic + :hi link NvimTreeImageFile Title + ]]) +< +See |nvim-tree-highlight| for details. + +============================================================================== + 3. COMMANDS *nvim-tree-commands* + +*:NvimTreeOpen* + + Opens the tree. See |nvim-tree-api.tree.open()| + + Calls: `api.tree.open({ path = "" })` + +*:NvimTreeClose* + + Closes the tree. See |nvim-tree-api.tree.close()| + + Calls: `api.tree.close()` + +*:NvimTreeToggle* + + Open or close the tree. See |nvim-tree-api.tree.toggle()| + + Calls: `api.tree.toggle({ path = "", find_file = false, update_root = false, focus = true, })` + +*:NvimTreeFocus* + + Open the tree if it is closed, and then focus on the tree. + + See |nvim-tree-api.tree.open()| + + Calls: `api.tree.open()` + +*:NvimTreeRefresh* + + Refresh the tree. See |nvim-tree-api.tree.reload()| + + Calls: `api.tree.reload()` + +*:NvimTreeFindFile* + + The command will change the cursor in the tree for the current bufname. + + It will also open the leafs of the tree leading to the file in the buffer + (if you opened a file with something else than the NvimTree, like `fzf` or + `:split`) + + Invoke with a bang `:NvimTreeFindFile!` to update the root. + + See |nvim-tree-api.tree.find_file()| + + Calls: `api.tree.find_file({ update_root = , open = true, focus = true, })` + +*:NvimTreeFindFileToggle* + + close the tree or change the cursor in the tree for the current bufname, + similar to combination of |:NvimTreeToggle| and |:NvimTreeFindFile|. Takes an + optional path argument. + + Invoke with a bang `:NvimTreeFindFileToggle!` to update the root. + + See |nvim-tree-api.tree.toggle()| + + Calls: `api.tree.toggle({ path = "", update_root = , find_file = true, focus = true, })` + +*:NvimTreeClipboard* + + Print clipboard content for both cut and copy + + See |nvim-tree-api.fs.print_clipboard()| + + Calls: `api.fs.print_clipboard()` + +*:NvimTreeResize* + + Resize the NvimTree window to the given size. Example: `:NvimTreeResize 50` + resizes the window to the width of 50. If the size starts with "+" or "-" it + adds or removes the given value to the current window width. + Example `:NvimTreeResize -20` removes the value 20 from the current width. And + `:NvimTreeResize +20` adds the value 20 to the current width. + +*:NvimTreeCollapse* + + Collapses the nvim-tree recursively. + + See |nvim-tree-api.tree.collapse_all()| + + Calls: `api.tree.collapse_all(false)` + +*:NvimTreeCollapseKeepBuffers* + + Collapses the nvim-tree recursively, but keep the directories open, which are + used in an open buffer. + + See |nvim-tree-api.tree.collapse_all()| + + Calls: `api.tree.collapse_all(true)` + +*:NvimTreeHiTest* + + Show nvim-tree highlight groups similar to `:so $VIMRUNTIME/syntax/hitest.vim` + + See |nvim-tree-api.diagnostics.hi_test()| + + Calls: `api.diagnostics.hi_test()` + +============================================================================== + 4. SETUP *nvim-tree-setup* + +You must run setup() function once to initialise nvim-tree. It may be called +again to apply a change in configuration without restarting nvim. + +setup() function takes one optional argument: configuration table. If omitted +nvim-tree will be initialised with default configuration. + +The first setup() call is cheap: it does nothing more than validate / apply +the configuration. Nothing happens until the tree is first opened. + +Subsequent setup() calls are expensive as they tear down the world before +applying configuration. + +Following is the default configuration. See |nvim-tree-opts| for details. >lua + + require("nvim-tree").setup { -- BEGIN_DEFAULT_OPTS + on_attach = "default", + hijack_cursor = false, + auto_reload_on_write = true, + disable_netrw = false, + hijack_netrw = true, + hijack_unnamed_buffer_when_opening = false, + root_dirs = {}, + prefer_startup_root = false, + sync_root_with_cwd = false, + reload_on_bufenter = false, + respect_buf_cwd = false, + select_prompts = false, + sort = { + sorter = "name", + folders_first = true, + files_first = false, + }, + view = { + centralize_selection = false, + cursorline = true, + debounce_delay = 15, + side = "left", + preserve_window_proportions = false, + number = false, + relativenumber = false, + signcolumn = "yes", + width = 30, + float = { + enable = false, + quit_on_focus_loss = true, + open_win_config = { + relative = "editor", + border = "rounded", + width = 30, + height = 30, + row = 1, + col = 1, + }, + }, + }, + renderer = { + add_trailing = false, + group_empty = false, + full_name = false, + root_folder_label = ":~:s?$?/..?", + indent_width = 2, + special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, + hidden_display = "none", + symlink_destination = true, + decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", }, + highlight_git = "none", + highlight_diagnostics = "none", + highlight_opened_files = "none", + highlight_modified = "none", + highlight_hidden = "none", + highlight_bookmarks = "none", + highlight_clipboard = "name", + indent_markers = { + enable = false, + inline_arrows = true, + icons = { + corner = "└", + edge = "│", + item = "│", + bottom = "─", + none = " ", + }, + }, + icons = { + web_devicons = { + file = { + enable = true, + color = true, + }, + folder = { + enable = false, + color = true, + }, + }, + git_placement = "before", + modified_placement = "after", + hidden_placement = "after", + diagnostics_placement = "signcolumn", + bookmarks_placement = "signcolumn", + padding = " ", + symlink_arrow = " ➛ ", + show = { + file = true, + folder = true, + folder_arrow = true, + git = true, + modified = true, + hidden = false, + diagnostics = true, + bookmarks = true, + }, + glyphs = { + default = "", + symlink = "", + bookmark = "󰆤", + modified = "●", + hidden = "󰜌", + folder = { + arrow_closed = "", + arrow_open = "", + default = "", + open = "", + empty = "", + empty_open = "", + symlink = "", + symlink_open = "", + }, + git = { + unstaged = "✗", + staged = "✓", + unmerged = "", + renamed = "➜", + untracked = "★", + deleted = "", + ignored = "◌", + }, + }, + }, + }, + hijack_directories = { + enable = true, + auto_open = true, + }, + update_focused_file = { + enable = false, + update_root = { + enable = false, + ignore_list = {}, + }, + exclude = false, + }, + system_open = { + cmd = "", + args = {}, + }, + git = { + enable = true, + show_on_dirs = true, + show_on_open_dirs = true, + disable_for_dirs = {}, + timeout = 400, + cygwin_support = false, + }, + diagnostics = { + enable = false, + show_on_dirs = false, + show_on_open_dirs = true, + debounce_delay = 500, + severity = { + min = vim.diagnostic.severity.HINT, + max = vim.diagnostic.severity.ERROR, + }, + icons = { + hint = "", + info = "", + warning = "", + error = "", + }, + }, + modified = { + enable = false, + show_on_dirs = true, + show_on_open_dirs = true, + }, + filters = { + enable = true, + git_ignored = true, + dotfiles = false, + git_clean = false, + no_buffer = false, + no_bookmark = false, + custom = {}, + exclude = {}, + }, + live_filter = { + prefix = "[FILTER]: ", + always_show_folders = true, + }, + filesystem_watchers = { + enable = true, + debounce_delay = 50, + ignore_dirs = { + "/.ccls-cache", + "/build", + "/node_modules", + "/target", + }, + }, + actions = { + use_system_clipboard = true, + change_dir = { + enable = true, + global = false, + restrict_above_cwd = false, + }, + expand_all = { + max_folder_discovery = 300, + exclude = {}, + }, + file_popup = { + open_win_config = { + col = 1, + row = 1, + relative = "cursor", + border = "shadow", + style = "minimal", + }, + }, + open_file = { + quit_on_open = false, + eject = true, + resize_window = true, + relative_path = true, + window_picker = { + enable = true, + picker = "default", + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + exclude = { + filetype = { "notify", "packer", "qf", "diff", "fugitive", "fugitiveblame" }, + buftype = { "nofile", "terminal", "help" }, + }, + }, + }, + remove_file = { + close_window = true, + }, + }, + trash = { + cmd = "gio trash", + }, + tab = { + sync = { + open = false, + close = false, + ignore = {}, + }, + }, + notify = { + threshold = vim.log.levels.INFO, + absolute_path = true, + }, + help = { + sort_by = "key", + }, + ui = { + confirm = { + remove = true, + trash = true, + default_yes = false, + }, + }, + experimental = { + }, + log = { + enable = false, + truncate = false, + types = { + all = false, + config = false, + copy_paste = false, + dev = false, + diagnostics = false, + git = false, + profile = false, + watcher = false, + }, + }, + } -- END_DEFAULT_OPTS +< + +============================================================================== + 5. OPTS *nvim-tree-opts* + +*nvim-tree.on_attach* +Runs when creating the nvim-tree buffer. Use this to set your nvim-tree +specific mappings. See |nvim-tree-mappings|. +When on_attach is not a function, |nvim-tree-mappings-default| will be called. + Type: `function(bufnr) | string`, Default: `"default"` + +*nvim-tree.hijack_cursor* +Keeps the cursor on the first letter of the filename when moving in the tree. + Type: `boolean`, Default: `false` + +*nvim-tree.auto_reload_on_write* +Reloads the explorer every time a buffer is written to. + Type: `boolean`, Default: `true` + +*nvim-tree.disable_netrw* +Completely disable netrw + Type: `boolean`, Default: `false` + +It is strongly advised to eagerly disable netrw, due to race conditions at vim +startup. +Set the following at the very beginning of your `init.lua` / `init.vim`: >lua + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 +< +*nvim-tree.hijack_netrw* +Hijack netrw windows (overridden if |disable_netrw| is `true`) + Type: `boolean`, Default: `true` + +*nvim-tree.hijack_unnamed_buffer_when_opening* +Opens in place of the unnamed buffer if it's empty. + Type: `boolean`, Default: `false` + +*nvim-tree.root_dirs* +Preferred root directories. +Only relevant when `update_focused_file.update_root` is `true` + Type: `{string}`, Default: `{}` + +*nvim-tree.prefer_startup_root* +Prefer startup root directory when updating root directory of the tree. +Only relevant when `update_focused_file.update_root` is `true` + Type: `boolean`, Default: `false` + +*nvim-tree.sync_root_with_cwd* +Changes the tree root directory on `DirChanged` and refreshes the tree. + Type: `boolean`, Default: `false` + +*nvim-tree.reload_on_bufenter* +Automatically reloads the tree on `BufEnter` nvim-tree. + Type: `boolean`, Default: `false` + +*nvim-tree.respect_buf_cwd* +Will change cwd of nvim-tree to that of new buffer's when opening nvim-tree. + Type: `boolean`, Default: `false` + +*nvim-tree.select_prompts* +Use |vim.ui.select| style prompts. Necessary when using a UI prompt decorator +such as dressing.nvim or telescope-ui-select.nvim +Type: `boolean`, Default: `false` + +============================================================================== + 5.1 OPTS: SORT *nvim-tree-opts-sort* + +File and folder sorting options. + +*nvim-tree.sort.sorter* +Changes how files within the same directory are sorted. +Can be one of `"name"`, `"case_sensitive"`, `"modification_time"`, `"extension"`, +`"suffix"`, `"filetype"` or a function. +`"extension"` uses all suffixes e.g. `foo.tar.gz` -> `.tar.gz` +`"suffix"` uses the last e.g. `.gz` + Type: `string` | `function(nodes)`, Default: `"name"` + + Function may perform a sort or return a string with one of the above + methods. It is passed a table of nodes to be sorted, each node containing: + - `absolute_path`: `string` + - `executable`: `boolean` + - `extension`: `string` + - `filetype`: `string` + - `link_to`: `string` + - `name`: `string` + - `type`: `"directory"` | `"file"` | `"link"` + + Example: sort by name length: >lua + local sorter = function(nodes) + table.sort(nodes, function(a, b) + return #a.name < #b.name + end) + end +< +*nvim-tree.sort.folders_first* +Sort folders before files. Has no effect when |nvim-tree.sort.sorter| is a +function. + Type: `boolean`, Default: `true` + +*nvim-tree.sort.files_first* +Sort files before folders. Has no effect when |nvim-tree.sort.sorter| is a +function. If set to `true` it overrides |nvim-tree.sort.folders_first|. + Type: `boolean`, Default: `false` + +============================================================================== + 5.2 OPTS: VIEW *nvim-tree-opts-view* + +*nvim-tree.view.centralize_selection* +When entering nvim-tree, reposition the view so that the current node is +initially centralized, see |zz|. + Type: `boolean`, Default: `false` + +*nvim-tree.view.cursorline* +Enable |cursorline| in the tree window. + Type: `boolean`, Default: `true` + +*nvim-tree.view.debounce_delay* +Idle milliseconds before some reload / refresh operations. +Increase if you experience performance issues around screen refresh. + Type: `number`, Default: `15` (ms) + +*nvim-tree.view.side* +Side of the tree, can be `"left"`, `"right"`. + Type: `string`, Default: `"left"` + +*nvim-tree.view.preserve_window_proportions* +Preserves window proportions when opening a file. +If `false`, the height and width of windows other than nvim-tree will be equalized. + Type: `boolean`, Default: `false` + +*nvim-tree.view.number* +Print the line number in front of each line. + Type: `boolean`, Default: `false` + +*nvim-tree.view.relativenumber* +Show the line number relative to the line with the cursor in front of each line. +If the option `view.number` is also `true`, the number on the cursor line +will be the line number instead of `0`. + Type: `boolean`, Default: `false` + +*nvim-tree.view.signcolumn* +Show |signcolumn|. Value can be `"yes"`, `"auto"`, `"no"`. + Type: `string`, Default: `"yes"` + +*nvim-tree.view.width* +Width of the window: can be a `%` string, a number representing columns, a +function or a table. +A table indicates that the view should be dynamically sized based on the +longest line. + Type: `string | number | table | fun(): number|string` + Default: `30` + + *nvim-tree.view.width.min* + Minimum dynamic width. + Type: `string | number | fun(): number|string` + Default: `30` + + *nvim-tree.view.width.max* + Maximum dynamic width, -1 for unbounded. + Type: `string | number | fun(): number|string` + Default: `-1` + + *nvim-tree.view.width.padding* + Extra padding to the right. + Type: `number | fun(): number|string` + Default: `1` + +*nvim-tree.view.float* +Use nvim-tree in a floating window. + + *nvim-tree.view.float.enable* + Tree window will be floating. + Type: `boolean`, Default: `false` + + *nvim-tree.view.float.quit_on_focus_loss* + Close the floating tree window when it loses focus. + Type: `boolean`, Default: `true` + + *nvim-tree.view.float.open_win_config* + Floating window config. See |nvim_open_win| for more details. + Type: `table | function` returning a table + Default: + `{` + `relative = "editor",` + `border = "rounded",` + `width = 30,` + `height = 30,` + `row = 1,` + `col = 1,` + `}` + +============================================================================== + 5.3 OPTS: RENDERER *nvim-tree-opts-renderer* + +*nvim-tree.renderer.add_trailing* +Appends a trailing slash to folder names. + Type: `boolean`, Default: `false` + +*nvim-tree.renderer.group_empty* +Compact folders that only contain a single folder into one node. +Boolean or function that takes one argument (the relative path +of grouped folders) and returns a string to be displayed. + Type: `boolean | function(relative_path):string`, Default: `false` + +*nvim-tree.renderer.full_name* +Display node whose name length is wider than the width of nvim-tree window in +floating window. + Type: `boolean`, Default: `false` + +*nvim-tree.renderer.root_folder_label* +In what format to show root folder. See `:help filename-modifiers` for +available `string` options. +Set to `false` to hide the root folder. + Type: `string` or `boolean` or `function(root_cwd)`, Default: `":~:s?$?/..?"` + + Function is passed the absolute path of the root folder and should + return a string. e.g. >lua + my_root_folder_label = function(path) + return ".../" .. vim.fn.fnamemodify(path, ":t") + end +< +*nvim-tree.renderer.indent_width* +Number of spaces for an each tree nesting level. Minimum 1. + Type: `number`, Default: `2` + +*nvim-tree.renderer.special_files* +A list of filenames that gets highlighted with `NvimTreeSpecialFile`. + Type: `table`, Default: `{ "Cargo.toml", "Makefile", "README.md", "readme.md", }` + +*nvim-tree.renderer.hidden_display* +Show a summary of hidden files below the tree using `NvimTreeHiddenDisplay + Type: `function | string`, Default: `"none"` + + Possible string values are: + - `"none"`: Doesn't inform anything about hidden files. + - `"simple"`: Shows how many hidden files are in a folder. + - `"all"`: Shows how many files are hidden and the number of hidden + files per reason why they're hidden. + + Example `"all"`: + If a folder has 14 hidden items for various reasons, the display might + show: > + (14 total git: 5, dotfile: 9) +< + If a function is provided, it receives a table `hidden_stats` where keys are + reasons and values are the count of hidden files for that reason. + + The `hidden_stats` argument is structured as follows, where is the + number of hidden files related to the field: >lua + hidden_stats = { + bookmark = , + buf = , + custom = , + dotfile = , + git = , + live_filter = , + } +< + Example of function that can be passed: >lua + function(hidden_stats) + local total_count = 0 + for reason, count in pairs(hidden_stats) do + total_count = total_count + count + end + + if total_count > 0 then + return "(" .. tostring(total_count) .. " hidden)" + end + return nil + end +< + +*nvim-tree.renderer.symlink_destination* +Whether to show the destination of the symlink. + Type: `boolean`, Default: `true` + +*nvim-tree.renderer.decorators* +Highlighting and icons for the nodes, in increasing order of precedence. +Uses strings to specify builtin decorators otherwise specify your +`nvim_tree.api.decorator.UserDecorator` class. + Type: `nvim_tree.api.decorator.Name[]`, Default: >lua + { + "Git", + "Open", + "Hidden", + "Modified", + "Bookmark", + "Diagnostics", + "Copied", + "Cut", + } +< +*nvim-tree.renderer.highlight_git* +Enable highlight for git attributes using `NvimTreeGit*HL` highlight groups. +Requires |nvim-tree.git.enable| +Value can be `"none"`, `"icon"`, `"name"` or `"all"`. + Type: `string`, Default: `"none"` + +*nvim-tree.renderer.highlight_diagnostics* +Enable highlight for diagnostics using `NvimTreeDiagnostic*HL` highlight groups. +Requires |nvim-tree.diagnostics.enable| +Value can be `"none"`, `"icon"`, `"name"` or `"all"`. + Type: `string`, Default: `"none"` + +*nvim-tree.renderer.highlight_opened_files* +Highlight icons and/or names for |bufloaded()| files using the +`NvimTreeOpenedHL` highlight group. +See |nvim-tree-api.navigate.opened.next()| and |nvim-tree-api.navigate.opened.prev()| +Value can be `"none"`, `"icon"`, `"name"` or `"all"`. + Type: `string`, Default: `"none"` + +*nvim-tree.renderer.highlight_modified* +Highlight icons and/or names for modified files using the +`NvimTreeModifiedFile` highlight group. +Requires |nvim-tree.modified.enable| +Value can be `"none"`, `"icon"`, `"name"` or `"all"` + Type: `string`, Default `"none"` + +*nvim-tree.renderer.highlight_hidden* +Highlight icons and/or names for hidden files (dotfiles) using the +`NvimTreeHiddenFileHL` highlight group. +Value can be `"none"`, `"icon"`, `"name"` or `"all"` + Type: `string`, Default `"none"` + +*nvim-tree.renderer.highlight_bookmarks* +Highlight bookmarked using the `NvimTreeBookmarkHL` group. +Value can be `"none"`, `"icon"`, `"name"` or `"all"` + Type: `string`, Default `"none"` + +*nvim-tree.renderer.highlight_clipboard* +Enable highlight for clipboard items using the `NvimTreeCutHL` and +`NvimTreeCopiedHL` groups. +Value can be `"none"`, `"icon"`, `"name"` or `"all"`. + Type: `string`, Default: `"name"` + +*nvim-tree.renderer.indent_markers* +Configuration options for tree indent markers. + + *nvim-tree.renderer.indent_markers.enable* + Display indent markers when folders are open + Type: `boolean`, Default: `false` + + *nvim-tree.renderer.indent_markers.inline_arrows* + Display folder arrows in the same column as indent marker + when using |renderer.icons.show.folder_arrow| + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.indent_markers.icons* + Icons shown before the file/directory. Length 1. + Type: `table`, Default: >lua + { + corner = "└", + edge = "│", + item = "│", + bottom = "─", + none = " ", + } +< +*nvim-tree.renderer.icons* +Configuration options for icons. + +`renderer.icons.*_placement` options may be: +- `"before"` : before file/folder, after the file/folders icons +- `"after"` : after file/folder +- `"signcolumn"` : far left, requires |nvim-tree.view.signcolumn| enabled +- `"right_align"` : far right + + *nvim-tree.renderer.icons.web_devicons* + Configure optional plugin `"nvim-tree/nvim-web-devicons"` + + *nvim-tree.renderer.icons.web_devicons.file* + File icons. + + *nvim-tree.renderer.icons.web_devicons.file.enable* + Show icons on files. + Overrides |nvim-tree.renderer.icons.glyphs.default| + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.web_devicons.file.color* + Use icon colors for files. Overrides highlight groups. + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.web_devicons.folder* + Folder icons. + + *nvim-tree.renderer.icons.web_devicons.folder.enable* + Show icons on folders. + Overrides |nvim-tree.renderer.icons.glyphs.folder| + Type: `boolean`, Default: `false` + + *nvim-tree.renderer.icons.web_devicons.folder.color* + Use icon colors for folders. Overrides highlight groups. + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.git_placement* + Git icons placement. + Type: `string`, Default: `"before"` + + *nvim-tree.renderer.icons.diagnostics_placement* + Diganostic icon placement. + Type: `string`, Default: `"signcolumn"` + + *nvim-tree.renderer.icons.modified_placement* + Modified icon placement. + Type: `string`, Default: `"after"` + + *nvim-tree.renderer.icons.hidden_placement* + Hidden icon placement. + Type: `string`, Default: `"after"` + + *nvim-tree.renderer.icons.bookmarks_placement* + Bookmark icon placement. + Type: `string`, Default: `signcolumn` + + *nvim-tree.renderer.icons.padding* + Inserted between icon and filename. + Type: `string`, Default: `" "` + + *nvim-tree.renderer.icons.symlink_arrow* + Used as a separator between symlinks' source and target. + Type: `string`, Default: `" ➛ "` + + *nvim-tree.renderer.icons.show* + Configuration options for showing icon types. + Left to right order: file/folder, git, modified, hidden, diagnostics, bookmarked. + + *nvim-tree.renderer.icons.show.file* + Show an icon before the file name. + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.folder* + Show an icon before the folder name. + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.folder_arrow* + Show a small arrow before the folder node. Arrow will be a part of the + node when using |renderer.indent_markers|. + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.git* + Show a git status icon, see |renderer.icons.git_placement| + Requires |git.enable| `= true` + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.modified* + Show a modified icon, see |renderer.icons.modified_placement| + Requires |modified.enable| `= true` + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.hidden* + Show a hidden icon, see |renderer.icons.hidden_placement| + Type: `boolean`, Default: `false` + + *nvim-tree.renderer.icons.show.diagnostics* + Show a diagnostics status icon, see |renderer.icons.diagnostics_placement| + Requires |diagnostics.enable| `= true` + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.show.bookmarks* + Show a bookmark icon, see |renderer.icons.bookmarks_placement| + Type: `boolean`, Default: `true` + + *nvim-tree.renderer.icons.glyphs* + Configuration options for icon glyphs. + NOTE: Do not set any glyphs to more than two characters if it's going + to appear in the signcolumn. + + *nvim-tree.renderer.icons.glyphs.default* + Glyph for files. + Overridden by |nvim-tree.renderer.icons.web_devicons| if available. + Type: `string`, Default: `""` + + *nvim-tree.renderer.icons.glyphs.symlink* + Glyph for symlinks to files. + Type: `string`, Default: `""` + + *nvim-tree.renderer.icons.glyphs.modified* + Icon to display for modified files. + Type: `string`, Default: `"●"` + + *nvim-tree.renderer.icons.glyphs.hidden* + Icon to display for hidden files. + Type: `string`, Default: `"󰜌""` + + *nvim-tree.renderer.icons.glyphs.folder* + Glyphs for directories. + Overridden by |nvim-tree.renderer.icons.web_devicons| if available. + Type: `table`, Default: + `{` + `arrow_closed = "",` + `arrow_open = "",` + `default = "",` + `open = "",` + `empty = "",` + `empty_open = "",` + `symlink = "",` + `symlink_open = "",` + `}` + + *nvim-tree.renderer.icons.glyphs.git* + Glyphs for git status. + Type: `table`, Default: + `{` + `unstaged = "✗",` + `staged = "✓",` + `unmerged = "",` + `renamed = "➜",` + `untracked = "★",` + `deleted = "",` + `ignored = "◌",` + `}` + +============================================================================== + 5.4 OPTS: HIJACK DIRECTORIES *nvim-tree-opts-hijack-directories* + +*nvim-tree.hijack_directories.enable* +Enable the feature. +Disable this option if you use vim-dirvish or dirbuf.nvim. +If `hijack_netrw` and `disable_netrw` are `false`, this feature will be disabled. + Type: `boolean`, Default: `true` + +*nvim-tree.hijack_directories.auto_open* +Opens the tree if the tree was previously closed. + Type: `boolean`, Default: `true` + +============================================================================== + 5.5 OPTS: UPDATE FOCUSED FILE *nvim-tree-opts-update-focused-file* + +Update the focused file on `BufEnter`, un-collapses the folders recursively +until it finds the file. + +*nvim-tree.update_focused_file.enable* +Enable this feature. + Type: `boolean`, Default: `false` + +*nvim-tree.update_focused_file.update_root* +Update the root directory of the tree if the file is not under current +root directory. It prefers vim's cwd and `root_dirs`. +Otherwise it falls back to the folder containing the file. +Only relevant when `update_focused_file.enable` is `true` + + *nvim-tree.update_focused_file.update_root.enable* + Type: `boolean`, Default: `false` + + *nvim-tree.update_focused_file.update_root.ignore_list* + List of buffer names and filetypes that will not update the root dir + of the tree if the file isn't found under the current root directory. + Only relevant when `update_focused_file.update_root.enable` and + `update_focused_file.enable` are `true`. + Type: {string}, Default: `{}` + +*nvim-tree.update_focused_file.exclude* +A function that returns true if the file should not be focused when opening. +Takes the `BufEnter` event as an argument. see |autocmd-events| + Type: {function}, Default: `false` + +============================================================================== + 5.6 OPTS: SYSTEM OPEN *nvim-tree-opts-system-open* + +Open a file or directory in your preferred application. + +|vim.ui.open| was introduced in neovim 0.10 and is the default. + +Once nvim-tree minimum neovim version is updated to 0.10, these options will +no longer be necessary and will be removed. + +*nvim-tree.system_open.cmd* +The open command itself. + Type: `string`, Default: `""` + +neovim >= 0.10 defaults to |vim.ui.open| + +neovim < 0.10 defaults to: + UNIX: `"xdg-open"` + macOS: `"open"` + Windows: `"cmd"` + +*nvim-tree.system_open.args* +Optional argument list. + Type: {string}, Default: `{}` + +Leave empty for OS specific default: + Windows: `{ "/c", "start", '""' }` + +============================================================================== + 5.7 OPTS: GIT *nvim-tree-opts-git* + +Git operations are run in the background thus status may not immediately +appear. + +Processes will be killed if they exceed |nvim-tree.git.timeout| + +Git integration will be disabled following 5 timeouts and you will be +notified. + +*nvim-tree.git.enable* +Enable / disable the feature. + Type: `boolean`, Default: `true` + +*nvim-tree.git.show_on_dirs* +Show status icons of children when directory itself has no status icon. + Type: `boolean`, Default: `true` + +*nvim-tree.git.show_on_open_dirs* +Show status icons of children on directories that are open. +Only relevant when `git.show_on_dirs` is `true`. + Type: `boolean`, Default: `true` + +*nvim-tree.git.disable_for_dirs* +Disable git integration when git top-level matches these paths. +Strings may be relative, evaluated via |fnamemodify| `":p"` +Function is passed an absolute path and returns true for disable. + Type: `string[] | fun(path: string): boolean`, Default: `{}` + +*nvim-tree.git.timeout* +Kills the git process after some time if it takes too long. +Git integration will be disabled after 10 git jobs exceed this timeout. + Type: `number`, Default: `400` (ms) + +*nvim-tree.git.cygwin_support* +Use `cygpath` if available to resolve paths for git. + Type: `boolean`, Default: `false` + +============================================================================== + 5.8 OPTS: DIAGNOSTICS *nvim-tree-opts-diagnostics* + +LSP and COC diagnostics. + +*nvim-tree.diagnostics.enable* +Enable/disable the feature. + Type: `boolean`, Default: `false` + +*nvim-tree.diagnostics.debounce_delay* +Idle milliseconds between diagnostic event and update. + Type: `number`, Default: `500` (ms) + +*nvim-tree.diagnostics.show_on_dirs* +Show diagnostic icons on parent directories. + Type: `boolean`, Default: `false` + +*nvim-tree.diagnostics.show_on_open_dirs* +Show diagnostics icons on directories that are open. +Only relevant when `diagnostics.show_on_dirs` is `true`. + Type: `boolean`, Default: `true` + +*nvim-tree.diagnostics.severity* +Severity for which the diagnostics will be displayed. See |diagnostic-severity| + + *nvim-tree.diagnostics.severity.min* + Minimum severity. + Type: |vim.diagnostic.severity|, Default: `vim.diagnostic.severity.HINT` + + *nvim-tree.diagnostics.severity.max* + Maximum severity. + Type: |vim.diagnostic.severity|, Default: `vim.diagnostic.severity.ERROR` + +*nvim-tree.diagnostics.icons* +Icons for diagnostic severity. + Type: `table`, Default: >lua + { + hint = "", + info = "", + warning = "", + error = "" + } +< +============================================================================== + 5.9 OPTS: MODIFIED *nvim-tree-opts-modified* + +Indicate which file have unsaved modification. + +You will still need to set |renderer.icons.show.modified| `= true` or +|renderer.highlight_modified| `= true` to be able to see modified status in the +tree. + +*nvim-tree.modified.enable* +Enable / disable the feature. + Type: `boolean`, Default: `false` + +*nvim-tree.modified.show_on_dirs* +Show modified indication on directory whose children are modified. + Type: `boolean`, Default: `true` + +*nvim-tree.modified.show_on_open_dirs* +Show modified indication on open directories. +Only relevant when |modified.show_on_dirs| is `true`. + Type: `boolean`, Default: `true` + +============================================================================== + 5.10 OPTS: FILTERS *nvim-tree-opts-filters* + +File / folder filters that may be toggled. + +*nvim-tree.filters.enable* +Enable / disable all filters including live filter. +Toggle via |nvim-tree-api.tree.toggle_enable_filters()| + Type: `boolean`, Default: `true` + +*nvim-tree.filters.git_ignored* +Ignore files based on `.gitignore`. Requires |git.enable| `= true` +Toggle via |nvim-tree-api.tree.toggle_gitignore_filter()|, default `I` + Type: `boolean`, Default: `true` + +*nvim-tree.filters.dotfiles* +Do not show dotfiles: files starting with a `.` +Toggle via |nvim-tree-api.tree.toggle_hidden_filter()|, default `H` + Type: `boolean`, Default: `false` + +*nvim-tree.filters.git_clean* +Do not show files with no git status. This will show ignored files when +|nvim-tree.filters.git_ignored| is set, as they are effectively dirty. +Toggle via |nvim-tree-api.tree.toggle_git_clean_filter()|, default `C` + Type: `boolean`, Default: `false` + +*nvim-tree.filters.no_buffer* +Do not show files that have no |buflisted()| buffer. +Toggle via |nvim-tree-api.tree.toggle_no_buffer_filter()|, default `B` +For performance reasons this may not immediately update on buffer +delete/wipe. A reload or filesystem event will result in an update. + Type: `boolean`, Default: `false` + +*nvim-tree.filters.no_bookmark* +Do not show files that are not bookarked. +Toggle via |nvim-tree-api.tree.toggle_no_bookmark_filter()|, default `M` +Enabling this is not useful as there is no means yet to persist bookmarks. + Type: `boolean`, Default: `false` + +*nvim-tree.filters.custom* +Custom list of vim regex for file/directory names that will not be shown. +Backslashes must be escaped e.g. "^\\.git". See |string-match|. +Toggle via |nvim-tree-api.tree.toggle_custom_filter()|, default `U` + Type: {string} | `function(absolute_path)`, Default: `{}` + +*nvim-tree.filters.exclude* +List of directories or files to exclude from filtering: always show them. +Overrides `filters.git_ignored`, `filters.dotfiles` and `filters.custom`. + Type: {string}, Default: `{}` + +============================================================================== + 5.11 OPTS: LIVE FILTER *nvim-tree-opts-live-filter* + +Configurations for the live_filtering feature. +The live filter allows you to filter the tree nodes dynamically, based on +regex matching (see |vim.regex|). +This feature is bound to the `f` key by default. +The filter can be cleared with the `F` key by default. + +*nvim-tree.live_filter.prefix* +Prefix of the filter displayed in the buffer. + Type: `string`, Default: `"[FILTER]: "` + +*nvim-tree.live_filter.always_show_folders* +Whether to filter folders or not. + Type: `boolean`, Default: `true` + +============================================================================== + 5.12 OPTS: FILESYSTEM WATCHERS *nvim-tree-opts-filesystem-watchers* + +Will use file system watcher (libuv fs_event) to watch the filesystem for +changes. +Using this will disable BufEnter / BufWritePost events in nvim-tree which +were used to update the whole tree. With this feature, the tree will be +updated only for the appropriate folder change, resulting in better +performance. + +*nvim-tree.filesystem_watchers.enable* +Enable / disable the feature. + Type: `boolean`, Default: `true` + +*nvim-tree.filesystem_watchers.debounce_delay* +Idle milliseconds between filesystem change and action. + Type: `number`, Default: `50` (ms) + +*nvim-tree.filesystem_watchers.ignore_dirs* +List of vim regex for absolute directory paths that will not be watched or +function returning whether a path should be ignored. +Strings must be backslash escaped e.g. `"my-proj/\\.build$"`. See |string-match|. +Function is passed an absolute path. +Useful when path is not in `.gitignore` or git integration is disabled. + Type: `string[] | fun(path: string): boolean`, Default: >lua + { + "/.ccls-cache", + "/build", + "/node_modules", + "/target", + } +< +============================================================================== + 5.13 OPTS: ACTIONS *nvim-tree-opts-actions* + +*nvim-tree.actions.use_system_clipboard* +A boolean value that toggle the use of system clipboard when copy/paste +function are invoked. When enabled, copied text will be stored in registers +'+' (system), otherwise, it will be stored in '1' and '"'. + Type: `boolean`, Default: `true` + +*nvim-tree.actions.change_dir* +vim |current-directory| behaviour. + + *nvim-tree.actions.change_dir.enable* + Change the working directory when changing directories in the tree. + Type: `boolean`, Default: `true` + + *nvim-tree.actions.change_dir.global* + Use `:cd` instead of `:lcd` when changing directories. + Type: `boolean`, Default: `false` + + *nvim-tree.actions.change_dir.restrict_above_cwd* + Restrict changing to a directory above the global cwd. + Type: `boolean`, Default: `false` + +*nvim-tree.actions.expand_all* +Configuration for expand_all behaviour. + + *nvim-tree.actions.expand_all.max_folder_discovery* + Limit the number of folders being explored when expanding every folders. + Avoids hanging neovim when running this action on very large folders. + Type: `number`, Default: `300` + + *nvim-tree.actions.expand_all.exclude* + A list of directories that should not be expanded automatically. + E.g `{ ".git", "target", "build" }` etc. + Type: `table`, Default: `{}` + +*nvim-tree.actions.file_popup* +Configuration for file_popup behaviour. + + *nvim-tree.actions.file_popup.open_win_config* + Floating window config for file_popup. See |nvim_open_win| for more details. + You shouldn't define `"width"` and `"height"` values here. They will be + overridden to fit the file_popup content. + Type: `table`, Default: + `{` + `col = 1,` + `row = 1,` + `relative = "cursor",` + `border = "shadow",` + `style = "minimal",` + `}` + +*nvim-tree.actions.open_file* +Configuration options for opening a file from nvim-tree. + + *nvim-tree.actions.open_file.quit_on_open* + Closes the explorer when opening a file. + Type: `boolean`, Default: `false` + + *nvim-tree.actions.open_file.eject* + Prevent new opened file from opening in the same window as the tree. + Type: `boolean`, Default: `true` + + *nvim-tree.actions.open_file.resize_window* + Resizes the tree when opening a file. + Type: `boolean`, Default: `true` + + *nvim-tree.experimental.actions.open_file.relative_path* + Buffers opened by nvim-tree will use with relative paths instead of + absolute. + Type: `boolean`, Default: `true` + + *nvim-tree.actions.open_file.window_picker* + Window picker configuration. + + *nvim-tree.actions.open_file.window_picker.enable* + Enable the feature. If the feature is not enabled, files will open in + window from which you last opened the tree. + Type: `boolean`, Default: `true` + + *nvim-tree.actions.open_file.window_picker.picker* + Change the default window picker: a string `"default"` or a function. + The function should return the window id that will open the node, + or `nil` if an invalid window is picked or user cancelled the action. + The picker may create a new window. + Type: `string` | `function`, Default: `"default"` + e.g. s1n7ax/nvim-window-picker plugin: >lua + window_picker = { + enable = true, + picker = require("window-picker").pick_window, +< + *nvim-tree.actions.open_file.window_picker.chars* + A string of chars used as identifiers by the window picker. + Type: `string`, Default: `"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"` + + *nvim-tree.actions.open_file.window_picker.exclude* + Table of buffer option names mapped to a list of option values that + indicates to the picker that the buffer's window should not be + selectable. + Type: `table`, Default: >lua + { + filetype = { + "notify", + "packer", + "qf", + "diff", + "fugitive", + "fugitiveblame", + }, + buftype = { + "nofile", + "terminal", + "help", + }, + } +< +*nvim-tree.actions.remove_file.close_window* +Close any window displaying a file when removing the file from the tree. + Type: `boolean`, Default: `true` + +============================================================================== + 5.14 OPTS: TRASH *nvim-tree-opts-trash* + +*nvim-tree.trash.cmd* +The command used to trash items (must be installed on your system). +Default `"gio trash"` from glib2 is a commonly shipped linux package. +macOS default `"trash"` requires the homebrew package `trash` +Windows default `"trash"` requires `trash-cli` or similar + Type: `string`, Default: `"gio trash"` or `"trash"` + +============================================================================== + 5.15 OPTS: TAB *nvim-tree-opts-tab* + +*nvim-tree.tab.sync* +Configuration for syncing nvim-tree across tabs. + + *nvim-tree.tab.sync.open* + Opens the tree automatically when switching tabpage or opening a new + tabpage if the tree was previously open. + Type: `boolean`, Default: `false` + + *nvim-tree.tab.sync.close* + Closes the tree across all tabpages when the tree is closed. + Type: `boolean`, Default: `false` + + *nvim-tree.tab.sync.ignore* + List of filetypes or buffer names on new tab that will prevent + |nvim-tree.tab.sync.open| and |nvim-tree.tab.sync.close| + Type: {string}, Default: `{}` + +============================================================================== + 5.16 OPTS: NOTIFY *nvim-tree-opts-notify* + +*nvim-tree.notify.threshold* +Specify minimum notification level, uses the values from |vim.log.levels| + Type: `enum`, Default: `vim.log.levels.INFO` + +`ERROR`: hard errors e.g. failure to read from the file system. +`WARNING`: non-fatal errors e.g. unable to system open a file. +`INFO:` information only e.g. file copy path confirmation. +`DEBUG:` information for troubleshooting, e.g. failures in some window closing operations. + +*nvim-tree.notify.absolute_path* +Whether to use absolute paths or item names in fs action notifications. + Type: `boolean`, Default: `true` + +============================================================================== + 5.17 OPTS: HELP *nvim-tree-opts-help* + +*nvim-tree.help.sort_by* +Defines how mappings are sorted in the help window. +Can be `"key"` (sort alphabetically by keymap) +or `"desc"` (sort alphabetically by description). + Type: `string`, Default: `"key"` + +============================================================================== + 5.18 OPTS: UI *nvim-tree-opts-ui* + +*nvim-tree.ui.confirm* +Confirmation prompts. + + *nvim-tree.ui.confirm.remove* + Prompt before removing. + Type: `boolean`, Default: `true` + + *nvim-tree.ui.confirm.trash* + Prompt before trashing. + Type: `boolean`, Default: `true` + + *nvim-tree.ui.confirm.default_yes* + If `true` the prompt will be `"Y/n"`, otherwise `"y/N"`. + Type: `boolean`, Default: `false` + +============================================================================== + 5.19 OPTS: EXPERIMENTAL *nvim-tree-opts-experimental* + +*nvim-tree.experimental* +Experimental features that may become default or optional functionality. +In the event of a problem please disable the experiment and raise an issue. + +============================================================================== + 5.20 OPTS: LOG *nvim-tree-opts-log* + +Configuration for diagnostic logging. + +*nvim-tree.log.enable* +Enable logging to a file `nvim-tree.log` in |stdpath| `"log"`, usually +`${XDG_STATE_HOME}/nvim` + Type: `boolean`, Default: `false` + +*nvim-tree.log.truncate* +Remove existing log file at startup. + Type: `boolean`, Default: `false` + +*nvim-tree.log.types* +Specify which information to log. + + *nvim-tree.log.types.all* + Everything. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.profile* + Timing of some operations. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.config* + Options and mappings, at startup. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.copy_paste* + File copy and paste actions. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.dev* + Used for local development only. Not useful for users. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.diagnostics* + LSP and COC processing, verbose. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.git* + Git processing, verbose. + Type: `boolean`, Default: `false` + + *nvim-tree.log.types.watcher* + |nvim-tree.filesystem_watchers| processing, verbose. + Type: `boolean`, Default: `false` + +============================================================================== + 6. API *nvim-tree-api* + +Nvim-tree's public API can be used to access features. e.g. >lua + local api = require("nvim-tree.api") + api.tree.toggle() +< +This module exposes stable functionalities, it is advised to use this in order +to avoid breaking configurations due to internal breaking changes. + +The api is separated in multiple modules, which can be accessed with +`api..` + +Functions accepting {node} as their first argument will use the node under the +cursor when that argument is not present or nil. + +============================================================================== + 6.1 API TREE *nvim-tree-api.tree* + +tree.open({opts}) *nvim-tree-api.tree.open()* + Open the tree, focusing it if already open. + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {path} (string) root directory for the tree + • {current_window} (boolean) open the tree in the current window + • {winid} (number) open the tree in the specified |winid|, + overrides {current_window} + • {find_file} (boolean) find the current buffer + • {update_root} (boolean) requires {find_file}, see + |nvim-tree.update_focused_file.update_root| + +tree.toggle({opts}) *nvim-tree-api.tree.toggle()* + Open or close the tree. + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {path} (string) root directory for the tree + • {current_window} (boolean) open the tree in the current window + • {winid} (number) open the tree in the specified |winid|, + overrides {current_window} + • {find_file} (boolean) find the current buffer + • {update_root} (boolean) requires {find_file}, see + |nvim-tree.update_focused_file.update_root| + • {focus} (boolean) focus the tree when opening, default true + +tree.close() *nvim-tree-api.tree.close()* + Close the tree, affecting all tabs as per |nvim-tree.tab.sync.close| + +tree.close_in_this_tab() *nvim-tree-api.tree.close_in_this_tab()* + Close the tree in this tab only. + +tree.close_in_all_tabs() *nvim-tree-api.tree.close_in_all_tabs()* + Close the tree in all tabs. + +tree.focus() *nvim-tree-api.tree.focus()* + Focus the tree, opening it if necessary. + Retained for compatibility, use |tree.open()| with no arguments instead. + +tree.reload() *nvim-tree-api.tree.reload()* + Refresh the tree. Does nothing if closed. + +tree.resize({opts}) *nvim-tree-api.tree.resize()* + Resize the tree, persisting the new size. + Resets to |nvim-tree.view.width| when no {opts} provided. + See |:NvimTreeResize| + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {width} (table) new |nvim-tree.view.width| value + • {absolute} (number) set the width + • {relative} (number) increase or decrease the width + + Only one option is supported, in the priority order above. + {absolute} and {relative} do nothing when {width} is a function. + +tree.change_root({path}) *nvim-tree-api.tree.change_root()* + Change the tree's root to a path. + + Parameters: ~ + • {path} (string) absolute or relative path + + *nvim-tree-api.tree.change_root_to_node()* +tree.change_root_to_node({node}) + Change the tree's root to a folder node or the parent of a file node. + + Parameters: ~ + • {node} (Node) folder or file + + *nvim-tree-api.tree.change_root_to_parent()* +tree.change_root_to_parent({node}) + Change the tree's root to the parent of a node. + + Parameters: ~ + • {node} (Node) folder or file + +tree.get_node_under_cursor() *nvim-tree-api.tree.get_node_under_cursor()* + Retrieve the currently focused node. + + Return: ~ + node or nil if tree is not visible + +tree.get_nodes() *nvim-tree-api.tree.get_nodes()* + Retrieve a hierarchical list of all the nodes. This is a cloned list for + reference purposes only and should not be passed into other API functions. + + Return: ~ + table of nodes + +tree.find_file({opts}) *nvim-tree-api.tree.find_file()* + Find and focus a file or folder in the tree. Finds current buffer unless + otherwise specified. + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {buf} (string|number) absolute/relative path OR bufnr to find + • {open} (boolean) open the tree if necessary + • {current_window} (boolean) requires {open}, open in the current window + • {winid} (number) open the tree in the specified |winid|, + overrides {current_window} + • {update_root} (boolean) see |nvim-tree.update_focused_file.update_root| + • {focus} (boolean) focus the tree + +tree.search_node() *nvim-tree-api.tree.search_node()* + Open the search dialogue as per the search_node action. + +tree.collapse_all({keep_buffers}) *nvim-tree-api.tree.collapse_all()* + Collapse the tree. + + Parameters: ~ + • {keep_buffers} (boolean) do not collapse nodes with open buffers. + +tree.expand_all({node}) *nvim-tree-api.tree.expand_all()* + Recursively expand all nodes under the tree root or specified folder. + + Parameters: ~ + • {node} (Node|nil) folder + + *nvim-tree-api.tree.toggle_enable_filters()* +tree.toggle_enable_filters() + Toggle |nvim-tree.filters.enable| all filters. + + *nvim-tree-api.tree.toggle_gitignore_filter()* +tree.toggle_gitignore_filter() + Toggle |nvim-tree.filters.git_ignored| filter. + + *nvim-tree-api.tree.toggle_git_clean_filter()* +tree.toggle_git_clean_filter() + Toggle |nvim-tree.filters.git_clean| filter. + + *nvim-tree-api.tree.toggle_no_buffer_filter()* +tree.toggle_no_buffer_filter() + Toggle |nvim-tree.filters.no_buffer| filter. + + *nvim-tree-api.tree.toggle_no_bookmark_filter()* +tree.toggle_no_bookmark_filter() + Toggle |nvim-tree.filters.no_bookmark| filter. + + *nvim-tree-api.tree.toggle_custom_filter()* +tree.toggle_custom_filter() + Toggle |nvim-tree.filters.custom| filter. + + *nvim-tree-api.tree.toggle_hidden_filter()* +tree.toggle_hidden_filter() + Toggle |nvim-tree.filters.dotfiles| filter. + +tree.toggle_help() *nvim-tree-api.tree.toggle_help()* + Toggle help view. + +tree.is_tree_buf({bufnr}) *nvim-tree-api.tree.is_tree_buf()* + Checks if a buffer is an nvim-tree. + + Parameters: ~ + • {bufnr} (number|nil) buffer handle, 0 or nil for current buffer + + Return: ~ + (boolean) buffer is an nvim-tree buffer + +tree.is_visible({opts}) *nvim-tree-api.tree.is_visible()* + Checks if nvim-tree is visible on the current, specified or any tab. + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {tabpage} (number) as per |nvim_get_current_tabpage()| + • {any_tabpage} (boolean) visible on any tab, default false + + Return: ~ + (boolean) nvim-tree is visible + +tree.winid({opts}) *nvim-tree-api.tree.winid()* + Retrieve the winid of the open tree. + + Parameters: ~ + • {opts} (table) optional parameters + + Options: ~ + • {tabpage} (number|nil) tabpage, 0 or nil for current, default nil + + Return: ~ + (number) winid or nil if tree is not visible + +============================================================================== + 6.2 API FILE SYSTEM *nvim-tree-api.fs* + +fs.create({node}) *nvim-tree-api.fs.create()* + Prompt to create a file or directory. Use a trailing `/` for a directory. + Multiple directories/files may be created e.g. `foo/bar/baz` + + Parameters: ~ + • {node} (Node|nil) parent, uses the parent of a file. + +fs.remove({node}) *nvim-tree-api.fs.remove()* + Delete a file or folder from the file system. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.trash({node}) *nvim-tree-api.fs.trash()* + Trash a file or folder as per |nvim-tree.trash| + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.rename_node({node}) *nvim-tree-api.fs.rename_node()* + Prompt to rename a file or folder. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.rename({node}) *nvim-tree-api.fs.rename()* + Prompt to rename a file or folder by name. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.rename_basename({node}) *nvim-tree-api.fs.rename_basename()* + Prompt to rename a file or folder by name with extension omitted. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.rename_sub({node}) *nvim-tree-api.fs.rename_sub()* + Prompt to rename a file or folder by absolute path with name omitted. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.rename_full({node}) *nvim-tree-api.fs.rename_full()* + Prompt to rename a file or folder by absolute path. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.cut({node}) *nvim-tree-api.fs.cut()* + Cut a file or folder to the nvim-tree clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.paste({node}) *nvim-tree-api.fs.paste()* + Paste a file or folder from the nvim-tree clipboard. + + Parameters: ~ + • {node} (Node|nil) destination folder, uses the parent of a file. + +fs.copy.node({node}) *nvim-tree-api.fs.copy.node()* + Copy a file or folder from the nvim-tree clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.copy.absolute_path({node}) *nvim-tree-api.fs.copy.absolute_path()* + Copy the absolute path of a file or folder to the system clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.copy.basename({node}) *nvim-tree-api.fs.copy.basename()* + Copy the name of a file or folder with extension omitted to the system + clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.copy.filename({node}) *nvim-tree-api.fs.copy.filename()* + Copy the name of a file or folder to the system clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.copy.relative_path({node}) *nvim-tree-api.fs.copy.relative_path()* + Copy the path of a file or folder relative to the tree root to the system + clipboard. + + Parameters: ~ + • {node} (Node|nil) file or folder + +fs.clear_clipboard() *nvim-tree-api.fs.clear_clipboard()* + Clear the nvim-tree clipboard. + +fs.print_clipboard() *nvim-tree-api.fs.print_clipboard()* + Print the contents of the nvim-tree clipboard. + +============================================================================== + 6.3 API NODE *nvim-tree-api.node* + +Parameters: ~ + • {node} (Node|nil) file or folder + +node.open.edit({node}) *nvim-tree-api.node.open.edit()* + File: open as per |nvim-tree.actions.open_file| + Folder: expand or collapse + Root: change directory up + + *nvim-tree-api.node.open.replace_tree_buffer()* +node.open.replace_tree_buffer({node}) + |nvim-tree-api.node.edit()|, file will be opened in place: in the + nvim-tree window. + + *nvim-tree-api.node.open.no_window_picker()* +node.open.no_window_picker({node}) + |nvim-tree-api.node.edit()|, window picker will never be used as per + |nvim-tree.actions.open_file.window_picker.enable| `false` + +node.open.vertical({node}) *nvim-tree-api.node.open.vertical()* + |nvim-tree-api.node.edit()|, file will be opened in a new vertical split. + +node.open.horizontal({node}) *nvim-tree-api.node.open.horizontal()* + |nvim-tree-api.node.edit()|, file will be opened in a new horizontal split. + + *nvim-tree-api.node.open.toggle_group_empty()* +node.open.toggle_group_empty({node}) + Toggle |nvim-tree.renderer.group_empty| for a specific folder. + Does nothing on files. + Needs |nvim-tree.renderer.group_empty| set. + +node.open.drop({node}) *nvim-tree-api.node.open.drop()* + Switch to window with selected file if it exists. + Open file otherwise. + See: `:h :drop`. + + File: open file using `:drop` + Folder: expand or collapse + Root: change directory up + +node.open.tab({node}) *nvim-tree-api.node.open.tab()* + |nvim-tree-api.node.edit()|, file will be opened in a new tab. + + *nvim-tree-api.node.open.tab_drop()* +node.open.tab_drop({node}) + Switch to tab containing window with selected file if it exists. + Open file in new tab otherwise. + + File: open file using `tab :drop` + Folder: expand or collapse + Root: change directory up + +node.open.preview({node}) *nvim-tree-api.node.open.preview()* + |nvim-tree-api.node.edit()|, file buffer will have |bufhidden| set to `delete`. + + *nvim-tree-api.node.open.preview_no_picker()* +node.open.preview_no_picker({node}) + |nvim-tree-api.node.edit()|, file buffer will have |bufhidden| set to `delete`. + window picker will never be used as per + |nvim-tree.actions.open_file.window_picker.enable| `false` + +node.navigate.git.next({node}) *nvim-tree-api.node.navigate.git.next()* + Navigate to the next item showing git status. + + *nvim-tree-api.node.navigate.git.next_recursive()* +node.navigate.git.next_recursive({node}) + Alternative to |nvim-tree-api.node.navigate.git.next()| that navigates to + the next file showing git status, recursively. + Needs |nvim-tree.git.show_on_dirs| set. + + *nvim-tree-api.node.navigate.git.next_skip_gitignored()* +node.navigate.git.next_skip_gitignored({node}) + Same as |node.navigate.git.next()|, but skips gitignored files. + +node.navigate.git.prev({node}) *nvim-tree-api.node.navigate.git.prev()* + Navigate to the previous item showing git status. + + *nvim-tree-api.node.navigate.git.prev_recursive()* +node.navigate.git.prev_recursive({node}) + Alternative to |nvim-tree-api.node.navigate.git.prev()| that navigates to + the previous file showing git status, recursively. + Needs |nvim-tree.git.show_on_dirs| set. + + *nvim-tree-api.node.navigate.git.prev_skip_gitignored()* +node.navigate.git.prev_skip_gitignored({node}) + Same as |node.navigate.git.prev()|, but skips gitignored files. + + *nvim-tree-api.node.navigate.diagnostics.next()* +node.navigate.diagnostics.next({node}) + Navigate to the next item showing diagnostic status. + + *nvim-tree-api.node.navigate.diagnostics.next_recursive()* +node.navigate.diagnostics.next_recursive({node}) + Alternative to |nvim-tree-api.node.navigate.diagnostics.next()| that + navigates to the next file showing diagnostic status, recursively. + Needs |nvim-tree.diagnostics.show_on_dirs| set. + + *nvim-tree-api.node.navigate.diagnostics.prev()* +node.navigate.diagnostics.prev({node}) + Navigate to the next item showing diagnostic status. + + *nvim-tree-api.node.navigate.diagnostics.prev_recursive()* +node.navigate.diagnostics.prev_recursive({node}) + Alternative to |nvim-tree-api.node.navigate.diagnostics.prev()| that + navigates to the previous file showing diagnostic status, recursively. + Needs |nvim-tree.diagnostics.show_on_dirs| set. + + *nvim-tree-api.node.navigate.opened.next()* +node.navigate.opened.next({node}) + Navigate to the next |bufloaded()| item. + See |nvim-tree.renderer.highlight_opened_files| + + *nvim-tree-api.node.navigate.opened.prev()* +node.navigate.opened.prev({node}) + Navigate to the previous |bufloaded()| item. + See |nvim-tree.renderer.highlight_opened_files| + + *nvim-tree-api.node.navigate.sibling.next()* +node.navigate.sibling.next({node}) + Navigate to the next node in the current node's folder, wraps. + + *nvim-tree-api.node.navigate.sibling.prev()* +node.navigate.sibling.prev({node}) + Navigate to the previous node in the current node's folder, wraps. + + *nvim-tree-api.node.navigate.sibling.first()* +node.navigate.sibling.first({node}) + Navigate to the first node in the current node's folder. + + *nvim-tree-api.node.navigate.sibling.last()* +node.navigate.sibling.last({node}) + Navigate to the last node in the current node's folder. + + *nvim-tree-api.node.navigate.parent()* +node.navigate.parent({node}) + Navigate to the parent folder of the current node. + + *nvim-tree-api.node.navigate.parent_close()* +node.navigate.parent_close({node}) + |api.node.navigate.parent()|, closing that folder. + +node.show_info_popup({node}) *nvim-tree-api.node.show_info_popup()* + Open a popup window showing: fullpath, size, accessed, modified, created. + +node.run.cmd({node}) *nvim-tree-api.node.run.cmd()* + Enter |cmdline| with the full path of the node and the cursor at the start + of the line. + +node.run.system({node}) *nvim-tree-api.node.run.system()* + Execute |nvim-tree.system_open| + +============================================================================== + 6.4 API GIT *nvim-tree-api.git* + +git.reload() *nvim-tree-api.git.reload()* + Update the git status of the entire tree. + +============================================================================== + 6.5 API EVENTS *nvim-tree-api.events* + + *nvim-tree-api.events.subscribe()* +events.subscribe({event_type}, {callback}) + Register a handler for an event, see |nvim-tree-events| + + Parameters: ~ + • {event_type} (string) |nvim-tree-api.events.Event| + • {callback} (function) see |nvim_tree_events_kind| for parameters + +events.Event *nvim-tree-api.events.Event* + String enum: |nvim_tree_events_kind| + + +============================================================================== + 6.6 API LIVE FILTER *nvim-tree-api.live_filter* + +live_filter.start() *nvim-tree-api.live_filter.start()* + Enter |nvim-tree.live_filter| mode. + +live_filter.clear() *nvim-tree-api.live_filter.clear()* + Exit |nvim-tree.live_filter| mode. + +============================================================================== + 6.7 API MARKS *nvim-tree-api.marks* + +marks.get({node}) *nvim-tree-api.marks.get()* + Return the node if it is marked. + + Parameters: ~ + • {node} (Node) folder or file + +marks.list() *nvim-tree-api.marks.list()* + Retrieve all marked nodes. + + Return: ~ + (table) marked nodes + +marks.toggle({node}) *nvim-tree-api.marks.toggle()* + Toggle node mark. + + Parameters: ~ + • {node} (Node) folder or file + +marks.clear() *nvim-tree-api.marks.clear()* + Clear all marks. + +marks.bulk.delete() *nvim-tree-api.marks.bulk.delete()* + Delete all marked. Optionally prompts. + +marks.bulk.trash() *nvim-tree-api.marks.bulk.trash()* + Trash all marked. Optionally prompts. + +marks.bulk.move() *nvim-tree-api.marks.bulk.move()* + Prompts for a directory to move all marked nodes into. + +marks.navigate.next() *nvim-tree-api.marks.navigate.next()* + Navigate to the next marked node, wraps. + Opens files as per |nvim-tree.actions.open_file| + Works best with |nvim-tree.update_focused_file| enabled. + +marks.navigate.prev() *nvim-tree-api.marks.navigate.prev()* + As per |nvim-tree-api.marks.navigate.next()| + +marks.navigate.select() *nvim-tree-api.marks.navigate.select()* + Prompts for selection of a marked node, sorted by absolute paths. + A folder will be focused, a file will be opened. + +============================================================================== + 6.8 API CONFIG *nvim-tree-api.config* + + *nvim-tree-api.config.mappings.default_on_attach()* +config.mappings.default_on_attach({bufnr}) + Set all |nvim-tree-mappings-default|. Call from your |nvim-tree.on_attach| + + Parameters: ~ + • {bufnr} (number) nvim-tree buffer number passed to |nvim-tree.on_attach| + + *nvim-tree-api.config.mappings.get_keymap()* +config.mappings.get_keymap() + Retrieves all buffer local mappings for nvim-tree. + These are the mappings that are applied by |nvim-tree.on_attach|, which + may include default mappings. + + Return: ~ + (table) as per |nvim_buf_get_keymap()| + + *nvim-tree-api.config.mappings.get_keymap_default()* +config.mappings.get_keymap_default() + Retrieves the buffer local mappings for nvim-tree that are applied by + |nvim-tree-api.config.mappings.default_on_attach()| + + Return: ~ + (table) as per |nvim_buf_get_keymap()| + +============================================================================== + 6.9 API COMMANDS *nvim-tree-api.commands* + +commands.get() *nvim-tree-api.commands.get()* + Retrieve all commands, see |nvim-tree-commands| + + Return: ~ + (table) array containing |nvim_create_user_command()| parameters: + • {name} (string) + • {command} (function) + • {opts} (table) + +============================================================================== + 6.10 DIAGNOSTICS *nvim-tree-api.diagnostics* + +diagnostics.hi_test() *nvim-tree-api.diagnostics.hi_test()* + Open a new buffer displaying all nvim-tree highlight groups, their link + chain and concrete definition. + + Similar to `:so $VIMRUNTIME/syntax/hitest.vim` as per |:highlight| + +============================================================================== + 7. MAPPINGS *nvim-tree-mappings* + +Mappings are set via the |nvim-tree.on_attach| function, which is run upon +creating the nvim-tree buffer. Mappings are usually |nvim-tree-api| functions +however may be your own. + +When on_attach is not a function, |nvim-tree-mappings-default| will be used. + +Active mappings may be viewed via HELP, default `g?`. The mapping's description +is used when displaying HELP. + +The `on_attach` function is passed the `bufnr` of nvim-tree. Use +|vim.keymap.set()| or |nvim_set_keymap()| to define mappings as usual. e.g. >lua + + local function my_on_attach(bufnr) + local api = require("nvim-tree.api") + + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + -- copy default mappings here from defaults in next section + vim.keymap.set("n", "", api.tree.change_root_to_node, opts("CD")) + vim.keymap.set("n", "", api.node.open.replace_tree_buffer, opts("Open: In Place")) + --- + -- OR use all default mappings + api.config.mappings.default_on_attach(bufnr) + + -- remove a default + vim.keymap.del("n", "", { buffer = bufnr }) + + -- override a default + vim.keymap.set("n", "", api.tree.reload, opts("Refresh")) + + -- add your mappings + vim.keymap.set("n", "?", api.tree.toggle_help, opts("Help")) + --- + end + + require("nvim-tree").setup({ + --- + on_attach = my_on_attach, + --- + }) +< +Mouse support is defined in |KeyBindings| + +Single left mouse mappings can be achieved via ``. + +Single right / middle mouse mappings will require changes to |mousemodel| or |mouse|. + +|vim.keymap.set()| {rhs} is a `(function|string)` thus it may be necessary to +define your own function to map complex functionality e.g. >lua + + local function print_node_path() + local api = require("nvim-tree.api") + local node = api.tree.get_node_under_cursor() + print(node.absolute_path) + end + + -- on_attach + vim.keymap.set("n", "", print_node_path, opts("Print Path")) +< +============================================================================== + 7.1 MAPPINGS: DEFAULT *nvim-tree-mappings-default* + +In the absence of an |nvim-tree.on_attach| function, the following defaults +will be applied. + +You are encouraged to copy these to your own |nvim-tree.on_attach| function. >lua + + local api = require("nvim-tree.api") + + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + -- BEGIN_DEFAULT_ON_ATTACH + vim.keymap.set("n", "", api.tree.change_root_to_node, opts("CD")) + vim.keymap.set("n", "", api.node.open.replace_tree_buffer, opts("Open: In Place")) + vim.keymap.set("n", "", api.node.show_info_popup, opts("Info")) + vim.keymap.set("n", "", api.fs.rename_sub, opts("Rename: Omit Filename")) + vim.keymap.set("n", "", api.node.open.tab, opts("Open: New Tab")) + vim.keymap.set("n", "", api.node.open.vertical, opts("Open: Vertical Split")) + vim.keymap.set("n", "", api.node.open.horizontal, opts("Open: Horizontal Split")) + vim.keymap.set("n", "", api.node.navigate.parent_close, opts("Close Directory")) + vim.keymap.set("n", "", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "", api.node.open.preview, opts("Open Preview")) + vim.keymap.set("n", ">", api.node.navigate.sibling.next, opts("Next Sibling")) + vim.keymap.set("n", "<", api.node.navigate.sibling.prev, opts("Previous Sibling")) + vim.keymap.set("n", ".", api.node.run.cmd, opts("Run Command")) + vim.keymap.set("n", "-", api.tree.change_root_to_parent, opts("Up")) + vim.keymap.set("n", "a", api.fs.create, opts("Create File Or Directory")) + vim.keymap.set("n", "bd", api.marks.bulk.delete, opts("Delete Bookmarked")) + vim.keymap.set("n", "bt", api.marks.bulk.trash, opts("Trash Bookmarked")) + vim.keymap.set("n", "bmv", api.marks.bulk.move, opts("Move Bookmarked")) + vim.keymap.set("n", "B", api.tree.toggle_no_buffer_filter, opts("Toggle Filter: No Buffer")) + vim.keymap.set("n", "c", api.fs.copy.node, opts("Copy")) + vim.keymap.set("n", "C", api.tree.toggle_git_clean_filter, opts("Toggle Filter: Git Clean")) + vim.keymap.set("n", "[c", api.node.navigate.git.prev, opts("Prev Git")) + vim.keymap.set("n", "]c", api.node.navigate.git.next, opts("Next Git")) + vim.keymap.set("n", "d", api.fs.remove, opts("Delete")) + vim.keymap.set("n", "D", api.fs.trash, opts("Trash")) + vim.keymap.set("n", "E", api.tree.expand_all, opts("Expand All")) + vim.keymap.set("n", "e", api.fs.rename_basename, opts("Rename: Basename")) + vim.keymap.set("n", "]e", api.node.navigate.diagnostics.next, opts("Next Diagnostic")) + vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic")) + vim.keymap.set("n", "F", api.live_filter.clear, opts("Live Filter: Clear")) + vim.keymap.set("n", "f", api.live_filter.start, opts("Live Filter: Start")) + vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help")) + vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path")) + vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename")) + vim.keymap.set("n", "H", api.tree.toggle_hidden_filter, opts("Toggle Filter: Dotfiles")) + vim.keymap.set("n", "I", api.tree.toggle_gitignore_filter, opts("Toggle Filter: Git Ignore")) + vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling")) + vim.keymap.set("n", "K", api.node.navigate.sibling.first, opts("First Sibling")) + vim.keymap.set("n", "L", api.node.open.toggle_group_empty, opts("Toggle Group Empty")) + vim.keymap.set("n", "M", api.tree.toggle_no_bookmark_filter, opts("Toggle Filter: No Bookmark")) + vim.keymap.set("n", "m", api.marks.toggle, opts("Toggle Bookmark")) + vim.keymap.set("n", "o", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker")) + vim.keymap.set("n", "p", api.fs.paste, opts("Paste")) + vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory")) + vim.keymap.set("n", "q", api.tree.close, opts("Close")) + vim.keymap.set("n", "r", api.fs.rename, opts("Rename")) + vim.keymap.set("n", "R", api.tree.reload, opts("Refresh")) + vim.keymap.set("n", "s", api.node.run.system, opts("Run System")) + vim.keymap.set("n", "S", api.tree.search_node, opts("Search")) + vim.keymap.set("n", "u", api.fs.rename_full, opts("Rename: Full Path")) + vim.keymap.set("n", "U", api.tree.toggle_custom_filter, opts("Toggle Filter: Hidden")) + vim.keymap.set("n", "W", api.tree.collapse_all, opts("Collapse")) + vim.keymap.set("n", "x", api.fs.cut, opts("Cut")) + vim.keymap.set("n", "y", api.fs.copy.filename, opts("Copy Name")) + vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path")) + vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD")) + -- END_DEFAULT_ON_ATTACH +< +Alternatively, you may apply these default mappings from your |nvim-tree.on_attach| via +|nvim-tree-api.config.mappings.default_on_attach()| e.g. >lua + + local function my_on_attach(bufnr) + local api = require("nvim-tree.api") + + local function opts(desc) + return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true } + end + + api.config.mappings.default_on_attach(bufnr) + + -- your removals and mappings go here + end +< +============================================================================== + 8. HIGHLIGHT *nvim-tree-highlight* + +All the following highlight groups can be configured by hand. Aside from +`NvimTreeWindowPicker`, it is not advised to colorize the background of these +groups. + +Example |:highlight| >vim + :hi NvimTreeSymlink guifg=blue gui=bold,underline +< +It is recommended to enable |termguicolors| for the more pleasant 24-bit +colours. + +To view the nvim-tree highlight groups run |:NvimTreeHiTest| + +To view all active highlight groups run `:so $VIMRUNTIME/syntax/hitest.vim` +as per |:highlight| + +The `*HL` groups are additive as per |nvim-tree-opts-renderer| precedence. +Only present attributes will clobber each other. +In this example a modified, opened file will have magenta text, with cyan +undercurl: >vim + :hi NvimTreeOpenedHL guifg=magenta guisp=red gui=underline + :hi NvimTreeModifiedFileHL guisp=cyan gui=undercurl +< +To prevent usage of a highlight: + +- Before setup: link the group to `Normal` e.g. >vim + :hi NvimTreeExecFile Normal +< +- After setup: link it to `NONE`, to override the default link e.g. >lua + :hi! link NvimTreeExecFile NONE +< +============================================================================== + 8.1 HIGHLIGHT: DEFAULT *nvim-tree-highlight-default* + +|:highlight-link| `default` or |:highlight-default| define the groups on setup: + +Standard: > + NvimTreeNormal Normal + NvimTreeNormalFloat NormalFloat + NvimTreeNormalNC NormalFloat + + NvimTreeLineNr LineNr + NvimTreeWinSeparator WinSeparator + NvimTreeEndOfBuffer EndOfBuffer + NvimTreePopup Normal + NvimTreeSignColumn NvimTreeNormal + + NvimTreeCursorColumn CursorColumn + NvimTreeCursorLine CursorLine + NvimTreeCursorLineNr CursorLineNr + + NvimTreeStatusLine StatusLine + NvimTreeStatusLineNC StatusLineNC +< +File Text: > + NvimTreeExecFile SpellCap + NvimTreeImageFile SpellCap + NvimTreeSpecialFile SpellCap + NvimTreeSymlink SpellCap +< +Folder Text: > + NvimTreeRootFolder Title + NvimTreeFolderName Directory + NvimTreeEmptyFolderName Directory + NvimTreeOpenedFolderName Directory + NvimTreeSymlinkFolderName Directory +< +File Icons: > + NvimTreeFileIcon NvimTreeNormal + NvimTreeSymlinkIcon NvimTreeNormal +< +Folder Icons: > + NvimTreeFolderIcon guifg=#8094b4 ctermfg=Blue + NvimTreeOpenedFolderIcon NvimTreeFolderIcon + NvimTreeClosedFolderIcon NvimTreeFolderIcon + NvimTreeFolderArrowClosed NvimTreeIndentMarker + NvimTreeFolderArrowOpen NvimTreeIndentMarker +< +Indent: > + NvimTreeIndentMarker NvimTreeFileIcon +< +Picker: > + NvimTreeWindowPicker guifg=#ededed guibg=#4493c8 gui=bold ctermfg=White ctermbg=Cyan +< +Live Filter: > + NvimTreeLiveFilterPrefix PreProc + NvimTreeLiveFilterValue ModeMsg +< +Clipboard: > + NvimTreeCopiedHL SpellRare + NvimTreeCutHL SpellBad +< +Bookmarks: > + NvimTreeBookmarkIcon NvimTreeFolderIcon + NvimTreeBookmarkHL SpellLocal +< +Modified: > + NvimTreeModifiedIcon Type + NvimTreeModifiedFileHL NvimTreeModifiedIcon + NvimTreeModifiedFolderHL NvimTreeModifiedIcon + +Hidden: > + NvimTreeModifiedIcon Conceal + NvimTreeModifiedFileHL NvimTreeHiddenIcon + NvimTreeModifiedFolderHL NvimTreeHiddenFileHL +< +Hidden Display: > + NvimTreeHiddenDisplay Conceal +< +Opened: > + NvimTreeOpenedHL Special +< +Git Icon: > + NvimTreeGitDeletedIcon Statement + NvimTreeGitDirtyIcon Statement + NvimTreeGitIgnoredIcon Comment + NvimTreeGitMergeIcon Constant + NvimTreeGitNewIcon PreProc + NvimTreeGitRenamedIcon PreProc + NvimTreeGitStagedIcon Constant +< +Git File File Highlight: > + NvimTreeGitFileDeletedHL NvimTreeGitDeletedIcon + NvimTreeGitFileDirtyHL NvimTreeGitDirtyIcon + NvimTreeGitFileIgnoredHL NvimTreeGitIgnoredIcon + NvimTreeGitFileMergeHL NvimTreeGitMergeIcon + NvimTreeGitFileNewHL NvimTreeGitNewIcon + NvimTreeGitFileRenamedHL NvimTreeGitRenamedIcon + NvimTreeGitFileStagedHL NvimTreeGitStagedIcon +< +Git Folder Folder Highlight: > + NvimTreeGitFolderDeletedHL NvimTreeGitFileDeletedHL + NvimTreeGitFolderDirtyHL NvimTreeGitFileDirtyHL + NvimTreeGitFolderIgnoredHL NvimTreeGitFileIgnoredHL + NvimTreeGitFolderMergeHL NvimTreeGitFileMergeHL + NvimTreeGitFolderNewHL NvimTreeGitFileNewHL + NvimTreeGitFolderRenamedHL NvimTreeGitFileRenamedHL + NvimTreeGitFolderStagedHL NvimTreeGitFileStagedHL +< +Diagnostics Icon: > + NvimTreeDiagnosticErrorIcon DiagnosticError + NvimTreeDiagnosticWarnIcon DiagnosticWarn + NvimTreeDiagnosticInfoIcon DiagnosticInfo + NvimTreeDiagnosticHintIcon DiagnosticHint +< +Diagnostics File Highlight: > + NvimTreeDiagnosticErrorFileHL DiagnosticUnderlineError + NvimTreeDiagnosticWarnFileHL DiagnosticUnderlineWarn + NvimTreeDiagnosticInfoFileHL DiagnosticUnderlineInfo + NvimTreeDiagnosticHintFileHL DiagnosticUnderlineHint +< +Diagnostics Folder Highlight: > + NvimTreeDiagnosticErrorFolderHL NvimTreeDiagnosticErrorFileHL + NvimTreeDiagnosticWarnFolderHL NvimTreeDiagnosticWarnFileHL + NvimTreeDiagnosticInfoFolderHL NvimTreeDiagnosticInfoFileHL + NvimTreeDiagnosticHintFolderHL NvimTreeDiagnosticHintFileHL +< +============================================================================== + 8.2 HIGHLIGHT: OVERHAUL *nvim-tree-highlight-overhaul* + +See |nvim-tree-legacy-highlight| for old highlight group compatibility. + +2024-01-20: significant highlighting changes, some breaking: + +- Full cterm support. +- Standard vim highlight groups such |DiagnosticUnderlineError| are now the + defaults. +- Highlight groups named consistently. +- All `highlight_xxx` e.g. |nvim-tree.renderer.highlight_git| are granular, + allowing `"none"`, `"icon"`, `"name"` or `"all"` +- `highlight_xxx` has highlight groups for both File and Folder +- `highlight_xxx` is additive instead of overwriting. See + |nvim-tree-opts-renderer| for precedence. + +2024-01-29: disambiguate default highlights sharing groups: + +- NvimTreeRootFolder PreProc -> Title +- NvimTreeModified* Constant -> Type +- NvimTreeOpenedHL Constant -> Special +- NvimTreeBookmarkIcon Constant -> NvimTreeFolderIcon +- NvimTreeExecFile Constant -> SpellCap +- NvimTreeImageFile PreProc -> SpellCap +- NvimTreeSpecialFile PreProc -> SpellCap +- NvimTreeSymlink Statement -> SpellCap + +Approximate pre-overhaul values for the `SpellCap` groups may be set via: >lua + + vim.cmd([[ + :hi NvimTreeExecFile gui=bold guifg=#ffa0a0 + :hi NvimTreeSymlink gui=bold guifg=#ffff60 + :hi NvimTreeSpecialFile gui=bold,underline guifg=#ff80ff + :hi NvimTreeImageFile gui=bold guifg=#ff80ff + ]]) +< +============================================================================== + 9. EVENTS *nvim-tree-events* + +|nvim_tree_events| + +nvim-tree will dispatch events whenever an action is made. These events can be +subscribed to through handler functions. This allows for even further +customization of nvim-tree. + +A handler for an event is just a function which receives one argument, the +payload of the event. The payload is different for each event type. Refer +to |nvim_tree_registering_handlers| for more information. + +|nvim_tree_registering_handlers| + +Handlers are registered by calling |nvim-tree-api.events.subscribe()| +function with an |nvim-tree-api.events.Event| + +e.g. handler for node renamed: >lua + + local api = require("nvim-tree.api") + local Event = api.events.Event + + api.events.subscribe(Event.NodeRenamed, function(data) + print("Node renamed from " .. data.old_name .. " to " .. data.new_name) + end) +< +|nvim_tree_events_kind| + +- Event.Ready + When NvimTree has been initialized + • Note: Handler takes no parameter. + +- Event.TreeOpen + • Note: Handler takes no parameter. + +- Event.TreeClose + • Note: Handler takes no parameter. + +- Event.Resize - When NvimTree is resized. + handler parameters: ~ + size: `number` size of the view in columns. + +- Event.WillRenameNode + • Note: A node can either be a file or a directory. + handler parameters: ~ + {old_name} `{string}` Absolute path to the old node location. + {new_name} `{string}` Absolute path to the new node location. + +- Event.NodeRenamed + • Note: A node can either be a file or a directory. + handler parameters: ~ + {old_name} `{string}` Absolute path to the old node location. + {new_name} `{string}` Absolute path to the new node location. + +- Event.FileCreated + handler parameters: ~ + {fname} `{string}` Absolute path to the created file + +- Event.WillCreateFile + handler parameters: ~ + {fname} `{string}` Absolute path to the file to be + created + +- Event.FileRemoved + handler parameters: ~ + {fname} `{string}` Absolute path to the removed file. + +- Event.WillRemoveFile + handler parameters: ~ + {fname} `{string}` Absolute path to the file to be + removed + +- Event.FolderCreated + handler parameters: ~ + {folder_name} `{string}` Absolute path to the created folder. + +- Event.FolderRemoved + handler parameters: ~ + {folder_name} `{string}` Absolute path to the removed folder. + +- Event.TreeAttachedPost + Invoked after the tree's buffer has been created and mappings + have been applied: |nvim-tree-mappings| or |nvim-tree.on_attach| + handler parameters: ~ + {buf} `{number} `API buffer handle (buffer number) + +- Event.TreeRendered + Invoked every time the tree is redrawn. Normally this event + happens after |Event.TreeOpen| except that handlers of this + one will have access to the tree buffer populated with the + final content. + handler parameters: ~ + {bufnr} `{number} `API buffer handle (buffer number) + {winnr} `{number} `API window handle (window number) + +|nvim_tree_events_startup| + +There are two special startup events in the form of User autocommands: + +`NvimTreeRequired` first `require("nvim-tree")` +`NvimTreeSetup` `setup({})` completed + +Immediately before firing: a global variable of the same name will be set to a +value of 1. + +Example subscription: >lua + + vim.api.nvim_create_autocmd("User", { + pattern = "NvimTreeRequired", + callback = function(data) + --- + end, + }) +< +============================================================================== + 10. PROMPTS *nvim-tree-prompts* + +Some NvimTree actions use the builtin |vim.ui.select| prompt API for +confirmations when the |nvim_tree.select_prompts| option is set. + +The API accepts the optional `kind` key as part of the {opts} parameter, which +can can be used to identify the type of prompt, to allow user side +configurations for different types of prompts. + +- `nvimtree_overwrite_rename` + overwrite or rename during |nvim-tree-api.fs.paste()| + +- `nvimtree_remove` + delete during |nvim-tree-api.fs.remove()| + +- `nvimtree_trash` + send to trash during |nvim-tree-api.fs.trash()| + +- `nvimtree_bulk_delete` + delete all bookmarked during |nvim-tree-api.marks.bulk.delete()| + +- `nvimtree_bulk_trash` + send all bookmarked to trash during |nvim-tree-api.marks.bulk.trash()| + +============================================================================== + 11. DECORATORS *nvim-tree-decorators* + +Highlighting and icons for nodes are provided by Decorators. You may provide +your own in addition to the builtin decorators. + +Decorators may: +- Add icons +- Set highlight group for the name or icons +- Override node icon + +Specify decorators and their precedence via |nvim-tree.renderer.decorators| +e.g. defaults with a user decorator class being overridden only by Cut: >lua + { + "Git", + "Open", + "Hidden", + "Modified", + "Bookmark", + "Diagnostics", + "Copied", + MyDecorator, + "Cut", + } + +See `nvim-tree/_meta/api_decorator.lua` for full +`nvim_tree.api.decorator.UserDecorator` class documentation. +< +============================================================================== + 11.1. DECORATOR EXAMPLE *nvim-tree-decorator-example* +>lua + ---Create your decorator class + ---@class (exact) MyDecorator: nvim_tree.api.decorator.UserDecorator + ---@field private my_icon nvim_tree.api.HighlightedString + local MyDecorator = require("nvim-tree.api").decorator.UserDecorator:extend() + + ---Mandatory constructor :new() will be called once per tree render, with no arguments. + function MyDecorator:new() + self.enabled = true + self.highlight_range = "all" + self.icon_placement = "signcolumn" + + -- create your icon once, for convenience + self.my_icon = { str = "I", hl = { "MyIcon" } } + + -- Define the icon sign only once + -- Only needed if you are using icon_placement = "signcolumn" + self:define_sign(self.my_icon) + end + + ---Override node icon + ---@param node nvim_tree.api.Node + ---@return nvim_tree.api.HighlightedString? icon_node + function MyDecorator:icon_node(node) + if node.name == "example" then + return self.my_icon + else + return nil + end + end + + ---Return one icon for DecoratorIconPlacement + ---@param node nvim_tree.api.Node + ---@return nvim_tree.api.HighlightedString[]? icons + function MyDecorator:icons(node) + if node.name == "example" then + return { self.my_icon } + else + return nil + end + end + + ---Exactly one highlight group for DecoratorHighlightRange + ---@param node nvim_tree.api.Node + ---@return string? highlight_group + function MyDecorator:highlight_group(node) + if node.name == "example" then + return "MyHighlight" + else + return nil + end + end +< +============================================================================== + 12. OS SPECIFIC RESTRICTIONS *nvim-tree-os-specific* + +Windows WSL and PowerShell +- Trash is synchronized +- Executable file detection is disabled as this is non-performant and can + freeze nvim +- Some filesystem watcher error related to permissions will not be reported +- Some users have reported unspecified issues with + |nvim-tree.experimental.actions.open_file.relative_path|. Please report any + issues or disable this feature. + +============================================================================== + 13. NETRW *nvim-tree-netrw* + +|netrw| is a standard neovim plugin that is enabled by default. It provides, +amongst other functionality, a file/directory browser. + +It interferes with nvim-tree and the intended user experience is nvim-tree +replacing the |netrw| browser. + +It is strongly recommended to disable |netrw|. As it is a bundled plugin it +must be disabled manually at the start of your `init.lua` as per |netrw-noload|: >lua + + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 +< +There are many |netrw| features beyond the file browser. If you want to +keep using |netrw| without its browser features please ensure: + +|nvim-tree.disable_netrw| `= false` +|nvim-tree.hijack_netrw| ` = true` + +============================================================================== + 14. LEGACY *nvim-tree-legacy* + +Breaking refactors have been made however the legacy versions will be silently +migrated and used. +There are no plans to remove this migration. + +============================================================================== + 14.1 LEGACY: OPTS *nvim-tree-legacy-opts* + +Legacy options are translated to the current, making type and value changes as +needed. + +`update_cwd` |nvim-tree.sync_root_with_cwd| +`update_focused_file.update_cwd` |nvim-tree.update_focused_file.update_root| +`open_on_tab` |nvim-tree.tab.sync.open| +`ignore_buf_on_tab_change` |nvim-tree.tab.sync.ignore| +`renderer.root_folder_modifier` |nvim-tree.renderer.root_folder_label| +`update_focused_file.debounce_delay` |nvim-tree.view.debounce_delay| +`trash.require_confirm` |nvim-tree.ui.confirm.trash| +`view.adaptive_size` |nvim-tree.view.width| +`sort_by` |nvim-tree.sort.sorter| +`git.ignore` |nvim-tree.filters.git_ignored| +`renderer.icons.webdev_colors` |nvim-tree.renderer.icons.web_devicons.file.color| + +============================================================================== + 14.2 LEGACY: HIGHLIGHT *nvim-tree-legacy-highlight* + +Legacy highlight group are still obeyed when they are defined and the current +highlight group is not, hard linking as follows: > + + NvimTreeModifiedIcon NvimTreeModifiedFile + NvimTreeOpenedHL NvimTreeOpenedFile + NvimTreeBookmarkIcon NvimTreeBookmark + + NvimTreeGitDeletedIcon NvimTreeGitDeleted + NvimTreeGitDirtyIcon NvimTreeGitDirty + NvimTreeGitIgnoredIcon NvimTreeGitIgnored + NvimTreeGitMergeIcon NvimTreeGitMerge + NvimTreeGitNewIcon NvimTreeGitNew + NvimTreeGitRenamedIcon NvimTreeGitRenamed + NvimTreeGitStagedIcon NvimTreeGitStaged + + NvimTreeGitFileDeletedHL NvimTreeFileDeleted + NvimTreeGitFileDirtyHL NvimTreeFileDirty + NvimTreeGitFileIgnoredHL NvimTreeFileIgnored + NvimTreeGitFileMergeHL NvimTreeFileMerge + NvimTreeGitFileNewHL NvimTreeFileNew + NvimTreeGitFileRenamedHL NvimTreeFileRenamed + NvimTreeGitFileStagedHL NvimTreeFileStaged + + NvimTreeGitFolderDeletedHL NvimTreeFolderDeleted + NvimTreeGitFolderDirtyHL NvimTreeFolderDirty + NvimTreeGitFolderIgnoredHL NvimTreeFolderIgnored + NvimTreeGitFolderMergeHL NvimTreeFolderMerge + NvimTreeGitFolderNewHL NvimTreeFolderNew + NvimTreeGitFolderRenamedHL NvimTreeFolderRenamed + NvimTreeGitFolderStagedHL NvimTreeFolderStaged + + NvimTreeLspDiagnosticsError NvimTreeDiagnosticErrorIcon + NvimTreeLspDiagnosticsWarning NvimTreeDiagnosticWarnIcon + NvimTreeLspDiagnosticsInformation NvimTreeDiagnosticInfoIcon + NvimTreeLspDiagnosticsHint NvimTreeDiagnosticHintIcon + + NvimTreeLspDiagnosticsErrorText NvimTreeDiagnosticErrorFileHL + NvimTreeLspDiagnosticsWarningText NvimTreeDiagnosticWarnFileHL + NvimTreeLspDiagnosticsInformationText NvimTreeDiagnosticInfoFileHL + NvimTreeLspDiagnosticsHintText NvimTreeDiagnosticHintFileHL + + NvimTreeLspDiagnosticsErrorFolderText NvimTreeDiagnosticErrorFolderHL + NvimTreeLspDiagnosticsWarningFolderText NvimTreeDiagnosticWarnFolderHL + NvimTreeLspDiagnosticsInformationFolderText NvimTreeDiagnosticInfoFolderHL + NvimTreeLspDiagnosticsHintFolderText NvimTreeDiagnosticHintFolderHL +< +============================================================================== + 15 INDEX *nvim-tree-index* + +============================================================================== + 15.1 INDEX: OPTS *nvim-tree-index-opts* + +|nvim-tree.actions.change_dir| +|nvim-tree.actions.change_dir.enable| +|nvim-tree.actions.change_dir.global| +|nvim-tree.actions.change_dir.restrict_above_cwd| +|nvim-tree.actions.expand_all| +|nvim-tree.actions.expand_all.exclude| +|nvim-tree.actions.expand_all.max_folder_discovery| +|nvim-tree.actions.file_popup| +|nvim-tree.actions.file_popup.open_win_config| +|nvim-tree.actions.open_file| +|nvim-tree.actions.open_file.eject| +|nvim-tree.actions.open_file.quit_on_open| +|nvim-tree.actions.open_file.resize_window| +|nvim-tree.actions.open_file.window_picker| +|nvim-tree.actions.open_file.window_picker.chars| +|nvim-tree.actions.open_file.window_picker.enable| +|nvim-tree.actions.open_file.window_picker.exclude| +|nvim-tree.actions.open_file.window_picker.picker| +|nvim-tree.actions.remove_file.close_window| +|nvim-tree.actions.use_system_clipboard| +|nvim-tree.auto_reload_on_write| +|nvim-tree.diagnostics.debounce_delay| +|nvim-tree.diagnostics.enable| +|nvim-tree.diagnostics.icons| +|nvim-tree.diagnostics.severity| +|nvim-tree.diagnostics.severity.max| +|nvim-tree.diagnostics.severity.min| +|nvim-tree.diagnostics.show_on_dirs| +|nvim-tree.diagnostics.show_on_open_dirs| +|nvim-tree.disable_netrw| +|nvim-tree.experimental| +|nvim-tree.experimental.actions.open_file.relative_path| +|nvim-tree.filesystem_watchers.debounce_delay| +|nvim-tree.filesystem_watchers.enable| +|nvim-tree.filesystem_watchers.ignore_dirs| +|nvim-tree.filters.custom| +|nvim-tree.filters.dotfiles| +|nvim-tree.filters.enable| +|nvim-tree.filters.exclude| +|nvim-tree.filters.git_clean| +|nvim-tree.filters.git_ignored| +|nvim-tree.filters.no_bookmark| +|nvim-tree.filters.no_buffer| +|nvim-tree.git.cygwin_support| +|nvim-tree.git.disable_for_dirs| +|nvim-tree.git.enable| +|nvim-tree.git.show_on_dirs| +|nvim-tree.git.show_on_open_dirs| +|nvim-tree.git.timeout| +|nvim-tree.help.sort_by| +|nvim-tree.hijack_cursor| +|nvim-tree.hijack_directories.auto_open| +|nvim-tree.hijack_directories.enable| +|nvim-tree.hijack_netrw| +|nvim-tree.hijack_unnamed_buffer_when_opening| +|nvim-tree.live_filter.always_show_folders| +|nvim-tree.live_filter.prefix| +|nvim-tree.log.enable| +|nvim-tree.log.truncate| +|nvim-tree.log.types| +|nvim-tree.log.types.all| +|nvim-tree.log.types.config| +|nvim-tree.log.types.copy_paste| +|nvim-tree.log.types.dev| +|nvim-tree.log.types.diagnostics| +|nvim-tree.log.types.git| +|nvim-tree.log.types.profile| +|nvim-tree.log.types.watcher| +|nvim-tree.modified.enable| +|nvim-tree.modified.show_on_dirs| +|nvim-tree.modified.show_on_open_dirs| +|nvim-tree.notify.threshold| +|nvim-tree.on_attach| +|nvim-tree.prefer_startup_root| +|nvim-tree.reload_on_bufenter| +|nvim-tree.renderer.add_trailing| +|nvim-tree.renderer.decorators| +|nvim-tree.renderer.full_name| +|nvim-tree.renderer.group_empty| +|nvim-tree.renderer.hidden_display| +|nvim-tree.renderer.highlight_bookmarks| +|nvim-tree.renderer.highlight_clipboard| +|nvim-tree.renderer.highlight_diagnostics| +|nvim-tree.renderer.highlight_git| +|nvim-tree.renderer.highlight_hidden| +|nvim-tree.renderer.highlight_modified| +|nvim-tree.renderer.highlight_opened_files| +|nvim-tree.renderer.icons| +|nvim-tree.renderer.icons.bookmarks_placement| +|nvim-tree.renderer.icons.diagnostics_placement| +|nvim-tree.renderer.icons.git_placement| +|nvim-tree.renderer.icons.glyphs| +|nvim-tree.renderer.icons.glyphs.default| +|nvim-tree.renderer.icons.glyphs.folder| +|nvim-tree.renderer.icons.glyphs.git| +|nvim-tree.renderer.icons.glyphs.hidden| +|nvim-tree.renderer.icons.glyphs.modified| +|nvim-tree.renderer.icons.glyphs.symlink| +|nvim-tree.renderer.icons.hidden_placement| +|nvim-tree.renderer.icons.modified_placement| +|nvim-tree.renderer.icons.padding| +|nvim-tree.renderer.icons.show| +|nvim-tree.renderer.icons.show.bookmarks| +|nvim-tree.renderer.icons.show.diagnostics| +|nvim-tree.renderer.icons.show.file| +|nvim-tree.renderer.icons.show.folder| +|nvim-tree.renderer.icons.show.folder_arrow| +|nvim-tree.renderer.icons.show.git| +|nvim-tree.renderer.icons.show.hidden| +|nvim-tree.renderer.icons.show.modified| +|nvim-tree.renderer.icons.symlink_arrow| +|nvim-tree.renderer.icons.web_devicons| +|nvim-tree.renderer.icons.web_devicons.file| +|nvim-tree.renderer.icons.web_devicons.file.color| +|nvim-tree.renderer.icons.web_devicons.file.enable| +|nvim-tree.renderer.icons.web_devicons.folder| +|nvim-tree.renderer.icons.web_devicons.folder.color| +|nvim-tree.renderer.icons.web_devicons.folder.enable| +|nvim-tree.renderer.indent_markers| +|nvim-tree.renderer.indent_markers.enable| +|nvim-tree.renderer.indent_markers.icons| +|nvim-tree.renderer.indent_markers.inline_arrows| +|nvim-tree.renderer.indent_width| +|nvim-tree.renderer.root_folder_label| +|nvim-tree.renderer.special_files| +|nvim-tree.renderer.symlink_destination| +|nvim-tree.respect_buf_cwd| +|nvim-tree.root_dirs| +|nvim-tree.select_prompts| +|nvim-tree.sort.files_first| +|nvim-tree.sort.folders_first| +|nvim-tree.sort.sorter| +|nvim-tree.sync_root_with_cwd| +|nvim-tree.system_open.args| +|nvim-tree.system_open.cmd| +|nvim-tree.tab.sync| +|nvim-tree.tab.sync.close| +|nvim-tree.tab.sync.ignore| +|nvim-tree.tab.sync.open| +|nvim-tree.trash.cmd| +|nvim-tree.ui.confirm| +|nvim-tree.ui.confirm.default_yes| +|nvim-tree.ui.confirm.remove| +|nvim-tree.ui.confirm.trash| +|nvim-tree.update_focused_file.enable| +|nvim-tree.update_focused_file.exclude| +|nvim-tree.update_focused_file.update_root| +|nvim-tree.update_focused_file.update_root.enable| +|nvim-tree.update_focused_file.update_root.ignore_list| +|nvim-tree.view.centralize_selection| +|nvim-tree.view.cursorline| +|nvim-tree.view.debounce_delay| +|nvim-tree.view.float| +|nvim-tree.view.float.enable| +|nvim-tree.view.float.open_win_config| +|nvim-tree.view.float.quit_on_focus_loss| +|nvim-tree.view.number| +|nvim-tree.view.preserve_window_proportions| +|nvim-tree.view.relativenumber| +|nvim-tree.view.side| +|nvim-tree.view.signcolumn| +|nvim-tree.view.width| +|nvim-tree.view.width.max| +|nvim-tree.view.width.min| +|nvim-tree.view.width.padding| + +============================================================================== + 15.2 INDEX: API *nvim-tree-index-api* + +|nvim-tree-api.commands.get()| +|nvim-tree-api.config.mappings.default_on_attach()| +|nvim-tree-api.config.mappings.get_keymap()| +|nvim-tree-api.config.mappings.get_keymap_default()| +|nvim-tree-api.diagnostics.hi_test()| +|nvim-tree-api.events.subscribe()| +|nvim-tree-api.fs.clear_clipboard()| +|nvim-tree-api.fs.copy.absolute_path()| +|nvim-tree-api.fs.copy.basename()| +|nvim-tree-api.fs.copy.filename()| +|nvim-tree-api.fs.copy.node()| +|nvim-tree-api.fs.copy.relative_path()| +|nvim-tree-api.fs.create()| +|nvim-tree-api.fs.cut()| +|nvim-tree-api.fs.paste()| +|nvim-tree-api.fs.print_clipboard()| +|nvim-tree-api.fs.remove()| +|nvim-tree-api.fs.rename()| +|nvim-tree-api.fs.rename_basename()| +|nvim-tree-api.fs.rename_full()| +|nvim-tree-api.fs.rename_node()| +|nvim-tree-api.fs.rename_sub()| +|nvim-tree-api.fs.trash()| +|nvim-tree-api.git.reload()| +|nvim-tree-api.live_filter.clear()| +|nvim-tree-api.live_filter.start()| +|nvim-tree-api.marks.bulk.delete()| +|nvim-tree-api.marks.bulk.move()| +|nvim-tree-api.marks.bulk.trash()| +|nvim-tree-api.marks.clear()| +|nvim-tree-api.marks.get()| +|nvim-tree-api.marks.list()| +|nvim-tree-api.marks.navigate.next()| +|nvim-tree-api.marks.navigate.prev()| +|nvim-tree-api.marks.navigate.select()| +|nvim-tree-api.marks.toggle()| +|nvim-tree-api.node.navigate.diagnostics.next()| +|nvim-tree-api.node.navigate.diagnostics.next_recursive()| +|nvim-tree-api.node.navigate.diagnostics.prev()| +|nvim-tree-api.node.navigate.diagnostics.prev_recursive()| +|nvim-tree-api.node.navigate.git.next()| +|nvim-tree-api.node.navigate.git.next_recursive()| +|nvim-tree-api.node.navigate.git.next_skip_gitignored()| +|nvim-tree-api.node.navigate.git.prev()| +|nvim-tree-api.node.navigate.git.prev_recursive()| +|nvim-tree-api.node.navigate.git.prev_skip_gitignored()| +|nvim-tree-api.node.navigate.opened.next()| +|nvim-tree-api.node.navigate.opened.prev()| +|nvim-tree-api.node.navigate.parent()| +|nvim-tree-api.node.navigate.parent_close()| +|nvim-tree-api.node.navigate.sibling.first()| +|nvim-tree-api.node.navigate.sibling.last()| +|nvim-tree-api.node.navigate.sibling.next()| +|nvim-tree-api.node.navigate.sibling.prev()| +|nvim-tree-api.node.open.drop()| +|nvim-tree-api.node.open.edit()| +|nvim-tree-api.node.open.horizontal()| +|nvim-tree-api.node.open.no_window_picker()| +|nvim-tree-api.node.open.preview()| +|nvim-tree-api.node.open.preview_no_picker()| +|nvim-tree-api.node.open.replace_tree_buffer()| +|nvim-tree-api.node.open.tab()| +|nvim-tree-api.node.open.tab_drop()| +|nvim-tree-api.node.open.toggle_group_empty()| +|nvim-tree-api.node.open.vertical()| +|nvim-tree-api.node.run.cmd()| +|nvim-tree-api.node.run.system()| +|nvim-tree-api.node.show_info_popup()| +|nvim-tree-api.tree.change_root()| +|nvim-tree-api.tree.change_root_to_node()| +|nvim-tree-api.tree.change_root_to_parent()| +|nvim-tree-api.tree.close()| +|nvim-tree-api.tree.close_in_all_tabs()| +|nvim-tree-api.tree.close_in_this_tab()| +|nvim-tree-api.tree.collapse_all()| +|nvim-tree-api.tree.expand_all()| +|nvim-tree-api.tree.find_file()| +|nvim-tree-api.tree.focus()| +|nvim-tree-api.tree.get_nodes()| +|nvim-tree-api.tree.get_node_under_cursor()| +|nvim-tree-api.tree.is_tree_buf()| +|nvim-tree-api.tree.is_visible()| +|nvim-tree-api.tree.open()| +|nvim-tree-api.tree.reload()| +|nvim-tree-api.tree.resize()| +|nvim-tree-api.tree.search_node()| +|nvim-tree-api.tree.toggle()| +|nvim-tree-api.tree.toggle_custom_filter()| +|nvim-tree-api.tree.toggle_enable_filters()| +|nvim-tree-api.tree.toggle_git_clean_filter()| +|nvim-tree-api.tree.toggle_gitignore_filter()| +|nvim-tree-api.tree.toggle_help()| +|nvim-tree-api.tree.toggle_hidden_filter()| +|nvim-tree-api.tree.toggle_no_bookmark_filter()| +|nvim-tree-api.tree.toggle_no_buffer_filter()| +|nvim-tree-api.tree.winid()| + +============================================================================== + + vim:tw=78:ts=4:sw=4:et:ft=help:norl: diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree.lua new file mode 100644 index 0000000..09b5bac --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree.lua @@ -0,0 +1,758 @@ +local log = require("nvim-tree.log") +local view = require("nvim-tree.view") +local utils = require("nvim-tree.utils") +local actions = require("nvim-tree.actions") +local core = require("nvim-tree.core") +local notify = require("nvim-tree.notify") + +local _config = {} + +local M = { + init_root = "", +} + +--- Update the tree root to a directory or the directory containing +---@param path string relative or absolute +---@param bufnr number|nil +function M.change_root(path, bufnr) + -- skip if current file is in ignore_list + if type(bufnr) == "number" then + local ft + + if vim.fn.has("nvim-0.10") == 1 then + ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or "" + else + ft = vim.api.nvim_buf_get_option(bufnr, "filetype") or "" ---@diagnostic disable-line: deprecated + end + + for _, value in pairs(_config.update_focused_file.update_root.ignore_list) do + if utils.str_find(path, value) or utils.str_find(ft, value) then + return + end + end + end + + -- don't find inexistent + if vim.fn.filereadable(path) == 0 then + return + end + + local cwd = core.get_cwd() + if cwd == nil then + return + end + + local vim_cwd = vim.fn.getcwd() + + -- test if in vim_cwd + if utils.path_relative(path, vim_cwd) ~= path then + if vim_cwd ~= cwd then + actions.root.change_dir.fn(vim_cwd) + end + return + end + -- test if in cwd + if utils.path_relative(path, cwd) ~= path then + return + end + + -- otherwise test M.init_root + if _config.prefer_startup_root and utils.path_relative(path, M.init_root) ~= path then + actions.root.change_dir.fn(M.init_root) + return + end + -- otherwise root_dirs + for _, dir in pairs(_config.root_dirs) do + dir = vim.fn.fnamemodify(dir, ":p") + if utils.path_relative(path, dir) ~= path then + actions.root.change_dir.fn(dir) + return + end + end + -- finally fall back to the folder containing the file + actions.root.change_dir.fn(vim.fn.fnamemodify(path, ":p:h")) +end + +function M.tab_enter() + if view.is_visible({ any_tabpage = true }) then + local bufname = vim.api.nvim_buf_get_name(0) + + local ft + if vim.fn.has("nvim-0.10") == 1 then + ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) or "" + else + ft = vim.api.nvim_buf_get_option(0, "ft") ---@diagnostic disable-line: deprecated + end + + for _, filter in ipairs(M.config.tab.sync.ignore) do + if bufname:match(filter) ~= nil or ft:match(filter) ~= nil then + return + end + end + view.open({ focus_tree = false }) + + local explorer = core.get_explorer() + if explorer then + explorer.renderer:draw() + end + end +end + +function M.open_on_directory() + local should_proceed = _config.hijack_directories.auto_open or view.is_visible() + if not should_proceed then + return + end + + local buf = vim.api.nvim_get_current_buf() + local bufname = vim.api.nvim_buf_get_name(buf) + if vim.fn.isdirectory(bufname) ~= 1 then + return + end + + actions.root.change_dir.force_dirchange(bufname, true) +end + +---@return table +function M.get_config() + return M.config +end + +---@param disable_netrw boolean +---@param hijack_netrw boolean +local function manage_netrw(disable_netrw, hijack_netrw) + if hijack_netrw then + vim.cmd("silent! autocmd! FileExplorer *") + vim.cmd("autocmd VimEnter * ++once silent! autocmd! FileExplorer *") + end + if disable_netrw then + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 + end +end + +---@param name string|nil +function M.change_dir(name) + if name then + actions.root.change_dir.fn(name) + end + + if _config.update_focused_file.update_root.enable then + actions.tree.find_file.fn() + end +end + +---@param opts table +local function setup_autocommands(opts) + local augroup_id = vim.api.nvim_create_augroup("NvimTree", { clear = true }) + local function create_nvim_tree_autocmd(name, custom_opts) + local default_opts = { group = augroup_id } + vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts)) + 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, + }) + + if opts.tab.sync.open then + create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) }) + end + if opts.sync_root_with_cwd then + create_nvim_tree_autocmd("DirChanged", { + callback = function() + M.change_dir(vim.loop.cwd()) + end, + }) + end + if opts.update_focused_file.enable then + create_nvim_tree_autocmd("BufEnter", { + callback = function(event) + local exclude = opts.update_focused_file.exclude + if type(exclude) == "function" and exclude(event) then + return + end + utils.debounce("BufEnter:find_file", opts.view.debounce_delay, function() + actions.tree.find_file.fn() + end) + end, + }) + end + + if opts.hijack_directories.enable then + create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory }) + end + + if opts.view.centralize_selection then + create_nvim_tree_autocmd("BufEnter", { + pattern = "NvimTree_*", + callback = function() + vim.schedule(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]]) + end) + end) + end, + }) + end + + if opts.diagnostics.enable then + create_nvim_tree_autocmd("DiagnosticChanged", { + callback = function(ev) + log.line("diagnostics", "DiagnosticChanged") + require("nvim-tree.diagnostics").update_lsp(ev) + end, + }) + create_nvim_tree_autocmd("User", { + pattern = "CocDiagnosticChange", + callback = function() + log.line("diagnostics", "CocDiagnosticChange") + require("nvim-tree.diagnostics").update_coc() + end, + }) + end + + if opts.view.float.enable and opts.view.float.quit_on_focus_loss then + create_nvim_tree_autocmd("WinLeave", { + pattern = "NvimTree_*", + callback = function() + if utils.is_nvim_tree_buf(0) then + view.close() + end + end, + }) + end +end + +local DEFAULT_OPTS = { -- BEGIN_DEFAULT_OPTS + on_attach = "default", + hijack_cursor = false, + auto_reload_on_write = true, + disable_netrw = false, + hijack_netrw = true, + hijack_unnamed_buffer_when_opening = false, + root_dirs = {}, + prefer_startup_root = false, + sync_root_with_cwd = false, + reload_on_bufenter = false, + respect_buf_cwd = false, + select_prompts = false, + sort = { + sorter = "name", + folders_first = true, + files_first = false, + }, + view = { + centralize_selection = false, + cursorline = true, + debounce_delay = 15, + side = "left", + preserve_window_proportions = false, + number = false, + relativenumber = false, + signcolumn = "yes", + width = 30, + float = { + enable = false, + quit_on_focus_loss = true, + open_win_config = { + relative = "editor", + border = "rounded", + width = 30, + height = 30, + row = 1, + col = 1, + }, + }, + }, + renderer = { + add_trailing = false, + group_empty = false, + full_name = false, + root_folder_label = ":~:s?$?/..?", + indent_width = 2, + special_files = { "Cargo.toml", "Makefile", "README.md", "readme.md" }, + hidden_display = "none", + symlink_destination = true, + decorators = { "Git", "Open", "Hidden", "Modified", "Bookmark", "Diagnostics", "Copied", "Cut", }, + highlight_git = "none", + highlight_diagnostics = "none", + highlight_opened_files = "none", + highlight_modified = "none", + highlight_hidden = "none", + highlight_bookmarks = "none", + highlight_clipboard = "name", + indent_markers = { + enable = false, + inline_arrows = true, + icons = { + corner = "└", + edge = "│", + item = "│", + bottom = "─", + none = " ", + }, + }, + icons = { + web_devicons = { + file = { + enable = true, + color = true, + }, + folder = { + enable = false, + color = true, + }, + }, + git_placement = "before", + modified_placement = "after", + hidden_placement = "after", + diagnostics_placement = "signcolumn", + bookmarks_placement = "signcolumn", + padding = " ", + symlink_arrow = " ➛ ", + show = { + file = true, + folder = true, + folder_arrow = true, + git = true, + modified = true, + hidden = false, + diagnostics = true, + bookmarks = true, + }, + glyphs = { + default = "", + symlink = "", + bookmark = "󰆤", + modified = "●", + hidden = "󰜌", + folder = { + arrow_closed = "", + arrow_open = "", + default = "", + open = "", + empty = "", + empty_open = "", + symlink = "", + symlink_open = "", + }, + git = { + unstaged = "✗", + staged = "✓", + unmerged = "", + renamed = "➜", + untracked = "★", + deleted = "", + ignored = "◌", + }, + }, + }, + }, + hijack_directories = { + enable = true, + auto_open = true, + }, + update_focused_file = { + enable = false, + update_root = { + enable = false, + ignore_list = {}, + }, + exclude = false, + }, + system_open = { + cmd = "", + args = {}, + }, + git = { + enable = true, + show_on_dirs = true, + show_on_open_dirs = true, + disable_for_dirs = {}, + timeout = 400, + cygwin_support = false, + }, + diagnostics = { + enable = false, + show_on_dirs = false, + show_on_open_dirs = true, + debounce_delay = 500, + severity = { + min = vim.diagnostic.severity.HINT, + max = vim.diagnostic.severity.ERROR, + }, + icons = { + hint = "", + info = "", + warning = "", + error = "", + }, + }, + modified = { + enable = false, + show_on_dirs = true, + show_on_open_dirs = true, + }, + filters = { + enable = true, + git_ignored = true, + dotfiles = false, + git_clean = false, + no_buffer = false, + no_bookmark = false, + custom = {}, + exclude = {}, + }, + live_filter = { + prefix = "[FILTER]: ", + always_show_folders = true, + }, + filesystem_watchers = { + enable = true, + debounce_delay = 50, + ignore_dirs = { + "/.ccls-cache", + "/build", + "/node_modules", + "/target", + }, + }, + actions = { + use_system_clipboard = true, + change_dir = { + enable = true, + global = false, + restrict_above_cwd = false, + }, + expand_all = { + max_folder_discovery = 300, + exclude = {}, + }, + file_popup = { + open_win_config = { + col = 1, + row = 1, + relative = "cursor", + border = "shadow", + style = "minimal", + }, + }, + open_file = { + quit_on_open = false, + eject = true, + resize_window = true, + relative_path = true, + window_picker = { + enable = true, + picker = "default", + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", + exclude = { + filetype = { "notify", "packer", "qf", "diff", "fugitive", "fugitiveblame" }, + buftype = { "nofile", "terminal", "help" }, + }, + }, + }, + remove_file = { + close_window = true, + }, + }, + trash = { + cmd = "gio trash", + }, + tab = { + sync = { + open = false, + close = false, + ignore = {}, + }, + }, + notify = { + threshold = vim.log.levels.INFO, + absolute_path = true, + }, + help = { + sort_by = "key", + }, + ui = { + confirm = { + remove = true, + trash = true, + default_yes = false, + }, + }, + experimental = { + }, + log = { + enable = false, + truncate = false, + types = { + all = false, + config = false, + copy_paste = false, + dev = false, + diagnostics = false, + git = false, + profile = false, + watcher = false, + }, + }, +} -- END_DEFAULT_OPTS + +local function merge_options(conf) + return vim.tbl_deep_extend("force", DEFAULT_OPTS, conf or {}) +end + +local FIELD_SKIP_VALIDATE = { + open_win_config = true, +} + +local ACCEPTED_TYPES = { + on_attach = { "function", "string" }, + sort = { + sorter = { "function", "string" }, + }, + view = { + width = { + "string", + "function", + "number", + "table", + min = { "string", "function", "number" }, + max = { "string", "function", "number" }, + padding = { "function", "number" }, + }, + }, + renderer = { + hidden_display = { "function", "string" }, + group_empty = { "boolean", "function" }, + root_folder_label = { "function", "string", "boolean" }, + }, + update_focused_file = { + exclude = { "function" }, + }, + git = { + disable_for_dirs = { "function" }, + }, + filters = { + custom = { "function" }, + }, + filesystem_watchers = { + ignore_dirs = { "function" }, + }, + actions = { + open_file = { + window_picker = { + picker = { "function", "string" }, + }, + }, + }, +} + +local ACCEPTED_STRINGS = { + sort = { + sorter = { "name", "case_sensitive", "modification_time", "extension", "suffix", "filetype" }, + }, + view = { + side = { "left", "right" }, + signcolumn = { "yes", "no", "auto" }, + }, + renderer = { + hidden_display = { "none", "simple", "all" }, + highlight_git = { "none", "icon", "name", "all" }, + highlight_opened_files = { "none", "icon", "name", "all" }, + highlight_modified = { "none", "icon", "name", "all" }, + highlight_hidden = { "none", "icon", "name", "all" }, + highlight_bookmarks = { "none", "icon", "name", "all" }, + highlight_diagnostics = { "none", "icon", "name", "all" }, + highlight_clipboard = { "none", "icon", "name", "all" }, + icons = { + git_placement = { "before", "after", "signcolumn", "right_align" }, + modified_placement = { "before", "after", "signcolumn", "right_align" }, + hidden_placement = { "before", "after", "signcolumn", "right_align" }, + diagnostics_placement = { "before", "after", "signcolumn", "right_align" }, + bookmarks_placement = { "before", "after", "signcolumn", "right_align" }, + }, + }, + help = { + sort_by = { "key", "desc" }, + }, +} + +---@param conf table|nil +local function validate_options(conf) + local msg + + ---@param user any + ---@param def any + ---@param strs table + ---@param types table + ---@param prefix string + local function validate(user, def, strs, types, prefix) + -- if user's option is not a table there is nothing to do + if type(user) ~= "table" then + return + end + + -- only compare tables with contents that are not integer indexed + if type(def) ~= "table" or not next(def) or type(next(def)) == "number" then + -- unless the field can be a table (and is not a table in default config) + if vim.tbl_contains(types, "table") then + -- use a dummy default to allow all checks + def = {} + else + return + end + end + + for k, v in pairs(user) do + if not FIELD_SKIP_VALIDATE[k] then + local invalid + + if def[k] == nil and types[k] == nil then + -- option does not exist + invalid = string.format("Unknown option: %s%s", prefix, k) + elseif type(v) ~= type(def[k]) then + local expected + + if types[k] and #types[k] > 0 then + if not vim.tbl_contains(types[k], type(v)) then + expected = table.concat(types[k], "|") + end + else + expected = type(def[k]) + end + + if expected then + -- option is of the wrong type + invalid = string.format("Invalid option: %s%s. Expected %s, got %s", prefix, k, expected, type(v)) + end + elseif type(v) == "string" and strs[k] and not vim.tbl_contains(strs[k], v) then + -- option has type `string` but value is not accepted + invalid = string.format("Invalid value for field %s%s: '%s'", prefix, k, v) + end + + if invalid then + if msg then + msg = string.format("%s\n%s", msg, invalid) + else + msg = invalid + end + user[k] = nil + else + validate(v, def[k], strs[k] or {}, types[k] or {}, prefix .. k .. ".") + end + end + end + end + + validate(conf, DEFAULT_OPTS, ACCEPTED_STRINGS, ACCEPTED_TYPES, "") + + if msg then + notify.warn(msg .. "\n\nsee :help nvim-tree-opts for available configuration options") + end +end + +--- Apply OS specific localisations to DEFAULT_OPTS +local function localise_default_opts() + if utils.is_macos or utils.is_windows then + DEFAULT_OPTS.trash.cmd = "trash" + end +end + +function M.purge_all_state() + view.close_all_tabs() + view.abandon_all_windows() + local explorer = core.get_explorer() + if explorer then + require("nvim-tree.git").purge_state() + explorer:destroy() + core.reset_explorer() + end + -- purge orphaned that were not destroyed by their nodes + require("nvim-tree.watcher").purge_watchers() +end + +---@param conf table|nil +function M.setup(conf) + if vim.fn.has("nvim-0.9") == 0 then + notify.warn("nvim-tree.lua requires Neovim 0.9 or higher") + return + end + + M.init_root = vim.fn.getcwd() + + localise_default_opts() + + require("nvim-tree.legacy").migrate_legacy_options(conf or {}) + + validate_options(conf) + + local opts = merge_options(conf) + + local netrw_disabled = opts.disable_netrw or opts.hijack_netrw + + _config.root_dirs = opts.root_dirs + _config.prefer_startup_root = opts.prefer_startup_root + _config.update_focused_file = opts.update_focused_file + _config.hijack_directories = opts.hijack_directories + _config.hijack_directories.enable = _config.hijack_directories.enable and netrw_disabled + + manage_netrw(opts.disable_netrw, opts.hijack_netrw) + + M.config = opts + require("nvim-tree.notify").setup(opts) + require("nvim-tree.log").setup(opts) + + if log.enabled("config") then + log.line("config", "default config + user") + log.raw("config", "%s\n", vim.inspect(opts)) + end + + require("nvim-tree.actions").setup(opts) + require("nvim-tree.keymap").setup(opts) + require("nvim-tree.appearance").setup() + require("nvim-tree.diagnostics").setup(opts) + require("nvim-tree.explorer"):setup(opts) + require("nvim-tree.explorer.watch").setup(opts) + require("nvim-tree.git").setup(opts) + require("nvim-tree.git.utils").setup(opts) + require("nvim-tree.view").setup(opts) + require("nvim-tree.lib").setup(opts) + require("nvim-tree.renderer.components").setup(opts) + require("nvim-tree.buffers").setup(opts) + require("nvim-tree.help").setup(opts) + require("nvim-tree.watcher").setup(opts) + + setup_autocommands(opts) + + if vim.g.NvimTreeSetup ~= 1 then + -- first call to setup + require("nvim-tree.commands").setup() + else + -- subsequent calls to setup + M.purge_all_state() + end + + vim.g.NvimTreeSetup = 1 + vim.api.nvim_exec_autocmds("User", { pattern = "NvimTreeSetup" }) +end + +vim.g.NvimTreeRequired = 1 +vim.api.nvim_exec_autocmds("User", { pattern = "NvimTreeRequired" }) + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api.lua new file mode 100644 index 0000000..d684794 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api.lua @@ -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[] diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api_decorator.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api_decorator.lua new file mode 100644 index 0000000..d85fe02 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/_meta/api_decorator.lua @@ -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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/find-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/find-file.lua new file mode 100644 index 0000000..55e2f23 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/find-file.lua @@ -0,0 +1,97 @@ +local log = require("nvim-tree.log") +local view = require("nvim-tree.view") +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") + +local DirectoryNode = require("nvim-tree.node.directory") +local Iterator = require("nvim-tree.iterators.node-iterator") + +local M = {} + +local running = {} + +---Find a path in the tree, expand it and focus it +---@param path string relative or absolute +function M.fn(path) + local explorer = core.get_explorer() + if not explorer or not view.is_visible() then + return + end + + -- always match against the real path + local path_real = vim.loop.fs_realpath(path) + if not path_real then + return + end + + if running[path_real] then + return + end + running[path_real] = true + + local profile = log.profile_start("find file %s", path_real) + + -- refresh the contents of all parents, expanding groups as needed + if utils.get_node_from_path(path_real) == nil then + explorer:refresh_parent_nodes_for_path(vim.fn.fnamemodify(path_real, ":h")) + end + + local line = core.get_nodes_starting_line() + + local absolute_paths_searched = {} + + local found = Iterator.builder(core.get_explorer().nodes) + :matcher(function(node) + return node.absolute_path == path_real or node.link_to == path_real + end) + :applier(function(node) + local incremented_line = false + if not node.group_next then + line = line + 1 + incremented_line = true + end + + if vim.tbl_contains(absolute_paths_searched, node.absolute_path) then + return + end + table.insert(absolute_paths_searched, node.absolute_path) + + local abs_match = vim.startswith(path_real, node.absolute_path .. 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 + local dir = node:as(DirectoryNode) + if dir then + if not dir.group_next then + dir.open = true + end + if #dir.nodes == 0 then + core.get_explorer():expand(dir) + if dir.group_next and incremented_line then + line = line - 1 + end + end + end + end + end) + :recursor(function(node) + node = node and node:as(DirectoryNode) + if node then + return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) + else + return nil + end + end) + :iterate() + + if found and view.is_visible() then + explorer.renderer:draw() + view.set_cursor({ line, 0 }) + end + + running[path_real] = false + + log.profile_end(profile) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/init.lua new file mode 100644 index 0000000..55ae5a9 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/init.lua @@ -0,0 +1,6 @@ +local M = {} + +M.find_file = require("nvim-tree.actions.finders.find-file") +M.search_node = require("nvim-tree.actions.finders.search-node") + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/search-node.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/search-node.lua new file mode 100644 index 0000000..3ad65c3 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/finders/search-node.lua @@ -0,0 +1,116 @@ +local core = require("nvim-tree.core") +local find_file = require("nvim-tree.actions.finders.find-file").fn + +local M = {} + +---@param search_dir string|nil +---@param input_path string +---@return string|nil +local function search(search_dir, input_path) + local realpaths_searched = {} + local explorer = core.get_explorer() + + if not explorer then + return + end + + if not search_dir then + return + end + + ---@param dir string + ---@return string|nil + local function iter(dir) + local realpath, path, name, stat, handle, _ + + local filter_status = explorer.filters:prepare() + + handle, _ = vim.loop.fs_scandir(dir) + if not handle then + return + end + + realpath, _ = vim.loop.fs_realpath(dir) + if not realpath or vim.tbl_contains(realpaths_searched, realpath) then + return + end + table.insert(realpaths_searched, realpath) + + name, _ = vim.loop.fs_scandir_next(handle) + while name do + path = dir .. "/" .. name + + ---@type uv.fs_stat.result|nil + stat, _ = vim.loop.fs_stat(path) + if not stat then + break + end + + if not explorer.filters:should_filter(path, stat, filter_status) then + if string.find(path, "/" .. input_path .. "$") then + return path + end + + if stat.type == "directory" then + path = iter(path) + if path then + return path + end + end + end + + name, _ = vim.loop.fs_scandir_next(handle) + end + end + + return iter(search_dir) +end + +function M.fn() + if not core.get_explorer() then + return + end + + -- temporarily set &path + local bufnr = vim.api.nvim_get_current_buf() + + local path_existed, path_opt + if vim.fn.has("nvim-0.10") == 1 then + path_existed, path_opt = pcall(vim.api.nvim_get_option_value, "path", { buf = bufnr }) + vim.api.nvim_set_option_value("path", core.get_cwd() .. "/**", { buf = bufnr }) + else + path_existed, path_opt = pcall(vim.api.nvim_buf_get_option, bufnr, "path") ---@diagnostic disable-line: deprecated + vim.api.nvim_buf_set_option(bufnr, "path", core.get_cwd() .. "/**") ---@diagnostic disable-line: deprecated + end + + vim.ui.input({ prompt = "Search: ", completion = "file_in_path" }, function(input_path) + if not input_path or input_path == "" then + return + end + -- reset &path + if path_existed then + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("path", path_opt, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "path", path_opt) ---@diagnostic disable-line: deprecated + end + else + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("path", nil, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "path", nil) ---@diagnostic disable-line: deprecated + end + end + + -- strip trailing slash + input_path = string.gsub(input_path, "/$", "") + + -- search under cwd + local found = search(core.get_cwd(), input_path) + if found then + find_file(found) + end + end) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/clipboard.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/clipboard.lua new file mode 100644 index 0000000..3f3ed37 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/clipboard.lua @@ -0,0 +1,391 @@ +local lib = require("nvim-tree.lib") +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") +local events = require("nvim-tree.events") +local notify = require("nvim-tree.notify") + +local find_file = require("nvim-tree.actions.finders.find-file").fn + +local Class = require("nvim-tree.classic") +local DirectoryNode = require("nvim-tree.node.directory") + +---@alias ClipboardAction "copy" | "cut" +---@alias ClipboardData table + +---@alias ClipboardActionFn fun(source: string, dest: string): boolean, string? + +---@class (exact) Clipboard: Class +---@field private explorer Explorer +---@field private data ClipboardData +---@field private clipboard_name string +---@field private reg string +local Clipboard = Class:extend() + +---@class Clipboard +---@overload fun(args: ClipboardArgs): Clipboard + +---@class (exact) ClipboardArgs +---@field explorer Explorer + +---@protected +---@param args ClipboardArgs +function Clipboard:new(args) + self.explorer = args.explorer + + self.data = { + copy = {}, + cut = {}, + } + + self.clipboard_name = self.explorer.opts.actions.use_system_clipboard and "system" or "neovim" + self.reg = self.explorer.opts.actions.use_system_clipboard and "+" or "1" +end + +---@param source string +---@param destination string +---@return boolean +---@return string|nil +local function do_copy(source, destination) + local source_stats, err = vim.loop.fs_stat(source) + + if not source_stats then + log.line("copy_paste", "do_copy fs_stat '%s' failed '%s'", source, err) + return false, err + end + + log.line("copy_paste", "do_copy %s '%s' -> '%s'", source_stats.type, source, destination) + + if source == destination then + log.line("copy_paste", "do_copy source and destination are the same, exiting early") + return true + end + + if source_stats.type == "file" then + local success + success, err = vim.loop.fs_copyfile(source, destination) + if not success then + log.line("copy_paste", "do_copy fs_copyfile failed '%s'", err) + return false, err + end + return true + elseif source_stats.type == "directory" then + local handle + handle, err = vim.loop.fs_scandir(source) + if type(handle) == "string" then + return false, handle + elseif not handle then + log.line("copy_paste", "do_copy fs_scandir '%s' failed '%s'", source, err) + return false, err + end + + local success + success, err = vim.loop.fs_mkdir(destination, source_stats.mode) + if not success then + log.line("copy_paste", "do_copy fs_mkdir '%s' failed '%s'", destination, err) + return false, err + end + + while true do + local name, _ = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local new_name = utils.path_join({ source, name }) + local new_destination = utils.path_join({ destination, name }) + success, err = do_copy(new_name, new_destination) + if not success then + return false, err + end + end + else + err = string.format("'%s' illegal file type '%s'", source, source_stats.type) + log.line("copy_paste", "do_copy %s", err) + return false, err + end + + return true +end + +---@param source string +---@param dest string +---@param action ClipboardAction +---@param action_fn ClipboardActionFn +---@return boolean|nil -- success +---@return string|nil -- error message +local function do_single_paste(source, dest, action, action_fn) + local notify_source = notify.render_path(source) + + log.line("copy_paste", "do_single_paste '%s' -> '%s'", source, dest) + + local dest_stats, err, err_name = vim.loop.fs_stat(dest) + if not dest_stats and err_name ~= "ENOENT" then + notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (err or "???")) + return false, err + end + + local function on_process() + local success, error = action_fn(source, dest) + if not success then + notify.error("Could not " .. action .. " " .. notify_source .. " - " .. (error or "???")) + return false, error + end + + find_file(utils.path_remove_trailing(dest)) + end + + if dest_stats then + local input_opts = { + prompt = "Rename to ", + default = dest, + completion = "dir", + } + + if source == dest then + vim.ui.input(input_opts, function(new_dest) + utils.clear_prompt() + if new_dest then + do_single_paste(source, new_dest, action, action_fn) + end + end) + else + local prompt_select = "Overwrite " .. dest .. " ?" + local prompt_input = prompt_select .. " R(ename)/y/n: " + lib.prompt(prompt_input, prompt_select, { "", "y", "n" }, { "Rename", "Yes", "No" }, "nvimtree_overwrite_rename", function(item_short) + utils.clear_prompt() + if item_short == "y" then + on_process() + elseif item_short == "" or item_short == "r" then + vim.ui.input(input_opts, function(new_dest) + utils.clear_prompt() + if new_dest then + do_single_paste(source, new_dest, action, action_fn) + end + end) + end + end) + end + else + on_process() + end +end + +---@param node Node +---@param clip ClipboardData +local function toggle(node, clip) + if node.name == ".." then + return + end + local notify_node = notify.render_path(node.absolute_path) + + if utils.array_remove(clip, node) then + notify.info(notify_node .. " removed from clipboard.") + return + end + + table.insert(clip, node) + notify.info(notify_node .. " added to clipboard.") +end + +---Clear copied and cut +function Clipboard:clear_clipboard() + self.data.copy = {} + self.data.cut = {} + notify.info("Clipboard has been emptied.") + self.explorer.renderer:draw() +end + +---Copy one node +---@param node Node +function Clipboard:copy(node) + utils.array_remove(self.data.cut, node) + toggle(node, self.data.copy) + self.explorer.renderer:draw() +end + +---Cut one node +---@param node Node +function Clipboard:cut(node) + utils.array_remove(self.data.copy, node) + toggle(node, self.data.cut) + self.explorer.renderer:draw() +end + +---Paste cut or cop +---@private +---@param node Node +---@param action ClipboardAction +---@param action_fn ClipboardActionFn +function Clipboard:do_paste(node, action, action_fn) + if node.name == ".." then + node = self.explorer + else + local dir = node:as(DirectoryNode) + if dir then + node = dir:last_group_node() + end + end + local clip = self.data[action] + if #clip == 0 then + return + end + + local destination = node.absolute_path + local stats, err, err_name = vim.loop.fs_stat(destination) + if not stats and err_name ~= "ENOENT" then + log.line("copy_paste", "do_paste fs_stat '%s' failed '%s'", destination, err) + notify.error("Could not " .. action .. " " .. notify.render_path(destination) .. " - " .. (err or "???")) + return + end + local is_dir = stats and stats.type == "directory" + if not is_dir then + destination = vim.fn.fnamemodify(destination, ":p:h") + end + + for _, _node in ipairs(clip) do + local dest = utils.path_join({ destination, _node.name }) + do_single_paste(_node.absolute_path, dest, action, action_fn) + end + + self.data[action] = {} + if not self.explorer.opts.filesystem_watchers.enable then + self.explorer:reload_explorer() + end +end + +---@param source string +---@param destination string +---@return boolean +---@return string? +local function do_cut(source, destination) + log.line("copy_paste", "do_cut '%s' -> '%s'", source, destination) + + if source == destination then + log.line("copy_paste", "do_cut source and destination are the same, exiting early") + return true + end + + events._dispatch_will_rename_node(source, destination) + local success, errmsg = vim.loop.fs_rename(source, destination) + if not success then + log.line("copy_paste", "do_cut fs_rename failed '%s'", errmsg) + return false, errmsg + end + utils.rename_loaded_buffers(source, destination) + events._dispatch_node_renamed(source, destination) + return true +end + +---Paste cut (if present) or copy (if present) +---@param node Node +function Clipboard:paste(node) + if self.data.cut[1] ~= nil then + self:do_paste(node, "cut", do_cut) + elseif self.data.copy[1] ~= nil then + self:do_paste(node, "copy", do_copy) + end +end + +function Clipboard:print_clipboard() + local content = {} + if #self.data.cut > 0 then + table.insert(content, "Cut") + for _, node in pairs(self.data.cut) do + table.insert(content, " * " .. (notify.render_path(node.absolute_path))) + end + end + if #self.data.copy > 0 then + table.insert(content, "Copy") + for _, node in pairs(self.data.copy) do + table.insert(content, " * " .. (notify.render_path(node.absolute_path))) + end + end + + notify.info(table.concat(content, "\n") .. "\n") +end + +---@param content string +function Clipboard:copy_to_reg(content) + -- manually firing TextYankPost does not set vim.v.event + -- workaround: create a scratch buffer with the clipboard contents and send a yank command + local temp_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_text(temp_buf, 0, 0, 0, 0, { content }) + vim.api.nvim_buf_call(temp_buf, function() + vim.cmd(string.format('normal! "%sy$', self.reg)) + end) + vim.api.nvim_buf_delete(temp_buf, {}) + + notify.info(string.format("Copied %s to %s clipboard!", content, self.clipboard_name)) +end + +---@param node Node +function Clipboard:copy_filename(node) + if node.name == ".." then + -- root + self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t")) + else + -- node + self:copy_to_reg(node.name) + end +end + +---@param node Node +function Clipboard:copy_basename(node) + if node.name == ".." then + -- root + self:copy_to_reg(vim.fn.fnamemodify(self.explorer.absolute_path, ":t:r")) + else + -- node + self:copy_to_reg(vim.fn.fnamemodify(node.name, ":r")) + end +end + +---@param node Node +function Clipboard:copy_path(node) + if node.name == ".." then + -- root + self:copy_to_reg(utils.path_add_trailing("")) + else + -- node + local absolute_path = node.absolute_path + local cwd = core.get_cwd() + if cwd == nil then + return + end + + local relative_path = utils.path_relative(absolute_path, cwd) + if node:is(DirectoryNode) then + self:copy_to_reg(utils.path_add_trailing(relative_path)) + else + self:copy_to_reg(relative_path) + end + end +end + +---@param node Node +function Clipboard:copy_absolute_path(node) + if node.name == ".." then + node = self.explorer + end + + local absolute_path = node.absolute_path + local content = node.nodes ~= nil and utils.path_add_trailing(absolute_path) or absolute_path + self:copy_to_reg(content) +end + +---Node is cut. Will not be copied. +---@param node Node +---@return boolean +function Clipboard:is_cut(node) + return vim.tbl_contains(self.data.cut, node) +end + +---Node is copied. Will not be cut. +---@param node Node +---@return boolean +function Clipboard:is_copied(node) + return vim.tbl_contains(self.data.copy, node) +end + +return Clipboard diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/create-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/create-file.lua new file mode 100644 index 0000000..86145cd --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/create-file.lua @@ -0,0 +1,105 @@ +local utils = require("nvim-tree.utils") +local events = require("nvim-tree.events") +local core = require("nvim-tree.core") +local notify = require("nvim-tree.notify") + +local find_file = require("nvim-tree.actions.finders.find-file").fn + +local FileNode = require("nvim-tree.node.file") +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +---@param file string +local function create_and_notify(file) + events._dispatch_will_create_file(file) + local ok, fd = pcall(vim.loop.fs_open, file, "w", 420) + if not ok or type(fd) ~= "number" then + notify.error("Couldn't create file " .. notify.render_path(file)) + return + end + vim.loop.fs_close(fd) + events._dispatch_file_created(file) +end + +---@param iter function iterable +---@return integer +local function get_num_nodes(iter) + local i = 0 + for _ in iter do + i = i + 1 + end + return i +end + +---@param node Node? +function M.fn(node) + node = node or core.get_explorer() + if not node then + return + end + + local dir = node:is(FileNode) and node.parent or node:as(DirectoryNode) + if not dir then + return + end + + dir = dir:last_group_node() + + local containing_folder = utils.path_add_trailing(dir.absolute_path) + + local input_opts = { + prompt = "Create file ", + default = containing_folder, + completion = "file", + } + + vim.ui.input(input_opts, function(new_file_path) + utils.clear_prompt() + if not new_file_path or new_file_path == containing_folder then + return + end + + if utils.file_exists(new_file_path) then + notify.warn("Cannot create: file already exists") + return + end + + -- create a folder for each path element if the folder does not exist + -- if the answer ends with a /, create a file for the last path element + local is_last_path_file = not new_file_path:match(utils.path_separator .. "$") + local path_to_create = "" + local idx = 0 + + local num_nodes = get_num_nodes(utils.path_split(utils.path_remove_trailing(new_file_path))) + local is_error = false + for path in utils.path_split(new_file_path) do + idx = idx + 1 + local p = utils.path_remove_trailing(path) + if #path_to_create == 0 and vim.fn.has("win32") == 1 then + path_to_create = utils.path_join({ p, path_to_create }) + else + path_to_create = utils.path_join({ path_to_create, p }) + end + if is_last_path_file and idx == num_nodes then + create_and_notify(path_to_create) + elseif not utils.file_exists(path_to_create) then + local success = vim.loop.fs_mkdir(path_to_create, 493) + if not success then + notify.error("Could not create folder " .. notify.render_path(path_to_create)) + is_error = true + break + end + events._dispatch_folder_created(new_file_path) + end + end + if not is_error then + notify.info(notify.render_path(new_file_path) .. " was properly created") + end + + -- synchronously refreshes as we can't wait for the watchers + find_file(utils.path_remove_trailing(new_file_path)) + end) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/init.lua new file mode 100644 index 0000000..b308a86 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/init.lua @@ -0,0 +1,14 @@ +local M = {} + +M.create_file = require("nvim-tree.actions.fs.create-file") +M.remove_file = require("nvim-tree.actions.fs.remove-file") +M.rename_file = require("nvim-tree.actions.fs.rename-file") +M.trash = require("nvim-tree.actions.fs.trash") + +function M.setup(opts) + M.remove_file.setup(opts) + M.rename_file.setup(opts) + M.trash.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/remove-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/remove-file.lua new file mode 100644 index 0000000..8a0f67c --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/remove-file.lua @@ -0,0 +1,160 @@ +local core = require("nvim-tree.core") +local utils = require("nvim-tree.utils") +local events = require("nvim-tree.events") +local view = require("nvim-tree.view") +local lib = require("nvim-tree.lib") +local notify = require("nvim-tree.notify") + +local DirectoryLinkNode = require("nvim-tree.node.directory-link") +local DirectoryNode = require("nvim-tree.node.directory") + +local M = { + config = {}, +} + +---@param windows integer[] +local function close_windows(windows) + -- Prevent from closing when the win count equals 1 or 2, + -- where the win to remove could be the last opened. + -- For details see #2503. + if view.View.float.enable and #vim.api.nvim_list_wins() < 3 then + return + end + + for _, window in ipairs(windows) do + if vim.api.nvim_win_is_valid(window) then + vim.api.nvim_win_close(window, true) + end + end +end + +---@param absolute_path string +local function clear_buffer(absolute_path) + local bufs = vim.fn.getbufinfo({ bufloaded = 1, buflisted = 1 }) + for _, buf in pairs(bufs) do + if buf.name == absolute_path then + local tree_winnr = vim.api.nvim_get_current_win() + if buf.hidden == 0 and (#bufs > 1 or view.View.float.enable) then + vim.api.nvim_set_current_win(buf.windows[1]) + vim.cmd(":bn") + end + vim.api.nvim_buf_delete(buf.bufnr, { force = true }) + if not view.View.float.quit_on_focus_loss then + vim.api.nvim_set_current_win(tree_winnr) + end + if M.config.actions.remove_file.close_window then + close_windows(buf.windows) + end + return + end + end +end + +---@param cwd string +---@return boolean|nil +local function remove_dir(cwd) + local handle, err = vim.loop.fs_scandir(cwd) + if not handle then + notify.error(err) + return + end + + while true do + local name, _ = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local new_cwd = utils.path_join({ cwd, name }) + + -- Type must come from fs_stat and not fs_scandir_next to maintain sshfs compatibility + local stat = vim.loop.fs_stat(new_cwd) + local type = stat and stat.type or nil + + if type == "directory" then + local success = remove_dir(new_cwd) + if not success then + return false + end + else + local success = vim.loop.fs_unlink(new_cwd) + if not success then + return false + end + clear_buffer(new_cwd) + end + end + + return vim.loop.fs_rmdir(cwd) +end + +--- Remove a node, notify errors, dispatch events +---@param node Node +function M.remove(node) + local notify_node = notify.render_path(node.absolute_path) + if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then + local success = remove_dir(node.absolute_path) + if not success then + notify.error("Could not remove " .. notify_node) + return + end + events._dispatch_folder_removed(node.absolute_path) + else + events._dispatch_will_remove_file(node.absolute_path) + local success = vim.loop.fs_unlink(node.absolute_path) + if not success then + notify.error("Could not remove " .. notify_node) + return + end + events._dispatch_file_removed(node.absolute_path) + clear_buffer(node.absolute_path) + end + notify.info(notify_node .. " was properly removed.") +end + +---@param node Node +function M.fn(node) + if node.name == ".." then + return + end + + local function do_remove() + M.remove(node) + local explorer = core.get_explorer() + if not M.config.filesystem_watchers.enable and explorer then + explorer:reload_explorer() + end + end + + if M.config.ui.confirm.remove then + local prompt_select = "Remove " .. node.name .. "?" + local prompt_input, items_short, items_long + + if M.config.ui.confirm.default_yes then + prompt_input = prompt_select .. " Y/n: " + items_short = { "", "n" } + items_long = { "Yes", "No" } + else + prompt_input = prompt_select .. " y/N: " + items_short = { "", "y" } + items_long = { "No", "Yes" } + end + + lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_remove", function(item_short) + utils.clear_prompt() + if item_short == "y" or item_short == (M.config.ui.confirm.default_yes and "") then + do_remove() + end + end) + else + do_remove() + end +end + +function M.setup(opts) + M.config.ui = opts.ui + M.config.actions = opts.actions + M.config.filesystem_watchers = opts.filesystem_watchers +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/rename-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/rename-file.lua new file mode 100644 index 0000000..b4f9c94 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/rename-file.lua @@ -0,0 +1,181 @@ +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 find_file = require("nvim-tree.actions.finders.find-file").fn + +local DirectoryNode = require("nvim-tree.node.directory") + +local M = { + config = {}, +} + +---@param iter function iterable +---@return integer +local function get_num_nodes(iter) + local i = 0 + for _ in iter do + i = i + 1 + end + return i +end + +local ALLOWED_MODIFIERS = { + [":p"] = true, + [":p:h"] = true, + [":t"] = true, + [":t:r"] = true, +} + +local function err_fmt(from, to, reason) + return string.format("Cannot rename %s -> %s: %s", from, to, reason) +end + +local function rename_file_exists(node, to) + if not utils.is_macos then + return utils.file_exists(to) + end + + if string.lower(node) == string.lower(to) then + return false + end + + return utils.file_exists(to) +end + +---@param node Node +---@param to string +function M.rename(node, to) + local notify_from = notify.render_path(node.absolute_path) + local notify_to = notify.render_path(to) + + if rename_file_exists(notify_from, notify_to) then + notify.warn(err_fmt(notify_from, notify_to, "file already exists")) + return + end + + -- create a folder for each path element if the folder does not exist + local idx = 0 + local path_to_create = "" + + local num_nodes = get_num_nodes(utils.path_split(utils.path_remove_trailing(to))) + local is_error = false + for path in utils.path_split(to) do + idx = idx + 1 + + local p = utils.path_remove_trailing(path) + if #path_to_create == 0 and vim.fn.has("win32") == 1 then + path_to_create = utils.path_join({ p, path_to_create }) + else + path_to_create = utils.path_join({ path_to_create, p }) + end + + if idx == num_nodes then + events._dispatch_will_rename_node(node.absolute_path, to) + local success, err = vim.loop.fs_rename(node.absolute_path, to) + + if not success then + notify.warn(err_fmt(notify_from, notify_to, err)) + return + end + elseif not rename_file_exists(notify_from, path_to_create) then + local success = vim.loop.fs_mkdir(path_to_create, 493) + if not success then + notify.error("Could not create folder " .. notify.render_path(path_to_create)) + is_error = true + break + end + is_error = false + end + end + + if not is_error then + notify.info(string.format("%s -> %s", notify_from, notify_to)) + utils.rename_loaded_buffers(node.absolute_path, to) + events._dispatch_node_renamed(node.absolute_path, to) + end +end + +---@param default_modifier string|nil +---@return fun(node: Node, modifier: string) +function M.fn(default_modifier) + default_modifier = default_modifier or ":t" + + return function(node, modifier) + local explorer = core.get_explorer() + if not explorer then + return + end + + if type(node) ~= "table" then + node = explorer:get_node_at_cursor() + end + if not node then + return + end + + if type(modifier) ~= "string" then + modifier = default_modifier + end + + -- support for only specific modifiers have been implemented + if not ALLOWED_MODIFIERS[modifier] then + notify.warn("Modifier " .. vim.inspect(modifier) .. " is not in allowed list : " .. table.concat(ALLOWED_MODIFIERS, ",")) + return + end + + local dir = node:as(DirectoryNode) + if dir then + node = dir:last_group_node() + end + if node.name == ".." then + return + end + + local namelen = node.name:len() + local directory = node.absolute_path:sub(0, namelen * -1 - 1) + local default_path + local prepend = "" + local append = "" + default_path = vim.fn.fnamemodify(node.absolute_path, modifier) + if modifier:sub(0, 2) == ":t" then + prepend = directory + end + if modifier == ":t:r" then + local extension = vim.fn.fnamemodify(node.name, ":e") + append = extension:len() == 0 and "" or "." .. extension + end + if modifier == ":p:h" then + default_path = default_path .. "/" + end + + local input_opts = { + prompt = "Rename to ", + default = default_path, + completion = "file", + } + + vim.ui.input(input_opts, function(new_file_path) + utils.clear_prompt() + if not new_file_path then + return + end + + local full_new_path = prepend .. new_file_path .. append + + M.rename(node, full_new_path) + if not M.config.filesystem_watchers.enable then + explorer:reload_explorer() + end + + find_file(utils.path_remove_trailing(full_new_path)) + end) + end +end + +function M.setup(opts) + M.config.filesystem_watchers = opts.filesystem_watchers +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/trash.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/trash.lua new file mode 100644 index 0000000..c64dba0 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/fs/trash.lua @@ -0,0 +1,128 @@ +local core = require("nvim-tree.core") +local lib = require("nvim-tree.lib") +local notify = require("nvim-tree.notify") + +local DirectoryLinkNode = require("nvim-tree.node.directory-link") +local DirectoryNode = require("nvim-tree.node.directory") + +local M = { + config = {}, +} + +local utils = require("nvim-tree.utils") +local events = require("nvim-tree.events") + +---@param absolute_path string +local function clear_buffer(absolute_path) + local bufs = vim.fn.getbufinfo({ bufloaded = 1, buflisted = 1 }) + for _, buf in pairs(bufs) do + if buf.name == absolute_path then + if buf.hidden == 0 and #bufs > 1 then + local winnr = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(buf.windows[1]) + vim.cmd(":bn") + vim.api.nvim_set_current_win(winnr) + end + vim.api.nvim_buf_delete(buf.bufnr, {}) + return + end + end +end + +---@param node Node +function M.remove(node) + local binary = M.config.trash.cmd:gsub(" .*$", "") + if vim.fn.executable(binary) == 0 then + notify.warn(string.format("trash.cmd '%s' is not an executable.", M.config.trash.cmd)) + return + end + + local err_msg = "" + local function on_stderr(_, data) + err_msg = err_msg .. (data and table.concat(data, " ")) + end + + -- trashes a path (file or folder) + local function trash_path(on_exit) + local need_sync_wait = utils.is_windows + local job = vim.fn.jobstart(M.config.trash.cmd .. " " .. vim.fn.shellescape(node.absolute_path), { + detach = not need_sync_wait, + on_exit = on_exit, + on_stderr = on_stderr, + }) + if need_sync_wait then + vim.fn.jobwait({ job }) + end + end + + local explorer = core.get_explorer() + + if node:is(DirectoryNode) and not node:is(DirectoryLinkNode) then + trash_path(function(_, rc) + if rc ~= 0 then + notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") + return + end + events._dispatch_folder_removed(node.absolute_path) + if not M.config.filesystem_watchers.enable and explorer then + explorer:reload_explorer() + end + end) + else + events._dispatch_will_remove_file(node.absolute_path) + trash_path(function(_, rc) + if rc ~= 0 then + notify.warn("trash failed: " .. err_msg .. "; please see :help nvim-tree.trash") + return + end + events._dispatch_file_removed(node.absolute_path) + clear_buffer(node.absolute_path) + if not M.config.filesystem_watchers.enable and explorer then + explorer:reload_explorer() + end + end) + end +end + +---@param node Node +function M.fn(node) + if node.name == ".." then + return + end + + local function do_trash() + M.remove(node) + end + + if M.config.ui.confirm.trash then + local prompt_select = "Trash " .. node.name .. "?" + local prompt_input, items_short, items_long + + if M.config.ui.confirm.default_yes then + prompt_input = prompt_select .. " Y/n: " + items_short = { "", "n" } + items_long = { "Yes", "No" } + else + prompt_input = prompt_select .. " y/N: " + items_short = { "", "y" } + items_long = { "No", "Yes" } + end + + lib.prompt(prompt_input, prompt_select, items_short, items_long, "nvimtree_trash", function(item_short) + utils.clear_prompt() + if item_short == "y" or item_short == (M.config.ui.confirm.default_yes and "") then + do_trash() + end + end) + else + do_trash() + end +end + +function M.setup(opts) + M.config.ui = opts.ui + M.config.trash = opts.trash + M.config.filesystem_watchers = opts.filesystem_watchers +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/init.lua new file mode 100644 index 0000000..0a85a26 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/init.lua @@ -0,0 +1,17 @@ +local M = {} + +M.finders = require("nvim-tree.actions.finders") +M.fs = require("nvim-tree.actions.fs") +M.moves = require("nvim-tree.actions.moves") +M.node = require("nvim-tree.actions.node") +M.root = require("nvim-tree.actions.root") +M.tree = require("nvim-tree.actions.tree") + +function M.setup(opts) + M.fs.setup(opts) + M.node.setup(opts) + M.root.setup(opts) + M.tree.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/init.lua new file mode 100644 index 0000000..b1e5be7 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/init.lua @@ -0,0 +1,7 @@ +local M = {} + +M.item = require("nvim-tree.actions.moves.item") +M.parent = require("nvim-tree.actions.moves.parent") +M.sibling = require("nvim-tree.actions.moves.sibling") + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/item.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/item.lua new file mode 100644 index 0000000..8aacaae --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/item.lua @@ -0,0 +1,247 @@ +local utils = require("nvim-tree.utils") +local view = require("nvim-tree.view") +local core = require("nvim-tree.core") +local diagnostics = require("nvim-tree.diagnostics") + +local FileNode = require("nvim-tree.node.file") +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} +local MAX_DEPTH = 100 + +---Return the status of the node or nil if no status, depending on the type of +---status. +---@param node Node to inspect +---@param what string? type of status +---@param skip_gitignored boolean? default false +---@return boolean +local function status_is_valid(node, what, skip_gitignored) + if what == "git" then + local git_xy = node:get_git_xy() + return git_xy ~= nil and (not skip_gitignored or git_xy[1] ~= "!!") + elseif what == "diag" then + local diag_status = diagnostics.get_diag_status(node) + return diag_status ~= nil and diag_status.value ~= nil + elseif what == "opened" then + return vim.fn.bufloaded(node.absolute_path) ~= 0 + end + + return false +end + +---Move to the next node that has a valid status. If none found, don't move. +---@param explorer Explorer +---@param where string? where to move (forwards or backwards) +---@param what string? type of status +---@param skip_gitignored boolean? default false +local function move(explorer, where, what, skip_gitignored) + local first_node_line = core.get_nodes_starting_line() + local nodes_by_line = utils.get_nodes_by_line(explorer.nodes, first_node_line) + local iter_start, iter_end, iter_step, cur, first, nex + + local cursor = explorer:get_cursor_position() + if cursor and cursor[1] < first_node_line then + cur = cursor[1] + end + + if where == "next" then + iter_start, iter_end, iter_step = first_node_line, #nodes_by_line, 1 + elseif where == "prev" then + iter_start, iter_end, iter_step = #nodes_by_line, first_node_line, -1 + end + + for line = iter_start, iter_end, iter_step do + local node = nodes_by_line[line] + local valid = status_is_valid(node, what, skip_gitignored) + + if not first and valid then + first = line + end + + if cursor and line == cursor[1] then + cur = line + elseif valid and cur then + nex = line + break + end + end + + if nex then + view.set_cursor({ nex, 0 }) + elseif vim.o.wrapscan and first then + view.set_cursor({ first, 0 }) + end +end + +---@param node DirectoryNode +local function expand_node(node) + if not node.open then + -- Expand the node. + -- Should never collapse since we checked open. + node:expand_or_collapse(false) + end +end + +--- Move to the next node recursively. +---@param explorer Explorer +---@param what string? type of status +---@param skip_gitignored? boolean default false +local function move_next_recursive(explorer, what, skip_gitignored) + -- If the current node: + -- * is a directory + -- * and is not the root node + -- * and has a git/diag status + -- * and is not opened + -- expand it. + local node_init = explorer:get_node_at_cursor() + if not node_init then + return + end + local valid = false + if node_init.name ~= ".." then -- root node cannot have a status + valid = status_is_valid(node_init, what, skip_gitignored) + end + local node_dir = node_init:as(DirectoryNode) + if node_dir and valid and not node_dir.open then + node_dir:expand_or_collapse(false) + end + + move(explorer, "next", what, skip_gitignored) + + local node_cur = explorer:get_node_at_cursor() + if not node_cur then + return + end + + -- If we haven't moved at all at this point, return. + if node_init == node_cur then + return + end + + -- i is used to limit iterations. + local i = 0 + local dir_cur = node_cur:as(DirectoryNode) + while dir_cur and i < MAX_DEPTH do + expand_node(dir_cur) + + move(explorer, "next", what, skip_gitignored) + + -- Save current node. + node_cur = explorer:get_node_at_cursor() + dir_cur = node_cur and node_cur:as(DirectoryNode) + + i = i + 1 + end +end + +--- Move to the previous node recursively. +--- +--- move_prev_recursive: +--- +--- 1) Save current as node_init. +-- 2) Call a non-recursive prev. +--- 3) If current node is node_init's parent, call move_prev_recursive. +--- 4) Else: +--- 4.1) If current node is nil, is node_init (we didn't move), or is a file, return. +--- 4.2) The current file is a directory, expand it. +--- 4.3) Find node_init in current window, and move to it (if not found, return). +--- If node_init is the root node (name = ".."), directly move to position 1. +--- 4.4) Call a non-recursive prev. +--- 4.5) Save the current node and start back from 4.1. +--- +---@param explorer Explorer +---@param what string? type of status +---@param skip_gitignored boolean? default false +local function move_prev_recursive(explorer, what, skip_gitignored) + local node_init, node_cur + + -- 1) + node_init = explorer:get_node_at_cursor() + if node_init == nil then + return + end + + -- 2) + move(explorer, "prev", what, skip_gitignored) + + node_cur = explorer:get_node_at_cursor() + if node_cur == node_init.parent then + -- 3) + move_prev_recursive(explorer, what, skip_gitignored) + else + -- i is used to limit iterations. + local i = 0 + while i < MAX_DEPTH do + -- 4.1) + if + node_cur == nil + or node_cur == node_init -- we didn't move + or node_cur:is(FileNode) -- node is a file + then + return + end + + -- 4.2) + local node_dir = node_cur:as(DirectoryNode) + if node_dir then + expand_node(node_dir) + end + + -- 4.3) + if node_init.name == ".." then -- root node + view.set_cursor({ 1, 0 }) -- move to root node (position 1) + else + local node_init_line = utils.find_node_line(node_init) + if node_init_line < 0 then + return + end + view.set_cursor({ node_init_line, 0 }) + end + + -- 4.4) + move(explorer, "prev", what, skip_gitignored) + + -- 4.5) + node_cur = explorer:get_node_at_cursor() + + i = i + 1 + end + end +end + +---@class NavigationItemOpts +---@field where string? +---@field what string? +---@field skip_gitignored boolean? +---@field recurse boolean? + +---@param opts NavigationItemOpts +---@return fun() +function M.fn(opts) + return function() + local explorer = core.get_explorer() + if not explorer then + return + end + + local recurse = false + + -- recurse only valid for git and diag moves. + if (opts.what == "git" or opts.what == "diag") and opts.recurse ~= nil then + recurse = opts.recurse + end + + if not recurse then + move(explorer, opts.where, opts.what, opts.skip_gitignored) + return + end + + if opts.where == "next" then + move_next_recursive(explorer, opts.what, opts.skip_gitignored) + elseif opts.where == "prev" then + move_prev_recursive(explorer, opts.what, opts.skip_gitignored) + end + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/parent.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/parent.lua new file mode 100644 index 0000000..32ca839 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/parent.lua @@ -0,0 +1,44 @@ +local view = require("nvim-tree.view") +local utils = require("nvim-tree.utils") + +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +---@param should_close boolean|nil +---@return fun(node: Node): boolean|nil +function M.fn(should_close) + should_close = should_close or false + + ---@param node Node + return function(node) + local dir = node:as(DirectoryNode) + if dir then + dir = dir:last_group_node() + if should_close and dir.open then + dir.open = false + dir.explorer.renderer:draw() + return + end + end + + local parent = (node:get_parent_of_group() or node).parent + + if not parent or not parent.parent then + view.set_cursor({ 1, 0 }) + return + end + + local _, line = utils.find_node(parent.explorer.nodes, function(n) + return n.absolute_path == parent.absolute_path + end) + + view.set_cursor({ line + 1, 0 }) + if should_close then + parent.open = false + parent.explorer.renderer:draw() + end + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/sibling.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/sibling.lua new file mode 100644 index 0000000..cf5b492 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/moves/sibling.lua @@ -0,0 +1,53 @@ +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") +local Iterator = require("nvim-tree.iterators.node-iterator") + +local M = {} + +---@param direction string +---@return fun(node: Node): nil +function M.fn(direction) + return function(node) + if node.name == ".." or not direction then + return + end + + local first, last, next, prev = nil, nil, nil, nil + local found = false + local parent = node.parent or core.get_explorer() + Iterator.builder(parent and parent.nodes or {}) + :recursor(function() + return nil + end) + :applier(function(n) + first = first or n + last = n + if n.absolute_path == node.absolute_path then + found = true + return + end + prev = not found and n or prev + if found and not next then + next = n + end + end) + :iterate() + + local target_node + if direction == "first" then + target_node = first + elseif direction == "last" then + target_node = last + elseif direction == "next" then + target_node = next or first + else + target_node = prev or last + end + + if target_node then + utils.focus_file(target_node.absolute_path) + end + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/file-popup.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/file-popup.lua new file mode 100644 index 0000000..1b9ed6a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/file-popup.lua @@ -0,0 +1,93 @@ +local utils = require("nvim-tree.utils") + +local M = {} + +---@param node Node +---@return table +local function get_formatted_lines(node) + local stats = node.fs_stat + if stats == nil then + return { + "", + " Can't retrieve file information", + "", + } + end + + local fpath = " fullpath: " .. node.absolute_path + local created_at = " created: " .. os.date("%x %X", stats.birthtime.sec) + local modified_at = " modified: " .. os.date("%x %X", stats.mtime.sec) + local accessed_at = " accessed: " .. os.date("%x %X", stats.atime.sec) + local size = " size: " .. utils.format_bytes(stats.size) + + return { + fpath, + size, + accessed_at, + modified_at, + created_at, + } +end + +local current_popup = nil + +---@param node Node +local function setup_window(node) + local lines = get_formatted_lines(node) + + local max_width = vim.fn.max(vim.tbl_map(function(n) + return #n + end, lines)) + local open_win_config = vim.tbl_extend("force", M.open_win_config, { + width = max_width + 1, + height = #lines, + noautocmd = true, + zindex = 60, + }) + local winnr = vim.api.nvim_open_win(0, false, open_win_config) + current_popup = { + winnr = winnr, + file_path = node.absolute_path, + } + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.api.nvim_win_set_buf(winnr, bufnr) +end + +function M.close_popup() + if current_popup ~= nil then + vim.api.nvim_win_close(current_popup.winnr, true) + vim.cmd("augroup NvimTreeRemoveFilePopup | au! CursorMoved | augroup END") + + current_popup = nil + end +end + +---@param node Node +function M.toggle_file_info(node) + if node.name == ".." then + return + end + if current_popup ~= nil then + local is_same_node = current_popup.file_path == node.absolute_path + + M.close_popup() + + if is_same_node then + return + end + end + + setup_window(node) + + vim.api.nvim_create_autocmd("CursorMoved", { + group = vim.api.nvim_create_augroup("NvimTreeRemoveFilePopup", {}), + callback = M.close_popup, + }) +end + +function M.setup(opts) + M.open_win_config = opts.actions.file_popup.open_win_config +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/init.lua new file mode 100644 index 0000000..f82ad4a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/init.lua @@ -0,0 +1,14 @@ +local M = {} + +M.file_popup = require("nvim-tree.actions.node.file-popup") +M.open_file = require("nvim-tree.actions.node.open-file") +M.run_command = require("nvim-tree.actions.node.run-command") +M.system_open = require("nvim-tree.actions.node.system-open") + +function M.setup(opts) + require("nvim-tree.actions.node.system-open").setup(opts) + require("nvim-tree.actions.node.file-popup").setup(opts) + require("nvim-tree.actions.node.open-file").setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/open-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/open-file.lua new file mode 100644 index 0000000..1d1fc2e --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/open-file.lua @@ -0,0 +1,431 @@ +-- Copyright 2019 Yazdani Kiyan under MIT License +local lib = require("nvim-tree.lib") +local notify = require("nvim-tree.notify") +local utils = require("nvim-tree.utils") +local view = require("nvim-tree.view") + +local M = {} + +---Get single char from user input +---@return string +local function get_user_input_char() + local c = vim.fn.getchar() + while type(c) ~= "number" do + c = vim.fn.getchar() + end + return vim.fn.nr2char(c) +end + +---Get all windows in the current tabpage that aren't NvimTree. +---@return table with valid win_ids +local function usable_win_ids() + local tabpage = vim.api.nvim_get_current_tabpage() + local win_ids = vim.api.nvim_tabpage_list_wins(tabpage) + local tree_winid = view.get_winnr(tabpage) + + return vim.tbl_filter(function(id) + local bufid = vim.api.nvim_win_get_buf(id) + for option, v in pairs(M.window_picker.exclude) do + local ok, option_value + if vim.fn.has("nvim-0.10") == 1 then + ok, option_value = pcall(vim.api.nvim_get_option_value, option, { buf = bufid }) + else + ok, option_value = pcall(vim.api.nvim_buf_get_option, bufid, option) ---@diagnostic disable-line: deprecated + end + + if ok and vim.tbl_contains(v, option_value) then + return false + end + end + + 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 + end, win_ids) +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. +---@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 +--- no selectable windows, return -1. +local function pick_win_id() + local selectable = usable_win_ids() + + -- If there are no selectable windows: return. If there's only 1, return it without picking. + if #selectable == 0 then + return -1 + end + if #selectable == 1 then + return selectable[1] + end + + if #M.window_picker.chars < #selectable then + notify.error(string.format("More windows (%d) than actions.open_file.window_picker.chars (%d).", #selectable, #M.window_picker.chars)) + return nil + end + + local i = 1 + local win_opts_selectable = {} + local win_opts_unselectable = {} + local win_map = {} + local laststatus = vim.o.laststatus + vim.o.laststatus = 2 + + local tabpage = vim.api.nvim_get_current_tabpage() + local win_ids = vim.api.nvim_tabpage_list_wins(tabpage) + + local not_selectable = vim.tbl_filter(function(id) + return not vim.tbl_contains(selectable, id) + end, win_ids) + + if laststatus == 3 then + for _, win_id in ipairs(not_selectable) do + local ok_status, statusline + + if vim.fn.has("nvim-0.10") == 1 then + ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = win_id }) + else + ok_status, statusline = pcall(vim.api.nvim_win_get_option, win_id, "statusline") ---@diagnostic disable-line: deprecated + end + + win_opts_unselectable[win_id] = { + statusline = ok_status and statusline or "", + } + + -- Clear statusline for windows not selectable + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("statusline", " ", { win = win_id }) + else + vim.api.nvim_win_set_option(win_id, "statusline", " ") ---@diagnostic disable-line: deprecated + end + end + end + + -- Setup UI + for _, id in ipairs(selectable) do + local char = M.window_picker.chars:sub(i, i) + + local ok_status, statusline, ok_hl, winhl + if vim.fn.has("nvim-0.10") == 1 then + ok_status, statusline = pcall(vim.api.nvim_get_option_value, "statusline", { win = id }) + ok_hl, winhl = pcall(vim.api.nvim_get_option_value, "winhl", { win = id }) + else + ok_status, statusline = pcall(vim.api.nvim_win_get_option, id, "statusline") ---@diagnostic disable-line: deprecated + ok_hl, winhl = pcall(vim.api.nvim_win_get_option, id, "winhl") ---@diagnostic disable-line: deprecated + end + + win_opts_selectable[id] = { + statusline = ok_status and statusline or "", + winhl = ok_hl and winhl or "", + } + win_map[char] = id + + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("statusline", "%=" .. char .. "%=", { win = id }) + vim.api.nvim_set_option_value("winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker", { win = id }) + else + vim.api.nvim_win_set_option(id, "statusline", "%=" .. char .. "%=") ---@diagnostic disable-line: deprecated + vim.api.nvim_win_set_option(id, "winhl", "StatusLine:NvimTreeWindowPicker,StatusLineNC:NvimTreeWindowPicker") ---@diagnostic disable-line: deprecated + end + + i = i + 1 + if i > #M.window_picker.chars then + break + end + end + + vim.cmd("redraw") + if vim.opt.cmdheight._value ~= 0 then + print("Pick window: ") + end + local _, resp = pcall(get_user_input_char) + resp = (resp or ""):upper() + utils.clear_prompt() + + -- Restore window options + for _, id in ipairs(selectable) do + for opt, value in pairs(win_opts_selectable[id]) do + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value(opt, value, { win = id }) + else + vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated + end + end + end + + if laststatus == 3 then + for _, id in ipairs(not_selectable) do + -- Ensure window still exists at this point + if vim.api.nvim_win_is_valid(id) then + for opt, value in pairs(win_opts_unselectable[id]) do + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value(opt, value, { win = id }) + else + vim.api.nvim_win_set_option(id, opt, value) ---@diagnostic disable-line: deprecated + end + end + end + end + end + + vim.o.laststatus = laststatus + + if not vim.tbl_contains(vim.split(M.window_picker.chars, ""), resp) then + return + end + + return win_map[resp] +end + +local function open_file_in_tab(filename) + if M.quit_on_open then + view.close() + end + if M.relative_path then + filename = utils.path_relative(filename, vim.fn.getcwd()) + end + vim.cmd("tabe " .. vim.fn.fnameescape(filename)) +end + +local function drop(filename) + if M.quit_on_open then + view.close() + end + if M.relative_path then + filename = utils.path_relative(filename, vim.fn.getcwd()) + end + vim.cmd("drop " .. vim.fn.fnameescape(filename)) +end + +local function tab_drop(filename) + if M.quit_on_open then + view.close() + end + if M.relative_path then + filename = utils.path_relative(filename, vim.fn.getcwd()) + end + vim.cmd("tab :drop " .. vim.fn.fnameescape(filename)) +end + +local function on_preview(buf_loaded) + if not buf_loaded then + vim.bo.bufhidden = "delete" + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + group = vim.api.nvim_create_augroup("RemoveBufHidden", {}), + buffer = vim.api.nvim_get_current_buf(), + callback = function() + vim.bo.bufhidden = "" + end, + once = true, + }) + end + view.focus() +end + +local function get_target_winid(mode) + local target_winid + if not M.window_picker.enable or mode == "edit_no_picker" or mode == "preview_no_picker" then + target_winid = lib.target_winid + + -- first available window + if not vim.tbl_contains(vim.api.nvim_tabpage_list_wins(0), target_winid) then + target_winid = first_win_id() + end + else + -- pick a window + if type(M.window_picker.picker) == "function" then + target_winid = M.window_picker.picker() + else + target_winid = pick_win_id() + end + if target_winid == nil then + -- pick failed/cancelled + return + end + end + + if target_winid == -1 then + target_winid = lib.target_winid + end + return target_winid +end + +-- This is only to avoid the BufEnter for nvim-tree to trigger +-- which would cause find-file to run on an invalid file. +local function set_current_win_no_autocmd(winid, autocmd) + local eventignore = vim.opt.eventignore:get() + vim.opt.eventignore:append(autocmd) + vim.api.nvim_set_current_win(winid) + vim.opt.eventignore = eventignore +end + +local function open_in_new_window(filename, mode) + if type(mode) ~= "string" then + mode = "" + end + + local target_winid = get_target_winid(mode) + if not target_winid then + return + end + + -- non-floating, non-nvim-tree windows + local win_ids = vim.tbl_filter(function(id) + local config = vim.api.nvim_win_get_config(id) + local bufnr = vim.api.nvim_win_get_buf(id) + return config and config.relative == "" or utils.is_nvim_tree_buf(bufnr) + 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 new_window_side = (view.View.side == "right") and "aboveleft" or "belowright" + + -- Target is invalid: create new window + if not vim.tbl_contains(win_ids, target_winid) then + vim.cmd(new_window_side .. " vsplit") + target_winid = vim.api.nvim_get_current_win() + lib.target_winid = target_winid + + -- No need to split, as we created a new window. + create_new_window = false + if mode:match("split$") then + mode = "edit" + end + elseif not vim.o.hidden then + -- If `hidden` is not enabled, check if buffer in target window is + -- modified, and create new split if it is. + local target_bufid = vim.api.nvim_win_get_buf(target_winid) + + local modified + if vim.fn.has("nvim-0.10") == 1 then + modified = vim.api.nvim_get_option_value("modified", { buf = target_bufid }) + else + modified = vim.api.nvim_buf_get_option(target_bufid, "modified") ---@diagnostic disable-line: deprecated + end + + if modified then + if not mode:match("split$") then + mode = "vsplit" + end + end + end + + if (mode == "preview" or mode == "preview_no_picker") and view.View.float.enable then + -- ignore "WinLeave" autocmd on preview + -- because the registered "WinLeave" + -- will kill the floating window immediately + set_current_win_no_autocmd(target_winid, { "WinLeave", "BufEnter" }) + else + set_current_win_no_autocmd(target_winid, { "BufEnter" }) + end + + local fname + if M.relative_path then + fname = utils.escape_special_chars(vim.fn.fnameescape(utils.path_relative(filename, vim.fn.getcwd()))) + else + fname = utils.escape_special_chars(vim.fn.fnameescape(filename)) + end + + local command + if create_new_window then + -- generated from vim.api.nvim_parse_cmd("belowright vsplit foo", {}) + command = { cmd = "vsplit", mods = { split = new_window_side }, args = { fname } } + elseif mode:match("split$") then + command = { cmd = mode, args = { fname } } + else + command = { cmd = "edit", args = { fname } } + end + + pcall(vim.api.nvim_cmd, command, { output = false }) + lib.set_target_win() +end + +local function is_already_loaded(filename) + for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf_id) and filename == vim.api.nvim_buf_get_name(buf_id) then + return true + end + end + return false +end + +local function edit_in_current_buf(filename) + require("nvim-tree.view").abandon_current_window() + if M.relative_path then + filename = utils.path_relative(filename, vim.fn.getcwd()) + end + vim.cmd("keepalt keepjumps edit " .. vim.fn.fnameescape(filename)) +end + +---@param mode string +---@param filename string +---@return nil +function M.fn(mode, filename) + if type(mode) ~= "string" then + mode = "" + end + + if mode == "tabnew" then + return open_file_in_tab(filename) + end + + if mode == "drop" then + return drop(filename) + end + + if mode == "tab_drop" then + return tab_drop(filename) + end + + if mode == "edit_in_place" then + return edit_in_current_buf(filename) + end + + local buf_loaded = is_already_loaded(filename) + + local found_win = utils.get_win_buf_from_path(filename) + if found_win and (mode == "preview" or mode == "preview_no_picker") then + return + end + + if not found_win then + open_in_new_window(filename, mode) + else + vim.api.nvim_set_current_win(found_win) + vim.bo.bufhidden = "" + end + + if M.resize_window then + view.resize() + end + + if mode == "preview" or mode == "preview_no_picker" then + return on_preview(buf_loaded) + end + + if M.quit_on_open then + view.close() + end +end + +function M.setup(opts) + M.quit_on_open = opts.actions.open_file.quit_on_open + M.resize_window = opts.actions.open_file.resize_window + M.relative_path = opts.actions.open_file.relative_path + if opts.actions.open_file.window_picker.chars then + opts.actions.open_file.window_picker.chars = tostring(opts.actions.open_file.window_picker.chars):upper() + end + M.window_picker = opts.actions.open_file.window_picker +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/run-command.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/run-command.lua new file mode 100644 index 0000000..1d99b37 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/run-command.lua @@ -0,0 +1,26 @@ +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") + +local M = {} + +---Retrieves the absolute path to the node. +---Safely handles the node representing the current directory +---(the topmost node in the nvim-tree window) +---@param node Node +---@return string +local function get_node_path(node) + local cwd = core.get_cwd() + if node.name == ".." and cwd then + return utils.path_remove_trailing(cwd) + else + return node.absolute_path + end +end + +---@param node Node +function M.run_file_command(node) + local node_path = get_node_path(node) + vim.api.nvim_input(": " .. node_path .. "") +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/system-open.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/system-open.lua new file mode 100644 index 0000000..9cc11c4 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/node/system-open.lua @@ -0,0 +1,91 @@ +local notify = require("nvim-tree.notify") +local utils = require("nvim-tree.utils") + +local M = {} + +---@param node Node +local function user(node) + if #M.config.system_open.cmd == 0 then + require("nvim-tree.utils").notify.warn("Cannot open file with system application. Unrecognized platform.") + return + end + + local process = { + cmd = M.config.system_open.cmd, + args = M.config.system_open.args, + errors = "\n", + stderr = vim.loop.new_pipe(false), + } + table.insert(process.args, node.link_to or node.absolute_path) + + local opts = { + args = process.args, + stdio = { nil, nil, process.stderr }, + detached = true, + } + + process.handle, process.pid = vim.loop.spawn(process.cmd, opts, function(code) + process.stderr:read_stop() + process.stderr:close() + process.handle:close() + if code ~= 0 then + notify.warn(string.format("system_open failed with return code %d: %s", code, process.errors)) + end + end) + + table.remove(process.args) + if not process.handle then + notify.warn(string.format("system_open failed to spawn command '%s': %s", process.cmd, process.pid)) + return + end + vim.loop.read_start(process.stderr, function(err, data) + if err then + return + end + if data then + process.errors = process.errors .. data + end + end) + vim.loop.unref(process.handle) +end + +---@param node Node +local function native(node) + local _, err = vim.ui.open(node.link_to or node.absolute_path) + + -- err only provided on opener executable not found hence logging path is not useful + if err then + notify.warn(err) + end +end + +---@param node Node +function M.fn(node) + M.open(node) +end + +-- TODO #2430 always use native once 0.10 is the minimum neovim version +function M.setup(opts) + M.config = {} + M.config.system_open = opts.system_open or {} + + if vim.fn.has("nvim-0.10") == 1 and #M.config.system_open.cmd == 0 then + M.open = native + else + M.open = user + if #M.config.system_open.cmd == 0 then + if utils.is_windows then + M.config.system_open = { + cmd = "cmd", + args = { "/c", "start", '""' }, + } + elseif utils.is_macos then + M.config.system_open.cmd = "open" + elseif utils.is_unix then + M.config.system_open.cmd = "xdg-open" + end + end + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/change-dir.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/change-dir.lua new file mode 100644 index 0000000..0aa4161 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/change-dir.lua @@ -0,0 +1,105 @@ +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") + +local M = { + current_tab = vim.api.nvim_get_current_tabpage(), +} + +---@param name string +---@return string|nil +local function clean_input_cwd(name) + name = vim.fn.fnameescape(name) + local cwd = core.get_cwd() + if cwd == nil then + return + end + local root_parent_cwd = vim.fn.fnamemodify(utils.path_remove_trailing(cwd), ":h") + if name == ".." and root_parent_cwd then + return vim.fn.expand(root_parent_cwd) + else + return vim.fn.expand(name) + end +end + +---@param new_tabpage integer +---@return boolean +local function is_window_event(new_tabpage) + local is_event_scope_window = vim.v.event.scope == "window" or vim.v.event.changed_window + return is_event_scope_window and new_tabpage == M.current_tab +end + +---@param foldername string +---@return boolean +local function prevent_cwd_change(foldername) + local is_same_cwd = foldername == core.get_cwd() + local is_restricted_above = M.options.restrict_above_cwd and foldername < vim.fn.getcwd(-1, -1) + return is_same_cwd or is_restricted_above +end + +---@param input_cwd string +---@param with_open boolean|nil +function M.fn(input_cwd, with_open) + if not core.get_explorer() then + return + end + + local new_tabpage = vim.api.nvim_get_current_tabpage() + if is_window_event(new_tabpage) then + return + end + + local foldername = clean_input_cwd(input_cwd) + if foldername == nil or prevent_cwd_change(foldername) then + return + end + + M.current_tab = new_tabpage + M.force_dirchange(foldername, with_open) +end + +---@param global boolean +---@param path string +local function cd(global, path) + vim.cmd((global and "cd " or "lcd ") .. vim.fn.fnameescape(path)) +end + +---@return boolean +local function should_change_dir() + return M.options.enable and vim.tbl_isempty(vim.v.event) +end + +---@param f function +---@return fun(foldername: string, should_open_view: boolean|nil) +local function add_profiling_to(f) + return function(foldername, should_open_view) + local profile = log.profile_start("change dir %s", foldername) + f(foldername, should_open_view) + log.profile_end(profile) + end +end + +M.force_dirchange = add_profiling_to(function(foldername, should_open_view) + local valid_dir = vim.fn.isdirectory(foldername) == 1 -- prevent problems on non existing dirs + if valid_dir then + if should_change_dir() then + cd(M.options.global, foldername) + end + core.init(foldername) + end + + if should_open_view then + require("nvim-tree.lib").open() + else + local explorer = core.get_explorer() + if explorer then + explorer.renderer:draw() + end + end +end) + +function M.setup(options) + M.options = options.actions.change_dir +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/dir-up.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/dir-up.lua new file mode 100644 index 0000000..a8c41c8 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/dir-up.lua @@ -0,0 +1,22 @@ +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") + +local M = {} + +---@param node Node +function M.fn(node) + if not node or node.name == ".." then + require("nvim-tree.actions.root.change-dir").fn("..") + else + local cwd = core.get_cwd() + if cwd == nil then + return + end + + local newdir = vim.fn.fnamemodify(utils.path_remove_trailing(cwd), ":h") + require("nvim-tree.actions.root.change-dir").fn(newdir) + require("nvim-tree.actions.finders.find-file").fn(node.absolute_path) + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/init.lua new file mode 100644 index 0000000..1177e20 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/root/init.lua @@ -0,0 +1,10 @@ +local M = {} + +M.change_dir = require("nvim-tree.actions.root.change-dir") +M.dir_up = require("nvim-tree.actions.root.dir-up") + +function M.setup(opts) + M.change_dir.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/find-file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/find-file.lua new file mode 100644 index 0000000..8a05bf6 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/find-file.lua @@ -0,0 +1,71 @@ +local core = require("nvim-tree.core") +local lib = require("nvim-tree.lib") +local view = require("nvim-tree.view") +local finders_find_file = require("nvim-tree.actions.finders.find-file") + +local M = {} + +--- Find file or buffer +---@param opts ApiTreeFindFileOpts|nil|boolean legacy -> opts.buf +function M.fn(opts) + -- legacy arguments + if type(opts) == "string" then + opts = { + buf = opts, + } + end + opts = opts or {} + + -- do nothing if closed and open not requested + if not opts.open and not core.get_explorer() then + return + end + + local bufnr, path + + -- (optional) buffer number and path + local opts_buf = opts.buf + if type(opts_buf) == "nil" then + bufnr = vim.api.nvim_get_current_buf() + path = vim.api.nvim_buf_get_name(bufnr) + elseif type(opts_buf) == "number" then + if not vim.api.nvim_buf_is_valid(opts_buf) then + return + end + bufnr = opts_buf + path = vim.api.nvim_buf_get_name(bufnr) + elseif type(opts_buf) == "string" then + bufnr = nil + path = tostring(opts_buf) + else + return + end + + if view.is_visible() then + -- focus + if opts.focus then + lib.set_target_win() + view.focus() + end + elseif opts.open then + -- open + lib.open({ current_window = opts.current_window, winid = opts.winid }) + if not opts.focus then + vim.cmd("noautocmd wincmd p") + end + end + + -- update root + if opts.update_root or M.config.update_focused_file.update_root.enable then + require("nvim-tree").change_root(path, bufnr) + end + + -- find + finders_find_file.fn(path) +end + +function M.setup(opts) + M.config = opts or {} +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/init.lua new file mode 100644 index 0000000..2ff9e9b --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/init.lua @@ -0,0 +1,17 @@ +local M = {} + +M.find_file = require("nvim-tree.actions.tree.find-file") +M.modifiers = require("nvim-tree.actions.tree.modifiers") +M.open = require("nvim-tree.actions.tree.open") +M.toggle = require("nvim-tree.actions.tree.toggle") +M.resize = require("nvim-tree.actions.tree.resize") + +function M.setup(opts) + M.find_file.setup(opts) + M.modifiers.setup(opts) + M.open.setup(opts) + M.toggle.setup(opts) + M.resize.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua new file mode 100644 index 0000000..049be2b --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/collapse-all.lua @@ -0,0 +1,57 @@ +local utils = require("nvim-tree.utils") +local core = require("nvim-tree.core") +local Iterator = require("nvim-tree.iterators.node-iterator") + +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 + +---@param keep_buffers boolean +function M.fn(keep_buffers) + local explorer = core.get_explorer() + if not explorer then + return + end + + local node = explorer:get_node_at_cursor() + if not node then + return + end + + local matches = buf_match() + + Iterator.builder(explorer.nodes) + :hidden() + :applier(function(n) + local dir = n:as(DirectoryNode) + if dir then + dir.open = keep_buffers 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) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/expand-all.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/expand-all.lua new file mode 100644 index 0000000..6032c68 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/expand-all.lua @@ -0,0 +1,95 @@ +local core = require("nvim-tree.core") +local Iterator = require("nvim-tree.iterators.node-iterator") +local notify = require("nvim-tree.notify") + +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +---@param list string[] +---@return table +local function to_lookup_table(list) + local table = {} + for _, element in ipairs(list) do + table[element] = true + end + + return table +end + +---@param node DirectoryNode +local function expand(node) + node = node:last_group_node() + node.open = true + if #node.nodes == 0 then + core.get_explorer():expand(node) + end +end + +---@param expansion_count integer +---@param node Node +---@return boolean +local function should_expand(expansion_count, node) + local dir = node:as(DirectoryNode) + if not dir then + return false + end + local should_halt = expansion_count >= M.MAX_FOLDER_DISCOVERY + local should_exclude = M.EXCLUDE[dir.name] + return not should_halt and not dir.open and not should_exclude +end + +local function gen_iterator() + local expansion_count = 0 + + return function(parent) + if parent.parent and parent.nodes and not parent.open then + expansion_count = expansion_count + 1 + expand(parent) + end + + Iterator.builder(parent.nodes) + :hidden() + :applier(function(node) + if should_expand(expansion_count, node) then + expansion_count = expansion_count + 1 + node = node:as(DirectoryNode) + if node then + expand(node) + end + end + end) + :recursor(function(node) + return expansion_count < M.MAX_FOLDER_DISCOVERY and (node.group_next and { node.group_next } or (node.open and node.nodes)) + end) + :iterate() + + if expansion_count >= M.MAX_FOLDER_DISCOVERY then + return true + end + end +end + +---Expand the directory node or the root +---@param node Node +function M.fn(node) + local explorer = core.get_explorer() + local parent = node:as(DirectoryNode) or explorer + if not parent then + return + end + + if gen_iterator()(parent) then + notify.warn("expansion iteration was halted after " .. M.MAX_FOLDER_DISCOVERY .. " discovered folders") + end + if explorer then + explorer.renderer:draw() + end +end + +function M.setup(opts) + M.MAX_FOLDER_DISCOVERY = opts.actions.expand_all.max_folder_discovery + M.EXCLUDE = to_lookup_table(opts.actions.expand_all.exclude) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/init.lua new file mode 100644 index 0000000..f3ce27f --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/init.lua @@ -0,0 +1,10 @@ +local M = {} + +M.collapse_all = require("nvim-tree.actions.tree.modifiers.collapse-all") +M.expand_all = require("nvim-tree.actions.tree.modifiers.expand-all") + +function M.setup(opts) + M.expand_all.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/open.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/open.lua new file mode 100644 index 0000000..ff2da83 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/open.lua @@ -0,0 +1,55 @@ +local lib = require("nvim-tree.lib") +local view = require("nvim-tree.view") +local finders_find_file = require("nvim-tree.actions.finders.find-file") + +local M = {} + +---Open the tree, focusing if already open. +---@param opts ApiTreeOpenOpts|nil|string legacy -> opts.path +function M.fn(opts) + -- legacy arguments + if type(opts) == "string" then + opts = { + path = opts, + } + end + opts = opts or {} + + local previous_buf = vim.api.nvim_get_current_buf() + local previous_path = vim.api.nvim_buf_get_name(previous_buf) + + -- sanitise path + if type(opts.path) ~= "string" or vim.fn.isdirectory(opts.path) == 0 then + opts.path = nil + end + + if view.is_visible() then + -- focus + lib.set_target_win() + view.focus() + else + -- open + lib.open({ + path = opts.path, + current_window = opts.current_window, + winid = opts.winid, + }) + end + + -- find file + if M.config.update_focused_file.enable or opts.find_file then + -- update root + if opts.update_root then + require("nvim-tree").change_root(previous_path, previous_buf) + end + + -- find + finders_find_file.fn(previous_path) + end +end + +function M.setup(opts) + M.config = opts or {} +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/resize.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/resize.lua new file mode 100644 index 0000000..e8d4e95 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/resize.lua @@ -0,0 +1,51 @@ +local view = require("nvim-tree.view") + +local M = {} + +---Resize the tree, persisting the new size. +---@param opts ApiTreeResizeOpts|nil +function M.fn(opts) + if opts == nil then + -- reset to config values + view.configure_width() + view.resize() + return + end + + local options = opts or {} + local width_cfg = options.width + + if width_cfg ~= nil then + view.configure_width(width_cfg) + view.resize() + return + end + + if not view.is_width_determined() then + -- {absolute} and {relative} do nothing when {width} is a function. + return + end + + local absolute = options.absolute + if type(absolute) == "number" then + view.resize(absolute) + return + end + + local relative = options.relative + if type(relative) == "number" then + local relative_size = tostring(relative) + if relative > 0 then + relative_size = "+" .. relative_size + end + + view.resize(relative_size) + return + end +end + +function M.setup(opts) + M.config = opts or {} +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/toggle.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/toggle.lua new file mode 100644 index 0000000..10aa978 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/actions/tree/toggle.lua @@ -0,0 +1,76 @@ +local lib = require("nvim-tree.lib") +local view = require("nvim-tree.view") +local finders_find_file = require("nvim-tree.actions.finders.find-file") + +local M = {} + +---Toggle the tree. +---@param opts ApiTreeToggleOpts|nil|boolean legacy -> opts.find_file +---@param no_focus string|nil legacy -> opts.focus +---@param cwd boolean|nil legacy -> opts.path +---@param bang boolean|nil legacy -> opts.update_root +function M.fn(opts, no_focus, cwd, bang) + -- legacy arguments + if type(opts) == "boolean" then + opts = { + find_file = opts, + } + if type(cwd) == "string" then + opts.path = cwd + end + if type(no_focus) == "boolean" then + opts.focus = not no_focus + end + if type(bang) == "boolean" then + opts.update_root = bang + end + end + opts = opts or {} + + -- defaults + if opts.focus == nil then + opts.focus = true + end + + local previous_buf = vim.api.nvim_get_current_buf() + local previous_path = vim.api.nvim_buf_get_name(previous_buf) + + -- sanitise path + if type(opts.path) ~= "string" or vim.fn.isdirectory(opts.path) == 0 then + opts.path = nil + end + + if view.is_visible() then + -- close + view.close() + else + -- open + lib.open({ + path = opts.path, + current_window = opts.current_window, + winid = opts.winid, + }) + + -- find file + if M.config.update_focused_file.enable or opts.find_file then + -- update root + if opts.update_root then + require("nvim-tree").change_root(previous_path, previous_buf) + end + + -- find + finders_find_file.fn(previous_path) + end + + -- restore focus + if not opts.focus then + vim.cmd("noautocmd wincmd p") + end + end +end + +function M.setup(opts) + M.config = opts or {} +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/api.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/api.lua new file mode 100644 index 0000000..455d4b0 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/api.lua @@ -0,0 +1,321 @@ +local core = require("nvim-tree.core") +local view = require("nvim-tree.view") +local utils = require("nvim-tree.utils") +local actions = require("nvim-tree.actions") +local appearance_hi_test = require("nvim-tree.appearance.hi-test") +local events = require("nvim-tree.events") +local help = require("nvim-tree.help") +local keymap = require("nvim-tree.keymap") +local notify = require("nvim-tree.notify") + +local DirectoryNode = require("nvim-tree.node.directory") +local FileLinkNode = require("nvim-tree.node.file-link") +local RootNode = require("nvim-tree.node.root") +local UserDecorator = require("nvim-tree.renderer.decorator.user") + +local Api = { + tree = {}, + node = { + navigate = { + sibling = {}, + git = {}, + diagnostics = {}, + opened = {}, + }, + run = {}, + open = {}, + }, + events = {}, + marks = { + bulk = {}, + navigate = {}, + }, + fs = { + copy = {}, + }, + git = {}, + live_filter = {}, + config = { + mappings = {}, + }, + commands = {}, + diagnostics = {}, + decorator = {}, +} + +---Print error when setup not called. +---@param fn fun(...): any +---@return fun(...): any +local function wrap(fn) + return function(...) + if vim.g.NvimTreeSetup == 1 then + return fn(...) + else + notify.error("nvim-tree setup not called") + end + end +end + +---Invoke a method on the singleton explorer. +---Print error when setup not called. +---@param explorer_method string explorer method name +---@return fun(...): any +local function wrap_explorer(explorer_method) + return wrap(function(...) + local explorer = core.get_explorer() + if explorer then + return explorer[explorer_method](explorer, ...) + end + end) +end + +---Inject the node as the first argument if present otherwise do nothing. +---@param fn fun(node: Node, ...): any +---@return fun(node: Node?, ...): any +local function wrap_node(fn) + return function(node, ...) + node = node or wrap_explorer("get_node_at_cursor")() + if node then + return fn(node, ...) + end + end +end + +---Inject the node or nil as the first argument if absent. +---@param fn fun(node: Node?, ...): any +---@return fun(node: Node?, ...): any +local function wrap_node_or_nil(fn) + return function(node, ...) + node = node or wrap_explorer("get_node_at_cursor")() + return fn(node, ...) + end +end + +---Invoke a member's method on the singleton explorer. +---Print error when setup not called. +---@param explorer_member string explorer member name +---@param member_method string method name to invoke on member +---@param ... any passed to method +---@return fun(...): any +local function wrap_explorer_member_args(explorer_member, member_method, ...) + local method_args = ... + return wrap(function(...) + local explorer = core.get_explorer() + if explorer then + return explorer[explorer_member][member_method](explorer[explorer_member], method_args, ...) + end + end) +end + +---Invoke a member's method on the singleton explorer. +---Print error when setup not called. +---@param explorer_member string explorer member name +---@param member_method string method name to invoke on member +---@return fun(...): any +local function wrap_explorer_member(explorer_member, member_method) + return wrap(function(...) + local explorer = core.get_explorer() + if explorer then + return explorer[explorer_member][member_method](explorer[explorer_member], ...) + end + end) +end + +---@class ApiTreeOpenOpts +---@field path string|nil path +---@field current_window boolean|nil default false +---@field winid number|nil +---@field find_file boolean|nil default false +---@field update_root boolean|nil default false + +Api.tree.open = wrap(actions.tree.open.fn) +Api.tree.focus = Api.tree.open + +---@class ApiTreeToggleOpts +---@field path string|nil +---@field current_window boolean|nil default false +---@field winid number|nil +---@field find_file boolean|nil default false +---@field update_root boolean|nil default false +---@field focus boolean|nil default true + +Api.tree.toggle = wrap(actions.tree.toggle.fn) +Api.tree.close = wrap(view.close) +Api.tree.close_in_this_tab = wrap(view.close_this_tab_only) +Api.tree.close_in_all_tabs = wrap(view.close_all_tabs) +Api.tree.reload = wrap_explorer("reload_explorer") + +---@class ApiTreeResizeOpts +---@field width string|function|number|table|nil +---@field absolute number|nil +---@field relative number|nil + +Api.tree.resize = wrap(actions.tree.resize.fn) + +Api.tree.change_root = wrap(function(...) + require("nvim-tree").change_dir(...) +end) + +Api.tree.change_root_to_node = wrap_node(function(node) + if node.name == ".." or node:is(RootNode) then + actions.root.change_dir.fn("..") + else + node = node:as(DirectoryNode) + if node then + actions.root.change_dir.fn(node:last_group_node().absolute_path) + end + end +end) + +Api.tree.change_root_to_parent = wrap_node(actions.root.dir_up.fn) +Api.tree.get_node_under_cursor = wrap_explorer("get_node_at_cursor") +Api.tree.get_nodes = wrap_explorer("get_nodes") + +---@class ApiTreeFindFileOpts +---@field buf string|number|nil +---@field open boolean|nil default false +---@field current_window boolean|nil default false +---@field winid number|nil +---@field update_root boolean|nil default false +---@field focus boolean|nil default false + +Api.tree.find_file = wrap(actions.tree.find_file.fn) +Api.tree.search_node = wrap(actions.finders.search_node.fn) +Api.tree.collapse_all = wrap(actions.tree.modifiers.collapse_all.fn) +Api.tree.expand_all = wrap_node(actions.tree.modifiers.expand_all.fn) +Api.tree.toggle_enable_filters = wrap_explorer_member("filters", "toggle") +Api.tree.toggle_gitignore_filter = wrap_explorer_member_args("filters", "toggle", "git_ignored") +Api.tree.toggle_git_clean_filter = wrap_explorer_member_args("filters", "toggle", "git_clean") +Api.tree.toggle_no_buffer_filter = wrap_explorer_member_args("filters", "toggle", "no_buffer") +Api.tree.toggle_custom_filter = wrap_explorer_member_args("filters", "toggle", "custom") +Api.tree.toggle_hidden_filter = wrap_explorer_member_args("filters", "toggle", "dotfiles") +Api.tree.toggle_no_bookmark_filter = wrap_explorer_member_args("filters", "toggle", "no_bookmark") +Api.tree.toggle_help = wrap(help.toggle) +Api.tree.is_tree_buf = wrap(utils.is_nvim_tree_buf) + +---@class ApiTreeIsVisibleOpts +---@field tabpage number|nil +---@field any_tabpage boolean|nil default false + +Api.tree.is_visible = wrap(view.is_visible) + +---@class ApiTreeWinIdOpts +---@field tabpage number|nil default nil + +Api.tree.winid = wrap(view.winid) + +Api.fs.create = wrap_node_or_nil(actions.fs.create_file.fn) +Api.fs.remove = wrap_node(actions.fs.remove_file.fn) +Api.fs.trash = wrap_node(actions.fs.trash.fn) +Api.fs.rename_node = wrap_node(actions.fs.rename_file.fn(":t")) +Api.fs.rename = wrap_node(actions.fs.rename_file.fn(":t")) +Api.fs.rename_sub = wrap_node(actions.fs.rename_file.fn(":p:h")) +Api.fs.rename_basename = wrap_node(actions.fs.rename_file.fn(":t:r")) +Api.fs.rename_full = wrap_node(actions.fs.rename_file.fn(":p")) +Api.fs.cut = wrap_node(wrap_explorer_member("clipboard", "cut")) +Api.fs.paste = wrap_node(wrap_explorer_member("clipboard", "paste")) +Api.fs.clear_clipboard = wrap_explorer_member("clipboard", "clear_clipboard") +Api.fs.print_clipboard = wrap_explorer_member("clipboard", "print_clipboard") +Api.fs.copy.node = wrap_node(wrap_explorer_member("clipboard", "copy")) +Api.fs.copy.absolute_path = wrap_node(wrap_explorer_member("clipboard", "copy_absolute_path")) +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.relative_path = wrap_node(wrap_explorer_member("clipboard", "copy_path")) + +---@param mode string +---@param node Node +local function edit(mode, node) + local file_link = node:as(FileLinkNode) + local path = file_link and file_link.link_to or node.absolute_path + actions.node.open_file.fn(mode, path) +end + +---@param mode string +---@param toggle_group boolean? +---@return fun(node: Node) +local function open_or_expand_or_dir_up(mode, toggle_group) + ---@param node Node + return function(node) + local root = node:as(RootNode) + local dir = node:as(DirectoryNode) + + if root or node.name == ".." then + actions.root.change_dir.fn("..") + elseif dir then + dir:expand_or_collapse(toggle_group) + elseif not toggle_group then + edit(mode, node) + end + end +end + +Api.node.open.edit = wrap_node(open_or_expand_or_dir_up("edit")) +Api.node.open.drop = wrap_node(open_or_expand_or_dir_up("drop")) +Api.node.open.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.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.horizontal = wrap_node(open_or_expand_or_dir_up("split")) +Api.node.open.tab = wrap_node(open_or_expand_or_dir_up("tabnew")) +Api.node.open.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_no_picker = wrap_node(open_or_expand_or_dir_up("preview_no_picker")) + +Api.node.show_info_popup = wrap_node(actions.node.file_popup.toggle_file_info) +Api.node.run.cmd = wrap_node(actions.node.run_command.run_file_command) +Api.node.run.system = wrap_node(actions.node.system_open.fn) + +Api.node.navigate.sibling.next = wrap_node(actions.moves.sibling.fn("next")) +Api.node.navigate.sibling.prev = wrap_node(actions.moves.sibling.fn("prev")) +Api.node.navigate.sibling.first = wrap_node(actions.moves.sibling.fn("first")) +Api.node.navigate.sibling.last = wrap_node(actions.moves.sibling.fn("last")) +Api.node.navigate.parent = wrap_node(actions.moves.parent.fn(false)) +Api.node.navigate.parent_close = wrap_node(actions.moves.parent.fn(true)) +Api.node.navigate.git.next = wrap_node(actions.moves.item.fn({ where = "next", what = "git" })) +Api.node.navigate.git.next_skip_gitignored = wrap_node(actions.moves.item.fn({ where = "next", what = "git", skip_gitignored = true })) +Api.node.navigate.git.next_recursive = wrap_node(actions.moves.item.fn({ where = "next", what = "git", recurse = true })) +Api.node.navigate.git.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "git" })) +Api.node.navigate.git.prev_skip_gitignored = wrap_node(actions.moves.item.fn({ where = "prev", what = "git", skip_gitignored = true })) +Api.node.navigate.git.prev_recursive = wrap_node(actions.moves.item.fn({ where = "prev", what = "git", recurse = true })) +Api.node.navigate.diagnostics.next = wrap_node(actions.moves.item.fn({ where = "next", what = "diag" })) +Api.node.navigate.diagnostics.next_recursive = wrap_node(actions.moves.item.fn({ where = "next", what = "diag", recurse = true })) +Api.node.navigate.diagnostics.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "diag" })) +Api.node.navigate.diagnostics.prev_recursive = wrap_node(actions.moves.item.fn({ where = "prev", what = "diag", recurse = true })) +Api.node.navigate.opened.next = wrap_node(actions.moves.item.fn({ where = "next", what = "opened" })) +Api.node.navigate.opened.prev = wrap_node(actions.moves.item.fn({ where = "prev", what = "opened" })) + +Api.git.reload = wrap_explorer("reload_git") + +Api.events.subscribe = events.subscribe +Api.events.Event = events.Event + +Api.live_filter.start = wrap_explorer_member("live_filter", "start_filtering") +Api.live_filter.clear = wrap_explorer_member("live_filter", "clear_filter") + +Api.marks.get = wrap_node(wrap_explorer_member("marks", "get")) +Api.marks.list = wrap_explorer_member("marks", "list") +Api.marks.toggle = wrap_node(wrap_explorer_member("marks", "toggle")) +Api.marks.clear = wrap_explorer_member("marks", "clear") +Api.marks.bulk.delete = wrap_explorer_member("marks", "bulk_delete") +Api.marks.bulk.trash = wrap_explorer_member("marks", "bulk_trash") +Api.marks.bulk.move = wrap_explorer_member("marks", "bulk_move") +Api.marks.navigate.next = wrap_explorer_member("marks", "navigate_next") +Api.marks.navigate.prev = wrap_explorer_member("marks", "navigate_prev") +Api.marks.navigate.select = wrap_explorer_member("marks", "navigate_select") + +Api.config.mappings.get_keymap = wrap(keymap.get_keymap) +Api.config.mappings.get_keymap_default = wrap(keymap.get_keymap_default) +Api.config.mappings.default_on_attach = keymap.default_on_attach + +Api.diagnostics.hi_test = wrap(appearance_hi_test) + +Api.commands.get = wrap(function() + return require("nvim-tree.commands").get() +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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/hi-test.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/hi-test.lua new file mode 100644 index 0000000..25edb9c --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/hi-test.lua @@ -0,0 +1,140 @@ +local appearance = require("nvim-tree.appearance") + +local Class = require("nvim-tree.classic") + +-- others with name and links less than this arbitrary value are short +local SHORT_LEN = 50 + +---@class (exact) HighlightDisplay: Class for :NvimTreeHiTest +---@field group string nvim-tree highlight group name +---@field links string link chain to a concretely defined group +---@field def string :hi concrete definition after following any links +local HighlightDisplay = Class:extend() + +---@class HighlightDisplay +---@overload fun(args: HighlightDisplayArgs): HighlightDisplay + +---@class (exact) HighlightDisplayArgs +---@field group string nvim-tree highlight group name + +---@protected +---@param args HighlightDisplayArgs +function HighlightDisplay:new(args) + self.group = args.group + + local concrete = self.group + + -- maybe follow links + local links = {} + local link = vim.api.nvim_get_hl(0, { name = self.group }).link + while link do + table.insert(links, link) + concrete = link + link = vim.api.nvim_get_hl(0, { name = link }).link + end + self.links = table.concat(links, " ") + + -- concrete definition + local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { concrete } }, { output = true }) + if ok and type(res) == "string" then + self.def = res:gsub(".*xxx *", "") + else + self.def = "" + end +end + +---Render one group. +---@param bufnr number to render in +---@param fmt string format string for group, links, def +---@param l number line number to render at +---@return number l next line number +function HighlightDisplay:render(bufnr, fmt, l) + 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_add_highlight(bufnr, -1, self.group, l, 0, #self.group) + + return l + 1 +end + +---Render many groups. +---@param header string before with underline line +---@param displays HighlightDisplay[] highlight group +---@param bufnr number to render in +---@param l number line number to start at +---@return number l next line number +local function render_displays(header, displays, bufnr, l) + local max_group_len = 0 + local max_links_len = 0 + + -- build all highlight groups, using name only + for _, display in ipairs(displays) do + max_group_len = math.max(max_group_len, #display.group) + max_links_len = math.max(max_links_len, #display.links) + end + + -- header + vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { header, (header:gsub(".", "-")) }) + l = l + 2 + + -- render and highlight + local fmt = string.format("%%-%d.%ds %%-%d.%ds %%s", max_group_len, max_group_len, max_links_len, max_links_len) + for _, display in ipairs(displays) do + l = display:render(bufnr, fmt, l) + end + + return l +end + +---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim +---Display all nvim-tree and neovim highlight groups, their link chain and actual definition +return function() + -- create a buffer + local bufnr = vim.api.nvim_create_buf(false, true) + + local l = 0 + + -- nvim-tree groups, ordered + local displays = {} + for _, highlight_group in ipairs(appearance.HIGHLIGHT_GROUPS) do + local display = HighlightDisplay({ group = highlight_group.group }) + table.insert(displays, display) + end + l = render_displays("nvim-tree", displays, bufnr, l) + + vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { "" }) + l = l + 1 + + -- built in groups, ordered opaquely by nvim + local displays_short, displays_long = {}, {} + local ok, out = pcall(vim.api.nvim_cmd, { cmd = "highlight" }, { output = true }) + if ok then + for group in string.gmatch(out, "(%w*)%s+xxx") do + if group:find("NvimTree", 1, true) ~= 1 then + local display = HighlightDisplay({ group = group }) + if #display.group + #display.links > SHORT_LEN then + table.insert(displays_long, display) + else + table.insert(displays_short, display) + end + end + end + end + + -- short ones first + l = render_displays("other, short", displays_short, bufnr, l) + vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { "" }) + l = l + 1 + + -- long + render_displays("other, long", displays_long, bufnr, l) + + -- finalise and focus the buffer + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated + end + + vim.cmd.buffer(bufnr) +end diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/init.lua new file mode 100644 index 0000000..61714af --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/appearance/init.lua @@ -0,0 +1,210 @@ +local M = {} + +---@class HighlightGroup +---@field group string +---@field link string|nil +---@field def string|nil + +---@type HighlightGroup[] +-- All highlight groups: linked or directly defined. +-- Please add new groups to help and preserve order. +-- Please avoid directly defined groups to preserve accessibility for TUI. +M.HIGHLIGHT_GROUPS = { + + -- Standard + { group = "NvimTreeNormal", link = "Normal" }, + { group = "NvimTreeNormalFloat", link = "NormalFloat" }, + { group = "NvimTreeNormalFloatBorder", link = "FloatBorder" }, + { group = "NvimTreeNormalNC", link = "NvimTreeNormal" }, + + { group = "NvimTreeLineNr", link = "LineNr" }, + { group = "NvimTreeWinSeparator", link = "WinSeparator" }, + { group = "NvimTreeEndOfBuffer", link = "EndOfBuffer" }, + { group = "NvimTreePopup", link = "Normal" }, + { group = "NvimTreeSignColumn", link = "NvimTreeNormal" }, + + { group = "NvimTreeCursorColumn", link = "CursorColumn" }, + { group = "NvimTreeCursorLine", link = "CursorLine" }, + { group = "NvimTreeCursorLineNr", link = "CursorLineNr" }, + + { group = "NvimTreeStatusLine", link = "StatusLine" }, + { group = "NvimTreeStatusLineNC", link = "StatusLineNC" }, + + -- File Text + { group = "NvimTreeExecFile", link = "Question" }, + { group = "NvimTreeImageFile", link = "Question" }, + { group = "NvimTreeSpecialFile", link = "Title" }, + { group = "NvimTreeSymlink", link = "Underlined" }, + + -- Folder Text + { group = "NvimTreeRootFolder", link = "Title" }, + { group = "NvimTreeFolderName", link = "Directory" }, + { group = "NvimTreeEmptyFolderName", link = "Directory" }, + { group = "NvimTreeOpenedFolderName", link = "Directory" }, + { group = "NvimTreeSymlinkFolderName", link = "Directory" }, + + -- File Icons + { group = "NvimTreeFileIcon", link = "NvimTreeNormal" }, + { group = "NvimTreeSymlinkIcon", link = "NvimTreeNormal" }, + + -- Folder Icons + { group = "NvimTreeFolderIcon", def = "guifg=#8094b4 ctermfg=Blue" }, + { group = "NvimTreeOpenedFolderIcon", link = "NvimTreeFolderIcon" }, + { group = "NvimTreeClosedFolderIcon", link = "NvimTreeFolderIcon" }, + { group = "NvimTreeFolderArrowClosed", link = "NvimTreeIndentMarker" }, + { group = "NvimTreeFolderArrowOpen", link = "NvimTreeIndentMarker" }, + + -- Indent + { group = "NvimTreeIndentMarker", link = "NvimTreeFolderIcon" }, + + -- Picker + { group = "NvimTreeWindowPicker", def = "guifg=#ededed guibg=#4493c8 gui=bold ctermfg=White ctermbg=DarkBlue" }, + + -- LiveFilter + { group = "NvimTreeLiveFilterPrefix", link = "PreProc" }, + { group = "NvimTreeLiveFilterValue", link = "ModeMsg" }, + + -- Clipboard + { group = "NvimTreeCutHL", link = "SpellBad" }, + { group = "NvimTreeCopiedHL", link = "SpellRare" }, + + -- Bookmark + { group = "NvimTreeBookmarkIcon", link = "NvimTreeFolderIcon" }, + { group = "NvimTreeBookmarkHL", link = "SpellLocal" }, + + -- Modified + { group = "NvimTreeModifiedIcon", link = "Type" }, + { group = "NvimTreeModifiedFileHL", link = "NvimTreeModifiedIcon" }, + { group = "NvimTreeModifiedFolderHL", link = "NvimTreeModifiedFileHL" }, + + -- Hidden + { group = "NvimTreeHiddenIcon", link = "Conceal" }, + { group = "NvimTreeHiddenFileHL", link = "NvimTreeHiddenIcon" }, + { group = "NvimTreeHiddenFolderHL", link = "NvimTreeHiddenFileHL" }, + + -- Hidden Display + { group = "NvimTreeHiddenDisplay", link = "Conceal" }, + + -- Opened + { group = "NvimTreeOpenedHL", link = "Special" }, + + -- Git Icon + { group = "NvimTreeGitDeletedIcon", link = "Statement" }, + { group = "NvimTreeGitDirtyIcon", link = "Statement" }, + { group = "NvimTreeGitIgnoredIcon", link = "Comment" }, + { group = "NvimTreeGitMergeIcon", link = "Constant" }, + { group = "NvimTreeGitNewIcon", link = "PreProc" }, + { group = "NvimTreeGitRenamedIcon", link = "PreProc" }, + { group = "NvimTreeGitStagedIcon", link = "Constant" }, + + -- Git File Highlight + { group = "NvimTreeGitFileDeletedHL", link = "NvimTreeGitDeletedIcon" }, + { group = "NvimTreeGitFileDirtyHL", link = "NvimTreeGitDirtyIcon" }, + { group = "NvimTreeGitFileIgnoredHL", link = "NvimTreeGitIgnoredIcon" }, + { group = "NvimTreeGitFileMergeHL", link = "NvimTreeGitMergeIcon" }, + { group = "NvimTreeGitFileNewHL", link = "NvimTreeGitNewIcon" }, + { group = "NvimTreeGitFileRenamedHL", link = "NvimTreeGitRenamedIcon" }, + { group = "NvimTreeGitFileStagedHL", link = "NvimTreeGitStagedIcon" }, + + -- Git Folder Highlight + { group = "NvimTreeGitFolderDeletedHL", link = "NvimTreeGitFileDeletedHL" }, + { group = "NvimTreeGitFolderDirtyHL", link = "NvimTreeGitFileDirtyHL" }, + { group = "NvimTreeGitFolderIgnoredHL", link = "NvimTreeGitFileIgnoredHL" }, + { group = "NvimTreeGitFolderMergeHL", link = "NvimTreeGitFileMergeHL" }, + { group = "NvimTreeGitFolderNewHL", link = "NvimTreeGitFileNewHL" }, + { group = "NvimTreeGitFolderRenamedHL", link = "NvimTreeGitFileRenamedHL" }, + { group = "NvimTreeGitFolderStagedHL", link = "NvimTreeGitFileStagedHL" }, + + -- Diagnostics Icon + { group = "NvimTreeDiagnosticErrorIcon", link = "DiagnosticError" }, + { group = "NvimTreeDiagnosticWarnIcon", link = "DiagnosticWarn" }, + { group = "NvimTreeDiagnosticInfoIcon", link = "DiagnosticInfo" }, + { group = "NvimTreeDiagnosticHintIcon", link = "DiagnosticHint" }, + + -- Diagnostics File Highlight + { group = "NvimTreeDiagnosticErrorFileHL", link = "DiagnosticUnderlineError" }, + { group = "NvimTreeDiagnosticWarnFileHL", link = "DiagnosticUnderlineWarn" }, + { group = "NvimTreeDiagnosticInfoFileHL", link = "DiagnosticUnderlineInfo" }, + { group = "NvimTreeDiagnosticHintFileHL", link = "DiagnosticUnderlineHint" }, + + -- Diagnostics Folder Highlight + { group = "NvimTreeDiagnosticErrorFolderHL", link = "NvimTreeDiagnosticErrorFileHL" }, + { group = "NvimTreeDiagnosticWarnFolderHL", link = "NvimTreeDiagnosticWarnFileHL" }, + { group = "NvimTreeDiagnosticInfoFolderHL", link = "NvimTreeDiagnosticInfoFileHL" }, + { group = "NvimTreeDiagnosticHintFolderHL", link = "NvimTreeDiagnosticHintFileHL" }, +} + +-- nvim-tree highlight groups to legacy +M.LEGACY_LINKS = { + NvimTreeModifiedIcon = "NvimTreeModifiedFile", + + NvimTreeOpenedHL = "NvimTreeOpenedFile", + + NvimTreeBookmarkIcon = "NvimTreeBookmark", + + NvimTreeGitDeletedIcon = "NvimTreeGitDeleted", + NvimTreeGitDirtyIcon = "NvimTreeGitDirty", + NvimTreeGitIgnoredIcon = "NvimTreeGitIgnored", + NvimTreeGitMergeIcon = "NvimTreeGitMerge", + NvimTreeGitNewIcon = "NvimTreeGitNew", + NvimTreeGitRenamedIcon = "NvimTreeGitRenamed", + NvimTreeGitStagedIcon = "NvimTreeGitStaged", + + NvimTreeGitFileDeletedHL = "NvimTreeFileDeleted", + NvimTreeGitFileDirtyHL = "NvimTreeFileDirty", + NvimTreeGitFileIgnoredHL = "NvimTreeFileIgnored", + NvimTreeGitFileMergeHL = "NvimTreeFileMerge", + NvimTreeGitFileNewHL = "NvimTreeFileNew", + NvimTreeGitFileRenamedHL = "NvimTreeFileRenamed", + NvimTreeGitFileStagedHL = "NvimTreeFileStaged", + + NvimTreeGitFolderDeletedHL = "NvimTreeFolderDeleted", + NvimTreeGitFolderDirtyHL = "NvimTreeFolderDirty", + NvimTreeGitFolderIgnoredHL = "NvimTreeFolderIgnored", + NvimTreeGitFolderMergeHL = "NvimTreeFolderMerge", + NvimTreeGitFolderNewHL = "NvimTreeFolderNew", + NvimTreeGitFolderRenamedHL = "NvimTreeFolderRenamed", + NvimTreeGitFolderStagedHL = "NvimTreeFolderStaged", + + NvimTreeDiagnosticErrorIcon = "NvimTreeLspDiagnosticsError", + NvimTreeDiagnosticWarnIcon = "NvimTreeLspDiagnosticsWarning", + NvimTreeDiagnosticInfoIcon = "NvimTreeLspDiagnosticsInformation", + NvimTreeDiagnosticHintIcon = "NvimTreeLspDiagnosticsHint", + + NvimTreeDiagnosticErrorFileHL = "NvimTreeLspDiagnosticsErrorText", + NvimTreeDiagnosticWarnFileHL = "NvimTreeLspDiagnosticsWarningText", + NvimTreeDiagnosticInfoFileHL = "NvimTreeLspDiagnosticsInformationText", + NvimTreeDiagnosticHintFileHL = "NvimTreeLspDiagnosticsHintText", + + NvimTreeDiagnosticErrorFolderHL = "NvimTreeLspDiagnosticsErrorFolderText", + NvimTreeDiagnosticWarnFolderHL = "NvimTreeLspDiagnosticsWarningFolderText", + NvimTreeDiagnosticInfoFolderHL = "NvimTreeLspDiagnosticsInformationFolderText", + NvimTreeDiagnosticHintFolderHL = "NvimTreeLspDiagnosticsHintFolderText", +} + +function M.setup() + -- non-linked + for _, g in ipairs(M.HIGHLIGHT_GROUPS) do + if g.def then + vim.api.nvim_command("hi def " .. g.group .. " " .. g.def) + end + end + + -- hard link override when legacy only is present + for from, to in pairs(M.LEGACY_LINKS) do + local hl_from = vim.api.nvim_get_hl(0, { name = from }) + local hl_to = vim.api.nvim_get_hl(0, { name = to }) + if vim.tbl_isempty(hl_from) and not vim.tbl_isempty(hl_to) then + vim.api.nvim_command("hi link " .. from .. " " .. to) + end + end + + -- default links + for _, g in ipairs(M.HIGHLIGHT_GROUPS) do + if g.link then + vim.api.nvim_command("hi def link " .. g.group .. " " .. g.link) + end + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/buffers.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/buffers.lua new file mode 100644 index 0000000..d53c157 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/buffers.lua @@ -0,0 +1,65 @@ +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +---@type table record of which file is modified +M._modified = {} + +---refresh M._modified +function M.reload_modified() + M._modified = {} + local bufs = vim.fn.getbufinfo({ bufmodified = 1, buflisted = 1 }) + for _, buf in pairs(bufs) do + local path = buf.name + if path ~= "" then -- not a [No Name] buffer + -- mark all the parent as modified as well + while M._modified[path] ~= true do + -- no need to keep going if already recorded + -- This also prevents an infinite loop + M._modified[path] = true + path = vim.fn.fnamemodify(path, ":h") + end + end + end +end + +---@param node Node +---@return boolean +function M.is_modified(node) + if not M.config.modified.enable then + return false + end + + if not M._modified[node.absolute_path] then + return false + end + + local dir = node:as(DirectoryNode) + if dir then + if not M.config.modified.show_on_dirs then + return false + end + + if dir.open and not M.config.modified.show_on_open_dirs then + return false + end + end + + return true +end + +---A buffer exists for the node's absolute path +---@param node Node +---@return boolean +function M.is_opened(node) + return node and vim.fn.bufloaded(node.absolute_path) > 0 +end + +---@param opts table +function M.setup(opts) + M.config = { + modified = opts.modified, + } +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/classic.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/classic.lua new file mode 100644 index 0000000..6e856bc --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/classic.lua @@ -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 +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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/commands.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/commands.lua new file mode 100644 index 0000000..bff880c --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/commands.lua @@ -0,0 +1,157 @@ +local api = require("nvim-tree.api") +local view = require("nvim-tree.view") + +local M = {} + +local CMDS = { + { + name = "NvimTreeOpen", + opts = { + desc = "nvim-tree: open", + nargs = "?", + complete = "dir", + }, + command = function(c) + api.tree.open({ path = c.args }) + end, + }, + { + name = "NvimTreeClose", + opts = { + desc = "nvim-tree: close", + bar = true, + }, + command = function() + api.tree.close() + end, + }, + { + name = "NvimTreeToggle", + opts = { + desc = "nvim-tree: toggle", + nargs = "?", + complete = "dir", + }, + command = function(c) + api.tree.toggle({ + find_file = false, + focus = true, + path = c.args, + update_root = false, + }) + end, + }, + { + name = "NvimTreeFocus", + opts = { + desc = "nvim-tree: focus", + bar = true, + }, + command = function() + api.tree.open() + end, + }, + { + name = "NvimTreeRefresh", + opts = { + desc = "nvim-tree: refresh", + bar = true, + }, + command = function() + api.tree.reload() + end, + }, + { + name = "NvimTreeClipboard", + opts = { + desc = "nvim-tree: print clipboard", + bar = true, + }, + command = function() + api.fs.print_clipboard() + end, + }, + { + name = "NvimTreeFindFile", + opts = { + desc = "nvim-tree: find file", + bang = true, + bar = true, + }, + command = function(c) + api.tree.find_file({ + open = true, + focus = true, + update_root = c.bang, + }) + end, + }, + { + name = "NvimTreeFindFileToggle", + opts = { + desc = "nvim-tree: find file, toggle", + bang = true, + nargs = "?", + complete = "dir", + }, + command = function(c) + api.tree.toggle({ + find_file = true, + focus = true, + path = c.args, + update_root = c.bang, + }) + end, + }, + { + name = "NvimTreeResize", + opts = { + desc = "nvim-tree: resize", + nargs = 1, + bar = true, + }, + command = function(c) + view.resize(c.args) + end, + }, + { + name = "NvimTreeCollapse", + opts = { + desc = "nvim-tree: collapse", + bar = true, + }, + command = function() + api.tree.collapse_all(false) + end, + }, + { + name = "NvimTreeCollapseKeepBuffers", + opts = { + desc = "nvim-tree: collapse, keep directories open", + bar = true, + }, + command = function() + api.tree.collapse_all(true) + end, + }, + { + name = "NvimTreeHiTest", + opts = { + desc = "nvim-tree: highlight test", + }, + command = api.diagnostics.hi_test, + }, +} + +function M.get() + return vim.deepcopy(CMDS) +end + +function M.setup() + for _, cmd in ipairs(CMDS) do + local opts = vim.tbl_extend("force", cmd.opts, { force = true }) + vim.api.nvim_create_user_command(cmd.name, cmd.command, opts) + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/core.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/core.lua new file mode 100644 index 0000000..60d4a0a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/core.lua @@ -0,0 +1,67 @@ +local events = require("nvim-tree.events") +local notify = require("nvim-tree.notify") +local view = require("nvim-tree.view") +local log = require("nvim-tree.log") + +local M = {} + +---@type Explorer|nil +local TreeExplorer = nil +local first_init_done = false + +---@param foldername string +function M.init(foldername) + local profile = log.profile_start("core init %s", foldername) + + if TreeExplorer then + TreeExplorer:destroy() + end + + local err, path + + if foldername then + path, err = vim.loop.fs_realpath(foldername) + else + path, err = vim.loop.cwd() + end + if path then + TreeExplorer = require("nvim-tree.explorer")({ path = path }) + else + notify.error(err) + TreeExplorer = nil + end + + if not first_init_done then + events._dispatch_ready() + first_init_done = true + end + log.profile_end(profile) +end + +---@return Explorer|nil +function M.get_explorer() + return TreeExplorer +end + +function M.reset_explorer() + TreeExplorer = nil +end + +---@return string|nil +function M.get_cwd() + return TreeExplorer and TreeExplorer.absolute_path +end + +---@return integer +function M.get_nodes_starting_line() + local offset = 1 + if view.is_root_folder_visible(M.get_cwd()) then + offset = offset + 1 + end + if TreeExplorer and TreeExplorer.live_filter.filter then + return offset + 1 + end + return offset +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/diagnostics.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/diagnostics.lua new file mode 100644 index 0000000..e18b992 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/diagnostics.lua @@ -0,0 +1,245 @@ +local core = require("nvim-tree.core") +local utils = require("nvim-tree.utils") +local view = require("nvim-tree.view") +local log = require("nvim-tree.log") + +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +---COC severity level strings to LSP severity levels +---@enum COC_SEVERITY_LEVELS +local COC_SEVERITY_LEVELS = { + Error = 1, + Warning = 2, + Information = 3, + Hint = 4, +} + +---Absolute Node path to LSP severity level +---@alias NodeSeverities table + +---@class DiagStatus +---@field value lsp.DiagnosticSeverity|nil +---@field cache_version integer + +--- The buffer-severity mappings derived during the last diagnostic list update. +---@type NodeSeverities +local NODE_SEVERITIES = {} + +---The cache version number of the buffer-severity mappings. +---@type integer +local NODE_SEVERITIES_VERSION = 0 + +---@param path string +---@return string +local function uniformize_path(path) + return utils.canonical_path(path:gsub("\\", "/")) +end + +---Severity is within diagnostics.severity.min, diagnostics.severity.max +---@param severity lsp.DiagnosticSeverity +---@param config table +---@return boolean +local function is_severity_in_range(severity, config) + return config.max <= severity and severity <= config.min +end + +---Handle any COC exceptions, preventing any propagation +---@param err string +local function handle_coc_exception(err) + log.line("diagnostics", "handle_coc_exception: %s", vim.inspect(err)) + local notify = true + + -- avoid distractions on interrupts (CTRL-C) + if err:find("Vim:Interrupt") or err:find("Keyboard interrupt") then + notify = false + end + + if notify then + require("nvim-tree.notify").error("Diagnostics update from coc.nvim failed. " .. vim.inspect(err)) + end +end + +---COC service initialized +---@return boolean +local function is_using_coc() + return vim.g.coc_service_initialized == 1 +end + +---Marshal severities from COC. Does nothing when COC service not started. +---@return NodeSeverities +local function from_coc() + if not is_using_coc() then + return {} + end + + local ok, diagnostic_list = xpcall(function() + return vim.fn.CocAction("diagnosticList") + end, handle_coc_exception) + if not ok or type(diagnostic_list) ~= "table" or vim.tbl_isempty(diagnostic_list) then + return {} + end + + local buffer_severity = {} + for _, diagnostic in ipairs(diagnostic_list) do + local bufname = uniformize_path(diagnostic.file) + local coc_severity = COC_SEVERITY_LEVELS[diagnostic.severity] + local highest_severity = buffer_severity[bufname] or coc_severity + if is_severity_in_range(highest_severity, M.severity) then + buffer_severity[bufname] = math.min(highest_severity, coc_severity) + end + end + + return buffer_severity +end + +---Maybe retrieve severity level from the cache +---@param node Node +---@return DiagStatus +local function from_cache(node) + local nodepath = uniformize_path(node.absolute_path) + local max_severity = nil + if not node:is(DirectoryNode) then + -- direct cache hit for files + max_severity = NODE_SEVERITIES[nodepath] + else + -- dirs should be searched in the list of cached buffer names by prefix + for bufname, severity in pairs(NODE_SEVERITIES) do + local node_contains_buf = vim.startswith(bufname, nodepath .. "/") + if node_contains_buf then + if not max_severity or severity < max_severity then + max_severity = severity + end + end + end + end + return { value = max_severity, cache_version = NODE_SEVERITIES_VERSION } +end + +---Fired on DiagnosticChanged for a single buffer. +---This will be called on set and reset of diagnostics. +---On disabling LSP, a reset event will be sent for all buffers. +---@param ev table standard event with data.diagnostics populated +function M.update_lsp(ev) + if not M.enable or not ev or not ev.data or not ev.data.diagnostics then + return + end + + local profile_event = log.profile_start("DiagnosticChanged event") + + ---@type vim.Diagnostic[] + local diagnostics = ev.data.diagnostics + + -- use the buffer from the event, as ev.data.diagnostics will be empty on resolved diagnostics + local bufname = uniformize_path(vim.api.nvim_buf_get_name(ev.buf)) + + ---@type vim.diagnostic.Severity? + local new_severity = nil + + -- most severe (lowest) severity in user range + for _, diagnostic in ipairs(diagnostics) do + if diagnostic.severity >= M.severity.max and diagnostic.severity <= M.severity.min then + if not new_severity or diagnostic.severity < new_severity then + new_severity = diagnostic.severity + end + end + end + + -- record delta and schedule a redraw + if new_severity ~= NODE_SEVERITIES[bufname] then + NODE_SEVERITIES[bufname] = new_severity + NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 + + utils.debounce("DiagnosticChanged redraw", M.debounce_delay, function() + local profile_redraw = log.profile_start("DiagnosticChanged redraw") + + local explorer = core.get_explorer() + if explorer then + explorer.renderer:draw() + end + + log.profile_end(profile_redraw) + end) + end + + log.profile_end(profile_event) +end + +---Fired on CocDiagnosticChanged events: +---debounced retrieval, cache update, version increment and draw +function M.update_coc() + if not M.enable then + return + end + utils.debounce("CocDiagnosticChanged update", M.debounce_delay, function() + local profile = log.profile_start("CocDiagnosticChanged update") + NODE_SEVERITIES = from_coc() + NODE_SEVERITIES_VERSION = NODE_SEVERITIES_VERSION + 1 + if log.enabled("diagnostics") then + for bufname, severity in pairs(NODE_SEVERITIES) do + log.line("diagnostics", "COC Indexing bufname '%s' with severity %d", bufname, severity) + end + end + log.profile_end(profile) + + local bufnr = view.get_bufnr() + local should_draw = bufnr and vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) + if should_draw then + local explorer = core.get_explorer() + if explorer then + explorer.renderer:draw() + end + end + end) +end + +---Maybe retrieve diagnostic status for a node. +---Returns cached value when node's version matches. +---@param node Node +---@return DiagStatus|nil +function M.get_diag_status(node) + if not M.enable then + return nil + end + + -- dir but we shouldn't show on dirs at all + if node:is(DirectoryNode) and not M.show_on_dirs then + return nil + end + + -- here, we do a lazy update of the diagnostic status carried by the node. + -- This is by design, as diagnostics and nodes live in completely separate + -- worlds, and this module is the link between the two + if not node.diag_status or node.diag_status.cache_version < NODE_SEVERITIES_VERSION then + node.diag_status = from_cache(node) + end + + local dir = node:as(DirectoryNode) + + -- file + if not dir then + return node.diag_status + end + + -- dir is closed or we should show on open_dirs + if not dir.open or M.show_on_open_dirs then + return node.diag_status + end + return nil +end + +function M.setup(opts) + M.enable = opts.diagnostics.enable + M.debounce_delay = opts.diagnostics.debounce_delay + M.severity = opts.diagnostics.severity + + if M.enable then + log.line("diagnostics", "setup") + end + + M.show_on_dirs = opts.diagnostics.show_on_dirs + M.show_on_open_dirs = opts.diagnostics.show_on_open_dirs +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/enum.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/enum.lua new file mode 100644 index 0000000..e990822 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/enum.lua @@ -0,0 +1,14 @@ +local M = {} + +---Reason for filter in filter.lua +---@enum FILTER_REASON +M.FILTER_REASON = { + none = 0, -- It's not filtered + git = 1, + buf = 2, + dotfile = 4, + custom = 8, + bookmark = 16, +} + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/events.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/events.lua new file mode 100644 index 0000000..7b57333 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/events.lua @@ -0,0 +1,119 @@ +local notify = require("nvim-tree.notify") + +local M = {} + +local global_handlers = {} + +M.Event = { + Ready = "Ready", + WillRenameNode = "WillRenameNode", + NodeRenamed = "NodeRenamed", + TreeOpen = "TreeOpen", + TreeClose = "TreeClose", + WillCreateFile = "WillCreateFile", + FileCreated = "FileCreated", + WillRemoveFile = "WillRemoveFile", + FileRemoved = "FileRemoved", + FolderCreated = "FolderCreated", + FolderRemoved = "FolderRemoved", + Resize = "Resize", + TreeAttachedPost = "TreeAttachedPost", + TreeRendered = "TreeRendered", +} + +---@param event_name string +---@return table +local function get_handlers(event_name) + return global_handlers[event_name] or {} +end + +---@param event_name string +---@param handler function +function M.subscribe(event_name, handler) + local handlers = get_handlers(event_name) + table.insert(handlers, handler) + global_handlers[event_name] = handlers +end + +---@param event_name string +---@param payload table|nil +local function dispatch(event_name, payload) + for _, handler in pairs(get_handlers(event_name)) do + local success, error = pcall(handler, payload) + if not success then + notify.error("Handler for event " .. event_name .. " errored. " .. vim.inspect(error)) + end + end +end + +--@private +function M._dispatch_ready() + dispatch(M.Event.Ready) +end + +--@private +function M._dispatch_will_rename_node(old_name, new_name) + dispatch(M.Event.WillRenameNode, { old_name = old_name, new_name = new_name }) +end + +--@private +function M._dispatch_node_renamed(old_name, new_name) + dispatch(M.Event.NodeRenamed, { old_name = old_name, new_name = new_name }) +end + +--@private +function M._dispatch_will_remove_file(fname) + dispatch(M.Event.WillRemoveFile, { fname = fname }) +end + +--@private +function M._dispatch_file_removed(fname) + dispatch(M.Event.FileRemoved, { fname = fname }) +end + +--@private +function M._dispatch_will_create_file(fname) + dispatch(M.Event.WillCreateFile, { fname = fname }) +end + +--@private +function M._dispatch_file_created(fname) + dispatch(M.Event.FileCreated, { fname = fname }) +end + +--@private +function M._dispatch_folder_created(folder_name) + dispatch(M.Event.FolderCreated, { folder_name = folder_name }) +end + +--@private +function M._dispatch_folder_removed(folder_name) + dispatch(M.Event.FolderRemoved, { folder_name = folder_name }) +end + +--@private +function M._dispatch_on_tree_open() + dispatch(M.Event.TreeOpen, nil) +end + +--@private +function M._dispatch_on_tree_close() + dispatch(M.Event.TreeClose, nil) +end + +--@private +function M._dispatch_on_tree_resize(size) + dispatch(M.Event.Resize, size) +end + +--@private +function M._dispatch_tree_attached_post(buf) + dispatch(M.Event.TreeAttachedPost, buf) +end + +--@private +function M._dispatch_on_tree_rendered(bufnr, winnr) + dispatch(M.Event.TreeRendered, { bufnr = bufnr, winnr = winnr }) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/filters.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/filters.lua new file mode 100644 index 0000000..6223068 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/filters.lua @@ -0,0 +1,287 @@ +local utils = require("nvim-tree.utils") +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON + +local Class = require("nvim-tree.classic") + +---@alias FilterType "custom" | "dotfiles" | "git_ignored" | "git_clean" | "no_buffer" | "no_bookmark" + +---@class (exact) Filters: Class +---@field enabled boolean +---@field state table +---@field private explorer Explorer +---@field private exclude_list string[] filters.exclude +---@field private ignore_list table filters.custom string table +---@field private custom_function (fun(absolute_path: string): boolean)|nil filters.custom function +local Filters = Class:extend() + +---@class Filters +---@overload fun(args: FiltersArgs): Filters + +---@class (exact) FiltersArgs +---@field explorer Explorer + +---@protected +---@param args FiltersArgs +function Filters:new(args) + self.explorer = args.explorer + self.ignore_list = {} + self.exclude_list = self.explorer.opts.filters.exclude + self.custom_function = nil + + self.enabled = self.explorer.opts.filters.enable + self.state = { + custom = true, + dotfiles = self.explorer.opts.filters.dotfiles, + git_ignored = self.explorer.opts.filters.git_ignored, + git_clean = self.explorer.opts.filters.git_clean, + no_buffer = self.explorer.opts.filters.no_buffer, + no_bookmark = self.explorer.opts.filters.no_bookmark, + } + + local custom_filter = self.explorer.opts.filters.custom + if type(custom_filter) == "function" then + self.custom_function = custom_filter + else + if custom_filter and #custom_filter > 0 then + for _, filter_name in pairs(custom_filter) do + self.ignore_list[filter_name] = true + end + end + end +end + +---@private +---@param path string +---@return boolean +function Filters:is_excluded(path) + for _, node in ipairs(self.exclude_list) do + if path:match(node) then + return true + end + end + return false +end + +---Check if the given path is git clean/ignored +---@private +---@param path string Absolute path +---@param project GitProject from prepare +---@return boolean +function Filters:git(path, project) + if type(project) ~= "table" or type(project.files) ~= "table" or type(project.dirs) ~= "table" then + return false + end + + -- default status to clean + local xy = project.files[path] + xy = xy or project.dirs.direct[path] and project.dirs.direct[path][1] + xy = xy or project.dirs.indirect[path] and project.dirs.indirect[path][1] + + -- filter ignored; overrides clean as they are effectively dirty + if self.state.git_ignored and xy == "!!" then + return true + end + + -- filter clean + if self.state.git_clean and not xy then + return true + end + + return false +end + +---Check if the given path has no listed buffer +---@private +---@param path string Absolute path +---@param bufinfo table vim.fn.getbufinfo { buflisted = 1 } +---@return boolean +function Filters:buf(path, bufinfo) + if not self.state.no_buffer or type(bufinfo) ~= "table" then + return false + end + + -- filter files with no open buffer and directories containing no open buffers + for _, b in ipairs(bufinfo) do + if b.name == path or b.name:find(path .. "/", 1, true) then + return false + end + end + + return true +end + +---@private +---@param path string +---@return boolean +function Filters:dotfile(path) + return self.state.dotfiles and utils.path_basename(path):sub(1, 1) == "." +end + +---Bookmark is present +---@private +---@param path string +---@param path_type string|nil filetype of path +---@param bookmarks table path, filetype table of bookmarked files +---@return boolean +function Filters:bookmark(path, path_type, bookmarks) + if not self.state.no_bookmark then + return false + end + -- if bookmark is empty, we should see a empty filetree + if next(bookmarks) == nil then + return true + end + + local mark_parent = utils.path_add_trailing(path) + for mark, mark_type in pairs(bookmarks) do + if path == mark then + return false + end + + if path_type == "directory" then + -- check if path is mark's parent + if vim.fn.stridx(mark, mark_parent) == 0 then + return false + end + end + if mark_type == "directory" then + -- check if mark is path's parent + local path_parent = utils.path_add_trailing(mark) + if vim.fn.stridx(path, path_parent) == 0 then + return false + end + end + end + + return true +end + +---@private +---@param path string +---@return boolean +function Filters:custom(path) + if not self.state.custom then + return false + end + + local basename = utils.path_basename(path) + + -- filter user's custom function + if self.custom_function and self.custom_function(path) then + return true + end + + -- filter custom regexes + local relpath = utils.path_relative(path, vim.loop.cwd()) + for pat, _ in pairs(self.ignore_list) do + if vim.fn.match(relpath, pat) ~= -1 or vim.fn.match(basename, pat) ~= -1 then + return true + end + end + + local idx = path:match(".+()%.[^.]+$") + if idx then + if self.ignore_list["*" .. string.sub(path, idx)] == true then + return true + end + end + + return false +end + +---Prepare arguments for should_filter. This is done prior to should_filter for efficiency reasons. +---@param project GitProject? optional results of git.load_projects(...) +---@return table +--- project: reference +--- bufinfo: empty unless no_buffer set: vim.fn.getbufinfo { buflisted = 1 } +--- bookmarks: absolute paths to boolean +function Filters:prepare(project) + local status = { + project = project or {}, + bufinfo = {}, + bookmarks = {}, + } + + if self.state.no_buffer then + status.bufinfo = vim.fn.getbufinfo({ buflisted = 1 }) + end + + local explorer = require("nvim-tree.core").get_explorer() + if explorer then + for _, node in pairs(explorer.marks:list()) do + status.bookmarks[node.absolute_path] = node.type + end + end + + return status +end + +---Check if the given path should be filtered. +---@param path string Absolute path +---@param fs_stat uv.fs_stat.result|nil fs_stat of file +---@param status table from prepare +---@return boolean +function Filters:should_filter(path, fs_stat, status) + if not self.enabled then + return false + end + + -- exclusions override all filters + if self:is_excluded(path) then + return false + end + + return self:git(path, status.project) + or self:buf(path, status.bufinfo) + or self:dotfile(path) + or self:custom(path) + or self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks) +end + +--- Check if the given path should be filtered, and provide the reason why it was +---@param path string Absolute path +---@param fs_stat uv.fs_stat.result|nil fs_stat of file +---@param status table from prepare +---@return FILTER_REASON +function Filters:should_filter_as_reason(path, fs_stat, status) + if not self.enabled then + return FILTER_REASON.none + end + + if self:is_excluded(path) then + return FILTER_REASON.none + end + + if self:git(path, status.project) then + return FILTER_REASON.git + elseif self:buf(path, status.bufinfo) then + return FILTER_REASON.buf + elseif self:dotfile(path) then + return FILTER_REASON.dotfile + elseif self:custom(path) then + return FILTER_REASON.custom + elseif self:bookmark(path, fs_stat and fs_stat.type, status.bookmarks) then + return FILTER_REASON.bookmark + else + return FILTER_REASON.none + end +end + +---Toggle a type and refresh +---@private +---@param type FilterType? nil to disable all +function Filters:toggle(type) + if not type or self.state[type] == nil then + self.enabled = not self.enabled + else + self.state[type] = not self.state[type] + end + + local node = self.explorer:get_node_at_cursor() + self.explorer:reload_explorer() + if node then + utils.focus_node_or_parent(node) + end +end + +return Filters diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/init.lua new file mode 100644 index 0000000..0fdd46a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/init.lua @@ -0,0 +1,542 @@ +local appearance = require("nvim-tree.appearance") +local buffers = require("nvim-tree.buffers") +local core = require("nvim-tree.core") +local git = require("nvim-tree.git") +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") +local view = require("nvim-tree.view") +local node_factory = require("nvim-tree.node.factory") + +local DirectoryNode = require("nvim-tree.node.directory") +local RootNode = require("nvim-tree.node.root") +local Watcher = require("nvim-tree.watcher") + +local Iterator = require("nvim-tree.iterators.node-iterator") +local NodeIterator = require("nvim-tree.iterators.node-iterator") + +local Filters = require("nvim-tree.explorer.filters") +local Marks = require("nvim-tree.marks") +local LiveFilter = require("nvim-tree.explorer.live-filter") +local Sorter = require("nvim-tree.explorer.sorter") +local Clipboard = require("nvim-tree.actions.fs.clipboard") +local Renderer = require("nvim-tree.renderer") + +local FILTER_REASON = require("nvim-tree.enum").FILTER_REASON + +local config + +---@class (exact) Explorer: RootNode +---@field uid_explorer number vim.loop.hrtime() at construction time +---@field opts table user options +---@field augroup_id integer +---@field renderer Renderer +---@field filters Filters +---@field live_filter LiveFilter +---@field sorters Sorter +---@field marks Marks +---@field clipboard Clipboard +local Explorer = RootNode:extend() + +---@class Explorer +---@overload fun(args: ExplorerArgs): Explorer + +---@class (exact) ExplorerArgs +---@field path string + +---@protected +---@param args ExplorerArgs +function Explorer:new(args) + Explorer.super.new(self, { + explorer = self, + absolute_path = args.path, + name = "..", + }) + + self.uid_explorer = vim.loop.hrtime() + self.augroup_id = vim.api.nvim_create_augroup("NvimTree_Explorer_" .. self.uid_explorer, {}) + + self.open = true + self.opts = config + + self.sorters = Sorter({ explorer = self }) + self.renderer = Renderer({ explorer = self }) + self.filters = Filters({ explorer = self }) + self.live_filter = LiveFilter({ explorer = self }) + self.marks = Marks({ explorer = self }) + self.clipboard = Clipboard({ explorer = self }) + + self:create_autocmds() + + self:_load(self) +end + +function Explorer:destroy() + log.line("dev", "Explorer:destroy") + + vim.api.nvim_del_augroup_by_id(self.augroup_id) + + RootNode.destroy(self) +end + +function Explorer:create_autocmds() + -- reset and draw (highlights) when colorscheme is changed + vim.api.nvim_create_autocmd("ColorScheme", { + group = self.augroup_id, + callback = function() + appearance.setup() + view.reset_winhl() + self.renderer:draw() + end, + }) + + vim.api.nvim_create_autocmd("BufWritePost", { + group = self.augroup_id, + callback = function() + if self.opts.auto_reload_on_write and not self.opts.filesystem_watchers.enable then + self:reload_explorer() + end + end, + }) + + vim.api.nvim_create_autocmd("BufReadPost", { + group = self.augroup_id, + callback = function(data) + if (self.filters.state.no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then + utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function() + self:reload_explorer() + end) + end + end, + }) + + -- update opened file buffers + vim.api.nvim_create_autocmd("BufUnload", { + group = self.augroup_id, + callback = function(data) + if (self.filters.state.no_buffer or self.opts.highlight_opened_files ~= "none") and vim.bo[data.buf].buftype == "" then + utils.debounce("Buf:filter_buffer_" .. self.uid_explorer, self.opts.view.debounce_delay, function() + self:reload_explorer() + end) + end + end, + }) + + vim.api.nvim_create_autocmd("BufEnter", { + group = self.augroup_id, + pattern = "NvimTree_*", + callback = function() + if utils.is_nvim_tree_buf(0) then + if vim.fn.getcwd() ~= core.get_cwd() or (self.opts.reload_on_bufenter and not self.opts.filesystem_watchers.enable) then + self:reload_explorer() + end + end + end, + }) + + vim.api.nvim_create_autocmd("User", { + group = self.augroup_id, + pattern = { "FugitiveChanged", "NeogitStatusRefreshed" }, + callback = function() + if not self.opts.filesystem_watchers.enable and self.opts.git.enable then + self:reload_git() + end + end, + }) + + if self.opts.hijack_cursor then + vim.api.nvim_create_autocmd("CursorMoved", { + group = self.augroup_id, + pattern = "NvimTree_*", + callback = function() + if utils.is_nvim_tree_buf(0) then + self:place_cursor_on_node() + end + end, + }) + end + + if self.opts.modified.enable then + vim.api.nvim_create_autocmd({ "BufModifiedSet", "BufWritePost" }, { + group = self.augroup_id, + callback = function() + utils.debounce("Buf:modified_" .. self.uid_explorer, self.opts.view.debounce_delay, function() + buffers.reload_modified() + self:reload_explorer() + end) + end, + }) + end +end + +---@param node DirectoryNode +function Explorer:expand(node) + self:_load(node) +end + +---@param node DirectoryNode +---@param project GitProject? +---@return Node[]? +function Explorer:reload(node, project) + local cwd = node.link_to or node.absolute_path + local handle = vim.loop.fs_scandir(cwd) + if not handle then + return + end + + local profile = log.profile_start("reload %s", node.absolute_path) + + local filter_status = self.filters:prepare(project) + + if node.group_next then + node.nodes = { node.group_next } + node.group_next = nil + end + + local remain_childs = {} + + local node_ignored = node:is_git_ignored() + ---@type table + local nodes_by_path = utils.key_by(node.nodes, "absolute_path") + + -- To reset we must 'zero' everything that we use + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }) + + while true do + local name, _ = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local abs = utils.path_join({ cwd, name }) + ---@type uv.fs_stat.result|nil + local stat = vim.loop.fs_lstat(abs) + + local filter_reason = self.filters:should_filter_as_reason(abs, stat, filter_status) + if filter_reason == FILTER_REASON.none then + remain_childs[abs] = true + + -- Recreate node if type changes. + if nodes_by_path[abs] then + local n = nodes_by_path[abs] + + if not stat or n.type ~= stat.type then + utils.array_remove(node.nodes, n) + n:destroy() + nodes_by_path[abs] = nil + end + end + + if not nodes_by_path[abs] then + local new_child = node_factory.create({ + explorer = self, + parent = node, + absolute_path = abs, + name = name, + fs_stat = stat + }) + if new_child then + table.insert(node.nodes, new_child) + nodes_by_path[abs] = new_child + end + else + local n = nodes_by_path[abs] + if n then + n.executable = utils.is_executable(abs) or false + n.fs_stat = stat + end + end + else + for reason, value in pairs(FILTER_REASON) do + if filter_reason == value then + node.hidden_stats[reason] = node.hidden_stats[reason] + 1 + end + end + end + end + + node.nodes = vim.tbl_map( + self:update_git_statuses(nodes_by_path, node_ignored, project), + vim.tbl_filter(function(n) + if remain_childs[n.absolute_path] then + return remain_childs[n.absolute_path] + else + n:destroy() + return false + end + end, node.nodes) + ) + + local single_child = node:single_child_directory() + if config.renderer.group_empty and node.parent and single_child then + node.group_next = single_child + local ns = self:reload(single_child, project) + node.nodes = ns or {} + log.profile_end(profile) + return ns + end + + self.sorters:sort(node.nodes) + self.live_filter:apply_filter(node) + log.profile_end(profile) + return node.nodes +end + +---Refresh contents of all nodes to a path: actual directory and links. +---Groups will be expanded if needed. +---@param path string absolute path +function Explorer:refresh_parent_nodes_for_path(path) + local profile = log.profile_start("refresh_parent_nodes_for_path %s", path) + + -- collect parent nodes from the top down + local parent_nodes = {} + NodeIterator.builder({ self }) + :recursor(function(node) + return node.nodes + end) + :applier(function(node) + local abs_contains = node.absolute_path and path:find(node.absolute_path, 1, true) == 1 + local link_contains = node.link_to and path:find(node.link_to, 1, true) == 1 + if abs_contains or link_contains then + table.insert(parent_nodes, node) + end + end) + :iterate() + + -- refresh in order; this will expand groups as needed + for _, node in ipairs(parent_nodes) do + local toplevel = git.get_toplevel(node.absolute_path) + local project = git.get_project(toplevel) or {} + + self:reload(node, project) + git.update_parent_projects(node, project, toplevel) + end + + log.profile_end(profile) +end + +---@private +---@param node DirectoryNode +function Explorer:_load(node) + local cwd = node.link_to or node.absolute_path + local project = git.load_project(cwd) + self:explore(node, project, self) +end + +---@private +---@param nodes_by_path Node[] +---@param node_ignored boolean +---@param project GitProject? +---@return fun(node: Node): Node +function Explorer:update_git_statuses(nodes_by_path, node_ignored, project) + return function(node) + if nodes_by_path[node.absolute_path] then + node:update_git_status(node_ignored, project) + end + return node + end +end + +---@private +---@param handle uv.uv_fs_t +---@param cwd string +---@param node DirectoryNode +---@param project GitProject +---@param parent Explorer +function Explorer:populate_children(handle, cwd, node, project, parent) + local node_ignored = node:is_git_ignored() + local nodes_by_path = utils.bool_record(node.nodes, "absolute_path") + + local filter_status = parent.filters:prepare(project) + + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }) + + while true do + local name, _ = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + local abs = utils.path_join({ cwd, name }) + + if Watcher.is_fs_event_capable(abs) then + local profile = log.profile_start("populate_children %s", abs) + + ---@type uv.fs_stat.result|nil + local stat = vim.loop.fs_lstat(abs) + local filter_reason = parent.filters:should_filter_as_reason(abs, stat, filter_status) + if filter_reason == FILTER_REASON.none and not nodes_by_path[abs] then + local child = node_factory.create({ + explorer = self, + parent = node, + absolute_path = abs, + name = name, + fs_stat = stat + }) + if child then + table.insert(node.nodes, child) + nodes_by_path[child.absolute_path] = true + child:update_git_status(node_ignored, project) + end + else + for reason, value in pairs(FILTER_REASON) do + if filter_reason == value then + node.hidden_stats[reason] = node.hidden_stats[reason] + 1 + end + end + end + + log.profile_end(profile) + end + end +end + +---@private +---@param node DirectoryNode +---@param project GitProject +---@param parent Explorer +---@return Node[]|nil +function Explorer:explore(node, project, parent) + local cwd = node.link_to or node.absolute_path + local handle = vim.loop.fs_scandir(cwd) + if not handle then + return + end + + local profile = log.profile_start("explore %s", node.absolute_path) + + self:populate_children(handle, cwd, node, project, parent) + + local is_root = not node.parent + local single_child = node:single_child_directory() + if config.renderer.group_empty and not is_root and single_child then + local child_cwd = single_child.link_to or single_child.absolute_path + local child_project = git.load_project(child_cwd) + node.group_next = single_child + local ns = self:explore(single_child, child_project, parent) + node.nodes = ns or {} + + log.profile_end(profile) + return ns + end + + parent.sorters:sort(node.nodes) + parent.live_filter:apply_filter(node) + + log.profile_end(profile) + return node.nodes +end + +---@private +---@param projects GitProject[] +function Explorer:refresh_nodes(projects) + Iterator.builder({ self }) + :applier(function(n) + local dir = n:as(DirectoryNode) + if dir then + local toplevel = git.get_toplevel(dir.cwd or dir.link_to or dir.absolute_path) + self:reload(dir, projects[toplevel] or {}) + end + end) + :recursor(function(n) + return n.group_next and { n.group_next } or (n.open and n.nodes) + end) + :iterate() +end + +local event_running = false +function Explorer:reload_explorer() + if event_running or vim.v.exiting ~= vim.NIL then + return + end + event_running = true + + local projects = git.reload_all_projects() + self:refresh_nodes(projects) + if view.is_visible() then + self.renderer:draw() + end + event_running = false +end + +function Explorer:reload_git() + if not git.config.git.enable or event_running then + return + end + event_running = true + + local projects = git.reload_all_projects() + git.reload_node_status(self, projects) + self.renderer:draw() + event_running = false +end + +---Cursor position as per vim.api.nvim_win_get_cursor +---nil on no explorer or invalid view win +---@return integer[]|nil +function Explorer:get_cursor_position() + local winnr = view.get_winnr() + if not winnr or not vim.api.nvim_win_is_valid(winnr) then + return + end + + return vim.api.nvim_win_get_cursor(winnr) +end + +---@return Node|nil +function Explorer:get_node_at_cursor() + local cursor = self:get_cursor_position() + if not cursor then + return + end + + if cursor[1] == 1 and view.is_root_folder_visible(core.get_cwd()) then + return self + end + + return utils.get_nodes_by_line(self.nodes, core.get_nodes_starting_line())[cursor[1]] +end + +function Explorer:place_cursor_on_node() + local ok, search = pcall(vim.fn.searchcount) + if ok and search and search.exact_match == 1 then + return + end + + local node = self:get_node_at_cursor() + if not node or node.name == ".." then + return + end + node = node:get_parent_of_group() or node + + local line = vim.api.nvim_get_current_line() + local cursor = vim.api.nvim_win_get_cursor(0) + local idx = vim.fn.stridx(line, node.name) + + if idx >= 0 then + vim.api.nvim_win_set_cursor(0, { cursor[1], idx }) + end +end + +---Api.tree.get_nodes +---@return nvim_tree.api.Node +function Explorer:get_nodes() + return self:clone() +end + +function Explorer:setup(opts) + config = opts +end + +return Explorer diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/live-filter.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/live-filter.lua new file mode 100644 index 0000000..62a7dd9 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/live-filter.lua @@ -0,0 +1,229 @@ +local view = require("nvim-tree.view") +local utils = require("nvim-tree.utils") + +local Class = require("nvim-tree.classic") +local Iterator = require("nvim-tree.iterators.node-iterator") +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) LiveFilter: Class +---@field explorer Explorer +---@field prefix string +---@field always_show_folders boolean +---@field filter string +local LiveFilter = Class:extend() + +---@class LiveFilter +---@overload fun(args: LiveFilterArgs): LiveFilter + +---@class (exact) LiveFilterArgs +---@field explorer Explorer + +---@protected +---@param args LiveFilterArgs +function LiveFilter:new(args) + self.explorer = args.explorer + self.prefix = self.explorer.opts.live_filter.prefix + self.always_show_folders = self.explorer.opts.live_filter.always_show_folders + self.filter = nil +end + +---@param node_ Node? +local function reset_filter(self, node_) + node_ = node_ or self.explorer + + if node_ == nil then + return + end + + local dir_ = node_:as(DirectoryNode) + if dir_ then + dir_.hidden_stats = vim.tbl_deep_extend("force", dir_.hidden_stats or {}, { live_filter = 0, }) + end + + Iterator.builder(node_.nodes) + :hidden() + :applier(function(node) + node.hidden = false + local dir = node:as(DirectoryNode) + if dir then + dir.hidden_stats = vim.tbl_deep_extend("force", dir.hidden_stats or {}, { live_filter = 0, }) + end + end) + :iterate() +end + +local overlay_bufnr = 0 +local overlay_winnr = 0 + +local function remove_overlay(self) + if view.View.float.enable and view.View.float.quit_on_focus_loss then + -- return to normal nvim-tree float behaviour when filter window is closed + vim.api.nvim_create_autocmd("WinLeave", { + pattern = "NvimTree_*", + group = vim.api.nvim_create_augroup("NvimTree", { clear = false }), + callback = function() + if utils.is_nvim_tree_buf(0) then + view.close() + end + end, + }) + end + + vim.api.nvim_win_close(overlay_winnr, true) + vim.api.nvim_buf_delete(overlay_bufnr, { force = true }) + overlay_bufnr = 0 + overlay_winnr = 0 + + if self.filter == "" then + self:clear_filter() + end +end + +---@param node Node +---@return boolean +local function matches(self, node) + if not self.explorer.filters.enabled then + return true + end + + local path = node.absolute_path + local name = vim.fn.fnamemodify(path, ":t") + return vim.regex(self.filter):match_str(name) ~= nil +end + +---@param node_ DirectoryNode? +function LiveFilter:apply_filter(node_) + if not self.filter or self.filter == "" then + reset_filter(self, node_) + return + end + + -- this iterator cannot yet be refactored with the Iterator module + -- since the node mapper is based on its children + local function iterate(node) + local filtered_nodes = 0 + local nodes = node.group_next and { node.group_next } or node.nodes + + node.hidden_stats = vim.tbl_deep_extend("force", node.hidden_stats or {}, { + live_filter = 0, + }) + + if nodes then + for _, n in pairs(nodes) do + iterate(n) + if n.hidden then + filtered_nodes = filtered_nodes + 1 + end + end + end + + node.hidden_stats.live_filter = filtered_nodes + + local has_nodes = nodes and (self.always_show_folders or #nodes > filtered_nodes) + local ok, is_match = pcall(matches, self, node) + node.hidden = not (has_nodes or (ok and is_match)) + end + + iterate(node_ or self.explorer) +end + +local function record_char(self) + vim.schedule(function() + self.filter = vim.api.nvim_buf_get_lines(overlay_bufnr, 0, -1, false)[1] + self:apply_filter() + self.explorer.renderer:draw() + end) +end + +local function configure_buffer_overlay(self) + overlay_bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_buf_attach(overlay_bufnr, true, { + on_lines = function() + return record_char(self) + end, + }) + + vim.api.nvim_create_autocmd("InsertLeave", { + callback = function() + return remove_overlay(self) + end, + once = true, + }) + + vim.api.nvim_buf_set_keymap(overlay_bufnr, "i", "", "stopinsert", {}) +end + +---@return integer +local function calculate_overlay_win_width(self) + local wininfo = vim.fn.getwininfo(view.get_winnr())[1] + + if wininfo then + return wininfo.width - wininfo.textoff - #self.prefix + end + + return 20 +end + +local function create_overlay(self) + if view.View.float.enable then + -- don't close nvim-tree float when focus is changed to filter window + vim.api.nvim_clear_autocmds({ + event = "WinLeave", + pattern = "NvimTree_*", + group = vim.api.nvim_create_augroup("NvimTree", { clear = false }), + }) + end + + configure_buffer_overlay(self) + overlay_winnr = vim.api.nvim_open_win(overlay_bufnr, true, { + col = 1, + row = 0, + relative = "cursor", + width = calculate_overlay_win_width(self), + height = 1, + border = "none", + style = "minimal", + }) + + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("modifiable", true, { buf = overlay_bufnr }) + else + vim.api.nvim_buf_set_option(overlay_bufnr, "modifiable", true) ---@diagnostic disable-line: deprecated + end + + vim.api.nvim_buf_set_lines(overlay_bufnr, 0, -1, false, { self.filter }) + vim.cmd("startinsert") + vim.api.nvim_win_set_cursor(overlay_winnr, { 1, #self.filter + 1 }) +end + +function LiveFilter:start_filtering() + view.View.live_filter.prev_focused_node = self.explorer:get_node_at_cursor() + self.filter = self.filter or "" + + self.explorer.renderer:draw() + local row = require("nvim-tree.core").get_nodes_starting_line() - 1 + local col = #self.prefix > 0 and #self.prefix - 1 or 1 + view.set_cursor({ row, col }) + -- needs scheduling to let the cursor move before initializing the window + vim.schedule(function() + return create_overlay(self) + end) +end + +function LiveFilter:clear_filter() + local node = self.explorer:get_node_at_cursor() + local last_node = view.View.live_filter.prev_focused_node + + self.filter = nil + reset_filter(self) + self.explorer.renderer:draw() + + if node then + utils.focus_file(node.absolute_path) + elseif last_node then + utils.focus_file(last_node.absolute_path) + end +end + +return LiveFilter diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/sorter.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/sorter.lua new file mode 100644 index 0000000..799cfa4 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/sorter.lua @@ -0,0 +1,331 @@ +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 +---@overload fun(args: SorterArgs): Sorter + +---@class (exact) SorterArgs +---@field explorer Explorer + +---@protected +---@param args SorterArgs +function Sorter:new(args) + self.explorer = args.explorer +end + +---Create a shallow copy of a portion of a list. +---@param t table +---@param first integer First index, inclusive +---@param last integer Last index, inclusive +---@return table +local function tbl_slice(t, first, last) + local slice = {} + for i = first, last or #t, 1 do + table.insert(slice, t[i]) + end + + return slice +end + +---Evaluate folders_first and sort.files_first returning nil when no order is necessary +---@private +---@type SorterComparator +function Sorter:folders_or_files_first(a, b) + if not (self.explorer.opts.sort.folders_first or self.explorer.opts.sort.files_first) then + return nil + end + + if not a:is(DirectoryNode) and b:is(DirectoryNode) then + -- file <> folder + return self.explorer.opts.sort.files_first + elseif a:is(DirectoryNode) and not b:is(DirectoryNode) then + -- folder <> file + return not self.explorer.opts.sort.files_first + end + + return nil +end + +---@private +---@param t Node[] +---@param first number +---@param mid number +---@param last number +---@param comparator SorterComparator +function Sorter:merge(t, first, mid, last, comparator) + local n1 = mid - first + 1 + local n2 = last - mid + local ls = tbl_slice(t, first, mid) + local rs = tbl_slice(t, mid + 1, last) + local i = 1 + local j = 1 + local k = first + + while i <= n1 and j <= n2 do + if comparator(self, ls[i], rs[j]) then + t[k] = ls[i] + i = i + 1 + else + t[k] = rs[j] + j = j + 1 + end + k = k + 1 + end + + while i <= n1 do + t[k] = ls[i] + i = i + 1 + k = k + 1 + end + + while j <= n2 do + t[k] = rs[j] + j = j + 1 + k = k + 1 + end +end + +---@private +---@param t Node[] +---@param first number +---@param last number +---@param comparator SorterComparator +function Sorter:split_merge(t, first, last, comparator) + if (last - first) < 1 then + return + end + + local mid = math.floor((first + last) / 2) + + self:split_merge(t, first, mid, comparator) + self:split_merge(t, mid + 1, last, comparator) + self:merge(t, first, mid, last, comparator) +end + +---Perform a merge sort using sorter option. +---@param t Node[] +function Sorter:sort(t) + if self[self.explorer.opts.sort.sorter] then + self:split_merge(t, 1, #t, self[self.explorer.opts.sort.sorter]) + elseif type(self.explorer.opts.sort.sorter) == "function" then + local t_user = {} + local origin_index = {} + + for _, n in ipairs(t) do + table.insert(t_user, { + absolute_path = n.absolute_path, + executable = n.executable, + extension = n.extension, + filetype = vim.filetype.match({ filename = n.name }), + link_to = n.link_to, + name = n.name, + type = n.type, + }) + table.insert(origin_index, n) + end + + -- user may return a SorterType + local ret = self.explorer.opts.sort.sorter(t_user) + if self[ret] then + self:split_merge(t, 1, #t, self[ret]) + return + end + + -- do merge sort for prevent memory exceed + local user_index = {} + for i, v in ipairs(t_user) do + if type(v.absolute_path) == "string" and user_index[v.absolute_path] == nil then + user_index[v.absolute_path] = i + end + end + + -- if missing value found, then using origin_index + local mini_comparator = function(_, a, b) + local a_index = user_index[a.absolute_path] or origin_index[a.absolute_path] + local b_index = user_index[b.absolute_path] or origin_index[b.absolute_path] + + if type(a_index) == "number" and type(b_index) == "number" then + return a_index <= b_index + end + return (a_index or 0) <= (b_index or 0) + end + + self:split_merge(t, 1, #t, mini_comparator) -- sort by user order + end +end + +---@private +---@param a Node +---@param b Node +---@param ignore_case boolean +---@return boolean +function Sorter:name_case(a, b, ignore_case) + if not (a and b) then + return true + end + + local early_return = self:folders_or_files_first(a, b) + if early_return ~= nil then + return early_return + end + + if ignore_case then + return a.name:lower() <= b.name:lower() + else + return a.name <= b.name + end +end + +---@private +---@type SorterComparator +function Sorter:case_sensitive(a, b) + return self:name_case(a, b, false) +end + +---@private +---@type SorterComparator +function Sorter:name(a, b) + return self:name_case(a, b, true) +end + +---@private +---@type SorterComparator +function Sorter:modification_time(a, b) + if not (a and b) then + return true + end + + local early_return = self:folders_or_files_first(a, b) + if early_return ~= nil then + return early_return + end + + local last_modified_a = 0 + local last_modified_b = 0 + + if a.fs_stat ~= nil then + last_modified_a = a.fs_stat.mtime.sec + end + + if b.fs_stat ~= nil then + last_modified_b = b.fs_stat.mtime.sec + end + + return last_modified_b <= last_modified_a +end + +---@private +---@type SorterComparator +function Sorter:suffix(a, b) + if not (a and b) then + return true + end + + -- directories go first + local early_return = self:folders_or_files_first(a, b) + if early_return ~= nil then + return early_return + elseif a.nodes and b.nodes then + return self:name(a, b) + end + + -- dotfiles go second + if a.name:sub(1, 1) == "." and b.name:sub(1, 1) ~= "." then + return true + elseif a.name:sub(1, 1) ~= "." and b.name:sub(1, 1) == "." then + return false + elseif a.name:sub(1, 1) == "." and b.name:sub(1, 1) == "." then + return self:name(a, b) + end + + -- unsuffixed go third + local a_suffix_ndx = a.name:find("%.%w+$") + local b_suffix_ndx = b.name:find("%.%w+$") + + if not a_suffix_ndx and b_suffix_ndx then + return true + elseif a_suffix_ndx and not b_suffix_ndx then + return false + elseif not (a_suffix_ndx and b_suffix_ndx) then + return self:name(a, b) + end + + -- finally, compare by suffixes + local a_suffix = a.name:sub(a_suffix_ndx) + local b_suffix = b.name:sub(b_suffix_ndx) + + if a_suffix and not b_suffix then + return true + elseif not a_suffix and b_suffix then + return false + elseif a_suffix:lower() == b_suffix:lower() then + return self:name(a, b) + end + + return a_suffix:lower() < b_suffix:lower() +end + +---@private +---@type SorterComparator +function Sorter:extension(a, b) + if not (a and b) then + return true + end + + local early_return = self:folders_or_files_first(a, b) + if early_return ~= nil then + return early_return + end + + if a.extension and not b.extension then + return true + elseif not a.extension and b.extension then + return false + end + + local a_ext = (a.extension or ""):lower() + local b_ext = (b.extension or ""):lower() + if a_ext == b_ext then + return self:name(a, b) + end + + return a_ext < b_ext +end + +---@private +---@type SorterComparator +function Sorter:filetype(a, b) + local a_ft = vim.filetype.match({ filename = a.name }) + local b_ft = vim.filetype.match({ filename = b.name }) + + -- directories first + local early_return = self:folders_or_files_first(a, b) + if early_return ~= nil then + return early_return + end + + -- one is nil, the other wins + if a_ft and not b_ft then + return true + elseif not a_ft and b_ft then + return false + end + + -- same filetype or both nil, sort by name + if a_ft == b_ft then + return self:name(a, b) + end + + return a_ft < b_ft +end + +return Sorter diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/watch.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/watch.lua new file mode 100644 index 0000000..06eb429 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/explorer/watch.lua @@ -0,0 +1,100 @@ +local log = require("nvim-tree.log") +local git = require("nvim-tree.git") +local utils = require("nvim-tree.utils") +local Watcher = require("nvim-tree.watcher").Watcher + +local M = { + config = {}, + uid = 0, +} + +---@param path string +---@return boolean +local function is_git(path) + -- If $GIT_DIR is set, consider its value to be equivalent to '.git'. + -- Expand $GIT_DIR (and `path`) to a full path (see :help filename-modifiers), since + -- it's possible to set it to a relative path. We want to make our best + -- effort to expand that to a valid absolute path. + if vim.fn.fnamemodify(path, ":p") == vim.fn.fnamemodify(vim.env.GIT_DIR, ":p") then + return true + elseif vim.fn.fnamemodify(path, ":t") == ".git" then + return true + else + return false + end +end + +local IGNORED_PATHS = { + -- disable watchers on kernel filesystems + -- which have a lot of unwanted events + "/sys", + "/proc", + "/dev", +} + +---@param path string +---@return boolean +local function is_folder_ignored(path) + for _, folder in ipairs(IGNORED_PATHS) do + if vim.startswith(path, folder) then + return true + end + end + + if type(M.config.filesystem_watchers.ignore_dirs) == "table" then + for _, ignore_dir in ipairs(M.config.filesystem_watchers.ignore_dirs) do + if vim.fn.match(path, ignore_dir) ~= -1 then + return true + end + end + elseif type(M.config.filesystem_watchers.ignore_dirs) == "function" then + return M.config.filesystem_watchers.ignore_dirs(path) + end + + return false +end + +---@param node DirectoryNode +---@return Watcher|nil +function M.create_watcher(node) + if not M.config.filesystem_watchers.enable or type(node) ~= "table" then + return nil + end + + local path = node.link_to or node.absolute_path + if is_git(path) or is_folder_ignored(path) then + return nil + end + + ---@param watcher Watcher + local function callback(watcher) + log.line("watcher", "node event scheduled refresh %s", watcher.data.context) + utils.debounce(watcher.data.context, M.config.filesystem_watchers.debounce_delay, function() + if watcher.destroyed then + return + end + if node.link_to then + log.line("watcher", "node event executing refresh '%s' -> '%s'", node.link_to, node.absolute_path) + else + log.line("watcher", "node event executing refresh '%s'", node.absolute_path) + end + git.refresh_dir(node) + end) + end + + M.uid = M.uid + 1 + return Watcher:create({ + path = path, + callback = callback, + data = { + context = "explorer:watch:" .. path .. ":" .. M.uid + } + }) +end + +function M.setup(opts) + M.config.filesystem_watchers = opts.filesystem_watchers + M.uid = 0 +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/init.lua new file mode 100644 index 0000000..a0294b7 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/init.lua @@ -0,0 +1,415 @@ +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") +local git_utils = require("nvim-tree.git.utils") + +local GitRunner = require("nvim-tree.git.runner") +local Watcher = require("nvim-tree.watcher").Watcher +local Iterator = require("nvim-tree.iterators.node-iterator") +local DirectoryNode = require("nvim-tree.node.directory") + +---Git short format status xy +---@alias GitXY string + +-- Git short-format status +---@alias GitPathXY table + +-- Git short-format statuses +---@alias GitPathXYs table + +---Git short-format statuses for a single node +---@class (exact) GitNodeStatus +---@field file GitXY? +---@field dir table<"direct" | "indirect", GitXY[]>? + +---Git state for an entire repo +---@class (exact) GitProject +---@field files GitProjectFiles? +---@field dirs GitProjectDirs? +---@field watcher Watcher? + +---@alias GitProjectFiles GitPathXY +---@alias GitProjectDirs table<"direct" | "indirect", GitPathXYs> + +local M = { + config = {}, + + ---all projects keyed by toplevel + ---@type table + _projects_by_toplevel = {}, + + ---index of paths inside toplevels, false when not inside a project + ---@type table + _toplevels_by_path = {}, + + -- git dirs by toplevel + ---@type table + _git_dirs_by_toplevel = {}, +} + +-- Files under .git that should result in a reload when changed. +-- Utilities (like watchman) can also write to this directory (often) and aren't useful for us. +local WATCHED_FILES = { + "FETCH_HEAD", -- remote ref + "HEAD", -- local ref + "HEAD.lock", -- HEAD will not always be updated e.g. revert + "config", -- user config + "index", -- staging area +} + +---@param toplevel string|nil +---@param path string|nil +---@param project GitProject +---@param project_files GitProjectFiles? +local function reload_git_project(toplevel, path, project, project_files) + if path then + for p in pairs(project.files) do + if p:find(path, 1, true) == 1 then + project.files[p] = nil + end + end + project.files = vim.tbl_deep_extend("force", project.files, project_files) + else + project.files = project_files or {} + end + + project.dirs = git_utils.project_files_to_project_dirs(project.files, toplevel) +end + +--- Is this path in a known ignored directory? +---@param path string +---@param project GitProject +---@return boolean +local function path_ignored_in_project(path, project) + if not path or not project then + return false + end + + if project.files then + for p, xy in pairs(project.files) do + if xy == "!!" and vim.startswith(path, p) then + return true + end + end + end + return false +end + +---@return GitProject[] maybe empty +function M.reload_all_projects() + if not M.config.git.enable then + return {} + end + + for toplevel in pairs(M._projects_by_toplevel) do + M.reload_project(toplevel) + end + + return M._projects_by_toplevel +end + +--- Reload one project. Does nothing when no project or path is ignored +---@param toplevel string? +---@param path string? optional path to update only +---@param callback function? +function M.reload_project(toplevel, path, callback) + local project = M._projects_by_toplevel[toplevel] --[[@as GitProject]] + + if not toplevel or not project or not M.config.git.enable then + if callback then + callback() + end + return + end + + if path and (path:find(toplevel, 1, true) ~= 1 or path_ignored_in_project(path, project)) then + if callback then + callback() + end + return + end + + ---@type GitRunnerArgs + local args = { + toplevel = toplevel, + path = path, + list_untracked = git_utils.should_show_untracked(toplevel), + list_ignored = true, + timeout = M.config.git.timeout, + } + + if callback then + ---@param path_xy GitPathXY + args.callback = function(path_xy) + reload_git_project(toplevel, path, project, path_xy) + callback() + end + GitRunner:run(args) + else + -- TODO #1974 use callback once async/await is available + reload_git_project(toplevel, path, project, GitRunner:run(args)) + end +end + +--- Retrieve a known project +---@param toplevel string? +---@return GitProject? project +function M.get_project(toplevel) + return M._projects_by_toplevel[toplevel] +end + +--- Retrieve the toplevel for a path. nil on: +--- git disabled +--- not part of a project +--- not a directory +--- path in git.disable_for_dirs +---@param path string absolute +---@return string|nil +function M.get_toplevel(path) + if not path then + return nil + end + + if not M.config.git.enable then + return nil + end + + local tl = M._toplevels_by_path[path] + if tl then + return tl + elseif tl == false then + return nil + end + + local stat, _ = vim.loop.fs_stat(path) + if not stat or stat.type ~= "directory" then + return nil + end + + -- short-circuit any known ignored paths + for root, project in pairs(M._projects_by_toplevel) do + if project and path_ignored_in_project(path, project) then + M._toplevels_by_path[path] = root + return root + end + end + + -- attempt to fetch toplevel + local toplevel, git_dir = git_utils.get_toplevel(path) + if not toplevel or not git_dir then + return nil + end + local toplevel_norm = vim.fn.fnamemodify(toplevel, ":p") + + -- ignore disabled paths + if type(M.config.git.disable_for_dirs) == "table" then + for _, disabled_for_dir in ipairs(M.config.git.disable_for_dirs) do + local disabled_norm = vim.fn.fnamemodify(disabled_for_dir, ":p") + if toplevel_norm == disabled_norm then + return nil + end + end + elseif type(M.config.git.disable_for_dirs) == "function" then + if M.config.git.disable_for_dirs(toplevel_norm) then + return nil + end + end + + M._toplevels_by_path[path] = toplevel + + M._git_dirs_by_toplevel[toplevel] = git_dir + + toplevel = M._toplevels_by_path[path] + if toplevel == false then + return nil + else + return toplevel + end +end + +local function reload_tree_at(toplevel) + if not M.config.git.enable or not toplevel then + return nil + end + + log.line("watcher", "git event executing '%s'", toplevel) + local root_node = utils.get_node_from_path(toplevel) + if not root_node then + return + end + + M.reload_project(toplevel, nil, function() + local project = M.get_project(toplevel) + + Iterator.builder(root_node.nodes) + :hidden() + :applier(function(node) + local parent_ignored = node.parent and node.parent:is_git_ignored() or false + node:update_git_status(parent_ignored, project) + end) + :recursor(function(node) + return node.nodes and #node.nodes > 0 and node.nodes + end) + :iterate() + + root_node.explorer.renderer:draw() + end) +end + +--- Load the project status for a path. Does nothing when no toplevel for path. +--- Only fetches project status when unknown, otherwise returns existing. +---@param path string absolute +---@return GitProject maybe empty +function M.load_project(path) + if not M.config.git.enable then + return {} + end + + local toplevel = M.get_toplevel(path) + if not toplevel then + M._toplevels_by_path[path] = false + return {} + end + + local project = M._projects_by_toplevel[toplevel] + if project then + return project + end + + local path_xys = GitRunner:run({ + toplevel = toplevel, + list_untracked = git_utils.should_show_untracked(toplevel), + list_ignored = true, + timeout = M.config.git.timeout, + }) + + local watcher = nil + if M.config.filesystem_watchers.enable then + log.line("watcher", "git start") + + ---@param w Watcher + local callback = function(w) + log.line("watcher", "git event scheduled '%s'", w.data.toplevel) + utils.debounce("git:watcher:" .. w.data.toplevel, M.config.filesystem_watchers.debounce_delay, function() + if w.destroyed then + return + end + reload_tree_at(w.data.toplevel) + end) + end + + local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join({ toplevel, ".git" }) + watcher = Watcher:create({ + path = git_dir, + files = WATCHED_FILES, + callback = callback, + data = { + toplevel = toplevel, + } + }) + end + + if path_xys then + M._projects_by_toplevel[toplevel] = { + files = path_xys, + dirs = git_utils.project_files_to_project_dirs(path_xys, toplevel), + watcher = watcher, + } + return M._projects_by_toplevel[toplevel] + else + M._toplevels_by_path[path] = false + return {} + end +end + +---@param dir DirectoryNode +---@param project GitProject? +---@param root string? +function M.update_parent_projects(dir, project, root) + while project and dir do + -- step up to the containing project + if dir.absolute_path == root then + -- stop at the top of the tree + if not dir.parent then + break + end + + root = M.get_toplevel(dir.parent.absolute_path) + + -- stop when no more projects + if not root then + break + end + + -- update the containing project + project = M.get_project(root) + M.reload_project(root, dir.absolute_path, nil) + end + + -- update status + dir:update_git_status(dir.parent and dir.parent:is_git_ignored() or false, project) + + -- maybe parent + dir = dir.parent + end +end + +---Refresh contents and git status for a single directory +---@param dir DirectoryNode +function M.refresh_dir(dir) + local node = dir:get_parent_of_group() or dir + local toplevel = M.get_toplevel(dir.absolute_path) + + M.reload_project(toplevel, dir.absolute_path, function() + local project = M.get_project(toplevel) or {} + + dir.explorer:reload(node, project) + + M.update_parent_projects(dir, project, toplevel) + + dir.explorer.renderer:draw() + end) +end + +---@param dir DirectoryNode? +---@param projects GitProject[] +function M.reload_node_status(dir, projects) + dir = dir and dir:as(DirectoryNode) + if not dir or #dir.nodes == 0 then + return + end + + local toplevel = M.get_toplevel(dir.absolute_path) + local project = projects[toplevel] or {} + for _, node in ipairs(dir.nodes) do + node:update_git_status(dir:is_git_ignored(), project) + M.reload_node_status(node:as(DirectoryNode), projects) + end +end + +function M.purge_state() + log.line("git", "purge_state") + + for _, project in pairs(M._projects_by_toplevel) do + if project.watcher then + project.watcher:destroy() + end + end + + M._projects_by_toplevel = {} + M._toplevels_by_path = {} + M._git_dirs_by_toplevel = {} +end + +--- Disable git integration permanently +function M.disable_git_integration() + log.line("git", "disabling git integration") + M.purge_state() + M.config.git.enable = false +end + +function M.setup(opts) + M.config.git = opts.git + M.config.filesystem_watchers = opts.filesystem_watchers +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/runner.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/runner.lua new file mode 100644 index 0000000..0c54057 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/runner.lua @@ -0,0 +1,273 @@ +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") +local notify = require("nvim-tree.notify") + +local Class = require("nvim-tree.classic") + +---@class (exact) GitRunner: Class +---@field private toplevel string absolute path +---@field private path string? absolute path +---@field private list_untracked boolean +---@field private list_ignored boolean +---@field private timeout integer +---@field private callback fun(path_xy: GitPathXY)? +---@field private path_xy GitPathXY +---@field private rc integer? -- -1 indicates timeout +local GitRunner = Class:extend() + +---@class GitRunner +---@overload fun(args: GitRunnerArgs): GitRunner + +---@class (exact) GitRunnerArgs +---@field toplevel string absolute path +---@field path string? absolute path +---@field list_untracked boolean +---@field list_ignored boolean +---@field timeout integer +---@field callback fun(path_xy: GitPathXY)? + +local timeouts = 0 +local MAX_TIMEOUTS = 5 + +---@protected +---@param args GitRunnerArgs +function GitRunner:new(args) + self.toplevel = args.toplevel + self.path = args.path + self.list_untracked = args.list_untracked + self.list_ignored = args.list_ignored + self.timeout = args.timeout + self.callback = args.callback + + self.path_xy = {} + self.rc = nil +end + +---@private +---@param status string +---@param path string|nil +function GitRunner:parse_status_output(status, path) + if not path then + return + end + + -- replacing slashes if on windows + if vim.fn.has("win32") == 1 then + path = path:gsub("/", "\\") + end + if #status > 0 and #path > 0 then + self.path_xy[utils.path_remove_trailing(utils.path_join({ self.toplevel, path }))] = status + end +end + +---@private +---@param prev_output string +---@param incoming string +---@return string +function GitRunner:handle_incoming_data(prev_output, incoming) + if incoming and utils.str_find(incoming, "\n") then + local prev = prev_output .. incoming + local i = 1 + local skip_next_line = false + for line in prev:gmatch("[^\n]*\n") do + if skip_next_line then + skip_next_line = false + else + local status = line:sub(1, 2) + local path = line:sub(4, -2) + if utils.str_find(status, "R") then + -- skip next line if it is a rename entry + skip_next_line = true + end + self:parse_status_output(status, path) + end + i = i + #line + end + + return prev:sub(i, -1) + end + + if incoming then + return prev_output .. incoming + end + + for line in prev_output:gmatch("[^\n]*\n") do + self:parse_status_output(line) + end + + return "" +end + +---@private +---@param stdout_handle uv.uv_pipe_t +---@param stderr_handle uv.uv_pipe_t +---@return uv.spawn.options +function GitRunner:get_spawn_options(stdout_handle, stderr_handle) + local untracked = self.list_untracked and "-u" or nil + local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no" + return { + args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path }, + cwd = self.toplevel, + stdio = { nil, stdout_handle, stderr_handle }, + } +end + +---@private +---@param output string +function GitRunner:log_raw_output(output) + if log.enabled("git") and output and type(output) == "string" then + log.raw("git", "%s", output) + log.line("git", "done") + end +end + +---@private +---@param callback function|nil +function GitRunner:run_git_job(callback) + local handle, pid + local stdout = vim.loop.new_pipe(false) + local stderr = vim.loop.new_pipe(false) + local timer = vim.loop.new_timer() + + if stdout == nil or stderr == nil or timer == nil then + return + end + + local function on_finish(rc) + self.rc = rc or 0 + if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then + if callback then + callback() + end + return + end + timer:stop() + timer:close() + stdout:read_stop() + stderr:read_stop() + stdout:close() + stderr:close() + + -- don't close the handle when killing as it will leave a zombie + if rc == -1 then + pcall(vim.loop.kill, pid, "sigkill") + elseif handle then + handle:close() + end + + if callback then + callback() + end + end + + local spawn_options = self:get_spawn_options(stdout, stderr) + log.line("git", "running job with timeout %dms", self.timeout) + log.line("git", "git %s", table.concat(utils.array_remove_nils(spawn_options.args), " ")) + + handle, pid = vim.loop.spawn( + "git", + spawn_options, + vim.schedule_wrap(function(rc) + on_finish(rc) + end) + ) + + timer:start( + self.timeout, + 0, + vim.schedule_wrap(function() + on_finish(-1) + end) + ) + + local output_leftover = "" + local function manage_stdout(err, data) + if err then + return + end + if data then + data = data:gsub("%z", "\n") + end + self:log_raw_output(data) + output_leftover = self:handle_incoming_data(output_leftover, data) + end + + local function manage_stderr(_, data) + self:log_raw_output(data) + end + + vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout)) + vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr)) +end + +---@private +function GitRunner:wait() + local function is_done() + return self.rc ~= nil + end + + while not vim.wait(30, is_done) do + end +end + +---@private +function GitRunner:finalise() + if self.rc == -1 then + log.line("git", "job timed out %s %s", self.toplevel, self.path) + timeouts = timeouts + 1 + if timeouts == MAX_TIMEOUTS then + notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, + self.timeout)) + require("nvim-tree.git").disable_git_integration() + end + elseif self.rc ~= 0 then + log.line("git", "job fail rc %d %s %s", self.rc, self.toplevel, self.path) + else + log.line("git", "job success %s %s", self.toplevel, self.path) + end +end + +---Return nil when callback present +---@private +---@return GitPathXY? +function GitRunner:execute() + local async = self.callback ~= nil + local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", self.toplevel, self.path) + + if async and self.callback then + -- async, always call back + self:run_git_job(function() + log.profile_end(profile) + + self:finalise() + + self.callback(self.path_xy) + end) + else + -- sync, maybe call back + self:run_git_job() + self:wait() + + log.profile_end(profile) + + self:finalise() + + if self.callback then + self.callback(self.path_xy) + else + return self.path_xy + end + end +end + +---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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/utils.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/utils.lua new file mode 100644 index 0000000..b805ebf --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/git/utils.lua @@ -0,0 +1,191 @@ +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") + +local M = { + use_cygpath = false, +} + +--- Retrieve the git toplevel directory +---@param cwd string path +---@return string|nil toplevel absolute path +---@return string|nil git_dir absolute path +function M.get_toplevel(cwd) + local profile = log.profile_start("git toplevel git_dir %s", cwd) + + -- both paths are absolute + local cmd = { "git", "-C", cwd, "rev-parse", "--show-toplevel", "--absolute-git-dir" } + log.line("git", "%s", table.concat(cmd, " ")) + + local out = vim.fn.system(cmd) + + log.raw("git", out) + log.profile_end(profile) + + if vim.v.shell_error ~= 0 or not out or #out == 0 or out:match("fatal") then + return nil, nil + end + + local toplevel, git_dir = out:match("([^\n]+)\n+([^\n]+)") + if not toplevel then + return nil, nil + end + if not git_dir then + git_dir = utils.path_join({ toplevel, ".git" }) + end + + -- git always returns path with forward slashes + if vim.fn.has("win32") == 1 then + -- msys2 git support + -- cygpath calls must in array format to avoid shell compatibility issues + if M.use_cygpath then + toplevel = vim.fn.system({ "cygpath", "-w", toplevel }) + if vim.v.shell_error ~= 0 then + return nil, nil + end + -- remove trailing newline(\n) character added by vim.fn.system + toplevel = toplevel:gsub("\n", "") + git_dir = vim.fn.system({ "cygpath", "-w", git_dir }) + if vim.v.shell_error ~= 0 then + return nil, nil + end + -- remove trailing newline(\n) character added by vim.fn.system + git_dir = git_dir:gsub("\n", "") + end + toplevel = toplevel:gsub("/", "\\") + git_dir = git_dir:gsub("/", "\\") + end + + return toplevel, git_dir +end + +---@type table +local untracked = {} + +---@param cwd string +---@return boolean +function M.should_show_untracked(cwd) + if untracked[cwd] ~= nil then + return untracked[cwd] + end + + local profile = log.profile_start("git untracked %s", cwd) + + local cmd = { "git", "-C", cwd, "config", "status.showUntrackedFiles" } + log.line("git", table.concat(cmd, " ")) + + local has_untracked = vim.fn.system(cmd) + + log.raw("git", has_untracked) + log.profile_end(profile) + + untracked[cwd] = vim.trim(has_untracked) ~= "no" + return untracked[cwd] +end + +---@param t table? +---@param k string|integer +---@return table +local function nil_insert(t, k) + t = t or {} + t[k] = true + return t +end + +---@param project_files GitProjectFiles +---@param cwd string|nil +---@return GitProjectDirs +function M.project_files_to_project_dirs(project_files, cwd) + ---@type GitProjectDirs + local project_dirs = {} + + project_dirs.direct = {} + for p, s in pairs(project_files) do + if s ~= "!!" then + local modified = vim.fn.fnamemodify(p, ":h") + project_dirs.direct[modified] = nil_insert(project_dirs.direct[modified], s) + end + end + + project_dirs.indirect = {} + for dirname, statuses in pairs(project_dirs.direct) do + for s, _ in pairs(statuses) do + local modified = dirname + while modified ~= cwd and modified ~= "/" do + modified = vim.fn.fnamemodify(modified, ":h") + project_dirs.indirect[modified] = nil_insert(project_dirs.indirect[modified], s) + end + end + end + + for _, d in pairs(project_dirs) do + for dirname, statuses in pairs(d) do + local new_statuses = {} + for s, _ in pairs(statuses) do + table.insert(new_statuses, s) + end + d[dirname] = new_statuses + end + end + + return project_dirs +end + +---Git file status for an absolute path +---@param parent_ignored boolean +---@param project GitProject? +---@param path string +---@param path_fallback string? alternative file path when no other file status +---@return GitNodeStatus +function M.git_status_file(parent_ignored, project, path, path_fallback) + ---@type GitNodeStatus + local ns + + if parent_ignored then + ns = { + file = "!!" + } + elseif project and project.files then + ns = { + file = project.files[path] or project.files[path_fallback] + } + else + ns = {} + end + + return ns +end + +---Git file and directory status for an absolute path +---@param parent_ignored boolean +---@param project GitProject? +---@param path string +---@param path_fallback string? alternative file path when no other file status +---@return GitNodeStatus? +function M.git_status_dir(parent_ignored, project, path, path_fallback) + ---@type GitNodeStatus? + local ns + + if parent_ignored then + ns = { + file = "!!" + } + elseif project then + ns = { + file = project.files and (project.files[path] or project.files[path_fallback]), + dir = project.dirs and { + direct = project.dirs.direct and project.dirs.direct[path], + indirect = project.dirs.indirect and project.dirs.indirect[path], + }, + } + end + + return ns +end + +function M.setup(opts) + if opts.git.cygwin_support then + M.use_cygpath = vim.fn.executable("cygpath") == 1 + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/help.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/help.lua new file mode 100644 index 0000000..e5d6e8a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/help.lua @@ -0,0 +1,262 @@ +local keymap = require("nvim-tree.keymap") +local api = {} -- circular dependency + +local PAT_MOUSE = "^<.*Mouse" +local PAT_CTRL = "^" e.g. "CTRL-v>" + lhs = lhs:gsub("^", "<") + + -- shorten ctrls + if lhs:lower():match("^") + + return lhs +end + +--- Remove prefix 'nvim-tree: ' +--- Hardcoded to keep default_on_attach simple +---@param desc string +---@return string +local function tidy_desc(desc) + return desc and desc:gsub("^nvim%-tree: ", "") or "" +end + +--- sort vim command lhs roughly as per :help index +---@param a string +---@param b string +---@return boolean +local function sort_lhs(a, b) + -- mouse first + if a:match(PAT_MOUSE) and not b:match(PAT_MOUSE) then + return true + elseif not a:match(PAT_MOUSE) and b:match(PAT_MOUSE) then + return false + end + + -- ctrl next + if a:match(PAT_CTRL) and not b:match(PAT_CTRL) then + return true + elseif not a:match(PAT_CTRL) and b:match(PAT_CTRL) then + return false + end + + -- special next + if a:match(PAT_SPECIAL) and not b:match(PAT_SPECIAL) then + return true + elseif not a:match(PAT_SPECIAL) and b:match(PAT_SPECIAL) then + return false + end + + -- remainder alpha + return a:gsub("[^a-zA-Z]", "") < b:gsub("[^a-zA-Z]", "") +end + +--- Compute all lines for the buffer +---@param map table keymap.get_keymap +---@return table strings of text +---@return table arrays of arguments 3-6 for nvim_buf_add_highlight() +---@return number maximum length of text +local function compute(map) + local head_lhs = "nvim-tree mappings" + local head_rhs1 = "exit: q" + local head_rhs2 = string.format("sort by %s: s", M.config.sort_by == "key" and "description" or "keymap") + + -- formatted lhs and desc from active keymap + local mappings = vim.tbl_map(function(m) + return { lhs = tidy_lhs(m.lhs), desc = tidy_desc(m.desc) } + end, map) + + -- sorter function for mappings + local sort_fn + + if M.config.sort_by == "desc" then + sort_fn = function(a, b) + return a.desc:lower() < b.desc:lower() + end + else + -- by default sort roughly by lhs + sort_fn = function(a, b) + return sort_lhs(a.lhs, b.lhs) + end + end + + table.sort(mappings, sort_fn) + + -- longest lhs and description + local max_lhs = 0 + local max_desc = 0 + for _, l in pairs(mappings) do + max_lhs = math.max(#l.lhs, max_lhs) + max_desc = math.max(#l.desc, max_desc) + end + + -- increase desc if lines are shorter than the header + max_desc = math.max(max_desc, #head_lhs + #head_rhs1 - max_lhs) + + -- header text, not padded + local lines = { + head_lhs .. string.rep(" ", max_desc + max_lhs - #head_lhs - #head_rhs1 + 2) .. head_rhs1, + string.rep(" ", max_desc + max_lhs - #head_rhs2 + 2) .. head_rhs2, + } + local width = #lines[1] + + -- header highlight, assume one character keys + local hl = { + { "NvimTreeFolderName", 0, 0, #head_lhs }, + { "NvimTreeFolderName", 0, width - 1, width }, + { "NvimTreeFolderName", 1, width - 1, width }, + } + + -- mappings, left padded 1 + local fmt = string.format(" %%-%ds %%-%ds", max_lhs, max_desc) + for i, l in ipairs(mappings) do + -- format in left aligned columns + local line = string.format(fmt, l.lhs, l.desc) + table.insert(lines, line) + width = math.max(#line, width) + + -- highlight lhs + table.insert(hl, { "NvimTreeFolderName", i + 1, 1, #l.lhs + 1 }) + end + + return lines, hl, width +end + +--- close the window and delete the buffer, if they exist +local function close() + if M.winnr then + vim.api.nvim_win_close(M.winnr, true) + M.winnr = nil + end + if M.bufnr then + vim.api.nvim_buf_delete(M.bufnr, { force = true }) + M.bufnr = nil + end +end + +--- open a new window and buffer +local function open() + -- close existing, shouldn't be necessary + close() + + -- fetch all mappings + local map = keymap.get_keymap() + + -- text and highlight + local lines, hl, width = compute(map) + + -- create the buffer + M.bufnr = vim.api.nvim_create_buf(false, true) + + -- populate it + vim.api.nvim_buf_set_lines(M.bufnr, 0, -1, false, lines) + + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("modifiable", false, { buf = M.bufnr }) + else + vim.api.nvim_buf_set_option(M.bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated + end + + -- highlight it + for _, h in ipairs(hl) do + vim.api.nvim_buf_add_highlight(M.bufnr, -1, h[1], h[2], h[3], h[4]) + end + + -- open a very restricted window + M.winnr = vim.api.nvim_open_win(M.bufnr, true, { + relative = "editor", + border = "single", + width = width, + height = #lines, + row = 1, + col = 0, + style = "minimal", + noautocmd = true, + }) + + -- style it a bit like the tree + vim.wo[M.winnr].winhl = WIN_HL + vim.wo[M.winnr].cursorline = M.config.cursorline + + local function toggle_sort() + M.config.sort_by = (M.config.sort_by == "desc") and "key" or "desc" + open() + end + + -- hardcoded + local help_keymaps = { + q = { fn = close, desc = "nvim-tree: exit help" }, + [""] = { fn = close, desc = "nvim-tree: exit help" }, -- hidden + s = { fn = toggle_sort, desc = "nvim-tree: toggle sorting method" }, + } + + -- api help binding closes + for _, m in ipairs(map) do + if m.callback == api.tree.toggle_help then + help_keymaps[m.lhs] = { fn = close, desc = "nvim-tree: exit help" } + end + end + + for k, v in pairs(help_keymaps) do + vim.keymap.set("n", k, v.fn, { + desc = v.desc, + buffer = M.bufnr, + noremap = true, + silent = true, + nowait = true, + }) + end + + -- close window and delete buffer on leave + vim.api.nvim_create_autocmd({ "BufLeave", "WinLeave" }, { + buffer = M.bufnr, + once = true, + callback = close, + }) +end + +function M.toggle() + if M.winnr or M.bufnr then + close() + else + open() + end +end + +function M.setup(opts) + M.config.cursorline = opts.view.cursorline + M.config.sort_by = opts.help.sort_by + + api = require("nvim-tree.api") +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/iterators/node-iterator.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/iterators/node-iterator.lua new file mode 100644 index 0000000..8cd4abd --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/iterators/node-iterator.lua @@ -0,0 +1,79 @@ +---@class NodeIterator +local NodeIterator = {} +NodeIterator.__index = NodeIterator + +---@param nodes Node[] +---@return NodeIterator +function NodeIterator.builder(nodes) + return setmetatable({ + nodes = nodes, + _filter_hidden = function(node) + return not node.hidden + end, + _apply_fn_on_node = function(_) end, + _match = function(_) end, + _recurse_with = function(node) + return node.nodes + end, + }, NodeIterator) +end + +---@return NodeIterator +function NodeIterator:hidden() + self._filter_hidden = function(_) + return true + end + return self +end + +---@param f fun(node: Node): boolean +---@return NodeIterator +function NodeIterator:matcher(f) + self._match = f + return self +end + +---@param f fun(node: Node, i: number) +---@return NodeIterator +function NodeIterator:applier(f) + self._apply_fn_on_node = f + return self +end + +---@param f fun(node: Node): any +---@return NodeIterator +function NodeIterator:recursor(f) + self._recurse_with = f + return self +end + +---@return Node|nil +---@return number|nil +function NodeIterator:iterate() + local iteration_count = 0 + local function iter(nodes) + for _, node in ipairs(nodes) do + if self._filter_hidden(node) then + if not node.group_next then + iteration_count = iteration_count + 1 + end + if self._match(node) then + return node, iteration_count + end + self._apply_fn_on_node(node, iteration_count) + local children = self._recurse_with(node) + if children then + local n = iter(children) + if n then + return n, iteration_count + end + end + end + end + return nil, 0 + end + + return iter(self.nodes) +end + +return NodeIterator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/keymap.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/keymap.lua new file mode 100644 index 0000000..94029de --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/keymap.lua @@ -0,0 +1,116 @@ +local M = {} + +--- Apply mappings to a scratch buffer and return buffer local mappings +---@param fn fun(bufnr: integer) on_attach or default_on_attach +---@return table as per vim.api.nvim_buf_get_keymap +local function generate_keymap(fn) + -- create an unlisted scratch buffer + local scratch_bufnr = vim.api.nvim_create_buf(false, true) + + -- apply mappings + fn(scratch_bufnr) + + -- retrieve all + local keymap = vim.api.nvim_buf_get_keymap(scratch_bufnr, "") + + -- delete the scratch buffer + vim.api.nvim_buf_delete(scratch_bufnr, { force = true }) + + return keymap +end + +---@return table +function M.get_keymap() + return generate_keymap(M.on_attach) +end + +---@return table +function M.get_keymap_default() + return generate_keymap(M.default_on_attach) +end + +---@param bufnr integer +function M.default_on_attach(bufnr) + local api = require("nvim-tree.api") + + local function opts(desc) + return { + desc = "nvim-tree: " .. desc, + buffer = bufnr, + noremap = true, + silent = true, + nowait = true, + } + end + + -- BEGIN_DEFAULT_ON_ATTACH + vim.keymap.set("n", "", api.tree.change_root_to_node, opts("CD")) + vim.keymap.set("n", "", api.node.open.replace_tree_buffer, opts("Open: In Place")) + vim.keymap.set("n", "", api.node.show_info_popup, opts("Info")) + vim.keymap.set("n", "", api.fs.rename_sub, opts("Rename: Omit Filename")) + vim.keymap.set("n", "", api.node.open.tab, opts("Open: New Tab")) + vim.keymap.set("n", "", api.node.open.vertical, opts("Open: Vertical Split")) + vim.keymap.set("n", "", api.node.open.horizontal, opts("Open: Horizontal Split")) + vim.keymap.set("n", "", api.node.navigate.parent_close, opts("Close Directory")) + vim.keymap.set("n", "", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "", api.node.open.preview, opts("Open Preview")) + vim.keymap.set("n", ">", api.node.navigate.sibling.next, opts("Next Sibling")) + vim.keymap.set("n", "<", api.node.navigate.sibling.prev, opts("Previous Sibling")) + vim.keymap.set("n", ".", api.node.run.cmd, opts("Run Command")) + vim.keymap.set("n", "-", api.tree.change_root_to_parent, opts("Up")) + vim.keymap.set("n", "a", api.fs.create, opts("Create File Or Directory")) + vim.keymap.set("n", "bd", api.marks.bulk.delete, opts("Delete Bookmarked")) + vim.keymap.set("n", "bt", api.marks.bulk.trash, opts("Trash Bookmarked")) + vim.keymap.set("n", "bmv", api.marks.bulk.move, opts("Move Bookmarked")) + vim.keymap.set("n", "B", api.tree.toggle_no_buffer_filter, opts("Toggle Filter: No Buffer")) + vim.keymap.set("n", "c", api.fs.copy.node, opts("Copy")) + vim.keymap.set("n", "C", api.tree.toggle_git_clean_filter, opts("Toggle Filter: Git Clean")) + vim.keymap.set("n", "[c", api.node.navigate.git.prev, opts("Prev Git")) + vim.keymap.set("n", "]c", api.node.navigate.git.next, opts("Next Git")) + vim.keymap.set("n", "d", api.fs.remove, opts("Delete")) + vim.keymap.set("n", "D", api.fs.trash, opts("Trash")) + vim.keymap.set("n", "E", api.tree.expand_all, opts("Expand All")) + vim.keymap.set("n", "e", api.fs.rename_basename, opts("Rename: Basename")) + vim.keymap.set("n", "]e", api.node.navigate.diagnostics.next, opts("Next Diagnostic")) + vim.keymap.set("n", "[e", api.node.navigate.diagnostics.prev, opts("Prev Diagnostic")) + vim.keymap.set("n", "F", api.live_filter.clear, opts("Live Filter: Clear")) + vim.keymap.set("n", "f", api.live_filter.start, opts("Live Filter: Start")) + vim.keymap.set("n", "g?", api.tree.toggle_help, opts("Help")) + vim.keymap.set("n", "gy", api.fs.copy.absolute_path, opts("Copy Absolute Path")) + vim.keymap.set("n", "ge", api.fs.copy.basename, opts("Copy Basename")) + vim.keymap.set("n", "H", api.tree.toggle_hidden_filter, opts("Toggle Filter: Dotfiles")) + vim.keymap.set("n", "I", api.tree.toggle_gitignore_filter, opts("Toggle Filter: Git Ignore")) + vim.keymap.set("n", "J", api.node.navigate.sibling.last, opts("Last Sibling")) + vim.keymap.set("n", "K", api.node.navigate.sibling.first, opts("First Sibling")) + vim.keymap.set("n", "L", api.node.open.toggle_group_empty, opts("Toggle Group Empty")) + vim.keymap.set("n", "M", api.tree.toggle_no_bookmark_filter, opts("Toggle Filter: No Bookmark")) + vim.keymap.set("n", "m", api.marks.toggle, opts("Toggle Bookmark")) + vim.keymap.set("n", "o", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "O", api.node.open.no_window_picker, opts("Open: No Window Picker")) + vim.keymap.set("n", "p", api.fs.paste, opts("Paste")) + vim.keymap.set("n", "P", api.node.navigate.parent, opts("Parent Directory")) + vim.keymap.set("n", "q", api.tree.close, opts("Close")) + vim.keymap.set("n", "r", api.fs.rename, opts("Rename")) + vim.keymap.set("n", "R", api.tree.reload, opts("Refresh")) + vim.keymap.set("n", "s", api.node.run.system, opts("Run System")) + vim.keymap.set("n", "S", api.tree.search_node, opts("Search")) + vim.keymap.set("n", "u", api.fs.rename_full, opts("Rename: Full Path")) + vim.keymap.set("n", "U", api.tree.toggle_custom_filter, opts("Toggle Filter: Hidden")) + vim.keymap.set("n", "W", api.tree.collapse_all, opts("Collapse")) + vim.keymap.set("n", "x", api.fs.cut, opts("Cut")) + vim.keymap.set("n", "y", api.fs.copy.filename, opts("Copy Name")) + vim.keymap.set("n", "Y", api.fs.copy.relative_path, opts("Copy Relative Path")) + vim.keymap.set("n", "<2-LeftMouse>", api.node.open.edit, opts("Open")) + vim.keymap.set("n", "<2-RightMouse>", api.tree.change_root_to_node, opts("CD")) + -- END_DEFAULT_ON_ATTACH +end + +function M.setup(opts) + if type(opts.on_attach) ~= "function" then + M.on_attach = M.default_on_attach + else + M.on_attach = opts.on_attach + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/legacy.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/legacy.lua new file mode 100644 index 0000000..7ba4f98 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/legacy.lua @@ -0,0 +1,100 @@ +local utils = require("nvim-tree.utils") +local notify = require("nvim-tree.notify") + +local M = {} + +-- silently move, please add to help nvim-tree-legacy-opts +local function refactored(opts) + -- 2022/06/20 + utils.move_missing_val(opts, "update_focused_file", "update_cwd", opts, "update_focused_file", "update_root", true) + utils.move_missing_val(opts, "", "update_cwd", opts, "", "sync_root_with_cwd", true) + + -- 2022/11/07 + utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "open", false) + utils.move_missing_val(opts, "", "open_on_tab", opts, "tab.sync", "close", true) + utils.move_missing_val(opts, "", "ignore_buf_on_tab_change", opts, "tab.sync", "ignore", true) + + -- 2022/11/22 + utils.move_missing_val(opts, "renderer", "root_folder_modifier", opts, "renderer", "root_folder_label", true) + + -- 2023/01/01 + utils.move_missing_val(opts, "update_focused_file", "debounce_delay", opts, "view", "debounce_delay", true) + + -- 2023/01/08 + utils.move_missing_val(opts, "trash", "require_confirm", opts, "ui.confirm", "trash", true) + + -- 2023/01/15 + if type(opts.view) == "table" and opts.view.adaptive_size ~= nil then + if opts.view.adaptive_size and type(opts.view.width) ~= "table" then + local width = opts.view.width + opts.view.width = { + min = width, + } + end + opts.view.adaptive_size = nil + end + + -- 2023/07/15 + utils.move_missing_val(opts, "", "sort_by", opts, "sort", "sorter", true) + + -- 2023/07/16 + utils.move_missing_val(opts, "git", "ignore", opts, "filters", "git_ignored", true) + + -- 2023/08/26 + utils.move_missing_val(opts, "renderer.icons", "webdev_colors", opts, "renderer.icons.web_devicons.file", "color", true) + + -- 2023/10/08 + if type(opts.renderer) == "table" and type(opts.renderer.highlight_diagnostics) == "boolean" then + opts.renderer.highlight_diagnostics = opts.renderer.highlight_diagnostics and "name" or "none" + end + + -- 2023/10/21 + if type(opts.renderer) == "table" and type(opts.renderer.highlight_git) == "boolean" then + opts.renderer.highlight_git = opts.renderer.highlight_git and "name" or "none" + end + + -- 2024/02/15 + if type(opts.update_focused_file) == "table" then + if type(opts.update_focused_file.update_root) ~= "table" then + opts.update_focused_file.update_root = { enable = opts.update_focused_file.update_root } + end + end + utils.move_missing_val(opts, "update_focused_file", "ignore_list", opts, "update_focused_file.update_root", "ignore_list", true) +end + +local function deprecated(opts) + if type(opts.view) == "table" and opts.view.hide_root_folder then + notify.info("view.hide_root_folder is deprecated, please set renderer.root_folder_label = false") + end +end + +local function removed(opts) + if opts.auto_close then + notify.warn("auto close feature has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Auto-Close") + opts.auto_close = nil + end + + if opts.focus_empty_on_setup then + notify.warn("focus_empty_on_setup has been removed: https://github.com/nvim-tree/nvim-tree.lua/wiki/Open-At-Startup") + opts.focus_empty_on_setup = nil + end + + if opts.create_in_closed_folder then + notify.warn( + "create_in_closed_folder has been removed and is now the default behaviour. You may use api.fs.create to add a file under your desired node.") + end + opts.create_in_closed_folder = nil +end + +function M.migrate_legacy_options(opts) + -- silently move + refactored(opts) + + -- warn + deprecated(opts) + + -- warn and delete + removed(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/lib.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/lib.lua new file mode 100644 index 0000000..abd9d01 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/lib.lua @@ -0,0 +1,144 @@ +local view = require("nvim-tree.view") +local core = require("nvim-tree.core") +local events = require("nvim-tree.events") +local notify = require("nvim-tree.notify") + +---@class LibOpenOpts +---@field path string|nil path +---@field current_window boolean|nil default false +---@field winid number|nil + +local M = { + target_winid = nil, +} + +function M.set_target_win() + local id = vim.api.nvim_get_current_win() + local tree_id = view.get_winnr() + if tree_id and id == tree_id then + M.target_winid = 0 + return + end + + M.target_winid = id +end + +---@param cwd string +local function handle_buf_cwd(cwd) + if M.respect_buf_cwd and cwd ~= core.get_cwd() then + require("nvim-tree.actions.root.change-dir").fn(cwd) + end +end + +local function open_view_and_draw() + local cwd = vim.fn.getcwd() + view.open() + handle_buf_cwd(cwd) + + local explorer = core.get_explorer() + if explorer then + explorer.renderer:draw() + end +end + +local function should_hijack_current_buf() + local bufnr = vim.api.nvim_get_current_buf() + local bufname = vim.api.nvim_buf_get_name(bufnr) + + local bufmodified, ft + if vim.fn.has("nvim-0.10") == 1 then + bufmodified = vim.api.nvim_get_option_value("modified", { buf = bufnr }) + ft = vim.api.nvim_get_option_value("ft", { buf = bufnr }) + else + bufmodified = vim.api.nvim_buf_get_option(bufnr, "modified") ---@diagnostic disable-line: deprecated + ft = vim.api.nvim_buf_get_option(bufnr, "ft") ---@diagnostic disable-line: deprecated + end + + local should_hijack_unnamed = M.hijack_unnamed_buffer_when_opening and bufname == "" and not bufmodified and ft == "" + local should_hijack_dir = bufname ~= "" and vim.fn.isdirectory(bufname) == 1 and M.hijack_directories.enable + + return should_hijack_dir or should_hijack_unnamed +end + +---@param prompt_input string +---@param prompt_select string +---@param items_short string[] +---@param items_long string[] +---@param kind string|nil +---@param callback fun(item_short: string|nil) +function M.prompt(prompt_input, prompt_select, items_short, items_long, kind, callback) + local function format_item(short) + for i, s in ipairs(items_short) do + if short == s then + return items_long[i] + end + end + return "" + end + + if M.select_prompts then + vim.ui.select(items_short, { prompt = prompt_select, kind = kind, format_item = format_item }, function(item_short) + callback(item_short) + end) + else + vim.ui.input({ prompt = prompt_input, default = items_short[1] or "" }, function(item_short) + if item_short then + callback(string.lower(item_short and item_short:sub(1, 1)) or nil) + end + end) + end +end + +---Open the tree, initialising as needed. Maybe hijack the current buffer. +---@param opts LibOpenOpts|nil +function M.open(opts) + opts = opts or {} + + M.set_target_win() + if not core.get_explorer() or opts.path then + if opts.path then + core.init(opts.path) + else + local cwd, err = vim.loop.cwd() + if not cwd then + notify.error(string.format("current working directory unavailable: %s", err)) + return + end + core.init(cwd) + end + end + + local explorer = core.get_explorer() + + if should_hijack_current_buf() then + view.close_this_tab_only() + view.open_in_win() + if explorer then + explorer.renderer:draw() + end + elseif opts.winid then + view.open_in_win({ hijack_current_buf = false, resize = false, winid = opts.winid }) + if explorer then + explorer.renderer:draw() + end + elseif opts.current_window then + view.open_in_win({ hijack_current_buf = false, resize = false }) + if explorer then + explorer.renderer:draw() + end + else + open_view_and_draw() + end + view.restore_tab_state() + events._dispatch_on_tree_open() +end + +function M.setup(opts) + M.hijack_unnamed_buffer_when_opening = opts.hijack_unnamed_buffer_when_opening + M.hijack_directories = opts.hijack_directories + M.respect_buf_cwd = opts.respect_buf_cwd + M.select_prompts = opts.select_prompts + M.group_empty = opts.renderer.group_empty +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/log.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/log.lua new file mode 100644 index 0000000..9665c13 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/log.lua @@ -0,0 +1,124 @@ +---@alias LogTypes "all" | "config" | "copy_paste" | "dev" | "diagnostics" | "git" | "profile" | "watcher" + +---@type table +local types = {} + +---@type string +local file_path + +local M = {} + +--- Write to log file +---@param typ string as per log.types config +---@param fmt string for string.format +---@param ... any arguments for string.format +function M.raw(typ, fmt, ...) + if not M.enabled(typ) then + return + end + + local line = string.format(fmt, ...) + local file = io.open(file_path, "a") + if file then + io.output(file) + io.write(line) + io.close(file) + end +end + +--- Write to a new file +---@param typ LogTypes as per log.types config +---@param path string absolute path +---@param fmt string for string.format +---@param ... any arguments for string.format +function M.file(typ, path, fmt, ...) + if not M.enabled(typ) then + return + end + + local line = string.format(fmt, ...) + local file = io.open(path, "w") + if file then + io.output(file) + io.write(line) + io.close(file) + end +end + +---@class Profile +---@field start number nanos +---@field tag string + +--- Write profile start to log file +--- START is prefixed +---@param fmt string for string.format +---@param ... any arguments for string.format +---@return Profile to pass to profile_end +function M.profile_start(fmt, ...) + local profile = {} + if M.enabled("profile") then + profile.start = vim.loop.hrtime() + profile.tag = string.format((fmt or "???"), ...) + M.line("profile", "START %s", profile.tag) + end + return profile +end + +--- Write profile end to log file +--- END is prefixed and duration in seconds is suffixed +---@param profile Profile returned from profile_start +function M.profile_end(profile) + if M.enabled("profile") and type(profile) == "table" then + local millis = profile.start and math.modf((vim.loop.hrtime() - profile.start) / 1000000) or -1 + M.line("profile", "END %s %dms", profile.tag or "", millis) + end +end + +--- Write to log file +--- time and typ are prefixed and a trailing newline is added +---@param typ LogTypes as per log.types config +---@param fmt string for string.format +---@param ... any arguments for string.format +function M.line(typ, fmt, ...) + if M.enabled(typ) then + M.raw(typ, string.format("[%s] [%s] %s\n", os.date("%Y-%m-%d %H:%M:%S"), typ, (fmt or "???")), ...) + end +end + +local inspect_opts = {} + +---@param opts table +function M.set_inspect_opts(opts) + inspect_opts = opts +end + +--- Write to log file the inspection of a node +---@param typ LogTypes as per log.types config +---@param node Node node to be inspected +---@param fmt string for string.format +---@param ... any arguments for string.format +function M.node(typ, node, fmt, ...) + if M.enabled(typ) then + M.raw(typ, string.format("[%s] [%s] %s\n%s\n", os.date("%Y-%m-%d %H:%M:%S"), typ, (fmt or "???"), vim.inspect(node, inspect_opts)), ...) + end +end + +--- Logging is enabled for typ or all +---@param typ LogTypes as per log.types config +---@return boolean +function M.enabled(typ) + return file_path ~= nil and (types[typ] or types.all) +end + +function M.setup(opts) + if opts.log and opts.log.enable and opts.log.types then + types = opts.log.types + file_path = string.format("%s/nvim-tree.log", vim.fn.stdpath("log"), os.date("%H:%M:%S"), vim.env.USER) + if opts.log.truncate then + os.remove(file_path) + end + require("nvim-tree.notify").debug("nvim-tree.lua logging to " .. file_path) + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/marks/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/marks/init.lua new file mode 100644 index 0000000..c940f99 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/marks/init.lua @@ -0,0 +1,271 @@ +local Iterator = require("nvim-tree.iterators.node-iterator") +local core = require("nvim-tree.core") +local lib = require("nvim-tree.lib") +local notify = require("nvim-tree.notify") +local open_file = require("nvim-tree.actions.node.open-file") +local remove_file = require("nvim-tree.actions.fs.remove-file") +local rename_file = require("nvim-tree.actions.fs.rename-file") +local trash = require("nvim-tree.actions.fs.trash") +local utils = require("nvim-tree.utils") + +local Class = require("nvim-tree.classic") +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) Marks: Class +---@field private explorer Explorer +---@field private marks table by absolute path +local Marks = Class:extend() + +---@class Marks +---@overload fun(args: MarksArgs): Marks + +---@class (exact) MarksArgs +---@field explorer Explorer + +---@protected +---@param args MarksArgs +function Marks:new(args) + self.explorer = args.explorer + + self.marks = {} +end + +---Clear all marks and reload if watchers disabled +---@private +function Marks:clear_reload() + self:clear() + if not self.explorer.opts.filesystem_watchers.enable then + self.explorer:reload_explorer() + end +end + +---Clear all marks and redraw +---@public +function Marks:clear() + self.marks = {} + self.explorer.renderer:draw() +end + +---@public +---@param node Node +function Marks:toggle(node) + if node.absolute_path == nil then + return + end + + if self:get(node) then + self.marks[node.absolute_path] = nil + else + self.marks[node.absolute_path] = node + end + + self.explorer.renderer:draw() +end + +---Return node if marked +---@public +---@param node Node +---@return Node|nil +function Marks:get(node) + return node and self.marks[node.absolute_path] +end + +---List marked nodes +---@public +---@return Node[] +function Marks:list() + local list = {} + for _, node in pairs(self.marks) do + table.insert(list, node) + end + return list +end + +---Delete marked; each removal will be optionally notified +---@public +function Marks:bulk_delete() + if not next(self.marks) then + notify.warn("No bookmarks to delete.") + return + end + + local function execute() + for _, node in pairs(self.marks) do + remove_file.remove(node) + end + self:clear_reload() + end + + if self.explorer.opts.ui.confirm.remove then + local prompt_select = "Remove bookmarked ?" + local prompt_input = prompt_select .. " y/N: " + lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_delete", function(item_short) + utils.clear_prompt() + if item_short == "y" then + execute() + end + end) + else + execute() + end +end + +---Trash marked; each removal will be optionally notified +---@public +function Marks:bulk_trash() + if not next(self.marks) then + notify.warn("No bookmarks to trash.") + return + end + + local function execute() + for _, node in pairs(self.marks) do + trash.remove(node) + end + self:clear_reload() + end + + if self.explorer.opts.ui.confirm.trash then + local prompt_select = "Trash bookmarked ?" + local prompt_input = prompt_select .. " y/N: " + lib.prompt(prompt_input, prompt_select, { "", "y" }, { "No", "Yes" }, "nvimtree_bulk_trash", function(item_short) + utils.clear_prompt() + if item_short == "y" then + execute() + end + end) + else + execute() + end +end + +---Move marked +---@public +function Marks:bulk_move() + if not next(self.marks) then + notify.warn("No bookmarks to move.") + return + end + + local node_at_cursor = self.explorer:get_node_at_cursor() + local default_path = core.get_cwd() + + if node_at_cursor and node_at_cursor:is(DirectoryNode) then + default_path = node_at_cursor.absolute_path + elseif node_at_cursor and node_at_cursor.parent then + default_path = node_at_cursor.parent.absolute_path + end + + local input_opts = { + prompt = "Move to: ", + default = default_path, + completion = "dir", + } + + vim.ui.input(input_opts, function(location) + utils.clear_prompt() + if not location or location == "" then + return + end + if vim.fn.filewritable(location) ~= 2 then + notify.warn(location .. " is not writable, cannot move.") + return + end + + for _, node in pairs(self.marks) do + local head = vim.fn.fnamemodify(node.absolute_path, ":t") + local to = utils.path_join({ location, head }) + rename_file.rename(node, to) + end + + self:clear_reload() + end) +end + +---Focus nearest marked node in direction. +---@private +---@param up boolean +function Marks:navigate(up) + local node = self.explorer:get_node_at_cursor() + if not node then + return + end + + local first, prev, next, last = nil, nil, nil, nil + local found = false + + Iterator.builder(self.explorer.nodes) + :recursor(function(n) + local dir = n:as(DirectoryNode) + return dir and dir.open and dir.nodes + end) + :applier(function(n) + if n.absolute_path == node.absolute_path then + found = true + return + end + + if not self:get(n) then + return + end + + last = n + first = first or n + + if found and not next then + next = n + end + + if not found then + prev = n + end + end) + :iterate() + + if not found then + return + end + + if up then + utils.focus_node_or_parent(prev or last) + else + utils.focus_node_or_parent(next or first) + end +end + +---@public +function Marks:navigate_prev() + self:navigate(true) +end + +---@public +function Marks:navigate_next() + self:navigate(false) +end + +---Prompts for selection of a marked node, sorted by absolute paths. +---A folder will be focused, a file will be opened. +---@public +function Marks:navigate_select() + local list = vim.tbl_map(function(n) + return n.absolute_path + end, self:list()) + + table.sort(list) + + vim.ui.select(list, { + prompt = "Select a file to open or a folder to focus", + }, function(choice) + if not choice or choice == "" then + return + end + local node = self.marks[choice] + if node and not node:is(DirectoryNode) and not utils.get_win_buf_from_path(node.absolute_path) then + open_file.fn("edit", node.absolute_path) + elseif node then + utils.focus_file(node.absolute_path) + end + end) +end + +return Marks diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory-link.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory-link.lua new file mode 100644 index 0000000..9666ca4 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory-link.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory.lua new file mode 100644 index 0000000..0965e4a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/directory.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/factory.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/factory.lua new file mode 100644 index 0000000..adaaa5a --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/factory.lua @@ -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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file-link.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file-link.lua new file mode 100644 index 0000000..b13c88f --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file-link.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file.lua new file mode 100644 index 0000000..6e16040 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/file.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/init.lua new file mode 100644 index 0000000..da0dcc5 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/init.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/link.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/link.lua new file mode 100644 index 0000000..c3bc164 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/link.lua @@ -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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/root.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/root.lua new file mode 100644 index 0000000..f991dec --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/node/root.lua @@ -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? 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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/notify.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/notify.lua new file mode 100644 index 0000000..7449960 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/notify.lua @@ -0,0 +1,68 @@ +local M = {} + +local config = { + threshold = vim.log.levels.INFO, + absolute_path = true, +} + +local title_support +---@return boolean +function M.supports_title() + if title_support == nil then + title_support = (package.loaded.notify and (vim.notify == require("notify") or vim.notify == require("notify").notify)) + or (package.loaded.noice and (vim.notify == require("noice").notify or vim.notify == require("noice.source.notify").notify)) + or (package.loaded.notifier and require("notifier.config").has_component("nvim")) + or false + end + + return title_support +end + +local modes = { + { name = "trace", level = vim.log.levels.TRACE }, + { name = "debug", level = vim.log.levels.DEBUG }, + { name = "info", level = vim.log.levels.INFO }, + { name = "warn", level = vim.log.levels.WARN }, + { name = "error", level = vim.log.levels.ERROR }, +} + +do + local dispatch = function(level, msg) + if level < config.threshold or not msg then + return + end + + vim.schedule(function() + if not M.supports_title() then + -- add title to the message, with a newline if the message is multiline + msg = string.format("[NvimTree]%s%s", (msg:match("\n") and "\n" or " "), msg) + end + + vim.notify(msg, level, { title = "NvimTree" }) + end) + end + + for _, x in ipairs(modes) do + M[x.name] = function(msg) + return dispatch(x.level, msg) + end + end +end + +---@param path string +---@return string +function M.render_path(path) + if config.absolute_path then + return path + else + return vim.fn.fnamemodify(path .. "/", ":h:t") + end +end + +function M.setup(opts) + opts = opts or {} + config.threshold = opts.notify.threshold or vim.log.levels.INFO + config.absolute_path = opts.notify.absolute_path +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/builder.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/builder.lua new file mode 100644 index 0000000..e08eb9e --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/builder.lua @@ -0,0 +1,464 @@ +local notify = require("nvim-tree.notify") +local utils = require("nvim-tree.utils") +local view = require("nvim-tree.view") + +local Class = require("nvim-tree.classic") + +local DirectoryNode = require("nvim-tree.node.directory") + +local BookmarkDecorator = require("nvim-tree.renderer.decorator.bookmarks") +local CopiedDecorator = require("nvim-tree.renderer.decorator.copied") +local CutDecorator = require("nvim-tree.renderer.decorator.cut") +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") + +---@alias HighlightedString nvim_tree.api.HighlightedString + +-- Builtin Decorators +---@type table +local BUILTIN_DECORATORS = { + Git = GitDecorator, + Open = OpenDecorator, + Hidden = HiddenDecorator, + Modified = ModifiedDecorator, + Bookmark = BookmarkDecorator, + Diagnostics = DiagnosticsDecorator, + Copied = CopiedDecorator, + Cut = CutDecorator, +} + +---@class (exact) AddHighlightArgs +---@field group string[] +---@field line number +---@field col_start number +---@field col_end number + +---@class (exact) Builder +---@field lines string[] includes icons etc. +---@field hl_args AddHighlightArgs[] line highlights +---@field signs string[] line signs +---@field extmarks table[] extra marks for right icon placement +---@field virtual_lines table[] virtual lines for hidden count display +---@field private explorer Explorer +---@field private index number +---@field private depth number +---@field private combined_groups table combined group names +---@field private markers boolean[] indent markers +---@field private decorators Decorator[] +---@field private hidden_display fun(node: Node): string|nil +---@field private api_nodes table? optional map of uids to api node for user decorators +local Builder = Class:extend() + +---@class Builder +---@overload fun(args: BuilderArgs): Builder + +---@class (exact) BuilderArgs +---@field explorer Explorer + +---@protected +---@param args BuilderArgs +function Builder:new(args) + self.explorer = args.explorer + self.index = 0 + self.depth = 0 + self.hl_args = {} + self.combined_groups = {} + self.lines = {} + self.markers = {} + self.signs = {} + self.extmarks = {} + self.virtual_lines = {} + self.decorators = {} + 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 + +---Insert ranged highlight groups into self.highlights +---@private +---@param groups string[] +---@param start number +---@param end_ number|nil +function Builder:insert_highlight(groups, start, end_) + table.insert(self.hl_args, { groups, self.index, start, end_ or -1 }) +end + +---@private +---@param highlighted_strings HighlightedString[] +---@return string +function Builder:unwrap_highlighted_strings(highlighted_strings) + if not highlighted_strings then + return "" + end + + local string = "" + for _, v in ipairs(highlighted_strings) do + if #v.str > 0 then + if v.hl and type(v.hl) == "table" then + self:insert_highlight(v.hl, #string, #string + #v.str) + end + string = string.format("%s%s", string, v.str) + end + end + return string +end + +---@private +---@param indent_markers HighlightedString[] +---@param arrows HighlightedString[]|nil +---@param icon HighlightedString +---@param name HighlightedString +---@param node table +---@return HighlightedString[] +function Builder:format_line(indent_markers, arrows, icon, name, node) + local added_len = 0 + local function add_to_end(t1, t2) + if not t2 then + return + end + for _, v in ipairs(t2) do + if added_len > 0 then + table.insert(t1, { str = self.explorer.opts.renderer.icons.padding }) + end + table.insert(t1, v) + end + + -- first add_to_end don't need padding + -- hence added_len is calculated at the end to be used next time + added_len = 0 + for _, v in ipairs(t2) do + added_len = added_len + #v.str + 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 } + add_to_end(line, { icon }) + + for _, d in ipairs(self.decorators) do + add_to_end(line, d:icons_before(not d:is(UserDecorator) and node or api_node)) + end + + add_to_end(line, { name }) + + for _, d in ipairs(self.decorators) do + add_to_end(line, d:icons_after(not d:is(UserDecorator) and node or api_node)) + end + + local rights = {} + for _, d in ipairs(self.decorators) do + add_to_end(rights, d:icons_right_align(not d:is(UserDecorator) and node or api_node)) + end + if #rights > 0 then + self.extmarks[self.index] = rights + end + + return line +end + +---@private +---@param node 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 + local d, sign_name + for i = #self.decorators, 1, -1 do + d = self.decorators[i] + sign_name = d:sign_name(not d:is(UserDecorator) and node or api_node) + if sign_name then + self.signs[self.index] = sign_name + break + end + end +end + +---Create a highlight group for groups with later groups overriding previous. +---Combined group name is less than the 200 byte limit of highlight group names +---@private +---@param groups string[] highlight group names +---@return string group_name "NvimTreeCombinedHL" .. sha256 +function Builder:create_combined_group(groups) + local combined_name = string.format("NvimTreeCombinedHL%s", vim.fn.sha256(table.concat(groups))) + + -- only create if necessary + if not self.combined_groups[combined_name] then + self.combined_groups[combined_name] = true + local combined_hl = {} + + -- build the highlight, overriding values + for _, group in ipairs(groups) do + local hl = vim.api.nvim_get_hl(0, { name = group, link = false }) + combined_hl = vim.tbl_extend("force", combined_hl, hl) + end + + -- add highlights to the global namespace + vim.api.nvim_set_hl(0, combined_name, combined_hl) + + table.insert(self.combined_groups, combined_name) + end + + return combined_name +end + +---Calculate decorated icon and name for a node. +---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. +---@private +---@param node Node +---@return HighlightedString icon +---@return HighlightedString name +function Builder:icon_name_decorated(node) + -- use the api node for user decorators + local api_node = self.api_nodes and self.api_nodes[node.uid_node] --[[@as Node]] + + -- base case + local icon = node:highlighted_icon() + local name = node:highlighted_name() + + -- calculate node icon and all decorated highlight groups + local icon_groups = {} + local name_groups = {} + local hl_icon, hl_name + for _, d in ipairs(self.decorators) do + -- maybe overridde icon + icon = d:icon_node((not d:is(UserDecorator) and node or api_node)) or icon + + 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 + + -- add one or many icon groups + if #icon_groups > 1 then + table.insert(icon.hl, self:create_combined_group(icon_groups)) + else + table.insert(icon.hl, icon_groups[1]) + end + + -- add one or many name groups + if #name_groups > 1 then + table.insert(name.hl, self:create_combined_group(name_groups)) + else + table.insert(name.hl, name_groups[1]) + end + + return icon, name +end + +---Insert node line into self.lines, calling Builder:build_lines for each directory +---@private +---@param node Node +---@param idx integer line number starting at 1 +---@param num_children integer of node +function Builder:build_line(node, idx, num_children) + -- various components + local indent_markers = pad.get_indent_markers(self.depth, idx, num_children, node, self.markers) + local arrows = pad.get_arrows(node) + + -- decorated node icon and name + local icon, name = self:icon_name_decorated(node) + + local line = self:format_line(indent_markers, arrows, icon, name, node) + table.insert(self.lines, self:unwrap_highlighted_strings(line)) + + self.index = self.index + 1 + + local dir = node:as(DirectoryNode) + if dir then + dir = dir:last_group_node() + if dir.open then + self.depth = self.depth + 1 + self:build_lines(dir) + self.depth = self.depth - 1 + end + end +end + +---Add virtual lines for rendering hidden count information per node +---@private +function Builder:add_hidden_count_string(node, idx, num_children) + if not node.open then + return + end + local hidden_count_string = self.hidden_display(node.hidden_stats) + if hidden_count_string and hidden_count_string ~= "" then + local indent_markers = pad.get_indent_markers(self.depth, idx or 0, num_children or 0, node, self.markers, 1) + local indent_width = self.explorer.opts.renderer.indent_width + + local indent_padding = string.rep(" ", indent_width) + local indent_string = indent_padding .. indent_markers.str + local line_nr = #self.lines - 1 + self.virtual_lines[line_nr] = self.virtual_lines[line_nr] or {} + + -- NOTE: We are inserting in depth order because of current traversal + -- if we change the traversal, we might need to sort by depth before rendering `self.virtual_lines` + -- to maintain proper ordering of parent and child folder hidden count info. + table.insert(self.virtual_lines[line_nr], { + { indent_string, indent_markers.hl }, + { string.rep(indent_padding, (node.parent == nil and 0 or 1)) .. hidden_count_string, "NvimTreeHiddenDisplay" }, + }) + end +end + +---Number of visible nodes +---@private +---@param nodes Node[] +---@return integer +function Builder:num_visible(nodes) + if not self.explorer.live_filter.filter then + return #nodes + end + + local i = 0 + for _, n in pairs(nodes) do + if not n.hidden then + i = i + 1 + end + end + return i +end + +---@private +function Builder:build_lines(node) + if not node then + node = self.explorer + end + local num_children = self:num_visible(node.nodes) + local idx = 1 + for _, n in ipairs(node.nodes) do + if not n.hidden then + self:build_signs(n) + self:build_line(n, idx, num_children) + idx = idx + 1 + end + end + self:add_hidden_count_string(node) +end + +---@private +---@param root_label function|string +---@return string +function Builder:format_root_name(root_label) + if type(root_label) == "function" then + local label = root_label(self.explorer.absolute_path) + if type(label) == "string" then + return label + end + elseif type(root_label) == "string" then + return utils.path_remove_trailing(vim.fn.fnamemodify(self.explorer.absolute_path, root_label)) + end + return "???" +end + +---@private +function Builder:build_header() + if view.is_root_folder_visible(self.explorer.absolute_path) then + local root_name = self:format_root_name(self.explorer.opts.renderer.root_folder_label) + table.insert(self.lines, root_name) + self:insert_highlight({ "NvimTreeRootFolder" }, 0, string.len(root_name)) + self.index = 1 + end + + if self.explorer.live_filter.filter then + local filter_line = string.format("%s/%s/", self.explorer.opts.live_filter.prefix, self.explorer.live_filter.filter) + table.insert(self.lines, filter_line) + local prefix_length = string.len(self.explorer.opts.live_filter.prefix) + self:insert_highlight({ "NvimTreeLiveFilterPrefix" }, 0, prefix_length) + self:insert_highlight({ "NvimTreeLiveFilterValue" }, prefix_length, string.len(filter_line)) + self.index = self.index + 1 + end +end + +---Sanitize lines for rendering. +---Replace newlines with literal \n +---@private +function Builder:sanitize_lines() + self.lines = vim.tbl_map(function(line) + return line and line:gsub("\n", "\\n") or "" + end, self.lines) +end + +---Build all lines with highlights and signs +---@return Builder +function Builder:build() + self:build_header() + self:build_lines() + self:sanitize_lines() + return self +end + +---@private +---@param opts table +---@return fun(node: Node): string|nil +function Builder:setup_hidden_display_function(opts) + local hidden_display = opts.renderer.hidden_display + -- options are already validated, so ´hidden_display´ can ONLY be `string` or `function` if type(hidden_display) == "string" then + if type(hidden_display) == "string" then + if hidden_display == "none" then + return function() + return nil + end + elseif hidden_display == "simple" then + return function(hidden_stats) + return utils.default_format_hidden_count(hidden_stats, true) + end + else -- "all" + return function(hidden_stats) + return utils.default_format_hidden_count(hidden_stats, false) + end + end + else -- "function + return function(hidden_stats) + -- In case of missing field such as live_filter we zero it, otherwise keep field as is + hidden_stats = vim.tbl_deep_extend("force", { + live_filter = 0, + git = 0, + buf = 0, + dotfile = 0, + custom = 0, + bookmark = 0, + }, hidden_stats or {}) + + local ok, result = pcall(hidden_display, hidden_stats) + if not ok then + notify.warn( + "Problem occurred in the function ``opts.renderer.hidden_display`` see nvim-tree.renderer.hidden_display on :h nvim-tree") + return nil + end + return result + end + end +end + +return Builder diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/devicons.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/devicons.lua new file mode 100644 index 0000000..ad91d05 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/devicons.lua @@ -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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/full-name.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/full-name.lua new file mode 100644 index 0000000..5474281 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/full-name.lua @@ -0,0 +1,116 @@ +local M = {} + +local utils = require("nvim-tree.utils") + +local function hide(win) + if win then + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + end +end + +-- reduce signcolumn/foldcolumn from window width +local function effective_win_width() + local win_width = vim.fn.winwidth(0) + + -- return zero if the window cannot be found + local win_id = vim.fn.win_getid() + + if win_id == 0 then + return win_width + end + + -- if the window does not exist the result is an empty list + local win_info = vim.fn.getwininfo(win_id) + + -- check if result table is empty + if next(win_info) == nil then + return win_width + end + + return win_width - win_info[1].textoff +end + +local function show() + local line_nr = vim.api.nvim_win_get_cursor(0)[1] + if vim.wo.wrap then + return + end + -- only work for left tree + if vim.api.nvim_win_get_position(0)[2] ~= 0 then + return + end + + local line = vim.fn.getline(".") + local leftcol = vim.fn.winsaveview().leftcol + -- hide full name if left column of node in nvim-tree win is not zero + if leftcol ~= 0 then + return + end + + local text_width = vim.fn.strdisplaywidth(vim.fn.substitute(line, "[^[:print:]]*$", "", "g")) + local win_width = effective_win_width() + + if text_width < win_width then + return + end + + M.popup_win = vim.api.nvim_open_win(vim.api.nvim_create_buf(false, false), false, { + relative = "win", + row = 0, + bufpos = { vim.api.nvim_win_get_cursor(0)[1] - 1, 0 }, + width = math.min(text_width, vim.o.columns - 2), + height = 1, + noautocmd = true, + style = "minimal", + }) + + 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 }) + vim.api.nvim_win_call(M.popup_win, function() + vim.api.nvim_buf_set_lines(0, 0, -1, true, { line }) + for _, extmark in ipairs(extmarks) do + -- nvim 0.10 luadoc is incorrect: vim.api.keyset.get_extmark_item is missing the extmark_id at the start + + ---@cast extmark table + ---@type integer + local col = extmark[3] + ---@type vim.api.keyset.extmark_details + local details = extmark[4] + + vim.api.nvim_buf_add_highlight(0, ns_id, details.hl_group, 0, col, details.end_col) + end + vim.cmd([[ setlocal nowrap cursorline noswapfile nobuflisted buftype=nofile bufhidden=hide ]]) + end) +end + +M.setup = function(opts) + M.config = opts.renderer + if not M.config.full_name then + return + end + + local group = vim.api.nvim_create_augroup("nvim_tree_floating_node", { clear = true }) + vim.api.nvim_create_autocmd({ "BufLeave", "CursorMoved" }, { + group = group, + pattern = { "NvimTree_*" }, + callback = function() + if utils.is_nvim_tree_buf(0) then + hide(M.popup_win) + end + end, + }) + + vim.api.nvim_create_autocmd({ "CursorMoved" }, { + group = group, + pattern = { "NvimTree_*" }, + callback = function() + if utils.is_nvim_tree_buf(0) then + show() + end + end, + }) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/init.lua new file mode 100644 index 0000000..748bf88 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/init.lua @@ -0,0 +1,13 @@ +local M = {} + +M.full_name = require("nvim-tree.renderer.components.full-name") +M.devicons = require("nvim-tree.renderer.components.devicons") +M.padding = require("nvim-tree.renderer.components.padding") + +function M.setup(opts) + M.full_name.setup(opts) + M.devicons.setup(opts) + M.padding.setup(opts) +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/padding.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/padding.lua new file mode 100644 index 0000000..ccb550e --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/components/padding.lua @@ -0,0 +1,132 @@ +local DirectoryNode = require("nvim-tree.node.directory") + +local M = {} + +local function check_siblings_for_folder(node, with_arrows) + if with_arrows then + local has_files = false + local has_folders = false + for _, n in pairs(node.parent.nodes) do + if n.nodes and node.absolute_path ~= n.absolute_path then + has_folders = true + end + if not n.nodes then + has_files = true + end + if has_files and has_folders then + return true + end + end + end + return false +end + +local function get_padding_indent_markers(depth, idx, nodes_number, markers, with_arrows, inline_arrows, node, early_stop) + local base_padding = with_arrows and (not node.nodes or depth > 0) and " " or "" + local padding = (inline_arrows or depth == 0) and base_padding or "" + + if depth > 0 then + local has_folder_sibling = check_siblings_for_folder(node, with_arrows) + local indent = string.rep(" ", M.config.indent_width - 1) + markers[depth] = idx ~= nodes_number + for i = 1, depth - early_stop do + local glyph + if idx == nodes_number and i == depth then + local bottom_width = M.config.indent_width - 2 + (with_arrows and not inline_arrows and has_folder_sibling and 2 or 0) + glyph = M.config.indent_markers.icons.corner + .. string.rep(M.config.indent_markers.icons.bottom, bottom_width) + .. (M.config.indent_width > 1 and " " or "") + elseif markers[i] and i == depth then + glyph = M.config.indent_markers.icons.item .. indent + elseif markers[i] then + glyph = M.config.indent_markers.icons.edge .. indent + else + glyph = M.config.indent_markers.icons.none .. indent + end + + if not with_arrows or (inline_arrows and (depth ~= i or not node.nodes)) then + padding = padding .. glyph + elseif inline_arrows then + padding = padding + elseif idx ~= nodes_number and depth == i and not node.nodes and has_folder_sibling then + padding = padding .. base_padding .. glyph .. base_padding + else + padding = padding .. base_padding .. glyph + end + end + end + return padding +end + +---@param depth integer +---@param idx integer +---@param nodes_number integer +---@param node Node +---@param markers table +---@param early_stop integer? +---@return HighlightedString +function M.get_indent_markers(depth, idx, nodes_number, node, markers, early_stop) + local str = "" + + local show_arrows = M.config.icons.show.folder_arrow + local show_markers = M.config.indent_markers.enable + local inline_arrows = M.config.indent_markers.inline_arrows + local indent_width = M.config.indent_width + + if show_markers then + str = str .. get_padding_indent_markers(depth, idx, nodes_number, markers, show_arrows, inline_arrows, node, early_stop or 0) + else + str = str .. string.rep(" ", depth * indent_width) + end + + return { str = str, hl = { "NvimTreeIndentMarker" } } +end + +---@param node Node +---@return HighlightedString[]|nil +function M.get_arrows(node) + if not M.config.icons.show.folder_arrow then + return + end + + local str + local hl = "NvimTreeFolderArrowClosed" + + local dir = node:as(DirectoryNode) + if dir then + if dir.open then + str = M.config.icons.glyphs.folder["arrow_open"] .. " " + hl = "NvimTreeFolderArrowOpen" + else + str = M.config.icons.glyphs.folder["arrow_closed"] .. " " + end + elseif M.config.indent_markers.enable then + str = "" + else + str = " " + end + + return { str = str, hl = { hl } } +end + +function M.setup(opts) + M.config = opts.renderer + + if M.config.indent_width < 1 then + M.config.indent_width = 1 + end + + local function check_marker(symbol) + if #symbol == 0 then + return " " + end + -- return the first character from the UTF-8 encoded string; we may use utf8.codes from Lua 5.3 when available + return symbol:match("[%z\1-\127\194-\244][\128-\191]*") + end + + for k, v in pairs(M.config.indent_markers.icons) do + M.config.indent_markers.icons[k] = check_marker(v) + end +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/bookmarks.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/bookmarks.lua new file mode 100644 index 0000000..c661748 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/bookmarks.lua @@ -0,0 +1,47 @@ +local Decorator = require("nvim-tree.renderer.decorator") + +---@class (exact) BookmarkDecorator: Decorator +---@field private explorer Explorer +---@field private icon HighlightedString? +local BookmarkDecorator = Decorator:extend() + +---@class BookmarkDecorator +---@overload fun(args: DecoratorArgs): BookmarkDecorator + +---@protected +---@param args DecoratorArgs +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" }, + } + self:define_sign(self.icon) + end +end + +---Bookmark icon: renderer.icons.show.bookmarks and node is marked +---@param node Node +---@return HighlightedString[]? icons +function BookmarkDecorator:icons(node) + if self.explorer.marks:get(node) then + return { self.icon } + end +end + +---Bookmark highlight: renderer.highlight_bookmarks and node is marked +---@param node Node +---@return string? highlight_group +function BookmarkDecorator:highlight_group(node) + if self.highlight_range ~= "none" and self.explorer.marks:get(node) then + return "NvimTreeBookmarkHL" + end +end + +return BookmarkDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/copied.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/copied.lua new file mode 100644 index 0000000..4c452e0 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/copied.lua @@ -0,0 +1,29 @@ +local Decorator = require("nvim-tree.renderer.decorator") + +---@class (exact) CopiedDecorator: Decorator +---@field private explorer Explorer +local CopiedDecorator = Decorator:extend() + +---@class CopiedDecorator +---@overload fun(args: DecoratorArgs): CopiedDecorator + +---@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 + +---Copied highlight: renderer.highlight_clipboard and node is copied +---@param node Node +---@return string? highlight_group +function CopiedDecorator:highlight_group(node) + if self.highlight_range ~= "none" and self.explorer.clipboard:is_copied(node) then + return "NvimTreeCopiedHL" + end +end + +return CopiedDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/cut.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/cut.lua new file mode 100644 index 0000000..8a212da --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/cut.lua @@ -0,0 +1,29 @@ +local Decorator = require("nvim-tree.renderer.decorator") + +---@class (exact) CutDecorator: Decorator +---@field private explorer Explorer +local CutDecorator = Decorator:extend() + +---@class CutDecorator +---@overload fun(args: DecoratorArgs): CutDecorator + +---@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 + +---Cut highlight: renderer.highlight_clipboard and node is cut +---@param node Node +---@return string? highlight_group +function CutDecorator:highlight_group(node) + if self.highlight_range ~= "none" and self.explorer.clipboard:is_cut(node) then + return "NvimTreeCutHL" + end +end + +return CutDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/diagnostics.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/diagnostics.lua new file mode 100644 index 0000000..35f7610 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/diagnostics.lua @@ -0,0 +1,105 @@ +local diagnostics = require("nvim-tree.diagnostics") + +local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") + +-- highlight groups by severity +local HG_ICON = { + [vim.diagnostic.severity.ERROR] = "NvimTreeDiagnosticErrorIcon", + [vim.diagnostic.severity.WARN] = "NvimTreeDiagnosticWarnIcon", + [vim.diagnostic.severity.INFO] = "NvimTreeDiagnosticInfoIcon", + [vim.diagnostic.severity.HINT] = "NvimTreeDiagnosticHintIcon", +} +local HG_FILE = { + [vim.diagnostic.severity.ERROR] = "NvimTreeDiagnosticErrorFileHL", + [vim.diagnostic.severity.WARN] = "NvimTreeDiagnosticWarnFileHL", + [vim.diagnostic.severity.INFO] = "NvimTreeDiagnosticInfoFileHL", + [vim.diagnostic.severity.HINT] = "NvimTreeDiagnosticHintFileHL", +} +local HG_FOLDER = { + [vim.diagnostic.severity.ERROR] = "NvimTreeDiagnosticErrorFolderHL", + [vim.diagnostic.severity.WARN] = "NvimTreeDiagnosticWarnFolderHL", + [vim.diagnostic.severity.INFO] = "NvimTreeDiagnosticInfoFolderHL", + [vim.diagnostic.severity.HINT] = "NvimTreeDiagnosticHintFolderHL", +} +-- opts.diagnostics.icons. +local ICON_KEYS = { + ["error"] = vim.diagnostic.severity.ERROR, + ["warning"] = vim.diagnostic.severity.WARN, + ["info"] = vim.diagnostic.severity.INFO, + ["hint"] = vim.diagnostic.severity.HINT, +} + +---@class (exact) DiagnosticsDecorator: Decorator +---@field private explorer Explorer +---@field private diag_icons HighlightedString[]? +local DiagnosticsDecorator = Decorator:extend() + +---@class DiagnosticsDecorator +---@overload fun(args: DecoratorArgs): DiagnosticsDecorator + +---@protected +---@param args DecoratorArgs +function DiagnosticsDecorator:new(args) + self.explorer = args.explorer + + self.enabled = true + 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 + self.diag_icons[sev] = { + str = self.explorer.opts.diagnostics.icons[name], + hl = { HG_ICON[sev] }, + } + self:define_sign(self.diag_icons[sev]) + end + end +end + +---Diagnostic icon: diagnostics.enable, renderer.icons.show.diagnostics and node has status +---@param node Node +---@return HighlightedString[]? icons +function DiagnosticsDecorator:icons(node) + if node and self.diag_icons then + local diag_status = diagnostics.get_diag_status(node) + local diag_value = diag_status and diag_status.value + + if diag_value then + return { self.diag_icons[diag_value] } + end + end +end + +---Diagnostic highlight: diagnostics.enable, renderer.highlight_diagnostics and node has status +---@param node Node +---@return string? highlight_group +function DiagnosticsDecorator:highlight_group(node) + if self.highlight_range == "none" then + return nil + end + + local diag_status = diagnostics.get_diag_status(node) + local diag_value = diag_status and diag_status.value + + if not diag_value then + return nil + end + + local group + if node:is(DirectoryNode) then + group = HG_FOLDER[diag_value] + else + group = HG_FILE[diag_value] + end + + if group then + return group + else + return nil + end +end + +return DiagnosticsDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/git.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/git.lua new file mode 100644 index 0000000..c62ef00 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/git.lua @@ -0,0 +1,224 @@ +local notify = require("nvim-tree.notify") + +local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) GitHighlightedString: nvim_tree.api.HighlightedString +---@field ord number decreasing priority + +---@alias GitStatusStrings "deleted" | "ignored" | "renamed" | "staged" | "unmerged" | "unstaged" | "untracked" + +---@alias GitIconsByStatus table human status +---@alias GitIconsByXY table porcelain status +---@alias GitGlyphsByStatus table from opts + +---@class (exact) GitDecorator: Decorator +---@field private explorer Explorer +---@field private file_hl_by_xy table? +---@field private folder_hl_by_xy table? +---@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 + + if self.highlight_range ~= "none" then + self:build_file_folder_hl_by_xy() + end + + if self.explorer.opts.renderer.icons.show.git then + self:build_icons_by_status(self.explorer.opts.renderer.icons.glyphs.git) + self:build_icons_by_xy(self.icons_by_status) + + for _, icon in pairs(self.icons_by_status) do + self:define_sign(icon) + end + end +end + +---@param glyphs GitGlyphsByStatus +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.unstaged = { str = glyphs.unstaged, hl = { "NvimTreeGitDirtyIcon" }, ord = 2 } + self.icons_by_status.renamed = { str = glyphs.renamed, hl = { "NvimTreeGitRenamedIcon" }, ord = 3 } + self.icons_by_status.deleted = { str = glyphs.deleted, hl = { "NvimTreeGitDeletedIcon" }, ord = 4 } + self.icons_by_status.unmerged = { str = glyphs.unmerged, hl = { "NvimTreeGitMergeIcon" }, ord = 5 } + self.icons_by_status.untracked = { str = glyphs.untracked, hl = { "NvimTreeGitNewIcon" }, ord = 6 } + self.icons_by_status.ignored = { str = glyphs.ignored, hl = { "NvimTreeGitIgnoredIcon" }, ord = 7 } +end + +---@param icons GitIconsByXY +function GitDecorator:build_icons_by_xy(icons) + self.icons_by_xy = { + ["M "] = { icons.staged }, + [" M"] = { icons.unstaged }, + ["C "] = { icons.staged }, + [" C"] = { icons.unstaged }, + ["CM"] = { icons.unstaged }, + [" T"] = { icons.unstaged }, + ["T "] = { icons.staged }, + ["TM"] = { icons.staged, icons.unstaged }, + ["MM"] = { icons.staged, icons.unstaged }, + ["MD"] = { icons.staged }, + ["A "] = { icons.staged }, + ["AD"] = { icons.staged }, + [" A"] = { icons.untracked }, + -- not sure about this one + ["AA"] = { icons.unmerged, icons.untracked }, + ["AU"] = { icons.unmerged, icons.untracked }, + ["AM"] = { icons.staged, icons.unstaged }, + ["??"] = { icons.untracked }, + ["R "] = { icons.renamed }, + [" R"] = { icons.renamed }, + ["RM"] = { icons.unstaged, icons.renamed }, + ["UU"] = { icons.unmerged }, + ["UD"] = { icons.unmerged }, + ["UA"] = { icons.unmerged }, + [" D"] = { icons.deleted }, + ["D "] = { icons.deleted }, + ["DA"] = { icons.unstaged }, + ["RD"] = { icons.deleted }, + ["DD"] = { icons.deleted }, + ["DU"] = { icons.deleted, icons.unmerged }, + ["!!"] = { icons.ignored }, + dirty = { icons.unstaged }, + } +end + +function GitDecorator:build_file_folder_hl_by_xy() + self.file_hl_by_xy = { + ["M "] = "NvimTreeGitFileStagedHL", + ["C "] = "NvimTreeGitFileStagedHL", + ["AA"] = "NvimTreeGitFileStagedHL", + ["AD"] = "NvimTreeGitFileStagedHL", + ["MD"] = "NvimTreeGitFileStagedHL", + ["T "] = "NvimTreeGitFileStagedHL", + ["TT"] = "NvimTreeGitFileStagedHL", + [" M"] = "NvimTreeGitFileDirtyHL", + ["CM"] = "NvimTreeGitFileDirtyHL", + [" C"] = "NvimTreeGitFileDirtyHL", + [" T"] = "NvimTreeGitFileDirtyHL", + ["MM"] = "NvimTreeGitFileDirtyHL", + ["AM"] = "NvimTreeGitFileDirtyHL", + dirty = "NvimTreeGitFileDirtyHL", + ["A "] = "NvimTreeGitFileStagedHL", + ["??"] = "NvimTreeGitFileNewHL", + ["AU"] = "NvimTreeGitFileMergeHL", + ["UU"] = "NvimTreeGitFileMergeHL", + ["UD"] = "NvimTreeGitFileMergeHL", + ["DU"] = "NvimTreeGitFileMergeHL", + ["UA"] = "NvimTreeGitFileMergeHL", + [" D"] = "NvimTreeGitFileDeletedHL", + ["DD"] = "NvimTreeGitFileDeletedHL", + ["RD"] = "NvimTreeGitFileDeletedHL", + ["D "] = "NvimTreeGitFileDeletedHL", + ["R "] = "NvimTreeGitFileRenamedHL", + ["RM"] = "NvimTreeGitFileRenamedHL", + [" R"] = "NvimTreeGitFileRenamedHL", + ["!!"] = "NvimTreeGitFileIgnoredHL", + [" A"] = "none", + } + + self.folder_hl_by_xy = {} + for k, v in pairs(self.file_hl_by_xy) do + self.folder_hl_by_xy[k] = v:gsub("File", "Folder") + end +end + +---Git icons: git.enable, renderer.icons.show.git and node has status +---@param node Node +---@return HighlightedString[]? icons +function GitDecorator:icons(node) + if not self.icons_by_xy then + return nil + end + + local git_xy = node:get_git_xy() + if git_xy == nil then + return nil + end + + local inserted = {} + local iconss = {} + + for _, s in pairs(git_xy) do + local icons = self.icons_by_xy[s] + if not icons then + if self.highlight_range == "none" then + notify.warn(string.format("Unrecognized git state '%s'", git_xy)) + end + return nil + end + + for _, icon in pairs(icons) do + if #icon.str > 0 then + if not inserted[icon] then + table.insert(iconss, icon) + inserted[icon] = true + end + end + end + end + + if #iconss == 0 then + return nil + end + + -- sort icons so it looks slightly better + table.sort(iconss, function(a, b) + return a.ord < b.ord + end) + + return iconss +end + +---Get the first icon as the sign if appropriate +---@param node Node +---@return string|nil name +function GitDecorator:sign_name(node) + if self.icon_placement ~= "signcolumn" then + return + end + + local icons = self:icons(node) + if icons and #icons > 0 then + return icons[1].hl[1] + end +end + +---Git highlight: git.enable, renderer.highlight_git and node has status +---@param node Node +---@return string? highlight_group +function GitDecorator:highlight_group(node) + if self.highlight_range == "none" then + return nil + end + + local git_xy = node:get_git_xy() + if not git_xy then + return nil + end + + if node:is(DirectoryNode) then + return self.folder_hl_by_xy[git_xy[1]] + else + return self.file_hl_by_xy[git_xy[1]] + end +end + +return GitDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/hidden.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/hidden.lua new file mode 100644 index 0000000..87168ce --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/hidden.lua @@ -0,0 +1,54 @@ +local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) HiddenDecorator: Decorator +---@field private explorer Explorer +---@field private icon HighlightedString? +local HiddenDecorator = Decorator:extend() + +---@class HiddenDecorator +---@overload fun(args: DecoratorArgs): HiddenDecorator + +---@protected +---@param args DecoratorArgs +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" }, + } + self:define_sign(self.icon) + end +end + +---Hidden icon: renderer.icons.show.hidden and node starts with `.` (dotfile). +---@param node Node +---@return HighlightedString[]? icons +function HiddenDecorator:icons(node) + if node:is_dotfile() then + return { self.icon } + end +end + +---Hidden highlight: renderer.highlight_hidden and node starts with `.` (dotfile). +---@param node Node +---@return string? highlight_group +function HiddenDecorator:highlight_group(node) + if self.highlight_range == "none" or not node:is_dotfile() then + return nil + end + + if node:is(DirectoryNode) then + return "NvimTreeHiddenFolderHL" + else + return "NvimTreeHiddenFileHL" + end +end + +return HiddenDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/init.lua new file mode 100644 index 0000000..97ea697 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/init.lua @@ -0,0 +1,133 @@ +local Class = require("nvim-tree.classic") + +---Abstract Decorator +---@class (exact) Decorator: Class +---@field protected enabled boolean +---@field protected highlight_range nvim_tree.api.decorator.HighlightRange +---@field protected icon_placement nvim_tree.api.decorator.IconPlacement +local Decorator = Class:extend() + +---@class (exact) DecoratorArgs +---@field explorer Explorer + +---Abstract icon override, optionally implemented +---@param node Node +---@return HighlightedString? icon_node +function Decorator:icon_node(node) + return self:nop(node) +end + +---Abstract icons, optionally implemented +---@protected +---@param node Node +---@return HighlightedString[]? icons +function Decorator:icons(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 + + if self.enabled and self.highlight_range ~= "none" then + local hl = self:highlight_group(node) + + if self.highlight_range == "all" or self.highlight_range == "icon" then + icon_hl = hl + end + if self.highlight_range == "all" or self.highlight_range == "name" then + name_hl = hl + end + end + + return icon_hl, name_hl +end + +---Maybe icon sign +---@param node Node +---@return string? name +function Decorator:sign_name(node) + if not self.enabled or self.icon_placement ~= "signcolumn" then + return + end + + local icons = self:icons(node) + if icons and #icons > 0 then + return icons[1].hl[1] + end +end + +---Icons when "before" +---@param node Node +---@return HighlightedString[]? icons +function Decorator:icons_before(node) + if not self.enabled or self.icon_placement ~= "before" then + return + end + + return self:icons(node) +end + +---Icons when "after" +---@param node Node +---@return HighlightedString[]? icons +function Decorator:icons_after(node) + if not self.enabled or self.icon_placement ~= "after" then + return + end + + return self:icons(node) +end + +---Icons when "right_align" +---@param node Node +---@return HighlightedString[]? icons +function Decorator:icons_right_align(node) + if not self.enabled or self.icon_placement ~= "right_align" then + return + end + + return self:icons(node) +end + +---Define a sign +---@protected +---@param icon HighlightedString? +function Decorator:define_sign(icon) + if icon and #icon.hl > 0 then + local name = icon.hl[1] + + if not vim.tbl_isempty(vim.fn.sign_getdefined(name)) then + vim.fn.sign_undefine(name) + end + + -- don't use sign if not defined + if #icon.str < 1 then + self.icon_placement = "none" + return + end + + -- byte index of the next character, allowing for wide + local bi = vim.fn.byteidx(icon.str, 1) + + -- first (wide) character, falls back to empty string + local text = string.sub(icon.str, 1, bi) + vim.fn.sign_define(name, { + text = text, + texthl = name, + }) + end +end + +return Decorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/modified.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/modified.lua new file mode 100644 index 0000000..c182e30 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/modified.lua @@ -0,0 +1,56 @@ +local buffers = require("nvim-tree.buffers") + +local Decorator = require("nvim-tree.renderer.decorator") +local DirectoryNode = require("nvim-tree.node.directory") + +---@class (exact) ModifiedDecorator: Decorator +---@field private explorer Explorer +---@field private icon HighlightedString? +local ModifiedDecorator = Decorator:extend() + +---@class ModifiedDecorator +---@overload fun(args: DecoratorArgs): ModifiedDecorator + +---@protected +---@param args DecoratorArgs +function ModifiedDecorator:new(args) + self.explorer = args.explorer + + self.enabled = true + self.highlight_range = self.explorer.opts.renderer.highlight_modified or "none" + 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" }, + } + self:define_sign(self.icon) + end +end + +---Modified icon: modified.enable, renderer.icons.show.modified and node is modified +---@param node Node +---@return HighlightedString[]? icons +function ModifiedDecorator:icons(node) + if buffers.is_modified(node) then + return { self.icon } + end +end + +---Modified highlight: modified.enable, renderer.highlight_modified and node is modified +---@param node Node +---@return string? highlight_group +function ModifiedDecorator:highlight_group(node) + if self.highlight_range == "none" or not buffers.is_modified(node) then + return nil + end + + if node:is(DirectoryNode) then + return "NvimTreeModifiedFolderHL" + else + return "NvimTreeModifiedFileHL" + end +end + +return ModifiedDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/opened.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/opened.lua new file mode 100644 index 0000000..240dce4 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/opened.lua @@ -0,0 +1,32 @@ +local buffers = require("nvim-tree.buffers") + +local Decorator = require("nvim-tree.renderer.decorator") + +---@class (exact) OpenDecorator: Decorator +---@field private explorer Explorer +---@field private icon HighlightedString|nil +local OpenDecorator = Decorator:extend() + +---@class OpenDecorator +---@overload fun(args: DecoratorArgs): OpenDecorator + +---@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 + +---Opened highlight: renderer.highlight_opened_files and node has an open buffer +---@param node Node +---@return string? highlight_group +function OpenDecorator:highlight_group(node) + if self.highlight_range ~= "none" and buffers.is_opened(node) then + return "NvimTreeOpenedHL" + end +end + +return OpenDecorator diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/user.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/user.lua new file mode 100644 index 0000000..df55f54 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/decorator/user.lua @@ -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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/init.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/init.lua new file mode 100644 index 0000000..00d49f7 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/renderer/init.lua @@ -0,0 +1,119 @@ +local log = require("nvim-tree.log") +local view = require("nvim-tree.view") +local events = require("nvim-tree.events") + +local Class = require("nvim-tree.classic") +local Builder = require("nvim-tree.renderer.builder") + +local SIGN_GROUP = "NvimTreeRendererSigns" + +local namespace_highlights_id = vim.api.nvim_create_namespace("NvimTreeHighlights") +local namespace_extmarks_id = vim.api.nvim_create_namespace("NvimTreeExtmarks") +local namespace_virtual_lines_id = vim.api.nvim_create_namespace("NvimTreeVirtualLines") + +---@class (exact) Renderer: Class +---@field explorer Explorer +local Renderer = Class:extend() + +---@class Renderer +---@overload fun(args: RendererArgs): Renderer + +---@class (exact) RendererArgs +---@field explorer Explorer + +---@protected +---@param args RendererArgs +function Renderer:new(args) + self.explorer = args.explorer +end + +---@private +---@param bufnr number +---@param lines string[] +---@param hl_args AddHighlightArgs[] +---@param signs string[] +---@param extmarks table[] extra marks for right icon placement +---@param virtual_lines table[] virtual lines for hidden count display +function Renderer:_draw(bufnr, lines, hl_args, signs, extmarks, virtual_lines) + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("modifiable", true, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "modifiable", true) ---@diagnostic disable-line: deprecated + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + self:render_hl(bufnr, hl_args) + + if vim.fn.has("nvim-0.10") == 1 then + vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated + end + + vim.fn.sign_unplace(SIGN_GROUP) + for i, sign_name in pairs(signs) do + vim.fn.sign_place(0, SIGN_GROUP, sign_name, bufnr, { lnum = i + 1 }) + end + + vim.api.nvim_buf_clear_namespace(bufnr, namespace_extmarks_id, 0, -1) + for i, extname in pairs(extmarks) do + for _, mark in ipairs(extname) do + vim.api.nvim_buf_set_extmark(bufnr, namespace_extmarks_id, i, -1, { + virt_text = { { mark.str, mark.hl } }, + virt_text_pos = "right_align", + hl_mode = "combine", + }) + end + end + + vim.api.nvim_buf_clear_namespace(bufnr, namespace_virtual_lines_id, 0, -1) + for line_nr, vlines in pairs(virtual_lines) do + vim.api.nvim_buf_set_extmark(bufnr, namespace_virtual_lines_id, line_nr, 0, { + virt_lines = vlines, + virt_lines_above = false, + virt_lines_leftcol = true, + }) + end +end + +---@private +function Renderer:render_hl(bufnr, hl) + if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then + return + end + vim.api.nvim_buf_clear_namespace(bufnr, namespace_highlights_id, 0, -1) + for _, data in ipairs(hl) do + if type(data[1]) == "table" then + for _, group in ipairs(data[1]) do + vim.api.nvim_buf_add_highlight(bufnr, namespace_highlights_id, group, data[2], data[3], data[4]) + end + end + end +end + +function Renderer:draw() + local bufnr = view.get_bufnr() + if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then + return + end + + local profile = log.profile_start("draw") + + local cursor = vim.api.nvim_win_get_cursor(view.get_winnr() or 0) + + local builder = Builder(self.explorer):build() + + self:_draw(bufnr, builder.lines, builder.hl_args, builder.signs, builder.extmarks, builder.virtual_lines) + + if cursor and #builder.lines >= cursor[1] then + vim.api.nvim_win_set_cursor(view.get_winnr() or 0, cursor) + end + + view.grow_from_content() + + log.profile_end(profile) + + events._dispatch_on_tree_rendered(bufnr, view.get_winnr()) +end + +return Renderer diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/utils.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/utils.lua new file mode 100644 index 0000000..194845f --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/utils.lua @@ -0,0 +1,643 @@ +local Iterator = require("nvim-tree.iterators.node-iterator") +local notify = require("nvim-tree.notify") + +local M = { + debouncers = {}, +} + +M.is_unix = vim.fn.has("unix") == 1 +M.is_macos = vim.fn.has("mac") == 1 or vim.fn.has("macunix") == 1 +M.is_wsl = vim.fn.has("wsl") == 1 +-- false for WSL +M.is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win32unix") == 1 + +---@param haystack string +---@param needle string +---@return boolean +function M.str_find(haystack, needle) + return vim.fn.stridx(haystack, needle) ~= -1 +end + +---@param path string +---@return string|uv.uv_fs_t +function M.read_file(path) + local fd = vim.loop.fs_open(path, "r", 438) + if not fd then + return "" + end + local stat = vim.loop.fs_fstat(fd) + if not stat then + return "" + end + local data = vim.loop.fs_read(fd, stat.size, 0) + vim.loop.fs_close(fd) + return data or "" +end + +local path_separator = package.config:sub(1, 1) +---@param paths string[] +---@return string +function M.path_join(paths) + return table.concat(vim.tbl_map(M.path_remove_trailing, paths), path_separator) +end + +---@param path string +---@return fun(): string +function M.path_split(path) + return path:gmatch("[^" .. path_separator .. "]+" .. path_separator .. "?") +end + +--- Get the basename of the given path. +---@param path string +---@return string +function M.path_basename(path) + path = M.path_remove_trailing(path) + local i = path:match("^.*()" .. path_separator) + if not i then + return path + end + return path:sub(i + 1, #path) +end + +--- Check if there are parentheses before brackets, it causes problems for windows. +--- Refer to issue #2862 and #2961 for more details. +local function has_parentheses_and_brackets(path) + local _, i_parentheses = path:find("(", 1, true) + local _, i_brackets = path:find("[", 1, true) + if i_parentheses and i_brackets then + return true + end + return false +end + +--- Path normalizations for windows only +local function win_norm_path(path) + if path == nil then + return path + end + local norm_path = path + -- Normalize for issue #2862 and #2961 + if has_parentheses_and_brackets(norm_path) then + norm_path = norm_path:gsub("/", "\\") + end + -- Normalize the drive letter + norm_path = norm_path:gsub("^%l:", function(drive) + return drive:upper() + end) + return norm_path +end + +--- Get a path relative to another path. +---@param path string +---@param relative_to string|nil +---@return string +function M.path_relative(path, relative_to) + if relative_to == nil then + return path + end + + local norm_path = path + if M.is_windows then + norm_path = win_norm_path(norm_path) + end + + local _, r = norm_path:find(M.path_add_trailing(relative_to), 1, true) + local p = norm_path + if r then + -- take the relative path starting after '/' + -- if somehow given a completely matching path, + -- returns "" + p = norm_path:sub(r + 1) + end + return p +end + +---@param path string +---@return string +function M.path_add_trailing(path) + if path:sub(-1) == path_separator then + return path + end + + return path .. path_separator +end + +---@param path string +---@return string +function M.path_remove_trailing(path) + local p, _ = path:gsub(path_separator .. "$", "") + return p +end + +M.path_separator = path_separator + +--- Get the node and index of the node from the tree that matches the predicate. +--- The explored nodes are those displayed on the view. +---@param nodes Node[] +---@param fn fun(node: Node): boolean +---@return table|nil +---@return number +function M.find_node(nodes, fn) + local node, i = Iterator.builder(nodes) + :matcher(fn) + :recursor(function(node) + return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) + end) + :iterate() + i = require("nvim-tree.view").is_root_folder_visible() and i or i - 1 + if node and node.explorer.live_filter.filter then + i = i + 1 + end + return node, i +end + +-- Find the line number of a node. +-- Return -1 is node is nil or not found. +---@param node Node? +---@return integer +function M.find_node_line(node) + if not node then + return -1 + end + + local first_node_line = require("nvim-tree.core").get_nodes_starting_line() + local nodes_by_line = M.get_nodes_by_line(require("nvim-tree.core").get_explorer().nodes, first_node_line) + local iter_start, iter_end = first_node_line, #nodes_by_line + + for line = iter_start, iter_end, 1 do + if nodes_by_line[line] == node then + return line + end + end + + return -1 +end + +-- get the node in the tree state depending on the absolute path of the node +-- (grouped or hidden too) +---@param path string +---@return Node|nil +---@return number|nil +function M.get_node_from_path(path) + local explorer = require("nvim-tree.core").get_explorer() + + -- tree may not yet be loaded + if not explorer then + return + end + + if explorer.absolute_path == path then + return explorer + end + + return Iterator.builder(explorer.nodes) + :hidden() + :matcher(function(node) + return node.absolute_path == path or node.link_to == path + end) + :recursor(function(node) + if node.group_next then + return { node.group_next } + end + if node.nodes then + return node.nodes + end + end) + :iterate() +end + +M.default_format_hidden_count = function(hidden_count, simple) + local parts = {} + local total_count = 0 + for reason, count in pairs(hidden_count) do + total_count = total_count + count + if count > 0 then + table.insert(parts, reason .. ": " .. tostring(count)) + end + end + + local hidden_count_string = table.concat(parts, ", ") -- if empty then is "" (empty string) + if simple then + hidden_count_string = "" + end + if total_count > 0 then + return "(" .. tostring(total_count) .. (simple and " hidden" or " total ") .. hidden_count_string .. ")" + end + return nil +end + +--- Return visible nodes indexed by line +---@param nodes_all Node[] +---@param line_start number +---@return table +function M.get_nodes_by_line(nodes_all, line_start) + local nodes_by_line = {} + local line = line_start + + Iterator.builder(nodes_all) + :applier(function(node) + if node.group_next then + return + end + nodes_by_line[line] = node + line = line + 1 + end) + :recursor(function(node) + return node.group_next and { node.group_next } or (node.open and #node.nodes > 0 and node.nodes) + end) + :iterate() + + return nodes_by_line +end + +function M.rename_loaded_buffers(old_path, new_path) + -- delete new if it exists + for _, buf in pairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + local buf_name = vim.api.nvim_buf_get_name(buf) + if buf_name == new_path then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end + + -- rename old to new + for _, buf in pairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + local buf_name = vim.api.nvim_buf_get_name(buf) + local exact_match = buf_name == old_path + local child_match = (buf_name:sub(1, #old_path) == old_path and buf_name:sub(#old_path + 1, #old_path + 1) == path_separator) + if exact_match or child_match then + vim.api.nvim_buf_set_name(buf, new_path .. buf_name:sub(#old_path + 1)) + -- to avoid the 'overwrite existing file' error message on write for + -- normal files + local buftype + if vim.fn.has("nvim-0.10") == 1 then + buftype = vim.api.nvim_get_option_value("buftype", { buf = buf }) + else + buftype = vim.api.nvim_buf_get_option(buf, "buftype") ---@diagnostic disable-line: deprecated + end + + if buftype == "" then + vim.api.nvim_buf_call(buf, function() + vim.cmd("silent! write!") + vim.cmd("edit") + end) + end + end + end + end +end + +---@param path string path to file or directory +---@return boolean +function M.file_exists(path) + local _, error = vim.loop.fs_stat(path) + return error == nil +end + +---@param path string +---@return string +function M.canonical_path(path) + if M.is_windows and path:match("^%a:") then + return path:sub(1, 1):upper() .. path:sub(2) + end + return path +end + +--- Escapes special characters in string for windows, refer to issue #2862 and #2961 for more details. +local function escape_special_char_for_windows(path) + if has_parentheses_and_brackets(path) then + return path:gsub("\\", "/"):gsub("/ ", "\\ ") + end + return path:gsub("%(", "\\("):gsub("%)", "\\)") +end + +--- Escapes special characters in string if windows else returns unmodified string. +---@param path string +---@return string|nil +function M.escape_special_chars(path) + if path == nil then + return path + end + return M.is_windows and escape_special_char_for_windows(path) or path +end + +--- Create empty sub-tables if not present +---@param tbl table to create empty inside of +---@param path string dot separated string of sub-tables +---@return table deepest sub-table +function M.table_create_missing(tbl, path) + local t = tbl + for s in string.gmatch(path, "([^%.]+)%.*") do + if t[s] == nil then + t[s] = {} + end + t = t[s] + end + + return t +end + +--- Move a value from src to dst if value is nil on dst. +--- Remove value from src +---@param src table to copy from +---@param src_path string dot separated string of sub-tables +---@param src_pos string value pos +---@param dst table to copy to +---@param dst_path string dot separated string of sub-tables, created when missing +---@param dst_pos string value pos +---@param remove boolean +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 + if src[pos] and type(src[pos]) == "table" then + src = src[pos] + else + return + end + end + local src_val = src[src_pos] + if src_val == nil then + return + end + + dst = M.table_create_missing(dst, dst_path) + if dst[dst_pos] == nil then + dst[dst_pos] = src_val + end + + if remove then + src[src_pos] = nil + end +end + +local function round(value) + -- Amount of digits to round to after floating point. + local digits = 2 + local round_number = 10 ^ digits + return math.floor((value * round_number) + 0.5) / round_number +end + +function M.format_bytes(bytes) + local units = { "B", "K", "M", "G", "T", "P", "E", "Z", "Y" } + local i = "i" -- bInary + + bytes = math.max(bytes, 0) + local pow = math.floor((bytes and math.log(bytes) or 0) / math.log(1024)) + pow = math.min(pow, #units) + + local value = round(bytes / (1024 ^ pow)) + + pow = pow + 1 + + -- units[pow] == nil when size == 0 B or size >= 1024 YiB + if units[pow] == nil or pow == 1 then + if bytes < 1024 then + return bytes .. " " .. units[1] + else + -- Use the biggest adopted multiple of 2 instead of bytes. + value = round(bytes / (1024 ^ (#units - 1))) + -- For big numbers decimal part is not useful. + return string.format("%.0f %s%s%s", value, units[#units], i, units[1]) + end + else + return value .. " " .. units[pow] .. i .. units[1] + end +end + +function M.key_by(tbl, key) + local keyed = {} + for _, val in ipairs(tbl) do + if val[key] then + keyed[val[key]] = val + end + end + return keyed +end + +function M.bool_record(tbl, key) + local keyed = {} + for _, val in ipairs(tbl) do + keyed[val[key]] = true + end + return keyed +end + +local function timer_stop_close(timer) + if timer:is_active() then + timer:stop() + end + if not timer:is_closing() then + timer:close() + end +end + +---Execute callback timeout ms after the latest invocation with context. +---Waiting invocations for that context will be discarded. +---Invocation will be rescheduled while a callback is being executed. +---Caller must ensure that callback performs the same or functionally equivalent actions. +--- +---@param context string identifies the callback to debounce +---@param timeout number ms to wait +---@param callback function to execute on completion +function M.debounce(context, timeout, callback) + -- all execution here is done in a synchronous context; no thread safety required + + M.debouncers[context] = M.debouncers[context] or {} + local debouncer = M.debouncers[context] + + -- cancel waiting or executing timer + if debouncer.timer then + timer_stop_close(debouncer.timer) + end + + local timer = vim.loop.new_timer() + if not timer then + return + end + debouncer.timer = timer + timer:start(timeout, 0, function() + timer_stop_close(timer) + + -- reschedule when callback is running + if debouncer.executing then + M.debounce(context, timeout, callback) + return + end + + -- call back at a safe time + debouncer.executing = true + vim.schedule(function() + callback() + debouncer.executing = false + + -- no other timer waiting + if debouncer.timer == timer then + M.debouncers[context] = nil + end + end) + end) +end + +function M.focus_file(path) + local _, i = M.find_node(require("nvim-tree.core").get_explorer().nodes, function(node) + return node.absolute_path == path + end) + require("nvim-tree.view").set_cursor({ i + 1, 1 }) +end + +---Focus node passed as parameter if visible, otherwise focus first visible parent. +---If none of the parents is visible focus root. +---If node is nil do nothing. +---@param node Node? node to focus +function M.focus_node_or_parent(node) + local explorer = require("nvim-tree.core").get_explorer() + + if explorer == nil then + return + end + + while node do + local found_node, i = M.find_node(explorer.nodes, function(node_) + return node_.absolute_path == node.absolute_path + end) + + if found_node or node.parent == nil then + require("nvim-tree.view").set_cursor({ i + 1, 1 }) + break + end + + node = node.parent + end +end + +---@param path string +---@return integer|nil +---@return integer|nil +function M.get_win_buf_from_path(path) + for _, w in pairs(vim.api.nvim_tabpage_list_wins(0)) do + local b = vim.api.nvim_win_get_buf(w) + if vim.api.nvim_buf_get_name(b) == path then + return w, b + end + end + return nil, nil +end + +function M.clear_prompt() + if vim.opt.cmdheight._value ~= 0 then + vim.cmd("normal! :") + end +end + +--- Return a new table with values from array +---@param array table +---@return table +function M.array_shallow_clone(array) + local to = {} + for _, v in ipairs(array) do + table.insert(to, v) + end + return to +end + +--- Remove and return item from array if present. +---@param array table +---@param item any +---@return any|nil removed +function M.array_remove(array, item) + if not array then + return nil + end + for i, v in ipairs(array) do + if v == item then + table.remove(array, i) + return v + end + end +end + +---@param array table +---@return table +function M.array_remove_nils(array) + return vim.tbl_filter(function(v) + return v ~= nil + end, array) +end + +--- Is the buffer named NvimTree_[0-9]+ a tree? filetype is "NvimTree" or not readable file. +--- This is cheap, as the readable test should only ever be needed when resuming a vim session. +---@param bufnr number|nil may be 0 or nil for current +---@return boolean +function M.is_nvim_tree_buf(bufnr) + if bufnr == nil then + bufnr = 0 + end + if vim.api.nvim_buf_is_valid(bufnr) then + local bufname = vim.api.nvim_buf_get_name(bufnr) + if vim.fn.fnamemodify(bufname, ":t"):match("^NvimTree_[0-9]+$") then + if vim.bo[bufnr].filetype == "NvimTree" then + return true + elseif vim.fn.filereadable(bufname) == 0 then + return true + end + end + end + return false +end + +--- path is an executable file or directory +---@param absolute_path string +---@return boolean +function M.is_executable(absolute_path) + if M.is_windows or M.is_wsl then + --- executable detection on windows is buggy and not performant hence it is disabled + return false + else + return vim.loop.fs_access(absolute_path, "X") or false + end +end + +---List of all option info/values +---@param opts vim.api.keyset.option passed directly to vim.api.nvim_get_option_info2 and vim.api.nvim_get_option_value +---@param was_set boolean filter was_set +---@return { info: vim.api.keyset.get_option_info, val: any }[] +function M.enumerate_options(opts, was_set) + local res = {} + + local infos = vim.tbl_filter(function(info) + if opts.buf and info.scope ~= "buf" then + return false + elseif opts.win and info.scope ~= "win" then + return false + else + return true + end + end, vim.api.nvim_get_all_options_info()) + + for _, info in vim.spairs(infos) do + local _, info2 = pcall(vim.api.nvim_get_option_info2, info.name, opts) + if not was_set or info2.was_set then + local val = pcall(vim.api.nvim_get_option_value, info.name, opts) + table.insert(res, { info = info2, val = val }) + end + end + + return res +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/view.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/view.lua new file mode 100644 index 0000000..21c4c63 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/view.lua @@ -0,0 +1,630 @@ +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 = {} + +---@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 }, +} + +---@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) + + bufnr = M.get_bufnr() + for _, option in ipairs(BUFFER_OPTIONS) do + vim.api.nvim_set_option_value(option.name, option.value, { buf = bufnr }) + end + + require("nvim-tree.keymap").on_attach(M.get_bufnr()) + + 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 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 +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()) + + if vim.fn.has("nvim-0.10") == 1 then + local eventignore = vim.api.nvim_get_option_value("eventignore", {}) + vim.api.nvim_set_option_value("eventignore", "all", {}) + + for k, v in pairs(M.View.winopts) do + vim.api.nvim_set_option_value(k, v, { scope = "local" }) + end + + vim.api.nvim_set_option_value("eventignore", eventignore, {}) + else + local eventignore = vim.api.nvim_get_option("eventignore") ---@diagnostic disable-line: deprecated + vim.api.nvim_set_option("eventignore", "all") ---@diagnostic disable-line: deprecated + + -- #3009 vim.api.nvim_win_set_option does not set local scope without explicit winid. + -- Revert to opt_local instead of propagating it through for just the 0.10 path. + for k, v in pairs(M.View.winopts) do + vim.opt_local[k] = v + end + + vim.api.nvim_set_option("eventignore", eventignore) ---@diagnostic disable-line: deprecated + end +end + +---@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 +---@return boolean +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 + +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 diff --git a/pack/ant/start/nvim-tree.lua/lua/nvim-tree/watcher.lua b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/watcher.lua new file mode 100644 index 0000000..960e924 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/lua/nvim-tree/watcher.lua @@ -0,0 +1,272 @@ +local notify = require("nvim-tree.notify") +local log = require("nvim-tree.log") +local utils = require("nvim-tree.utils") + +local Class = require("nvim-tree.classic") + +local FS_EVENT_FLAGS = { + -- inotify or equivalent will be used; fallback to stat has not yet been implemented + stat = false, + -- recursive is not functional in neovim's libuv implementation + recursive = false, +} + +local M = { + config = {}, +} + +---Registry of all events +---@type Event[] +local events = {} + +---@class (exact) Event: Class +---@field destroyed boolean +---@field private path string +---@field private fs_event uv.uv_fs_event_t? +---@field private listeners function[] +local Event = Class:extend() + +---@class Event +---@overload fun(args: EventArgs): Event + +---@class (exact) EventArgs +---@field path string + +---@protected +---@param args EventArgs +function Event:new(args) + self.destroyed = false + self.path = args.path + self.fs_event = nil + self.listeners = {} +end + +---Static factory method +---Creates and starts an Event +---nil on failure to start +---@param args EventArgs +---@return Event? +function Event:create(args) + log.line("watcher", "Event:create '%s'", args.path) + + local event = Event(args) + + if event:start() then + events[event.path] = event + return event + else + return nil + end +end + +---@return boolean +function Event:start() + log.line("watcher", "Event:start '%s'", self.path) + + local rc, _, name + + self.fs_event, _, name = vim.loop.new_fs_event() + if not self.fs_event then + self.fs_event = nil + notify.warn(string.format("Could not initialize an fs_event watcher for path %s : %s", self.path, name)) + return false + end + + local event_cb = vim.schedule_wrap(function(err, filename) + if err then + log.line("watcher", "event_cb '%s' '%s' FAIL : %s", self.path, filename, err) + local message = string.format("File system watcher failed (%s) for path %s, halting watcher.", err, self.path) + if err == "EPERM" and (utils.is_windows or utils.is_wsl) then + -- on directory removal windows will cascade the filesystem events out of order + log.line("watcher", message) + self:destroy() + else + self:destroy(message) + end + else + log.line("watcher", "event_cb '%s' '%s'", self.path, filename) + for _, listener in ipairs(self.listeners) do + listener(filename) + end + end + end) + + rc, _, name = self.fs_event:start(self.path, FS_EVENT_FLAGS, event_cb) + if rc ~= 0 then + if name == "EMFILE" then + M.disable_watchers("fs.inotify.max_user_watches exceeded, see https://github.com/nvim-tree/nvim-tree.lua/wiki/Troubleshooting") + else + notify.warn(string.format("Could not start the fs_event watcher for path %s : %s", self.path, name)) + end + return false + end + + return true +end + +---@param listener function +function Event:add(listener) + table.insert(self.listeners, listener) +end + +---@param listener function +function Event:remove(listener) + utils.array_remove(self.listeners, listener) + if #self.listeners == 0 then + self:destroy() + end +end + +---@param message string|nil +function Event:destroy(message) + log.line("watcher", "Event:destroy '%s'", self.path) + + if self.fs_event then + if message then + notify.warn(message) + end + + local rc, _, name = self.fs_event:stop() + if rc ~= 0 then + notify.warn(string.format("Could not stop the fs_event watcher for path %s : %s", self.path, name)) + end + self.fs_event = nil + end + + self.destroyed = true + events[self.path] = nil +end + +---Registry of all watchers +---@type Watcher[] +local watchers = {} + +---@class (exact) Watcher: Class +---@field data table user data +---@field destroyed boolean +---@field private path string +---@field private callback fun(watcher: Watcher) +---@field private files string[]? +---@field private listener fun(filename: string)? +---@field private event Event +local Watcher = Class:extend() + +---@class Watcher +---@overload fun(args: WatcherArgs): Watcher + +---@class (exact) WatcherArgs +---@field path string +---@field files string[]|nil +---@field callback fun(watcher: Watcher) +---@field data table? user data + +---@protected +---@param args WatcherArgs +function Watcher:new(args) + self.data = args.data + self.destroyed = false + self.path = args.path + self.callback = args.callback + self.files = args.files + self.listener = nil +end + +---Static factory method +---Creates and starts a Watcher +---nil on failure to create Event +---@param args WatcherArgs +---@return Watcher|nil +function Watcher:create(args) + log.line("watcher", "Watcher:create '%s' %s", args.path, vim.inspect(args.files)) + + local event = events[args.path] or Event:create({ path = args.path }) + if not event then + return nil + end + + local watcher = Watcher(args) + + watcher.event = event + + watcher:start() + + table.insert(watchers, watcher) + + return watcher +end + +function Watcher:start() + self.listener = function(filename) + if not self.files or vim.tbl_contains(self.files, filename) then + self.callback(self) + end + end + + self.event:add(self.listener) +end + +function Watcher:destroy() + log.line("watcher", "Watcher:destroy '%s'", self.path) + + self.event:remove(self.listener) + + utils.array_remove( + watchers, + self + ) + + self.destroyed = true +end + +M.Watcher = Watcher + +--- Permanently disable watchers and purge all state following a catastrophic error. +---@param msg string +function M.disable_watchers(msg) + notify.warn(string.format("Disabling watchers: %s", msg)) + M.config.filesystem_watchers.enable = false + require("nvim-tree").purge_all_state() +end + +function M.purge_watchers() + log.line("watcher", "purge_watchers") + + for _, w in ipairs(utils.array_shallow_clone(watchers)) do + w:destroy() + end + + for _, e in pairs(events) do + e:destroy() + end +end + +--- Windows NT will present directories that cannot be enumerated. +--- Detect these by attempting to start an event monitor. +---@param path string +---@return boolean +function M.is_fs_event_capable(path) + if not utils.is_windows then + return true + end + + local fs_event = vim.loop.new_fs_event() + if not fs_event then + return false + end + + if fs_event:start(path, FS_EVENT_FLAGS, function() end) ~= 0 then + return false + end + + if fs_event:stop() ~= 0 then + return false + end + + return true +end + +function M.setup(opts) + M.config.filesystem_watchers = opts.filesystem_watchers +end + +return M diff --git a/pack/ant/start/nvim-tree.lua/release-please-config.json b/pack/ant/start/nvim-tree.lua/release-please-config.json new file mode 100644 index 0000000..bb81fab --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/release-please-config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "bootstrap-sha": "34780aca5bac0a58c163ea30719a276fead1bd95", + "packages": { + ".": { + "package-name": "nvim-tree", + "release-type": "simple" + } + } +} diff --git a/pack/ant/start/nvim-tree.lua/scripts/doc-comments.sh b/pack/ant/start/nvim-tree.lua/scripts/doc-comments.sh new file mode 100755 index 0000000..c31bbea --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/scripts/doc-comments.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +out=$(grep -nr "^--- @" lua) + +if [ "$out" ]; then + last_file="" + while read -r line; do + file="$(echo "$line" | cut -d: -f1)" + if [[ "$file" != "$last_file" ]]; then + echo "$file:" >&2 + last_file="$file" + fi + echo "$line" | awk -F: '{ printf(" line %s: %s\n", $2, $3) }' >&2 + done <<< "$out" + exit 1 +fi diff --git a/pack/ant/start/nvim-tree.lua/scripts/help-update.sh b/pack/ant/start/nvim-tree.lua/scripts/help-update.sh new file mode 100755 index 0000000..474e259 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/scripts/help-update.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +# run after changing nvim-tree.lua DEFAULT_OPTS or keymap.lua M.default_on_attach +# scrapes and updates nvim-tree-lua.txt +# run from repository root: scripts/help-update.sh OR make help-update + + +# +# DEFAULT_OPTS +# +begin="BEGIN_DEFAULT_OPTS" +end="END_DEFAULT_OPTS" + +# scrape DEFAULT_OPTS, indented at 2 +sed -n -e "/${begin}/,/${end}/{ /${begin}/d; /${end}/d; p; }" lua/nvim-tree.lua > /tmp/DEFAULT_OPTS.2.lua + +# indent some more +sed -e "s/^ / /" /tmp/DEFAULT_OPTS.2.lua > /tmp/DEFAULT_OPTS.6.lua + +# help, indented at 6 +sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_OPTS.6.lua + }; /${end}/p; d; }" doc/nvim-tree-lua.txt + + +# +# opts index +# +begin="nvim-tree-index-opts\*" +end="=====================" + +printf '\n' > /tmp/index-opts.txt +sed -E " +/^ *\*(nvim-tree\..*)\*$/! d ; +s/^.*\*(.*)\*/|\1|/g +" doc/nvim-tree-lua.txt | sort -d >> /tmp/index-opts.txt +printf '\n' >> /tmp/index-opts.txt + +sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/index-opts.txt + }; /${end}/p; d; }" doc/nvim-tree-lua.txt + +# +# api index +# +begin="nvim-tree-index-api\*" +end="=====================" + +printf '\n' > /tmp/index-api.txt +sed -E " +/\*(nvim-tree-api.*\(\))\*/! d ; +s/^.*\*(.*)\*/|\1|/g +" doc/nvim-tree-lua.txt | sort -d >> /tmp/index-api.txt +printf '\n' >> /tmp/index-api.txt + +sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/index-api.txt + }; /${end}/p; d; }" doc/nvim-tree-lua.txt + +# +# DEFAULT_ON_ATTACH +# + +begin="BEGIN_DEFAULT_ON_ATTACH" +end="END_DEFAULT_ON_ATTACH" + +# scrape DEFAULT_ON_ATTACH, indented at 2 +sed -n -e "/${begin}/,/${end}/{ /${begin}/d; /${end}/d; p; }" lua/nvim-tree/keymap.lua > /tmp/DEFAULT_ON_ATTACH.lua + +# help lua +sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_ON_ATTACH.lua + }; /${end}/p; d; }" doc/nvim-tree-lua.txt + +# help human +echo > /tmp/DEFAULT_ON_ATTACH.help +sed -E "s/^ *vim.keymap.set\(\"n\", \"(.*)\",.*api(.*),.*opts\(\"(.*)\".*$/'\`\1\`' '\3' '|nvim-tree-api\2()|'/g +" /tmp/DEFAULT_ON_ATTACH.lua | while read -r line +do + eval "printf '%-17.17s %-26.26s %s\n' ${line}" >> /tmp/DEFAULT_ON_ATTACH.help +done +echo >> /tmp/DEFAULT_ON_ATTACH.help +begin="Show the mappings:" +end="======" +sed -i -e "/${begin}/,/${end}/{ /${begin}/{p; r /tmp/DEFAULT_ON_ATTACH.help + }; /${end}/p; d; }" doc/nvim-tree-lua.txt diff --git a/pack/ant/start/nvim-tree.lua/scripts/luals-check.sh b/pack/ant/start/nvim-tree.lua/scripts/luals-check.sh new file mode 100755 index 0000000..6c562b6 --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/scripts/luals-check.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +# Performs a lua-language-server check on all files. +# luals-out/check.json will be produced on any issues, returning 1. +# Outputs only check.json to stdout, all other messages to stderr, to allow jq etc. +# $VIMRUNTIME specifies neovim runtime path, defaults to "/usr/share/nvim/runtime" if unset. + +if [ -z "${VIMRUNTIME}" ]; then + export VIMRUNTIME="/usr/share/nvim/runtime" +fi + +DIR_SRC="${PWD}/lua" +DIR_OUT="${PWD}/luals-out" +FILE_LUARC="${DIR_OUT}/luarc.json" + +# clear output +rm -rf "${DIR_OUT}" +mkdir "${DIR_OUT}" + +# Uncomment runtime.version for strict neovim baseline 5.1 +# It is not set normally, to prevent luals loading 5.1 and 5.x, resulting in both versions being chosen on vim.lsp.buf.definition() +cat "${PWD}/.luarc.json" | sed -E 's/.luals-check-only//g' > "${FILE_LUARC}" + +# execute inside lua to prevent luals itself from being checked +OUT=$(lua-language-server --check="${DIR_SRC}" --configpath="${FILE_LUARC}" --checklevel=Information --logpath="${DIR_OUT}" --loglevel=error) +RC=$? + +echo "${OUT}" >&2 + +if [ $RC -ne 0 ]; then + echo "failed with RC=$RC" + exit $RC +fi + +# any output is a fail +case "${OUT}" in + *Diagnosis\ completed,\ no\ problems\ found*) + exit 0 + ;; + *) + cat "${DIR_OUT}/check.json" + exit 1 + ;; +esac + diff --git a/pack/ant/start/nvim-tree.lua/scripts/setup-hooks.sh b/pack/ant/start/nvim-tree.lua/scripts/setup-hooks.sh new file mode 100755 index 0000000..696556c --- /dev/null +++ b/pack/ant/start/nvim-tree.lua/scripts/setup-hooks.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +ln -sf ../../.hooks/pre-commit.sh .git/hooks/pre-commit diff --git a/vimrc b/vimrc index 7f7d8ca..3f8e6ae 100644 --- a/vimrc +++ b/vimrc @@ -46,6 +46,7 @@ nnoremap :Limelight!! nnoremap :call ACPToggleMargins() nnoremap :w nnoremap :call ACPActodoTodo() +nnoremap :NvimTreeToggle nnoremap :vsplit nnoremap :set invhlsearch nnoremap :call ACPToggleSpellEnUs()