The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .codecov.yml
├── .github
    ├── ISSUE_TEMPLATE
    │   ├── bug_report.yml
    │   └── feature_request.yml
    └── workflows
    │   ├── .luarc-5.1.json
    │   ├── .luarc-luajit-master.json
    │   ├── ci.yml
    │   ├── luals-check.yml
    │   ├── luarocks.yml
    │   └── protect_release_branches.yml
├── .gitignore
├── .lazy.lua
├── .luacov
├── .luarc.json
├── .stylua.toml
├── .styluaignore
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── doc
    └── neo-tree.txt
├── lua
    ├── neo-tree.lua
    └── neo-tree
    │   ├── collections.lua
    │   ├── command
    │       ├── completion.lua
    │       ├── init.lua
    │       └── parser.lua
    │   ├── defaults.lua
    │   ├── events
    │       ├── init.lua
    │       └── queue.lua
    │   ├── git
    │       ├── ignored.lua
    │       ├── init.lua
    │       ├── status.lua
    │       └── utils.lua
    │   ├── health
    │       ├── init.lua
    │       └── typecheck.lua
    │   ├── log.lua
    │   ├── setup
    │       ├── deprecations.lua
    │       ├── init.lua
    │       ├── mapping-helper.lua
    │       └── netrw.lua
    │   ├── sources
    │       ├── buffers
    │       │   ├── commands.lua
    │       │   ├── components.lua
    │       │   ├── init.lua
    │       │   └── lib
    │       │   │   └── items.lua
    │       ├── common
    │       │   ├── commands.lua
    │       │   ├── components.lua
    │       │   ├── container.lua
    │       │   ├── file-items.lua
    │       │   ├── file-nesting.lua
    │       │   ├── filters
    │       │   │   ├── filter_fzy.lua
    │       │   │   └── init.lua
    │       │   ├── help.lua
    │       │   ├── hijack_cursor.lua
    │       │   ├── node_expander.lua
    │       │   └── preview.lua
    │       ├── document_symbols
    │       │   ├── commands.lua
    │       │   ├── components.lua
    │       │   ├── init.lua
    │       │   └── lib
    │       │   │   ├── client_filters.lua
    │       │   │   ├── kinds.lua
    │       │   │   └── symbols_utils.lua
    │       ├── filesystem
    │       │   ├── commands.lua
    │       │   ├── components.lua
    │       │   ├── init.lua
    │       │   └── lib
    │       │   │   ├── filter.lua
    │       │   │   ├── filter_external.lua
    │       │   │   ├── fs_actions.lua
    │       │   │   ├── fs_scan.lua
    │       │   │   ├── fs_watch.lua
    │       │   │   └── globtopattern.lua
    │       ├── git_status
    │       │   ├── commands.lua
    │       │   ├── components.lua
    │       │   ├── init.lua
    │       │   └── lib
    │       │   │   └── items.lua
    │       └── manager.lua
    │   ├── types
    │       ├── components.lua
    │       ├── config.lua
    │       ├── events.lua
    │       └── fixes
    │       │   ├── compat-0.10.lua
    │       │   └── uv.lua
    │   ├── ui
    │       ├── highlights.lua
    │       ├── inputs.lua
    │       ├── popups.lua
    │       ├── renderer.lua
    │       ├── selector.lua
    │       └── windows.lua
    │   └── utils
    │       ├── _compat.lua
    │       ├── filesize
    │           ├── LICENSE
    │           └── filesize.lua
    │       └── init.lua
├── mise.toml
├── plugin
    └── neo-tree.lua
├── release.sh
└── tests
    ├── mininit.lua
    ├── neo-tree
        ├── command
        │   ├── command_current_spec.lua
        │   └── command_spec.lua
        ├── events
        │   └── queue_spec.lua
        ├── hacks
        │   └── hacks_spec.lua
        ├── keymap
        │   └── normalization_spec.lua
        ├── manager
        │   └── state_spec.lua
        ├── qol_spec.lua
        ├── sources
        │   ├── container_spec.lua
        │   ├── filesystem
        │   │   └── filesystem_netrw_hijack_spec.lua
        │   ├── manager_spec.lua
        │   └── navigate_spec.lua
        ├── ui
        │   └── icons_spec.lua
        └── utils
        │   └── path_spec.lua
    └── utils
        ├── fs.lua
        ├── init.lua
        └── verify.lua


/.codecov.yml:
--------------------------------------------------------------------------------
 1 | coverage:
 2 |   status:
 3 |     project:
 4 |       default:
 5 |         informational: true
 6 |         only_pulls: true
 7 |     patch:
 8 |       default:
 9 |         informational: true
10 |         only_pulls: true
11 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
 1 | name: Bug Report
 2 | description: File a bug / issue.
 3 | title: "BUG: "
 4 | labels: [bug]
 5 | body:
 6 |   - type: markdown
 7 |     attributes:
 8 |       value: |
 9 |         **Before** reporting an issue, make sure to read [`:h neo-tree.txt`](https://github.com/nvim-neo-tree/neo-tree.nvim/blob/v3.x/doc/neo-tree.txt) and search [existing issues](https://github.com/nvim-neo-tree/neo-tree.nvim/issues). Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/nvim-neo-tree/neo-tree.nvim/discussions) and will be closed.
10 |   - type: checkboxes
11 |     attributes:
12 |       label: Did you check docs and existing issues?
13 |       description: Make sure you checked all of the below before submitting an issue
14 |       options:
15 |         - label: I have read all the docs.
16 |           required: true
17 |         - label: I have searched the existing issues.
18 |           required: true
19 |         - label: I have searched the existing discussions.
20 |           required: true
21 |   - type: input
22 |     attributes:
23 |       label: "Neovim Version (nvim -v)"
24 |       placeholder: "NVIM v0.10.3"
25 |     validations:
26 |       required: true
27 |   - type: input
28 |     attributes:
29 |       label: "Operating System / Version"
30 |       placeholder: "MacOS 11.5"
31 |     validations:
32 |       required: true
33 |   - type: textarea
34 |     attributes:
35 |       label: Describe the Bug
36 |       description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim.
37 |     validations:
38 |       required: true
39 |   - type: textarea
40 |     attributes:
41 |       label: Screenshots, Traceback
42 |       description: Screenshot and traceback if exists. Not required.
43 |     validations:
44 |       required: false
45 |   - type: textarea
46 |     attributes:
47 |       label: Steps to Reproduce
48 |       description: Steps to reproduce the behavior. Describe with the exact commands and keypresses.
49 |       placeholder: |
50 |         1.
51 |         2.
52 |         3.
53 |     validations:
54 |       required: true
55 |   - type: textarea
56 |     attributes:
57 |       label: Expected Behavior
58 |       description: A concise description of what you expected to happen.
59 |     validations:
60 |       required: true
61 |   - type: textarea
62 |     attributes:
63 |       label: Your Configuration
64 |       description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`
65 |       value: |
66 |         -- template from https://lazy.folke.io/developers#reprolua, feel free to replace if you have your own minimal init.lua
67 |         vim.env.LAZY_STDPATH = ".repro"
68 |         load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
69 | 
70 |         require("lazy.minit").repro({
71 |           spec = {
72 |             {
73 |               "nvim-neo-tree/neo-tree.nvim",
74 |               branch = "v3.x", -- or "main"
75 |               dependencies = {
76 |                 "nvim-lua/plenary.nvim",
77 |                 "nvim-tree/nvim-web-devicons", -- not strictly required, but recommended
78 |                 "MunifTanjim/nui.nvim",
79 |                 -- { "3rd/image.nvim", opts = {} }, -- Optional image support
80 |               },
81 |               lazy = false,
82 |               ---@module "neo-tree"
83 |               ---@type neotree.Config?
84 |               opts = {
85 |                 -- fill any relevant options here
86 |               },
87 |             }
88 |           },
89 |         })
90 |         vim.g.mapleader = " "
91 |         vim.keymap.set("n", "<leader>e", "<Cmd>Neotree<CR>")
92 |         -- do anything else you need to do to reproduce the issue
93 |       render: Lua
94 |     validations:
95 |       required: true
96 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
 1 | name: Feature Request
 2 | description: Suggest a new feature.
 3 | title: "FEATURE: "
 4 | labels: [enhancement]
 5 | body:
 6 |   - type: checkboxes
 7 |     attributes:
 8 |       label: Did you check the docs?
 9 |       description: Make sure you read all the docs before submitting a feature request.
10 |       options:
11 |         - label: I have read all the docs.
12 |           required: true
13 |   - type: textarea
14 |     validations:
15 |       required: true
16 |     attributes:
17 |       label: Is your feature request related to a problem? Please describe.
18 |       description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
19 |   - type: textarea
20 |     validations:
21 |       required: true
22 |     attributes:
23 |       label: Describe the solution you'd like.
24 |       description: A clear and concise description of what you want to happen.
25 |   - type: textarea
26 |     validations:
27 |       required: false
28 |     attributes:
29 |       label: Describe alternatives you've considered.
30 |       description: A clear and concise description of any alternative solutions or features you've considered.
31 |   - type: textarea
32 |     validations:
33 |       required: false
34 |     attributes:
35 |       label: Additional Context
36 |       description: Add any other context or screenshots about the feature request here.
37 | 


--------------------------------------------------------------------------------
/.github/workflows/.luarc-5.1.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
 3 |   "diagnostics": {
 4 |     "libraryFiles": "Disable"
 5 |   },
 6 |   "runtime": {
 7 |     "version": "LuaJIT",
 8 |     "path": [
 9 |       "lua/?.lua",
10 |       "lua/?/init.lua",
11 |       "library/?.lua",
12 |       "library/?/init.lua"
13 |     ]
14 |   },
15 |   "workspace": {
16 |     "checkThirdParty": "Disable",
17 |     "library": [
18 |       "$PWD/.dependencies/pack/vendor/start/plenary.nvim",
19 |       "$PWD/.dependencies/pack/vendor/start/nui.nvim",
20 |       "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons",
21 |       "$PWD/.dependencies/pack/vendor/start/snacks.nvim",
22 |       "${3rd}/luassert",
23 |       "${3rd}/busted",
24 |       "${3rd}/luv",
25 |       "$VIMRUNTIME"
26 |     ],
27 |     "ignoreDir": [
28 |       ".dependencies",
29 |       ".luarocks",
30 |       ".lua"
31 |     ]
32 |   }
33 | }
34 | 


--------------------------------------------------------------------------------
/.github/workflows/.luarc-luajit-master.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
 3 |   "diagnostics": {
 4 |     "libraryFiles": "Disable"
 5 |   },
 6 |   "runtime": {
 7 |     "version": "LuaJIT",
 8 |     "path": [
 9 |       "lua/?.lua",
10 |       "lua/?/init.lua",
11 |       "library/?.lua",
12 |       "library/?/init.lua"
13 |     ]
14 |   },
15 |   "workspace": {
16 |     "checkThirdParty": "Disable",
17 |     "library": [
18 |       "$PWD/.dependencies/pack/vendor/start/plenary.nvim",
19 |       "$PWD/.dependencies/pack/vendor/start/nui.nvim",
20 |       "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons",
21 |       "$PWD/.dependencies/pack/vendor/start/snacks.nvim",
22 |       "${3rd}/luassert",
23 |       "${3rd}/busted",
24 |       "${3rd}/luv",
25 |       "$VIMRUNTIME"
26 |     ],
27 |     "ignoreDir": [
28 |       ".dependencies",
29 |       ".luarocks",
30 |       ".lua"
31 |     ]
32 |   }
33 | }
34 | 


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: CI
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - main
 6 |       - v1.x
 7 |       - v2.x
 8 |       - v3.x
 9 |   pull_request:
10 |   workflow_dispatch:
11 | 
12 | jobs:
13 |   stylua-check:
14 |     runs-on: ubuntu-latest
15 |     steps:
16 |       - uses: actions/checkout@v4
17 | 
18 |       - name: Check formatting
19 |         uses: JohnnyMorganz/stylua-action@v4
20 |         with:
21 |           token: ${{ secrets.GITHUB_TOKEN }}
22 |           version: latest
23 |           args: --color always --check lua/
24 | 
25 |   plenary-tests:
26 |     runs-on: ${{ matrix.os }}
27 |     strategy:
28 |       fail-fast: false
29 |       matrix:
30 |         include:
31 |           - os: ubuntu-22.04
32 |             rev: nightly/nvim-linux-x86_64.tar.gz
33 |           - os: ubuntu-22.04
34 |             rev: v0.8.3/nvim-linux64.tar.gz
35 |           - os: ubuntu-22.04
36 |             rev: v0.9.5/nvim-linux64.tar.gz
37 |           - os: ubuntu-22.04
38 |             rev: v0.10.4/nvim-linux-x86_64.tar.gz
39 |     steps:
40 |       - uses: actions/checkout@v4
41 |       - run: date +%F > todays-date
42 |       - name: Restore cache for today's nightly.
43 |         uses: actions/cache@v4
44 |         with:
45 |           path: build
46 |           key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }}
47 |       - name: Prepare
48 |         run: |
49 |           test -d build || {
50 |             mkdir -p build
51 |             curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/build"
52 |           }
53 | 
54 |       # - name: Get Luver Cache Key
55 |       #   id: luver-cache-key
56 |       #   env:
57 |       #     CI_RUNNER_OS: ${{ runner.os }}
58 |       #   run: |
59 |       #     echo "::set-output name=value::${CI_RUNNER_OS}-luver-v1-$(date -u +%Y-%m-%d)"
60 |       #   shell: bash
61 |       # - name: Setup Luver Cache
62 |       #   uses: actions/cache@v2
63 |       #   with:
64 |       #     path: ~/.local/share/luver
65 |       #     key: ${{ steps.luver-cache-key.outputs.value }}
66 | 
67 |       # - name: Setup Lua
68 |       #   uses: MunifTanjim/luver-action@v1
69 |       #   with:
70 |       #     default: 5.1.5
71 |       #     lua_versions: 5.1.5
72 |       #     luarocks_versions: 5.1.5:3.8.0
73 |       # - name: Setup luacov
74 |       #   run: |
75 |       #     luarocks install luacov
76 | 
77 |       - name: Run tests
78 |         run: |
79 |           export PATH="${PWD}/build/bin:${PATH}"
80 |           make setup
81 |           make test
82 | 
83 |       # - name: Upload coverage to Codecov
84 |       #   uses: codecov/codecov-action@v2
85 | 


--------------------------------------------------------------------------------
/.github/workflows/luals-check.yml:
--------------------------------------------------------------------------------
 1 | name: Lua Language Server Diagnostics
 2 | on:
 3 |   pull_request: ~
 4 |   push:
 5 |     branches:
 6 |       - '*'
 7 | 
 8 | jobs:
 9 |   luals-check:
10 |     strategy:
11 |       matrix:
12 |         neovim: ["0.10"]
13 |         lua: ["5.1", "luajit-master"]
14 |     runs-on: ubuntu-latest
15 | 
16 |     steps:
17 |     - name: Checkout
18 |       uses: actions/checkout@v4
19 | 
20 |     - uses: luarocks/gh-actions-lua@v10
21 |       with:
22 |         luaVersion: ${{matrix.lua}}
23 | 
24 |     - name: Install lua-language-server
25 |       uses: jdx/mise-action@v2
26 |       with:
27 |         mise_toml: |
28 |           [tools]
29 |           neovim = "${{ matrix.neovim }}"
30 |           cargo-binstall = "latest"
31 |           "cargo:emmylua_check" = "latest"
32 |           "cargo:emmylua_ls" = "latest"
33 |           lua-language-server = "3.13.9"
34 | 
35 |     - name: Run lua-language-server check
36 |       run: |
37 |         LUARC=".github/workflows/.luarc-${{ matrix.lua }}.json"
38 |         make luals-check CONFIGURATION="$LUARC"
39 | 
40 |     - name: Run emmylua_check
41 |       continue-on-error: true # Doesn't type-check well enough to be worth erroring on, but this runs so fast we might as well help test this out.
42 |       run: |
43 |         LUARC=".github/workflows/.luarc-${{ matrix.lua }}.json"
44 |         make emmylua-check CONFIGURATION="$LUARC"
45 | 


--------------------------------------------------------------------------------
/.github/workflows/luarocks.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Push to Luarocks
 3 | 
 4 | on:
 5 |   push:
 6 |     tags:
 7 |       - '*'
 8 |   workflow_dispatch:
 9 |   pull_request: # Will test the luarocks installation on PR, without uploading
10 | 
11 | jobs:
12 |   luarocks-upload:
13 |     runs-on: ubuntu-22.04
14 |     steps:
15 |       - uses: actions/checkout@v3
16 |         with:
17 |           fetch-depth: 0 # Required to count the commits
18 |       - name: Get Version
19 |         run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV
20 |       - name: LuaRocks Upload
21 |         uses: nvim-neorocks/luarocks-tag-release@v5
22 |         env:
23 |           LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
24 |         with:
25 |           version: ${{ env.LUAROCKS_VERSION }}
26 |           dependencies: |
27 |             plenary.nvim
28 |             nvim-web-devicons
29 |             nui.nvim
30 | 


--------------------------------------------------------------------------------
/.github/workflows/protect_release_branches.yml:
--------------------------------------------------------------------------------
 1 | # This is a basic workflow to help you get started with Actions
 2 | 
 3 | name: No PRs to Release Branches
 4 | 
 5 | # Controls when the workflow will run
 6 | on:
 7 |   # Triggers the workflow on push or pull request events but only for the v1.x branch
 8 |   pull_request:
 9 |     types: [opened, edited, ready_for_review]
10 | 
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 |   # This workflow contains a single job called "build"
14 |   check_target:
15 |     # The type of runner that the job will run on
16 |     runs-on: ubuntu-latest
17 |     # Steps represent a sequence of tasks that will be executed as part of the job
18 |     steps:
19 |       # Runs a single command using the runners shell
20 |       - name: Fail when targeting v2
21 |         run: |
22 |           target=${{ github.base_ref }}
23 |           echo "Target is: $target"
24 |           if [[ $target != "main" ]]; then
25 |             echo "PRs must target main"
26 |             exit 1
27 |           else
28 |             exit 0
29 |           fi
30 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Compiled Lua sources
 2 | luac.out
 3 | 
 4 | # luarocks build files
 5 | *.src.rock
 6 | *.zip
 7 | *.tar.gz
 8 | 
 9 | # Object files
10 | *.o
11 | *.os
12 | *.ko
13 | *.obj
14 | *.elf
15 | 
16 | # Precompiled Headers
17 | *.gch
18 | *.pch
19 | 
20 | # Libraries
21 | *.lib
22 | *.a
23 | *.la
24 | *.lo
25 | *.def
26 | *.exp
27 | 
28 | # Shared objects (inc. Windows DLLs)
29 | *.dll
30 | *.so
31 | *.so.*
32 | *.dylib
33 | 
34 | # Executables
35 | *.exe
36 | *.out
37 | *.app
38 | *.i*86
39 | *.x86_64
40 | *.hex
41 | 
42 | # Vim tag files
43 | tags
44 | 
45 | # Others
46 | .testcache
47 | .dependencies
48 | luacov.*.out
49 | 
50 | tests/repro
51 | .repro
52 | 


--------------------------------------------------------------------------------
/.lazy.lua:
--------------------------------------------------------------------------------
 1 | local root = vim.fs.find({ "neo-tree.nvim" }, { upward = true })[1]
 2 | local deps_dir = root .. "/.dependencies/pack/vendor/start"
 3 | return {
 4 |   {
 5 |     "folke/snacks.nvim",
 6 |     dir = deps_dir .. "/snacks.nvim",
 7 |   },
 8 |   {
 9 |     "MunifTanjim/nui.nvim",
10 |     dir = deps_dir .. "/nui.nvim",
11 |   },
12 |   {
13 |     "nvim-tree/nvim-web-devicons",
14 |     dir = deps_dir .. "/nvim-web-devicons",
15 |   },
16 |   {
17 |     "nvim-lua/plenary.nvim",
18 |     dir = deps_dir .. "/plenary.nvim",
19 |   },
20 | }
21 | 


--------------------------------------------------------------------------------
/.luacov:
--------------------------------------------------------------------------------
1 | include = {
2 |    "lua%/neo%-tree",
3 | }
4 | 


--------------------------------------------------------------------------------
/.luarc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
 3 |   "diagnostics": {
 4 |     "libraryFiles": "Disable"
 5 |   },
 6 |   "runtime": {
 7 |     "version": "LuaJIT",
 8 |     "path": [
 9 |       "lua/?.lua",
10 |       "lua/?/init.lua",
11 |       "library/?.lua",
12 |       "library/?/init.lua"
13 |     ]
14 |   },
15 |   "workspace": {
16 |     "checkThirdParty": "Disable",
17 |     "library": [
18 |       "$PWD/.dependencies/pack/vendor/start/plenary.nvim",
19 |       "$PWD/.dependencies/pack/vendor/start/nui.nvim",
20 |       "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons",
21 |       "$PWD/.dependencies/pack/vendor/start/snacks.nvim",
22 |       "${3rd}/luassert",
23 |       "${3rd}/busted",
24 |       "${3rd}/luv",
25 |       "$VIMRUNTIME"
26 |     ],
27 |     "ignoreDir": [
28 |       ".dependencies",
29 |       ".luarocks",
30 |       ".lua",
31 |       ".repro"
32 |     ]
33 |   }
34 | }
35 | 


--------------------------------------------------------------------------------
/.stylua.toml:
--------------------------------------------------------------------------------
1 | column_width = 100
2 | line_endings = "Unix"
3 | indent_type = "Spaces"
4 | indent_width = 2
5 | quote_style = "AutoPreferDouble"
6 | syntax = "LuaJIT"
7 | 


--------------------------------------------------------------------------------
/.styluaignore:
--------------------------------------------------------------------------------
1 | **/defaults.lua
2 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing to Neo-tree
 2 | 
 3 | Contributions are welcome! To keep everything clean and tidy, please follow the
 4 | guidelines below.
 5 | 
 6 | ## Code Style
 7 | 
 8 | This is open for debate, but here is the current style choices being observed:
 9 | 
10 | - snake_case for all variables and functions
11 | - unless it is a class, then use PascalCase
12 | - other OOP things, like method names should use camelCase
13 | - BUT we don't currently have any OOP parts and I don't think we want any
14 | 
15 | I prefer `local name = function()` over `local function name()`, just to be
16 | consistent with the `M.name = function()` exports.
17 | 
18 | ### StyLua
19 | 
20 | We use (StyLua)[https://github.com/JohnnyMorganz/StyLua] to enforce consistency
21 | in code. You should install it on your local machine. PRs will be checked with
22 | this tool.
23 | 
24 | ## Commit Messages
25 | 
26 | We use **semantic**, aka **conventional** commit messages. The official guide
27 | can be found here: https://www.conventionalcommits.org/en/v1.0.0/
28 | 
29 | You can also just take a look at the commit history to get the idea. The
30 | optional scope for this project would usually be the source, i.e.
31 | `feat(filesystem): add awesome feature that does xyz`.
32 | 
33 | ## Branching
34 | 
35 | The default branch is set to `main` and all Pull Requests should target this
36 | branch. After a short testing period, it will be merged to the current release
37 | branch.
38 | 
39 | This project requires a **linear history**. I don't trust merge commits.
40 | This means you will have to rebase your branch on main before the pull request
41 | can be merged. This can get a bit annoying in a busy repository, but I think it
42 | is worth the effort.
43 | 
44 | ## Documentation
45 | 
46 | All new features should be documented in the commit they were added in. The
47 | current strategy is to maintain:
48 | 
49 | - Config Options: added to [defaults](lua/neo-tree/defaults.lua) and described
50 |   in comments. This is the bare minimum documentation for an option.
51 | - The README contains "back of the box" high level overview of features. It is
52 |   meant for people trying to decide if they want to install this plugin or not.
53 |   It should include references to the help file for more information: 
54 |   `:h neo-tree-setup`
55 | - Whether something should be mentioned in the README or just in the help file
56 |   is a completely subjective judement call that is made on a case by case basis 
57 |   based on how many people are likely to be interested in that information.
58 | - The vim help file [doc/neo-tree.txt](doc/neo-tree.txt) is the definitive
59 |   reference and should contain all information needed to configure and use the
60 |   plugin.
61 | - OUR DOCUMENTATION IS NOT GOOD ENOUGH! Consider the current level of documentation
62 |   the bare minumum and not the ideal. More documentation would be greatly appreciated.
63 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # --- Builder Stage ---
 2 | FROM alpine:latest AS builder
 3 | 
 4 | RUN apk update && apk add --no-cache \
 5 |     build-base \
 6 |     ninja-build \
 7 |     cmake \
 8 |     coreutils \
 9 |     curl \
10 |     gettext-tiny-dev \
11 |     git
12 | 
13 | # Install neovim
14 | RUN git clone --depth=1 https://github.com/neovim/neovim --branch release-0.10
15 | RUN cd neovim && make CMAKE_BUILD_TYPE=RelWithDebInfo && make install
16 | 
17 | # --- Final Stage ---
18 | FROM alpine:latest
19 | 
20 | RUN apk update && apk add --no-cache \
21 |     libstdc++ # Often needed for C++ applications
22 | 
23 | COPY --from=builder /usr/local/bin/nvim /usr/local/bin/nvim
24 | COPY --from=builder /usr/local/share /usr/local/share
25 | 
26 | ARG PLUG_DIR="/root/.local/share/nvim/site/pack/packer/start"
27 | RUN mkdir -p $PLUG_DIR
28 | 
29 | RUN apk add --no-cache git # Git is needed to clone plugins in the final image
30 | 
31 | RUN git clone --depth=1 https://github.com/nvim-lua/plenary.nvim $PLUG_DIR/plenary.nvim
32 | RUN git clone --depth=1 https://github.com/MunifTanjim/nui.nvim $PLUG_DIR/nui.nvim
33 | RUN git clone --depth=1 https://github.com/nvim-tree/nvim-web-devicons.git $PLUG_DIR/nvim-web-devicons
34 | COPY . $PLUG_DIR/neo-tree.nvim
35 | 
36 | WORKDIR $PLUG_DIR/neo-tree.nvim
37 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2021 cseickel (https://github.com/cseickel) and nvim-neo-tree
 4 | maintainers.
 5 | 
 6 | Permission is hereby granted, free of charge, to any person obtaining a copy
 7 | of this software and associated documentation files (the "Software"), to deal
 8 | in the Software without restriction, including without limitation the rights
 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 | 
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 | 
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | 


--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
 1 | .PHONY: test
 2 | test:
 3 | 	nvim --headless --noplugin -u tests/mininit.lua -c "lua require('plenary.test_harness').test_directory('tests/neo-tree/', {minimal_init='tests/mininit.lua',sequential=true})"
 4 | 
 5 | .PHONY: test-docker
 6 | test-docker:
 7 | 	docker build -t neo-tree .
 8 | 	docker run --rm neo-tree make test
 9 | 
10 | .PHONY: format
11 | format:
12 | 	stylua --glob '*.lua' --glob '!defaults.lua' .
13 | 
14 | # Dependencies:
15 | 
16 | DEPS := ${CURDIR}/.dependencies/pack/vendor/start
17 | 
18 | $(DEPS):
19 | 	mkdir -p "$(DEPS)"
20 | 
21 | $(DEPS)/nui.nvim: $(DEPS)
22 | 	@test -d "$(DEPS)/nui.nvim" || git clone https://github.com/MunifTanjim/nui.nvim "$(DEPS)/nui.nvim"
23 | 
24 | $(DEPS)/nvim-web-devicons: $(DEPS)
25 | 	@test -d "$(DEPS)/nvim-web-devicons" || git clone https://github.com/nvim-tree/nvim-web-devicons "$(DEPS)/nvim-web-devicons"
26 | 
27 | $(DEPS)/plenary.nvim: $(DEPS)
28 | 	@test -d "$(DEPS)/plenary.nvim" || git clone https://github.com/nvim-lua/plenary.nvim "$(DEPS)/plenary.nvim"
29 | 
30 | $(DEPS)/snacks.nvim: $(DEPS)
31 | 	@test -d "$(DEPS)/snacks.nvim" || git clone https://github.com/folke/snacks.nvim "$(DEPS)/snacks.nvim"
32 | 
33 | setup: $(DEPS)/nui.nvim $(DEPS)/nvim-web-devicons $(DEPS)/plenary.nvim $(DEPS)/snacks.nvim
34 | 	@echo "[setup] environment ready"
35 | 
36 | .PHONY: clean
37 | clean:
38 | 	rm -rf "$(DEPS)"
39 | 
40 | CONFIGURATION = ${CURDIR}/.luarc.json
41 | luals-check: setup
42 | 	VIMRUNTIME="`nvim --clean --headless --cmd 'lua io.write(vim.env.VIMRUNTIME)' --cmd 'quit'`" lua-language-server --configpath=$(CONFIGURATION) --check=.
43 | 
44 | emmylua-check: setup
45 | 	VIMRUNTIME="`nvim --clean --headless --cmd 'lua io.write(vim.env.VIMRUNTIME)' --cmd 'quit'`" emmylua_check -c $(CONFIGURATION) -i ".dependencies/**" --  .
46 | 


--------------------------------------------------------------------------------
/lua/neo-tree.lua:
--------------------------------------------------------------------------------
  1 | local M = {}
  2 | 
  3 | --- To be removed in a future release, use this instead:
  4 | --- ```lua
  5 | --- require("neo-tree.command").execute({ action = "close" })
  6 | --- ```
  7 | ---@deprecated
  8 | M.close_all = function()
  9 |   require("neo-tree.command").execute({ action = "close" })
 10 | end
 11 | 
 12 | ---@type neotree.Config?
 13 | local new_user_config = nil
 14 | 
 15 | ---Updates the config of neo-tree using the latest user config passed through setup, if any.
 16 | ---@return neotree.Config.Base
 17 | M.ensure_config = function()
 18 |   if not M.config or new_user_config then
 19 |     M.config = require("neo-tree.setup").merge_config(new_user_config)
 20 |     new_user_config = nil
 21 |   end
 22 |   return M.config
 23 | end
 24 | 
 25 | ---@param ignore_filetypes string[]?
 26 | ---@param ignore_winfixbuf boolean?
 27 | M.get_prior_window = function(ignore_filetypes, ignore_winfixbuf)
 28 |   local utils = require("neo-tree.utils")
 29 |   ignore_filetypes = ignore_filetypes or {}
 30 |   local ignore = utils.list_to_dict(ignore_filetypes)
 31 |   ignore["neo-tree"] = true
 32 | 
 33 |   local tabid = vim.api.nvim_get_current_tabpage()
 34 |   local wins = utils.prior_windows[tabid]
 35 |   if wins == nil then
 36 |     return -1
 37 |   end
 38 |   local win_index = #wins
 39 |   while win_index > 0 do
 40 |     local last_win = wins[win_index]
 41 |     if type(last_win) == "number" then
 42 |       local success, is_valid = pcall(vim.api.nvim_win_is_valid, last_win)
 43 |       if success and is_valid and not (ignore_winfixbuf and utils.is_winfixbuf(last_win)) then
 44 |         local buf = vim.api.nvim_win_get_buf(last_win)
 45 |         local ft = vim.bo[buf].filetype
 46 |         local bt = vim.bo[buf].buftype or "normal"
 47 |         if ignore[ft] ~= true and ignore[bt] ~= true then
 48 |           return last_win
 49 |         end
 50 |       end
 51 |     end
 52 |     win_index = win_index - 1
 53 |   end
 54 |   return -1
 55 | end
 56 | 
 57 | M.paste_default_config = function()
 58 |   local utils = require("neo-tree.utils")
 59 |   ---@type string
 60 |   local base_path = assert(debug.getinfo(utils.truthy).source:match("@(.*)/utils/init.lua
quot;))
 61 |   ---@type string
 62 |   local config_path = base_path .. utils.path_separator .. "defaults.lua"
 63 |   ---@type string[]?
 64 |   local lines = vim.fn.readfile(config_path)
 65 |   if lines == nil then
 66 |     error("Could not read neo-tree.defaults")
 67 |   end
 68 | 
 69 |   -- read up to the end of the config, jut to omit the final return
 70 |   ---@type string[]
 71 |   local config = {}
 72 |   for _, line in ipairs(lines) do
 73 |     table.insert(config, line)
 74 |     if line == "}" then
 75 |       break
 76 |     end
 77 |   end
 78 | 
 79 |   vim.api.nvim_put(config, "l", true, false)
 80 |   vim.schedule(function()
 81 |     vim.cmd("normal! `[v`]=")
 82 |   end)
 83 | end
 84 | 
 85 | M.set_log_level = function(level)
 86 |   require("neo-tree.log").set_level(level)
 87 | end
 88 | 
 89 | ---Ideally this should only be in plugin/neo-tree.lua but lazy-loading might mean this runs before bufenter
 90 | ---@param path string? The path to check
 91 | ---@return boolean hijacked Whether we hijacked a buffer
 92 | local function try_netrw_hijack(path)
 93 |   if not path or #path == 0 then
 94 |     return false
 95 |   end
 96 | 
 97 |   local stats = (vim.uv or vim.loop).fs_stat(path)
 98 |   if not stats or stats.type ~= "directory" then
 99 |     return false
100 |   end
101 | 
102 |   return require("neo-tree.setup.netrw").hijack()
103 | end
104 | 
105 | ---@param config neotree.Config
106 | M.setup = function(config)
107 |   -- merging is deferred until ensure_config
108 |   new_user_config = config
109 |   if vim.v.vim_did_enter == 0 then
110 |     try_netrw_hijack(vim.fn.argv(0) --[[@as string]])
111 |   end
112 | end
113 | 
114 | M.show_logs = function()
115 |   vim.cmd("tabnew " .. require("neo-tree.log").outfile)
116 | end
117 | 
118 | return M
119 | 


--------------------------------------------------------------------------------
/lua/neo-tree/collections.lua:
--------------------------------------------------------------------------------
  1 | local log = require("neo-tree.log")
  2 | 
  3 | ---@class neotree.collections.ListNode
  4 | ---@field prev neotree.collections.ListNode?
  5 | ---@field next neotree.collections.ListNode?
  6 | ---@field value any
  7 | 
  8 | local Node = {}
  9 | function Node:new(value)
 10 |   local props = { prev = nil, next = nil, value = value }
 11 |   setmetatable(props, self)
 12 |   self.__index = self
 13 |   return props
 14 | end
 15 | 
 16 | ---@class neotree.collections.LinkedList
 17 | ---@field head neotree.collections.ListNode?
 18 | ---@field tail neotree.collections.ListNode?
 19 | ---@field size integer
 20 | local LinkedList = {}
 21 | 
 22 | ---@return neotree.collections.LinkedList
 23 | function LinkedList:new()
 24 |   local props = { head = nil, tail = nil, size = 0 }
 25 |   setmetatable(props, self)
 26 |   self.__index = self
 27 |   return props
 28 | end
 29 | 
 30 | ---@param node neotree.collections.ListNode
 31 | function LinkedList:add_node(node)
 32 |   if self.head == nil then
 33 |     self.head = node
 34 |     self.tail = node
 35 |   else
 36 |     self.tail.next = node
 37 |     node.prev = self.tail
 38 |     self.tail = node
 39 |   end
 40 |   self.size = self.size + 1
 41 |   return node
 42 | end
 43 | 
 44 | ---@param node neotree.collections.ListNode
 45 | function LinkedList:remove_node(node)
 46 |   if node.prev ~= nil then
 47 |     node.prev.next = node.next
 48 |   end
 49 |   if node.next ~= nil then
 50 |     node.next.prev = node.prev
 51 |   end
 52 |   if self.head == node then
 53 |     self.head = node.next
 54 |   end
 55 |   if self.tail == node then
 56 |     self.tail = node.prev
 57 |   end
 58 |   self.size = self.size - 1
 59 |   node.prev = nil
 60 |   node.next = nil
 61 |   node.value = nil
 62 | end
 63 | 
 64 | -- First in Last Out
 65 | ---@class neotree.collections.Queue
 66 | ---@field _list neotree.collections.LinkedList
 67 | local Queue = {}
 68 | 
 69 | ---@return neotree.collections.Queue
 70 | function Queue:new()
 71 |   local props = { _list = LinkedList:new() }
 72 |   setmetatable(props, self)
 73 |   self.__index = self
 74 |   return props
 75 | end
 76 | 
 77 | ---Add an element to the end of the queue.
 78 | ---@param value any The value to add.
 79 | function Queue:add(value)
 80 |   self._list:add_node(Node:new(value))
 81 | end
 82 | 
 83 | ---Iterates over the entire list, running func(value) on each element.
 84 | ---If func returns true, the element is removed from the list.
 85 | ---@param func function The function to run on each element.
 86 | ---@return table? result
 87 | function Queue:for_each(func)
 88 |   local node = self._list.head
 89 |   while node ~= nil do
 90 |     local result = func(node.value)
 91 |     local node_is_next = false
 92 |     if result then
 93 |       if type(result) == "boolean" then
 94 |         local node_to_remove = node
 95 |         node = node.next
 96 |         node_is_next = true
 97 |         self._list:remove_node(node_to_remove)
 98 |       elseif type(result) == "table" then
 99 |         if result.handled == true then
100 |           log.trace(
101 |             "Handler ",
102 |             node.value.id,
103 |             " for "
104 |               .. node.value.event
105 |               .. " returned handled = true, skipping the rest of the queue."
106 |           )
107 |           return result
108 |         end
109 |       end
110 |     end
111 |     if not node_is_next then
112 |       ---@diagnostic disable-next-line: need-check-nil
113 |       node = node.next
114 |     end
115 |   end
116 | end
117 | 
118 | function Queue:is_empty()
119 |   return self._list.size == 0
120 | end
121 | 
122 | function Queue:remove_by_id(id)
123 |   local current = self._list.head
124 |   while current ~= nil do
125 |     local is_match = false
126 |     local item = current.value
127 |     if item ~= nil then
128 |       local item_id = item.id or item
129 |       if item_id == id then
130 |         is_match = true
131 |       end
132 |     end
133 |     if is_match then
134 |       local next = current.next
135 |       self._list:remove_node(current)
136 |       current = next
137 |     else
138 |       current = current.next
139 |     end
140 |   end
141 | end
142 | 
143 | return {
144 |   Queue = Queue,
145 |   LinkedList = LinkedList,
146 | }
147 | 


--------------------------------------------------------------------------------
/lua/neo-tree/command/completion.lua:
--------------------------------------------------------------------------------
  1 | local parser = require("neo-tree.command.parser")
  2 | local utils = require("neo-tree.utils")
  3 | 
  4 | local M = {
  5 |   show_key_value_completions = true,
  6 | }
  7 | 
  8 | ---@param key_prefix string?
  9 | ---@param base_path string
 10 | ---@return string paths_string
 11 | local get_path_completions = function(key_prefix, base_path)
 12 |   key_prefix = key_prefix or ""
 13 |   local completions = {}
 14 |   local expanded = parser.resolve_path(base_path)
 15 |   local path_completions = vim.fn.glob(expanded .. "*", false, true)
 16 |   for _, completion in ipairs(path_completions) do
 17 |     if expanded ~= base_path then
 18 |       -- we need to recreate the relative path from the aboluste path
 19 |       -- first strip trailing slashes to normalize
 20 |       if expanded:sub(-1) == utils.path_separator then
 21 |         expanded = expanded:sub(1, -2)
 22 |       end
 23 |       if base_path:sub(-1) == utils.path_separator then
 24 |         base_path = base_path:sub(1, -2)
 25 |       end
 26 |       -- now put just the current completion onto the base_path being used
 27 |       completion = base_path .. string.sub(completion, #expanded + 1)
 28 |     end
 29 |     table.insert(completions, key_prefix .. completion)
 30 |   end
 31 | 
 32 |   return table.concat(completions, "\n")
 33 | end
 34 | 
 35 | ---@param key_prefix string?
 36 | ---@return string references_string
 37 | local get_ref_completions = function(key_prefix)
 38 |   key_prefix = key_prefix or ""
 39 |   local completions = { key_prefix .. "HEAD" }
 40 |   local ok, refs = utils.execute_command("git show-ref")
 41 |   if not ok then
 42 |     return ""
 43 |   end
 44 |   for _, ref in ipairs(refs) do
 45 |     local _, i = ref:find("refs%/%a+%/")
 46 |     if i then
 47 |       table.insert(completions, key_prefix .. ref:sub(i + 1))
 48 |     end
 49 |   end
 50 | 
 51 |   return table.concat(completions, "\n")
 52 | end
 53 | 
 54 | ---@param argLead string
 55 | ---@param cmdLine string
 56 | ---@return string candidates_string
 57 | M.complete_args = function(argLead, cmdLine)
 58 |   local candidates = {}
 59 |   local existing = utils.split(cmdLine, " ")
 60 |   local parsed = parser.parse(existing, false)
 61 | 
 62 |   local eq = string.find(argLead, "=")
 63 |   if eq == nil then
 64 |     if M.show_key_value_completions then
 65 |       -- may be the start of a new key=value pair
 66 |       for _, key in ipairs(parser.list_args) do
 67 |         key = tostring(key)
 68 |         if key:find(argLead, 1, true) and not parsed[key] then
 69 |           table.insert(candidates, key .. "=")
 70 |         end
 71 |       end
 72 | 
 73 |       for _, key in ipairs(parser.path_args) do
 74 |         key = tostring(key)
 75 |         if key:find(argLead, 1, true) and not parsed[key] then
 76 |           table.insert(candidates, key .. "=./")
 77 |         end
 78 |       end
 79 | 
 80 |       for _, key in ipairs(parser.ref_args) do
 81 |         key = tostring(key)
 82 |         if key:find(argLead, 1, true) and not parsed[key] then
 83 |           table.insert(candidates, key .. "=")
 84 |         end
 85 |       end
 86 |     end
 87 |   else
 88 |     -- continuation of a key=value pair
 89 |     local key = string.sub(argLead, 1, eq - 1)
 90 |     local value = string.sub(argLead, eq + 1)
 91 |     local arg_type = parser.argtype_lookup[key]
 92 |     if arg_type == parser.argtypes.PATH then
 93 |       return get_path_completions(key .. "=", value)
 94 |     elseif arg_type == parser.argtypes.REF then
 95 |       return get_ref_completions(key .. "=")
 96 |     elseif arg_type == parser.argtypes.LIST then
 97 |       local valid_values = parser.arguments[key].values
 98 |       if valid_values and not (parsed[key] and #parsed[key] > 0) then
 99 |         for _, vv in ipairs(valid_values) do
100 |           if vv:find(value, 1, true) then
101 |             table.insert(candidates, key .. "=" .. vv)
102 |           end
103 |         end
104 |       end
105 |     end
106 |   end
107 | 
108 |   -- may be a value without a key
109 |   for value, key in pairs(parser.reverse_lookup) do
110 |     value = tostring(value)
111 |     local key_already_used = false
112 |     if parser.argtype_lookup[key] == parser.argtypes.LIST then
113 |       key_already_used = type(parsed[key]) ~= "nil"
114 |     else
115 |       key_already_used = type(parsed[value]) ~= "nil"
116 |     end
117 | 
118 |     if not key_already_used and value:find(argLead, 1, true) then
119 |       table.insert(candidates, value)
120 |     end
121 |   end
122 | 
123 |   if #candidates == 0 then
124 |     -- default to path completion
125 |     return get_path_completions(nil, argLead) .. "\n" .. get_ref_completions(nil)
126 |   end
127 |   return table.concat(candidates, "\n")
128 | end
129 | 
130 | return M
131 | 


--------------------------------------------------------------------------------
/lua/neo-tree/command/parser.lua:
--------------------------------------------------------------------------------
  1 | local uv = vim.uv or vim.loop
  2 | local utils = require("neo-tree.utils")
  3 | local _compat = require("neo-tree.utils._compat")
  4 | 
  5 | ---@enum neotree.command.ParserArgument.Type
  6 | local argtype = {
  7 |   FLAG = "<FLAG>",
  8 |   LIST = "<LIST>",
  9 |   PATH = "<PATH>",
 10 |   REF = "<REF>",
 11 | }
 12 | 
 13 | ---@class neotree.command.Parser
 14 | ---@field argtypes table<string, neotree.command.ParserArgument.Type>
 15 | local M = {
 16 |   argtypes = argtype,
 17 | }
 18 | 
 19 | ---@param all_source_names string[]
 20 | M.setup = function(all_source_names)
 21 |   local source_names = vim.deepcopy(all_source_names, _compat.noref())
 22 |   table.insert(source_names, "migrations")
 23 | 
 24 |   -- A special source referring to the last used source.
 25 |   table.insert(source_names, "last")
 26 | 
 27 |   ---@class neotree.command.ParserArgument
 28 |   ---@field type neotree.command.ParserArgument.Type
 29 | 
 30 |   -- For lists, the first value is the default value.
 31 |   ---@class neotree.command.ParserArguments
 32 |   ---@field [string] neotree.command.ParserArgument
 33 |   ---@field values string[]
 34 |   local arguments = {
 35 |     action = {
 36 |       type = M.argtypes.LIST,
 37 |       values = {
 38 |         "close",
 39 |         "focus",
 40 |         "show",
 41 |       },
 42 |     },
 43 |     position = {
 44 |       type = M.argtypes.LIST,
 45 |       values = {
 46 |         "left",
 47 |         "right",
 48 |         "top",
 49 |         "bottom",
 50 |         "float",
 51 |         "current",
 52 |       },
 53 |     },
 54 |     source = {
 55 |       type = M.argtypes.LIST,
 56 |       values = source_names,
 57 |     },
 58 |     dir = { type = M.argtypes.PATH, stat_type = "directory" },
 59 |     reveal_file = { type = M.argtypes.PATH, stat_type = "file" },
 60 |     git_base = { type = M.argtypes.REF },
 61 |     toggle = { type = M.argtypes.FLAG },
 62 |     reveal = { type = M.argtypes.FLAG },
 63 |     reveal_force_cwd = { type = M.argtypes.FLAG },
 64 |     selector = { type = M.argtypes.FLAG },
 65 |   }
 66 | 
 67 |   local arg_type_lookup = {}
 68 |   local list_args = {}
 69 |   local path_args = {}
 70 |   local ref_args = {}
 71 |   local flag_args = {}
 72 |   local reverse_lookup = {}
 73 |   for name, def in pairs(arguments) do
 74 |     arg_type_lookup[name] = def.type
 75 |     if def.type == M.argtypes.LIST then
 76 |       table.insert(list_args, name)
 77 |       for _, vv in ipairs(def.values) do
 78 |         reverse_lookup[tostring(vv)] = name
 79 |       end
 80 |     elseif def.type == M.argtypes.PATH then
 81 |       table.insert(path_args, name)
 82 |     elseif def.type == M.argtypes.FLAG then
 83 |       table.insert(flag_args, name)
 84 |       reverse_lookup[name] = M.argtypes.FLAG
 85 |     elseif def.type == M.argtypes.REF then
 86 |       table.insert(ref_args, name)
 87 |     else
 88 |       error("Unknown type: " .. def.type)
 89 |     end
 90 |   end
 91 | 
 92 |   M.arguments = arguments
 93 |   M.list_args = list_args
 94 |   M.path_args = path_args
 95 |   M.ref_args = ref_args
 96 |   M.flag_args = flag_args
 97 |   M.argtype_lookup = arg_type_lookup
 98 |   M.reverse_lookup = reverse_lookup
 99 | end
100 | 
101 | ---@param path string
102 | ---@param validate_type string?
103 | M.resolve_path = function(path, validate_type)
104 |   path = vim.fs.normalize(path)
105 |   local expanded = vim.fn.expand(path)
106 |   local abs_path = vim.fn.fnamemodify(expanded, ":p")
107 |   if validate_type then
108 |     local stat = uv.fs_stat(abs_path)
109 |     if not stat or stat.type ~= validate_type then
110 |       error("Invalid path: " .. path .. " is not a " .. validate_type)
111 |     end
112 |   end
113 |   return abs_path
114 | end
115 | 
116 | ---@param ref string
117 | M.verify_git_ref = function(ref)
118 |   local ok, _ = utils.execute_command("git rev-parse --verify " .. ref)
119 |   return ok
120 | end
121 | 
122 | ---@class neotree.command.Parser.Parsed
123 | ---@field [string] string|boolean
124 | 
125 | ---@param result neotree.command.Parser.Parsed
126 | ---@param arg string
127 | local parse_arg = function(result, arg)
128 |   if type(arg) ~= "string" then
129 |     return
130 |   end
131 |   local eq = arg:find("=")
132 |   if eq then
133 |     local key = arg:sub(1, eq - 1)
134 |     local value = arg:sub(eq + 1)
135 |     local def = M.arguments[key]
136 |     if not def.type then
137 |       error("Invalid argument: " .. arg)
138 |     end
139 | 
140 |     if def.type == M.argtypes.PATH then
141 |       result[key] = M.resolve_path(value, def.stat_type)
142 |     elseif def.type == M.argtypes.FLAG then
143 |       if value == "true" then
144 |         result[key] = true
145 |       elseif value == "false" then
146 |         result[key] = false
147 |       else
148 |         error("Invalid value for " .. key .. ": " .. value)
149 |       end
150 |     elseif def.type == M.argtypes.REF then
151 |       if not M.verify_git_ref(value) then
152 |         error("Invalid value for " .. key .. ": " .. value)
153 |       end
154 |       result[key] = value
155 |     else
156 |       result[key] = value
157 |     end
158 |   else
159 |     local value = arg
160 |     local key = M.reverse_lookup[value]
161 |     if key == nil then
162 |       -- maybe it's a git ref
163 |       if M.verify_git_ref(value) then
164 |         result["git_base"] = value
165 |         return
166 |       end
167 |       -- maybe it's a path
168 |       local path = M.resolve_path(value)
169 |       local stat = uv.fs_stat(path)
170 |       if stat then
171 |         if stat.type == "directory" then
172 |           result["dir"] = path
173 |         elseif stat.type == "file" then
174 |           result["reveal_file"] = path
175 |         end
176 |       else
177 |         error("Invalid argument: " .. arg)
178 |       end
179 |     elseif key == M.argtypes.FLAG then
180 |       result[value] = true
181 |     else
182 |       result[key] = value
183 |     end
184 |   end
185 | end
186 | 
187 | ---@param args string|string[]
188 | ---@param strict_checking boolean
189 | ---@return neotree.command.Parser.Parsed parsed_args
190 | M.parse = function(args, strict_checking)
191 |   require("neo-tree").ensure_config()
192 |   local result = {}
193 | 
194 |   if type(args) == "string" then
195 |     args = utils.split(args, " ")
196 |   end
197 |   -- read args from user
198 |   for _, arg in ipairs(args) do
199 |     local success, err = pcall(parse_arg, result, arg)
200 |     if strict_checking and not success then
201 |       error(err)
202 |     end
203 |   end
204 | 
205 |   return result
206 | end
207 | 
208 | return M
209 | 


--------------------------------------------------------------------------------
/lua/neo-tree/events/init.lua:
--------------------------------------------------------------------------------
  1 | local q = require("neo-tree.events.queue")
  2 | local log = require("neo-tree.log")
  3 | local utils = require("neo-tree.utils")
  4 | 
  5 | ---@class neotree.event.Functions
  6 | local M = {
  7 |   -- Well known event names, you can make up your own
  8 |   AFTER_RENDER = "after_render",
  9 |   BEFORE_FILE_ADD = "before_file_add",
 10 |   BEFORE_FILE_DELETE = "before_file_delete",
 11 |   BEFORE_FILE_MOVE = "before_file_move",
 12 |   BEFORE_FILE_RENAME = "before_file_rename",
 13 |   BEFORE_RENDER = "before_render",
 14 |   FILE_ADDED = "file_added",
 15 |   FILE_DELETED = "file_deleted",
 16 |   FILE_MOVED = "file_moved",
 17 |   FILE_OPENED = "file_opened",
 18 |   FILE_OPEN_REQUESTED = "file_open_requested",
 19 |   FILE_RENAMED = "file_renamed",
 20 |   FS_EVENT = "fs_event",
 21 |   GIT_EVENT = "git_event",
 22 |   GIT_STATUS_CHANGED = "git_status_changed",
 23 |   STATE_CREATED = "state_created",
 24 |   NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter",
 25 |   NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave",
 26 |   NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update",
 27 |   NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter",
 28 |   NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave",
 29 |   NEO_TREE_POPUP_INPUT_READY = "neo_tree_popup_input_ready",
 30 |   NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close",
 31 |   NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open",
 32 |   NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close",
 33 |   NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open",
 34 |   NEO_TREE_PREVIEW_BEFORE_RENDER = "neo_tree_preview_before_render",
 35 |   VIM_AFTER_SESSION_LOAD = "vim_after_session_load",
 36 |   VIM_BUFFER_ADDED = "vim_buffer_added",
 37 |   VIM_BUFFER_CHANGED = "vim_buffer_changed",
 38 |   VIM_BUFFER_DELETED = "vim_buffer_deleted",
 39 |   VIM_BUFFER_ENTER = "vim_buffer_enter",
 40 |   VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set",
 41 |   VIM_COLORSCHEME = "vim_colorscheme",
 42 |   VIM_CURSOR_MOVED = "vim_cursor_moved",
 43 |   VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed",
 44 |   VIM_DIR_CHANGED = "vim_dir_changed",
 45 |   VIM_INSERT_LEAVE = "vim_insert_leave",
 46 |   VIM_LEAVE = "vim_leave",
 47 |   VIM_LSP_REQUEST = "vim_lsp_request",
 48 |   VIM_RESIZED = "vim_resized",
 49 |   VIM_TAB_CLOSED = "vim_tab_closed",
 50 |   VIM_TERMINAL_ENTER = "vim_terminal_enter",
 51 |   VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal",
 52 |   VIM_WIN_CLOSED = "vim_win_closed",
 53 |   VIM_WIN_ENTER = "vim_win_enter",
 54 | }
 55 | 
 56 | ---@param autocmds string
 57 | ---@return string event
 58 | ---@return string? pattern
 59 | local parse_autocmd_string = function(autocmds)
 60 |   local parsed = vim.split(autocmds, " ")
 61 |   return parsed[1], parsed[2]
 62 | end
 63 | 
 64 | ---@param event_name neotree.EventName|string
 65 | ---@param autocmds string[]
 66 | ---@param debounce_frequency integer?
 67 | ---@param seed_fn function?
 68 | ---@param nested boolean?
 69 | M.define_autocmd_event = function(event_name, autocmds, debounce_frequency, seed_fn, nested)
 70 |   log.debug("Defining autocmd event: %s", event_name)
 71 |   local augroup_name = "NeoTreeEvent_" .. event_name
 72 |   q.define_event(event_name, {
 73 |     setup = function()
 74 |       local augroup = vim.api.nvim_create_augroup(augroup_name, { clear = false })
 75 |       for _, autocmd in ipairs(autocmds) do
 76 |         local event, pattern = parse_autocmd_string(autocmd)
 77 |         log.trace("Registering autocmds on %s %s", event, pattern or "")
 78 |         vim.api.nvim_create_autocmd({ event }, {
 79 |           pattern = pattern or "*",
 80 |           group = augroup,
 81 |           nested = nested,
 82 |           callback = function(args)
 83 |             ---@class neotree.event.Autocmd.CallbackArgs : neotree._vim.api.keyset.create_autocmd.callback_args
 84 |             ---@field afile string
 85 |             local event_args = args --[[@as neotree._vim.api.keyset.create_autocmd.callback_args]]
 86 |             event_args.afile = args.file or ""
 87 |             M.fire_event(event_name, event_args)
 88 |           end,
 89 |         })
 90 |       end
 91 |     end,
 92 |     seed = seed_fn,
 93 |     teardown = function()
 94 |       log.trace("Teardown autocmds for ", event_name)
 95 |       vim.api.nvim_create_augroup(augroup_name, { clear = true })
 96 |     end,
 97 |     debounce_frequency = debounce_frequency,
 98 |     debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
 99 |   })
100 | end
101 | 
102 | M.clear_all_events = q.clear_all_events
103 | M.define_event = q.define_event
104 | M.destroy_event = q.destroy_event
105 | M.fire_event = q.fire_event
106 | 
107 | M.subscribe = q.subscribe
108 | M.unsubscribe = q.unsubscribe
109 | 
110 | return M
111 | 


--------------------------------------------------------------------------------
/lua/neo-tree/events/queue.lua:
--------------------------------------------------------------------------------
  1 | local utils = require("neo-tree.utils")
  2 | local log = require("neo-tree.log")
  3 | local Queue = require("neo-tree.collections").Queue
  4 | 
  5 | ---@type table<string, neotree.collections.Queue?>
  6 | local event_queues = {}
  7 | ---@type table <string, neotree.event.Definition?>
  8 | local event_definitions = {}
  9 | local M = {}
 10 | 
 11 | ---@class neotree.event.Handler.Result
 12 | ---@field handled boolean?
 13 | 
 14 | ---@class neotree.event.Handler
 15 | ---@field event neotree.EventName|string
 16 | ---@field handler fun(table?):(neotree.event.Handler.Result?)
 17 | ---@field id string?
 18 | 
 19 | local typecheck = require("neo-tree.health.typecheck")
 20 | local validate = typecheck.validate
 21 | ---@param event_handler neotree.event.Handler
 22 | local validate_event_handler = function(event_handler)
 23 |   return validate("event_handler", event_handler, function(eh)
 24 |     validate("event", eh.event, "string")
 25 |     validate("handler", eh.handler, "function")
 26 |   end)
 27 | end
 28 | 
 29 | M.clear_all_events = function()
 30 |   for event_name, queue in pairs(event_queues) do
 31 |     M.destroy_event(event_name)
 32 |   end
 33 |   event_queues = {}
 34 | end
 35 | 
 36 | ---@class neotree.event.Definition
 37 | ---@field teardown function?
 38 | ---@field setup function?
 39 | ---@field setup_was_run boolean?
 40 | 
 41 | ---@param event_name neotree.EventName|string
 42 | ---@param opts neotree.event.Definition
 43 | M.define_event = function(event_name, opts)
 44 |   local existing = event_definitions[event_name]
 45 |   if existing ~= nil then
 46 |     error("Event already defined: " .. event_name)
 47 |   end
 48 |   event_definitions[event_name] = opts
 49 | end
 50 | 
 51 | ---@param event_name neotree.EventName|string
 52 | ---@return boolean existed_and_destroyed
 53 | M.destroy_event = function(event_name)
 54 |   local existing = event_definitions[event_name]
 55 |   if existing == nil then
 56 |     return false
 57 |   end
 58 |   if existing.setup_was_run and type(existing.teardown) == "function" then
 59 |     local success, result = pcall(existing.teardown)
 60 |     if not success then
 61 |       error("Error in teardown for " .. event_name .. ": " .. result)
 62 |     end
 63 |     existing.setup_was_run = false
 64 |   end
 65 |   event_queues[event_name] = nil
 66 |   return true
 67 | end
 68 | 
 69 | ---@param event neotree.EventName|string
 70 | ---@param args table
 71 | local fire_event_internal = function(event, args)
 72 |   local queue = event_queues[event]
 73 |   if queue == nil then
 74 |     return nil
 75 |   end
 76 |   --log.trace("Firing event: ", event, " with args: ", args)
 77 | 
 78 |   if queue:is_empty() then
 79 |     --log.trace("Event queue is empty")
 80 |     return nil
 81 |   end
 82 |   local seed = utils.get_value(event_definitions, event .. ".seed")
 83 |   if seed ~= nil then
 84 |     local success, result = pcall(seed, args)
 85 |     if success and result then
 86 |       log.trace("Seed for " .. event .. " returned: " .. tostring(result))
 87 |     elseif success then
 88 |       log.trace("Seed for " .. event .. " returned falsy, cancelling event")
 89 |     else
 90 |       log.error("Error in seed function for " .. event .. ": " .. result)
 91 |     end
 92 |   end
 93 | 
 94 |   return queue:for_each(function(event_handler)
 95 |     local remove_node = event_handler == nil or event_handler.cancelled
 96 |     if not remove_node then
 97 |       local success, result = pcall(event_handler.handler, args)
 98 |       local id = event_handler.id or event_handler
 99 |       if success then
100 |         log.trace("Handler ", id, " for " .. event .. " called successfully.")
101 |       else
102 |         log.error(string.format("Error in event handler for event %s[%s]: %s", event, id, result))
103 |       end
104 |       if event_handler.once then
105 |         event_handler.cancelled = true
106 |         return true
107 |       end
108 |       return result
109 |     end
110 |   end)
111 | end
112 | 
113 | ---@param event neotree.EventName|string
114 | ---@param args any?
115 | M.fire_event = function(event, args)
116 |   local freq = utils.get_value(event_definitions, event .. ".debounce_frequency", 0, true)
117 |   local strategy = utils.get_value(event_definitions, event .. ".debounce_strategy", 0, true)
118 |   log.trace("Firing event: ", event, " with args: ", args)
119 |   if freq > 0 then
120 |     utils.debounce("EVENT_FIRED: " .. event, function()
121 |       fire_event_internal(event, args or {})
122 |     end, freq, strategy)
123 |   else
124 |     return fire_event_internal(event, args or {})
125 |   end
126 | end
127 | 
128 | ---@param event_handler neotree.event.Handler
129 | M.subscribe = function(event_handler)
130 |   validate_event_handler(event_handler)
131 | 
132 |   local queue = event_queues[event_handler.event]
133 |   if queue == nil then
134 |     log.debug("Creating queue for event: " .. event_handler.event)
135 |     queue = Queue:new()
136 |     local def = event_definitions[event_handler.event]
137 |     if def and type(def.setup) == "function" then
138 |       local success, result = pcall(def.setup)
139 |       if success then
140 |         def.setup_was_run = true
141 |         log.debug("Setup for event " .. event_handler.event .. " was run")
142 |       else
143 |         log.error("Error in setup for " .. event_handler.event .. ": " .. result)
144 |       end
145 |     end
146 |     event_queues[event_handler.event] = queue
147 |   end
148 |   log.debug("Adding event handler [", event_handler.id, "] for event: ", event_handler.event)
149 |   queue:add(event_handler)
150 | end
151 | 
152 | ---@param event_handler neotree.event.Handler
153 | M.unsubscribe = function(event_handler)
154 |   local queue = event_queues[event_handler.event]
155 |   if queue == nil then
156 |     return nil
157 |   end
158 |   queue:remove_by_id(event_handler.id or event_handler)
159 |   if queue:is_empty() then
160 |     M.destroy_event(event_handler.event)
161 |     event_queues[event_handler.event] = nil
162 |   else
163 |     event_queues[event_handler.event] = queue
164 |   end
165 | end
166 | 
167 | return M
168 | 


--------------------------------------------------------------------------------
/lua/neo-tree/git/ignored.lua:
--------------------------------------------------------------------------------
  1 | local Job = require("plenary.job")
  2 | local uv = vim.uv or vim.loop
  3 | 
  4 | local utils = require("neo-tree.utils")
  5 | local log = require("neo-tree.log")
  6 | local git_utils = require("neo-tree.git.utils")
  7 | 
  8 | local M = {}
  9 | local sep = utils.path_separator
 10 | 
 11 | ---@param ignored string[]
 12 | ---@param path string
 13 | ---@param _type neotree.Filetype
 14 | M.is_ignored = function(ignored, path, _type)
 15 |   if _type == "directory" and not utils.is_windows then
 16 |     path = path .. sep
 17 |   end
 18 | 
 19 |   return vim.tbl_contains(ignored, path)
 20 | end
 21 | 
 22 | local git_root_cache = {
 23 |   known_roots = {},
 24 |   dir_lookup = {},
 25 | }
 26 | local get_root_for_item = function(item)
 27 |   local dir = item.type == "directory" and item.path or item.parent_path
 28 |   if type(git_root_cache.dir_lookup[dir]) ~= "nil" then
 29 |     return git_root_cache.dir_lookup[dir]
 30 |   end
 31 |   --for _, root in ipairs(git_root_cache.known_roots) do
 32 |   --  if vim.startswith(dir, root) then
 33 |   --    git_root_cache.dir_lookup[dir] = root
 34 |   --    return root
 35 |   --  end
 36 |   --end
 37 |   local root = git_utils.get_repository_root(dir)
 38 |   if root then
 39 |     git_root_cache.dir_lookup[dir] = root
 40 |     table.insert(git_root_cache.known_roots, root)
 41 |   else
 42 |     git_root_cache.dir_lookup[dir] = false
 43 |   end
 44 |   return root
 45 | end
 46 | 
 47 | ---@param state neotree.State
 48 | ---@param items neotree.FileItem[]
 49 | M.mark_ignored = function(state, items, callback)
 50 |   local folders = {}
 51 |   log.trace("================================================================================")
 52 |   log.trace("IGNORED: mark_ignore BEGIN...")
 53 | 
 54 |   for _, item in ipairs(items) do
 55 |     local folder = utils.split_path(item.path)
 56 |     if folder then
 57 |       if not folders[folder] then
 58 |         folders[folder] = {}
 59 |       end
 60 |       table.insert(folders[folder], item.path)
 61 |     end
 62 |   end
 63 | 
 64 |   local function process_result(result)
 65 |     if utils.is_windows then
 66 |       --on Windows, git seems to return quotes and double backslash "path\\directory"
 67 |       result = vim.tbl_map(function(item)
 68 |         item = item:gsub("\\\\", "\\")
 69 |         return item
 70 |       end, result)
 71 |     else
 72 |       --check-ignore does not indicate directories the same as 'status' so we need to
 73 |       --add the trailing slash to the path manually if not on Windows.
 74 |       log.trace("IGNORED: Checking types of", #result, "items to see which ones are directories")
 75 |       for i, item in ipairs(result) do
 76 |         local stat = uv.fs_stat(item)
 77 |         if stat and stat.type == "directory" then
 78 |           result[i] = item .. sep
 79 |         end
 80 |       end
 81 |     end
 82 |     result = vim.tbl_map(function(item)
 83 |       -- remove leading and trailing " from git output
 84 |       item = item:gsub('^"', ""):gsub('"
#39;, "")
 85 |       -- convert octal encoded lines to utf-8
 86 |       item = git_utils.octal_to_utf8(item)
 87 |       return item
 88 |     end, result)
 89 |     return result
 90 |   end
 91 | 
 92 |   local function finalize(all_results)
 93 |     local show_gitignored = state.filtered_items and state.filtered_items.hide_gitignored == false
 94 |     log.trace("IGNORED: Comparing results to mark items as ignored:", show_gitignored)
 95 |     local ignored, not_ignored = 0, 0
 96 |     for _, item in ipairs(items) do
 97 |       if M.is_ignored(all_results, item.path, item.type) then
 98 |         item.filtered_by = item.filtered_by or {}
 99 |         item.filtered_by.gitignored = true
100 |         item.filtered_by.show_gitignored = show_gitignored
101 |         ignored = ignored + 1
102 |       else
103 |         not_ignored = not_ignored + 1
104 |       end
105 |     end
106 |     log.trace("IGNORED: mark_ignored is complete, ignored:", ignored, ", not ignored:", not_ignored)
107 |     log.trace("================================================================================")
108 |   end
109 | 
110 |   local all_results = {}
111 |   if type(callback) == "function" then
112 |     local jobs = {}
113 |     local running_jobs = 0
114 |     local job_count = 0
115 |     local completed_jobs = 0
116 | 
117 |     -- This is called when a job completes, and starts the next job if there are any left
118 |     -- or calls the callback if all jobs are complete.
119 |     -- It is also called once at the start to start the first 50 jobs.
120 |     --
121 |     -- This is done to avoid running too many jobs at once, which can cause a crash from
122 |     -- having too many open files.
123 |     local run_more_jobs = function()
124 |       while #jobs > 0 and running_jobs < 50 and job_count > completed_jobs do
125 |         local next_job = table.remove(jobs, #jobs)
126 |         next_job:start()
127 |         running_jobs = running_jobs + 1
128 |       end
129 | 
130 |       if completed_jobs == job_count then
131 |         finalize(all_results)
132 |         callback(all_results)
133 |       end
134 |     end
135 | 
136 |     for folder, folder_items in pairs(folders) do
137 |       local args = { "-C", folder, "check-ignore", "--stdin" }
138 |       ---@diagnostic disable-next-line: missing-fields
139 |       local job = Job:new({
140 |         command = "git",
141 |         args = args,
142 |         enabled_recording = true,
143 |         writer = folder_items,
144 |         on_start = function()
145 |           log.trace("IGNORED: Running async git with args: ", args)
146 |         end,
147 |         on_exit = function(self, code, _)
148 |           local result
149 |           if code ~= 0 then
150 |             log.debug("Failed to load ignored files for", folder, ":", self:stderr_result())
151 |             result = {}
152 |           else
153 |             result = self:result()
154 |           end
155 |           vim.list_extend(all_results, process_result(result))
156 | 
157 |           running_jobs = running_jobs - 1
158 |           completed_jobs = completed_jobs + 1
159 |           run_more_jobs()
160 |         end,
161 |       })
162 |       table.insert(jobs, job)
163 |       job_count = job_count + 1
164 |     end
165 | 
166 |     run_more_jobs()
167 |   else
168 |     for folder, folder_items in pairs(folders) do
169 |       local cmd = { "git", "-C", folder, "check-ignore", unpack(folder_items) }
170 |       log.trace("IGNORED: Running cmd: ", cmd)
171 |       local result = vim.fn.systemlist(cmd)
172 |       if vim.v.shell_error == 128 then
173 |         log.debug("Failed to load ignored files for", state.path, ":", result)
174 |         result = {}
175 |       end
176 |       vim.list_extend(all_results, process_result(result))
177 |     end
178 |     finalize(all_results)
179 |     return all_results
180 |   end
181 | end
182 | 
183 | return M
184 | 


--------------------------------------------------------------------------------
/lua/neo-tree/git/init.lua:
--------------------------------------------------------------------------------
 1 | local status = require("neo-tree.git.status")
 2 | local ignored = require("neo-tree.git.ignored")
 3 | local git_utils = require("neo-tree.git.utils")
 4 | 
 5 | local M = {
 6 |   get_repository_root = git_utils.get_repository_root,
 7 |   is_ignored = ignored.is_ignored,
 8 |   mark_ignored = ignored.mark_ignored,
 9 |   status = status.status,
10 |   status_async = status.status_async,
11 | }
12 | 
13 | return M
14 | 


--------------------------------------------------------------------------------
/lua/neo-tree/git/utils.lua:
--------------------------------------------------------------------------------
 1 | local Job = require("plenary.job")
 2 | 
 3 | local utils = require("neo-tree.utils")
 4 | local log = require("neo-tree.log")
 5 | 
 6 | local M = {}
 7 | 
 8 | M.get_repository_root = function(path, callback)
 9 |   local args = { "rev-parse", "--show-toplevel" }
10 |   if utils.truthy(path) then
11 |     args = { "-C", path, "rev-parse", "--show-toplevel" }
12 |   end
13 |   if type(callback) == "function" then
14 |     ---@diagnostic disable-next-line: missing-fields
15 |     Job:new({
16 |       command = "git",
17 |       args = args,
18 |       enabled_recording = true,
19 |       on_exit = function(self, code, _)
20 |         if code ~= 0 then
21 |           log.trace("GIT ROOT ERROR ", self:stderr_result())
22 |           callback(nil)
23 |           return
24 |         end
25 |         local git_root = self:result()[1]
26 | 
27 |         if utils.is_windows then
28 |           git_root = utils.windowize_path(git_root)
29 |         end
30 | 
31 |         log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
32 |         callback(git_root)
33 |       end,
34 |     }):start()
35 |   else
36 |     local ok, git_output = utils.execute_command({ "git", unpack(args) })
37 |     if not ok then
38 |       log.trace("GIT ROOT ERROR ", git_output)
39 |       return nil
40 |     end
41 |     local git_root = git_output[1]
42 | 
43 |     if utils.is_windows then
44 |       git_root = utils.windowize_path(git_root)
45 |     end
46 | 
47 |     log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
48 |     return git_root
49 |   end
50 | end
51 | 
52 | local convert_octal_char = function(octal)
53 |   return string.char(tonumber(octal, 8))
54 | end
55 | 
56 | M.octal_to_utf8 = function(text)
57 |   -- git uses octal encoding for utf-8 filepaths, convert octal back to utf-8
58 |   local success, converted = pcall(string.gsub, text, "\\([0-7][0-7][0-7])", convert_octal_char)
59 |   if success then
60 |     return converted
61 |   else
62 |     return text
63 |   end
64 | end
65 | 
66 | return M
67 | 


--------------------------------------------------------------------------------
/lua/neo-tree/log.lua:
--------------------------------------------------------------------------------
  1 | -- log.lua
  2 | --
  3 | -- Inspired by rxi/log.lua
  4 | -- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
  5 | --
  6 | -- This library is free software; you can redistribute it and/or modify it
  7 | -- under the terms of the MIT license. See LICENSE for details.
  8 | 
  9 | -- User configuration section
 10 | local default_config = {
 11 |   -- Name of the plugin. Prepended to log messages
 12 |   plugin = "neo-tree.nvim",
 13 | 
 14 |   -- Should print the output to neovim while running
 15 |   use_console = true,
 16 | 
 17 |   -- Should highlighting be used in console (using echohl)
 18 |   highlights = true,
 19 | 
 20 |   -- Should write to a file
 21 |   use_file = false,
 22 | 
 23 |   -- Any messages above this level will be logged.
 24 |   level = "info",
 25 | 
 26 |   -- Level configuration
 27 |   modes = {
 28 |     { name = "trace", hl = "None", level = vim.log.levels.TRACE },
 29 |     { name = "debug", hl = "None", level = vim.log.levels.DEBUG },
 30 |     { name = "info", hl = "None", level = vim.log.levels.INFO },
 31 |     { name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN },
 32 |     { name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR },
 33 |     { name = "fatal", hl = "ErrorMsg", level = vim.log.levels.ERROR },
 34 |   },
 35 | 
 36 |   -- Can limit the number of decimals displayed for floats
 37 |   float_precision = 0.01,
 38 | }
 39 | 
 40 | -- {{{ NO NEED TO CHANGE
 41 | local log = {}
 42 | 
 43 | local unpack = unpack
 44 | 
 45 | local notify = function(message, level_config)
 46 |   if type(vim.notify) == "table" then
 47 |     -- probably using nvim-notify
 48 |     vim.notify(message, level_config.level, { title = "Neo-tree" })
 49 |   else
 50 |     local nameupper = level_config.name:upper()
 51 |     local console_string = string.format("[Neo-tree %s] %s", nameupper, message)
 52 |     vim.notify(console_string, level_config.level)
 53 |   end
 54 | end
 55 | 
 56 | log.new = function(config, standalone)
 57 |   config = vim.tbl_deep_extend("force", default_config, config)
 58 | 
 59 |   local outfile =
 60 |     string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "data" }), config.plugin)
 61 | 
 62 |   local obj
 63 |   if standalone then
 64 |     obj = log
 65 |   else
 66 |     obj = {}
 67 |   end
 68 |   obj.outfile = outfile
 69 | 
 70 |   obj.use_file = function(file, quiet)
 71 |     if file == false then
 72 |       if not quiet then
 73 |         obj.info("[neo-tree] Logging to file disabled")
 74 |       end
 75 |       config.use_file = false
 76 |     else
 77 |       if type(file) == "string" then
 78 |         obj.outfile = file
 79 |       else
 80 |         obj.outfile = outfile
 81 |       end
 82 |       config.use_file = true
 83 |       if not quiet then
 84 |         obj.info("[neo-tree] Logging to file: " .. obj.outfile)
 85 |       end
 86 |     end
 87 |   end
 88 | 
 89 |   local levels = {}
 90 |   for i, v in ipairs(config.modes) do
 91 |     levels[v.name] = i
 92 |   end
 93 | 
 94 |   obj.set_level = function(level)
 95 |     if levels[level] then
 96 |       if config.level ~= level then
 97 |         config.level = level
 98 |       end
 99 |     else
100 |       notify("Invalid log level: " .. level, config.modes[5])
101 |     end
102 |   end
103 | 
104 |   local round = function(x, increment)
105 |     increment = increment or 1
106 |     x = x / increment
107 |     return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
108 |   end
109 | 
110 |   local make_string = function(...)
111 |     local t = {}
112 |     for i = 1, select("#", ...) do
113 |       local x = select(i, ...)
114 | 
115 |       if type(x) == "number" and config.float_precision then
116 |         x = tostring(round(x, config.float_precision))
117 |       elseif type(x) == "table" then
118 |         x = vim.inspect(x)
119 |         if #x > 300 then
120 |           x = x:sub(1, 300) .. "..."
121 |         end
122 |       else
123 |         x = tostring(x)
124 |       end
125 | 
126 |       t[#t + 1] = x
127 |     end
128 |     return table.concat(t, " ")
129 |   end
130 | 
131 |   local log_at_level = function(level, level_config, message_maker, ...)
132 |     -- Return early if we're below the config.level
133 |     if level < levels[config.level] then
134 |       return
135 |     end
136 |     -- Ignore this if vim is exiting
137 |     if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
138 |       return
139 |     end
140 |     local nameupper = level_config.name:upper()
141 | 
142 |     local msg = message_maker(...)
143 |     local info = debug.getinfo(2, "Sl")
144 |     local lineinfo = info.short_src .. ":" .. info.currentline
145 | 
146 |     -- Output to log file
147 |     if config.use_file then
148 |       local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
149 |       local fp = io.open(obj.outfile, "a")
150 |       if fp then
151 |         fp:write(str)
152 |         fp:close()
153 |       else
154 |         print("[neo-tree] Could not open log file: " .. obj.outfile)
155 |       end
156 |     end
157 | 
158 |     -- Output to console
159 |     if config.use_console and level > 2 then
160 |       vim.schedule(function()
161 |         notify(msg, level_config)
162 |       end)
163 |     end
164 |   end
165 | 
166 |   for i, x in ipairs(config.modes) do
167 |     obj[x.name] = function(...)
168 |       return log_at_level(i, x, make_string, ...)
169 |     end
170 | 
171 |     obj[("fmt_%s"):format(x.name)] = function()
172 |       return log_at_level(i, x, function(...)
173 |         local passed = { ... }
174 |         local fmt = table.remove(passed, 1)
175 |         local inspected = {}
176 |         for _, v in ipairs(passed) do
177 |           table.insert(inspected, vim.inspect(v))
178 |         end
179 |         return string.format(fmt, unpack(inspected))
180 |       end)
181 |     end
182 |   end
183 | end
184 | 
185 | log.new(default_config, true)
186 | -- }}}
187 | 
188 | return log
189 | 


--------------------------------------------------------------------------------
/lua/neo-tree/setup/deprecations.lua:
--------------------------------------------------------------------------------
  1 | local utils = require("neo-tree.utils")
  2 | 
  3 | local M = {}
  4 | 
  5 | local migrations = {}
  6 | 
  7 | M.show_migrations = function()
  8 |   if #migrations > 0 then
  9 |     local content = {}
 10 |     for _, message in ipairs(migrations) do
 11 |       vim.list_extend(content, vim.split("\n## " .. message, "\n", { trimempty = false }))
 12 |     end
 13 |     local header = "# Neo-tree configuration has been updated. Please review the changes below."
 14 |     table.insert(content, 1, header)
 15 |     local buf = vim.api.nvim_create_buf(false, true)
 16 |     vim.api.nvim_buf_set_lines(buf, 0, -1, false, content)
 17 |     vim.bo[buf].buftype = "nofile"
 18 |     vim.bo[buf].bufhidden = "wipe"
 19 |     vim.bo[buf].buflisted = false
 20 |     vim.bo[buf].swapfile = false
 21 |     vim.bo[buf].modifiable = false
 22 |     vim.bo[buf].filetype = "markdown"
 23 |     vim.api.nvim_buf_set_name(buf, "Neo-tree migrations")
 24 |     vim.defer_fn(function()
 25 |       vim.cmd(string.format("%ssplit", #content))
 26 |       vim.api.nvim_win_set_buf(0, buf)
 27 |     end, 100)
 28 |   end
 29 | end
 30 | 
 31 | ---@param config neotree.Config.Base
 32 | M.migrate = function(config)
 33 |   migrations = {}
 34 | 
 35 |   local moved = function(old, new, converter)
 36 |     local existing = utils.get_value(config, old)
 37 |     if type(existing) ~= "nil" then
 38 |       if type(converter) == "function" then
 39 |         existing = converter(existing)
 40 |       end
 41 |       utils.set_value(config, old, nil)
 42 |       utils.set_value(config, new, existing)
 43 |       migrations[#migrations + 1] =
 44 |         string.format("The `%s` option has been deprecated, please use `%s` instead.", old, new)
 45 |     end
 46 |   end
 47 | 
 48 |   local moved_inside = function(old, new_inside, converter)
 49 |     local existing = utils.get_value(config, old)
 50 |     if type(existing) ~= "nil" and type(existing) ~= "table" then
 51 |       if type(converter) == "function" then
 52 |         existing = converter(existing)
 53 |       end
 54 |       utils.set_value(config, old, {})
 55 |       local new = old .. "." .. new_inside
 56 |       utils.set_value(config, new, existing)
 57 |       migrations[#migrations + 1] =
 58 |         string.format("The `%s` option is replaced with a table, please move to `%s`.", old, new)
 59 |     end
 60 |   end
 61 | 
 62 |   local removed = function(key, desc)
 63 |     local value = utils.get_value(config, key)
 64 |     if type(value) ~= "nil" then
 65 |       utils.set_value(config, key, nil)
 66 |       migrations[#migrations + 1] =
 67 |         string.format("The `%s` option has been removed.\n%s", key, desc or "")
 68 |     end
 69 |   end
 70 | 
 71 |   local renamed_value = function(key, old_value, new_value)
 72 |     local value = utils.get_value(config, key)
 73 |     if value == old_value then
 74 |       utils.set_value(config, key, new_value)
 75 |       migrations[#migrations + 1] =
 76 |         string.format("The `%s=%s` option has been renamed to `%s`.", key, old_value, new_value)
 77 |     end
 78 |   end
 79 | 
 80 |   local opposite = function(value)
 81 |     return not value
 82 |   end
 83 | 
 84 |   local tab_to_source_migrator = function(labels)
 85 |     local converted_sources = {}
 86 |     for entry, label in pairs(labels) do
 87 |       table.insert(converted_sources, { source = entry, display_name = label })
 88 |     end
 89 |     return converted_sources
 90 |   end
 91 | 
 92 |   moved("filesystem.filters", "filesystem.filtered_items")
 93 |   moved("filesystem.filters.show_hidden", "filesystem.filtered_items.hide_dotfiles", opposite)
 94 |   moved("filesystem.filters.respect_gitignore", "filesystem.filtered_items.hide_gitignored")
 95 |   moved("open_files_do_not_replace_filetypes", "open_files_do_not_replace_types")
 96 |   moved("source_selector.tab_labels", "source_selector.sources", tab_to_source_migrator)
 97 |   removed("filesystem.filters.gitignore_source")
 98 |   removed("filesystem.filter_items.gitignore_source")
 99 |   renamed_value("filesystem.hijack_netrw_behavior", "open_split", "open_current")
100 |   for _, source in ipairs({ "filesystem", "buffers", "git_status" }) do
101 |     renamed_value(source .. "window.position", "split", "current")
102 |   end
103 |   moved_inside("filesystem.follow_current_file", "enabled")
104 |   moved_inside("buffers.follow_current_file", "enabled")
105 | 
106 |   -- v3.x
107 |   removed("close_floats_on_escape_key")
108 | 
109 |   -- v4.x
110 |   removed(
111 |     "enable_normal_mode_for_inputs",
112 |     [[
113 | Please use `neo_tree_popup_input_ready` event instead and call `stopinsert` inside the handler.
114 | <https://github.com/nvim-neo-tree/neo-tree.nvim/pull/1372>
115 | 
116 | See instructions in `:h neo-tree-events` for more details.
117 | 
118 | ```lua
119 | event_handlers = {
120 |   {
121 |     event = "neo_tree_popup_input_ready",
122 |     ---@param args { bufnr: integer, winid: integer }
123 |     handler = function(args)
124 |       vim.cmd("stopinsert")
125 |       vim.keymap.set("i", "<esc>", vim.cmd.stopinsert, { noremap = true, buffer = args.bufnr })
126 |     end,
127 |   }
128 | }
129 | ```
130 | ]]
131 |   )
132 | 
133 |   return migrations
134 | end
135 | 
136 | return M
137 | 


--------------------------------------------------------------------------------
/lua/neo-tree/setup/mapping-helper.lua:
--------------------------------------------------------------------------------
 1 | local utils = require("neo-tree.utils")
 2 | 
 3 | local M = {}
 4 | 
 5 | ---@param key string
 6 | M.normalize_map_key = function(key)
 7 |   if key == nil then
 8 |     return nil
 9 |   end
10 |   if key:match("^<[^>]+>
quot;) then
11 |     local parts = utils.split(key, "-")
12 |     if #parts == 2 then
13 |       local mod = parts[1]:lower()
14 |       if mod == "<a" then
15 |         mod = "<m"
16 |       end
17 |       local alpha = parts[2]
18 |       if #alpha > 2 then
19 |         alpha = alpha:lower()
20 |       end
21 |       key = string.format("%s-%s", mod, alpha)
22 |       return key
23 |     else
24 |       key = key:lower()
25 |       if key == "<backspace>" then
26 |         return "<bs>"
27 |       elseif key == "<enter>" then
28 |         return "<cr>"
29 |       elseif key == "<return>" then
30 |         return "<cr>"
31 |       end
32 |     end
33 |   end
34 |   return key
35 | end
36 | 
37 | ---@class neotree.SimpleMappings
38 | ---@field [string] string|function?
39 | 
40 | ---@class neotree.SimpleMappingsByMode
41 | ---@field [string] neotree.SimpleMappings?
42 | 
43 | ---@class neotree.Mappings : neotree.SimpleMappings
44 | ---@field [integer] neotree.SimpleMappingsByMode?
45 | 
46 | ---@param map neotree.Mappings
47 | ---@return neotree.Mappings new_map
48 | M.normalize_mappings = function(map)
49 |   local new_map = M.normalize_simple_mappings(map)
50 |   ---@cast new_map neotree.Mappings
51 |   for i, mappings_by_mode in ipairs(map) do
52 |     new_map[i] = {}
53 |     for mode, simple_mappings in pairs(mappings_by_mode) do
54 |       ---@cast simple_mappings neotree.SimpleMappings
55 |       new_map[i][mode] = M.normalize_simple_mappings(simple_mappings)
56 |     end
57 |   end
58 |   return new_map
59 | end
60 | 
61 | ---@param map neotree.SimpleMappings
62 | ---@return neotree.SimpleMappings new_map
63 | M.normalize_simple_mappings = function(map)
64 |   local new_map = {}
65 |   for key, value in pairs(map) do
66 |     if type(key) == "string" then
67 |       local normalized_key = M.normalize_map_key(key)
68 |       if normalized_key ~= nil then
69 |         new_map[normalized_key] = value
70 |       end
71 |     end
72 |   end
73 |   return new_map
74 | end
75 | 
76 | return M
77 | 


--------------------------------------------------------------------------------
/lua/neo-tree/setup/netrw.lua:
--------------------------------------------------------------------------------
  1 | local uv = vim.uv or vim.loop
  2 | local nt = require("neo-tree")
  3 | local utils = require("neo-tree.utils")
  4 | local M = {}
  5 | 
  6 | local get_position = function(source_name)
  7 |   local pos = utils.get_value(nt.config, source_name .. ".window.position", "left", true)
  8 |   return pos
  9 | end
 10 | 
 11 | ---@return neotree.Config.HijackNetrwBehavior
 12 | M.get_hijack_behavior = function()
 13 |   nt.ensure_config()
 14 |   return nt.config.filesystem.hijack_netrw_behavior
 15 | end
 16 | 
 17 | ---@return boolean hijacked Whether the hijack was successful
 18 | M.hijack = function()
 19 |   local hijack_behavior = M.get_hijack_behavior()
 20 |   if hijack_behavior == "disabled" then
 21 |     return false
 22 |   end
 23 | 
 24 |   -- ensure this is a directory
 25 |   local dir_bufnr = vim.api.nvim_get_current_buf()
 26 |   local path_to_hijack = vim.api.nvim_buf_get_name(dir_bufnr)
 27 |   local stats = uv.fs_stat(path_to_hijack)
 28 |   if not stats or stats.type ~= "directory" then
 29 |     return false
 30 |   end
 31 | 
 32 |   -- record where we are now
 33 |   local pos = get_position("filesystem")
 34 |   local should_open_current = hijack_behavior == "open_current" or pos == "current"
 35 |   local dir_window = vim.api.nvim_get_current_win()
 36 | 
 37 |   -- Now actually open the tree, with a very quick debounce because this may be
 38 |   -- called multiple times in quick succession.
 39 |   utils.debounce("hijack_netrw_" .. dir_window, function()
 40 |     local manager = require("neo-tree.sources.manager")
 41 |     local log = require("neo-tree.log")
 42 |     -- We will want to replace the "directory" buffer with either the "alternate"
 43 |     -- buffer or a new blank one.
 44 |     local replacement_buffer = vim.fn.bufnr("#")
 45 |     local is_currently_neo_tree = false
 46 |     if replacement_buffer > 0 then
 47 |       if vim.bo[replacement_buffer].filetype == "neo-tree" then
 48 |         -- don't hijack the current window if it's already a Neo-tree sidebar
 49 |         local position = vim.b[replacement_buffer].neo_tree_position
 50 |         if position == "current" then
 51 |           replacement_buffer = -1
 52 |         else
 53 |           is_currently_neo_tree = true
 54 |         end
 55 |       end
 56 |     end
 57 |     if not should_open_current then
 58 |       if replacement_buffer == dir_bufnr or replacement_buffer < 1 then
 59 |         replacement_buffer = vim.api.nvim_create_buf(true, false)
 60 |         log.trace("Created new buffer for netrw hijack", replacement_buffer)
 61 |       end
 62 |     end
 63 |     if replacement_buffer > 0 then
 64 |       log.trace("Replacing buffer in netrw hijack", replacement_buffer)
 65 |       pcall(vim.api.nvim_win_set_buf, dir_window, replacement_buffer)
 66 |     end
 67 | 
 68 |     -- If a window takes focus (e.g. lazy.nvim installing plugins on startup) in the time between the method call and
 69 |     -- this debounced callback, we should focus that window over neo-tree.
 70 |     local current_window = vim.api.nvim_get_current_win()
 71 |     local should_restore_cursor = current_window ~= dir_window
 72 | 
 73 |     local cleanup = vim.schedule_wrap(function()
 74 |       log.trace("Deleting buffer in netrw hijack", dir_bufnr)
 75 |       pcall(vim.api.nvim_buf_delete, dir_bufnr, { force = true })
 76 |       if should_restore_cursor then
 77 |         vim.api.nvim_set_current_win(current_window)
 78 |       end
 79 |     end)
 80 | 
 81 |     ---@type neotree.sources.filesystem.State
 82 |     local state
 83 |     if should_open_current and not is_currently_neo_tree then
 84 |       log.debug("hijack_netrw: opening current")
 85 |       state = manager.get_state("filesystem", nil, dir_window) --[[@as neotree.sources.filesystem.State]]
 86 |       state.current_position = "current"
 87 |     elseif is_currently_neo_tree then
 88 |       log.debug("hijack_netrw: opening in existing Neo-tree")
 89 |       state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]]
 90 |     else
 91 |       log.debug("hijack_netrw: opening default")
 92 |       manager.close_all_except("filesystem")
 93 |       state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]]
 94 |     end
 95 | 
 96 |     require("neo-tree.sources.filesystem")._navigate_internal(state, path_to_hijack, nil, cleanup)
 97 |   end, 10, utils.debounce_strategy.CALL_LAST_ONLY)
 98 | 
 99 |   return true
100 | end
101 | 
102 | return M
103 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/buffers/commands.lua:
--------------------------------------------------------------------------------
  1 | --This file should contain all commands meant to be used by mappings.
  2 | 
  3 | local cc = require("neo-tree.sources.common.commands")
  4 | local buffers = require("neo-tree.sources.buffers")
  5 | local utils = require("neo-tree.utils")
  6 | local manager = require("neo-tree.sources.manager")
  7 | 
  8 | ---@class neotree.sources.Buffers.Commands : neotree.sources.Common.Commands
  9 | local M = {}
 10 | 
 11 | local refresh = utils.wrap(manager.refresh, "buffers")
 12 | local redraw = utils.wrap(manager.redraw, "buffers")
 13 | 
 14 | M.add = function(state)
 15 |   cc.add(state, refresh)
 16 | end
 17 | 
 18 | M.add_directory = function(state)
 19 |   cc.add_directory(state, refresh)
 20 | end
 21 | 
 22 | M.buffer_delete = function(state)
 23 |   local node = state.tree:get_node()
 24 |   if node then
 25 |     if node.type == "message" then
 26 |       return
 27 |     end
 28 |     vim.api.nvim_buf_delete(node.extra.bufnr, { force = false, unload = false })
 29 |     refresh()
 30 |   end
 31 | end
 32 | 
 33 | ---Marks node as copied, so that it can be pasted somewhere else.
 34 | M.copy_to_clipboard = function(state)
 35 |   cc.copy_to_clipboard(state, redraw)
 36 | end
 37 | 
 38 | ---@type neotree.TreeCommandVisual
 39 | M.copy_to_clipboard_visual = function(state, selected_nodes)
 40 |   cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
 41 | end
 42 | 
 43 | ---Marks node as cut, so that it can be pasted (moved) somewhere else.
 44 | M.cut_to_clipboard = function(state)
 45 |   cc.cut_to_clipboard(state, redraw)
 46 | end
 47 | 
 48 | ---@type neotree.TreeCommandVisual
 49 | M.cut_to_clipboard_visual = function(state, selected_nodes)
 50 |   cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
 51 | end
 52 | 
 53 | M.copy = function(state)
 54 |   cc.copy(state, redraw)
 55 | end
 56 | 
 57 | M.move = function(state)
 58 |   cc.move(state, redraw)
 59 | end
 60 | 
 61 | M.show_debug_info = cc.show_debug_info
 62 | 
 63 | ---Pastes all items from the clipboard to the current directory.
 64 | M.paste_from_clipboard = function(state)
 65 |   cc.paste_from_clipboard(state, refresh)
 66 | end
 67 | 
 68 | M.delete = function(state)
 69 |   cc.delete(state, refresh)
 70 | end
 71 | 
 72 | ---Navigate up one level.
 73 | M.navigate_up = function(state)
 74 |   local parent_path, _ = utils.split_path(state.path)
 75 |   buffers.navigate(state, parent_path)
 76 | end
 77 | 
 78 | M.refresh = refresh
 79 | 
 80 | M.rename = function(state)
 81 |   cc.rename(state, refresh)
 82 | end
 83 | 
 84 | M.set_root = function(state)
 85 |   local node = state.tree:get_node()
 86 |   while node and node.type ~= "directory" do
 87 |     local parent_id = node:get_parent_id()
 88 |     node = parent_id and state.tree:get_node(parent_id) or nil
 89 |   end
 90 | 
 91 |   if not node then
 92 |     return
 93 |   end
 94 | 
 95 |   buffers.navigate(state, node:get_id())
 96 | end
 97 | 
 98 | cc._add_common_commands(M)
 99 | 
100 | return M
101 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/buffers/components.lua:
--------------------------------------------------------------------------------
 1 | -- This file contains the built-in components. Each componment is a function
 2 | -- that takes the following arguments:
 3 | --      config: A table containing the configuration provided by the user
 4 | --              when declaring this component in their renderer config.
 5 | --      node:   A NuiNode object for the currently focused node.
 6 | --      state:  The current state of the source providing the items.
 7 | --
 8 | -- The function should return either a table, or a list of tables, each of which
 9 | -- contains the following keys:
10 | --    text:      The text to display for this item.
11 | --    highlight: The highlight group to apply to this text.
12 | 
13 | local highlights = require("neo-tree.ui.highlights")
14 | local common = require("neo-tree.sources.common.components")
15 | local utils = require("neo-tree.utils")
16 | 
17 | ---@alias neotree.Component.Buffers._Key
18 | ---|"name"
19 | 
20 | ---@class neotree.Component.Buffers
21 | ---@field [1] neotree.Component.Buffers._Key|neotree.Component.Common._Key
22 | 
23 | ---@type table<neotree.Component.Buffers._Key, neotree.Renderer>
24 | local M = {}
25 | 
26 | ---@class (exact) neotree.Component.Buffers.Name : neotree.Component.Common.Name
27 | 
28 | ---@param config neotree.Component.Buffers.Name
29 | M.name = function(config, node, state)
30 |   local highlight = config.highlight or highlights.FILE_NAME_OPENED
31 |   local name = node.name
32 |   if node.type == "directory" then
33 |     if node:get_depth() == 1 then
34 |       highlight = highlights.ROOT_NAME
35 |       name = "OPEN BUFFERS in " .. name
36 |     else
37 |       highlight = highlights.DIRECTORY_NAME
38 |     end
39 |   elseif node.type == "terminal" then
40 |     if node:get_depth() == 1 then
41 |       highlight = highlights.ROOT_NAME
42 |       name = "TERMINALS"
43 |     else
44 |       highlight = highlights.FILE_NAME
45 |     end
46 |   elseif config.use_git_status_colors then
47 |     local git_status = state.components.git_status({}, node, state)
48 |     if git_status and git_status.highlight then
49 |       highlight = git_status.highlight
50 |     end
51 |   end
52 |   return {
53 |     text = name,
54 |     highlight = highlight,
55 |   }
56 | end
57 | 
58 | return vim.tbl_deep_extend("force", common, M)
59 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/buffers/init.lua:
--------------------------------------------------------------------------------
  1 | --This file should have all functions that are in the public api and either set
  2 | --or read the state of this source.
  3 | 
  4 | local utils = require("neo-tree.utils")
  5 | local renderer = require("neo-tree.ui.renderer")
  6 | local items = require("neo-tree.sources.buffers.lib.items")
  7 | local events = require("neo-tree.events")
  8 | local manager = require("neo-tree.sources.manager")
  9 | local git = require("neo-tree.git")
 10 | 
 11 | ---@class neotree.sources.Buffers : neotree.Source
 12 | local M = {
 13 |   name = "buffers",
 14 |   display_name = " 󰈚 Buffers ",
 15 | }
 16 | 
 17 | local wrap = function(func)
 18 |   return utils.wrap(func, M.name)
 19 | end
 20 | 
 21 | local get_state = function()
 22 |   return manager.get_state(M.name)
 23 | end
 24 | 
 25 | local follow_internal = function()
 26 |   if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then
 27 |     return
 28 |   end
 29 |   local bufnr = vim.api.nvim_get_current_buf()
 30 |   local path_to_reveal = manager.get_path_to_reveal(true) or tostring(bufnr)
 31 | 
 32 |   local state = get_state()
 33 |   if state.current_position == "float" then
 34 |     return false
 35 |   end
 36 |   if not state.path then
 37 |     return false
 38 |   end
 39 |   local window_exists = renderer.window_exists(state)
 40 |   if window_exists then
 41 |     local node = state.tree and state.tree:get_node()
 42 |     if node then
 43 |       if node:get_id() == path_to_reveal then
 44 |         -- already focused
 45 |         return false
 46 |       end
 47 |     end
 48 |     renderer.focus_node(state, path_to_reveal, true)
 49 |   end
 50 | end
 51 | 
 52 | M.follow = function()
 53 |   if vim.fn.bufname(0) == "COMMIT_EDITMSG" then
 54 |     return false
 55 |   end
 56 |   utils.debounce("neo-tree-buffer-follow", function()
 57 |     return follow_internal()
 58 |   end, 100, utils.debounce_strategy.CALL_LAST_ONLY)
 59 | end
 60 | 
 61 | local buffers_changed_internal = function()
 62 |   for _, tabid in ipairs(vim.api.nvim_list_tabpages()) do
 63 |     local state = manager.get_state(M.name, tabid)
 64 |     if state.path and renderer.window_exists(state) then
 65 |       items.get_opened_buffers(state)
 66 |       if state.follow_current_file.enabled then
 67 |         follow_internal()
 68 |       end
 69 |     end
 70 |   end
 71 | end
 72 | 
 73 | ---Calld by autocmd when any buffer is open, closed, renamed, etc.
 74 | M.buffers_changed = function()
 75 |   utils.debounce(
 76 |     "buffers_changed",
 77 |     buffers_changed_internal,
 78 |     100,
 79 |     utils.debounce_strategy.CALL_LAST_ONLY
 80 |   )
 81 | end
 82 | 
 83 | ---Navigate to the given path.
 84 | ---@param state neotree.State
 85 | ---@param path string? Path to navigate to. If empty, will navigate to the cwd.
 86 | ---@param path_to_reveal string?
 87 | ---@param callback function?
 88 | ---@param async boolean?
 89 | M.navigate = function(state, path, path_to_reveal, callback, async)
 90 |   state.dirty = false
 91 |   local path_changed = false
 92 |   if path == nil then
 93 |     path = vim.fn.getcwd()
 94 |   end
 95 |   if path ~= state.path then
 96 |     state.path = path
 97 |     path_changed = true
 98 |   end
 99 |   if path_to_reveal then
100 |     renderer.position.set(state, path_to_reveal)
101 |   end
102 | 
103 |   items.get_opened_buffers(state)
104 | 
105 |   if path_changed and state.bind_to_cwd then
106 |     vim.api.nvim_command("tcd " .. path)
107 |   end
108 | 
109 |   if type(callback) == "function" then
110 |     vim.schedule(callback)
111 |   end
112 | end
113 | 
114 | ---@class neotree.Config.Buffers.Renderers : neotree.Config.Renderers
115 | 
116 | ---@class (exact) neotree.Config.Buffers : neotree.Config.Source
117 | ---@field bind_to_cwd boolean?
118 | ---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile?
119 | ---@field group_empty_dirs boolean?
120 | ---@field show_unloaded boolean?
121 | ---@field terminals_first boolean?
122 | ---@field renderers neotree.Config.Buffers.Renderers?
123 | 
124 | ---Configures the plugin, should be called before the plugin is used.
125 | ---@param config neotree.Config.Buffers Configuration table containing any keys that the user wants to change from the defaults. May be empty to accept default values.
126 | ---@param global_config neotree.Config.Base
127 | M.setup = function(config, global_config)
128 |   --Configure events for before_render
129 |   if config.before_render then
130 |     --convert to new event system
131 |     manager.subscribe(M.name, {
132 |       event = events.BEFORE_RENDER,
133 |       handler = function(state)
134 |         local this_state = get_state()
135 |         if state == this_state then
136 |           config.before_render(this_state)
137 |         end
138 |       end,
139 |     })
140 |   elseif global_config.enable_git_status then
141 |     manager.subscribe(M.name, {
142 |       event = events.BEFORE_RENDER,
143 |       handler = function(state)
144 |         local this_state = get_state()
145 |         if state == this_state then
146 |           state.git_status_lookup = git.status(state.git_base)
147 |         end
148 |       end,
149 |     })
150 |     manager.subscribe(M.name, {
151 |       event = events.GIT_EVENT,
152 |       handler = M.buffers_changed,
153 |     })
154 |   end
155 | 
156 |   local refresh_events = {
157 |     events.VIM_BUFFER_ADDED,
158 |     events.VIM_BUFFER_DELETED,
159 |   }
160 |   if global_config.enable_refresh_on_write then
161 |     table.insert(refresh_events, events.VIM_BUFFER_CHANGED)
162 |   end
163 |   for _, e in ipairs(refresh_events) do
164 |     manager.subscribe(M.name, {
165 |       event = e,
166 |       handler = function(args)
167 |         if args.afile == "" or utils.is_real_file(args.afile) then
168 |           M.buffers_changed()
169 |         end
170 |       end,
171 |     })
172 |   end
173 | 
174 |   if config.bind_to_cwd then
175 |     manager.subscribe(M.name, {
176 |       event = events.VIM_DIR_CHANGED,
177 |       handler = wrap(manager.dir_changed),
178 |     })
179 |   end
180 | 
181 |   if global_config.enable_diagnostics then
182 |     manager.subscribe(M.name, {
183 |       event = events.STATE_CREATED,
184 |       handler = function(state)
185 |         state.diagnostics_lookup = utils.get_diagnostic_counts()
186 |       end,
187 |     })
188 |     manager.subscribe(M.name, {
189 |       event = events.VIM_DIAGNOSTIC_CHANGED,
190 |       handler = wrap(manager.diagnostics_changed),
191 |     })
192 |   end
193 | 
194 |   --Configure event handlers for modified files
195 |   if global_config.enable_modified_markers then
196 |     manager.subscribe(M.name, {
197 |       event = events.VIM_BUFFER_MODIFIED_SET,
198 |       handler = wrap(manager.opened_buffers_changed),
199 |     })
200 |   end
201 | 
202 |   -- Configure event handler for follow_current_file option
203 |   if config.follow_current_file.enabled then
204 |     manager.subscribe(M.name, {
205 |       event = events.VIM_BUFFER_ENTER,
206 |       handler = M.follow,
207 |     })
208 |     manager.subscribe(M.name, {
209 |       event = events.VIM_TERMINAL_ENTER,
210 |       handler = M.follow,
211 |     })
212 |   end
213 | end
214 | 
215 | return M
216 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/buffers/lib/items.lua:
--------------------------------------------------------------------------------
  1 | local renderer = require("neo-tree.ui.renderer")
  2 | local utils = require("neo-tree.utils")
  3 | local file_items = require("neo-tree.sources.common.file-items")
  4 | local log = require("neo-tree.log")
  5 | 
  6 | local M = {}
  7 | 
  8 | ---Get a table of all open buffers, along with all parent paths of those buffers.
  9 | ---The paths are the keys of the table, and all the values are 'true'.
 10 | M.get_opened_buffers = function(state)
 11 |   if state.loading then
 12 |     return
 13 |   end
 14 |   state.loading = true
 15 |   local context = file_items.create_context()
 16 |   context.state = state
 17 |   -- Create root folder
 18 |   local root = file_items.create_item(context, state.path, "directory") --[[@as neotree.FileItem.Directory]]
 19 |   root.name = vim.fn.fnamemodify(root.path, ":~")
 20 |   root.loaded = true
 21 |   root.search_pattern = state.search_pattern
 22 |   context.folders[root.path] = root
 23 |   local terminals = {}
 24 | 
 25 |   local function add_buffer(bufnr, path)
 26 |     local is_loaded = vim.api.nvim_buf_is_loaded(bufnr)
 27 |     if is_loaded or state.show_unloaded then
 28 |       local is_listed = vim.fn.buflisted(bufnr)
 29 |       if is_listed == 1 then
 30 |         if path == "" then
 31 |           path = "[No Name]"
 32 |         end
 33 |         local success, item = pcall(file_items.create_item, context, path, "file", bufnr)
 34 |         if success then
 35 |           item.extra = {
 36 |             bufnr = bufnr,
 37 |             is_listed = is_listed,
 38 |           }
 39 |         else
 40 |           log.error("Error creating item for " .. path .. ": " .. item)
 41 |         end
 42 |       end
 43 |     end
 44 |   end
 45 | 
 46 |   local bufs = vim.api.nvim_list_bufs()
 47 |   for _, b in ipairs(bufs) do
 48 |     local path = vim.api.nvim_buf_get_name(b)
 49 |     if vim.startswith(path, "term://") then
 50 |       local name = path:match("term://(.*)//.*")
 51 |       local abs_path = vim.fn.fnamemodify(name, ":p")
 52 |       local has_title, title = pcall(vim.api.nvim_buf_get_var, b, "term_title")
 53 |       local item = {
 54 |         name = has_title and title or name,
 55 |         ext = "terminal",
 56 |         path = abs_path,
 57 |         id = path,
 58 |         type = "terminal",
 59 |         loaded = true,
 60 |         extra = {
 61 |           bufnr = b,
 62 |           is_listed = true,
 63 |         },
 64 |       }
 65 |       if utils.is_subpath(state.path, abs_path) then
 66 |         table.insert(terminals, item)
 67 |       end
 68 |     elseif path == "" then
 69 |       add_buffer(b, path)
 70 |     else
 71 |       if #state.path > 1 then
 72 |         -- make sure this is within the root path
 73 |         if utils.is_subpath(state.path, path) then
 74 |           add_buffer(b, path)
 75 |         end
 76 |       else
 77 |         add_buffer(b, path)
 78 |       end
 79 |     end
 80 |   end
 81 | 
 82 |   local root_folders = { root }
 83 | 
 84 |   if #terminals > 0 then
 85 |     local terminal_root = {
 86 |       name = "Terminals",
 87 |       id = "Terminals",
 88 |       ext = "terminal",
 89 |       type = "terminal",
 90 |       children = terminals,
 91 |       loaded = true,
 92 |       search_pattern = state.search_pattern,
 93 |     }
 94 |     context.folders["Terminals"] = terminal_root
 95 |     if state.terminals_first then
 96 |       table.insert(root_folders, 1, terminal_root)
 97 |     else
 98 |       table.insert(root_folders, terminal_root)
 99 |     end
100 |   end
101 |   state.default_expanded_nodes = {}
102 |   for id, _ in pairs(context.folders) do
103 |     table.insert(state.default_expanded_nodes, id)
104 |   end
105 |   file_items.advanced_sort(root.children, state)
106 |   renderer.show_nodes(root_folders, state)
107 |   state.loading = false
108 | end
109 | 
110 | return M
111 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/common/help.lua:
--------------------------------------------------------------------------------
  1 | local Popup = require("nui.popup")
  2 | local NuiLine = require("nui.line")
  3 | local utils = require("neo-tree.utils")
  4 | local popups = require("neo-tree.ui.popups")
  5 | local highlights = require("neo-tree.ui.highlights")
  6 | local M = {}
  7 | 
  8 | ---@param text string
  9 | ---@param highlight string?
 10 | local add_text = function(text, highlight)
 11 |   local line = NuiLine()
 12 |   line:append(text, highlight)
 13 |   return line
 14 | end
 15 | 
 16 | ---@param state neotree.State
 17 | ---@param prefix_key string?
 18 | local get_sub_keys = function(state, prefix_key)
 19 |   local keys = utils.get_keys(state.resolved_mappings, true)
 20 |   if prefix_key then
 21 |     local len = prefix_key:len()
 22 |     local sub_keys = {}
 23 |     for _, key in ipairs(keys) do
 24 |       if #key > len and key:sub(1, len) == prefix_key then
 25 |         table.insert(sub_keys, key)
 26 |       end
 27 |     end
 28 |     return sub_keys
 29 |   else
 30 |     return keys
 31 |   end
 32 | end
 33 | 
 34 | ---@param key string
 35 | ---@param prefix string?
 36 | local function key_minus_prefix(key, prefix)
 37 |   if prefix then
 38 |     return key:sub(prefix:len() + 1)
 39 |   else
 40 |     return key
 41 |   end
 42 | end
 43 | 
 44 | ---Shows a help screen for the mapped commands when will execute those commands
 45 | ---when the corresponding key is pressed.
 46 | ---@param state neotree.State state of the source.
 47 | ---@param title string? if this is a sub-menu for a multi-key mapping, the title for the window.
 48 | ---@param prefix_key string? if this is a sub-menu, the start of tehe multi-key mapping
 49 | M.show = function(state, title, prefix_key)
 50 |   local tree_width = vim.api.nvim_win_get_width(state.winid)
 51 |   local keys = get_sub_keys(state, prefix_key)
 52 | 
 53 |   local lines = { add_text("") }
 54 |   lines[1] = add_text(" Press the corresponding key to execute the command.", "Comment")
 55 |   lines[2] = add_text("               Press <Esc> to cancel.", "Comment")
 56 |   lines[3] = add_text("")
 57 |   local header = NuiLine()
 58 |   header:append(string.format(" %14s", "KEY(S)"), highlights.ROOT_NAME)
 59 |   header:append("    ", highlights.DIM_TEXT)
 60 |   header:append("COMMAND", highlights.ROOT_NAME)
 61 |   lines[4] = header
 62 |   local max_width = #lines[1]:content()
 63 |   for _, key in ipairs(keys) do
 64 |     ---@type neotree.State.ResolvedMapping
 65 |     local value = state.resolved_mappings[key]
 66 |       or { text = "<error mapping for key " .. key .. ">", handler = function() end }
 67 |     local nline = NuiLine()
 68 |     nline:append(string.format(" %14s", key_minus_prefix(key, prefix_key)), highlights.FILTER_TERM)
 69 |     nline:append(" -> ", highlights.DIM_TEXT)
 70 |     nline:append(value.text, highlights.NORMAL)
 71 |     local line = nline:content()
 72 |     if #line > max_width then
 73 |       max_width = #line
 74 |     end
 75 |     table.insert(lines, nline)
 76 |   end
 77 | 
 78 |   local width = math.min(60, max_width + 1)
 79 |   local col
 80 |   if state.current_position == "right" then
 81 |     col = vim.o.columns - tree_width - width - 1
 82 |   else
 83 |     col = tree_width - 1
 84 |   end
 85 | 
 86 |   ---@type nui_popup_options
 87 |   local options = {
 88 |     position = {
 89 |       row = 2,
 90 |       col = col,
 91 |     },
 92 |     size = {
 93 |       width = width,
 94 |       height = #keys + 5,
 95 |     },
 96 |     enter = true,
 97 |     focusable = true,
 98 |     zindex = 50,
 99 |     relative = "editor",
100 |     win_options = {
101 |       foldenable = false, -- Prevent folds from hiding lines
102 |     },
103 |   }
104 | 
105 |   ---@return integer lines The number of screen lines that the popup should occupy at most
106 |   local popup_max_height = function()
107 |     -- statusline
108 |     local statusline_lines = 0
109 |     local laststatus = vim.o.laststatus
110 |     if laststatus ~= 0 then
111 |       local windows = vim.api.nvim_tabpage_list_wins(0)
112 |       if (laststatus == 1 and #windows > 1) or laststatus > 1 then
113 |         statusline_lines = 1
114 |       end
115 |     end
116 |     -- tabs
117 |     local tab_lines = 0
118 |     local showtabline = vim.o.showtabline
119 |     if showtabline ~= 0 then
120 |       local tabs = vim.api.nvim_list_tabpages()
121 |       if (showtabline == 1 and #tabs > 1) or showtabline == 2 then
122 |         tab_lines = 1
123 |       end
124 |     end
125 |     return vim.o.lines - vim.o.cmdheight - statusline_lines - tab_lines - 2
126 |   end
127 |   local max_height = popup_max_height()
128 |   if options.size.height > max_height then
129 |     options.size.height = max_height
130 |   end
131 | 
132 |   title = title or "Neotree Help"
133 |   options = popups.popup_options(title, width, options)
134 |   local popup = Popup(options)
135 |   popup:mount()
136 | 
137 |   local event = require("nui.utils.autocmd").event
138 |   popup:on({ event.VimResized }, function()
139 |     popup:update_layout({
140 |       size = {
141 |         height = math.min(options.size.height --[[@as integer]], popup_max_height()),
142 |         width = math.min(options.size.width --[[@as integer]], vim.o.columns - 2),
143 |       },
144 |     })
145 |   end)
146 |   popup:on({ event.BufLeave, event.BufDelete }, function()
147 |     popup:unmount()
148 |   end, { once = true })
149 | 
150 |   popup:map("n", "<esc>", function()
151 |     popup:unmount()
152 |   end, { noremap = true })
153 | 
154 |   for _, key in ipairs(keys) do
155 |     -- map everything except for <escape>
156 |     if string.match(key:lower(), "^<esc") == nil then
157 |       local value = state.resolved_mappings[key]
158 |         or { text = "<error mapping for key " .. key .. ">", handler = function() end }
159 |       popup:map("n", key_minus_prefix(key, prefix_key), function()
160 |         popup:unmount()
161 |         vim.api.nvim_set_current_win(state.winid)
162 |         value.handler()
163 |       end)
164 |     end
165 |   end
166 | 
167 |   for i, line in ipairs(lines) do
168 |     line:render(popup.bufnr, -1, i)
169 |   end
170 | end
171 | 
172 | return M
173 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/common/hijack_cursor.lua:
--------------------------------------------------------------------------------
 1 | local events = require("neo-tree.events")
 2 | local log = require("neo-tree.log")
 3 | local manager = require("neo-tree.sources.manager")
 4 | 
 5 | local M = {}
 6 | 
 7 | local hijack_cursor_handler = function()
 8 |   if vim.o.filetype ~= "neo-tree" then
 9 |     return
10 |   end
11 |   local success, source = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_source")
12 |   if not success then
13 |     log.debug("Cursor hijack failure: " .. vim.inspect(source))
14 |     return
15 |   end
16 |   local winid = nil
17 |   local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position")
18 |   if position == "current" then
19 |     winid = vim.api.nvim_get_current_win()
20 |   end
21 | 
22 |   local state = manager.get_state(source, nil, winid)
23 |   if not state or not state.tree then
24 |     return
25 |   end
26 |   local node = state.tree:get_node()
27 |   if not node then
28 |     return
29 |   end
30 |   log.debug("Cursor moved in tree window, hijacking cursor position")
31 |   local cursor = vim.api.nvim_win_get_cursor(0)
32 |   local row = cursor[1]
33 |   local current_line = vim.api.nvim_get_current_line()
34 |   local startIndex, _ = string.find(current_line, node.name, nil, true)
35 |   if startIndex then
36 |     vim.api.nvim_win_set_cursor(0, { row, startIndex - 1 })
37 |   end
38 | end
39 | 
40 | --Enables cursor hijack behavior for all sources
41 | M.setup = function()
42 |   events.subscribe({
43 |     event = events.VIM_CURSOR_MOVED,
44 |     handler = hijack_cursor_handler,
45 |     id = "neo-tree-hijack-cursor",
46 |   })
47 | end
48 | 
49 | return M
50 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/common/node_expander.lua:
--------------------------------------------------------------------------------
 1 | local log = require("neo-tree.log")
 2 | local utils = require("neo-tree.utils")
 3 | 
 4 | local M = {}
 5 | 
 6 | --- Recursively expand all loaded nodes under the given node
 7 | --- returns table with all discovered nodes that need to be loaded
 8 | ---@param node table a node to expand
 9 | ---@param state neotree.State current state of the source
10 | ---@return table discovered nodes that need to be loaded
11 | local function expand_loaded(node, state, prefetcher)
12 |   local function rec(current_node, to_load)
13 |     if prefetcher.should_prefetch(current_node) then
14 |       log.trace("Node " .. current_node:get_id() .. "not loaded, saving for later")
15 |       table.insert(to_load, current_node)
16 |     else
17 |       if not current_node:is_expanded() then
18 |         current_node:expand()
19 |         state.explicitly_opened_nodes[current_node:get_id()] = true
20 |       end
21 |       local children = state.tree:get_nodes(current_node:get_id())
22 |       log.debug("Expanding childrens of " .. current_node:get_id())
23 |       for _, child in ipairs(children) do
24 |         if utils.is_expandable(child) then
25 |           rec(child, to_load)
26 |         else
27 |           log.trace("Child: " .. (child.name or "") .. " is not expandable, skipping")
28 |         end
29 |       end
30 |     end
31 |   end
32 | 
33 |   local to_load = {}
34 |   rec(node, to_load)
35 |   return to_load
36 | end
37 | 
38 | --- Recursively expands all nodes under the given node collecting all unloaded nodes
39 | --- Then run prefetcher on all unloaded nodes. Finally, expand loded nodes.
40 | --- async method
41 | ---@param node table a node to expand
42 | ---@param state neotree.State current state of the source
43 | local function expand_and_load(node, state, prefetcher)
44 |   local to_load = expand_loaded(node, state, prefetcher)
45 |   for _, _node in ipairs(to_load) do
46 |     prefetcher.prefetch(state, _node)
47 |     -- no need to handle results as prefetch is recursive
48 |     expand_loaded(_node, state, prefetcher)
49 |   end
50 | end
51 | 
52 | --- Expands given node recursively loading all descendant nodes if needed
53 | --- Nodes will be loaded using given prefetcher
54 | --- async method
55 | ---@param state neotree.State current state of the source
56 | ---@param node table a node to expand
57 | ---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean`
58 | M.expand_directory_recursively = function(state, node, prefetcher)
59 |   log.debug("Expanding directory " .. node:get_id())
60 |   prefetcher = prefetcher or M.default_prefetcher
61 |   if not utils.is_expandable(node) then
62 |     return
63 |   end
64 | 
65 |   state.explicitly_opened_nodes = state.explicitly_opened_nodes or {}
66 |   if prefetcher.should_prefetch(node) then
67 |     local id = node:get_id()
68 |     state.explicitly_opened_nodes[id] = true
69 |     prefetcher.prefetch(state, node)
70 |     expand_loaded(node, state, prefetcher)
71 |   else
72 |     expand_and_load(node, state, prefetcher)
73 |   end
74 | end
75 | 
76 | M.default_prefetcher = {
77 |   prefetch = function(state, node)
78 |     log.debug("Default expander prefetch does nothing")
79 |   end,
80 |   should_prefetch = function(node)
81 |     return false
82 |   end,
83 | }
84 | 
85 | return M
86 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/document_symbols/commands.lua:
--------------------------------------------------------------------------------
 1 | --This file should contain all commands meant to be used by mappings.
 2 | local cc = require("neo-tree.sources.common.commands")
 3 | local utils = require("neo-tree.utils")
 4 | local manager = require("neo-tree.sources.manager")
 5 | local inputs = require("neo-tree.ui.inputs")
 6 | local filters = require("neo-tree.sources.common.filters")
 7 | 
 8 | ---@class neotree.sources.DocumentSymbols.Commands : neotree.sources.Common.Commands
 9 | ---@field [string] neotree.TreeCommand
10 | local M = {}
11 | local SOURCE_NAME = "document_symbols"
12 | M.refresh = utils.wrap(manager.refresh, SOURCE_NAME)
13 | M.redraw = utils.wrap(manager.redraw, SOURCE_NAME)
14 | 
15 | M.show_debug_info = function(state)
16 |   print(vim.inspect(state))
17 | end
18 | 
19 | ---@param node NuiTree.Node
20 | M.jump_to_symbol = function(state, node)
21 |   node = node or state.tree:get_node()
22 |   if node:get_depth() == 1 then
23 |     return
24 |   end
25 |   vim.api.nvim_set_current_win(state.lsp_winid)
26 |   vim.api.nvim_set_current_buf(state.lsp_bufnr)
27 |   local symbol_loc = node.extra.selection_range.start
28 |   vim.api.nvim_win_set_cursor(state.lsp_winid, { symbol_loc[1] + 1, symbol_loc[2] })
29 | end
30 | 
31 | M.rename = function(state)
32 |   local node = assert(state.tree:get_node())
33 |   if node:get_depth() == 1 then
34 |     return
35 |   end
36 |   local old_name = node.name
37 | 
38 |   ---@param new_name string?
39 |   local callback = function(new_name)
40 |     if not new_name or new_name == "" or new_name == old_name then
41 |       return
42 |     end
43 |     M.jump_to_symbol(state, node)
44 |     vim.lsp.buf.rename(new_name)
45 |     M.refresh(state)
46 |   end
47 |   local msg = string.format('Enter new name for "%s":', old_name)
48 |   inputs.input(msg, old_name, callback)
49 | end
50 | 
51 | M.open = M.jump_to_symbol
52 | 
53 | M.filter_on_submit = function(state)
54 |   filters.show_filter(state, true, true)
55 | end
56 | 
57 | M.filter = function(state)
58 |   filters.show_filter(state, true)
59 | end
60 | 
61 | cc._add_common_commands(M, "node") -- common tree commands
62 | cc._add_common_commands(M, "^open") -- open commands
63 | cc._add_common_commands(M, "^close_window
quot;)
64 | cc._add_common_commands(M, "source
quot;) -- source navigation
65 | cc._add_common_commands(M, "preview") -- preview
66 | cc._add_common_commands(M, "^cancel
quot;) -- cancel
67 | cc._add_common_commands(M, "help") -- help commands
68 | cc._add_common_commands(M, "with_window_picker
quot;) -- open using window picker
69 | cc._add_common_commands(M, "^toggle_auto_expand_width
quot;)
70 | 
71 | return M
72 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/document_symbols/components.lua:
--------------------------------------------------------------------------------
 1 | -- This file contains the built-in components. Each componment is a function
 2 | -- that takes the following arguments:
 3 | --      config: A table containing the configuration provided by the user
 4 | --              when declaring this component in their renderer config.
 5 | --      node:   A NuiNode object for the currently focused node.
 6 | --      state:  The current state of the source providing the items.
 7 | --
 8 | -- The function should return either a table, or a list of tables, each of which
 9 | -- contains the following keys:
10 | --    text:      The text to display for this item.
11 | --    highlight: The highlight group to apply to this text.
12 | 
13 | local highlights = require("neo-tree.ui.highlights")
14 | local common = require("neo-tree.sources.common.components")
15 | 
16 | ---@alias neotree.Component.DocumentSymbols._Key
17 | ---|"kind_icon"
18 | ---|"kind_name"
19 | ---|"name"
20 | 
21 | ---@class neotree.Component.DocumentSymbols Use the neotree.Component.DocumentSymbols.* types to get more specific types.
22 | ---@field [1] neotree.Component.DocumentSymbols._Key|neotree.Component.Common._Key
23 | 
24 | ---@type table<neotree.Component.DocumentSymbols._Key, neotree.Renderer>
25 | local M = {}
26 | 
27 | ---@class (exact) neotree.Component.DocumentSymbols.KindIcon : neotree.Component
28 | ---@field [1] "kind_icon"?
29 | ---@field provider neotree.IconProvider?
30 | 
31 | ---@param config neotree.Component.DocumentSymbols.KindIcon
32 | M.kind_icon = function(config, node, state)
33 |   local icon = {
34 |     text = node:get_depth() == 1 and "" or node.extra.kind.icon,
35 |     highlight = node.extra.kind.hl,
36 |   }
37 | 
38 |   if config.provider then
39 |     icon = config.provider(icon, node, state) or icon
40 |   end
41 | 
42 |   return icon
43 | end
44 | 
45 | ---@class (exact) neotree.Component.DocumentSymbols.KindName : neotree.Component
46 | ---@field [1] "kind_name"?
47 | 
48 | ---@param config neotree.Component.DocumentSymbols.KindName
49 | M.kind_name = function(config, node, state)
50 |   return {
51 |     text = node:get_depth() == 1 and "" or node.extra.kind.name,
52 |     highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME,
53 |   }
54 | end
55 | 
56 | ---@class (exact) neotree.Component.DocumentSymbols.Name : neotree.Component.Common.Name
57 | 
58 | ---@param config neotree.Component.DocumentSymbols.Name
59 | M.name = function(config, node, state)
60 |   return {
61 |     text = node.name,
62 |     highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME,
63 |   }
64 | end
65 | 
66 | return vim.tbl_deep_extend("force", common, M)
67 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/document_symbols/init.lua:
--------------------------------------------------------------------------------
  1 | --This file should have all functions that are in the public api and either set
  2 | --or read the state of this source.
  3 | 
  4 | local manager = require("neo-tree.sources.manager")
  5 | local events = require("neo-tree.events")
  6 | local utils = require("neo-tree.utils")
  7 | local symbols = require("neo-tree.sources.document_symbols.lib.symbols_utils")
  8 | local renderer = require("neo-tree.ui.renderer")
  9 | 
 10 | ---@class neotree.sources.DocumentSymbols : neotree.Source
 11 | local M = {
 12 |   name = "document_symbols",
 13 |   display_name = "  Symbols ",
 14 | }
 15 | 
 16 | local get_state = function()
 17 |   return manager.get_state(M.name)
 18 | end
 19 | 
 20 | ---Refresh the source with debouncing
 21 | ---@param args { afile: string }
 22 | local refresh_debounced = function(args)
 23 |   if utils.is_real_file(args.afile) == false then
 24 |     return
 25 |   end
 26 |   utils.debounce(
 27 |     "document_symbols_refresh",
 28 |     utils.wrap(manager.refresh, M.name),
 29 |     100,
 30 |     utils.debounce_strategy.CALL_LAST_ONLY
 31 |   )
 32 | end
 33 | 
 34 | ---Internal function to follow the cursor
 35 | local follow_symbol = function()
 36 |   local state = get_state()
 37 |   if state.lsp_bufnr ~= vim.api.nvim_get_current_buf() then
 38 |     return
 39 |   end
 40 |   local cursor = vim.api.nvim_win_get_cursor(state.lsp_winid)
 41 |   local node_id = symbols.get_symbol_by_loc(state.tree, { cursor[1] - 1, cursor[2] })
 42 |   if #node_id > 0 then
 43 |     renderer.focus_node(state, node_id, true)
 44 |   end
 45 | end
 46 | 
 47 | ---@class neotree.sources.documentsymbols.DebounceArgs
 48 | 
 49 | ---Follow the cursor with debouncing
 50 | ---@param args { afile: string }
 51 | local follow_debounced = function(args)
 52 |   if utils.is_real_file(args.afile) == false then
 53 |     return
 54 |   end
 55 |   utils.debounce(
 56 |     "document_symbols_follow",
 57 |     utils.wrap(follow_symbol, args.afile),
 58 |     100,
 59 |     utils.debounce_strategy.CALL_LAST_ONLY
 60 |   )
 61 | end
 62 | 
 63 | ---Navigate to the given path.
 64 | M.navigate = function(state, path, path_to_reveal, callback, async)
 65 |   state.lsp_winid, _ = utils.get_appropriate_window(state)
 66 |   state.lsp_bufnr = vim.api.nvim_win_get_buf(state.lsp_winid)
 67 |   state.path = vim.api.nvim_buf_get_name(state.lsp_bufnr)
 68 | 
 69 |   symbols.render_symbols(state)
 70 | 
 71 |   if type(callback) == "function" then
 72 |     vim.schedule(callback)
 73 |   end
 74 | end
 75 | 
 76 | ---@class neotree.Config.LspKindDisplay
 77 | ---@field icon string
 78 | ---@field hl string
 79 | 
 80 | ---@class neotree.Config.DocumentSymbols.Renderers : neotree.Config.Renderers
 81 | ---@field root neotree.Component.DocumentSymbols[]?
 82 | ---@field symbol neotree.Component.DocumentSymbols[]?
 83 | 
 84 | ---@class (exact) neotree.Config.DocumentSymbols : neotree.Config.Source
 85 | ---@field follow_cursor boolean?
 86 | ---@field client_filters neotree.lsp.ClientFilter?
 87 | ---@field custom_kinds table<integer, string>?
 88 | ---@field kinds table<string, neotree.Config.LspKindDisplay>?
 89 | ---@field renderers neotree.Config.DocumentSymbols.Renderers?
 90 | 
 91 | ---Configures the plugin, should be called before the plugin is used.
 92 | ---@param config neotree.Config.DocumentSymbols
 93 | ---@param global_config neotree.Config.Base
 94 | M.setup = function(config, global_config)
 95 |   symbols.setup(config)
 96 | 
 97 |   if config.before_render then
 98 |     manager.subscribe(M.name, {
 99 |       event = events.BEFORE_RENDER,
100 |       handler = function(state)
101 |         local this_state = get_state()
102 |         if state == this_state then
103 |           config.before_render(this_state)
104 |         end
105 |       end,
106 |     })
107 |   end
108 | 
109 |   local refresh_events = {
110 |     events.VIM_BUFFER_ENTER,
111 |     events.VIM_INSERT_LEAVE,
112 |     events.VIM_TEXT_CHANGED_NORMAL,
113 |   }
114 |   for _, event in ipairs(refresh_events) do
115 |     manager.subscribe(M.name, {
116 |       event = event,
117 |       handler = refresh_debounced,
118 |     })
119 |   end
120 | 
121 |   if config.follow_cursor then
122 |     manager.subscribe(M.name, {
123 |       event = events.VIM_CURSOR_MOVED,
124 |       handler = follow_debounced,
125 |     })
126 |   end
127 | end
128 | 
129 | return M
130 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/document_symbols/lib/client_filters.lua:
--------------------------------------------------------------------------------
 1 | ---Utilities function to filter the LSP servers
 2 | local utils = require("neo-tree.utils")
 3 | 
 4 | ---@class neotree.lsp.RespRaw
 5 | ---@field err lsp.ResponseError?
 6 | ---@field error lsp.ResponseError?
 7 | ---@field result any
 8 | 
 9 | local M = {}
10 | 
11 | ---@alias neotree.lsp.Filter fun(client_name: string): boolean
12 | 
13 | ---Filter clients
14 | ---@param filter_type "first" | "all"
15 | ---@param filter_fn neotree.lsp.Filter?
16 | ---@param resp table<integer, neotree.lsp.RespRaw>
17 | ---@return table<string, any>
18 | local filter_clients = function(filter_type, filter_fn, resp)
19 |   if resp == nil or type(resp) ~= "table" then
20 |     return {}
21 |   end
22 |   filter_fn = filter_fn or function(client_name)
23 |     return true
24 |   end
25 | 
26 |   local result = {}
27 |   for client_id, client_resp in pairs(resp) do
28 |     local client_name = vim.lsp.get_client_by_id(client_id).name
29 |     if filter_fn(client_name) and client_resp.result ~= nil then
30 |       result[client_name] = client_resp.result
31 |       if filter_type ~= "all" then
32 |         break
33 |       end
34 |     end
35 |   end
36 |   return result
37 | end
38 | 
39 | ---Filter only allowed clients
40 | ---@param allow_only string[] the list of clients to keep
41 | ---@return neotree.lsp.Filter
42 | local allow_only = function(allow_only)
43 |   return function(client_name)
44 |     return vim.tbl_contains(allow_only, client_name)
45 |   end
46 | end
47 | 
48 | ---Ignore clients
49 | ---@param ignore string[] the list of clients to remove
50 | ---@return neotree.lsp.Filter
51 | local ignore = function(ignore)
52 |   return function(client_name)
53 |     return not vim.tbl_contains(ignore, client_name)
54 |   end
55 | end
56 | 
57 | ---Main entry point for the filter
58 | ---@param resp table<integer, neotree.lsp.RespRaw>
59 | ---@return table<string, any>
60 | M.filter_resp = function(resp)
61 |   return {}
62 | end
63 | 
64 | ---@alias neotree.lsp.Filter.Type
65 | ---|"first" # Allow the first that matches
66 | ---|"all" # Allow all that match
67 | 
68 | ---@alias neotree.lsp.ClientFilter neotree.lsp.Filter.Type | { type: neotree.lsp.Filter.Type, fn: neotree.lsp.Filter, allow_only: string[], ignore: string[] }
69 | ---Setup the filter accordingly to the config
70 | ---@see neo-tree-document-symbols-source for more details on options that the filter accepts
71 | ---@param cfg_flt neotree.lsp.ClientFilter
72 | M.setup = function(cfg_flt)
73 |   local filter_type = "first"
74 |   local filter_fn = nil
75 | 
76 |   if type(cfg_flt) == "table" then
77 |     if cfg_flt.type == "all" then
78 |       filter_type = "all"
79 |     end
80 | 
81 |     if cfg_flt.fn ~= nil then
82 |       filter_fn = cfg_flt.fn
83 |     elseif cfg_flt.allow_only then
84 |       filter_fn = allow_only(cfg_flt.allow_only)
85 |     elseif cfg_flt.ignore then
86 |       filter_fn = ignore(cfg_flt.ignore)
87 |     end
88 |   elseif cfg_flt == "all" then
89 |     filter_type = "all"
90 |   end
91 | 
92 |   M.filter_resp = function(resp)
93 |     return filter_clients(filter_type, filter_fn, resp)
94 |   end
95 | end
96 | 
97 | return M
98 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/document_symbols/lib/kinds.lua:
--------------------------------------------------------------------------------
 1 | ---Helper module to render symbols' kinds
 2 | ---Need to be initialized by calling M.setup()
 3 | local M = {}
 4 | 
 5 | local kinds_id_to_name = {
 6 |   [0] = "Root",
 7 |   [1] = "File",
 8 |   [2] = "Module",
 9 |   [3] = "Namespace",
10 |   [4] = "Package",
11 |   [5] = "Class",
12 |   [6] = "Method",
13 |   [7] = "Property",
14 |   [8] = "Field",
15 |   [9] = "Constructor",
16 |   [10] = "Enum",
17 |   [11] = "Interface",
18 |   [12] = "Function",
19 |   [13] = "Variable",
20 |   [14] = "Constant",
21 |   [15] = "String",
22 |   [16] = "Number",
23 |   [17] = "Boolean",
24 |   [18] = "Array",
25 |   [19] = "Object",
26 |   [20] = "Key",
27 |   [21] = "Null",
28 |   [22] = "EnumMember",
29 |   [23] = "Struct",
30 |   [24] = "Event",
31 |   [25] = "Operator",
32 |   [26] = "TypeParameter",
33 | }
34 | 
35 | local kinds_map = {}
36 | 
37 | ---@class neotree.LspKindDisplay
38 | ---@field name string Display name
39 | ---@field icon string Icon to render
40 | ---@field hl string Highlight for the node
41 | 
42 | ---Get how the kind with kind_id should be rendered
43 | ---@param kind_id integer the kind_id to be render
44 | ---@return neotree.LspKindDisplay res
45 | M.get_kind = function(kind_id)
46 |   local kind_name = kinds_id_to_name[kind_id]
47 |   return vim.tbl_extend(
48 |     "force",
49 |     { name = kind_name or ("Unknown: " .. kind_id), icon = "?", hl = "" },
50 |     kind_name and (kinds_map[kind_name] or {}) or kinds_map["Unknown"]
51 |   )
52 | end
53 | 
54 | ---Setup the module with custom kinds
55 | ---@param custom_kinds table additional kinds, should be of the form { [kind_id] = kind_name }
56 | ---@param kinds_display table mapping of kind_name to corresponding display name, icon and hl group
57 | ---   { [kind_name] = {
58 | ---         name = kind_display_name,
59 | ---         icon = kind_icon,
60 | ---         hl = kind_hl
61 | ---         }, }
62 | M.setup = function(custom_kinds, kinds_display)
63 |   kinds_id_to_name = vim.tbl_deep_extend("force", kinds_id_to_name, custom_kinds or {})
64 |   kinds_map = kinds_display
65 | end
66 | 
67 | return M
68 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/filesystem/components.lua:
--------------------------------------------------------------------------------
 1 | -- This file contains the built-in components. Each componment is a function
 2 | -- that takes the following arguments:
 3 | --      config: A table containing the configuration provided by the user
 4 | --              when declaring this component in their renderer config.
 5 | --      node:   A NuiNode object for the currently focused node.
 6 | --      state:  The current state of the source providing the items.
 7 | --
 8 | -- The function should return either a table, or a list of tables, each of which
 9 | -- contains the following keys:
10 | --    text:      The text to display for this item.
11 | --    highlight: The highlight group to apply to this text.
12 | 
13 | local highlights = require("neo-tree.ui.highlights")
14 | local common = require("neo-tree.sources.common.components")
15 | 
16 | ---@alias neotree.Component.Filesystem._Key
17 | ---|"current_filter"
18 | 
19 | ---@class neotree.Component.Filesystem
20 | ---@field [1] neotree.Component.Filesystem._Key|neotree.Component.Common._Key
21 | 
22 | ---@type table<neotree.Component.Filesystem._Key, neotree.Renderer>
23 | local M = {}
24 | 
25 | ---@class (exact) neotree.Component.Filesystem.CurrentFilter : neotree.Component.Common.CurrentFilter
26 | 
27 | ---@param config neotree.Component.Filesystem.CurrentFilter
28 | M.current_filter = function(config, node, state)
29 |   local filter = node.search_pattern or ""
30 |   if filter == "" then
31 |     return {}
32 |   end
33 |   return {
34 |     {
35 |       text = "Find",
36 |       highlight = highlights.DIM_TEXT,
37 |     },
38 |     {
39 |       text = string.format('"%s"', filter),
40 |       highlight = config.highlight or highlights.FILTER_TERM,
41 |     },
42 |     {
43 |       text = "in",
44 |       highlight = highlights.DIM_TEXT,
45 |     },
46 |   }
47 | end
48 | 
49 | return vim.tbl_deep_extend("force", common, M)
50 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/filesystem/lib/fs_watch.lua:
--------------------------------------------------------------------------------
  1 | local events = require("neo-tree.events")
  2 | local log = require("neo-tree.log")
  3 | local git = require("neo-tree.git")
  4 | local utils = require("neo-tree.utils")
  5 | local uv = vim.uv or vim.loop
  6 | 
  7 | local M = {}
  8 | 
  9 | local flags = {
 10 |   watch_entry = false,
 11 |   stat = false,
 12 |   recursive = false,
 13 | }
 14 | 
 15 | local watched = {}
 16 | 
 17 | local get_dot_git_folder = function(path, callback)
 18 |   if type(callback) == "function" then
 19 |     git.get_repository_root(path, function(git_root)
 20 |       if git_root then
 21 |         local git_folder = utils.path_join(git_root, ".git")
 22 |         local stat = uv.fs_stat(git_folder)
 23 |         if stat and stat.type == "directory" then
 24 |           callback(git_folder, git_root)
 25 |         end
 26 |       else
 27 |         callback(nil, nil)
 28 |       end
 29 |     end)
 30 |   else
 31 |     local git_root = git.get_repository_root(path)
 32 |     if git_root then
 33 |       local git_folder = utils.path_join(git_root, ".git")
 34 |       local stat = uv.fs_stat(git_folder)
 35 |       if stat and stat.type == "directory" then
 36 |         return git_folder, git_root
 37 |       end
 38 |     end
 39 |     return nil, nil
 40 |   end
 41 | end
 42 | 
 43 | M.show_watched = function()
 44 |   local items = {}
 45 |   for _, handle in pairs(watched) do
 46 |     items[handle.path] = handle.references
 47 |   end
 48 |   log.info("Watched Folders: ", vim.inspect(items))
 49 | end
 50 | 
 51 | ---Watch a directory for changes to it's children. Not recursive.
 52 | ---@param path string The directory to watch.
 53 | ---@param custom_callback? function The callback to call when a change is detected.
 54 | ---@param allow_git_watch? boolean Allow watching of git folders.
 55 | M.watch_folder = function(path, custom_callback, allow_git_watch)
 56 |   if not allow_git_watch then
 57 |     if path:find("/%.git
quot;) or path:find("/%.git/") then
 58 |       -- git folders seem to throw off fs events constantly.
 59 |       log.debug("watch_folder(path): Skipping git folder: ", path)
 60 |       return
 61 |     end
 62 |   end
 63 |   local h = watched[path]
 64 |   if h == nil then
 65 |     log.trace("Starting new fs watch on: ", path)
 66 |     local callback = custom_callback
 67 |       or vim.schedule_wrap(function(err, fname)
 68 |         if fname and fname:match("^%.null[-]ls_.+") then
 69 |           -- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
 70 |           return
 71 |         end
 72 |         if err then
 73 |           log.error("file_event_callback: ", err)
 74 |           return
 75 |         end
 76 |         events.fire_event(events.FS_EVENT, { afile = path })
 77 |       end)
 78 |     h = {
 79 |       handle = uv.new_fs_event(),
 80 |       path = path,
 81 |       references = 0,
 82 |       active = false,
 83 |       callback = callback,
 84 |     }
 85 |     watched[path] = h
 86 |     --w:start(path, flags, callback)
 87 |   else
 88 |     log.trace("Incrementing references for fs watch on: ", path)
 89 |   end
 90 |   h.references = h.references + 1
 91 | end
 92 | 
 93 | M.watch_git_index = function(path, async)
 94 |   local function watch_git_folder(git_folder, git_root)
 95 |     if git_folder then
 96 |       local git_event_callback = vim.schedule_wrap(function(err, fname)
 97 |         if fname and fname:match("^.+%.lock
quot;) then
 98 |           return
 99 |         end
100 |         if fname and fname:match("^%._null-ls_.+") then
101 |           -- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
102 |           return
103 |         end
104 |         if err then
105 |           log.error("git_event_callback: ", err)
106 |           return
107 |         end
108 |         events.fire_event(events.GIT_EVENT, { path = fname, repository = git_root })
109 |       end)
110 | 
111 |       M.watch_folder(git_folder, git_event_callback, true)
112 |     end
113 |   end
114 | 
115 |   if async then
116 |     get_dot_git_folder(path, watch_git_folder)
117 |   else
118 |     watch_git_folder(get_dot_git_folder(path))
119 |   end
120 | end
121 | 
122 | M.updated_watched = function()
123 |   for path, w in pairs(watched) do
124 |     if w.references > 0 then
125 |       if not w.active then
126 |         log.trace("References added for fs watch on: ", path, ", starting.")
127 |         w.handle:start(path, flags, w.callback)
128 |         w.active = true
129 |       end
130 |     else
131 |       if w.active then
132 |         log.trace("No more references for fs watch on: ", path, ", stopping.")
133 |         w.handle:stop()
134 |         w.active = false
135 |       end
136 |     end
137 |   end
138 | end
139 | 
140 | ---Stop watching a directory. If there are no more references to the handle,
141 | ---it will be destroyed. Otherwise, the reference count will be decremented.
142 | ---@param path string The directory to stop watching.
143 | M.unwatch_folder = function(path, callback_id)
144 |   local h = watched[path]
145 |   if h then
146 |     log.trace("Decrementing references for fs watch on: ", path, callback_id)
147 |     h.references = h.references - 1
148 |   else
149 |     log.trace("(unwatch_folder) No fs watch found for: ", path)
150 |   end
151 | end
152 | 
153 | M.unwatch_git_index = function(path, async)
154 |   local function unwatch_git_folder(git_folder, _)
155 |     if git_folder then
156 |       M.unwatch_folder(git_folder)
157 |     end
158 |   end
159 | 
160 |   if async then
161 |     get_dot_git_folder(path, unwatch_git_folder)
162 |   else
163 |     unwatch_git_folder(get_dot_git_folder(path))
164 |   end
165 | end
166 | 
167 | ---Stop watching all directories. This is the nuclear option and it affects all
168 | ---sources.
169 | M.unwatch_all = function()
170 |   for _, h in pairs(watched) do
171 |     h.handle:stop()
172 |     h.handle = nil
173 |   end
174 |   watched = {}
175 | end
176 | 
177 | return M
178 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/filesystem/lib/globtopattern.lua:
--------------------------------------------------------------------------------
  1 | --(c) 2008-2011 David Manura.  Licensed under the same terms as Lua (MIT).
  2 | 
  3 | --Permission is hereby granted, free of charge, to any person obtaining a copy
  4 | --of this software and associated documentation files (the "Software"), to deal
  5 | --in the Software without restriction, including without limitation the rights
  6 | --to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 | --copies of the Software, and to permit persons to whom the Software is
  8 | --furnished to do so, subject to the following conditions:
  9 | 
 10 | --The above copyright notice and this permission notice shall be included in
 11 | --all copies or substantial portions of the Software.
 12 | 
 13 | --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 14 | --IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 15 | --FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 16 | --AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 17 | --LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 18 | --OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 19 | --THE SOFTWARE.
 20 | --(end license)
 21 | 
 22 | local M = { _TYPE = "module", _NAME = "globtopattern", _VERSION = "0.2.1.20120406" }
 23 | 
 24 | function M.globtopattern(g)
 25 |   -- Some useful references:
 26 |   -- - apr_fnmatch in Apache APR.  For example,
 27 |   --   http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
 28 |   --   which cites POSIX 1003.2-1992, section B.6.
 29 | 
 30 |   local p = "^" -- pattern being built
 31 |   local i = 0 -- index in g
 32 |   local c -- char at index i in g.
 33 | 
 34 |   -- unescape glob char
 35 |   local function unescape()
 36 |     if c == "\\" then
 37 |       i = i + 1
 38 |       c = g:sub(i, i)
 39 |       if c == "" then
 40 |         p = "[^]"
 41 |         return false
 42 |       end
 43 |     end
 44 |     return true
 45 |   end
 46 | 
 47 |   -- escape pattern char
 48 |   local function escape(c)
 49 |     return c:match("^%w
quot;) and c or "%" .. c
 50 |   end
 51 | 
 52 |   -- Convert tokens at end of charset.
 53 |   local function charset_end()
 54 |     while 1 do
 55 |       if c == "" then
 56 |         p = "[^]"
 57 |         return false
 58 |       elseif c == "]" then
 59 |         p = p .. "]"
 60 |         break
 61 |       else
 62 |         if not unescape() then
 63 |           break
 64 |         end
 65 |         local c1 = c
 66 |         i = i + 1
 67 |         c = g:sub(i, i)
 68 |         if c == "" then
 69 |           p = "[^]"
 70 |           return false
 71 |         elseif c == "-" then
 72 |           i = i + 1
 73 |           c = g:sub(i, i)
 74 |           if c == "" then
 75 |             p = "[^]"
 76 |             return false
 77 |           elseif c == "]" then
 78 |             p = p .. escape(c1) .. "%-]"
 79 |             break
 80 |           else
 81 |             if not unescape() then
 82 |               break
 83 |             end
 84 |             p = p .. escape(c1) .. "-" .. escape(c)
 85 |           end
 86 |         elseif c == "]" then
 87 |           p = p .. escape(c1) .. "]"
 88 |           break
 89 |         else
 90 |           p = p .. escape(c1)
 91 |           i = i - 1 -- put back
 92 |         end
 93 |       end
 94 |       i = i + 1
 95 |       c = g:sub(i, i)
 96 |     end
 97 |     return true
 98 |   end
 99 | 
100 |   -- Convert tokens in charset.
101 |   local function charset()
102 |     i = i + 1
103 |     c = g:sub(i, i)
104 |     if c == "" or c == "]" then
105 |       p = "[^]"
106 |       return false
107 |     elseif c == "^" or c == "!" then
108 |       i = i + 1
109 |       c = g:sub(i, i)
110 |       if c == "]" then
111 |         -- ignored
112 |       else
113 |         p = p .. "[^"
114 |         if not charset_end() then
115 |           return false
116 |         end
117 |       end
118 |     else
119 |       p = p .. "["
120 |       if not charset_end() then
121 |         return false
122 |       end
123 |     end
124 |     return true
125 |   end
126 | 
127 |   -- Convert tokens.
128 |   while 1 do
129 |     i = i + 1
130 |     c = g:sub(i, i)
131 |     if c == "" then
132 |       p = p .. "
quot;
133 |       break
134 |     elseif c == "?" then
135 |       p = p .. "."
136 |     elseif c == "*" then
137 |       p = p .. ".*"
138 |     elseif c == "[" then
139 |       if not charset() then
140 |         break
141 |       end
142 |     elseif c == "\\" then
143 |       i = i + 1
144 |       c = g:sub(i, i)
145 |       if c == "" then
146 |         p = p .. "\\
quot;
147 |         break
148 |       end
149 |       p = p .. escape(c)
150 |     else
151 |       p = p .. escape(c)
152 |     end
153 |   end
154 |   return p
155 | end
156 | 
157 | return M
158 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/git_status/commands.lua:
--------------------------------------------------------------------------------
 1 | --This file should contain all commands meant to be used by mappings.
 2 | 
 3 | local cc = require("neo-tree.sources.common.commands")
 4 | local utils = require("neo-tree.utils")
 5 | local manager = require("neo-tree.sources.manager")
 6 | 
 7 | ---@class neotree.sources.GitStatus.Commands : neotree.sources.Common.Commands
 8 | local M = {}
 9 | 
10 | local refresh = utils.wrap(manager.refresh, "git_status")
11 | local redraw = utils.wrap(manager.redraw, "git_status")
12 | 
13 | -- ----------------------------------------------------------------------------
14 | -- Common commands
15 | -- ----------------------------------------------------------------------------
16 | M.add = function(state)
17 |   cc.add(state, refresh)
18 | end
19 | 
20 | M.add_directory = function(state)
21 |   cc.add_directory(state, refresh)
22 | end
23 | 
24 | ---Marks node as copied, so that it can be pasted somewhere else.
25 | M.copy_to_clipboard = function(state)
26 |   cc.copy_to_clipboard(state, redraw)
27 | end
28 | 
29 | ---@type neotree.TreeCommandVisual
30 | M.copy_to_clipboard_visual = function(state, selected_nodes)
31 |   cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
32 | end
33 | 
34 | ---Marks node as cut, so that it can be pasted (moved) somewhere else.
35 | M.cut_to_clipboard = function(state)
36 |   cc.cut_to_clipboard(state, redraw)
37 | end
38 | 
39 | ---@type neotree.TreeCommandVisual
40 | M.cut_to_clipboard_visual = function(state, selected_nodes)
41 |   cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
42 | end
43 | 
44 | M.copy = function(state)
45 |   cc.copy(state, redraw)
46 | end
47 | 
48 | M.move = function(state)
49 |   cc.move(state, redraw)
50 | end
51 | 
52 | ---Pastes all items from the clipboard to the current directory.
53 | M.paste_from_clipboard = function(state)
54 |   cc.paste_from_clipboard(state, refresh)
55 | end
56 | 
57 | M.delete = function(state)
58 |   cc.delete(state, refresh)
59 | end
60 | 
61 | ---@type neotree.TreeCommandVisual
62 | M.delete_visual = function(state, selected_nodes)
63 |   cc.delete_visual(state, selected_nodes, refresh)
64 | end
65 | 
66 | M.refresh = refresh
67 | 
68 | M.rename = function(state)
69 |   cc.rename(state, refresh)
70 | end
71 | 
72 | cc._add_common_commands(M)
73 | 
74 | return M
75 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/git_status/components.lua:
--------------------------------------------------------------------------------
 1 | -- This file contains the built-in components. Each componment is a function
 2 | -- that takes the following arguments:
 3 | --      config: A table containing the configuration provided by the user
 4 | --              when declaring this component in their renderer config.
 5 | --      node:   A NuiNode object for the currently focused node.
 6 | --      state:  The current state of the source providing the items.
 7 | --
 8 | -- The function should return either a table, or a list of tables, each of which
 9 | -- contains the following keys:
10 | --    text:      The text to display for this item.
11 | --    highlight: The highlight group to apply to this text.
12 | 
13 | local highlights = require("neo-tree.ui.highlights")
14 | local common = require("neo-tree.sources.common.components")
15 | 
16 | ---@alias neotree.Component.GitStatus._Key
17 | ---|"name"
18 | 
19 | ---@class neotree.Component.GitStatus
20 | ---@field [1] neotree.Component.GitStatus._Key|neotree.Component.Common._Key
21 | 
22 | ---@type table<neotree.Component.GitStatus._Key, neotree.Renderer>
23 | local M = {}
24 | 
25 | ---@class (exact) neotree.Component.GitStatus.Name : neotree.Component.Common.Name
26 | ---@field [1] "current_filter"?
27 | ---@field use_git_status_colors boolean?
28 | 
29 | ---@param config neotree.Component.GitStatus.Name
30 | M.name = function(config, node, state)
31 |   local highlight = config.highlight or highlights.FILE_NAME_OPENED
32 |   local name = node.name
33 |   if node.type == "directory" then
34 |     if node:get_depth() == 1 then
35 |       highlight = highlights.ROOT_NAME
36 |       if node:has_children() then
37 |         name = "GIT STATUS for " .. name
38 |       else
39 |         name = "GIT STATUS (working tree clean) for " .. name
40 |       end
41 |     else
42 |       highlight = highlights.DIRECTORY_NAME
43 |     end
44 |   elseif config.use_git_status_colors then
45 |     local git_status = state.components.git_status({}, node, state)
46 |     if git_status and git_status.highlight then
47 |       highlight = git_status.highlight
48 |     end
49 |   end
50 |   return {
51 |     text = name,
52 |     highlight = highlight,
53 |   }
54 | end
55 | 
56 | return vim.tbl_deep_extend("force", common, M)
57 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/git_status/init.lua:
--------------------------------------------------------------------------------
  1 | --This file should have all functions that are in the public api and either set
  2 | --or read the state of this source.
  3 | 
  4 | local utils = require("neo-tree.utils")
  5 | local renderer = require("neo-tree.ui.renderer")
  6 | local items = require("neo-tree.sources.git_status.lib.items")
  7 | local events = require("neo-tree.events")
  8 | local manager = require("neo-tree.sources.manager")
  9 | 
 10 | ---@class neotree.sources.GitStatus : neotree.Source
 11 | local M = {
 12 |   name = "git_status",
 13 |   display_name = " 󰊢 Git ",
 14 | }
 15 | 
 16 | local wrap = function(func)
 17 |   return utils.wrap(func, M.name)
 18 | end
 19 | 
 20 | local get_state = function()
 21 |   return manager.get_state(M.name)
 22 | end
 23 | 
 24 | ---Navigate to the given path.
 25 | ---@param path string Path to navigate to. If empty, will navigate to the cwd.
 26 | M.navigate = function(state, path, path_to_reveal, callback, async)
 27 |   state.path = path or state.path
 28 |   state.dirty = false
 29 |   if path_to_reveal then
 30 |     renderer.position.set(state, path_to_reveal)
 31 |   end
 32 |   items.get_git_status(state)
 33 | 
 34 |   if type(callback) == "function" then
 35 |     vim.schedule(callback)
 36 |   end
 37 | end
 38 | 
 39 | M.refresh = function()
 40 |   manager.refresh(M.name)
 41 | end
 42 | 
 43 | ---@class neotree.Config.GitStatus.Renderers : neotree.Config.Renderers
 44 | 
 45 | ---@class (exact) neotree.Config.GitStatus : neotree.Config.Source
 46 | ---@field bind_to_cwd boolean?
 47 | ---@field renderers neotree.Config.GitStatus.Renderers?
 48 | 
 49 | ---Configures the plugin, should be called before the plugin is used.
 50 | ---@param config neotree.Config.GitStatus Configuration table containing any keys that the user
 51 | --wants to change from the defaults. May be empty to accept default values.
 52 | M.setup = function(config, global_config)
 53 |   if config.before_render then
 54 |     --convert to new event system
 55 |     manager.subscribe(M.name, {
 56 |       event = events.BEFORE_RENDER,
 57 |       handler = function(state)
 58 |         local this_state = get_state()
 59 |         if state == this_state then
 60 |           config.before_render(this_state)
 61 |         end
 62 |       end,
 63 |     })
 64 |   end
 65 | 
 66 |   if global_config.enable_refresh_on_write then
 67 |     manager.subscribe(M.name, {
 68 |       event = events.VIM_BUFFER_CHANGED,
 69 |       handler = function(args)
 70 |         if utils.is_real_file(args.afile) then
 71 |           M.refresh()
 72 |         end
 73 |       end,
 74 |     })
 75 |   end
 76 | 
 77 |   if config.bind_to_cwd then
 78 |     manager.subscribe(M.name, {
 79 |       event = events.VIM_DIR_CHANGED,
 80 |       handler = M.refresh,
 81 |     })
 82 |   end
 83 | 
 84 |   if global_config.enable_diagnostics then
 85 |     manager.subscribe(M.name, {
 86 |       event = events.STATE_CREATED,
 87 |       handler = function(state)
 88 |         state.diagnostics_lookup = utils.get_diagnostic_counts()
 89 |       end,
 90 |     })
 91 |     manager.subscribe(M.name, {
 92 |       event = events.VIM_DIAGNOSTIC_CHANGED,
 93 |       handler = wrap(manager.diagnostics_changed),
 94 |     })
 95 |   end
 96 | 
 97 |   --Configure event handlers for modified files
 98 |   if global_config.enable_modified_markers then
 99 |     manager.subscribe(M.name, {
100 |       event = events.VIM_BUFFER_MODIFIED_SET,
101 |       handler = wrap(manager.opened_buffers_changed),
102 |     })
103 |   end
104 | 
105 |   manager.subscribe(M.name, {
106 |     event = events.GIT_EVENT,
107 |     handler = M.refresh,
108 |   })
109 | end
110 | 
111 | return M
112 | 


--------------------------------------------------------------------------------
/lua/neo-tree/sources/git_status/lib/items.lua:
--------------------------------------------------------------------------------
 1 | local renderer = require("neo-tree.ui.renderer")
 2 | local file_items = require("neo-tree.sources.common.file-items")
 3 | local log = require("neo-tree.log")
 4 | local git = require("neo-tree.git")
 5 | 
 6 | local M = {}
 7 | 
 8 | ---Get a table of all open buffers, along with all parent paths of those buffers.
 9 | ---The paths are the keys of the table, and all the values are 'true'.
10 | ---@param state neotree.State
11 | M.get_git_status = function(state)
12 |   if state.loading then
13 |     return
14 |   end
15 |   state.loading = true
16 |   local status_lookup, project_root = git.status(state.git_base, true, state.path)
17 |   state.path = project_root or state.path or vim.fn.getcwd()
18 |   local context = file_items.create_context()
19 |   context.state = state
20 |   -- Create root folder
21 |   local root = file_items.create_item(context, state.path, "directory") --[[@as neotree.FileItem.Directory]]
22 |   root.name = vim.fn.fnamemodify(root.path, ":~")
23 |   root.loaded = true
24 |   root.search_pattern = state.search_pattern
25 |   context.folders[root.path] = root
26 | 
27 |   for path, status in pairs(status_lookup) do
28 |     local success, item = pcall(file_items.create_item, context, path, "file") --[[@as neotree.FileItem.File]]
29 |     item.status = status
30 |     if success then
31 |       item.extra = {
32 |         git_status = status,
33 |       }
34 |     else
35 |       log.error("Error creating item for " .. path .. ": " .. item)
36 |     end
37 |   end
38 | 
39 |   state.git_status_lookup = status_lookup
40 |   state.default_expanded_nodes = {}
41 |   for id, _ in pairs(context.folders) do
42 |     table.insert(state.default_expanded_nodes, id)
43 |   end
44 |   file_items.advanced_sort(root.children, state)
45 |   renderer.show_nodes({ root }, state)
46 |   state.loading = false
47 | end
48 | 
49 | return M
50 | 


--------------------------------------------------------------------------------
/lua/neo-tree/types/components.lua:
--------------------------------------------------------------------------------
 1 | ---@meta
 2 | 
 3 | ---@alias neotree.Renderer fun(config: table, node: NuiTree.Node, state: neotree.State):(neotree.Render.Node|neotree.Render.Node[])
 4 | 
 5 | ---@class (exact) neotree.Render.Node
 6 | ---@field text string The text to display.
 7 | ---@field highlight string The highlight for the text.
 8 | 
 9 | ---@class (exact) neotree.Component
10 | ---@field [1] string?
11 | ---@field enabled boolean?
12 | ---@field highlight string?
13 | 
14 | ---@alias neotree.IconProvider fun(icon: neotree.Render.Node, node: NuiTree.Node, state: neotree.State):(neotree.Render.Node|neotree.Render.Node[]|nil)
15 | 


--------------------------------------------------------------------------------
/lua/neo-tree/types/config.lua:
--------------------------------------------------------------------------------
  1 | ---@meta
  2 | 
  3 | ---@class neotree.Config.Mapping.Options
  4 | ---@field noremap boolean?
  5 | ---@field nowait boolean?
  6 | ---@field desc string?
  7 | 
  8 | ---@class neotree.Config.Window.Command.Configured : neotree.Config.Mapping.Options
  9 | ---@field [1] string?
 10 | ---@field command string?
 11 | ---@field config table?
 12 | 
 13 | ---@class neotree.Config.Source
 14 | ---@field window neotree.Config.Window?
 15 | ---@field renderers neotree.Config.Renderers?
 16 | ---@field commands table<string, neotree.Config.TreeCommand?>?
 17 | ---@field before_render fun(state: neotree.State)?
 18 | 
 19 | ---@class neotree.Config.SourceSelector.Item
 20 | ---@field source string?
 21 | ---@field padding integer|{left:integer,right:integer}?
 22 | ---@field separator string|{left:string,right:string, override?:string}?
 23 | 
 24 | ---@alias neotree.Config.SourceSelector.Separator.Override
 25 | ---|"right"   # When right and left separators meet, only show the right one.
 26 | ---|"left"    # When right and left separators meet, only show the left one.
 27 | ---|"active"  # Only use the left separator on the left of the active tab, and only the right afterwards.
 28 | ---|nil       # Show both separators.
 29 | 
 30 | ---@class neotree.Config.SourceSelector.Separator
 31 | ---@field left string?
 32 | ---@field right string?
 33 | ---@field override neotree.Config.SourceSelector.Separator.Override?
 34 | 
 35 | ---@class neotree.Config.SourceSelector
 36 | ---@field winbar boolean?
 37 | ---@field statusline boolean?
 38 | ---@field show_scrolled_off_parent_node boolean?
 39 | ---@field sources neotree.Config.SourceSelector.Item[]?
 40 | ---@field content_layout? "start"|"end"|"center"
 41 | ---@field tabs_layout? "equal"|"start"|"end"|"center"|"focus"
 42 | ---@field truncation_character string
 43 | ---@field tabs_min_width integer?
 44 | ---@field tabs_max_width integer?
 45 | ---@field padding integer|{left: integer, right:integer}?
 46 | ---@field separator neotree.Config.SourceSelector.Separator?
 47 | ---@field separator_active neotree.Config.SourceSelector.Separator?
 48 | ---@field show_separator_on_edge boolean?
 49 | ---@field highlight_tab string?
 50 | ---@field highlight_tab_active string?
 51 | ---@field highlight_background string?
 52 | ---@field highlight_separator string?
 53 | ---@field highlight_separator_active string?
 54 | 
 55 | ---@class neotree.Config.GitStatusAsync
 56 | ---@field batch_size integer?
 57 | ---@field batch_delay integer?
 58 | ---@field max_lines integer?
 59 | 
 60 | ---@class neotree.Config.Window.Size
 61 | ---@field height string|number?
 62 | ---@field width string|number?
 63 | 
 64 | ---@class neotree.Config.Window.Popup
 65 | ---@field title (fun(state:table):string)?
 66 | ---@field size neotree.Config.Window.Size?
 67 | ---@field border neotree.Config.BorderStyle?
 68 | 
 69 | ---@alias neotree.Config.TreeCommand string|neotree.TreeCommand|neotree.Config.Window.Command.Configured
 70 | 
 71 | ---@class (exact) neotree.Config.Commands
 72 | ---@field [string] function
 73 | 
 74 | ---@class (exact) neotree.Config.Window.Mappings
 75 | ---@field [string] neotree.Config.TreeCommand?
 76 | 
 77 | ---@class neotree.Config.Window
 78 | ---@field position string?
 79 | ---@field width integer?
 80 | ---@field height integer?
 81 | ---@field auto_expand_width boolean?
 82 | ---@field popup neotree.Config.Window.Popup?
 83 | ---@field insert_as "child"|"sibling"|nil
 84 | ---@field mapping_options neotree.Config.Mapping.Options?
 85 | ---@field mappings neotree.Config.Window.Mappings?
 86 | 
 87 | ---@class neotree.Config.Renderers
 88 | ---@field directory neotree.Component.Common[]?
 89 | ---@field file neotree.Component.Common[]?
 90 | ---@field message neotree.Component.Common[]?
 91 | ---@field terminal neotree.Component.Common[]?
 92 | 
 93 | ---@class neotree.Config.ComponentDefaults
 94 | ---@field container neotree.Component.Common.Container?
 95 | ---@field indent neotree.Component.Common.Indent?
 96 | ---@field icon neotree.Component.Common.Icon?
 97 | ---@field modified neotree.Component.Common.Modified?
 98 | ---@field name neotree.Component.Common.Name?
 99 | ---@field git_status neotree.Component.Common.GitStatus?
100 | ---@field file_size neotree.Component.Common.FileSize?
101 | ---@field type neotree.Component.Common.Type?
102 | ---@field last_modified neotree.Component.Common.LastModified?
103 | ---@field created neotree.Component.Common.Created?
104 | ---@field symlink_target neotree.Component.Common.SymlinkTarget?
105 | 
106 | ---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|""
107 | 
108 | ---@alias neotree.Config.SortFunction fun(a: NuiTree.Node, b: NuiTree.Node):boolean?
109 | 
110 | ---@class (exact) neotree.Config.Base
111 | ---@field sources string[]
112 | ---@field add_blank_line_at_top boolean
113 | ---@field auto_clean_after_session_restore boolean
114 | ---@field close_if_last_window boolean
115 | ---@field default_source string
116 | ---@field enable_diagnostics boolean
117 | ---@field enable_git_status boolean
118 | ---@field enable_modified_markers boolean
119 | ---@field enable_opened_markers boolean
120 | ---@field enable_refresh_on_write boolean
121 | ---@field enable_cursor_hijack boolean
122 | ---@field git_status_async boolean
123 | ---@field git_status_async_options neotree.Config.GitStatusAsync
124 | ---@field hide_root_node boolean
125 | ---@field retain_hidden_root_indent boolean
126 | ---@field log_level "trace"|"debug"|"info"|"warn"|"error"|"fatal"|nil
127 | ---@field log_to_file boolean|string
128 | ---@field open_files_in_last_window boolean
129 | ---@field open_files_do_not_replace_types string[]
130 | ---@field open_files_using_relative_paths boolean
131 | ---@field popup_border_style neotree.Config.BorderStyle
132 | ---@field resize_timer_interval integer|-1
133 | ---@field sort_case_insensitive boolean
134 | ---@field sort_function? neotree.Config.SortFunction
135 | ---@field use_popups_for_input boolean
136 | ---@field use_default_mappings boolean
137 | ---@field source_selector neotree.Config.SourceSelector
138 | ---@field event_handlers? neotree.event.Handler[]
139 | ---@field default_component_configs neotree.Config.ComponentDefaults
140 | ---@field renderers neotree.Config.Renderers
141 | ---@field nesting_rules neotree.filenesting.Rule[]
142 | ---@field commands table<string, neotree.Config.TreeCommand?>
143 | ---@field window neotree.Config.Window
144 | ---
145 | ---@field filesystem neotree.Config.Filesystem
146 | ---@field buffers neotree.Config.Buffers
147 | ---@field git_status neotree.Config.GitStatus
148 | ---@field document_symbols neotree.Config.DocumentSymbols
149 | ---@field bind_to_cwd boolean?
150 | 
151 | ---@class (partial) neotree.Config : neotree.Config.Base
152 | 


--------------------------------------------------------------------------------
/lua/neo-tree/types/events.lua:
--------------------------------------------------------------------------------
 1 | ---@meta
 2 | 
 3 | ---@enum neotree.EventName
 4 | local _ = {
 5 |   AFTER_RENDER = "after_render",
 6 |   BEFORE_FILE_ADD = "before_file_add",
 7 |   BEFORE_FILE_DELETE = "before_file_delete",
 8 |   BEFORE_FILE_MOVE = "before_file_move",
 9 |   BEFORE_FILE_RENAME = "before_file_rename",
10 |   BEFORE_RENDER = "before_render",
11 |   FILE_ADDED = "file_added",
12 |   FILE_DELETED = "file_deleted",
13 |   FILE_MOVED = "file_moved",
14 |   FILE_OPENED = "file_opened",
15 |   FILE_OPEN_REQUESTED = "file_open_requested",
16 |   FILE_RENAMED = "file_renamed",
17 |   FS_EVENT = "fs_event",
18 |   GIT_EVENT = "git_event",
19 |   GIT_STATUS_CHANGED = "git_status_changed",
20 |   STATE_CREATED = "state_created",
21 |   NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter",
22 |   NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave",
23 |   NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update",
24 |   NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter",
25 |   NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave",
26 |   NEO_TREE_POPUP_INPUT_READY = "neo_tree_popup_input_ready",
27 |   NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close",
28 |   NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open",
29 |   NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close",
30 |   NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open",
31 |   VIM_AFTER_SESSION_LOAD = "vim_after_session_load",
32 |   VIM_BUFFER_ADDED = "vim_buffer_added",
33 |   VIM_BUFFER_CHANGED = "vim_buffer_changed",
34 |   VIM_BUFFER_DELETED = "vim_buffer_deleted",
35 |   VIM_BUFFER_ENTER = "vim_buffer_enter",
36 |   VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set",
37 |   VIM_COLORSCHEME = "vim_colorscheme",
38 |   VIM_CURSOR_MOVED = "vim_cursor_moved",
39 |   VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed",
40 |   VIM_DIR_CHANGED = "vim_dir_changed",
41 |   VIM_INSERT_LEAVE = "vim_insert_leave",
42 |   VIM_LEAVE = "vim_leave",
43 |   VIM_LSP_REQUEST = "vim_lsp_request",
44 |   VIM_RESIZED = "vim_resized",
45 |   VIM_TAB_CLOSED = "vim_tab_closed",
46 |   VIM_TERMINAL_ENTER = "vim_terminal_enter",
47 |   VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal",
48 |   VIM_WIN_CLOSED = "vim_win_closed",
49 |   VIM_WIN_ENTER = "vim_win_enter",
50 | }
51 | 


--------------------------------------------------------------------------------
/lua/neo-tree/types/fixes/compat-0.10.lua:
--------------------------------------------------------------------------------
 1 | ---@meta
 2 | --- A backport from nightly for v0.10 type checking
 3 | 
 4 | --- @class neotree._vim.api.keyset.create_autocmd.callback_args
 5 | --- @field id integer autocommand id
 6 | --- @field event string name of the triggered event |autocmd-events|
 7 | --- @field group? integer autocommand group id, if any
 8 | --- @field match string expanded value of <amatch>
 9 | --- @field buf integer expanded value of <abuf>
10 | --- @field file string expanded value of <afile>
11 | --- @field data? any arbitrary data passed from |nvim_exec_autocmds()|                       *event-data*
12 | 


--------------------------------------------------------------------------------
/lua/neo-tree/types/fixes/uv.lua:
--------------------------------------------------------------------------------
 1 | ---@meta
 2 | 
 3 | ---@class uv
 4 | ---@field constants {O_RDONLY: integer, O_WRONLY: integer, O_RDWR: integer, O_APPEND: integer, O_CREAT: integer, O_DSYNC: integer, O_EXCL: integer, O_NOCTTY: integer, O_NONBLOCK: integer, O_RSYNC: integer, O_SYNC: integer, O_TRUNC: integer, SOCK_STREAM: integer, SOCK_DGRAM: integer, SOCK_SEQPACKET: integer, SOCK_RAW: integer, SOCK_RDM: integer, AF_UNIX: integer, AF_INET: integer, AF_INET6: integer, AF_IPX: integer, AF_NETLINK: integer, AF_X25: integer, AF_AX25: integer, AF_ATMPVC: integer, AF_APPLETALK: integer, AF_PACKET: integer, AI_ADDRCONFIG: integer, AI_V4MAPPED: integer, AI_ALL: integer, AI_NUMERICHOST: integer, AI_PASSIVE: integer, AI_NUMERICSERV: integer, SIGHUP: integer, SIGINT: integer, SIGQUIT: integer, SIGILL: integer, SIGTRAP: integer, SIGABRT: integer, SIGIOT: integer, SIGBUS: integer, SIGFPE: integer, SIGKILL: integer, SIGUSR1: integer, SIGSEGV: integer, SIGUSR2: integer, SIGPIPE: integer, SIGALRM: integer, SIGTERM: integer, SIGCHLD: integer, SIGSTKFLT: integer, SIGCONT: integer, SIGSTOP: integer, SIGTSTP: integer, SIGTTIN: integer, SIGWINCH: integer, SIGIO: integer, SIGPOLL: integer, SIGXFSZ: integer, SIGVTALRM: integer, SIGPROF: integer, UDP_RECVMMSG: integer, UDP_MMSG_CHUNK: integer, UDP_REUSEADDR: integer, UDP_PARTIAL: integer, UDP_IPV6ONLY: integer, TCP_IPV6ONLY: integer, UDP_MMSG_FREE: integer, SIGSYS: integer, SIGPWR: integer, SIGTTOU: integer, SIGURG: integer, SIGXCPU: integer}
 5 | local uv = {}
 6 | 
 7 | --- Opens path as a directory stream. Returns a handle that the user can pass to
 8 | --- `uv.fs_readdir()`. The `entries` parameter defines the maximum number of entries
 9 | --- that should be returned by each call to `uv.fs_readdir()`.
10 | ---
11 | --- **Returns (sync version):** `luv_dir_t userdata` or `fail`
12 | ---
13 | --- **Returns (async version):** `uv_fs_t userdata`
14 | ---
15 | ---@param  path             string
16 | ---@param  callback         nil
17 | ---@param  entries          integer?
18 | ---@return uv.luv_dir_t|nil dir
19 | ---@return uv.error.message|nil err
20 | ---@return uv.error.name|nil err_name
21 | ---
22 | ---@overload fun(path: string, callback: uv.fs_opendir.callback, entries?: integer):uv.uv_fs_t
23 | function uv.fs_opendir(path, callback, entries) end
24 | 


--------------------------------------------------------------------------------
/lua/neo-tree/ui/inputs.lua:
--------------------------------------------------------------------------------
  1 | local NuiInput = require("nui.input")
  2 | local nt = require("neo-tree")
  3 | local popups = require("neo-tree.ui.popups")
  4 | local events = require("neo-tree.events")
  5 | 
  6 | local M = {}
  7 | 
  8 | ---@param input NuiInput
  9 | ---@param callback function?
 10 | M.show_input = function(input, callback)
 11 |   input:mount()
 12 | 
 13 |   input:map("i", "<esc>", function()
 14 |     vim.cmd("stopinsert")
 15 |     input:unmount()
 16 |   end, { noremap = true })
 17 | 
 18 |   input:map("n", "<esc>", function()
 19 |     input:unmount()
 20 |   end, { noremap = true })
 21 | 
 22 |   input:map("n", "q", function()
 23 |     input:unmount()
 24 |   end, { noremap = true })
 25 | 
 26 |   input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
 27 | 
 28 |   local event = require("nui.utils.autocmd").event
 29 |   input:on({ event.BufLeave, event.BufDelete }, function()
 30 |     input:unmount()
 31 |     if callback then
 32 |       callback()
 33 |     end
 34 |   end, { once = true })
 35 | 
 36 |   if input.prompt_type ~= "confirm" then
 37 |     vim.schedule(function()
 38 |       events.fire_event(events.NEO_TREE_POPUP_INPUT_READY, {
 39 |         bufnr = input.bufnr,
 40 |         winid = input.winid,
 41 |       })
 42 |     end)
 43 |   end
 44 | end
 45 | 
 46 | ---@param message string
 47 | ---@param default_value string?
 48 | ---@param callback function
 49 | ---@param options nui_popup_options?
 50 | ---@param completion string?
 51 | M.input = function(message, default_value, callback, options, completion)
 52 |   if nt.config.use_popups_for_input then
 53 |     local popup_options = popups.popup_options(message, 10, options)
 54 | 
 55 |     local input = NuiInput(popup_options, {
 56 |       prompt = " ",
 57 |       default_value = default_value,
 58 |       on_submit = callback,
 59 |     })
 60 | 
 61 |     M.show_input(input)
 62 |   else
 63 |     local opts = {
 64 |       prompt = message .. "\n",
 65 |       default = default_value,
 66 |     }
 67 |     if vim.opt.cmdheight:get() == 0 then
 68 |       -- NOTE: I really don't know why but letters before the first '\n' is not rendered execpt in noice.nvim
 69 |       --       when vim.opt.cmdheight = 0 <2023-10-24, pysan3>
 70 |       opts.prompt = "Neo-tree Popup\n" .. opts.prompt
 71 |     end
 72 |     if completion then
 73 |       opts.completion = completion
 74 |     end
 75 |     vim.ui.input(opts, callback)
 76 |   end
 77 | end
 78 | 
 79 | ---Blocks if callback is omitted
 80 | ---@param message string
 81 | ---@param callback? fun(confirmed: boolean)
 82 | ---@return boolean? confirmed_if_no_callback
 83 | M.confirm = function(message, callback)
 84 |   if callback then
 85 |     if nt.config.use_popups_for_input then
 86 |       local popup_options = popups.popup_options(message, 10)
 87 | 
 88 |       ---@class NuiInput
 89 |       local input = NuiInput(popup_options, {
 90 |         prompt = " y/n: ",
 91 |         on_close = function()
 92 |           callback(false)
 93 |         end,
 94 |         on_submit = function(value)
 95 |           callback(value == "y" or value == "Y")
 96 |         end,
 97 |       })
 98 | 
 99 |       input.prompt_type = "confirm"
100 |       M.show_input(input)
101 |     else
102 |       callback(vim.fn.confirm(message, "&Yes\n&No") == 1)
103 |     end
104 |   else
105 |     return vim.fn.confirm(message, "&Yes\n&No") == 1
106 |   end
107 | end
108 | 
109 | return M
110 | 


--------------------------------------------------------------------------------
/lua/neo-tree/ui/popups.lua:
--------------------------------------------------------------------------------
  1 | local NuiText = require("nui.text")
  2 | local NuiPopup = require("nui.popup")
  3 | local nt = require("neo-tree")
  4 | local highlights = require("neo-tree.ui.highlights")
  5 | local log = require("neo-tree.log")
  6 | 
  7 | local M = {}
  8 | 
  9 | local winborder_option_exists = vim.fn.exists("&winborder") > 0
 10 | -- These borders will cause errors when trying to display border text with them
 11 | local invalid_borders = { "", "none", "shadow" }
 12 | ---@param title string
 13 | ---@param min_width integer?
 14 | ---@param override_options table?
 15 | M.popup_options = function(title, min_width, override_options)
 16 |   if string.len(title) ~= 0 then
 17 |     title = " " .. title .. " "
 18 |   end
 19 |   min_width = min_width or 30
 20 |   local width = string.len(title) + 2
 21 | 
 22 |   local popup_border_style = nt.config.popup_border_style
 23 |   if popup_border_style == "" then
 24 |     -- Try to use winborder
 25 |     if not winborder_option_exists or vim.tbl_contains(invalid_borders, vim.o.winborder) then
 26 |       popup_border_style = "single"
 27 |     else
 28 |       ---@diagnostic disable-next-line: cast-local-type
 29 |       popup_border_style = vim.o.winborder
 30 |     end
 31 |   end
 32 |   local popup_border_text = NuiText(title, highlights.FLOAT_TITLE)
 33 |   local col = 0
 34 |   -- fix popup position when using multigrid
 35 |   local popup_last_col = vim.api.nvim_win_get_position(0)[2] + width + 2
 36 |   if popup_last_col >= vim.o.columns then
 37 |     col = vim.o.columns - popup_last_col
 38 |   end
 39 |   ---@type nui_popup_options
 40 |   local popup_options = {
 41 |     ns_id = highlights.ns_id,
 42 |     relative = "cursor",
 43 |     position = {
 44 |       row = 1,
 45 |       col = col,
 46 |     },
 47 |     size = width,
 48 |     border = {
 49 |       text = {
 50 |         top = popup_border_text,
 51 |       },
 52 |       ---@diagnostic disable-next-line: assign-type-mismatch
 53 |       style = popup_border_style,
 54 |       highlight = highlights.FLOAT_BORDER,
 55 |     },
 56 |     win_options = {
 57 |       winhighlight = "Normal:"
 58 |         .. highlights.FLOAT_NORMAL
 59 |         .. ",FloatBorder:"
 60 |         .. highlights.FLOAT_BORDER,
 61 |     },
 62 |     buf_options = {
 63 |       bufhidden = "delete",
 64 |       buflisted = false,
 65 |       filetype = "neo-tree-popup",
 66 |     },
 67 |   }
 68 | 
 69 |   if popup_border_style == "NC" then
 70 |     local blank = NuiText(" ", highlights.TITLE_BAR)
 71 |     popup_border_text = NuiText(title, highlights.TITLE_BAR)
 72 |     popup_options.border = {
 73 |       style = { "▕", blank, "▏", "▏", " ", "▔", " ", "▕" },
 74 |       highlight = highlights.FLOAT_BORDER,
 75 |       text = {
 76 |         top = popup_border_text,
 77 |         top_align = "left",
 78 |       },
 79 |     }
 80 |   end
 81 | 
 82 |   if override_options then
 83 |     return vim.tbl_extend("force", popup_options, override_options)
 84 |   else
 85 |     return popup_options
 86 |   end
 87 | end
 88 | 
 89 | ---@param title string
 90 | ---@param message elem_or_list<string|integer>
 91 | ---@param size integer?
 92 | M.alert = function(title, message, size)
 93 |   local lines = {}
 94 |   local max_line_width = title:len()
 95 |   ---@param line any
 96 |   local add_line = function(line)
 97 |     line = tostring(line)
 98 |     if line:len() > max_line_width then
 99 |       max_line_width = line:len()
100 |     end
101 |     table.insert(lines, line)
102 |   end
103 | 
104 |   if type(message) == "table" then
105 |     for _, v in ipairs(message) do
106 |       add_line(v)
107 |     end
108 |   else
109 |     add_line(message)
110 |   end
111 | 
112 |   add_line("")
113 |   add_line(" Press <Escape> or <Enter> to close")
114 | 
115 |   local win_options = M.popup_options(title, 80)
116 |   win_options.zindex = 60
117 |   win_options.size = {
118 |     width = max_line_width + 4,
119 |     height = #lines + 1,
120 |   }
121 |   local win = NuiPopup(win_options)
122 |   win:mount()
123 | 
124 |   local success, msg = pcall(vim.api.nvim_buf_set_lines, win.bufnr, 0, 0, false, lines)
125 |   if success then
126 |     win:map("n", "<esc>", function()
127 |       win:unmount()
128 |     end, { noremap = true })
129 | 
130 |     win:map("n", "<enter>", function()
131 |       win:unmount()
132 |     end, { noremap = true })
133 | 
134 |     local event = require("nui.utils.autocmd").event
135 |     win:on({ event.BufLeave, event.BufDelete }, function()
136 |       win:unmount()
137 |     end, { once = true })
138 | 
139 |     -- why is this necessary?
140 |     vim.api.nvim_set_current_win(win.winid)
141 |   else
142 |     log.error(msg)
143 |     win:unmount()
144 |   end
145 | end
146 | 
147 | return M
148 | 


--------------------------------------------------------------------------------
/lua/neo-tree/ui/windows.lua:
--------------------------------------------------------------------------------
 1 | local locations = {}
 2 | 
 3 | locations.get_location = function(location)
 4 |   local tab = vim.api.nvim_get_current_tabpage()
 5 |   if not locations[tab] then
 6 |     locations[tab] = {}
 7 |   end
 8 |   local loc = locations[tab][location]
 9 |   if loc then
10 |     if loc.winid ~= 0 then
11 |       -- verify the window before we return it
12 |       if not vim.api.nvim_win_is_valid(loc.winid) then
13 |         loc.winid = 0
14 |       end
15 |     end
16 |     return loc
17 |   end
18 |   loc = {
19 |     source = nil,
20 |     name = location,
21 |     winid = 0,
22 |   }
23 |   locations[tab][location] = loc
24 |   return loc
25 | end
26 | 
27 | return locations
28 | 


--------------------------------------------------------------------------------
/lua/neo-tree/utils/_compat.lua:
--------------------------------------------------------------------------------
 1 | local compat = {}
 2 | ---@return boolean
 3 | compat.noref = function()
 4 |   return vim.fn.has("nvim-0.10") == 1 and true or {} --[[@as boolean]]
 5 | end
 6 | 
 7 | ---source: https://github.com/Validark/Lua-table-functions/blob/master/table.lua
 8 | ---Moves elements [f, e] from array a1 into a2 starting at index t
 9 | ---table.move implementation
10 | ---@generic T: table
11 | ---@param a1 T from which to draw elements from range
12 | ---@param f integer starting index for range
13 | ---@param e integer ending index for range
14 | ---@param t integer starting index to move elements from a1 within [f, e]
15 | ---@param a2 T the second table to move these elements to
16 | ---@default a2 = a1
17 | ---@returns a2
18 | local table_move = function(a1, f, e, t, a2)
19 |   a2 = a2 or a1
20 |   t = t + e
21 | 
22 |   for i = e, f, -1 do
23 |     t = t - 1
24 |     a2[t] = a1[i]
25 |   end
26 | 
27 |   return a2
28 | end
29 | ---source:
30 | compat.table_move = table.move or table_move
31 | 
32 | ---@vararg any
33 | local table_pack = function(...)
34 |   -- Returns a new table with parameters stored into an array, with field "n" being the total number of parameters
35 |   local t = { ... }
36 |   ---@diagnostic disable-next-line: inject-field
37 |   t.n = #t
38 |   return t
39 | end
40 | compat.table_pack = table.pack or table_pack
41 | 
42 | return compat
43 | 


--------------------------------------------------------------------------------
/lua/neo-tree/utils/filesize/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2016 Boris Nagaev
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/lua/neo-tree/utils/filesize/filesize.lua:
--------------------------------------------------------------------------------
  1 | -- lua-filesize, generate a human readable string describing the file size
  2 | -- Copyright (c) 2016 Boris Nagaev
  3 | -- See the LICENSE file for terms of use.
  4 | 
  5 | local si = {
  6 |   bits = { "b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb" },
  7 |   bytes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" },
  8 | }
  9 | 
 10 | local function isNan(num)
 11 |   -- http://lua-users.org/wiki/InfAndNanComparisons
 12 |   -- NaN is the only value that doesn't equal itself
 13 |   return num ~= num
 14 | end
 15 | 
 16 | local function roundNumber(num, digits)
 17 |   local fmt = "%." .. digits .. "f"
 18 |   return tonumber(fmt:format(num))
 19 | end
 20 | 
 21 | local function filesize(size, options)
 22 |   -- copy options to o
 23 |   local o = {}
 24 |   for key, value in pairs(options or {}) do
 25 |     o[key] = value
 26 |   end
 27 | 
 28 |   local function setDefault(name, default)
 29 |     if o[name] == nil then
 30 |       o[name] = default
 31 |     end
 32 |   end
 33 |   setDefault("bits", false)
 34 |   setDefault("unix", false)
 35 |   setDefault("base", 2)
 36 |   setDefault("round", o.unix and 1 or 2)
 37 |   setDefault("spacer", o.unix and "" or " ")
 38 |   setDefault("suffixes", {})
 39 |   setDefault("output", "string")
 40 |   setDefault("exponent", -1)
 41 | 
 42 |   assert(not isNan(size), "Invalid arguments")
 43 | 
 44 |   local ceil = (o.base > 2) and 1000 or 1024
 45 |   local negative = (size < 0)
 46 |   if negative then
 47 |     -- Flipping a negative number to determine the size
 48 |     size = -size
 49 |   end
 50 | 
 51 |   local result
 52 | 
 53 |   -- Zero is now a special case because bytes divide by 1
 54 |   if size == 0 then
 55 |     result = {
 56 |       0,
 57 |       o.unix and "" or (o.bits and "b" or "B"),
 58 |     }
 59 |   else
 60 |     -- Determining the exponent
 61 |     if o.exponent == -1 or isNan(o.exponent) then
 62 |       o.exponent = math.floor(math.log(size) / math.log(ceil))
 63 |     end
 64 | 
 65 |     -- Exceeding supported length, time to reduce & multiply
 66 |     if o.exponent > 8 then
 67 |       o.exponent = 8
 68 |     end
 69 | 
 70 |     local val
 71 |     if o.base == 2 then
 72 |       val = size / math.pow(2, o.exponent * 10)
 73 |     else
 74 |       val = size / math.pow(1000, o.exponent)
 75 |     end
 76 | 
 77 |     if o.bits then
 78 |       val = val * 8
 79 |       if val > ceil then
 80 |         val = val / ceil
 81 |         o.exponent = o.exponent + 1
 82 |       end
 83 |     end
 84 | 
 85 |     result = {
 86 |       roundNumber(val, o.exponent > 0 and o.round or 0),
 87 |       (o.base == 10 and o.exponent == 1) and (o.bits and "kb" or "kB")
 88 |         or si[o.bits and "bits" or "bytes"][o.exponent + 1],
 89 |     }
 90 | 
 91 |     if o.unix then
 92 |       result[2] = result[2]:sub(1, 1)
 93 | 
 94 |       if result[2] == "b" or result[2] == "B" then
 95 |         result = {
 96 |           math.floor(result[1]),
 97 |           "",
 98 |         }
 99 |       end
100 |     end
101 |   end
102 | 
103 |   assert(result)
104 | 
105 |   -- Decorating a 'diff'
106 |   if negative then
107 |     result[1] = -result[1]
108 |   end
109 | 
110 |   -- Applying custom suffix
111 |   result[2] = o.suffixes[result[2]] or result[2]
112 | 
113 |   -- Applying custom suffix
114 |   result[2] = o.suffixes[result[2]] or result[2]
115 | 
116 |   -- Returning Array, Object, or String (default)
117 |   if o.output == "array" then
118 |     return result
119 |   elseif o.output == "exponent" then
120 |     return o.exponent
121 |   elseif o.output == "object" then
122 |     return {
123 |       value = result[1],
124 |       suffix = result[2],
125 |     }
126 |   elseif o.output == "string" then
127 |     local value = tostring(result[1])
128 |     value = value:gsub("%.0
quot;, "")
129 |     local suffix = result[2]
130 |     return value .. o.spacer .. suffix
131 |   end
132 | end
133 | 
134 | return filesize
135 | 


--------------------------------------------------------------------------------
/mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | cargo-binstall = "latest"
3 | "cargo:emmylua_check" = "latest"
4 | "cargo:emmylua_ls" = "latest"
5 | lua-language-server = "latest"
6 | 


--------------------------------------------------------------------------------
/plugin/neo-tree.lua:
--------------------------------------------------------------------------------
 1 | if vim.g.loaded_neo_tree == 1 or vim.g.loaded_neo_tree == true then
 2 |   return
 3 | end
 4 | 
 5 | -- Possibly convert this to lua using customlist instead of custom in the future?
 6 | vim.api.nvim_create_user_command("Neotree", function(ctx)
 7 |   require("neo-tree.command")._command(unpack(ctx.fargs))
 8 | end, {
 9 |   nargs = "*",
10 |   complete = "custom,v:lua.require'neo-tree.command'.complete_args",
11 | })
12 | 
13 | ---@param path string? The path to check
14 | ---@return boolean hijacked Whether we hijacked a buffer
15 | local function try_netrw_hijack(path)
16 |   if not path or #path == 0 then
17 |     return false
18 |   end
19 | 
20 |   local stats = (vim.uv or vim.loop).fs_stat(path)
21 |   if not stats or stats.type ~= "directory" then
22 |     return false
23 |   end
24 | 
25 |   return require("neo-tree.setup.netrw").hijack()
26 | end
27 | 
28 | local augroup = vim.api.nvim_create_augroup("NeoTree_NetrwDeferred", { clear = true })
29 | 
30 | -- lazy load until bufenter/netrw hijack
31 | vim.api.nvim_create_autocmd({ "BufEnter" }, {
32 |   group = augroup,
33 |   callback = function(args)
34 |     return vim.g.neotree_watching_bufenter == 1 or try_netrw_hijack(args.file)
35 |   end,
36 | })
37 | 
38 | -- track window order
39 | vim.api.nvim_create_autocmd({ "WinEnter" }, {
40 |   callback = function(ev)
41 |     local win = vim.api.nvim_get_current_win()
42 |     local utils = require("neo-tree.utils")
43 |     if utils.is_floating(win) then
44 |       return
45 |     end
46 | 
47 |     if vim.bo[ev.buf].filetype == "neo-tree" then
48 |       return
49 |     end
50 | 
51 |     local tabid = vim.api.nvim_get_current_tabpage()
52 |     utils.prior_windows[tabid] = utils.prior_windows[tabid] or {}
53 |     local tab_windows = utils.prior_windows[tabid]
54 |     table.insert(tab_windows, win)
55 | 
56 |     -- prune history
57 |     local win_count = #tab_windows
58 |     if win_count > 100 then
59 |       if table.move then
60 |         utils.prior_windows[tabid] =
61 |           require("neo-tree.utils._compat").table_move(tab_windows, 80, win_count, 1, {})
62 |         return
63 |       end
64 | 
65 |       local new_array = {}
66 |       for i = 80, win_count do
67 |         table.insert(new_array, tab_windows[i])
68 |       end
69 |       utils.prior_windows[tabid] = new_array
70 |     end
71 |   end,
72 | })
73 | 
74 | -- setup session loading
75 | vim.api.nvim_create_autocmd("SessionLoadPost", {
76 |   callback = function()
77 |     if require("neo-tree").ensure_config().auto_clean_after_session_restore then
78 |       require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(true)
79 |     end
80 |   end,
81 | })
82 | 
83 | vim.g.loaded_neo_tree = 1
84 | 


--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
 1 | #/bin/bash
 2 | REPO="nvim-neo-tree/neo-tree.nvim"
 3 | LAST_VERSION=$(curl --silent "https://api.github.com/repos/$REPO/releases/latest" | jq -r .tag_name)
 4 | echo "LAST_VERSION=$LAST_VERSION"
 5 | MAJOR=$(cut -d. -f1 <<<"$LAST_VERSION")
 6 | MINOR=$(cut -d. -f2 <<<"$LAST_VERSION")
 7 | echo
 8 | 
 9 | RELEASE_BRANCH="${1:-v${MAJOR}.x}"
10 | echo "RELEASE_BRANCH=$RELEASE_BRANCH"
11 | NEXT_VERSION=$MAJOR.$((MINOR+1))
12 | NEW_VERSION="${2:-${NEXT_VERSION}}"
13 | echo "NEW_VERSION=$NEW_VERSION"
14 | echo
15 | 
16 | read -p "Are you sure you want to publish this release? " -n 1 -r
17 | echo    # (optional) move to a new line
18 | if [[ ! $REPLY =~ ^[Yy]$ ]]
19 | then
20 |     [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 # handle exits from shell or function but don't exit interactive shell
21 | fi
22 | 
23 | git fetch
24 | git checkout main
25 | git pull
26 | echo "Merging to ${RELEASE_BRANCH}"
27 | git checkout $RELEASE_BRANCH
28 | git pull
29 | if git merge --ff-only origin/main; then
30 |   git push
31 |   git tag -a $NEW_VERSION -m "Release ${NEW_VERSION}"
32 |   git push origin $NEW_VERSION
33 |   echo "Creating Release"
34 |   gh release create $NEW_VERSION --generate-notes
35 | else
36 |   echo "RELEASE FAILED! Could not fast-forward release to $RELEASE_BRANCH"
37 | fi
38 | git checkout main
39 | 


--------------------------------------------------------------------------------
/tests/mininit.lua:
--------------------------------------------------------------------------------
 1 | local root_dir = vim.fs.find("neo-tree.nvim", { upward = true, limit = 1 })[1]
 2 | assert(root_dir, "no neo-tree found")
 3 | 
 4 | package.path = ("%s;%s/?.lua;%s/?/init.lua"):format(package.path, root_dir, root_dir)
 5 | vim.opt.packpath:prepend(root_dir .. "/.dependencies")
 6 | 
 7 | vim.opt.rtp = {
 8 |   root_dir,
 9 |   vim.env.VIMRUNTIME,
10 | }
11 | 
12 | -- need this for tests to work
13 | vim.cmd.source(root_dir .. "/plugin/neo-tree.lua")
14 | 


--------------------------------------------------------------------------------
/tests/neo-tree/command/command_current_spec.lua:
--------------------------------------------------------------------------------
  1 | pcall(require, "luacov")
  2 | 
  3 | local Path = require("plenary.path")
  4 | local u = require("tests.utils")
  5 | local verify = require("tests.utils.verify")
  6 | 
  7 | local run_in_current_command = function(command, expected_tree_node)
  8 |   local winid = vim.api.nvim_get_current_win()
  9 | 
 10 |   vim.cmd(command)
 11 |   verify.window_handle_is(winid)
 12 |   verify.buf_name_endswith(string.format("neo-tree filesystem [%s]", winid), 1000)
 13 |   if expected_tree_node then
 14 |     verify.filesystem_tree_node_is(expected_tree_node, winid)
 15 |   end
 16 | end
 17 | 
 18 | local run_close_command = function(command)
 19 |   vim.cmd(command)
 20 |   u.wait_for(function() end, { interval = 200, timeout = 200 })
 21 | end
 22 | 
 23 | describe("Command", function()
 24 |   local test = u.fs.init_test({
 25 |     items = {
 26 |       {
 27 |         name = "foo",
 28 |         type = "dir",
 29 |         items = {
 30 |           {
 31 |             name = "bar",
 32 |             type = "dir",
 33 |             items = {
 34 |               { name = "baz1.txt", type = "file" },
 35 |               { name = "baz2.txt", type = "file", id = "deepfile2" },
 36 |             },
 37 |           },
 38 |         },
 39 |       },
 40 |       { name = "topfile1.txt", type = "file", id = "topfile1" },
 41 |       { name = "topfile2.txt", type = "file", id = "topfile2" },
 42 |     },
 43 |   })
 44 | 
 45 |   test.setup()
 46 | 
 47 |   local fs_tree = test.fs_tree
 48 | 
 49 |   after_each(function()
 50 |     u.clear_environment()
 51 |   end)
 52 | 
 53 |   describe("netrw style:", function()
 54 |     it("`:Neotree current` should show neo-tree in current window", function()
 55 |       local cmd = "Neotree current"
 56 |       run_in_current_command(cmd)
 57 |     end)
 58 | 
 59 |     it(
 60 |       "`:Neotree current reveal` should show neo-tree and reveal file in current window",
 61 |       function()
 62 |         local cmd = "Neotree current reveal"
 63 |         local testfile = fs_tree.lookup["topfile1"].abspath
 64 |         u.editfile(testfile)
 65 |         run_in_current_command(cmd, testfile)
 66 |       end
 67 |     )
 68 | 
 69 |     it("`:Neotree current reveal toggle` should toggle neo-tree in current window", function()
 70 |       local cmd = "Neotree current reveal toggle"
 71 |       local testfile = fs_tree.lookup["topfile1"].abspath
 72 |       u.editfile(testfile)
 73 |       local tree_winid = vim.api.nvim_get_current_win()
 74 | 
 75 |       -- toggle OPEN
 76 |       run_in_current_command(cmd, testfile)
 77 | 
 78 |       -- toggle CLOSE
 79 |       run_close_command(cmd)
 80 |       verify.window_handle_is(tree_winid)
 81 |       verify.buf_name_is(testfile)
 82 |     end)
 83 | 
 84 |     it(
 85 |       "`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is not a parent of file",
 86 |       function()
 87 |         vim.cmd("cd ~")
 88 |         local testfile = fs_tree.lookup["deepfile2"].abspath
 89 |         local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile
 90 |         run_in_current_command(cmd, testfile)
 91 |       end
 92 |     )
 93 | 
 94 |     it(
 95 |       "`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is a parent of file",
 96 |       function()
 97 |         local testfile = fs_tree.lookup["deepfile2"].abspath
 98 |         local testfile_dir = Path:new(testfile):parent().filename
 99 |         vim.cmd(string.format("cd %s", testfile_dir))
100 |         local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile
101 |         run_in_current_command(cmd, testfile)
102 |       end
103 |     )
104 |   end)
105 | 
106 |   test.teardown()
107 | end)
108 | 


--------------------------------------------------------------------------------
/tests/neo-tree/events/queue_spec.lua:
--------------------------------------------------------------------------------
 1 | pcall(require, "luacov")
 2 | 
 3 | describe("Event queue", function()
 4 |   it("should return data when handled = true", function()
 5 |     local events = require("neo-tree.events")
 6 |     events.subscribe({
 7 |       event = "test",
 8 |       handler = function()
 9 |         return { data = "first" }
10 |       end,
11 |     })
12 |     events.subscribe({
13 |       event = "test",
14 |       handler = function()
15 |         return { handled = true, data = "second" }
16 |       end,
17 |     })
18 |     events.subscribe({
19 |       event = "test",
20 |       handler = function()
21 |         return { data = "third" }
22 |       end,
23 |     })
24 |     local result = events.fire_event("test") or {}
25 |     local data = result.data
26 |     assert.are.same("second", data)
27 |   end)
28 | end)
29 | 


--------------------------------------------------------------------------------
/tests/neo-tree/hacks/hacks_spec.lua:
--------------------------------------------------------------------------------
 1 | local u = require("tests.utils")
 2 | local verify = require("tests.utils.verify")
 3 | describe("Opening buffers in neo-tree window", function()
 4 |   -- Just make sure we start all tests in the expected state
 5 |   before_each(function()
 6 |     u.eq(1, #vim.api.nvim_list_wins())
 7 |     u.eq(1, #vim.api.nvim_list_tabpages())
 8 |   end)
 9 | 
10 |   after_each(function()
11 |     u.clear_environment()
12 |   end)
13 | 
14 |   local width = 33
15 |   describe("should automatically redirect to other buffers", function()
16 |     it("without changing our own width", function()
17 |       require("neo-tree").setup({
18 |         window = {
19 |           width = width,
20 |         },
21 |       })
22 |       vim.cmd("e test.txt")
23 |       vim.cmd("Neotree")
24 |       local neotree = vim.api.nvim_get_current_win()
25 |       assert.are.equal(width, vim.api.nvim_win_get_width(neotree))
26 | 
27 |       vim.cmd("bnext")
28 |       verify.schedule(function()
29 |         return assert.are.equal(width, vim.api.nvim_win_get_width(neotree))
30 |       end, nil, "width should remain 33")
31 |     end)
32 |   end)
33 | end)
34 | 


--------------------------------------------------------------------------------
/tests/neo-tree/keymap/normalization_spec.lua:
--------------------------------------------------------------------------------
 1 | local helper = require("neo-tree.setup.mapping-helper")
 2 | describe("keymap normalization", function()
 3 |   it("passes basic tests", function()
 4 |     local tests = {
 5 |       { "<BS>", "<bs>" },
 6 |       { "<Backspace>", "<bs>" },
 7 |       { "<Enter>", "<cr>" },
 8 |       { "<C-W>", "<c-W>" },
 9 |       { "<A-q>", "<m-q>" },
10 |       { "<C-Left>", "<c-left>" },
11 |       { "<C-Right>", "<c-right>" },
12 |       { "<C-Up>", "<c-up>" },
13 |     }
14 |     for _, test in ipairs(tests) do
15 |       local key = helper.normalize_map_key(test[1])
16 |       assert(key == test[2], string.format("%s != %s", key, test[2]))
17 |     end
18 |   end)
19 |   it("allows for proper merging", function()
20 |     local defaults = helper.normalize_mappings({
21 |       ["n"] = "n",
22 |       ["<Esc>"] = "escape",
23 |       ["<C-j>"] = "j",
24 |       ["<c-J>"] = "capital_j",
25 |       ["a"] = "keep_this",
26 |     })
27 |     local new = helper.normalize_mappings({
28 |       ["n"] = "n",
29 |       ["<ESC>"] = "escape",
30 |       ["<c-j>"] = "j",
31 |       ["b"] = "override_this",
32 |     })
33 |     local merged = vim.tbl_deep_extend("force", defaults, new)
34 |     assert.are.same({
35 |       ["n"] = "n",
36 |       ["<esc>"] = "escape",
37 |       ["<c-j>"] = "j",
38 |       ["<c-J>"] = "capital_j",
39 |       ["a"] = "keep_this",
40 |       ["b"] = "override_this",
41 |     }, merged)
42 |   end)
43 | end)
44 | 


--------------------------------------------------------------------------------
/tests/neo-tree/manager/state_spec.lua:
--------------------------------------------------------------------------------
1 | describe("manager state", function()
2 |   it("can be retrieved at startup", function()
3 |     local fs_state = require("neo-tree.sources.manager").get_state("filesystem")
4 |     local buffers_state = require("neo-tree.sources.manager").get_state("buffers")
5 |     assert.are_equal(type(fs_state), "table")
6 |     assert.are_equal(type(buffers_state), "table")
7 |   end)
8 | end)
9 | 


--------------------------------------------------------------------------------
/tests/neo-tree/qol_spec.lua:
--------------------------------------------------------------------------------
 1 | local u = require("tests.utils")
 2 | local verify = require("tests.utils.verify")
 3 | describe("Neo-tree should be able to track previous windows", function()
 4 |   -- Just make sure we start all tests in the expected state
 5 |   before_each(function()
 6 |     u.eq(1, #vim.api.nvim_list_wins())
 7 |     u.eq(1, #vim.api.nvim_list_tabpages())
 8 |   end)
 9 | 
10 |   after_each(function()
11 |     u.clear_environment()
12 |   end)
13 | 
14 |   it("before opening", function()
15 |     vim.cmd.vsplit()
16 |     vim.cmd.split()
17 |     vim.cmd.wincmd("l")
18 |     local win = vim.api.nvim_get_current_win()
19 |     verify.schedule(function()
20 |       local prior_windows =
21 |         require("neo-tree.utils").prior_windows[vim.api.nvim_get_current_tabpage()]
22 |       return assert.are.same(win, prior_windows[#prior_windows])
23 |     end)
24 |   end)
25 | end)
26 | 


--------------------------------------------------------------------------------
/tests/neo-tree/sources/container_spec.lua:
--------------------------------------------------------------------------------
  1 | pcall(require, "luacov")
  2 | 
  3 | local ns_id = require("neo-tree.ui.highlights").ns_id
  4 | local u = require("tests.utils")
  5 | 
  6 | local config = {
  7 |   renderers = {
  8 |     directory = {
  9 |       {
 10 |         "container",
 11 |         content = {
 12 |           { "indent", zindex = 10 },
 13 |           { "icon", zindex = 10 },
 14 |           { "name", zindex = 10 },
 15 |           { "name", zindex = 5, align = "right" },
 16 |         },
 17 |       },
 18 |     },
 19 |     file = {
 20 |       {
 21 |         "container",
 22 |         content = {
 23 |           { "indent", zindex = 10 },
 24 |           { "icon", zindex = 10 },
 25 |           { "name", zindex = 10 },
 26 |           { "name", zindex = 20, align = "right" },
 27 |         },
 28 |       },
 29 |     },
 30 |   },
 31 |   window = {
 32 |     width = 40,
 33 |   },
 34 | }
 35 | 
 36 | local config_right = {
 37 |   renderers = {
 38 |     directory = {
 39 |       {
 40 |         "container",
 41 |         enable_character_fade = false,
 42 |         content = {
 43 |           { "indent", zindex = 10, align = "right" },
 44 |           { "icon", zindex = 10, align = "right" },
 45 |           { "name", zindex = 10, align = "right" },
 46 |         },
 47 |       },
 48 |     },
 49 |     file = {
 50 |       {
 51 |         "container",
 52 |         enable_character_fade = false,
 53 |         content = {
 54 |           { "indent", zindex = 10, align = "right" },
 55 |           { "icon", zindex = 10, align = "right" },
 56 |           { "name", zindex = 10, align = "right" },
 57 |         },
 58 |       },
 59 |     },
 60 |   },
 61 |   window = {
 62 |     width = 40,
 63 |   },
 64 | }
 65 | 
 66 | local test_dir = {
 67 |   items = {
 68 |     {
 69 |       name = "foo",
 70 |       type = "dir",
 71 |       items = {
 72 |         {
 73 |           name = "bar",
 74 |           type = "dir",
 75 |           items = {
 76 |             { name = "bar1.txt", type = "file" },
 77 |             { name = "bar2.txt", type = "file" },
 78 |           },
 79 |         },
 80 |         { name = "foo1.lua", type = "file" },
 81 |       },
 82 |     },
 83 |     { name = "bazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbaz", type = "dir" },
 84 |     { name = "1.md", type = "file" },
 85 |   },
 86 | }
 87 | 
 88 | describe("sources/components/container", function()
 89 |   local req_switch = u.get_require_switch()
 90 | 
 91 |   local test = u.fs.init_test(test_dir)
 92 |   test.setup()
 93 | 
 94 |   after_each(function()
 95 |     if req_switch then
 96 |       req_switch.restore()
 97 |     end
 98 | 
 99 |     u.clear_environment()
100 |   end)
101 | 
102 |   describe("should expand to width", function()
103 |     for pow = 4, 8 do
104 |       it(2 ^ pow, function()
105 |         config.window.width = 2 ^ pow
106 |         require("neo-tree").setup(config)
107 |         vim.cmd([[Neotree focus]])
108 |         u.wait_for(function()
109 |           return vim.bo.filetype == "neo-tree"
110 |         end)
111 | 
112 |         assert.equals(vim.bo.filetype, "neo-tree")
113 | 
114 |         local width = vim.api.nvim_win_get_width(0)
115 |         local lines = vim.api.nvim_buf_get_lines(0, 2, -1, false)
116 |         for _, line in ipairs(lines) do
117 |           assert.is_true(#line >= width)
118 |         end
119 |       end)
120 |     end
121 |   end)
122 | 
123 |   describe("right-align should matches width", function()
124 |     for pow = 4, 8 do
125 |       it(2 ^ pow, function()
126 |         config_right.window.width = 2 ^ pow
127 |         require("neo-tree").setup(config_right)
128 |         vim.cmd([[Neotree focus]])
129 |         u.wait_for(function()
130 |           return vim.bo.filetype == "neo-tree"
131 |         end)
132 | 
133 |         assert.equals(vim.bo.filetype, "neo-tree")
134 | 
135 |         local width = vim.api.nvim_win_get_width(0)
136 |         local lines = vim.api.nvim_buf_get_lines(0, 1, -1, false)
137 |         for _, line in ipairs(lines) do
138 |           line = vim.fn.trim(line, " ", 2)
139 |           assert.equals(width, vim.fn.strchars(line))
140 |         end
141 |       end)
142 |     end
143 |   end)
144 | 
145 |   test.teardown()
146 | end)
147 | 


--------------------------------------------------------------------------------
/tests/neo-tree/sources/filesystem/filesystem_netrw_hijack_spec.lua:
--------------------------------------------------------------------------------
 1 | pcall(require, "luacov")
 2 | 
 3 | local u = require("tests.utils")
 4 | local verify = require("tests.utils.verify")
 5 | 
 6 | describe("Filesystem netrw hijack", function()
 7 |   after_each(function()
 8 |     u.clear_environment()
 9 |   end)
10 | 
11 |   it("does not interfere with netrw when disabled", function()
12 |     require("neo-tree").setup({
13 |       filesystem = {
14 |         hijack_netrw_behavior = "disabled",
15 |         window = {
16 |           position = "left",
17 |         },
18 |       },
19 |     })
20 | 
21 |     vim.cmd("edit .")
22 | 
23 |     assert(#vim.api.nvim_list_wins() == 1, "there should only be one window")
24 | 
25 |     verify.after(100, function()
26 |       local name = vim.api.nvim_buf_get_name(0)
27 |       return name ~= "neo-tree filesystem [1]"
28 |     end, "the buffer should not be neo-tree")
29 |   end)
30 | 
31 |   it("opens in sidebar when behavior is open_default", function()
32 |     local file = "Makefile"
33 |     vim.cmd("edit " .. file)
34 | 
35 |     require("neo-tree").setup({
36 |       filesystem = {
37 |         hijack_netrw_behavior = "open_default",
38 |         window = {
39 |           position = "left",
40 |         },
41 |       },
42 |     })
43 | 
44 |     vim.cmd("edit .")
45 | 
46 |     verify.eventually(200, function()
47 |       return #vim.api.nvim_list_wins() == 2
48 |     end, "there should be two windows")
49 | 
50 |     verify.buf_name_endswith("neo-tree filesystem [1]")
51 | 
52 |     verify.eventually(100, function()
53 |       local expected_buf_name = "Makefile"
54 |       local buf_at_2 = vim.api.nvim_win_get_buf(vim.fn.win_getid(2))
55 |       local name_at_2 = vim.api.nvim_buf_get_name(buf_at_2)
56 |       if name_at_2:sub(-#expected_buf_name) == expected_buf_name then
57 |         return true
58 |       else
59 |         return false
60 |       end
61 |     end, file .. " is not at window 2")
62 |   end)
63 | 
64 |   -- This test is flaky and usually fails in github actions but not always
65 |   -- so I'm disabling it for now.
66 |   -- TODO: fix this test
67 |   --
68 |   --it("opens in in splits when behavior is open_current", function()
69 |   --  local file = "Makefile"
70 |   --  vim.cmd("edit " .. file)
71 | 
72 |   --  require("neo-tree").setup({
73 |   --    filesystem = {
74 |   --      hijack_netrw_behavior = "open_current",
75 |   --    },
76 |   --  })
77 | 
78 |   --  assert(#vim.api.nvim_list_wins() == 1, "Test should start with one window")
79 | 
80 |   --  vim.cmd("split .")
81 | 
82 |   --  verify.eventually(200, function()
83 |   --    if #vim.api.nvim_list_wins() ~= 2 then
84 |   --      return false
85 |   --    end
86 |   --    return vim.bo[0].filetype == "neo-tree"
87 |   --  end, "neotree is not in the second window")
88 |   --end)
89 | end)
90 | 


--------------------------------------------------------------------------------
/tests/neo-tree/sources/manager_spec.lua:
--------------------------------------------------------------------------------
  1 | pcall(require, "luacov")
  2 | 
  3 | local u = require("tests.utils")
  4 | local verify = require("tests.utils.verify")
  5 | 
  6 | local manager = require("neo-tree.sources.manager")
  7 | 
  8 | local get_dirs = function(winid)
  9 |   winid = winid or vim.api.nvim_get_current_win()
 10 |   local tabnr = vim.api.nvim_tabpage_get_number(vim.api.nvim_win_get_tabpage(winid))
 11 |   local winnr = vim.api.nvim_win_get_number(winid)
 12 |   return {
 13 |     win = vim.fn.getcwd(winnr),
 14 |     tab = vim.fn.getcwd(-1, tabnr),
 15 |     global = vim.fn.getcwd(-1, -1),
 16 |   }
 17 | end
 18 | 
 19 | local get_state_for_tab = function(tabid)
 20 |   for _, state in ipairs(manager._get_all_states()) do
 21 |     if state.tabid == tabid then
 22 |       return state
 23 |     end
 24 |   end
 25 | end
 26 | 
 27 | local get_tabnr = function(tabid)
 28 |   return vim.api.nvim_tabpage_get_number(tabid or vim.api.nvim_get_current_tabpage())
 29 | end
 30 | 
 31 | describe("Manager", function()
 32 |   local test = u.fs.init_test({
 33 |     items = {
 34 |       {
 35 |         name = "foo",
 36 |         type = "dir",
 37 |         items = {
 38 |           { name = "foofile1.txt", type = "file" },
 39 |         },
 40 |       },
 41 |       { name = "topfile1.txt", type = "file", id = "topfile1" },
 42 |     },
 43 |   })
 44 | 
 45 |   test.setup()
 46 | 
 47 |   local fs_tree = test.fs_tree
 48 | 
 49 |   -- Just make sure we start all tests in the expected state
 50 |   before_each(function()
 51 |     u.eq(1, #vim.api.nvim_list_wins())
 52 |     u.eq(1, #vim.api.nvim_list_tabpages())
 53 |     vim.cmd.lcd(fs_tree.abspath)
 54 |     vim.cmd.tcd(fs_tree.abspath)
 55 |     vim.cmd.cd(fs_tree.abspath)
 56 |   end)
 57 | 
 58 |   after_each(function()
 59 |     u.clear_environment()
 60 |   end)
 61 | 
 62 |   local setup_2_tabs = function()
 63 |     -- create 2 tabs
 64 |     local tab1 = vim.api.nvim_get_current_tabpage()
 65 |     local win1 = vim.api.nvim_get_current_win()
 66 |     vim.cmd.tabnew()
 67 |     local tab2 = vim.api.nvim_get_current_tabpage()
 68 |     local win2 = vim.api.nvim_get_current_win()
 69 |     u.neq(tab2, tab1)
 70 |     u.neq(win2, win1)
 71 | 
 72 |     -- set different directories
 73 |     vim.api.nvim_set_current_tabpage(tab2)
 74 |     local base_dir = vim.fn.getcwd()
 75 |     vim.cmd.tcd("foo")
 76 |     local new_dir = vim.fn.getcwd()
 77 | 
 78 |     -- open neo-tree
 79 |     vim.api.nvim_set_current_tabpage(tab1)
 80 |     vim.cmd.Neotree("show")
 81 |     vim.api.nvim_set_current_tabpage(tab2)
 82 |     vim.cmd.Neotree("show")
 83 | 
 84 |     return {
 85 |       tab1 = tab1,
 86 |       tab2 = tab2,
 87 |       win1 = win1,
 88 |       win2 = win2,
 89 |       tab1_dir = base_dir,
 90 |       tab2_dir = new_dir,
 91 |     }
 92 |   end
 93 | 
 94 |   it("should respect changed tab cwd", function()
 95 |     local ctx = setup_2_tabs()
 96 | 
 97 |     local state1 = get_state_for_tab(ctx.tab1)
 98 |     local state2 = get_state_for_tab(ctx.tab2)
 99 |     u.eq(ctx.tab1_dir, manager.get_cwd(state1))
100 |     u.eq(ctx.tab2_dir, manager.get_cwd(state2))
101 |   end)
102 | 
103 |   it("should have correct tab cwd after  tabs order is changed", function()
104 |     local ctx = setup_2_tabs()
105 | 
106 |     -- tab numbers should be the same as ids
107 |     u.eq(1, get_tabnr(ctx.tab1))
108 |     u.eq(2, get_tabnr(ctx.tab2))
109 | 
110 |     -- swap tabs
111 |     vim.cmd.tabfirst()
112 |     vim.cmd.tabmove("+1")
113 | 
114 |     -- make sure tabs have been swapped
115 |     u.eq(2, get_tabnr(ctx.tab1))
116 |     u.eq(1, get_tabnr(ctx.tab2))
117 | 
118 |     -- verify that tab dirs are the same as nvim tab cwd
119 |     local state1 = get_state_for_tab(ctx.tab1)
120 |     local state2 = get_state_for_tab(ctx.tab2)
121 |     u.eq(get_dirs(ctx.win1).tab, manager.get_cwd(state1))
122 |     u.eq(get_dirs(ctx.win2).tab, manager.get_cwd(state2))
123 |   end)
124 | end)
125 | 


--------------------------------------------------------------------------------
/tests/neo-tree/sources/navigate_spec.lua:
--------------------------------------------------------------------------------
 1 | pcall(require, "luacov")
 2 | 
 3 | local uv = vim.uv or vim.loop
 4 | 
 5 | ---Return all sources inside "lua/neo-tree/sources"
 6 | ---@return string[] # name of sources found
 7 | local function find_all_sources()
 8 |   local base_dir = "lua/neo-tree/sources"
 9 |   local result = {}
10 |   local fd = uv.fs_scandir(base_dir)
11 |   while fd do
12 |     local name, typ = uv.fs_scandir_next(fd)
13 |     if not name then
14 |       break
15 |     end
16 |     if typ == "directory" then
17 |       local ok, mod = pcall(require, "neo-tree.sources." .. name)
18 |       if ok and mod.name then
19 |         result[#result + 1] = name
20 |       end
21 |     end
22 |   end
23 |   return result
24 | end
25 | 
26 | describe("sources.navigate(...: #<nparams>)", function()
27 |   it("neo-tree.sources.filesystem.navigate exists", function()
28 |     local ok, mod = pcall(require, "neo-tree.sources.filesystem")
29 |     assert.is_true(ok)
30 |     assert.not_nil(mod.navigate)
31 |   end)
32 |   local filesystem_navigate_nparams =
33 |     debug.getinfo(require("neo-tree.sources.filesystem").navigate).nparams
34 |   it("neo-tree.sources.filesystem.navigate is a func and has args", function()
35 |     assert.not_nil(filesystem_navigate_nparams)
36 |     assert.is_true(filesystem_navigate_nparams > 0)
37 |   end)
38 |   for _, source in ipairs(find_all_sources()) do
39 |     describe(string.format("Test: %s.navigate", source), function()
40 |       it(source .. ".navigate is able to require and exists", function()
41 |         local ok, mod = pcall(require, "neo-tree.sources." .. source)
42 |         assert.is_true(ok)
43 |         assert.not_nil(mod.navigate)
44 |       end)
45 |       it(source .. ".navigate has same num of args as filesystem", function()
46 |         local nparams = debug.getinfo(require("neo-tree.sources." .. source).navigate).nparams
47 |         assert.are.equal(filesystem_navigate_nparams, nparams)
48 |       end)
49 |     end)
50 |   end
51 | end)
52 | 


--------------------------------------------------------------------------------
/tests/neo-tree/ui/icons_spec.lua:
--------------------------------------------------------------------------------
  1 | pcall(require, "luacov")
  2 | 
  3 | local ns_id = require("neo-tree.ui.highlights").ns_id
  4 | local u = require("tests.utils")
  5 | 
  6 | describe("ui/icons", function()
  7 |   local req_switch = u.get_require_switch()
  8 | 
  9 |   local test = u.fs.init_test({
 10 |     items = {
 11 |       {
 12 |         name = "foo",
 13 |         type = "dir",
 14 |         items = {
 15 |           {
 16 |             name = "bar",
 17 |             type = "dir",
 18 |             items = {
 19 |               { name = "bar1.txt", type = "file" },
 20 |               { name = "bar2.txt", type = "file" },
 21 |             },
 22 |           },
 23 |           { name = "foo1.lua", type = "file" },
 24 |         },
 25 |       },
 26 |       { name = "baz", type = "dir" },
 27 |       { name = "1.md", type = "file" },
 28 |     },
 29 |   })
 30 | 
 31 |   test.setup()
 32 | 
 33 |   local fs_tree = test.fs_tree
 34 | 
 35 |   after_each(function()
 36 |     if req_switch then
 37 |       req_switch.restore()
 38 |     end
 39 | 
 40 |     u.clear_environment()
 41 |   end)
 42 | 
 43 |   describe("w/ default_config", function()
 44 |     before_each(function()
 45 |       require("neo-tree").setup({})
 46 |     end)
 47 | 
 48 |     it("works w/o nvim-web-devicons", function()
 49 |       req_switch.disable_package("nvim-web-devicons")
 50 | 
 51 |       vim.cmd([[:Neotree focus]])
 52 |       u.wait_for_neo_tree()
 53 | 
 54 |       local winid = vim.api.nvim_get_current_win()
 55 |       local bufnr = vim.api.nvim_win_get_buf(winid)
 56 | 
 57 |       u.assert_buf_lines(bufnr, {
 58 |         string.format("  %s", fs_tree.abspath):sub(1, 42),
 59 |         "    baz",
 60 |         "    foo",
 61 |         "   * 1.md",
 62 |       })
 63 | 
 64 |       vim.api.nvim_win_set_cursor(winid, { 2, 0 })
 65 |       u.feedkeys("<CR>")
 66 | 
 67 |       vim.api.nvim_win_set_cursor(winid, { 3, 0 })
 68 |       u.feedkeys("<CR>")
 69 | 
 70 |       vim.wait(100)
 71 | 
 72 |       u.assert_buf_lines(bufnr, {
 73 |         string.format("  %s", fs_tree.abspath):sub(1, 42),
 74 |         "   󰉖 baz",
 75 |         "    foo",
 76 |         "   │  bar",
 77 |         "   └ * foo1.lua",
 78 |         "   * 1.md",
 79 |       })
 80 | 
 81 |       u.assert_highlight(bufnr, ns_id, 1, " ", "NeoTreeDirectoryIcon")
 82 |       u.assert_highlight(bufnr, ns_id, 2, "󰉖 ", "NeoTreeDirectoryIcon")
 83 |       u.assert_highlight(bufnr, ns_id, 4, " ", "NeoTreeDirectoryIcon")
 84 |       u.assert_highlight(bufnr, ns_id, 5, "* ", "NeoTreeFileIcon")
 85 |     end)
 86 | 
 87 |     it("works w/ nvim-web-devicons", function()
 88 |       vim.cmd([[:Neotree focus]])
 89 |       u.wait_for_neo_tree()
 90 | 
 91 |       local winid = vim.api.nvim_get_current_win()
 92 |       local bufnr = vim.api.nvim_win_get_buf(winid)
 93 | 
 94 |       u.assert_buf_lines(bufnr, {
 95 |         vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40),
 96 |         "    baz",
 97 |         "    foo",
 98 |         "    1.md",
 99 |       })
100 | 
101 |       vim.api.nvim_win_set_cursor(winid, { 2, 0 })
102 |       u.feedkeys("<CR>")
103 | 
104 |       vim.api.nvim_win_set_cursor(winid, { 3, 0 })
105 |       u.feedkeys("<CR>")
106 | 
107 |       vim.wait(100)
108 | 
109 |       u.assert_buf_lines(bufnr, {
110 |         vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40),
111 |         "   󰉖 baz",
112 |         "    foo",
113 |         "   │  bar",
114 |         "   └  foo1.lua",
115 |         "    1.md",
116 |       })
117 | 
118 |       u.assert_highlight(bufnr, ns_id, 1, " ", "NeoTreeDirectoryIcon")
119 |       u.assert_highlight(bufnr, ns_id, 2, "󰉖 ", "NeoTreeDirectoryIcon")
120 |       u.assert_highlight(bufnr, ns_id, 4, " ", "NeoTreeDirectoryIcon")
121 | 
122 |       local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, " ")
123 |       u.eq(#extmarks, 1)
124 |       u.neq(extmarks[1][4].hl_group, "NeoTreeFileIcon")
125 |     end)
126 |   end)
127 | 
128 |   describe("custom config", function()
129 |     local config
130 |     before_each(function()
131 |       config = {
132 |         default_component_configs = {
133 |           icon = {
134 |             folder_closed = "c",
135 |             folder_open = "o",
136 |             folder_empty = "e",
137 |             default = "f",
138 |             highlight = "TestNeoTreeFileIcon",
139 |           },
140 |         },
141 |       }
142 | 
143 |       require("neo-tree").setup(config)
144 |     end)
145 | 
146 |     it("works w/o nvim-web-devicons", function()
147 |       req_switch.disable_package("nvim-web-devicons")
148 | 
149 |       vim.cmd([[:Neotree focus]])
150 |       u.wait_for_neo_tree()
151 | 
152 |       local winid = vim.api.nvim_get_current_win()
153 |       local bufnr = vim.api.nvim_win_get_buf(winid)
154 | 
155 |       u.assert_buf_lines(bufnr, {
156 |         string.format(" o %s", fs_tree.abspath):sub(1, 40),
157 |         "   c baz",
158 |         "   c foo",
159 |         "   f 1.md",
160 |       })
161 | 
162 |       vim.api.nvim_win_set_cursor(winid, { 2, 0 })
163 |       u.feedkeys("<CR>")
164 | 
165 |       vim.api.nvim_win_set_cursor(winid, { 3, 0 })
166 |       u.feedkeys("<CR>")
167 | 
168 |       vim.wait(100)
169 | 
170 |       u.assert_buf_lines(bufnr, {
171 |         string.format(" o %s", fs_tree.abspath):sub(1, 40),
172 |         "   e baz",
173 |         "   o foo",
174 |         "   │ c bar",
175 |         "   └ f foo1.lua",
176 |         "   f 1.md",
177 |       })
178 | 
179 |       u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon")
180 |       u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon")
181 |       u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon")
182 |       u.assert_highlight(bufnr, ns_id, 5, "f ", config.default_component_configs.icon.highlight)
183 |     end)
184 | 
185 |     it("works w/ nvim-web-devicons", function()
186 |       vim.cmd([[:Neotree focus]])
187 |       u.wait_for_neo_tree()
188 | 
189 |       local winid = vim.api.nvim_get_current_win()
190 |       local bufnr = vim.api.nvim_win_get_buf(winid)
191 | 
192 |       u.assert_buf_lines(bufnr, {
193 |         vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40),
194 |         "   c baz",
195 |         "   c foo",
196 |         "    1.md",
197 |       })
198 | 
199 |       vim.api.nvim_win_set_cursor(winid, { 2, 0 })
200 |       u.feedkeys("<CR>")
201 | 
202 |       vim.api.nvim_win_set_cursor(winid, { 3, 0 })
203 |       u.feedkeys("<CR>")
204 | 
205 |       vim.wait(100)
206 | 
207 |       u.assert_buf_lines(bufnr, {
208 |         vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40),
209 |         "   e baz",
210 |         "   o foo",
211 |         "   │ c bar",
212 |         "   └  foo1.lua",
213 |         "    1.md",
214 |       })
215 | 
216 |       u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon")
217 |       u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon")
218 |       u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon")
219 | 
220 |       local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, " ")
221 |       u.eq(#extmarks, 1)
222 |       u.neq(extmarks[1][4].hl_group, config.default_component_configs.icon.highlight)
223 |     end)
224 |   end)
225 | 
226 |   test.teardown()
227 | end)
228 | 


--------------------------------------------------------------------------------
/tests/neo-tree/utils/path_spec.lua:
--------------------------------------------------------------------------------
 1 | pcall(require, "luacov")
 2 | local utils = require("neo-tree.utils")
 3 | 
 4 | describe("is_subpath", function()
 5 |   local common_tests = function()
 6 |     -- Relative paths
 7 |     assert.are.same(true, utils.is_subpath("a", "a/subpath"))
 8 |     assert.are.same(false, utils.is_subpath("a", "b/c"))
 9 |     assert.are.same(false, utils.is_subpath("a", "b"))
10 |   end
11 |   it("should work with unix paths", function()
12 |     local old = utils.is_windows
13 |     utils.is_windows = false
14 |     common_tests()
15 |     assert.are.same(true, utils.is_subpath("/a", "/a/subpath"))
16 |     assert.are.same(false, utils.is_subpath("/a", "/b/c"))
17 | 
18 |     -- Edge cases
19 |     assert.are.same(false, utils.is_subpath("", ""))
20 |     assert.are.same(true, utils.is_subpath("/", "/"))
21 | 
22 |     -- Paths with trailing slashes
23 |     assert.are.same(true, utils.is_subpath("/a/", "/a/subpath"))
24 |     assert.are.same(true, utils.is_subpath("/a/", "/a/subpath/"))
25 |     assert.are.same(true, utils.is_subpath("/a", "/a/subpath"))
26 |     assert.are.same(true, utils.is_subpath("/a", "/a/subpath/"))
27 | 
28 |     -- Paths with different casing
29 |     assert.are.same(true, utils.is_subpath("/TeSt", "/TeSt/subpath"))
30 |     assert.are.same(false, utils.is_subpath("/A", "/a/subpath"))
31 |     assert.are.same(false, utils.is_subpath("/A", "/a/subpath"))
32 |     utils.is_windows = old
33 |   end)
34 |   it("should work on windows paths", function()
35 |     local old = utils.is_windows
36 |     utils.is_windows = true
37 |     common_tests()
38 |     assert.are.same(true, utils.is_subpath("C:", "C:"))
39 |     assert.are.same(false, utils.is_subpath("C:", "D:"))
40 |     assert.are.same(true, utils.is_subpath("C:/A", [[C:\A]]))
41 | 
42 |     -- Test Windows paths with backslashes
43 |     assert.are.same(true, utils.is_subpath([[C:\Users\user]], [[C:\Users\user\Documents]]))
44 |     assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[D:\Users\user]]))
45 |     assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[C:\Users\usera]]))
46 | 
47 |     -- Test Windows paths with forward slashes
48 |     assert.are.same(true, utils.is_subpath("C:/Users/user", "C:/Users/user/Documents"))
49 |     assert.are.same(false, utils.is_subpath("C:/Users/user", "D:/Users/user"))
50 |     assert.are.same(false, utils.is_subpath("C:/Users/user", "C:/Users/usera"))
51 | 
52 |     -- Test Windows paths with drive letters
53 |     assert.are.same(true, utils.is_subpath("C:", "C:/Users/user"))
54 |     assert.are.same(false, utils.is_subpath("C:", "D:/Users/user"))
55 | 
56 |     -- Test Windows paths with UNC paths
57 |     assert.are.same(true, utils.is_subpath([[\\server\share]], [[\\server\share\folder]]))
58 |     assert.are.same(false, utils.is_subpath([[\\server\share]], [[\\server2\share]]))
59 | 
60 |     -- Test Windows paths with trailing backslashes
61 |     assert.are.same(true, utils.is_subpath([[C:\Users\user\]], [[C:\Users\user\Documents]]))
62 |     assert.are.same(true, utils.is_subpath("C:/Users/user/", "C:/Users/user/Documents"))
63 | 
64 |     utils.is_windows = old
65 |   end)
66 | end)
67 | 


--------------------------------------------------------------------------------
/tests/utils/fs.lua:
--------------------------------------------------------------------------------
 1 | local Path = require("plenary.path")
 2 | 
 3 | local fs = {}
 4 | 
 5 | function fs.create_temp_dir()
 6 |   -- Resolve for two reasons.
 7 |   -- 1. Follow any symlinks which make comparing paths fail. (on macOS, TMPDIR can be under /var which is symlinked to
 8 |   --    /private/var)
 9 |   -- 2. Remove any double separators (on macOS TMPDIR can end in a trailing / which absolute doesn't remove, this should
10 |   --    be coverted by https://github.com/nvim-lua/plenary.nvim/issues/330).
11 |   local temp_dir = vim.fn.resolve(
12 |     Path:new(
13 |       vim.fn.fnamemodify(vim.fn.tempname(), ":h"),
14 |       string.format("neo-tree-test-%s", vim.fn.rand())
15 |     ):absolute()
16 |   )
17 |   vim.fn.mkdir(temp_dir, "p")
18 |   return temp_dir
19 | end
20 | 
21 | function fs.create_dir(path)
22 |   local abspath = Path:new(path):absolute()
23 |   vim.fn.mkdir(abspath, "p")
24 | end
25 | 
26 | function fs.remove_dir(dir, recursive)
27 |   if vim.fn.isdirectory(dir) == 1 then
28 |     return vim.fn.delete(dir, recursive and "rf" or "d") == 0
29 |   end
30 |   return false
31 | end
32 | 
33 | function fs.write_file(path, content)
34 |   local abspath = Path:new(path):absolute()
35 |   fs.create_dir(vim.fn.fnamemodify(abspath, ":h"))
36 |   vim.fn.writefile(content or {}, abspath)
37 | end
38 | 
39 | function fs.create_fs_tree(fs_tree)
40 |   local function create_items(items, basedir, relative_root_path)
41 |     relative_root_path = relative_root_path or "."
42 | 
43 |     for _, item in ipairs(items) do
44 |       local relative_path = relative_root_path .. "/" .. item.name
45 | 
46 |       -- create lookups
47 |       fs_tree.lookup[relative_path] = item
48 |       if item.id then
49 |         fs_tree.lookup[item.id] = item
50 |       end
51 | 
52 |       -- create actual files and directories
53 |       if item.type == "dir" then
54 |         item.abspath = Path:new(basedir, item.name):absolute()
55 |         fs.create_dir(item.abspath)
56 |         if item.items then
57 |           create_items(item.items, item.abspath, relative_path)
58 |         end
59 |       elseif item.type == "file" then
60 |         item.abspath = Path:new(basedir, item.name):absolute()
61 |         fs.write_file(item.abspath)
62 |       end
63 |     end
64 |   end
65 | 
66 |   create_items(fs_tree.items, fs_tree.abspath)
67 | 
68 |   return fs_tree
69 | end
70 | 
71 | function fs.init_test(fs_tree)
72 |   fs_tree.lookup = {}
73 |   if not fs_tree.abspath then
74 |     fs_tree.abspath = fs.create_temp_dir()
75 |   end
76 | 
77 |   local function setup()
78 |     fs.remove_dir(fs_tree.abspath, true)
79 |     fs.create_fs_tree(fs_tree)
80 |     vim.cmd("tcd " .. fs_tree.abspath)
81 |   end
82 | 
83 |   local function teardown()
84 |     fs.remove_dir(fs_tree.abspath, true)
85 |   end
86 | 
87 |   return {
88 |     fs_tree = fs_tree,
89 |     setup = setup,
90 |     teardown = teardown,
91 |   }
92 | end
93 | 
94 | return fs
95 | 


--------------------------------------------------------------------------------
/tests/utils/init.lua:
--------------------------------------------------------------------------------
  1 | local mod = {
  2 |   fs = require("tests.utils.fs"),
  3 | }
  4 | 
  5 | function mod.clear_environment()
  6 |   -- Create fresh window
  7 |   vim.cmd("top new | wincmd o")
  8 |   local keepbufnr = vim.api.nvim_get_current_buf()
  9 |   -- Clear ALL neo-tree state
 10 |   require("neo-tree.sources.manager")._clear_state()
 11 |   -- Cleanup any remaining buffers
 12 |   for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
 13 |     if bufnr ~= keepbufnr then
 14 |       vim.api.nvim_buf_delete(bufnr, { force = true })
 15 |     end
 16 |   end
 17 |   assert(#vim.api.nvim_tabpage_list_wins(0) == 1, "Failed to properly clear tab")
 18 |   assert(#vim.api.nvim_list_bufs() == 1, "Failed to properly clear buffers")
 19 | end
 20 | 
 21 | mod.editfile = function(testfile)
 22 |   vim.cmd("e " .. testfile)
 23 |   assert.are.same(
 24 |     vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":p"),
 25 |     vim.fn.fnamemodify(testfile, ":p")
 26 |   )
 27 | end
 28 | 
 29 | function mod.eq(...)
 30 |   return assert.are.same(...)
 31 | end
 32 | 
 33 | function mod.neq(...)
 34 |   return assert["not"].are.same(...)
 35 | end
 36 | 
 37 | ---@param keys string
 38 | ---@param mode? string
 39 | function mod.feedkeys(keys, mode)
 40 |   vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), mode or "x", true)
 41 | end
 42 | 
 43 | ---@param tbl table
 44 | ---@param keys string[]
 45 | function mod.tbl_pick(tbl, keys)
 46 |   if not keys or #keys == 0 then
 47 |     return tbl
 48 |   end
 49 | 
 50 |   local new_tbl = {}
 51 |   for _, key in ipairs(keys) do
 52 |     new_tbl[key] = tbl[key]
 53 |   end
 54 |   return new_tbl
 55 | end
 56 | 
 57 | local orig_require = _G.require
 58 | -- can be used to enable/disable package
 59 | -- for specific tests
 60 | function mod.get_require_switch()
 61 |   local disabled_packages = {}
 62 | 
 63 |   local function fake_require(name)
 64 |     if vim.tbl_contains(disabled_packages, name) then
 65 |       return error("test: package disabled")
 66 |     end
 67 | 
 68 |     return orig_require(name)
 69 |   end
 70 | 
 71 |   return {
 72 |     disable_package = function(name)
 73 |       _G.require = fake_require
 74 |       package.loaded[name] = nil
 75 |       table.insert(disabled_packages, name)
 76 |     end,
 77 |     enable_package = function(name)
 78 |       _G.require = fake_require
 79 |       disabled_packages = vim.tbl_filter(function(package_name)
 80 |         return package_name ~= name
 81 |       end, disabled_packages)
 82 |     end,
 83 |     restore = function()
 84 |       disabled_packages = {}
 85 |       _G.require = orig_require
 86 |     end,
 87 |   }
 88 | end
 89 | 
 90 | ---@param bufnr number
 91 | ---@param lines string[]
 92 | ---@param linenr_start? integer (1-indexed)
 93 | ---@param linenr_end? integer (1-indexed, inclusive)
 94 | function mod.assert_buf_lines(bufnr, lines, linenr_start, linenr_end)
 95 |   mod.eq(
 96 |     lines,
 97 |     vim.api.nvim_buf_get_lines(
 98 |       bufnr,
 99 |       linenr_start and linenr_start - 1 or 0,
100 |       linenr_end or -1,
101 |       false
102 |     )
103 |   )
104 | end
105 | 
106 | ---@param bufnr number
107 | ---@param ns_id integer
108 | ---@param linenr integer (1-indexed)
109 | ---@param byte_start? integer (0-indexed)
110 | ---@param byte_end? integer (0-indexed, inclusive)
111 | function mod.get_line_extmarks(bufnr, ns_id, linenr, byte_start, byte_end)
112 |   return vim.api.nvim_buf_get_extmarks(
113 |     bufnr,
114 |     ns_id,
115 |     { linenr - 1, byte_start or 0 },
116 |     { linenr - 1, byte_end and byte_end + 1 or -1 },
117 |     { details = true }
118 |   )
119 | end
120 | 
121 | ---@param bufnr number
122 | ---@param ns_id integer
123 | ---@param linenr integer (1-indexed)
124 | ---@param text string
125 | ---@return table[]
126 | ---@return { byte_start: integer, byte_end: integer } info (byte range: 0-indexed, inclusive)
127 | function mod.get_text_extmarks(bufnr, ns_id, linenr, text)
128 |   local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1]
129 | 
130 |   local byte_start = string.find(line, text) -- 1-indexed
131 |   byte_start = byte_start - 1 -- 0-indexed
132 |   local byte_end = byte_start + #text - 1 -- inclusive
133 | 
134 |   local extmarks = vim.api.nvim_buf_get_extmarks(
135 |     bufnr,
136 |     ns_id,
137 |     { linenr - 1, byte_start },
138 |     { linenr - 1, byte_end },
139 |     { details = true }
140 |   )
141 | 
142 |   return extmarks, { byte_start = byte_start, byte_end = byte_end }
143 | end
144 | 
145 | ---@param extmark table
146 | ---@param linenr number (1-indexed)
147 | ---@param text string
148 | ---@param hl_group string
149 | function mod.assert_extmark(extmark, linenr, text, hl_group)
150 |   mod.eq(extmark[2], linenr - 1)
151 | 
152 |   if text then
153 |     local start_col = extmark[3]
154 |     mod.eq(extmark[4].end_col - start_col, #text)
155 |   end
156 | 
157 |   mod.eq(mod.tbl_pick(extmark[4], { "end_row", "hl_group" }), {
158 |     end_row = linenr - 1,
159 |     hl_group = hl_group,
160 |   })
161 | end
162 | 
163 | ---@param bufnr number
164 | ---@param ns_id integer
165 | ---@param linenr integer (1-indexed)
166 | ---@param text string
167 | ---@param hl_group string
168 | function mod.assert_highlight(bufnr, ns_id, linenr, text, hl_group)
169 |   local extmarks, info = mod.get_text_extmarks(bufnr, ns_id, linenr, text)
170 | 
171 |   mod.eq(#extmarks, 1)
172 |   mod.eq(extmarks[1][3], info.byte_start)
173 |   mod.assert_extmark(extmarks[1], linenr, text, hl_group)
174 | end
175 | 
176 | ---@param callback fun(): boolean
177 | ---@param options? { interval?: integer, timeout?: integer }
178 | function mod.wait_for(callback, options)
179 |   options = options or {}
180 |   vim.wait(options.timeout or 1000, callback, options.interval or 100)
181 | end
182 | 
183 | ---@param options? { interval?: integer, timeout?: integer }
184 | function mod.wait_for_neo_tree(options)
185 |   local verify = require("tests.utils.verify")
186 |   mod.wait_for(function()
187 |     return verify.get_state() ~= nil
188 |   end, options)
189 | end
190 | 
191 | return mod
192 | 


--------------------------------------------------------------------------------
/tests/utils/verify.lua:
--------------------------------------------------------------------------------
  1 | local verify = {}
  2 | 
  3 | verify.eventually = function(timeout, assertfunc, failmsg, ...)
  4 |   local success, args = false, { ... }
  5 |   vim.wait(timeout or 1000, function()
  6 |     success = assertfunc(unpack(args))
  7 |     return success
  8 |   end)
  9 |   assert(success, failmsg)
 10 | end
 11 | 
 12 | local id = 0
 13 | ---Waits until the next vim.schedule before running assertfunc
 14 | verify.schedule = function(assertfunc, timeout, failmsg)
 15 |   id = id + 1
 16 |   local scheduled_func_ran = false
 17 |   local success = false
 18 |   local args
 19 |   vim.schedule(function()
 20 |     args = { assertfunc() }
 21 |     success = args[1]
 22 |     scheduled_func_ran = true
 23 |   end)
 24 |   local notimeout, errcode = vim.wait(timeout or 1000, function()
 25 |     return scheduled_func_ran
 26 |   end)
 27 |   assert(success, failmsg)
 28 | end
 29 | 
 30 | verify.after = function(timeout, assertfunc, failmsg)
 31 |   vim.wait(timeout, function()
 32 |     return false
 33 |   end)
 34 |   assert(assertfunc(), failmsg)
 35 | end
 36 | 
 37 | verify.bufnr_is = function(bufnr, timeout)
 38 |   verify.eventually(timeout or 500, function()
 39 |     return bufnr == vim.api.nvim_get_current_buf()
 40 |   end, string.format("Current buffer is expected to be '%s' but is not", bufnr))
 41 | end
 42 | 
 43 | verify.bufnr_is_not = function(bufnr, timeout)
 44 |   verify.eventually(timeout or 500, function()
 45 |     return bufnr ~= vim.api.nvim_get_current_buf()
 46 |   end, string.format("Current buffer is '%s' when expected to not be", bufnr))
 47 | end
 48 | 
 49 | verify.buf_name_endswith = function(buf_name, timeout)
 50 |   verify.eventually(
 51 |     timeout or 500,
 52 |     function()
 53 |       if buf_name == "" then
 54 |         return true
 55 |       end
 56 |       local n = vim.api.nvim_buf_get_name(0)
 57 |       if n:sub(-#buf_name) == buf_name then
 58 |         return true
 59 |       else
 60 |         return false
 61 |       end
 62 |     end,
 63 |     string.format("Current buffer name is expected to be end with '%s' but it does not", buf_name)
 64 |   )
 65 | end
 66 | 
 67 | verify.buf_name_is = function(buf_name, timeout)
 68 |   verify.eventually(timeout or 500, function()
 69 |     return buf_name == vim.api.nvim_buf_get_name(0)
 70 |   end, string.format("Current buffer name is expected to be '%s' but is not", buf_name))
 71 | end
 72 | 
 73 | verify.tree_focused = function(timeout)
 74 |   verify.eventually(timeout or 1000, function()
 75 |     if not verify.get_state() then
 76 |       return false
 77 |     end
 78 |     return vim.bo[0].filetype == "neo-tree"
 79 |   end, "Current buffer is not a 'neo-tree' filetype")
 80 | end
 81 | 
 82 | verify.get_state = function(source_name, winid)
 83 |   if source_name == nil then
 84 |     local success
 85 |     success, source_name = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_source")
 86 |     if not success then
 87 |       return nil
 88 |     end
 89 |   end
 90 |   local state = require("neo-tree.sources.manager").get_state(source_name, nil, winid)
 91 |   if not state.tree then
 92 |     return nil
 93 |   end
 94 |   if not state._ready then
 95 |     return nil
 96 |   end
 97 |   return state
 98 | end
 99 | 
100 | verify.tree_node_is = function(source_name, expected_node_id, winid, timeout)
101 |   verify.eventually(timeout or 500, function()
102 |     local state = verify.get_state(source_name, winid)
103 |     if not state then
104 |       return false
105 |     end
106 |     local success, node = pcall(state.tree.get_node, state.tree)
107 |     if not success then
108 |       return false
109 |     end
110 |     if not node then
111 |       return false
112 |     end
113 |     local node_id = node:get_id()
114 |     if node_id == expected_node_id then
115 |       return true
116 |     end
117 |     return false
118 |   end, string.format("Tree node '%s' not focused", expected_node_id))
119 | end
120 | 
121 | verify.filesystem_tree_node_is = function(expected_node_id, winid, timeout)
122 |   verify.tree_node_is("filesystem", expected_node_id, winid, timeout)
123 | end
124 | 
125 | verify.buffers_tree_node_is = function(expected_node_id, winid, timeout)
126 |   verify.tree_node_is("buffers", expected_node_id, winid, timeout)
127 | end
128 | 
129 | verify.git_status_tree_node_is = function(expected_node_id, winid, timeout)
130 |   verify.tree_node_is("git_status", expected_node_id, winid, timeout)
131 | end
132 | 
133 | verify.window_handle_is = function(winid, timeout)
134 |   verify.eventually(timeout or 500, function()
135 |     return winid == vim.api.nvim_get_current_win()
136 |   end, string.format("Current window handle is expected to be '%s' but is not", winid))
137 | end
138 | 
139 | verify.window_handle_is_not = function(winid, timeout)
140 |   verify.eventually(timeout or 500, function()
141 |     return winid ~= vim.api.nvim_get_current_win()
142 |   end, string.format("Current window handle is not expected to be '%s' but it is", winid))
143 | end
144 | 
145 | return verify
146 | 


--------------------------------------------------------------------------------