├── .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.luaquot;)) 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_windowquot;) 64 | cc._add_common_commands(M, "sourcequot;) -- source navigation 65 | cc._add_common_commands(M, "preview") -- preview 66 | cc._add_common_commands(M, "^cancelquot;) -- cancel 67 | cc._add_common_commands(M, "help") -- help commands 68 | cc._add_common_commands(M, "with_window_pickerquot;) -- open using window picker 69 | cc._add_common_commands(M, "^toggle_auto_expand_widthquot;) 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("/%.gitquot;) 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("^.+%.lockquot;) 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("^%wquot;) 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("%.0quot;, "") 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 | --------------------------------------------------------------------------------