1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-04-13 13:59:10 +08:00

feat(neotree): add neotree support

This commit is contained in:
wsdjeg 2023-05-30 21:09:18 +08:00
parent f7fd9a193c
commit 5c83ab2f06
143 changed files with 34274 additions and 0 deletions

View File

@ -86,6 +86,9 @@ function! SpaceVim#layers#core#plugins() abort
call add(plugins, [g:_spacevim_root_dir . 'bundle/defx-sftp',{'merged' : 0}])
elseif g:spacevim_filemanager ==# 'nvim-tree'
call add(plugins, [g:_spacevim_root_dir . 'bundle/nvim-tree.lua',{'merged' : 0, 'loadconf' : 1}])
elseif g:spacevim_filemanager ==# 'neo-tree'
call add(plugins, [g:_spacevim_root_dir . 'bundle/neo-tree.nvim',{'merged' : 0, 'loadconf' : 1}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/nui.nvim',{'merged' : 0}])
endif
if !g:spacevim_vimcompatible

View File

@ -452,6 +452,13 @@ function! SpaceVim#layers#core#statusline#get(...) abort
\ . ' NvimTree '
\ . '%#SpaceVim_statusline_b_SpaceVim_statusline_c#'
\ . s:lsep . ' '
elseif &filetype ==# 'neo-tree'
return '%#SpaceVim_statusline_ia#' . s:winnr(1)
\ . '%#SpaceVim_statusline_ia_SpaceVim_statusline_b#' . s:lsep
\ . '%#SpaceVim_statusline_b#'
\ . ' NeoTree '
\ . '%#SpaceVim_statusline_b_SpaceVim_statusline_c#'
\ . s:lsep . ' '
elseif &filetype ==# 'Fuzzy'
return '%#SpaceVim_statusline_a_bold# Fuzzy %#SpaceVim_statusline_a_SpaceVim_statusline_b#' . s:lsep
\ . '%#SpaceVim_statusline_b# %{fuzzy#statusline()} %#SpaceVim_statusline_b_SpaceVim_statusline_c#' . s:lsep

10
bundle/neo-tree.nvim/.codecov.yml vendored Normal file
View File

@ -0,0 +1,10 @@
coverage:
status:
project:
default:
informational: true
only_pulls: true
patch:
default:
informational: true
only_pulls: true

View File

@ -0,0 +1,73 @@
name: CI
on:
push:
branches:
- main
- v1.x
- v2.x
- v3.x
pull_request:
jobs:
stylua-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Check formatting
uses: JohnnyMorganz/stylua-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: v0.16.0
args: --color always --check -g '!**/defaults.lua' lua/
plenary-tests:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v2
with:
path: build
key: ${{ runner.os }}-appimage-${{ hashFiles('todays-date') }}
- name: Prepare
run: |
test -d build || {
mkdir -p build
wget https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage
chmod +x nvim.appimage
mv nvim.appimage ./build/nvim
}
# - name: Get Luver Cache Key
# id: luver-cache-key
# env:
# CI_RUNNER_OS: ${{ runner.os }}
# run: |
# echo "::set-output name=value::${CI_RUNNER_OS}-luver-v1-$(date -u +%Y-%m-%d)"
# shell: bash
# - name: Setup Luver Cache
# uses: actions/cache@v2
# with:
# path: ~/.local/share/luver
# key: ${{ steps.luver-cache-key.outputs.value }}
# - name: Setup Lua
# uses: MunifTanjim/luver-action@v1
# with:
# default: 5.1.5
# lua_versions: 5.1.5
# luarocks_versions: 5.1.5:3.8.0
# - name: Setup luacov
# run: |
# luarocks install luacov
- name: Run tests
run: |
export PATH="${PWD}/build/:${PATH}"
./scripts/test.sh
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v2

View File

@ -0,0 +1,29 @@
# This is a basic workflow to help you get started with Actions
name: No PRs to Release Branches
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the v1.x branch
pull_request:
types: [opened, edited, ready_for_review]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
check_target:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Runs a single command using the runners shell
- name: Fail when targeting v2
run: |
target=${{ github.base_ref }}
echo "Target is: $target"
if [[ $target == "v2.x" ]]; then
echo "PRs must target main"
exit 1
else
exit 0
fi

47
bundle/neo-tree.nvim/.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Vim tag files
tags
# Others
.testcache
luacov.*.out

3
bundle/neo-tree.nvim/.luacov vendored Normal file
View File

@ -0,0 +1,3 @@
include = {
"lua%/neo%-tree",
}

3
bundle/neo-tree.nvim/.luarc.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"diagnostics.globals": ["vim"]
}

5
bundle/neo-tree.nvim/.stylua.toml vendored Normal file
View File

@ -0,0 +1,5 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"

1
bundle/neo-tree.nvim/.styluaignore vendored Normal file
View File

@ -0,0 +1 @@
**/defaults.lua

58
bundle/neo-tree.nvim/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,58 @@
# Contributing to Neo-tree
Contributions are welcome! To keep everything clean and tidy, please follow the
guidelines below.
## Code Style
This is open for debate, but here is the current style choices being observed:
- snake_case for all variables and functions
- unless it is a class, then use PascalCase
- other OOP things, like method names should use camelCase
- BUT we don't currently have any OOP parts and I don't think we want any
I prefer `local name = function()` over `local function name()`, just to be
consistent with the `M.name = function()` exports.
### StyLua
We use (StyLua)[https://github.com/JohnnyMorganz/StyLua] to enforce consistency
in code. You should install it on your local machine. PRs will be checked with
this tool.
## Commit Messages
We use **semantic**, aka **conventional** commit messages. The official guide
can be found here: https://www.conventionalcommits.org/en/v1.0.0/
You can also just take a look at the commit history to get the idea. The
optional scope for this project would usually be the source, i.e.
`feat(filesystem): add awesome feature that does xyz`.
## Branching
The default branch is set to the current major version to make it simple for end
users visiting the repo. Pull Requests, however, should go to the `main`
branch. After a short testing period, it will be merged to the current release
branch.
This project requires a **linear history**. I don't trust merge commits.
This means you will have to rebase your branch on main before the pull request
can be merged. This can get a bit annoying in a busy repository, but I think it
is worth the effort.
## Documentation
All new features should be documented in the commit they were added in. The
current strategy is to maintain:
- Config Options: added to [defaults](lua/neo-tree/defaults.lua) and described
in comments
- The README contains "back of the box" high level overview of features. It is
meant for people trying to decide if they want to install this plugin or not.
It should include references to the help file for more information:
`:h neo-tree-setup`
- The vim help file [doc/neo-tree.txt](doc/neo-tree.txt) is the definitive
reference and should contain all information needed to configure and use the
plugin.

18
bundle/neo-tree.nvim/Dockerfile vendored Normal file
View File

@ -0,0 +1,18 @@
FROM ubuntu:22.04
RUN apt update
# install neovim dependencies
RUN apt install -y git ninja-build gettext libtool libtool-bin autoconf \
automake cmake g++ pkg-config unzip curl doxygen
# install neovim
RUN git clone https://github.com/neovim/neovim
RUN cd neovim && make CMAKE_BUILD_TYPE=RelWithDebInfo && make install
# install required plugins
ARG PLUG_DIR="root/.local/share/nvim/site/pack/packer/start"
RUN git clone https://github.com/nvim-lua/plenary.nvim $PLUG_DIR/plenary.nvim
RUN git clone https://github.com/MunifTanjim/nui.nvim $PLUG_DIR/nui.nvim
COPY . $PLUG_DIR/neo-tree.nvim
WORKDIR $PLUG_DIR/neo-tree.nvim

22
bundle/neo-tree.nvim/LICENSE vendored Normal file
View File

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

12
bundle/neo-tree.nvim/Makefile vendored Normal file
View File

@ -0,0 +1,12 @@
.PHONY: test
test:
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})"
.PHONY: test-docker
test-docker:
docker build -t neo-tree .
docker run --rm neo-tree make test
.PHONY: format
format:
stylua --glob '*.lua' --glob '!defaults.lua' .

770
bundle/neo-tree.nvim/README.md vendored Normal file
View File

@ -0,0 +1,770 @@
# Neo-tree.nvim
Neo-tree is a Neovim plugin to browse the file system and other tree like
structures in whatever style suits you, including sidebars, floating windows,
netrw split style, or all of them at once!
![Neo-tree file system](https://github.com/nvim-neo-tree/resources/blob/main/images/Neo-tree-with-right-aligned-symbols.png)
### Breaking Changes BAD :bomb: :imp:
The biggest and most important feature of Neo-tree is that we will never
knowingly push a breaking change and interrupt your day. Bugs happen, but
breaking changes can always be avoided. When breaking changes are needed, there
will be a new branch that you can opt into, when it is a good time for you.
See [What is a Breaking Change?](#what-is-a-breaking-change) for details.
See [Changelog 2.0](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Changelog#20)
for breaking changes and deprecations in 2.0.
### User Experience GOOD :slightly_smiling_face: :thumbsup:
Aside from being polite about breaking changes, Neo-tree is also focused on the
little details of user experience. Everything should work exactly as you would
expect a sidebar to work without all of the glitchy behavior that is normally
accepted in (neo)vim sidebars. I can't stand glitchy behavior, and neither
should you!
- Neo-tree won't let other buffers take over its window.
- Neo-tree won't leave its window scrolled to the last line when there is
plenty of room to display the whole tree.
- Neo-tree does not need to be manually refreshed (set `use_libuv_file_watcher=true`)
- Neo-tree can intelligently follow the current file (set `follow_current_file=true`)
- Neo-tree is thoughtful about maintaining or setting focus on the right node
- Neo-tree windows in different tabs are completely separate
- `respect_gitignore` actually works!
Neo-tree is smooth, efficient, stable, and pays attention to the little details.
If you find anything janky, wanky, broken, or unintuitive, please open an issue
so we can fix it.
## Minimal Quickstart
#### Minimal Example for Packer:
```lua
-- Unless you are still migrating, remove the deprecated commands from v1.x
vim.cmd([[ let g:neo_tree_remove_legacy_commands = 1 ]])
use {
"nvim-neo-tree/neo-tree.nvim",
branch = "v2.x",
requires = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons", -- not strictly required, but recommended
"MunifTanjim/nui.nvim",
}
}
```
After installing, run:
```
:Neotree
```
Press `?` in the Neo-tree window to view the list of mappings.
## Quickstart
#### Longer Example for Packer:
```lua
use {
"nvim-neo-tree/neo-tree.nvim",
branch = "v2.x",
requires = {
"nvim-lua/plenary.nvim",
"nvim-tree/nvim-web-devicons", -- not strictly required, but recommended
"MunifTanjim/nui.nvim",
{
-- only needed if you want to use the commands with "_with_window_picker" suffix
's1n7ax/nvim-window-picker',
tag = "v1.*",
config = function()
require'window-picker'.setup({
autoselect_one = true,
include_current = false,
filter_rules = {
-- filter using buffer options
bo = {
-- if the file type is one of following, the window will be ignored
filetype = { 'neo-tree', "neo-tree-popup", "notify" },
-- if the buffer type is one of following, the window will be ignored
buftype = { 'terminal', "quickfix" },
},
},
other_win_hl_color = '#e35e4f',
})
end,
}
},
config = function ()
-- Unless you are still migrating, remove the deprecated commands from v1.x
vim.cmd([[ let g:neo_tree_remove_legacy_commands = 1 ]])
-- If you want icons for diagnostic errors, you'll need to define them somewhere:
vim.fn.sign_define("DiagnosticSignError",
{text = " ", texthl = "DiagnosticSignError"})
vim.fn.sign_define("DiagnosticSignWarn",
{text = " ", texthl = "DiagnosticSignWarn"})
vim.fn.sign_define("DiagnosticSignInfo",
{text = " ", texthl = "DiagnosticSignInfo"})
vim.fn.sign_define("DiagnosticSignHint",
{text = "", texthl = "DiagnosticSignHint"})
-- NOTE: this is changed from v1.x, which used the old style of highlight groups
-- in the form "LspDiagnosticsSignWarning"
require("neo-tree").setup({
close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab
popup_border_style = "rounded",
enable_git_status = true,
enable_diagnostics = true,
open_files_do_not_replace_types = { "terminal", "trouble", "qf" }, -- when opening files, do not use windows containing these filetypes or buftypes
sort_case_insensitive = false, -- used when sorting files and directories in the tree
sort_function = nil , -- use a custom function for sorting files and directories in the tree
-- sort_function = function (a,b)
-- if a.type == b.type then
-- return a.path > b.path
-- else
-- return a.type > b.type
-- end
-- end , -- this sorts files and directories descendantly
default_component_configs = {
container = {
enable_character_fade = true
},
indent = {
indent_size = 2,
padding = 1, -- extra padding on left hand side
-- indent guides
with_markers = true,
indent_marker = "│",
last_indent_marker = "└",
highlight = "NeoTreeIndentMarker",
-- expander config, needed for nesting files
with_expanders = nil, -- if nil and file nesting is enabled, will enable expanders
expander_collapsed = "",
expander_expanded = "",
expander_highlight = "NeoTreeExpander",
},
icon = {
folder_closed = "",
folder_open = "",
folder_empty = "ﰊ",
-- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there
-- then these will never be used.
default = "*",
highlight = "NeoTreeFileIcon"
},
modified = {
symbol = "[+]",
highlight = "NeoTreeModified",
},
name = {
trailing_slash = false,
use_git_status_colors = true,
highlight = "NeoTreeFileName",
},
git_status = {
symbols = {
-- Change type
added = "", -- or "✚", but this is redundant info if you use git_status_colors on the name
modified = "", -- or "", but this is redundant info if you use git_status_colors on the name
deleted = "✖",-- this can only be used in the git_status source
renamed = "",-- this can only be used in the git_status source
-- Status type
untracked = "",
ignored = "",
unstaged = "",
staged = "",
conflict = "",
}
},
},
-- A list of functions, each representing a global custom command
-- that will be available in all sources (if not overridden in `opts[source_name].commands`)
-- see `:h neo-tree-global-custom-commands`
commands = {},
window = {
position = "left",
width = 40,
mapping_options = {
noremap = true,
nowait = true,
},
mappings = {
["<space>"] = {
"toggle_node",
nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use
},
["<2-LeftMouse>"] = "open",
["<cr>"] = "open",
["<esc>"] = "revert_preview",
["P"] = { "toggle_preview", config = { use_float = true } },
["l"] = "focus_preview",
["S"] = "open_split",
["s"] = "open_vsplit",
-- ["S"] = "split_with_window_picker",
-- ["s"] = "vsplit_with_window_picker",
["t"] = "open_tabnew",
-- ["<cr>"] = "open_drop",
-- ["t"] = "open_tab_drop",
["w"] = "open_with_window_picker",
--["P"] = "toggle_preview", -- enter preview mode, which shows the current node without focusing
["C"] = "close_node",
-- ['C'] = 'close_all_subnodes',
["z"] = "close_all_nodes",
--["Z"] = "expand_all_nodes",
["a"] = {
"add",
-- this command supports BASH style brace expansion ("x{a,b,c}" -> xa,xb,xc). see `:h neo-tree-file-actions` for details
-- some commands may take optional config options, see `:h neo-tree-mappings` for details
config = {
show_path = "none" -- "none", "relative", "absolute"
}
},
["A"] = "add_directory", -- also accepts the optional config.show_path option like "add". this also supports BASH style brace expansion.
["d"] = "delete",
["r"] = "rename",
["y"] = "copy_to_clipboard",
["x"] = "cut_to_clipboard",
["p"] = "paste_from_clipboard",
["c"] = "copy", -- takes text input for destination, also accepts the optional config.show_path option like "add":
-- ["c"] = {
-- "copy",
-- config = {
-- show_path = "none" -- "none", "relative", "absolute"
-- }
--}
["m"] = "move", -- takes text input for destination, also accepts the optional config.show_path option like "add".
["q"] = "close_window",
["R"] = "refresh",
["?"] = "show_help",
["<"] = "prev_source",
[">"] = "next_source",
}
},
nesting_rules = {},
filesystem = {
filtered_items = {
visible = false, -- when true, they will just be displayed differently than normal items
hide_dotfiles = true,
hide_gitignored = true,
hide_hidden = true, -- only works on Windows for hidden files/directories
hide_by_name = {
--"node_modules"
},
hide_by_pattern = { -- uses glob style patterns
--"*.meta",
--"*/src/*/tsconfig.json",
},
always_show = { -- remains visible even if other settings would normally hide it
--".gitignored",
},
never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show
--".DS_Store",
--"thumbs.db"
},
never_show_by_pattern = { -- uses glob style patterns
--".null-ls_*",
},
},
follow_current_file = false, -- This will find and focus the file in the active buffer every
-- time the current file is changed while the tree is open.
group_empty_dirs = false, -- when true, empty folders will be grouped together
hijack_netrw_behavior = "open_default", -- netrw disabled, opening a directory opens neo-tree
-- in whatever position is specified in window.position
-- "open_current", -- netrw disabled, opening a directory opens within the
-- window like netrw would, regardless of window.position
-- "disabled", -- netrw left alone, neo-tree does not handle opening dirs
use_libuv_file_watcher = false, -- This will use the OS level file watchers to detect changes
-- instead of relying on nvim autocmd events.
window = {
mappings = {
["<bs>"] = "navigate_up",
["."] = "set_root",
["H"] = "toggle_hidden",
["/"] = "fuzzy_finder",
["D"] = "fuzzy_finder_directory",
["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm
-- ["D"] = "fuzzy_sorter_directory",
["f"] = "filter_on_submit",
["<c-x>"] = "clear_filter",
["[g"] = "prev_git_modified",
["]g"] = "next_git_modified",
},
fuzzy_finder_mappings = { -- define keymaps for filter popup window in fuzzy_finder_mode
["<down>"] = "move_cursor_down",
["<C-n>"] = "move_cursor_down",
["<up>"] = "move_cursor_up",
["<C-p>"] = "move_cursor_up",
},
},
commands = {} -- Add a custom command or override a global one using the same function name
},
buffers = {
follow_current_file = true, -- This will find and focus the file in the active buffer every
-- time the current file is changed while the tree is open.
group_empty_dirs = true, -- when true, empty folders will be grouped together
show_unloaded = true,
window = {
mappings = {
["bd"] = "buffer_delete",
["<bs>"] = "navigate_up",
["."] = "set_root",
}
},
},
git_status = {
window = {
position = "float",
mappings = {
["A"] = "git_add_all",
["gu"] = "git_unstage_file",
["ga"] = "git_add_file",
["gr"] = "git_revert_file",
["gc"] = "git_commit",
["gp"] = "git_push",
["gg"] = "git_commit_and_push",
}
}
}
})
vim.cmd([[nnoremap \ :Neotree reveal<cr>]])
end
}
```
_The above configuration is not everything that can be changed, it's just the
parts you might want to change first._
See `:h neo-tree` for full documentation. You can also preview that online at
[doc/neo-tree.txt](doc/neo-tree.txt), although it's best viewed within vim.
To see all of the default config options with commentary, you can view it online
at [lua/neo-tree/defaults.lua](lua/neo-tree/defaults.lua). You can also paste it
into a buffer after installing Neo-tree by running:
```
:lua require("neo-tree").paste_default_config()
```
#### Configuration for Nerd Fonts v3 Users
The following configuration should fix broken icons if you are using Nerd Fonts v3:
```lua
require("neo-tree").setup({
default_component_configs = {
icon = {
folder_empty = "󰜌",
folder_empty_open = "󰜌",
},
git_status = {
symbols = {
renamed = "󰁕",
unstaged = "󰄱",
},
},
},
document_symbols = {
kinds = {
File = { icon = "󰈙", hl = "Tag" },
Namespace = { icon = "󰌗", hl = "Include" },
Package = { icon = "󰏖", hl = "Label" },
Class = { icon = "󰌗", hl = "Include" },
Property = { icon = "󰆧", hl = "@property" },
Enum = { icon = "󰒻", hl = "@number" },
Function = { icon = "󰊕", hl = "Function" },
String = { icon = "󰀬", hl = "String" },
Number = { icon = "󰎠", hl = "Number" },
Array = { icon = "󰅪", hl = "Type" },
Object = { icon = "󰅩", hl = "Type" },
Key = { icon = "󰌋", hl = "" },
Struct = { icon = "󰌗", hl = "Type" },
Operator = { icon = "󰆕", hl = "Operator" },
TypeParameter = { icon = "󰊄", hl = "Type" },
StaticMethod = { icon = '󰠄 ', hl = 'Function' },
}
},
-- Add this section only if you've configured source selector.
source_selector = {
sources = {
{ source = "filesystem", display_name = " 󰉓 Files " },
{ source = "git_status", display_name = " 󰊢 Git " },
},
},
-- Other options ...
})
```
## The `:Neotree` Command
The single `:Neotree` command accepts a range of arguments that give you full
control over the details of what and where it will show. For example, the following
command will open a file browser on the right hand side, "revealing" the currently
active file:
```
:Neotree filesystem reveal right
```
Arguments can be specified as either a key=value pair or just as the value. The
key=value form is more verbose but may help with clarity. For example, the command
above can also be specified as:
```
:Neotree source=filesystem reveal=true position=right
```
All arguments are optional and can be specified in any order. If you issue the command
without any arguments, it will use default values for everything. For example:
```
:Neotree
```
will open the filesystem source on the left hand side and focus it, if you are using
the default config.
### Tab Completion
Neotree supports tab completion for all arguments. Once a given argument has a value,
it will stop suggesting those completions. It will also offer completions for paths.
The simplest way to disambiguate a path from another type of argument is to start
them with `/` or `./`.
### Arguments
Here is the full list of arguments you can use:
#### `action`
What to do. Can be one of:
| Option | Description |
|--------|-------------|
| focus | Show and/or switch focus to the specified Neotree window. DEFAULT |
| show | Show the window, but keep focus on your current window. |
| close | Close the window(s) specified. Can be combined with "position" and/or "source" to specify which window(s) to close. |
#### `source`
What to show. Can be one of:
| Option | Description |
|--------|-------------|
| filesystem | Show a file browser. DEFAULT |
| buffers | Show a list of currently open buffers. |
| git_status | Show the output of `git status` in a tree layout. |
#### `position`
Where to show it, can be one of:
| Option | Description |
|---------|-------------|
| left | Open as left hand sidebar. DEFAULT |
| right | Open as right hand sidebar. |
| top | Open as top window. |
| bottom | Open as bottom window. |
| float | Open as floating window. |
| current | Open within the current window, like netrw or vinegar would. |
#### `toggle`
This is a boolean flag. Adding this means that the window will be closed if it
is already open.
#### `dir`
The directory to set as the root/cwd of the specified window. If you include a
directory as one of the arguments, it will be assumed to be this option, you
don't need the full dir=/path. You may use any value that can be passed to the
'expand' function, such as `%:p:h:h` to specify two directories up from the
current file. For example:
```
:Neotree ./relative/path
:Neotree /home/user/relative/path
:Neotree dir=/home/user/relative/path
:Neotree position=current dir=relative/path
```
#### `git_base`
The base that is used to calculate the git status for each dir/file.
By default it uses `HEAD`, so it shows all changes that are not yet committed.
You can for example work on a feature branch, and set it to `main`. It will
show all changes that happened on the feature branch and main since you
branched off.
Any git ref, commit, tag, or sha will work.
```
:Neotree main
:Neotree v1.0
:Neotree git_base=8fe34be
:Neotree git_base=HEAD
```
#### `reveal`
This is a boolean flag. Adding this will make Neotree automatically find and
focus the current file when it opens.
#### `reveal_file`
A path to a file to reveal. This supersedes the "reveal" flag so there is no
need to specify both. Use this if you want to reveal something other than the
current file. If you include a path to a file as one of the arguments, it will
be assumed to be this option. Like "dir", you can pass any value that can be
passed to the 'expand' function. For example:
```
:Neotree reveal_file=/home/user/my/file.text
:Neotree position=current dir=%:p:h:h reveal_file=%:p
:Neotree current %:p:h:h %:p
```
One neat trick you can do with this is to open a Neotree window which is
focused on the file under the cursor using the `<cfile>` keyword:
```
nnoremap gd :Neotree float reveal_file=<cfile> reveal_force_cwd<cr>
```
#### `reveal_force_cwd`
This is a boolean flag. Normally, if you use one of the reveal options and the
given file is not within the current working directory, you will be asked if you
want to change the current working directory. If you include this flag, it will
automatically change the directory without prompting. This option implies
"reveal", so you do not need to specify both.
See `:h neo-tree-commands` for details and a full listing of available arguments.
### File Nesting
See `:h neo-tree-file-nesting` for more details about file nesting.
### Netrw Hijack
```
:edit .
:[v]split .
```
If `"filesystem.window.position"` is set to `"current"`, or if you have specified
`filesystem.hijack_netrw_behavior = "open_current"`, then any command
that would open a directory will open neo-tree in the specified window.
## Sources
Neo-tree is built on the idea of supporting various sources. Sources are
basically interface implementations whose job it is to provide a list of
hierarchical items to be rendered, along with commands that are appropriate to
those items.
### filesystem
The default source is `filesystem`, which displays your files and folders. This
is the default source in commands when none is specified.
This source can be used to:
- Browse the filesystem
- Control the current working directory of nvim
- Add/Copy/Delete/Move/Rename files and directories
- Search the filesystem
- Monitor git status and lsp diagnostics for the current working directory
### buffers
![Neo-tree buffers](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-buffers.png)
Another available source is `buffers`, which displays your open buffers. This is
the same list you would see from `:ls`. To show with the `buffers` list, use:
```
:Neotree buffers
```
### git_status
This view take the results of the `git status` command and display them in a
tree. It includes commands for adding, unstaging, reverting, and committing.
The screenshot below shows the result of `:Neotree float git_status` while the
filesystem is open in a sidebar:
![Neo-tree git_status](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-git_status.png)
You can specify a different git base here as well. But be aware that it is not
possible to unstage / revert a file that is already committed.
```
:Neotree float git_status git_base=main
```
### document_symbols
![Neo-tree document_symbols](https://github.com/nvim-neo-tree/resources/raw/main/images/neo-tree-document-symbols.png)
The document_symbols source lists the symbols in the current document obtained
by the LSP request "textDocument/documentSymbols". It currently supports the
following features:
- [x] UI:
- [x] Display all symbols in the current file with symbol kinds
- [x] Symbols nesting
- [x] Configurable kinds' name and icon
- [x] Auto-refresh symbol list
- [x] Follow cursor
- [ ] Commands
- [x] Jump to symbols, open symbol in split,... (`open_split` and friends)
- [x] Rename symbols (`rename`)
- [x] Preview symbol (`preview` and friends)
- [ ] Hover docs
- [ ] Call hierarchy
- [x] LSP
- [x] LSP Support
- [x] LSP server selection (ignore, allow_only, use first, use all, etc.)
- [ ] CoC Support
See #879 for the tracking issue of these features.
This source is currently experimental, so in order to use it, you need to first
add `"document_symbols"` to `config.sources` and open it with the command
```
:Neotree document_symbols
```
### Source Selector
![Neo-tree source selector](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-source-selector.png)
You can enable a clickable source selector in either the winbar (requires neovim 0.8+) or the statusline.
To do so, set one of these options to `true`:
```lua
require("neo-tree").setup({
source_selector = {
winbar = false,
statusline = false
}
})
```
There are many configuration options to change the style of these tabs.
See [lua/neo-tree/defaults.lua](lua/neo-tree/defaults.lua) for details.
## Configuration and Customization
This is designed to be flexible. The way that is achieved is by making
everything a function, or a string that identifies a built-in function. All of the
built-in functions can be replaced with your own implementation, or you can
add new ones.
Each node in the tree is created from the renderer specified for the given node
type, and each renderer is a list of component configs to be rendered in order.
Each component is a function, either built-in or specified in your config. Those
functions simply return the text and highlight group for the component.
Additionally, there is an events system that you can hook into. If you want to
show some new data point related to your files, gather it in the
`before_render` event, create a component to display it, and reference that
component in the renderer for the `file` and/or `directory` type.
Details on how to configure everything is in the help file at `:h
neo-tree-configuration` or online at
[neo-tree.txt](https://github.com/nvim-neo-tree/neo-tree.nvim/blob/main/doc/neo-tree.txt)
Recipes for customizations can be found on the [wiki](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Recipes). Recipes include
things like adding a component to show the
[Harpoon](https://github.com/ThePrimeagen/harpoon) index for files, or
responding to the `"file_opened"` event to auto clear the search when you open a
file.
## Why?
There are many tree plugins for (neo)vim, so why make another one? Well, I
wanted something that was:
1. Easy to maintain and enhance.
2. Stable.
3. Easy to customize.
### Easy to maintain and enhance
This plugin is designed to grow and be flexible. This is accomplished by making
the code as decoupled and functional as possible. Hopefully new contributors
will find it easy to work with.
One big difference between this plugin and the ones that came before it, which
is also what finally pushed me over the edge into making a new plugin, is that
we now have libraries to build upon that did not exist when other tree plugins
were created. Most notably, [nui.nvim](https://github.com/MunifTanjim/nui.nvim)
and [plenary.nvm](https://github.com/nvim-lua/plenary.nvim). Building upon
shared libraries will go a long way in making neo-tree easy to maintain.
### Stable
This project will have releases and release tags that follow a simplified
Semantic Versioning scheme. The quickstart instructions will always refer to
the latest stable major version. Following the **main** branch is for
contributors and those that always want bleeding edge. There will be branches
for **v1.x**, **v2.x**, etc which will receive updates after a short testing
period in **main**. You should be safe to follow those branches and be sure
your tree won't break in an update. There will also be tags for each release
pushed to those branches named **v1.1**, **v1.2**, etc. If stability is
critical to you, or a bug accidentally make it into **v1.x**, you can use those
tags instead. It's possible we may backport bug fixes to those tags, but no
garauntees on that.
There will never be a breaking change within a major version (1.x, 2.x, etc.) If
a breaking change is needed, there will be depracation warnings in the prior
major version, and the breaking change will happen in the next major version.
### Easy to Customize
Neo-tree follows in the spirit of plugins like
[lualine.nvim](https://github.com/nvim-lualine/lualine.nvim) and
[nvim-cokeline](https://github.com/noib3/nvim-cokeline). Everything will be
configurable and take either strings, tables, or functions. You can take sane
defaults or build your tree items from scratch. There should be the ability to
add any features you can think of through existing hooks in the setup function.
## What is a Breaking Change?
As of v1.30, a breaking change is defined as anything that _changes_ existing:
- vim commands (`:NeoTreeShow`, `:NeoTreeReveal`, etc)
- configuration options that are passed into the `setup()` function
- `NeoTree*` highlight groups
- lua functions exported in the following modules that are not prefixed with `_`:
* `neo-tree`
* `neo-tree.events`
* `neo-tree.sources.manager`
* `neo-tree.sources.*` (init.lua files)
* `neo-tree.sources.*.commands`
* `neo-tree.ui.renderer`
* `neo-tree.utils`
If there are other functions you would like to use that are not yet considered
part of the public API, please open an issue so we can discuss it.
## Contributions
Contributions are encouraged. Please see [CONTRIBUTING](CONTRIBUTING.md) for more details.
## Acknowledgements
This project relies upon these two excellent libraries:
- [nui.nvim](https://github.com/MunifTanjim/nui.nvim) for all UI components, including the tree!
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for backend utilities, such as scanning the filesystem.
The design is heavily inspired by these excellent plugins:
- [lualine.nvim](https://github.com/nvim-lualine/lualine.nvim)
- [nvim-cokeline](https://github.com/noib3/nvim-cokeline)
Everything I know about writing a tree control in lua, I learned from:
- [nvim-tree.lua](https://github.com/nvim-tree/nvim-tree.lua)

1747
bundle/neo-tree.nvim/doc/neo-tree.txt vendored Normal file

File diff suppressed because it is too large Load Diff

257
bundle/neo-tree.nvim/lua/neo-tree.lua vendored Normal file
View File

@ -0,0 +1,257 @@
local vim = vim
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local setup = require("neo-tree.setup")
-- If you add a new source, you need to add it to the sources table.
-- Each source should have a defaults module that contains the default values
-- for the source config, and a setup function that takes that config.
local sources = {
"filesystem",
"buffers",
"git_status",
}
local M = {}
local check_source = function(source_name)
if not utils.truthy(source_name) then
source_name = M.config.default_source
end
local success, result = pcall(require, "neo-tree.sources." .. source_name)
if not success then
error("Source " .. source_name .. " could not be loaded: ", result)
end
return source_name
end
local get_position = function(source_name)
local pos = utils.get_value(M, "config." .. source_name .. ".window.position", "left", false)
return pos
end
M.ensure_config = function()
if not M.config then
M.setup({ log_to_file = false }, true)
end
end
--DEPRECATED in v2.x
M.close_all_except = function(source_name)
-- this entire function is faulty now that position can be overriden at runtime
source_name = check_source(source_name)
local target_pos = get_position(source_name)
for _, name in ipairs(sources) do
if name ~= source_name then
local pos = utils.get_value(M, "config." .. name .. ".window.position", "left", false)
if pos == target_pos then
manager.close(name)
end
end
end
renderer.close_all_floating_windows()
end
--DEPRECATED in v2.x
M.close = manager.close
--DEPRECATED in v2.x, use manager.close_all()
M.close_all = function(at_position)
renderer.close_all_floating_windows()
if type(at_position) == "string" and at_position > "" then
for _, name in ipairs(sources) do
local pos = get_position(name)
if pos == at_position then
manager.close(name)
end
end
else
for _, name in ipairs(sources) do
manager.close(name)
end
end
end
--DEPRECATED in v2.x, use commands.execute()
M.float = function(source_name, toggle_if_open)
M.ensure_config()
source_name = check_source(source_name)
if toggle_if_open then
if renderer.close_floating_window(source_name) then
-- It was open, and now it's not.
return
end
end
renderer.close_all_floating_windows()
manager.close(source_name) -- in case this source is open in a sidebar
manager.float(source_name)
end
--DEPRECATED in v2.x, use commands.execute()
M.focus = function(source_name, close_others, toggle_if_open)
M.ensure_config()
source_name = check_source(source_name)
if get_position(source_name) == "current" then
M.show_in_split(source_name, toggle_if_open)
return
end
if toggle_if_open then
if manager.close(source_name) then
-- It was open, and now it's not.
return
end
end
if close_others == nil then
close_others = true
end
if close_others then
M.close_all_except(source_name)
end
manager.focus(source_name)
end
--DEPRECATED in v2.x, use commands.execute()
M.reveal_current_file = function(source_name, toggle_if_open, force_cwd)
M.ensure_config()
source_name = check_source(source_name)
if get_position(source_name) == "current" then
M.reveal_in_split(source_name, toggle_if_open)
return
end
if toggle_if_open then
if manager.close(source_name) then
-- It was open, and now it's not.
return
end
end
manager.reveal_current_file(source_name, nil, force_cwd)
end
--DEPRECATED in v2.x, use commands.execute()
M.reveal_in_split = function(source_name, toggle_if_open)
M.ensure_config()
source_name = check_source(source_name)
if toggle_if_open then
local state = manager.get_state(source_name, nil, vim.api.nvim_get_current_win())
if renderer.close(state) then
-- It was open, and now it's not.
return
end
end
--TODO: if we are currently in a sidebar, don't replace it with a split style
manager.reveal_in_split(source_name)
end
--DEPRECATED in v2.x, use commands.execute()
M.show_in_split = function(source_name, toggle_if_open)
M.ensure_config()
source_name = check_source(source_name)
if toggle_if_open then
local state = manager.get_state(source_name, nil, vim.api.nvim_get_current_win())
if renderer.close(state) then
-- It was open, and now it's not.
return
end
end
--TODO: if we are currently in a sidebar, don't replace it with a split style
manager.show_in_split(source_name)
end
M.get_prior_window = function(ignore_filetypes)
ignore_filetypes = ignore_filetypes or {}
local ignore = utils.list_to_dict(ignore_filetypes)
ignore["neo-tree"] = true
local tabid = vim.api.nvim_get_current_tabpage()
local wins = utils.get_value(M, "config.prior_windows", {}, true)[tabid]
if wins == nil then
return -1
end
local win_index = #wins
while win_index > 0 do
local last_win = wins[win_index]
if type(last_win) == "number" then
local success, is_valid = pcall(vim.api.nvim_win_is_valid, last_win)
if success and is_valid then
local buf = vim.api.nvim_win_get_buf(last_win)
local ft = vim.api.nvim_buf_get_option(buf, "filetype")
local bt = vim.api.nvim_buf_get_option(buf, "buftype") or "normal"
if ignore[ft] ~= true and ignore[bt] ~= true then
return last_win
end
end
end
win_index = win_index - 1
end
return -1
end
M.paste_default_config = function()
local base_path = debug.getinfo(utils.truthy).source:match("@(.*)/utils.lua$")
local config_path = base_path .. utils.path_separator .. "defaults.lua"
local lines = vim.fn.readfile(config_path)
if lines == nil then
error("Could not read neo-tree.defaults")
end
-- read up to the end of the config, jut to omit the final return
local config = {}
for _, line in ipairs(lines) do
table.insert(config, line)
if line == "}" then
break
end
end
vim.api.nvim_put(config, "l", true, false)
vim.schedule(function()
vim.cmd("normal! `[v`]=")
end)
end
M.buffer_enter_event = setup.buffer_enter_event
M.win_enter_event = setup.win_enter_event
--DEPRECATED in v2.x
--BREAKING CHANGE: Removed the do_not_focus and close_others options in 2.0
--M.show = function(source_name, do_not_focus, close_others, toggle_if_open)
M.show = function(source_name, toggle_if_open)
M.ensure_config()
source_name = check_source(source_name)
if get_position(source_name) == "current" then
M.show_in_split(source_name, toggle_if_open)
return
end
if toggle_if_open then
if manager.close(source_name) then
-- It was open, and now it's not.
return
end
end
M.close_all_except(source_name)
manager.show(source_name)
end
M.set_log_level = function(level)
log.set_level(level)
end
M.setup = function(config, is_auto_config)
M.config = require("neo-tree.setup").merge_config(config, is_auto_config)
local netrw = require("neo-tree.setup.netrw")
if not is_auto_config and netrw.get_hijack_netrw_behavior() ~= "disabled" then
vim.cmd("silent! autocmd! FileExplorer *")
netrw.hijack()
end
end
M.show_logs = function()
vim.cmd("tabnew " .. log.outfile)
end
return M

View File

@ -0,0 +1,127 @@
local log = require("neo-tree.log")
Node = {}
function Node:new(value)
local props = { prev = nil, next = nil, value = value }
setmetatable(props, self)
self.__index = self
return props
end
LinkedList = {}
function LinkedList:new()
local props = { head = nil, tail = nil, size = 0 }
setmetatable(props, self)
self.__index = self
return props
end
function LinkedList:add_node(node)
if self.head == nil then
self.head = node
self.tail = node
else
self.tail.next = node
node.prev = self.tail
self.tail = node
end
self.size = self.size + 1
return node
end
function LinkedList:remove_node(node)
if node.prev ~= nil then
node.prev.next = node.next
end
if node.next ~= nil then
node.next.prev = node.prev
end
if self.head == node then
self.head = node.next
end
if self.tail == node then
self.tail = node.prev
end
self.size = self.size - 1
node.prev = nil
node.next = nil
node.value = nil
end
-- First in Last Out
Queue = {}
function Queue:new()
local props = { _list = LinkedList:new() }
setmetatable(props, self)
self.__index = self
return props
end
---Add an element to the end of the queue.
---@param value any The value to add.
function Queue:add(value)
self._list:add_node(Node:new(value))
end
---Iterates over the entire list, running func(value) on each element.
---If func returns true, the element is removed from the list.
---@param func function The function to run on each element.
function Queue:for_each(func)
local node = self._list.head
while node ~= nil do
local result = func(node.value)
local node_is_next = false
if result then
if type(result) == "boolean" then
local node_to_remove = node
node = node.next
node_is_next = true
self._list:remove_node(node_to_remove)
elseif type(result) == "table" then
if type(result.handled) == "boolean" and result.handled == true then
log.trace(
"Handler ",
node.value.id,
" for "
.. node.value.event
.. " returned handled = true, skipping the rest of the queue."
)
return result
end
end
end
if not node_is_next then
node = node.next
end
end
end
function Queue:is_empty()
return self._list.size == 0
end
function Queue:remove_by_id(id)
local current = self._list.head
while current ~= nil do
local is_match = false
local item = current.value
if item ~= nil then
local item_id = item.id or item
if item_id == id then
is_match = true
end
end
if is_match then
local next = current.next
self._list:remove_node(current)
current = next
else
current = current.next
end
end
end
return {
Queue = Queue,
LinkedList = LinkedList,
}

View File

@ -0,0 +1,122 @@
local parser = require("neo-tree.command.parser")
local utils = require("neo-tree.utils")
local M = {
show_key_value_completions = true,
}
local get_path_completions = function(key_prefix, base_path)
key_prefix = key_prefix or ""
local completions = {}
local expanded = parser.resolve_path(base_path)
local path_completions = vim.fn.glob(expanded .. "*", false, true)
for _, completion in ipairs(path_completions) do
if expanded ~= base_path then
-- we need to recreate the relative path from the aboluste path
-- first strip trailing slashes to normalize
if expanded:sub(-1) == utils.path_separator then
expanded = expanded:sub(1, -2)
end
if base_path:sub(-1) == utils.path_separator then
base_path = base_path:sub(1, -2)
end
-- now put just the current completion onto the base_path being used
completion = base_path .. string.sub(completion, #expanded + 1)
end
table.insert(completions, key_prefix .. completion)
end
return table.concat(completions, "\n")
end
local get_ref_completions = function(key_prefix)
key_prefix = key_prefix or ""
local completions = { key_prefix .. "HEAD" }
local ok, refs = utils.execute_command("git show-ref")
if not ok then
return ""
end
for _, ref in ipairs(refs) do
local _, i = ref:find("refs%/%a+%/")
if i then
table.insert(completions, key_prefix .. ref:sub(i + 1))
end
end
return table.concat(completions, "\n")
end
M.complete_args = function(argLead, cmdLine)
local candidates = {}
local existing = utils.split(cmdLine, " ")
local parsed = parser.parse(existing, false)
local eq = string.find(argLead, "=")
if eq == nil then
if M.show_key_value_completions then
-- may be the start of a new key=value pair
for _, key in ipairs(parser.list_args) do
key = tostring(key)
if key:find(argLead, 1, true) and not parsed[key] then
table.insert(candidates, key .. "=")
end
end
for _, key in ipairs(parser.path_args) do
key = tostring(key)
if key:find(argLead, 1, true) and not parsed[key] then
table.insert(candidates, key .. "=./")
end
end
for _, key in ipairs(parser.ref_args) do
key = tostring(key)
if key:find(argLead, 1, true) and not parsed[key] then
table.insert(candidates, key .. "=")
end
end
end
else
-- continuation of a key=value pair
local key = string.sub(argLead, 1, eq - 1)
local value = string.sub(argLead, eq + 1)
local arg_type = parser.arg_type_lookup[key]
if arg_type == parser.PATH then
return get_path_completions(key .. "=", value)
elseif arg_type == parser.REF then
return get_ref_completions(key .. "=")
elseif arg_type == parser.LIST then
local valid_values = parser.arguments[key].values
if valid_values and not parsed[key] then
for _, vv in ipairs(valid_values) do
if vv:find(value) then
table.insert(candidates, key .. "=" .. vv)
end
end
end
end
end
-- may be a value without a key
for value, key in pairs(parser.reverse_lookup) do
value = tostring(value)
local key_already_used = false
if parser.arg_type_lookup[key] == parser.LIST then
key_already_used = type(parsed[key]) ~= "nil"
else
key_already_used = type(parsed[value]) ~= "nil"
end
if not key_already_used and value:find(argLead, 1, true) then
table.insert(candidates, value)
end
end
if #candidates == 0 then
-- default to path completion
return get_path_completions(nil, argLead) .. "\n" .. get_ref_completions(nil)
end
return table.concat(candidates, "\n")
end
return M

View File

@ -0,0 +1,207 @@
local parser = require("neo-tree.command.parser")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local inputs = require("neo-tree.ui.inputs")
local completion = require("neo-tree.command.completion")
local do_show_or_focus, handle_reveal
local M = {
complete_args = completion.complete_args,
}
---Executes a Neo-tree action from outside of a Neo-tree window,
---such as show, hide, navigate, etc.
---@param args table The action to execute. The table can have the following keys:
--- action = string The action to execute, can be one of:
--- "close",
--- "focus", <-- default value
--- "show",
--- source = string The source to use for this action. This will default
--- to the default_source specified in the user's config.
--- Can be one of:
--- "filesystem",
--- "buffers",
--- "git_status",
-- "migrations"
--- position = string The position this action will affect. This will default
--- to the the last used position or the position specified
--- in the user's config for the given source. Can be one of:
--- "left",
--- "right",
--- "float",
--- "current"
--- toggle = boolean Whether to toggle the visibility of the Neo-tree window.
--- reveal = boolean Whether to reveal the current file in the Neo-tree window.
--- reveal_file = string The specific file to reveal.
--- dir = string The root directory to set.
--- git_base = string The git base used for diff
M.execute = function(args)
local nt = require("neo-tree")
nt.ensure_config()
if args.source == "migrations" then
require("neo-tree.setup.deprecations").show_migrations()
return
end
args.action = args.action or "focus"
-- handle close action, which can specify a source and/or position
if args.action == "close" then
if args.source then
manager.close(args.source, args.position)
else
manager.close_all(args.position)
end
return
end
-- The rest of the actions require a source
args.source = args.source or nt.config.default_source
-- If position=current was requested, but we are currently in a neo-tree window,
-- then we need to override that.
if args.position == "current" and vim.bo.filetype == "neo-tree" then
local position = vim.api.nvim_buf_get_var(0, "neo_tree_position")
if position then
args.position = position
end
end
-- Now get the correct state
local state
local requested_position = args.position or nt.config[args.source].window.position
if requested_position == "current" then
local winid = vim.api.nvim_get_current_win()
state = manager.get_state(args.source, nil, winid)
else
state = manager.get_state(args.source, nil, nil)
end
-- Next handle toggle, the rest is irrelevant if there is a window to toggle
if args.toggle then
if renderer.close(state) then
-- It was open, and now it's not.
return
end
end
-- Handle position override
local default_position = nt.config[args.source].window.position
local current_position = state.current_position or default_position
local position_changed = false
if args.position then
state.current_position = args.position
position_changed = args.position ~= current_position
end
-- Handle setting directory if requested
local path_changed = false
if utils.truthy(args.dir) then
if #args.dir > 1 and args.dir:sub(-1) == utils.path_separator then
args.dir = args.dir:sub(1, -2)
end
path_changed = state.path ~= args.dir
else
args.dir = state.path
end
-- Handle setting git ref
local git_base_changed = state.git_base ~= args.git_base
if utils.truthy(args.git_base) then
state.git_base = args.git_base
end
-- Handle reveal logic
args.reveal = args.reveal or args.reveal_force_cwd
local do_reveal = utils.truthy(args.reveal_file)
if args.reveal and not do_reveal then
args.reveal_file = manager.get_path_to_reveal()
do_reveal = utils.truthy(args.reveal_file)
end
-- All set, now show or focus the window
local force_navigate = path_changed or do_reveal or git_base_changed or state.dirty
if position_changed and args.position ~= "current" and current_position ~= "current" then
manager.close(args.source)
end
if do_reveal then
handle_reveal(args, state)
else
do_show_or_focus(args, state, force_navigate)
end
end
---Parses and executes the command line. Use execute(args) instead.
---@param ... string Argument as strings.
M._command = function(...)
local args = parser.parse({ ... }, true)
M.execute(args)
end
do_show_or_focus = function(args, state, force_navigate)
local window_exists = renderer.window_exists(state)
local function close_other_sources()
if not window_exists then
-- Clear the space in case another source is already open
local target_position = args.position or state.current_position or state.window.position
if target_position ~= "current" then
manager.close_all(target_position)
end
end
end
if args.action == "show" then
-- "show" means show the window without focusing it
if window_exists and not force_navigate then
-- There's nothing to do here, we are already at the target state
return
end
close_other_sources()
local current_win = vim.api.nvim_get_current_win()
manager.navigate(state, args.dir, args.reveal_file, function()
-- navigate changes the window to neo-tree, so just quickly hop back to the original window
vim.api.nvim_set_current_win(current_win)
end, false)
elseif args.action == "focus" then
-- "focus" mean open and jump to the window if closed, and just focus it if already opened
if window_exists then
vim.api.nvim_set_current_win(state.winid)
end
if force_navigate or not window_exists then
close_other_sources()
manager.navigate(state, args.dir, args.reveal_file, nil, false)
end
end
end
handle_reveal = function(args, state)
-- Deal with cwd if we need to
local cwd = state.path
local path = args.reveal_file
if cwd == nil then
cwd = manager.get_cwd(state)
end
if args.reveal_force_cwd and not utils.is_subpath(cwd, path) then
args.dir, _ = utils.split_path(path)
do_show_or_focus(args, state, true)
return
elseif not utils.is_subpath(cwd, path) then
-- force was not specified, so we need to ask the user
cwd, _ = utils.split_path(path)
inputs.confirm("File not in cwd. Change cwd to " .. cwd .. "?", function(response)
if response == true then
args.dir = cwd
else
args.reveal_file = nil
end
do_show_or_focus(args, state, true)
end)
return
else
do_show_or_focus(args, state, true)
end
end
return M

View File

@ -0,0 +1,175 @@
local utils = require("neo-tree.utils")
local M = {
FLAG = "<FLAG>",
LIST = "<LIST>",
PATH = "<PATH>",
REF = "<REF>",
}
M.setup = function(all_source_names)
local source_names = utils.table_copy(all_source_names)
table.insert(source_names, "migrations")
-- For lists, the first value is the default value.
local arguments = {
action = {
type = M.LIST,
values = {
"close",
"focus",
"show",
},
},
position = {
type = M.LIST,
values = {
"left",
"right",
"top",
"bottom",
"float",
"current",
},
},
source = {
type = M.LIST,
values = source_names,
},
dir = { type = M.PATH, stat_type = "directory" },
reveal_file = { type = M.PATH, stat_type = "file" },
git_base = { type = M.REF },
toggle = { type = M.FLAG },
reveal = { type = M.FLAG },
reveal_force_cwd = { type = M.FLAG },
}
local arg_type_lookup = {}
local list_args = {}
local path_args = {}
local ref_args = {}
local flag_args = {}
local reverse_lookup = {}
for name, def in pairs(arguments) do
arg_type_lookup[name] = def.type
if def.type == M.LIST then
table.insert(list_args, name)
for _, vv in ipairs(def.values) do
reverse_lookup[tostring(vv)] = name
end
elseif def.type == M.PATH then
table.insert(path_args, name)
elseif def.type == M.FLAG then
table.insert(flag_args, name)
reverse_lookup[name] = M.FLAG
elseif def.type == M.REF then
table.insert(ref_args, name)
else
error("Unknown type: " .. def.type)
end
end
M.arguments = arguments
M.list_args = list_args
M.path_args = path_args
M.ref_args = ref_args
M.flag_args = flag_args
M.arg_type_lookup = arg_type_lookup
M.reverse_lookup = reverse_lookup
end
M.resolve_path = function(path, validate_type)
local expanded = vim.fn.expand(path)
local abs_path = vim.fn.fnamemodify(expanded, ":p")
if validate_type then
local stat = vim.loop.fs_stat(abs_path)
if stat.type ~= validate_type then
error("Invalid path: " .. path .. " is not a " .. validate_type)
end
end
return abs_path
end
M.verify_git_ref = function(ref)
local ok, _ = utils.execute_command("git rev-parse --verify " .. ref)
return ok
end
local parse_arg = function(result, arg)
if type(arg) == "string" then
local eq = arg:find("=")
if eq then
local key = arg:sub(1, eq - 1)
local value = arg:sub(eq + 1)
local def = M.arguments[key]
if not def.type then
error("Invalid argument: " .. arg)
end
if def.type == M.PATH then
result[key] = M.resolve_path(value, def.stat_type)
elseif def.type == M.FLAG then
if value == "true" then
result[key] = true
elseif value == "false" then
result[key] = false
else
error("Invalid value for " .. key .. ": " .. value)
end
elseif def.type == M.REF then
if not M.verify_git_ref(value) then
error("Invalid value for " .. key .. ": " .. value)
end
result[key] = value
else
result[key] = value
end
else
local value = arg
local key = M.reverse_lookup[value]
if key == nil then
-- maybe it's a git ref
if M.verify_git_ref(value) then
result["git_base"] = value
return
end
-- maybe it's a path
local path = M.resolve_path(value)
local stat = vim.loop.fs_stat(path)
if stat then
if stat.type == "directory" then
result["dir"] = path
elseif stat.type == "file" then
result["reveal_file"] = path
end
else
error("Invalid argument: " .. arg)
end
elseif key == M.FLAG then
result[value] = true
else
result[key] = value
end
end
end
end
M.parse = function(args, strict_checking)
require("neo-tree").ensure_config()
local result = {}
if type(args) == "string" then
args = utils.split(args, " ")
end
-- read args from user
for _, arg in ipairs(args) do
local success, err = pcall(parse_arg, result, arg)
if strict_checking and not success then
error(err)
end
end
return result
end
return M

View File

@ -0,0 +1,615 @@
local config = {
-- If a user has a sources list it will replace this one.
-- Only sources listed here will be loaded.
-- You can also add an external source by adding it's name to this list.
-- The name used here must be the same name you would use in a require() call.
sources = {
"filesystem",
"buffers",
"git_status",
-- "document_symbols",
},
add_blank_line_at_top = false, -- Add a blank line at the top of the tree.
auto_clean_after_session_restore = false, -- Automatically clean up broken neo-tree buffers saved in sessions
close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab
-- popup_border_style is for input and confirmation dialogs.
-- Configurtaion of floating window is done in the individual source sections.
-- "NC" is a special style that works well with NormalNC set
close_floats_on_escape_key = true,
default_source = "filesystem",
enable_diagnostics = true,
enable_git_status = true,
enable_modified_markers = true, -- Show markers for files with unsaved changes.
enable_opened_markers = true, -- Enable tracking of opened files. Required for `components.name.highlight_opened_files`
enable_refresh_on_write = true, -- Refresh the tree when a file is written. Only used if `use_libuv_file_watcher` is false.
git_status_async = true,
-- These options are for people with VERY large git repos
git_status_async_options = {
batch_size = 1000, -- how many lines of git status results to process at a time
batch_delay = 10, -- delay in ms between batches. Spreads out the workload to let other processes run.
max_lines = 10000, -- How many lines of git status results to process. Anything after this will be dropped.
-- Anything before this will be used. The last items to be processed are the untracked files.
},
hide_root_node = false, -- Hide the root node.
retain_hidden_root_indent = false, -- IF the root node is hidden, keep the indentation anyhow.
-- This is needed if you use expanders because they render in the indent.
log_level = "info", -- "trace", "debug", "info", "warn", "error", "fatal"
log_to_file = false, -- true, false, "/path/to/file.log", use :NeoTreeLogs to show the file
open_files_in_last_window = true, -- false = open files in top left window
open_files_do_not_replace_types = { "terminal", "trouble", "qf" }, -- when opening files, do not use windows containing these filetypes or buftypes
popup_border_style = "NC", -- "double", "none", "rounded", "shadow", "single" or "solid"
resize_timer_interval = 500, -- in ms, needed for containers to redraw right aligned and faded content
-- set to -1 to disable the resize timer entirely
-- -- NOTE: this will speed up to 50 ms for 1 second following a resize
sort_case_insensitive = false, -- used when sorting files and directories in the tree
sort_function = nil , -- uses a custom function for sorting files and directories in the tree
use_popups_for_input = true, -- If false, inputs will use vim.ui.input() instead of custom floats.
use_default_mappings = true,
-- source_selector provides clickable tabs to switch between sources.
source_selector = {
winbar = false, -- toggle to show selector on winbar
statusline = false, -- toggle to show selector on statusline
show_scrolled_off_parent_node = false, -- this will replace the tabs with the parent path
-- of the top visible node when scrolled down.
sources = {
{ source = "filesystem" },
{ source = "buffers" },
{ source = "git_status" },
},
content_layout = "start", -- only with `tabs_layout` = "equal", "focus"
-- start : |/ 裡 bufname \/...
-- end : |/ 裡 bufname \/...
-- center : |/ 裡 bufname \/...
tabs_layout = "equal", -- start, end, center, equal, focus
-- start : |/ a \/ b \/ c \ |
-- end : | / a \/ b \/ c \|
-- center : | / a \/ b \/ c \ |
-- equal : |/ a \/ b \/ c \|
-- active : |/ focused tab \/ b \/ c \|
truncation_character = "", -- character to use when truncating the tab label
tabs_min_width = nil, -- nil | int: if int padding is added based on `content_layout`
tabs_max_width = nil, -- this will truncate text even if `text_trunc_to_fit = false`
padding = 0, -- can be int or table
-- padding = { left = 2, right = 0 },
-- separator = "▕", -- can be string or table, see below
separator = { left = "", right= "" },
-- separator = { left = "/", right = "\\", override = nil }, -- |/ a \/ b \/ c \...
-- separator = { left = "/", right = "\\", override = "right" }, -- |/ a \ b \ c \...
-- separator = { left = "/", right = "\\", override = "left" }, -- |/ a / b / c /...
-- separator = { left = "/", right = "\\", override = "active" },-- |/ a / b:active \ c \...
-- separator = "|", -- || a | b | c |...
separator_active = nil, -- set separators around the active tab. nil falls back to `source_selector.separator`
show_separator_on_edge = false,
-- true : |/ a \/ b \/ c \|
-- false : | a \/ b \/ c |
highlight_tab = "NeoTreeTabInactive",
highlight_tab_active = "NeoTreeTabActive",
highlight_background = "NeoTreeTabInactive",
highlight_separator = "NeoTreeTabSeparatorInactive",
highlight_separator_active = "NeoTreeTabSeparatorActive",
},
--
--event_handlers = {
-- {
-- event = "before_render",
-- handler = function (state)
-- -- add something to the state that can be used by custom components
-- end
-- },
-- {
-- event = "file_opened",
-- handler = function(file_path)
-- --auto close
-- require("neo-tree").close_all()
-- end
-- },
-- {
-- event = "file_opened",
-- handler = function(file_path)
-- --clear search after opening a file
-- require("neo-tree.sources.filesystem").reset_search()
-- end
-- },
-- {
-- event = "file_renamed",
-- handler = function(args)
-- -- fix references to file
-- print(args.source, " renamed to ", args.destination)
-- end
-- },
-- {
-- event = "file_moved",
-- handler = function(args)
-- -- fix references to file
-- print(args.source, " moved to ", args.destination)
-- end
-- },
-- {
-- event = "neo_tree_buffer_enter",
-- handler = function()
-- vim.cmd 'highlight! Cursor blend=100'
-- end
-- },
-- {
-- event = "neo_tree_buffer_leave",
-- handler = function()
-- vim.cmd 'highlight! Cursor guibg=#5f87af blend=0'
-- end
-- },
-- {
-- event = "neo_tree_window_before_open",
-- handler = function(args)
-- print("neo_tree_window_before_open", vim.inspect(args))
-- end
-- },
-- {
-- event = "neo_tree_window_after_open",
-- handler = function(args)
-- vim.cmd("wincmd =")
-- end
-- },
-- {
-- event = "neo_tree_window_before_close",
-- handler = function(args)
-- print("neo_tree_window_before_close", vim.inspect(args))
-- end
-- },
-- {
-- event = "neo_tree_window_after_close",
-- handler = function(args)
-- vim.cmd("wincmd =")
-- end
-- }
--},
default_component_configs = {
container = {
enable_character_fade = true,
width = "100%",
right_padding = 0,
},
--diagnostics = {
-- symbols = {
-- hint = "H",
-- info = "I",
-- warn = "!",
-- error = "X",
-- },
-- highlights = {
-- hint = "DiagnosticSignHint",
-- info = "DiagnosticSignInfo",
-- warn = "DiagnosticSignWarn",
-- error = "DiagnosticSignError",
-- },
--},
indent = {
indent_size = 2,
padding = 1,
-- indent guides
with_markers = true,
indent_marker = "",
last_indent_marker = "",
highlight = "NeoTreeIndentMarker",
-- expander config, needed for nesting files
with_expanders = nil, -- if nil and file nesting is enabled, will enable expanders
expander_collapsed = "",
expander_expanded = "",
expander_highlight = "NeoTreeExpander",
},
icon = {
folder_closed = "",
folder_open = "",
folder_empty = "",
folder_empty_open = "",
-- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there
-- then these will never be used.
default = "*",
highlight = "NeoTreeFileIcon"
},
modified = {
symbol = "[+] ",
highlight = "NeoTreeModified",
},
name = {
trailing_slash = false,
highlight_opened_files = false, -- Requires `enable_opened_markers = true`.
-- Take values in { false (no highlight), true (only loaded),
-- "all" (both loaded and unloaded)}. For more information,
-- see the `show_unloaded` config of the `buffers` source.
use_git_status_colors = true,
highlight = "NeoTreeFileName",
},
git_status = {
symbols = {
-- Change type
added = "", -- NOTE: you can set any of these to an empty string to not show them
deleted = "",
modified = "",
renamed = "",
-- Status type
untracked = "",
ignored = "",
unstaged = "",
staged = "",
conflict = "",
},
align = "right",
},
},
renderers = {
directory = {
{ "indent" },
{ "icon" },
{ "current_filter" },
{
"container",
content = {
{ "name", zindex = 10 },
-- {
-- "symlink_target",
-- zindex = 10,
-- highlight = "NeoTreeSymbolicLinkTarget",
-- },
{ "clipboard", zindex = 10 },
{ "diagnostics", errors_only = true, zindex = 20, align = "right", hide_when_expanded = true },
{ "git_status", zindex = 20, align = "right", hide_when_expanded = true },
},
},
},
file = {
{ "indent" },
{ "icon" },
{
"container",
content = {
{
"name",
zindex = 10
},
-- {
-- "symlink_target",
-- zindex = 10,
-- highlight = "NeoTreeSymbolicLinkTarget",
-- },
{ "clipboard", zindex = 10 },
{ "bufnr", zindex = 10 },
{ "modified", zindex = 20, align = "right" },
{ "diagnostics", zindex = 20, align = "right" },
{ "git_status", zindex = 20, align = "right" },
},
},
},
message = {
{ "indent", with_markers = false },
{ "name", highlight = "NeoTreeMessage" },
},
terminal = {
{ "indent" },
{ "icon" },
{ "name" },
{ "bufnr" }
}
},
nesting_rules = {},
-- Global custom commands that will be available in all sources (if not overridden in `opts[source_name].commands`)
--
-- You can then reference the custom command by adding a mapping to it:
-- globally -> `opts.window.mappings`
-- locally -> `opt[source_name].window.mappings` to make it source specific.
--
-- commands = { | window { | filesystem {
-- hello = function() | mappings = { | commands = {
-- print("Hello world") | ["<C-c>"] = "hello" | hello = function()
-- end | } | print("Hello world in filesystem")
-- } | } | end
--
-- see `:h neo-tree-global-custom-commands`
commands = {}, -- A list of functions
window = { -- see https://github.com/MunifTanjim/nui.nvim/tree/main/lua/nui/popup for
-- possible options. These can also be functions that return these options.
position = "left", -- left, right, top, bottom, float, current
width = 40, -- applies to left and right positions
height = 15, -- applies to top and bottom positions
auto_expand_width = false, -- expand the window when file exceeds the window width. does not work with position = "float"
popup = { -- settings that apply to float position only
size = {
height = "80%",
width = "50%",
},
position = "50%", -- 50% means center it
-- you can also specify border here, if you want a different setting from
-- the global popup_border_style.
},
same_level = false, -- Create and paste/move files/directories on the same level as the directory under cursor (as opposed to within the directory under cursor).
insert_as = "child", -- Affects how nodes get inserted into the tree during creation/pasting/moving of files if the node under the cursor is a directory:
-- "child": Insert nodes as children of the directory under cursor.
-- "sibling": Insert nodes as siblings of the directory under cursor.
-- Mappings for tree window. See `:h neo-tree-mappings` for a list of built-in commands.
-- You can also create your own commands by providing a function instead of a string.
mapping_options = {
noremap = true,
nowait = true,
},
mappings = {
["<space>"] = {
"toggle_node",
nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use
},
["<2-LeftMouse>"] = "open",
["<cr>"] = "open",
["<esc>"] = "revert_preview",
["P"] = { "toggle_preview", config = { use_float = true } },
["l"] = "focus_preview",
["S"] = "open_split",
-- ["S"] = "split_with_window_picker",
["s"] = "open_vsplit",
-- ["s"] = "vsplit_with_window_picker",
["t"] = "open_tabnew",
-- ["<cr>"] = "open_drop",
-- ["t"] = "open_tab_drop",
["w"] = "open_with_window_picker",
["C"] = "close_node",
["z"] = "close_all_nodes",
--["Z"] = "expand_all_nodes",
["R"] = "refresh",
["a"] = {
"add",
-- some commands may take optional config options, see `:h neo-tree-mappings` for details
config = {
show_path = "none", -- "none", "relative", "absolute"
}
},
["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options.
["d"] = "delete",
["r"] = "rename",
["y"] = "copy_to_clipboard",
["x"] = "cut_to_clipboard",
["p"] = "paste_from_clipboard",
["c"] = "copy", -- takes text input for destination, also accepts the config.show_path and config.insert_as options
["m"] = "move", -- takes text input for destination, also accepts the config.show_path and config.insert_as options
["e"] = "toggle_auto_expand_width",
["q"] = "close_window",
["?"] = "show_help",
["<"] = "prev_source",
[">"] = "next_source",
},
},
filesystem = {
window = {
mappings = {
["H"] = "toggle_hidden",
["/"] = "fuzzy_finder",
["D"] = "fuzzy_finder_directory",
--["/"] = "filter_as_you_type", -- this was the default until v1.28
["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm
-- ["D"] = "fuzzy_sorter_directory",
["f"] = "filter_on_submit",
["<C-x>"] = "clear_filter",
["<bs>"] = "navigate_up",
["."] = "set_root",
["[g"] = "prev_git_modified",
["]g"] = "next_git_modified",
},
fuzzy_finder_mappings = { -- define keymaps for filter popup window in fuzzy_finder_mode
["<down>"] = "move_cursor_down",
["<C-n>"] = "move_cursor_down",
["<up>"] = "move_cursor_up",
["<C-p>"] = "move_cursor_up",
},
},
async_directory_scan = "auto", -- "auto" means refreshes are async, but it's synchronous when called from the Neotree commands.
-- "always" means directory scans are always async.
-- "never" means directory scans are never async.
scan_mode = "shallow", -- "shallow": Don't scan into directories to detect possible empty directory a priori
-- "deep": Scan into directories to detect empty or grouped empty directories a priori.
bind_to_cwd = true, -- true creates a 2-way binding between vim's cwd and neo-tree's root
cwd_target = {
sidebar = "tab", -- sidebar is when position = left or right
current = "window" -- current is when position = current
},
-- The renderer section provides the renderers that will be used to render the tree.
-- The first level is the node type.
-- For each node type, you can specify a list of components to render.
-- Components are rendered in the order they are specified.
-- The first field in each component is the name of the function to call.
-- The rest of the fields are passed to the function as the "config" argument.
filtered_items = {
visible = false, -- when true, they will just be displayed differently than normal items
force_visible_in_empty_folder = false, -- when true, hidden files will be shown if the root folder is otherwise empty
show_hidden_count = true, -- when true, the number of hidden items in each folder will be shown as the last entry
hide_dotfiles = true,
hide_gitignored = true,
hide_hidden = true, -- only works on Windows for hidden files/directories
hide_by_name = {
".DS_Store",
"thumbs.db"
--"node_modules",
},
hide_by_pattern = { -- uses glob style patterns
--"*.meta",
--"*/src/*/tsconfig.json"
},
always_show = { -- remains visible even if other settings would normally hide it
--".gitignored",
},
never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show
--".DS_Store",
--"thumbs.db"
},
never_show_by_pattern = { -- uses glob style patterns
--".null-ls_*",
},
},
find_by_full_path_words = false, -- `false` means it only searches the tail of a path.
-- `true` will change the filter into a full path
-- search with space as an implicit ".*", so
-- `fi init`
-- will match: `./sources/filesystem/init.lua
--find_command = "fd", -- this is determined automatically, you probably don't need to set it
--find_args = { -- you can specify extra args to pass to the find command.
-- fd = {
-- "--exclude", ".git",
-- "--exclude", "node_modules"
-- }
--},
---- or use a function instead of list of strings
--find_args = function(cmd, path, search_term, args)
-- if cmd ~= "fd" then
-- return args
-- end
-- --maybe you want to force the filter to always include hidden files:
-- table.insert(args, "--hidden")
-- -- but no one ever wants to see .git files
-- table.insert(args, "--exclude")
-- table.insert(args, ".git")
-- -- or node_modules
-- table.insert(args, "--exclude")
-- table.insert(args, "node_modules")
-- --here is where it pays to use the function, you can exclude more for
-- --short search terms, or vary based on the directory
-- if string.len(search_term) < 4 and path == "/home/cseickel" then
-- table.insert(args, "--exclude")
-- table.insert(args, "Library")
-- end
-- return args
--end,
group_empty_dirs = false, -- when true, empty folders will be grouped together
search_limit = 50, -- max number of search results when using filters
follow_current_file = false, -- This will find and focus the file in the active buffer every time
-- the current file is changed while the tree is open.
hijack_netrw_behavior = "open_default", -- netrw disabled, opening a directory opens neo-tree
-- in whatever position is specified in window.position
-- "open_current",-- netrw disabled, opening a directory opens within the
-- window like netrw would, regardless of window.position
-- "disabled", -- netrw left alone, neo-tree does not handle opening dirs
use_libuv_file_watcher = false, -- This will use the OS level file watchers to detect changes
-- instead of relying on nvim autocmd events.
},
buffers = {
bind_to_cwd = true,
follow_current_file = true, -- This will find and focus the file in the active buffer every time
-- the current file is changed while the tree is open.
group_empty_dirs = true, -- when true, empty directories will be grouped together
show_unloaded = false, -- When working with sessions, for example, restored but unfocused buffers
-- are mark as "unloaded". Turn this on to view these unloaded buffer.
window = {
mappings = {
["<bs>"] = "navigate_up",
["."] = "set_root",
["bd"] = "buffer_delete",
},
},
},
git_status = {
window = {
mappings = {
["A"] = "git_add_all",
["gu"] = "git_unstage_file",
["ga"] = "git_add_file",
["gr"] = "git_revert_file",
["gc"] = "git_commit",
["gp"] = "git_push",
["gg"] = "git_commit_and_push",
},
},
},
document_symbols = {
follow_cursor = false,
client_filters = "first",
renderers = {
root = {
{"indent"},
{"icon", default="C" },
{"name", zindex = 10},
},
symbol = {
{"indent", with_expanders = true},
{"kind_icon", default="?" },
{"container",
content = {
{"name", zindex = 10},
{"kind_name", zindex = 20, align = "right"},
}
}
},
},
window = {
mappings = {
["<cr>"] = "jump_to_symbol",
["o"] = "jump_to_symbol",
["A"] = "noop", -- also accepts the config.show_path and config.insert_as options.
["d"] = "noop",
["y"] = "noop",
["x"] = "noop",
["p"] = "noop",
["c"] = "noop",
["m"] = "noop",
["a"] = "noop",
["/"] = "filter",
["f"] = "filter_on_submit",
},
},
custom_kinds = {
-- define custom kinds here (also remember to add icon and hl group to kinds)
-- ccls
-- [252] = 'TypeAlias',
-- [253] = 'Parameter',
-- [254] = 'StaticMethod',
-- [255] = 'Macro',
},
kinds = {
Unknown = { icon = "?", hl = "" },
Root = { icon = "", hl = "NeoTreeRootName" },
File = { icon = "", hl = "Tag" },
Module = { icon = "", hl = "Exception" },
Namespace = { icon = "", hl = "Include" },
Package = { icon = "", hl = "Label" },
Class = { icon = "", hl = "Include" },
Method = { icon = "", hl = "Function" },
Property = { icon = "", hl = "@property" },
Field = { icon = "", hl = "@field" },
Constructor = { icon = "", hl = "@constructor" },
Enum = { icon = "", hl = "@number" },
Interface = { icon = "", hl = "Type" },
Function = { icon = "", hl = "Function" },
Variable = { icon = "", hl = "@variable" },
Constant = { icon = "", hl = "Constant" },
String = { icon = "", hl = "String" },
Number = { icon = "", hl = "Number" },
Boolean = { icon = "", hl = "Boolean" },
Array = { icon = "", hl = "Type" },
Object = { icon = "", hl = "Type" },
Key = { icon = "", hl = "" },
Null = { icon = "", hl = "Constant" },
EnumMember = { icon = "", hl = "Number" },
Struct = { icon = "", hl = "Type" },
Event = { icon = "", hl = "Constant" },
Operator = { icon = "", hl = "Operator" },
TypeParameter = { icon = "", hl = "Type" },
-- ccls
-- TypeAlias = { icon = ' ', hl = 'Type' },
-- Parameter = { icon = ' ', hl = '@parameter' },
-- StaticMethod = { icon = 'ﴂ ', hl = 'Function' },
-- Macro = { icon = ' ', hl = 'Macro' },
}
},
example = {
renderers = {
custom = {
{"indent"},
{"icon", default="C" },
{"custom"},
{"name"}
}
},
window = {
mappings = {
["<cr>"] = "toggle_node",
["<C-e>"] = "example_command",
["d"] = "show_debug_info",
},
},
},
}
return config

View File

@ -0,0 +1,93 @@
local vim = vim
local q = require("neo-tree.events.queue")
local log = require("neo-tree.log")
local utils = require("neo-tree.utils")
local M = {
-- Well known event names, you can make up your own
BEFORE_RENDER = "before_render",
AFTER_RENDER = "after_render",
FILE_ADDED = "file_added",
FILE_DELETED = "file_deleted",
BEFORE_FILE_MOVE = "before_file_move",
FILE_MOVED = "file_moved",
FILE_OPEN_REQUESTED = "file_open_requested",
FILE_OPENED = "file_opened",
BEFORE_FILE_RENAME = "before_file_rename",
FILE_RENAMED = "file_renamed",
FS_EVENT = "fs_event",
GIT_EVENT = "git_event",
GIT_STATUS_CHANGED = "git_status_changed",
NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter",
NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave",
NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update",
NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter",
NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave",
NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close",
NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open",
NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close",
NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open",
VIM_AFTER_SESSION_LOAD = "vim_after_session_load",
VIM_BUFFER_ADDED = "vim_buffer_added",
VIM_BUFFER_CHANGED = "vim_buffer_changed",
VIM_BUFFER_DELETED = "vim_buffer_deleted",
VIM_BUFFER_ENTER = "vim_buffer_enter",
VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set",
VIM_COLORSCHEME = "vim_colorscheme",
VIM_CURSOR_MOVED = "vim_cursor_moved",
VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed",
VIM_DIR_CHANGED = "vim_dir_changed",
VIM_INSERT_LEAVE = "vim_insert_leave",
VIM_LEAVE = "vim_leave",
VIM_LSP_REQUEST = "vim_lsp_request",
VIM_RESIZED = "vim_resized",
VIM_TAB_CLOSED = "vim_tab_closed",
VIM_TERMINAL_ENTER = "vim_terminal_enter",
VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal",
VIM_WIN_CLOSED = "vim_win_closed",
VIM_WIN_ENTER = "vim_win_enter",
}
M.define_autocmd_event = function(event_name, autocmds, debounce_frequency, seed_fn, nested)
local opts = {
setup = function()
local tpl =
":lua require('neo-tree.events').fire_event('%s', { afile = vim.fn.expand('<afile>') })"
local callback = string.format(tpl, event_name)
if nested then
callback = "++nested " .. callback
end
local autocmd = table.concat(autocmds, ",")
if not vim.startswith(autocmd, "User") then
autocmd = autocmd .. " *"
end
local cmds = {
"augroup NeoTreeEvent_" .. event_name,
"autocmd " .. autocmd .. " " .. callback,
"augroup END",
}
log.trace("Registering autocmds: %s", table.concat(cmds, "\n"))
vim.cmd(table.concat(cmds, "\n"))
end,
seed = seed_fn,
teardown = function()
log.trace("Teardown autocmds for ", event_name)
vim.cmd(string.format("autocmd! NeoTreeEvent_%s", event_name))
end,
debounce_frequency = debounce_frequency,
debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
}
log.debug("Defining autocmd event: %s", event_name)
q.define_event(event_name, opts)
end
M.clear_all_events = q.clear_all_events
M.define_event = q.define_event
M.destroy_event = q.destroy_event
M.fire_event = q.fire_event
M.subscribe = q.subscribe
M.unsubscribe = q.unsubscribe
return M

View File

@ -0,0 +1,144 @@
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local Queue = require("neo-tree.collections").Queue
local event_queues = {}
local event_definitions = {}
local M = {}
local validate_event_handler = function(event_handler)
if type(event_handler) ~= "table" then
error("Event handler must be a table")
end
if type(event_handler.event) ~= "string" then
error("Event handler must have an event")
end
if type(event_handler.handler) ~= "function" then
error("Event handler must have a handler")
end
end
M.clear_all_events = function()
for event_name, queue in pairs(event_queues) do
M.destroy_event(event_name)
end
event_queues = {}
end
M.define_event = function(event_name, opts)
local existing = event_definitions[event_name]
if existing ~= nil then
error("Event already defined: " .. event_name)
end
event_definitions[event_name] = opts
end
M.destroy_event = function(event_name)
local existing = event_definitions[event_name]
if existing == nil then
return false
end
if existing.setup_was_run and type(existing.teardown) == "function" then
local success, result = pcall(existing.teardown)
if not success then
error("Error in teardown for " .. event_name .. ": " .. result)
end
existing.setup_was_run = false
end
event_queues[event_name] = nil
return true
end
local fire_event_internal = function(event, args)
local queue = event_queues[event]
if queue == nil then
return nil
end
--log.trace("Firing event: ", event, " with args: ", args)
if queue:is_empty() then
--log.trace("Event queue is empty")
return nil
end
local seed = utils.get_value(event_definitions, event .. ".seed")
if seed ~= nil then
local success, result = pcall(seed, args)
if success and result then
log.trace("Seed for " .. event .. " returned: " .. tostring(result))
elseif success then
log.trace("Seed for " .. event .. " returned falsy, cancelling event")
else
log.error("Error in seed function for " .. event .. ": " .. result)
end
end
return queue:for_each(function(event_handler)
local remove_node = event_handler == nil or event_handler.cancelled
if not remove_node then
local success, result = pcall(event_handler.handler, args)
local id = event_handler.id or event_handler
if success then
log.trace("Handler ", id, " for " .. event .. " called successfully.")
else
log.error(string.format("Error in event handler for event %s[%s]: %s", event, id, result))
end
if event_handler.once then
event_handler.cancelled = true
return true
end
return result
end
end)
end
M.fire_event = function(event, args)
local freq = utils.get_value(event_definitions, event .. ".debounce_frequency", 0, true)
local strategy = utils.get_value(event_definitions, event .. ".debounce_strategy", 0, true)
log.trace("Firing event: ", event, " with args: ", args)
if freq > 0 then
utils.debounce("EVENT_FIRED: " .. event, function()
fire_event_internal(event, args or {})
end, freq, strategy)
else
return fire_event_internal(event, args or {})
end
end
M.subscribe = function(event_handler)
validate_event_handler(event_handler)
local queue = event_queues[event_handler.event]
if queue == nil then
log.debug("Creating queue for event: " .. event_handler.event)
queue = Queue:new()
local def = event_definitions[event_handler.event]
if def and type(def.setup) == "function" then
local success, result = pcall(def.setup)
if success then
def.setup_was_run = true
log.debug("Setup for event " .. event_handler.event .. " was run")
else
log.error("Error in setup for " .. event_handler.event .. ": " .. result)
end
end
event_queues[event_handler.event] = queue
end
log.debug("Adding event handler [", event_handler.id, "] for event: ", event_handler.event)
queue:add(event_handler)
end
M.unsubscribe = function(event_handler)
local queue = event_queues[event_handler.event]
if queue == nil then
return nil
end
queue:remove_by_id(event_handler.id or event_handler)
if queue:is_empty() then
M.destroy_event(event_handler.event)
event_queues[event_handler.event] = nil
else
event_queues[event_handler.event] = queue
end
end
return M

View File

@ -0,0 +1,156 @@
local Job = require("plenary.job")
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local git_utils = require("neo-tree.git.utils")
local M = {}
local sep = utils.path_separator
M.is_ignored = function(ignored, path, _type)
if _type == "directory" and not utils.is_windows then
path = path .. sep
end
return vim.tbl_contains(ignored, path)
end
local git_root_cache = {
known_roots = {},
dir_lookup = {},
}
local get_root_for_item = function(item)
local dir = item.type == "directory" and item.path or item.parent_path
if type(git_root_cache.dir_lookup[dir]) ~= "nil" then
return git_root_cache.dir_lookup[dir]
end
--for _, root in ipairs(git_root_cache.known_roots) do
-- if vim.startswith(dir, root) then
-- git_root_cache.dir_lookup[dir] = root
-- return root
-- end
--end
local root = git_utils.get_repository_root(dir)
if root then
git_root_cache.dir_lookup[dir] = root
table.insert(git_root_cache.known_roots, root)
else
git_root_cache.dir_lookup[dir] = false
end
return root
end
M.mark_ignored = function(state, items, callback)
local folders = {}
log.trace("================================================================================")
log.trace("IGNORED: mark_ignore BEGIN...")
for _, item in ipairs(items) do
local folder = utils.split_path(item.path)
if folder then
if not folders[folder] then
folders[folder] = {}
end
table.insert(folders[folder], item.path)
end
end
local function process_result(result)
if utils.is_windows then
--on Windows, git seems to return quotes and double backslash "path\\directory"
result = vim.tbl_map(function(item)
item = item:gsub("\\\\", "\\")
return item
end, result)
else
--check-ignore does not indicate directories the same as 'status' so we need to
--add the trailing slash to the path manually if not on Windows.
log.trace("IGNORED: Checking types of", #result, "items to see which ones are directories")
for i, item in ipairs(result) do
local stat = vim.loop.fs_stat(item)
if stat and stat.type == "directory" then
result[i] = item .. sep
end
end
end
result = vim.tbl_map(function(item)
-- remove leading and trailing " from git output
item = item:gsub('^"', ""):gsub('"$', "")
-- convert octal encoded lines to utf-8
item = git_utils.octal_to_utf8(item)
return item
end, result)
return result
end
local function finalize(all_results)
local show_anyway = state.filtered_items and state.filtered_items.hide_gitignored == false
log.trace("IGNORED: Comparing results to mark items as ignored, show_anyway:", show_anyway)
local ignored, not_ignored = 0, 0
for _, item in ipairs(items) do
if M.is_ignored(all_results, item.path, item.type) then
item.filtered_by = item.filtered_by or {}
item.filtered_by.gitignored = true
item.filtered_by.show_anyway = show_anyway
ignored = ignored + 1
else
not_ignored = not_ignored + 1
end
end
log.trace("IGNORED: mark_ignored is complete, ignored:", ignored, ", not ignored:", not_ignored)
log.trace("================================================================================")
end
local all_results = {}
if type(callback) == "function" then
local jobs = {}
local progress = 0
for folder, folder_items in pairs(folders) do
local args = { "-C", folder, "check-ignore", "--stdin" }
local job = Job:new({
command = "git",
args = args,
enabled_recording = true,
writer = folder_items,
on_start = function()
log.trace("IGNORED: Running async git with args: ", args)
end,
on_exit = function(self, code, _)
local result
if code ~= 0 then
log.debug("Failed to load ignored files for", state.path, ":", self:stderr_result())
result = {}
else
result = self:result()
end
vim.list_extend(all_results, process_result(result))
progress = progress + 1
if progress == #jobs then
finalize(all_results)
callback(all_results)
end
end,
})
table.insert(jobs, job)
end
for _, job in ipairs(jobs) do
job:start()
end
else
for folder, folder_items in pairs(folders) do
local cmd = { "git", "-C", folder, "check-ignore", unpack(folder_items) }
log.trace("IGNORED: Running cmd: ", cmd)
local result = vim.fn.systemlist(cmd)
if vim.v.shell_error == 128 then
log.debug("Failed to load ignored files for", state.path, ":", result)
result = {}
end
vim.list_extend(all_results, process_result(result))
end
finalize(all_results)
return all_results
end
end
return M

View File

@ -0,0 +1,13 @@
local status = require("neo-tree.git.status")
local ignored = require("neo-tree.git.ignored")
local git_utils = require("neo-tree.git.utils")
local M = {
get_repository_root = git_utils.get_repository_root,
is_ignored = ignored.is_ignored,
mark_ignored = ignored.mark_ignored,
status = status.status,
status_async = status.status_async,
}
return M

View File

@ -0,0 +1,339 @@
local utils = require("neo-tree.utils")
local events = require("neo-tree.events")
local Job = require("plenary.job")
local log = require("neo-tree.log")
local git_utils = require("neo-tree.git.utils")
local M = {}
local function get_simple_git_status_code(status)
-- Prioritze M then A over all others
if status:match("U") or status == "AA" or status == "DD" then
return "U"
elseif status:match("M") then
return "M"
elseif status:match("[ACR]") then
return "A"
elseif status:match("!$") then
return "!"
elseif status:match("?$") then
return "?"
else
local len = #status
while len > 0 do
local char = status:sub(len, len)
if char ~= " " then
return char
end
len = len - 1
end
return status
end
end
local function get_priority_git_status_code(status, other_status)
if not status then
return other_status
elseif not other_status then
return status
elseif status == "U" or other_status == "U" then
return "U"
elseif status == "?" or other_status == "?" then
return "?"
elseif status == "M" or other_status == "M" then
return "M"
elseif status == "A" or other_status == "A" then
return "A"
else
return status
end
end
local parse_git_status_line = function(context, line)
context.lines_parsed = context.lines_parsed + 1
if type(line) ~= "string" then
return
end
if #line < 4 then
return
end
local git_root = context.git_root
local git_status = context.git_status
local exclude_directories = context.exclude_directories
local line_parts = vim.split(line, " ")
if #line_parts < 2 then
return
end
local status = line_parts[1]
local relative_path = line_parts[2]
-- rename output is `R000 from/filename to/filename`
if status:match("^R") then
relative_path = line_parts[3]
end
-- remove any " due to whitespace or utf-8 in the path
relative_path = relative_path:gsub('^"', ""):gsub('"$', "")
-- convert octal encoded lines to utf-8
relative_path = git_utils.octal_to_utf8(relative_path)
if utils.is_windows == true then
relative_path = utils.windowize_path(relative_path)
end
local absolute_path = utils.path_join(git_root, relative_path)
-- merge status result if there are results from multiple passes
local existing_status = git_status[absolute_path]
if existing_status then
local merged = ""
local i = 0
while i < 2 do
i = i + 1
local existing_char = #existing_status >= i and existing_status:sub(i, i) or ""
local new_char = #status >= i and status:sub(i, i) or ""
local merged_char = get_priority_git_status_code(existing_char, new_char)
merged = merged .. merged_char
end
status = merged
end
git_status[absolute_path] = status
if not exclude_directories then
-- Now bubble this status up to the parent directories
local parts = utils.split(absolute_path, utils.path_separator)
table.remove(parts) -- pop the last part so we don't override the file's status
utils.reduce(parts, "", function(acc, part)
local path = acc .. utils.path_separator .. part
if utils.is_windows == true then
path = path:gsub("^" .. utils.path_separator, "")
end
local path_status = git_status[path]
local file_status = get_simple_git_status_code(status)
git_status[path] = get_priority_git_status_code(path_status, file_status)
return path
end)
end
end
---Parse "git status" output for the current working directory.
---@base git ref base
---@exclude_directories boolean Whether to skip bubling up status to directories
---@path string Path to run the git status command in, defaults to cwd.
---@return table table Table with the path as key and the status as value.
---@return table, string|nil The git root for the specified path.
M.status = function(base, exclude_directories, path)
local git_root = git_utils.get_repository_root(path)
if not utils.truthy(git_root) then
return {}
end
local C = git_root
local staged_cmd = { "git", "-C", C, "diff", "--staged", "--name-status", base, "--" }
local staged_ok, staged_result = utils.execute_command(staged_cmd)
if not staged_ok then
return {}
end
local unstaged_cmd = { "git", "-C", C, "diff", "--name-status" }
local unstaged_ok, unstaged_result = utils.execute_command(unstaged_cmd)
if not unstaged_ok then
return {}
end
local untracked_cmd = { "git", "-C", C, "ls-files", "--exclude-standard", "--others" }
local untracked_ok, untracked_result = utils.execute_command(untracked_cmd)
if not untracked_ok then
return {}
end
local context = {
git_root = git_root,
git_status = {},
exclude_directories = exclude_directories,
lines_parsed = 0,
}
for _, line in ipairs(staged_result) do
parse_git_status_line(context, line)
end
for _, line in ipairs(unstaged_result) do
if line then
line = " " .. line
end
parse_git_status_line(context, line)
end
for _, line in ipairs(untracked_result) do
if line then
line = "? " .. line
end
parse_git_status_line(context, line)
end
return context.git_status, git_root
end
local function parse_lines_batch(context, job_complete_callback)
local i, batch_size = 0, context.batch_size
if context.lines_total == nil then
-- first time through, get the total number of lines
context.lines_total = math.min(context.max_lines, #context.lines)
context.lines_parsed = 0
if context.lines_total == 0 then
if type(job_complete_callback) == "function" then
job_complete_callback()
end
return
end
end
batch_size = math.min(context.batch_size, context.lines_total - context.lines_parsed)
while i < batch_size do
i = i + 1
parse_git_status_line(context, context.lines[context.lines_parsed + 1])
end
if context.lines_parsed >= context.lines_total then
if type(job_complete_callback) == "function" then
job_complete_callback()
end
else
-- add small delay so other work can happen
vim.defer_fn(function()
parse_lines_batch(context, job_complete_callback)
end, context.batch_delay)
end
end
M.status_async = function(path, base, opts)
git_utils.get_repository_root(path, function(git_root)
if utils.truthy(git_root) then
log.trace("git.status.status_async called")
else
log.trace("status_async: not a git folder: ", path)
return false
end
local event_id = "git_status_" .. git_root
local context = {
git_root = git_root,
git_status = {},
exclude_directories = false,
lines = {},
lines_parsed = 0,
batch_size = opts.batch_size or 1000,
batch_delay = opts.batch_delay or 10,
max_lines = opts.max_lines or 100000,
}
local should_process = function(err, line, job, err_msg)
if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
job:shutdown()
return false
end
if err and err > 0 then
log.error(err_msg, err, line)
return false
end
return true
end
local job_complete_callback = function()
utils.debounce(event_id, nil, nil, nil, utils.debounce_action.COMPLETE_ASYNC_JOB)
vim.schedule(function()
events.fire_event(events.GIT_STATUS_CHANGED, {
git_root = context.git_root,
git_status = context.git_status,
})
end)
end
local parse_lines = vim.schedule_wrap(function()
parse_lines_batch(context, job_complete_callback)
end)
utils.debounce(event_id, function()
local staged_job = Job:new({
command = "git",
args = { "-C", git_root, "diff", "--staged", "--name-status", base, "--" },
enable_recording = false,
maximium_results = context.max_lines,
on_stdout = vim.schedule_wrap(function(err, line, job)
if should_process(err, line, job, "status_async staged error:") then
table.insert(context.lines, line)
end
end),
on_stderr = function(err, line)
if err and err > 0 then
log.error("status_async staged error: ", err, line)
end
end,
})
local unstaged_job = Job:new({
command = "git",
args = { "-C", git_root, "diff", "--name-status" },
enable_recording = false,
maximium_results = context.max_lines,
on_stdout = vim.schedule_wrap(function(err, line, job)
if should_process(err, line, job, "status_async unstaged error:") then
if line then
line = " " .. line
end
table.insert(context.lines, line)
end
end),
on_stderr = function(err, line)
if err and err > 0 then
log.error("status_async unstaged error: ", err, line)
end
end,
})
local untracked_job = Job:new({
command = "git",
args = { "-C", git_root, "ls-files", "--exclude-standard", "--others" },
enable_recording = false,
maximium_results = context.max_lines,
on_stdout = vim.schedule_wrap(function(err, line, job)
if should_process(err, line, job, "status_async untracked error:") then
if line then
line = "? " .. line
end
table.insert(context.lines, line)
end
end),
on_stderr = function(err, line)
if err and err > 0 then
log.error("status_async untracked error: ", err, line)
end
end,
})
Job:new({
command = "git",
args = {
"-C",
git_root,
"config",
"--get",
"status.showUntrackedFiles",
},
enabled_recording = true,
on_exit = function(self, _, _)
local result = self:result()
log.debug("git status.showUntrackedFiles =", result[1])
if result[1] == "no" then
unstaged_job:after(parse_lines)
Job.chain(staged_job, unstaged_job)
else
untracked_job:after(parse_lines)
Job.chain(staged_job, unstaged_job, untracked_job)
end
end,
}):start()
end, 1000, utils.debounce_strategy.CALL_FIRST_AND_LAST, utils.debounce_action.START_ASYNC_JOB)
return true
end)
end
return M

View File

@ -0,0 +1,65 @@
local Job = require("plenary.job")
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local M = {}
M.get_repository_root = function(path, callback)
local args = { "rev-parse", "--show-toplevel" }
if utils.truthy(path) then
args = { "-C", path, "rev-parse", "--show-toplevel" }
end
if type(callback) == "function" then
Job:new({
command = "git",
args = args,
enabled_recording = true,
on_exit = function(self, code, _)
if code ~= 0 then
log.trace("GIT ROOT ERROR ", self:stderr_result())
callback(nil)
return
end
local git_root = self:result()[1]
if utils.is_windows then
git_root = utils.windowize_path(git_root)
end
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
callback(git_root)
end,
}):start()
else
local ok, git_root = utils.execute_command({ "git", unpack(args) })
if not ok then
log.trace("GIT ROOT ERROR ", git_root)
return nil
end
git_root = git_root[1]
if utils.is_windows then
git_root = utils.windowize_path(git_root)
end
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
return git_root
end
end
local convert_octal_char = function(octal)
return string.char(tonumber(octal, 8))
end
M.octal_to_utf8 = function(text)
-- git uses octal encoding for utf-8 filepaths, convert octal back to utf-8
local success, converted = pcall(string.gsub, text, "\\([0-7][0-7][0-7])", convert_octal_char)
if success then
return converted
else
return text
end
end
return M

View File

@ -0,0 +1,189 @@
-- log.lua
--
-- Inspired by rxi/log.lua
-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
local vim = vim
-- User configuration section
local default_config = {
-- Name of the plugin. Prepended to log messages
plugin = "neo-tree.nvim",
-- Should print the output to neovim while running
use_console = true,
-- Should highlighting be used in console (using echohl)
highlights = true,
-- Should write to a file
use_file = false,
-- Any messages above this level will be logged.
level = "info",
-- Level configuration
modes = {
{ name = "trace", hl = "None", level = vim.log.levels.TRACE },
{ name = "debug", hl = "None", level = vim.log.levels.DEBGUG },
{ name = "info", hl = "None", level = vim.log.levels.INFO },
{ name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN },
{ name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR },
{ name = "fatal", hl = "ErrorMsg", level = vim.log.levels.ERROR },
},
-- Can limit the number of decimals displayed for floats
float_precision = 0.01,
}
-- {{{ NO NEED TO CHANGE
local log = {}
local unpack = unpack or table.unpack
local notify = function(message, level_config)
if type(vim.notify) == "table" then
-- probably using nvim-notify
vim.notify(message, level_config.level, { title = "Neo-tree" })
else
local nameupper = level_config.name:upper()
local console_string = string.format("[Neo-tree %s] %s", nameupper, message)
vim.notify(console_string, level_config.level)
end
end
log.new = function(config, standalone)
config = vim.tbl_deep_extend("force", default_config, config)
local outfile =
string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "data" }), config.plugin)
local obj
if standalone then
obj = log
else
obj = {}
end
obj.outfile = outfile
obj.use_file = function(file, quiet)
if file == false then
if not quiet then
obj.info("[neo-tree] Logging to file disabled")
end
config.use_file = false
else
if type(file) == "string" then
obj.outfile = file
else
obj.outfile = outfile
end
config.use_file = true
if not quiet then
obj.info("[neo-tree] Logging to file: " .. obj.outfile)
end
end
end
local levels = {}
for i, v in ipairs(config.modes) do
levels[v.name] = i
end
obj.set_level = function(level)
if levels[level] then
if config.level ~= level then
config.level = level
end
else
notify("Invalid log level: " .. level, config.modes[5])
end
end
local round = function(x, increment)
increment = increment or 1
x = x / increment
return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
end
local make_string = function(...)
local t = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
if type(x) == "number" and config.float_precision then
x = tostring(round(x, config.float_precision))
elseif type(x) == "table" then
x = vim.inspect(x)
if #x > 300 then
x = x:sub(1, 300) .. "..."
end
else
x = tostring(x)
end
t[#t + 1] = x
end
return table.concat(t, " ")
end
local log_at_level = function(level, level_config, message_maker, ...)
-- Return early if we're below the config.level
if level < levels[config.level] then
return
end
-- Ignnore this if vim is exiting
if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
return
end
local nameupper = level_config.name:upper()
local msg = message_maker(...)
local info = debug.getinfo(2, "Sl")
local lineinfo = info.short_src .. ":" .. info.currentline
-- Output to log file
if config.use_file then
local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
local fp = io.open(obj.outfile, "a")
if fp then
fp:write(str)
fp:close()
else
print("[neo-tree] Could not open log file: " .. obj.outfile)
end
end
-- Output to console
if config.use_console and level > 2 then
vim.schedule(function()
notify(msg, level_config)
end)
end
end
for i, x in ipairs(config.modes) do
obj[x.name] = function(...)
return log_at_level(i, x, make_string, ...)
end
obj[("fmt_%s"):format(x.name)] = function()
return log_at_level(i, x, function(...)
local passed = { ... }
local fmt = table.remove(passed, 1)
local inspected = {}
for _, v in ipairs(passed) do
table.insert(inspected, vim.inspect(v))
end
return string.format(fmt, unpack(inspected))
end)
end
end
end
log.new(default_config, true)
-- }}}
return log

View File

@ -0,0 +1,93 @@
local utils = require("neo-tree.utils")
local M = {}
local migrations = {}
M.show_migrations = function()
if #migrations > 0 then
for i, message in ipairs(migrations) do
migrations[i] = " * " .. message
end
table.insert(
migrations,
1,
"# Neo-tree configuration has been updated. Please review the changes below."
)
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, migrations)
vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(buf, "buflisted", false)
vim.api.nvim_buf_set_option(buf, "swapfile", false)
vim.api.nvim_buf_set_option(buf, "modifiable", false)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
vim.api.nvim_buf_set_name(buf, "Neo-tree migrations")
vim.defer_fn(function()
vim.cmd(string.format("%ssplit", #migrations))
vim.api.nvim_win_set_buf(0, buf)
end, 100)
end
end
M.migrate = function(config)
migrations = {}
local moved = function(old, new, converter)
local existing = utils.get_value(config, old)
if type(existing) ~= "nil" then
if type(converter) == "function" then
existing = converter(existing)
end
utils.set_value(config, new, existing)
config[old] = nil
migrations[#migrations + 1] =
string.format("The `%s` option has been deprecated, please use `%s` instead.", old, new)
end
end
local removed = function(key)
local value = utils.get_value(config, key)
if type(value) ~= "nil" then
utils.set_value(config, key, nil)
migrations[#migrations + 1] = string.format("The `%s` option has been removed.", key)
end
end
local renamed_value = function(key, old_value, new_value)
local value = utils.get_value(config, key)
if value == old_value then
utils.set_value(config, key, new_value)
migrations[#migrations + 1] =
string.format("The `%s=%s` option has been renamed to `%s`.", key, old_value, new_value)
end
end
local opposite = function(value)
return not value
end
local tab_to_source_migrator = function(labels)
local converted_sources = {}
for entry, label in pairs(labels) do
table.insert(converted_sources, { source = entry, display_name = label })
end
return converted_sources
end
moved("filesystem.filters", "filesystem.filtered_items")
moved("filesystem.filters.show_hidden", "filesystem.filtered_items.hide_dotfiles", opposite)
moved("filesystem.filters.respect_gitignore", "filesystem.filtered_items.hide_gitignored")
moved("open_files_do_not_replace_filetypes", "open_files_do_not_replace_types")
moved("source_selector.tab_labels", "source_selector.sources", tab_to_source_migrator)
removed("filesystem.filters.gitignore_source")
removed("filesystem.filter_items.gitignore_source")
renamed_value("filesystem.hijack_netrw_behavior", "open_split", "open_current")
for _, source in ipairs({ "filesystem", "buffers", "git_status" }) do
renamed_value(source .. "window.position", "split", "current")
end
return migrations
end
return M

View File

@ -0,0 +1,739 @@
local utils = require("neo-tree.utils")
local defaults = require("neo-tree.defaults")
local mapping_helper = require("neo-tree.setup.mapping-helper")
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local file_nesting = require("neo-tree.sources.common.file-nesting")
local highlights = require("neo-tree.ui.highlights")
local manager = require("neo-tree.sources.manager")
local netrw = require("neo-tree.setup.netrw")
local M = {}
local normalize_mappings = function(config)
if config == nil then
return false
end
local mappings = utils.get_value(config, "window.mappings", nil)
if mappings then
local fixed = mapping_helper.normalize_map(mappings)
config.window.mappings = fixed
return true
else
return false
end
end
local events_setup = false
local define_events = function()
if events_setup then
return
end
events.define_event(events.FS_EVENT, {
debounce_frequency = 100,
debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY,
})
local v = vim.version()
local diag_autocmd = "DiagnosticChanged"
if v.major < 1 and v.minor < 6 then
diag_autocmd = "User LspDiagnosticsChanged"
end
events.define_autocmd_event(events.VIM_DIAGNOSTIC_CHANGED, { diag_autocmd }, 500, function(args)
args.diagnostics_lookup = utils.get_diagnostic_counts()
return args
end)
local update_opened_buffers = function(args)
args.opened_buffers = utils.get_opened_buffers()
return args
end
events.define_autocmd_event(events.VIM_AFTER_SESSION_LOAD, { "SessionLoadPost" }, 200)
events.define_autocmd_event(events.VIM_BUFFER_ADDED, { "BufAdd" }, 200, update_opened_buffers)
events.define_autocmd_event(
events.VIM_BUFFER_DELETED,
{ "BufDelete" },
200,
update_opened_buffers
)
events.define_autocmd_event(events.VIM_BUFFER_ENTER, { "BufEnter", "BufWinEnter" }, 0)
events.define_autocmd_event(
events.VIM_BUFFER_MODIFIED_SET,
{ "BufModifiedSet" },
0,
update_opened_buffers
)
events.define_autocmd_event(events.VIM_COLORSCHEME, { "ColorScheme" }, 0)
events.define_autocmd_event(events.VIM_CURSOR_MOVED, { "CursorMoved" }, 100)
events.define_autocmd_event(events.VIM_DIR_CHANGED, { "DirChanged" }, 200, nil, true)
events.define_autocmd_event(events.VIM_INSERT_LEAVE, { "InsertLeave" }, 200)
events.define_autocmd_event(events.VIM_LEAVE, { "VimLeavePre" })
events.define_autocmd_event(events.VIM_RESIZED, { "VimResized" }, 100)
events.define_autocmd_event(events.VIM_TAB_CLOSED, { "TabClosed" })
events.define_autocmd_event(events.VIM_TERMINAL_ENTER, { "TermEnter" }, 0)
events.define_autocmd_event(events.VIM_TEXT_CHANGED_NORMAL, { "TextChanged" }, 200)
events.define_autocmd_event(events.VIM_WIN_CLOSED, { "WinClosed" })
events.define_autocmd_event(events.VIM_WIN_ENTER, { "WinEnter" }, 0, nil, true)
events.define_autocmd_event(events.GIT_EVENT, { "User FugitiveChanged" }, 100)
events.define_event(events.GIT_STATUS_CHANGED, { debounce_frequency = 0 })
events_setup = true
events.subscribe({
event = events.VIM_LEAVE,
handler = function()
events.clear_all_events()
end,
})
events.subscribe({
event = events.VIM_RESIZED,
handler = function()
require("neo-tree.ui.renderer").update_floating_window_layouts()
end,
})
end
local prior_window_options = {}
--- Store the current window options so we can restore them when we close the tree.
--- @param winid number | nil The window id to store the options for, defaults to current window
local store_local_window_settings = function(winid)
winid = winid or vim.api.nvim_get_current_win()
local neo_tree_settings_applied, _ =
pcall(vim.api.nvim_win_get_var, winid, "neo_tree_settings_applied")
if neo_tree_settings_applied then
-- don't store our own window settings
return
end
prior_window_options[tostring(winid)] = {
cursorline = vim.wo.cursorline,
cursorlineopt = vim.wo.cursorlineopt,
foldcolumn = vim.wo.foldcolumn,
wrap = vim.wo.wrap,
list = vim.wo.list,
spell = vim.wo.spell,
number = vim.wo.number,
relativenumber = vim.wo.relativenumber,
winhighlight = vim.wo.winhighlight,
}
end
--- Restore the window options for the current window
--- @param winid number | nil The window id to restore the options for, defaults to current window
local restore_local_window_settings = function(winid)
winid = winid or vim.api.nvim_get_current_win()
-- return local window settings to their prior values
local wo = prior_window_options[tostring(winid)]
if wo then
vim.wo.cursorline = wo.cursorline
vim.wo.cursorlineopt = wo.cursorlineopt
vim.wo.foldcolumn = wo.foldcolumn
vim.wo.wrap = wo.wrap
vim.wo.list = wo.list
vim.wo.spell = wo.spell
vim.wo.number = wo.number
vim.wo.relativenumber = wo.relativenumber
vim.wo.winhighlight = wo.winhighlight
log.debug("Window settings restored")
vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", false)
else
log.debug("No window settings to restore")
end
end
local last_buffer_enter_filetype = nil
M.buffer_enter_event = function()
-- if it is a neo-tree window, just set local options
if vim.bo.filetype == "neo-tree" then
if last_buffer_enter_filetype == "neo-tree" then
-- we've switched to another neo-tree window
events.fire_event(events.NEO_TREE_BUFFER_LEAVE)
else
store_local_window_settings()
end
vim.cmd([[
setlocal cursorline
setlocal cursorlineopt=line
setlocal nowrap
setlocal nolist nospell nonumber norelativenumber
]])
local winhighlight =
"Normal:NeoTreeNormal,NormalNC:NeoTreeNormalNC,SignColumn:NeoTreeSignColumn,CursorLine:NeoTreeCursorLine,FloatBorder:NeoTreeFloatBorder,StatusLine:NeoTreeStatusLine,StatusLineNC:NeoTreeStatusLineNC,VertSplit:NeoTreeVertSplit,EndOfBuffer:NeoTreeEndOfBuffer"
if vim.version().minor >= 7 then
vim.cmd("setlocal winhighlight=" .. winhighlight .. ",WinSeparator:NeoTreeWinSeparator")
else
vim.cmd("setlocal winhighlight=" .. winhighlight)
end
events.fire_event(events.NEO_TREE_BUFFER_ENTER)
last_buffer_enter_filetype = vim.bo.filetype
vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", true)
return
end
if vim.bo.filetype == "neo-tree-popup" then
vim.cmd([[
setlocal winhighlight=Normal:NeoTreeFloatNormal,FloatBorder:NeoTreeFloatBorder
setlocal nolist nospell nonumber norelativenumber
]])
events.fire_event(events.NEO_TREE_POPUP_BUFFER_ENTER)
last_buffer_enter_filetype = vim.bo.filetype
return
end
if last_buffer_enter_filetype == "neo-tree" then
events.fire_event(events.NEO_TREE_BUFFER_LEAVE)
end
if last_buffer_enter_filetype == "neo-tree-popup" then
events.fire_event(events.NEO_TREE_POPUP_BUFFER_LEAVE)
end
last_buffer_enter_filetype = vim.bo.filetype
-- there is nothing more we want to do with floating windows
if utils.is_floating() then
return
end
-- if vim is trying to open a dir, then we hijack it
if netrw.hijack() then
return
end
-- For all others, make sure another buffer is not hijacking our window
-- ..but not if the position is "current"
local prior_buf = vim.fn.bufnr("#")
if prior_buf < 1 then
return
end
local winid = vim.api.nvim_get_current_win()
local prior_type = vim.api.nvim_buf_get_option(prior_buf, "filetype")
if prior_type == "neo-tree" then
local success, position = pcall(vim.api.nvim_buf_get_var, prior_buf, "neo_tree_position")
if not success then
-- just bail out now, the rest of these lookups will probably fail too.
return
end
if position == "current" then
-- nothing to do here, files are supposed to open in same window
return
end
local current_tabid = vim.api.nvim_get_current_tabpage()
local neo_tree_tabid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_tabid")
if neo_tree_tabid ~= current_tabid then
-- This a new tab, so the alternate being neo-tree doesn't matter.
return
end
local neo_tree_winid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_winid")
local current_winid = vim.api.nvim_get_current_win()
if neo_tree_winid ~= current_winid then
-- This is not the neo-tree window, so the alternate being neo-tree doesn't matter.
return
end
local bufname = vim.api.nvim_buf_get_name(0)
log.debug("redirecting buffer " .. bufname .. " to new split")
vim.cmd("b#")
-- Using schedule at this point fixes problem with syntax
-- highlighting in the buffer. I also prevents errors with diagnostics
-- trying to work with the buffer as it's being closed.
vim.schedule(function()
-- try to delete the buffer, only because if it was new it would take
-- on options from the neo-tree window that are undesirable.
pcall(vim.cmd, "bdelete " .. bufname)
local fake_state = {
window = {
position = position,
},
}
utils.open_file(fake_state, bufname)
end)
end
end
M.win_enter_event = function()
local win_id = vim.api.nvim_get_current_win()
if utils.is_floating(win_id) then
return
end
-- if the new win is not a floating window, make sure all neo-tree floats are closed
manager.close_all("float")
if M.config.close_if_last_window then
local tabid = vim.api.nvim_get_current_tabpage()
local wins = utils.get_value(M, "config.prior_windows", {})[tabid]
local prior_exists = utils.truthy(wins)
local non_floating_wins = vim.tbl_filter(function(win)
return not utils.is_floating(win)
end, vim.api.nvim_tabpage_list_wins(tabid))
local win_count = #non_floating_wins
log.trace("checking if last window")
log.trace("prior window exists = ", prior_exists)
log.trace("win_count: ", win_count)
if prior_exists and win_count == 1 and vim.o.filetype == "neo-tree" then
local position = vim.api.nvim_buf_get_var(0, "neo_tree_position")
local source = vim.api.nvim_buf_get_var(0, "neo_tree_source")
if position ~= "current" then
-- close_if_last_window just doesn't make sense for a split style
log.trace("last window, closing")
local state = require("neo-tree.sources.manager").get_state(source)
if state == nil then
return
end
local mod = utils.get_opened_buffers()
log.debug("close_if_last_window, modified files found: ", vim.inspect(mod))
for filename, buf_info in pairs(mod) do
if buf_info.modified then
local buf_name, message
if vim.startswith(filename, "[No Name]#") then
buf_name = string.sub(filename, 11)
message = "Cannot close because an unnamed buffer is modified. Please save or discard this file."
else
buf_name = filename
message = "Cannot close because one of the files is modified. Please save or discard changes."
end
log.trace("close_if_last_window, showing unnamed modified buffer: ", filename)
vim.schedule(function()
log.warn(message)
vim.cmd("rightbelow vertical split")
vim.api.nvim_win_set_width(win_id, state.window.width or 40)
vim.cmd("b" .. buf_name)
end)
return
end
end
vim.cmd("q!")
return
end
end
end
if vim.o.filetype == "neo-tree" then
local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position")
if position == "current" then
-- make sure the buffer wasn't moved to a new window
local neo_tree_winid = vim.api.nvim_buf_get_var(0, "neo_tree_winid")
local current_winid = vim.api.nvim_get_current_win()
local current_bufnr = vim.api.nvim_get_current_buf()
if neo_tree_winid ~= current_winid then
-- At this point we know that either the neo-tree window was split,
-- or the neo-tree buffer is being shown in another window for some other reason.
-- Sometime the split is just the first step in the process of opening somethig else,
-- so instead of fixing this right away, we add a short delay and check back again to see
-- if the buffer is still in this window.
local old_state = manager.get_state("filesystem", nil, neo_tree_winid)
vim.schedule(function()
local bufnr = vim.api.nvim_get_current_buf()
if bufnr ~= current_bufnr then
-- The neo-tree buffer was replaced with something else, so we don't need to do anything.
log.trace("neo-tree buffer replaced with something else - no further action required")
return
end
-- create a new tree for this window
local state = manager.get_state("filesystem", nil, current_winid)
state.path = old_state.path
state.current_position = "current"
local renderer = require("neo-tree.ui.renderer")
state.force_open_folders = renderer.get_expanded_nodes(old_state.tree)
require("neo-tree.sources.filesystem")._navigate_internal(state, nil, nil, nil, false)
end)
return
end
end
-- it's a neo-tree window, ignore
return
end
M.config.prior_windows = M.config.prior_windows or {}
local tabid = vim.api.nvim_get_current_tabpage()
local tab_windows = M.config.prior_windows[tabid]
if tab_windows == nil then
tab_windows = {}
M.config.prior_windows[tabid] = tab_windows
end
table.insert(tab_windows, win_id)
-- prune the history when it gets too big
if #tab_windows > 100 then
local new_array = {}
local win_count = #tab_windows
for i = 80, win_count do
table.insert(new_array, tab_windows[i])
end
M.config.prior_windows[tabid] = new_array
end
end
M.set_log_level = function(level)
log.set_level(level)
end
local function merge_global_components_config(components, config)
local indent_exists = false
local merged_components = {}
local do_merge
do_merge = function(component)
local name = component[1]
if type(name) == "string" then
if name == "indent" then
indent_exists = true
end
local merged = { name }
local global_config = config.default_component_configs[name]
if global_config then
for k, v in pairs(global_config) do
merged[k] = v
end
end
for k, v in pairs(component) do
merged[k] = v
end
if name == "container" then
for i, child in ipairs(component.content) do
merged.content[i] = do_merge(child)
end
end
return merged
else
log.error("component name is the wrong type", component)
end
end
for _, component in ipairs(components) do
local merged = do_merge(component)
table.insert(merged_components, merged)
end
-- If the indent component is not specified, then add it.
-- We do this because it used to be implicitly added, so we don't want to
-- break any existing configs.
if not indent_exists then
local indent = { "indent" }
for k, v in pairs(config.default_component_configs.indent or {}) do
indent[k] = v
end
table.insert(merged_components, 1, indent)
end
return merged_components
end
local merge_renderers = function(default_config, source_default_config, user_config)
-- This can't be a deep copy/merge. If a renderer is specified in the target it completely
-- replaces the base renderer.
if source_default_config == nil then
-- first override the default config global renderer with the user's global renderers
for name, renderer in pairs(user_config.renderers or {}) do
log.debug("overriding global renderer for " .. name)
default_config.renderers[name] = renderer
end
else
-- then override the global renderers with the source specific renderers
source_default_config.renderers = source_default_config.renderers or {}
for name, renderer in pairs(default_config.renderers or {}) do
if source_default_config.renderers[name] == nil then
log.debug("overriding source renderer for " .. name)
local r = {}
-- Only copy components that exist in the target source.
-- This alllows us to specify global renderers that include components from all sources,
-- even if some of those components are not universal
for _, value in ipairs(renderer) do
if value[1] and source_default_config.components[value[1]] ~= nil then
table.insert(r, value)
end
end
source_default_config.renderers[name] = r
end
end
-- if user sets renderers, completely wipe the default ones
local source_name = source_default_config.name
for name, _ in pairs(source_default_config.renderers) do
local user = utils.get_value(user_config, source_name .. ".renderers." .. name)
if user then
source_default_config.renderers[name] = nil
end
end
end
end
M.merge_config = function(user_config, is_auto_config)
local default_config = vim.deepcopy(defaults)
user_config = vim.deepcopy(user_config or {})
local migrations = require("neo-tree.setup.deprecations").migrate(user_config)
if #migrations > 0 then
-- defer to make sure it is the last message printed
vim.defer_fn(function()
vim.cmd(
"echohl WarningMsg | echo 'Some options have changed, please run `:Neotree migrations` to see the changes' | echohl NONE"
)
end, 50)
end
if user_config.log_level ~= nil then
M.set_log_level(user_config.log_level)
end
log.use_file(user_config.log_to_file, true)
log.debug("setup")
events.clear_all_events()
define_events()
-- Prevent accidentally opening another file in the neo-tree window.
events.subscribe({
event = events.VIM_BUFFER_ENTER,
handler = M.buffer_enter_event,
})
-- Setup autocmd for neo-tree BufLeave, to restore window settings.
-- This is set to happen just before leaving the window.
-- The patterns used should ensure it only runs in neo-tree windows where position = "current"
local augroup = vim.api.nvim_create_augroup("NeoTree_BufLeave", { clear = true })
local bufleave = function(data)
-- Vim patterns in autocmds are not quite precise enough
-- so we are doing a second stage filter in lua
local pattern = "neo%-tree [^ ]+ %[1%d%d%d%]"
if string.match(data.file, pattern) then
restore_local_window_settings()
end
end
vim.api.nvim_create_autocmd({ "BufWinLeave" }, {
group = augroup,
pattern = "neo-tree *",
callback = bufleave,
})
if user_config.event_handlers ~= nil then
for _, handler in ipairs(user_config.event_handlers) do
events.subscribe(handler)
end
end
highlights.setup()
-- used to either limit the sources that or loaded, or add extra external sources
local all_sources = {}
local all_source_names = {}
for _, source in ipairs(user_config.sources or default_config.sources) do
local parts = utils.split(source, ".")
local name = parts[#parts]
local is_internal_ns, is_external_ns = false, false
local module
if #parts == 1 then
-- might be a module name in the internal namespace
is_internal_ns, module = pcall(require, "neo-tree.sources." .. source)
end
if is_internal_ns then
name = module.name or name
all_sources[name] = "neo-tree.sources." .. name
else
-- fully qualified module name
-- or just a root level module name
is_external_ns, module = pcall(require, source)
if is_external_ns then
name = module.name or name
all_sources[name] = source
else
log.error("Source module not found", source)
name = nil
end
end
if name then
default_config[name] = module.default_config or default_config[name]
table.insert(all_source_names, name)
end
end
log.debug("Sources to load: ", vim.inspect(all_sources))
require("neo-tree.command.parser").setup(all_source_names)
-- setup the default values for all sources
normalize_mappings(default_config)
normalize_mappings(user_config)
merge_renderers(default_config, nil, user_config)
for source_name, mod_root in pairs(all_sources) do
local module = require(mod_root)
default_config[source_name] = default_config[source_name]
or {
renderers = {},
components = {},
}
local source_default_config = default_config[source_name]
source_default_config.components = module.components or require(mod_root .. ".components")
source_default_config.commands = module.commands or require(mod_root .. ".commands")
source_default_config.name = source_name
source_default_config.display_name = module.display_name or source_default_config.name
if user_config.use_default_mappings == false then
default_config.window.mappings = {}
source_default_config.window.mappings = {}
end
-- Make sure all the mappings are normalized so they will merge properly.
normalize_mappings(source_default_config)
normalize_mappings(user_config[source_name])
-- merge the global config with the source specific config
source_default_config.window = vim.tbl_deep_extend(
"force",
default_config.window or {},
source_default_config.window or {},
user_config.window or {}
)
merge_renderers(default_config, source_default_config, user_config)
--validate the window.position
local pos_key = source_name .. ".window.position"
local position = utils.get_value(user_config, pos_key, "left", true)
local valid_positions = {
left = true,
right = true,
top = true,
bottom = true,
float = true,
current = true,
}
if not valid_positions[position] then
log.error("Invalid value for ", pos_key, ": ", position)
user_config[source_name].window.position = "left"
end
end
--print(vim.inspect(default_config.filesystem))
-- Moving user_config.sources to user_config.orig_sources
user_config.orig_sources = user_config.sources and user_config.sources or {}
-- apply the users config
M.config = vim.tbl_deep_extend("force", default_config, user_config)
-- RE: 873, fixes issue with invalid source checking by overriding
-- source table with name table
-- Setting new "sources" to be the parsed names of the sources
M.config.sources = all_source_names
if ( M.config.source_selector.winbar or M.config.source_selector.statusline )
and M.config.source_selector.sources
and not user_config.default_source then
-- Set the default source to the head of these
-- This resolves some weirdness with the source selector having
-- a different "head" item than our current default.
-- Removing this line makes Neo-tree show the "filesystem"
-- source instead of whatever the first item in the config is.
-- Probably don't remove this unless you have a better fix for that
M.config.default_source = M.config.source_selector.sources[1].source
end
-- Check if the default source is not included in config.sources
-- log a warning and then "pick" the first in the sources list
local match = false
for _, source in ipairs(M.config.sources) do
if source == M.config.default_source then
match = true
break
end
end
if not match then
M.config.default_source = M.config.sources[1]
log.warn(string.format("Invalid default source found in configuration. Using first available source: %s", M.config.default_source))
end
if not M.config.enable_git_status then
M.config.git_status_async = false
end
-- Validate that the source_selector.sources are all available and if any
-- aren't, remove them
local source_selector_sources = {}
for _, ss_source in ipairs(M.config.source_selector.sources or {}) do
local source_match = false
for _, source in ipairs(M.config.sources) do
if ss_source.source == source then
source_match = true
break
end
end
if source_match then
table.insert(source_selector_sources, ss_source)
else
log.debug(string.format("Unable to locate Neo-tree extension %s", ss_source.source))
end
end
M.config.source_selector.sources = source_selector_sources
file_nesting.setup(M.config.nesting_rules)
for source_name, mod_root in pairs(all_sources) do
for name, rndr in pairs(M.config[source_name].renderers) do
M.config[source_name].renderers[name] = merge_global_components_config(rndr, M.config)
end
local module = require(mod_root)
if M.config.commands then
M.config[source_name].commands =
vim.tbl_extend("keep", M.config[source_name].commands or {}, M.config.commands)
end
manager.setup(source_name, M.config[source_name], M.config, module)
manager.redraw(source_name)
end
if M.config.auto_clean_after_session_restore then
require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(false)
events.subscribe({
event = events.VIM_AFTER_SESSION_LOAD,
handler = function()
require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(true)
end,
})
end
events.subscribe({
event = events.VIM_COLORSCHEME,
handler = highlights.setup,
id = "neo-tree-highlight",
})
events.subscribe({
event = events.VIM_WIN_ENTER,
handler = M.win_enter_event,
id = "neo-tree-win-enter",
})
--Dispose ourselves if the tab closes
events.subscribe({
event = events.VIM_TAB_CLOSED,
handler = function(args)
local tabnr = tonumber(args.afile)
log.debug("VIM_TAB_CLOSED: disposing state for tabnr", tabnr)
-- Internally we use tabids to track state but <afile> is tabnr of a tab that has already been
-- closed so there is no way to get its tabid. Instead dispose all tabs that are no longer valid.
-- Must be scheduled because nvim_tabpage_is_valid does not work inside TabClosed event callback.
vim.schedule_wrap(manager.dispose_invalid_tabs)()
end,
})
--Dispose ourselves if the tab closes
events.subscribe({
event = events.VIM_WIN_CLOSED,
handler = function(args)
local winid = tonumber(args.afile)
log.debug("VIM_WIN_CLOSED: disposing state for window", winid)
manager.dispose_window(winid)
end,
})
local rt = utils.get_value(M.config, "resize_timer_interval", 50, true)
require("neo-tree.ui.renderer").resize_timer_interval = rt
return M.config
end
return M

View File

@ -0,0 +1,62 @@
local utils = require("neo-tree.utils")
local M = {}
M.normalize_map_key = function(key)
if key == nil then
return nil
end
if key:match("^<[^>]+>$") then
local parts = utils.split(key, "-")
if #parts == 2 then
local mod = parts[1]:lower()
if mod == "<a" then
mod = "<m"
end
local alpha = parts[2]
if #alpha > 2 then
alpha = alpha:lower()
end
key = string.format("%s-%s", mod, alpha)
return key
else
key = key:lower()
if key == "<backspace>" then
return "<bs>"
elseif key == "<enter>" then
return "<cr>"
elseif key == "<return>" then
return "<cr>"
end
end
end
return key
end
M.normalize_map = function(map)
local new_map = {}
for key, value in pairs(map) do
local normalized_key = M.normalize_map_key(key)
if normalized_key ~= nil then
new_map[normalized_key] = value
end
end
return new_map
end
local tests = {
{ "<BS>", "<bs>" },
{ "<Backspace>", "<bs>" },
{ "<Enter>", "<cr>" },
{ "<C-W>", "<c-W>" },
{ "<A-q>", "<m-q>" },
{ "<C-Left>", "<c-left>" },
{ "<C-Right>", "<c-right>" },
{ "<C-Up>", "<c-up>" },
}
for _, test in ipairs(tests) do
local key = M.normalize_map_key(test[1])
assert(key == test[2], string.format("%s != %s", key, test[2]))
end
return M

View File

@ -0,0 +1,103 @@
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local command = require("neo-tree.command")
local M = {}
local get_position = function(source_name)
local nt = require("neo-tree")
local pos = utils.get_value(nt.config, source_name .. ".window.position", "left", true)
return pos
end
M.get_hijack_netrw_behavior = function()
local nt = require("neo-tree")
local option = "filesystem.hijack_netrw_behavior"
local hijack_behavior = utils.get_value(nt.config, option, "open_default", true)
if hijack_behavior == "disabled" then
return hijack_behavior
elseif hijack_behavior == "open_default" then
return hijack_behavior
elseif hijack_behavior == "open_current" then
return hijack_behavior
else
log.error("Invalid value for " .. option .. ": " .. hijack_behavior)
return "disabled"
end
end
M.hijack = function()
local hijack_behavior = M.get_hijack_netrw_behavior()
if hijack_behavior == "disabled" then
return false
end
-- ensure this is a directory
local bufname = vim.api.nvim_buf_get_name(0)
local stats = vim.loop.fs_stat(bufname)
if not stats then
return false
end
if stats.type ~= "directory" then
return false
end
-- record where we are now
local pos = get_position("filesystem")
local should_open_current = hijack_behavior == "open_current" or pos == "current"
local winid = vim.api.nvim_get_current_win()
local dir_bufnr = vim.api.nvim_get_current_buf()
-- Now actually open the tree, with a very quick debounce because this may be
-- called multiple times in quick succession.
utils.debounce("hijack_netrw_" .. winid, function()
-- We will want to replace the "directory" buffer with either the "alternate"
-- buffer or a new blank one.
local replace_with_bufnr = vim.fn.bufnr("#")
local is_currently_neo_tree = false
if replace_with_bufnr > 0 then
if vim.api.nvim_buf_get_option(replace_with_bufnr, "filetype") == "neo-tree" then
-- don't hijack the current window if it's already a Neo-tree sidebar
local _, position = pcall(vim.api.nvim_buf_get_var, replace_with_bufnr, "neo_tree_position")
if position ~= "current" then
is_currently_neo_tree = true
else
replace_with_bufnr = -1
end
end
end
if not should_open_current then
if replace_with_bufnr == dir_bufnr or replace_with_bufnr < 1 then
replace_with_bufnr = vim.api.nvim_create_buf(true, false)
log.trace("Created new buffer for netrw hijack", replace_with_bufnr)
end
end
if replace_with_bufnr > 0 then
log.trace("Replacing buffer in netrw hijack", replace_with_bufnr)
pcall(vim.api.nvim_win_set_buf, winid, replace_with_bufnr)
end
local remove_dir_buf = vim.schedule_wrap(function()
log.trace("Deleting buffer in netrw hijack", dir_bufnr)
pcall(vim.api.nvim_buf_delete, dir_bufnr, { force = true })
end)
local state
if should_open_current and not is_currently_neo_tree then
log.debug("hijack_netrw: opening current")
state = manager.get_state("filesystem", nil, winid)
state.current_position = "current"
elseif is_currently_neo_tree then
log.debug("hijack_netrw: opening in existing Neo-tree")
state = manager.get_state("filesystem")
else
log.debug("hijack_netrw: opening default")
manager.close_all_except("filesystem")
state = manager.get_state("filesystem")
end
require("neo-tree.sources.filesystem")._navigate_internal(state, bufname, nil, remove_dir_buf)
end, 10, utils.debounce_strategy.CALL_LAST_ONLY)
return true
end
return M

View File

@ -0,0 +1,92 @@
--This file should contain all commands meant to be used by mappings.
local vim = vim
local cc = require("neo-tree.sources.common.commands")
local buffers = require("neo-tree.sources.buffers")
local utils = require("neo-tree.utils")
local manager = require("neo-tree.sources.manager")
local M = {}
local refresh = utils.wrap(manager.refresh, "buffers")
local redraw = utils.wrap(manager.redraw, "buffers")
M.add = function(state)
cc.add(state, refresh)
end
M.add_directory = function(state)
cc.add_directory(state, refresh)
end
M.buffer_delete = function(state)
local node = state.tree:get_node()
if node then
if node.type == "message" then
return
end
vim.api.nvim_buf_delete(node.extra.bufnr, { force = false, unload = false })
refresh()
end
end
---Marks node as copied, so that it can be pasted somewhere else.
M.copy_to_clipboard = function(state)
cc.copy_to_clipboard(state, redraw)
end
M.copy_to_clipboard_visual = function(state, selected_nodes)
cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
end
---Marks node as cut, so that it can be pasted (moved) somewhere else.
M.cut_to_clipboard = function(state)
cc.cut_to_clipboard(state, redraw)
end
M.cut_to_clipboard_visual = function(state, selected_nodes)
cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
end
M.copy = function(state)
cc.copy(state, redraw)
end
M.move = function(state)
cc.move(state, redraw)
end
M.show_debug_info = cc.show_debug_info
---Pastes all items from the clipboard to the current directory.
M.paste_from_clipboard = function(state)
cc.paste_from_clipboard(state, refresh)
end
M.delete = function(state)
cc.delete(state, refresh)
end
---Navigate up one level.
M.navigate_up = function(state)
local parent_path, _ = utils.split_path(state.path)
buffers.navigate(state, parent_path)
end
M.refresh = refresh
M.rename = function(state)
cc.rename(state, refresh)
end
M.set_root = function(state)
local tree = state.tree
local node = tree:get_node()
if node.type == "directory" then
buffers.navigate(state, node.id)
end
end
cc._add_common_commands(M)
return M

View File

@ -0,0 +1,48 @@
-- This file contains the built-in components. Each componment is a function
-- that takes the following arguments:
-- config: A table containing the configuration provided by the user
-- when declaring this component in their renderer config.
-- node: A NuiNode object for the currently focused node.
-- state: The current state of the source providing the items.
--
-- The function should return either a table, or a list of tables, each of which
-- contains the following keys:
-- text: The text to display for this item.
-- highlight: The highlight group to apply to this text.
local highlights = require("neo-tree.ui.highlights")
local common = require("neo-tree.sources.common.components")
local utils = require("neo-tree.utils")
local M = {}
M.name = function(config, node, state)
local highlight = config.highlight or highlights.FILE_NAME_OPENED
local name = node.name
if node.type == "directory" then
if node:get_depth() == 1 then
highlight = highlights.ROOT_NAME
name = "OPEN BUFFERS in " .. name
else
highlight = highlights.DIRECTORY_NAME
end
elseif node.type == "terminal" then
if node:get_depth() == 1 then
highlight = highlights.ROOT_NAME
name = "TERMINALS"
else
highlight = highlights.FILE_NAME
end
elseif config.use_git_status_colors then
local git_status = state.components.git_status({}, node, state)
if git_status and git_status.highlight then
highlight = git_status.highlight
end
end
return {
text = name,
highlight = highlight,
}
end
return vim.tbl_deep_extend("force", common, M)

View File

@ -0,0 +1,191 @@
--This file should have all functions that are in the public api and either set
--or read the state of this source.
local vim = vim
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local items = require("neo-tree.sources.buffers.lib.items")
local events = require("neo-tree.events")
local manager = require("neo-tree.sources.manager")
local git = require("neo-tree.git")
local M = {
name = "buffers",
display_name = "  Buffers "
}
local wrap = function(func)
return utils.wrap(func, M.name)
end
local get_state = function()
return manager.get_state(M.name)
end
local follow_internal = function()
if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then
return
end
local bufnr = vim.api.nvim_get_current_buf()
local path_to_reveal = manager.get_path_to_reveal(true) or tostring(bufnr)
local state = get_state()
if state.current_position == "float" then
return false
end
if not state.path then
return false
end
local window_exists = renderer.window_exists(state)
if window_exists then
local node = state.tree and state.tree:get_node()
if node then
if node:get_id() == path_to_reveal then
-- already focused
return false
end
end
renderer.focus_node(state, path_to_reveal, true)
end
end
M.follow = function()
if vim.fn.bufname(0) == "COMMIT_EDITMSG" then
return false
end
utils.debounce("neo-tree-buffer-follow", function()
return follow_internal()
end, 100, utils.debounce_strategy.CALL_LAST_ONLY)
end
local buffers_changed_internal = function()
for _, tabid in ipairs(vim.api.nvim_list_tabpages()) do
local state = manager.get_state(M.name, tabid)
if state.path and renderer.window_exists(state) then
items.get_opened_buffers(state)
if state.follow_current_file then
follow_internal()
end
end
end
end
---Calld by autocmd when any buffer is open, closed, renamed, etc.
M.buffers_changed = function()
utils.debounce(
"buffers_changed",
buffers_changed_internal,
100,
utils.debounce_strategy.CALL_LAST_ONLY
)
end
---Navigate to the given path.
---@param path string Path to navigate to. If empty, will navigate to the cwd.
M.navigate = function(state, path, path_to_reveal)
state.dirty = false
local path_changed = false
if path == nil then
path = vim.fn.getcwd()
end
if path ~= state.path then
state.path = path
path_changed = true
end
if path_to_reveal then
renderer.position.set(state, path_to_reveal)
end
items.get_opened_buffers(state)
if path_changed and state.bind_to_cwd then
vim.api.nvim_command("tcd " .. path)
end
end
---Configures the plugin, should be called before the plugin is used.
---@param config table Configuration table containing any keys that the user
--wants to change from the defaults. May be empty to accept default values.
M.setup = function(config, global_config)
--Configure events for before_render
if config.before_render then
--convert to new event system
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
config.before_render(this_state)
end
end,
})
elseif global_config.enable_git_status then
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
state.git_status_lookup = git.status(state.git_base)
end
end,
})
manager.subscribe(M.name, {
event = events.GIT_EVENT,
handler = M.buffers_changed,
})
end
local refresh_events = {
events.VIM_BUFFER_ADDED,
events.VIM_BUFFER_DELETED,
}
if global_config.enable_refresh_on_write then
table.insert(refresh_events, events.VIM_BUFFER_CHANGED)
end
for _, e in ipairs(refresh_events) do
manager.subscribe(M.name, {
event = e,
handler = function(args)
if args.afile == "" or utils.is_real_file(args.afile) then
M.buffers_changed()
end
end,
})
end
if config.bind_to_cwd then
manager.subscribe(M.name, {
event = events.VIM_DIR_CHANGED,
handler = wrap(manager.dir_changed),
})
end
if global_config.enable_diagnostics then
manager.subscribe(M.name, {
event = events.VIM_DIAGNOSTIC_CHANGED,
handler = wrap(manager.diagnostics_changed),
})
end
--Configure event handlers for modified files
if global_config.enable_modified_markers then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_MODIFIED_SET,
handler = wrap(manager.opened_buffers_changed),
})
end
-- Configure event handler for follow_current_file option
if config.follow_current_file then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_ENTER,
handler = M.follow,
})
manager.subscribe(M.name, {
event = events.VIM_TERMINAL_ENTER,
handler = M.follow,
})
end
end
return M

View File

@ -0,0 +1,108 @@
local vim = vim
local renderer = require("neo-tree.ui.renderer")
local utils = require("neo-tree.utils")
local file_items = require("neo-tree.sources.common.file-items")
local log = require("neo-tree.log")
local M = {}
---Get a table of all open buffers, along with all parent paths of those buffers.
---The paths are the keys of the table, and all the values are 'true'.
M.get_opened_buffers = function(state)
if state.loading then
return
end
state.loading = true
local context = file_items.create_context()
context.state = state
-- Create root folder
local root = file_items.create_item(context, state.path, "directory")
root.name = vim.fn.fnamemodify(root.path, ":~")
root.loaded = true
root.search_pattern = state.search_pattern
context.folders[root.path] = root
local terminals = {}
local function add_buffer(bufnr, path)
local is_loaded = vim.api.nvim_buf_is_loaded(bufnr)
if is_loaded or state.show_unloaded then
local is_listed = vim.fn.buflisted(bufnr)
if is_listed == 1 then
if path == "" then
path = "[No Name]"
end
local success, item = pcall(file_items.create_item, context, path, "file", bufnr)
if success then
item.extra = {
bufnr = bufnr,
is_listed = is_listed,
}
else
log.error("Error creating item for " .. path .. ": " .. item)
end
end
end
end
local bufs = vim.api.nvim_list_bufs()
for _, b in ipairs(bufs) do
local path = vim.api.nvim_buf_get_name(b)
if vim.startswith(path, "term://") then
local name = path:match("term://(.*)//.*")
local abs_path = vim.fn.fnamemodify(name, ":p")
local has_title, title = pcall(vim.api.nvim_buf_get_var, b, "term_title")
local item = {
name = has_title and title or name,
ext = "terminal",
path = abs_path,
id = path,
type = "terminal",
loaded = true,
extra = {
bufnr = b,
is_listed = true,
},
}
if utils.is_subpath(state.path, abs_path) then
table.insert(terminals, item)
end
elseif path == "" then
add_buffer(b, path)
else
if #state.path > 1 then
local rootsub = path:sub(1, #state.path)
-- make sure this is within the root path
if rootsub == state.path then
add_buffer(b, path)
end
else
add_buffer(b, path)
end
end
end
local root_folders = { root }
if #terminals > 0 then
local terminal_root = {
name = "Terminals",
id = "Terminals",
ext = "terminal",
type = "terminal",
children = terminals,
loaded = true,
search_pattern = state.search_pattern,
}
context.folders["Terminals"] = terminal_root
root_folders[2] = terminal_root
end
state.default_expanded_nodes = {}
for id, _ in pairs(context.folders) do
table.insert(state.default_expanded_nodes, id)
end
file_items.deep_sort(root.children)
renderer.show_nodes(root_folders, state)
state.loading = false
end
return M

View File

@ -0,0 +1,748 @@
--This file should contain all commands meant to be used by mappings.
local vim = vim
local fs_actions = require("neo-tree.sources.filesystem.lib.fs_actions")
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local events = require("neo-tree.events")
local inputs = require("neo-tree.ui.inputs")
local popups = require("neo-tree.ui.popups")
local log = require("neo-tree.log")
local help = require("neo-tree.sources.common.help")
local Preview = require("neo-tree.sources.common.preview")
---Gets the node parent folder
---@param state table to look for nodes
---@return table? node
local function get_folder_node(state)
local tree = state.tree
local node = tree:get_node()
local last_id = node:get_id()
while node do
local insert_as_local = state.config.insert_as
local insert_as_global = require("neo-tree").config.window.insert_as
local use_parent
if insert_as_local then
use_parent = insert_as_local == "sibling"
else
use_parent = insert_as_global == "sibling"
end
local is_open_dir = node.type == "directory" and (node:is_expanded() or node.empty_expanded)
if use_parent and not is_open_dir then
return tree:get_node(node:get_parent_id())
end
if node.type == "directory" then
return node
end
local parent_id = node:get_parent_id()
if not parent_id or parent_id == last_id then
return node
else
last_id = parent_id
node = tree:get_node(parent_id)
end
end
end
---The using_root_directory is used to decide what part of the filename to show
-- the user when asking for a new filename to e.g. create, copy to or move to.
---@param state table The state of the source
---@return string The root path from which the relative source path should be taken
local function get_using_root_directory(state)
-- default to showing only the basename of the path
local using_root_directory = get_folder_node(state):get_id()
local show_path = state.config.show_path
if show_path == "absolute" then
using_root_directory = ""
elseif show_path == "relative" then
using_root_directory = state.path
elseif show_path ~= nil and show_path ~= "none" then
log.warn(
'A neo-tree mapping was setup with a config.show_path option with invalid value: "'
.. show_path
.. '", falling back to its default: nil/"none"'
)
end
return using_root_directory
end
local M = {}
---Adds all missing common commands to the given module
---@param to_source_command_module table The commands module for a source
---@param pattern string? A pattern specifying which commands to add, nil to add all
M._add_common_commands = function(to_source_command_module, pattern)
for name, func in pairs(M) do
if
type(name) == "string"
and not to_source_command_module[name]
and (not pattern or name:find(pattern))
and not name:find("^_")
then
to_source_command_module[name] = func
end
end
end
---Add a new file or dir at the current node
---@param state table The state of the source
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
M.add = function(state, callback)
local node = get_folder_node(state)
local in_directory = node:get_id()
local using_root_directory = get_using_root_directory(state)
fs_actions.create_node(in_directory, callback, using_root_directory)
end
---Add a new file or dir at the current node
---@param state table The state of the source
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
M.add_directory = function(state, callback)
local node = get_folder_node(state)
local in_directory = node:get_id()
local using_root_directory = get_using_root_directory(state)
fs_actions.create_directory(in_directory, callback, using_root_directory)
end
M.expand_all_nodes = function(state, toggle_directory)
if toggle_directory == nil then
toggle_directory = function(_, node)
node:expand()
end
end
--state.explicitly_opened_directories = state.explicitly_opened_directories or {}
local expand_node
expand_node = function(node)
local id = node:get_id()
if node.type == "directory" and not node:is_expanded() then
toggle_directory(state, node)
node = state.tree:get_node(id)
end
local children = state.tree:get_nodes(id)
if children then
for _, child in ipairs(children) do
if child.type == "directory" then
expand_node(child)
end
end
end
end
for _, node in ipairs(state.tree:get_nodes()) do
expand_node(node)
end
renderer.redraw(state)
end
M.close_node = function(state, callback)
local tree = state.tree
local node = tree:get_node()
local parent_node = tree:get_node(node:get_parent_id())
local target_node
if node:has_children() and node:is_expanded() then
target_node = node
else
target_node = parent_node
end
local root = tree:get_nodes()[1]
local is_root = target_node:get_id() == root:get_id()
if target_node and target_node:has_children() and not is_root then
target_node:collapse()
renderer.redraw(state)
renderer.focus_node(state, target_node:get_id())
end
end
M.close_all_subnodes = function(state)
local tree = state.tree
local node = tree:get_node()
local parent_node = tree:get_node(node:get_parent_id())
local target_node
if node:has_children() and node:is_expanded() then
target_node = node
else
target_node = parent_node
end
renderer.collapse_all_nodes(tree, target_node:get_id())
renderer.redraw(state)
renderer.focus_node(state, target_node:get_id())
end
M.close_all_nodes = function(state)
renderer.collapse_all_nodes(state.tree)
renderer.redraw(state)
end
M.close_window = function(state)
renderer.close(state)
end
M.toggle_auto_expand_width = function(state)
if state.window.position == "float" then
return
end
state.window.auto_expand_width = state.window.auto_expand_width == false
local width = utils.resolve_width(state.window.width)
if not state.window.auto_expand_width then
if (state.window.last_user_width or width) >= vim.api.nvim_win_get_width(0) then
state.window.last_user_width = width
end
vim.api.nvim_win_set_width(0, state.window.last_user_width)
state.win_width = state.window.last_user_width
state.longest_width_exact = 0
log.trace(string.format("Collapse auto_expand_width."))
end
renderer.redraw(state)
end
local copy_node_to_clipboard = function(state, node)
state.clipboard = state.clipboard or {}
local existing = state.clipboard[node.id]
if existing and existing.action == "copy" then
state.clipboard[node.id] = nil
else
state.clipboard[node.id] = { action = "copy", node = node }
log.info("Copied " .. node.name .. " to clipboard")
end
end
---Marks node as copied, so that it can be pasted somewhere else.
M.copy_to_clipboard = function(state, callback)
local node = state.tree:get_node()
if node.type == "message" then
return
end
copy_node_to_clipboard(state, node)
if callback then
callback()
end
end
M.copy_to_clipboard_visual = function(state, selected_nodes, callback)
for _, node in ipairs(selected_nodes) do
if node.type ~= "message" then
copy_node_to_clipboard(state, node)
end
end
if callback then
callback()
end
end
local cut_node_to_clipboard = function(state, node)
state.clipboard = state.clipboard or {}
local existing = state.clipboard[node.id]
if existing and existing.action == "cut" then
state.clipboard[node.id] = nil
else
state.clipboard[node.id] = { action = "cut", node = node }
log.info("Cut " .. node.name .. " to clipboard")
end
end
---Marks node as cut, so that it can be pasted (moved) somewhere else.
M.cut_to_clipboard = function(state, callback)
local node = state.tree:get_node()
cut_node_to_clipboard(state, node)
if callback then
callback()
end
end
M.cut_to_clipboard_visual = function(state, selected_nodes, callback)
for _, node in ipairs(selected_nodes) do
if node.type ~= "message" then
cut_node_to_clipboard(state, node)
end
end
if callback then
callback()
end
end
--------------------------------------------------------------------------------
-- Git commands
--------------------------------------------------------------------------------
M.git_add_file = function(state)
local node = state.tree:get_node()
if node.type == "message" then
return
end
local path = node:get_id()
local cmd = { "git", "add", path }
vim.fn.system(cmd)
events.fire_event(events.GIT_EVENT)
end
M.git_add_all = function(state)
local cmd = { "git", "add", "-A" }
vim.fn.system(cmd)
events.fire_event(events.GIT_EVENT)
end
M.git_commit = function(state, and_push)
local width = vim.fn.winwidth(0) - 2
local row = vim.api.nvim_win_get_height(0) - 3
local popup_options = {
relative = "win",
position = {
row = row,
col = 0,
},
size = width,
}
inputs.input("Commit message: ", "", function(msg)
local cmd = { "git", "commit", "-m", msg }
local title = "git commit"
local result = vim.fn.systemlist(cmd)
if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then
popups.alert("ERROR: git commit", result)
return
end
if and_push then
title = "git commit && git push"
cmd = { "git", "push" }
local result2 = vim.fn.systemlist(cmd)
table.insert(result, "")
for i = 1, #result2 do
table.insert(result, result2[i])
end
end
events.fire_event(events.GIT_EVENT)
popups.alert(title, result)
end, popup_options)
end
M.git_commit_and_push = function(state)
M.git_commit(state, true)
end
M.git_push = function(state)
inputs.confirm("Are you sure you want to push your changes?", function(yes)
if yes then
local result = vim.fn.systemlist({ "git", "push" })
events.fire_event(events.GIT_EVENT)
popups.alert("git push", result)
end
end)
end
M.git_unstage_file = function(state)
local node = state.tree:get_node()
if node.type == "message" then
return
end
local path = node:get_id()
local cmd = { "git", "reset", "--", path }
vim.fn.system(cmd)
events.fire_event(events.GIT_EVENT)
end
M.git_revert_file = function(state)
local node = state.tree:get_node()
if node.type == "message" then
return
end
local path = node:get_id()
local cmd = { "git", "checkout", "HEAD", "--", path }
local msg = string.format("Are you sure you want to revert %s?", node.name)
inputs.confirm(msg, function(yes)
if yes then
vim.fn.system(cmd)
events.fire_event(events.GIT_EVENT)
end
end)
end
--------------------------------------------------------------------------------
-- END Git commands
--------------------------------------------------------------------------------
M.next_source = function(state)
local sources = require("neo-tree").config.sources
local sources = require("neo-tree").config.source_selector.sources
local next_source = sources[1]
for i, source_info in ipairs(sources) do
if source_info.source == state.name then
next_source = sources[i + 1]
if not next_source then
next_source = sources[1]
end
break
end
end
require("neo-tree.command").execute({
source = next_source.source,
position = state.current_position,
action = "focus",
})
end
M.prev_source = function(state)
local sources = require("neo-tree").config.sources
local sources = require("neo-tree").config.source_selector.sources
local next_source = sources[#sources]
for i, source_info in ipairs(sources) do
if source_info.source == state.name then
next_source = sources[i - 1]
if not next_source then
next_source = sources[#sources]
end
break
end
end
require("neo-tree.command").execute({
source = next_source.source,
position = state.current_position,
action = "focus",
})
end
M.show_debug_info = function(state)
print(vim.inspect(state))
end
---Pastes all items from the clipboard to the current directory.
---@param state table The state of the source
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
M.paste_from_clipboard = function(state, callback)
if state.clipboard then
local folder = get_folder_node(state):get_id()
-- Convert to list so to make it easier to pop items from the stack.
local clipboard_list = {}
for _, item in pairs(state.clipboard) do
table.insert(clipboard_list, item)
end
state.clipboard = nil
local handle_next_paste, paste_complete
paste_complete = function(source, destination)
if callback then
local insert_as = require("neo-tree").config.window.insert_as
-- open the folder so the user can see the new files
local node = insert_as == "sibling" and state.tree:get_node() or state.tree:get_node(folder)
if not node then
log.warn("Could not find node for " .. folder)
end
callback(node, destination)
end
local next_item = table.remove(clipboard_list)
if next_item then
handle_next_paste(next_item)
end
end
handle_next_paste = function(item)
if item.action == "copy" then
fs_actions.copy_node(
item.node.path,
folder .. utils.path_separator .. item.node.name,
paste_complete
)
elseif item.action == "cut" then
fs_actions.move_node(
item.node.path,
folder .. utils.path_separator .. item.node.name,
paste_complete
)
end
end
local next_item = table.remove(clipboard_list)
if next_item then
handle_next_paste(next_item)
end
end
end
---Copies a node to a new location, using typed input.
---@param state table The state of the source
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
M.copy = function(state, callback)
local node = state.tree:get_node()
if node.type == "message" then
return
end
local using_root_directory = get_using_root_directory(state)
fs_actions.copy_node(node.path, nil, callback, using_root_directory)
end
---Moves a node to a new location, using typed input.
---@param state table The state of the source
---@param callback function The callback to call when the command is done. Called with the parent node as the argument.
M.move = function(state, callback)
local node = state.tree:get_node()
if node.type == "message" then
return
end
local using_root_directory = get_using_root_directory(state)
fs_actions.move_node(node.path, nil, callback, using_root_directory)
end
M.delete = function(state, callback)
local tree = state.tree
local node = tree:get_node()
if node.type == "file" or node.type == "directory" then
fs_actions.delete_node(node.path, callback)
else
log.warn("The `delete` command can only be used on files and directories")
end
end
M.delete_visual = function(state, selected_nodes, callback)
local paths_to_delete = {}
for _, node_to_delete in pairs(selected_nodes) do
if node_to_delete.type == "file" or node_to_delete.type == "directory" then
table.insert(paths_to_delete, node_to_delete.path)
end
end
fs_actions.delete_nodes(paths_to_delete, callback)
end
M.preview = function(state)
Preview.show(state)
end
M.revert_preview = function()
Preview.hide()
end
--
-- Multi-purpose function to back out of whatever we are in
M.cancel = function(state)
if Preview.is_active() then
Preview.hide()
else
if state.current_position == "float" then
renderer.close_all_floating_windows()
end
end
end
M.toggle_preview = function(state)
Preview.toggle(state)
end
M.focus_preview = function()
Preview.focus()
end
---Open file or directory
---@param state table The state of the source
---@param open_cmd string The vim command to use to open the file
---@param toggle_directory function The function to call to toggle a directory
---open/closed
local open_with_cmd = function(state, open_cmd, toggle_directory, open_file)
local tree = state.tree
local success, node = pcall(tree.get_node, tree)
if node.type == "message" then
return
end
if not (success and node) then
log.debug("Could not get node.")
return
end
local function open()
M.revert_preview()
local path = node.path or node:get_id()
local bufnr = node.extra and node.extra.bufnr
if node.type == "terminal" then
path = node:get_id()
end
if type(open_file) == "function" then
open_file(state, path, open_cmd, bufnr)
else
utils.open_file(state, path, open_cmd, bufnr)
end
local extra = node.extra or {}
local pos = extra.position or extra.end_position
if pos ~= nil then
vim.api.nvim_win_set_cursor(0, { (pos[1] or 0) + 1, pos[2] or 0 })
vim.api.nvim_win_call(0, function()
vim.cmd("normal! zvzz") -- expand folds and center cursor
end)
end
end
if utils.is_expandable(node) then
if toggle_directory and node.type == "directory" then
toggle_directory(node)
elseif node:has_children() then
if node:is_expanded() and node.type ~= "directory" then
return open()
end
local updated = false
if node:is_expanded() then
updated = node:collapse()
else
updated = node:expand()
end
if updated then
renderer.redraw(state)
end
end
else
open()
end
end
---Open file or directory in the closest window
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open = function(state, toggle_directory)
open_with_cmd(state, "e", toggle_directory)
end
---Open file or directory in a split of the closest window
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open_split = function(state, toggle_directory)
open_with_cmd(state, "split", toggle_directory)
end
---Open file or directory in a vertical split of the closest window
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open_vsplit = function(state, toggle_directory)
open_with_cmd(state, "vsplit", toggle_directory)
end
---Open file or directory in a new tab
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open_tabnew = function(state, toggle_directory)
open_with_cmd(state, "tabnew", toggle_directory)
end
---Open file or directory or focus it if a buffer already exists with it
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open_drop = function(state, toggle_directory)
open_with_cmd(state, "drop", toggle_directory)
end
---Open file or directory in new tab or focus it if a buffer already exists with it
---@param state table The state of the source
---@param toggle_directory function The function to call to toggle a directory
---open/closed
M.open_tab_drop = function(state, toggle_directory)
open_with_cmd(state, "tab drop", toggle_directory)
end
M.rename = function(state, callback)
local tree = state.tree
local node = tree:get_node()
if node.type == "message" then
return
end
fs_actions.rename_node(node.path, callback)
end
---Expands or collapses the current node.
M.toggle_node = function(state, toggle_directory)
local tree = state.tree
local node = tree:get_node()
if not utils.is_expandable(node) then
return
end
if node.type == "directory" and toggle_directory then
toggle_directory(node)
elseif node:has_children() then
local updated = false
if node:is_expanded() then
updated = node:collapse()
else
updated = node:expand()
end
if updated then
renderer.redraw(state)
end
end
end
---Expands or collapses the current node.
M.toggle_directory = function(state, toggle_directory)
local tree = state.tree
local node = tree:get_node()
if node.type ~= "directory" then
return
end
M.toggle_node(state, toggle_directory)
end
---Marks potential windows with letters and will open the give node in the picked window.
---@param state table The state of the source
---@param path string The path to open
---@param cmd string Command that is used to perform action on picked window
local use_window_picker = function(state, path, cmd)
local success, picker = pcall(require, "window-picker")
if not success then
print(
"You'll need to install window-picker to use this command: https://github.com/s1n7ax/nvim-window-picker"
)
return
end
local events = require("neo-tree.events")
local event_result = events.fire_event(events.FILE_OPEN_REQUESTED, {
state = state,
path = path,
open_cmd = cmd,
}) or {}
if event_result.handled then
events.fire_event(events.FILE_OPENED, path)
return
end
local picked_window_id = picker.pick_window()
if picked_window_id then
vim.api.nvim_set_current_win(picked_window_id)
local result, err = pcall(vim.cmd, cmd .. " " .. vim.fn.fnameescape(path))
if result or err == "Vim(edit):E325: ATTENTION" then
-- fixes #321
vim.api.nvim_buf_set_option(0, "buflisted", true)
events.fire_event(events.FILE_OPENED, path)
else
log.error("Error opening file:", err)
end
end
end
---Marks potential windows with letters and will open the give node in the picked window.
M.open_with_window_picker = function(state, toggle_directory)
open_with_cmd(state, "edit", toggle_directory, use_window_picker)
end
---Marks potential windows with letters and will open the give node in a split next to the picked window.
M.split_with_window_picker = function(state, toggle_directory)
open_with_cmd(state, "split", toggle_directory, use_window_picker)
end
---Marks potential windows with letters and will open the give node in a vertical split next to the picked window.
M.vsplit_with_window_picker = function(state, toggle_directory)
open_with_cmd(state, "vsplit", toggle_directory, use_window_picker)
end
M.show_help = function(state)
help.show(state)
end
return M

View File

@ -0,0 +1,440 @@
-- This file contains the built-in components. Each componment is a function
-- that takes the following arguments:
-- config: A table containing the configuration provided by the user
-- when declaring this component in their renderer config.
-- node: A NuiNode object for the currently focused node.
-- state: The current state of the source providing the items.
--
-- The function should return either a table, or a list of tables, each of which
-- contains the following keys:
-- text: The text to display for this item.
-- highlight: The highlight group to apply to this text.
local highlights = require("neo-tree.ui.highlights")
local utils = require("neo-tree.utils")
local file_nesting = require("neo-tree.sources.common.file-nesting")
local container = require("neo-tree.sources.common.container")
local log = require("neo-tree.log")
local M = {}
local make_two_char = function(symbol)
if vim.fn.strchars(symbol) == 1 then
return symbol .. " "
else
return symbol
end
end
-- only works in the buffers component, but it's here so we don't have to defined
-- multple renderers.
M.bufnr = function(config, node, state)
local highlight = config.highlight or highlights.BUFFER_NUMBER
local bufnr = node.extra and node.extra.bufnr
if not bufnr then
return {}
end
return {
text = string.format("#%s", bufnr),
highlight = highlight,
}
end
M.clipboard = function(config, node, state)
local clipboard = state.clipboard or {}
local clipboard_state = clipboard[node:get_id()]
if not clipboard_state then
return {}
end
return {
text = " (" .. clipboard_state.action .. ")",
highlight = config.highlight or highlights.DIM_TEXT,
}
end
M.container = container.render
M.current_filter = function(config, node, state)
local filter = node.search_pattern or ""
if filter == "" then
return {}
end
return {
{
text = "Find",
highlight = highlights.DIM_TEXT,
},
{
text = string.format('"%s"', filter),
highlight = config.highlight or highlights.FILTER_TERM,
},
{
text = "in",
highlight = highlights.DIM_TEXT,
},
}
end
M.diagnostics = function(config, node, state)
local diag = state.diagnostics_lookup or {}
local diag_state = diag[node:get_id()]
if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then
return {}
end
if not diag_state then
return {}
end
if config.errors_only and diag_state.severity_number > 1 then
return {}
end
local severity = diag_state.severity_string
local defined = vim.fn.sign_getdefined("DiagnosticSign" .. severity)
if not defined then
-- backwards compatibility...
local old_severity = severity
if severity == "Warning" then
old_severity = "Warn"
elseif severity == "Information" then
old_severity = "Info"
end
defined = vim.fn.sign_getdefined("LspDiagnosticsSign" .. old_severity)
end
defined = defined and defined[1]
-- check for overrides in the component config
local severity_lower = severity:lower()
if config.symbols and config.symbols[severity_lower] then
defined = defined or { texthl = "Diagnostic" .. severity }
defined.text = config.symbols[severity_lower]
end
if config.highlights and config.highlights[severity_lower] then
defined = defined or { text = severity:sub(1, 1) }
defined.texthl = config.highlights[severity_lower]
end
if defined and defined.text and defined.texthl then
return {
text = make_two_char(defined.text),
highlight = defined.texthl,
}
else
return {
text = severity:sub(1, 1),
highlight = "Diagnostic" .. severity,
}
end
end
M.git_status = function(config, node, state)
local git_status_lookup = state.git_status_lookup
if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then
return {}
end
if not git_status_lookup then
return {}
end
local git_status = git_status_lookup[node.path]
if not git_status then
if node.filtered_by and node.filtered_by.gitignored then
git_status = "!!"
else
return {}
end
end
local symbols = config.symbols or {}
local change_symbol
local change_highlt = highlights.FILE_NAME
local status_symbol = symbols.staged
local status_highlt = highlights.GIT_STAGED
if node.type == "directory" and git_status:len() == 1 then
status_symbol = nil
end
if git_status:sub(1, 1) == " " then
status_symbol = symbols.unstaged
status_highlt = highlights.GIT_UNSTAGED
end
if git_status:match("?$") then
status_symbol = nil
status_highlt = highlights.GIT_UNTRACKED
change_symbol = symbols.untracked
change_highlt = highlights.GIT_UNTRACKED
-- all variations of merge conflicts
elseif git_status == "DD" then
status_symbol = symbols.conflict
status_highlt = highlights.GIT_CONFLICT
change_symbol = symbols.deleted
change_highlt = highlights.GIT_CONFLICT
elseif git_status == "UU" then
status_symbol = symbols.conflict
status_highlt = highlights.GIT_CONFLICT
change_symbol = symbols.modified
change_highlt = highlights.GIT_CONFLICT
elseif git_status == "AA" then
status_symbol = symbols.conflict
status_highlt = highlights.GIT_CONFLICT
change_symbol = symbols.added
change_highlt = highlights.GIT_CONFLICT
elseif git_status:match("U") then
status_symbol = symbols.conflict
status_highlt = highlights.GIT_CONFLICT
if git_status:match("A") then
change_symbol = symbols.added
elseif git_status:match("D") then
change_symbol = symbols.deleted
end
change_highlt = highlights.GIT_CONFLICT
-- end merge conflict section
elseif git_status:match("M") then
change_symbol = symbols.modified
change_highlt = highlights.GIT_MODIFIED
elseif git_status:match("R") then
change_symbol = symbols.renamed
change_highlt = highlights.GIT_RENAMED
elseif git_status:match("[ACT]") then
change_symbol = symbols.added
change_highlt = highlights.GIT_ADDED
elseif git_status:match("!") then
status_symbol = nil
change_symbol = symbols.ignored
change_highlt = highlights.GIT_IGNORED
elseif git_status:match("D") then
change_symbol = symbols.deleted
change_highlt = highlights.GIT_DELETED
end
if change_symbol or status_symbol then
local components = {}
if type(change_symbol) == "string" and #change_symbol > 0 then
table.insert(components, {
text = make_two_char(change_symbol),
highlight = change_highlt,
})
end
if type(status_symbol) == "string" and #status_symbol > 0 then
table.insert(components, {
text = make_two_char(status_symbol),
highlight = status_highlt,
})
end
return components
else
return {
text = "[" .. git_status .. "]",
highlight = config.highlight or change_highlt,
}
end
end
M.filtered_by = function(config, node, state)
local result = {}
if type(node.filtered_by) == "table" then
local fby = node.filtered_by
if fby.name then
result = {
text = "(hide by name)",
highlight = highlights.HIDDEN_BY_NAME,
}
elseif fby.pattern then
result = {
text = "(hide by pattern)",
highlight = highlights.HIDDEN_BY_NAME,
}
elseif fby.gitignored then
result = {
text = "(gitignored)",
highlight = highlights.GIT_IGNORED,
}
elseif fby.dotfiles then
result = {
text = "(dotfile)",
highlight = highlights.DOTFILE,
}
elseif fby.hidden then
result = {
text = "(hidden)",
highlight = highlights.WINDOWS_HIDDEN,
}
end
fby = nil
end
return result
end
M.icon = function(config, node, state)
local icon = config.default or " "
local highlight = config.highlight or highlights.FILE_ICON
if node.type == "directory" then
highlight = highlights.DIRECTORY_ICON
if node.loaded and not node:has_children() then
icon = not node.empty_expanded and config.folder_empty or config.folder_empty_open
elseif node:is_expanded() then
icon = config.folder_open or "-"
else
icon = config.folder_closed or "+"
end
elseif node.type == "file" or node.type == "terminal" then
local success, web_devicons = pcall(require, "nvim-web-devicons")
if success then
local devicon, hl = web_devicons.get_icon(node.name, node.ext)
icon = devicon or icon
highlight = hl or highlight
end
end
local filtered_by = M.filtered_by(config, node, state)
return {
text = icon .. " ",
highlight = filtered_by.highlight or highlight,
}
end
M.modified = function(config, node, state)
local opened_buffers = state.opened_buffers or {}
local buf_info = opened_buffers[node.path]
if buf_info and buf_info.modified then
return {
text = (make_two_char(config.symbol) or "[+]"),
highlight = config.highlight or highlights.MODIFIED,
}
else
return {}
end
end
M.name = function(config, node, state)
local highlight = config.highlight or highlights.FILE_NAME
local text = node.name
if node.type == "directory" then
highlight = highlights.DIRECTORY_NAME
if config.trailing_slash and text ~= "/" then
text = text .. "/"
end
end
if node:get_depth() == 1 and node.type ~= "message" then
highlight = highlights.ROOT_NAME
else
local filtered_by = M.filtered_by(config, node, state)
highlight = filtered_by.highlight or highlight
if config.use_git_status_colors then
local git_status = state.components.git_status({}, node, state)
if git_status and git_status.highlight then
highlight = git_status.highlight
end
end
end
local hl_opened = config.highlight_opened_files
if hl_opened then
local opened_buffers = state.opened_buffers or {}
if
(hl_opened == "all" and opened_buffers[node.path])
or (opened_buffers[node.path] and opened_buffers[node.path].loaded)
then
highlight = highlights.FILE_NAME_OPENED
end
end
if type(config.right_padding) == "number" then
if config.right_padding > 0 then
text = text .. string.rep(" ", config.right_padding)
end
else
text = text
end
return {
text = text,
highlight = highlight,
}
end
M.indent = function(config, node, state)
if not state.skip_marker_at_level then
state.skip_marker_at_level = {}
end
local strlen = vim.fn.strdisplaywidth
local skip_marker = state.skip_marker_at_level
local indent_size = config.indent_size or 2
local padding = config.padding or 0
local level = node.level
local with_markers = config.with_markers
local with_expanders = config.with_expanders == nil and file_nesting.is_enabled()
or config.with_expanders
local marker_highlight = config.highlight or highlights.INDENT_MARKER
local expander_highlight = config.expander_highlight or config.highlight or highlights.EXPANDER
local function get_expander()
if with_expanders and utils.is_expandable(node) then
return node:is_expanded() and (config.expander_expanded or "")
or (config.expander_collapsed or "")
end
end
if indent_size == 0 or level < 2 or not with_markers then
local len = indent_size * level + padding
local expander = get_expander()
if level == 0 or not expander then
return {
text = string.rep(" ", len),
}
end
return {
text = string.rep(" ", len - strlen(expander) - 1) .. expander .. " ",
highlight = expander_highlight,
}
end
local indent_marker = config.indent_marker or ""
local last_indent_marker = config.last_indent_marker or ""
skip_marker[level] = node.is_last_child
local indent = {}
if padding > 0 then
table.insert(indent, { text = string.rep(" ", padding) })
end
for i = 1, level do
local char = ""
local spaces_count = indent_size
local highlight = nil
if i > 1 and not skip_marker[i] or i == level then
spaces_count = spaces_count - 1
char = indent_marker
highlight = marker_highlight
if i == level then
local expander = get_expander()
if expander then
char = expander
highlight = expander_highlight
elseif node.is_last_child then
char = last_indent_marker
spaces_count = spaces_count - (vim.api.nvim_strwidth(last_indent_marker) - 1)
end
end
end
table.insert(indent, { text = char .. string.rep(" ", spaces_count), highlight = highlight })
end
return indent
end
M.symlink_target = function(config, node, state)
if node.is_link then
return {
text = string.format(" ➛ %s", node.link_to),
highlight = config.highlight or highlights.SYMBOLIC_LINK_TARGET,
}
else
return {}
end
end
return M

View File

@ -0,0 +1,331 @@
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local highlights = require("neo-tree.ui.highlights")
local log = require("neo-tree.log")
local M = {}
local calc_rendered_width = function(rendered_item)
local width = 0
for _, item in ipairs(rendered_item) do
if item.text then
width = width + vim.fn.strchars(item.text)
end
end
return width
end
local calc_container_width = function(config, node, state, context)
local container_width = 0
if type(config.width) == "string" then
if config.width == "fit_content" then
container_width = context.max_width
elseif config.width == "100%" then
container_width = context.available_width
elseif config.width:match("^%d+%%$") then
local percent = tonumber(config.width:sub(1, -2)) / 100
container_width = math.floor(percent * context.available_width)
else
error("Invalid container width: " .. config.width)
end
elseif type(config.width) == "number" then
container_width = config.width
elseif type(config.width) == "function" then
container_width = config.width(node, state)
else
error("Invalid container width: " .. config.width)
end
if config.min_width then
container_width = math.max(container_width, config.min_width)
end
if config.max_width then
container_width = math.min(container_width, config.max_width)
end
context.container_width = container_width
return container_width
end
local render_content = function(config, node, state, context)
local add_padding = function(rendered_item, should_pad)
for _, data in ipairs(rendered_item) do
if data.text then
local padding = (should_pad and #data.text and data.text:sub(1, 1) ~= " ") and " " or ""
data.text = padding .. data.text
should_pad = data.text:sub(#data.text) ~= " "
end
end
return should_pad
end
local max_width = 0
local grouped_by_zindex = utils.group_by(config.content, "zindex")
for zindex, items in pairs(grouped_by_zindex) do
local should_pad = { left = false, right = false }
local zindex_rendered = { left = {}, right = {} }
local rendered_width = 0
for _, item in ipairs(items) do
local rendered_item = renderer.render_component(item, node, state, context.available_width)
if rendered_item then
local align = item.align or "left"
should_pad[align] = add_padding(rendered_item, should_pad[align])
vim.list_extend(zindex_rendered[align], rendered_item)
rendered_width = rendered_width + calc_rendered_width(rendered_item)
end
end
max_width = math.max(max_width, rendered_width)
grouped_by_zindex[zindex] = zindex_rendered
end
context.max_width = max_width
context.grouped_by_zindex = grouped_by_zindex
return context
end
---Takes a list of rendered components and truncates them to fit the container width
---@param layer table The list of rendered components.
---@param skip_count number The number of characters to skip from the begining/left.
---@param max_length number The maximum number of characters to return.
local truncate_layer_keep_left = function(layer, skip_count, max_length)
local result = {}
local taken = 0
local skipped = 0
for _, item in ipairs(layer) do
local remaining_to_skip = skip_count - skipped
if remaining_to_skip > 0 then
if #item.text <= remaining_to_skip then
skipped = skipped + vim.fn.strchars(item.text)
item.text = ""
else
item.text = item.text:sub(remaining_to_skip)
if #item.text + taken > max_length then
item.text = item.text:sub(1, max_length - taken)
end
table.insert(result, item)
taken = taken + #item.text
skipped = skipped + remaining_to_skip
end
elseif taken <= max_length then
if #item.text + taken > max_length then
item.text = item.text:sub(1, max_length - taken)
end
table.insert(result, item)
taken = taken + vim.fn.strchars(item.text)
end
end
return result
end
---Takes a list of rendered components and truncates them to fit the container width
---@param layer table The list of rendered components.
---@param skip_count number The number of characters to skip from the end/right.
---@param max_length number The maximum number of characters to return.
local truncate_layer_keep_right = function(layer, skip_count, max_length)
local result = {}
local taken = 0
local skipped = 0
local i = #layer
while i > 0 do
local item = layer[i]
i = i - 1
local text_length = vim.fn.strchars(item.text)
local remaining_to_skip = skip_count - skipped
if remaining_to_skip > 0 then
if text_length <= remaining_to_skip then
skipped = skipped + text_length
item.text = ""
else
item.text = vim.fn.strcharpart(item.text, 0, text_length - remaining_to_skip)
text_length = vim.fn.strchars(item.text)
if text_length + taken > max_length then
item.text = vim.fn.strcharpart(item.text, text_length - (max_length - taken))
text_length = vim.fn.strchars(item.text)
end
table.insert(result, item)
taken = taken + text_length
skipped = skipped + remaining_to_skip
end
elseif taken <= max_length then
if text_length + taken > max_length then
item.text = vim.fn.strcharpart(item.text, text_length - (max_length - taken))
text_length = vim.fn.strchars(item.text)
end
table.insert(result, item)
taken = taken + text_length
end
end
return result
end
local fade_content = function(layer, fade_char_count)
local text = layer[#layer].text
if not text or #text == 0 then
return
end
local hl = layer[#layer].highlight or "Normal"
local fade = {
highlights.get_faded_highlight_group(hl, 0.68),
highlights.get_faded_highlight_group(hl, 0.6),
highlights.get_faded_highlight_group(hl, 0.35),
}
for i = 3, 1, -1 do
if #text >= i and fade_char_count >= i then
layer[#layer].text = text:sub(1, -i - 1)
for j = i, 1, -1 do
-- force no padding for each faded character
local entry = { text = text:sub(-j, -j), highlight = fade[i - j + 1], no_padding = true }
table.insert(layer, entry)
end
break
end
end
end
local try_fade_content = function(layer, fade_char_count)
local success, err = pcall(fade_content, layer, fade_char_count)
if not success then
log.debug("Error while trying to fade content: ", err)
end
end
local merge_content = function(context)
-- Heres the idea:
-- * Starting backwards from the layer with the highest zindex
-- set the left and right tables to the content of the layer
-- * If a layer has more content than will fit, the left side will be truncated.
-- * If the available space is not used up, move on to the next layer
-- * With each subsequent layer, if the length of that layer is greater then the existing
-- length for that side (left or right), then clip that layer and append whatver portion is
-- not covered up to the appropriate side.
-- * Check again to see if we have used up the available width, short circuit if we have.
-- * Repeat until all layers have been merged.
-- * Join the left and right tables together and return.
--
local remaining_width = context.container_width
local left, right = {}, {}
local left_width, right_width = 0, 0
local wanted_width = 0
if context.left_padding and context.left_padding > 0 then
table.insert(left, { text = string.rep(" ", context.left_padding) })
remaining_width = remaining_width - context.left_padding
left_width = left_width + context.left_padding
wanted_width = wanted_width + context.left_padding
end
if context.right_padding and context.right_padding > 0 then
remaining_width = remaining_width - context.right_padding
wanted_width = wanted_width + context.right_padding
end
local keys = utils.get_keys(context.grouped_by_zindex, true)
if type(keys) ~= "table" then
return {}
end
local i = #keys
while i > 0 do
local key = keys[i]
local layer = context.grouped_by_zindex[key]
i = i - 1
if utils.truthy(layer.right) then
local width = calc_rendered_width(layer.right)
wanted_width = wanted_width + width
if remaining_width > 0 then
context.has_right_content = true
if width > remaining_width then
local truncated = truncate_layer_keep_right(layer.right, right_width, remaining_width)
vim.list_extend(right, truncated)
remaining_width = 0
else
remaining_width = remaining_width - width
vim.list_extend(right, layer.right)
right_width = right_width + width
end
end
end
if utils.truthy(layer.left) then
local width = calc_rendered_width(layer.left)
wanted_width = wanted_width + width
if remaining_width > 0 then
if width > remaining_width then
local truncated = truncate_layer_keep_left(layer.left, left_width, remaining_width)
if context.enable_character_fade then
try_fade_content(truncated, 3)
end
vim.list_extend(left, truncated)
remaining_width = 0
else
remaining_width = remaining_width - width
if context.enable_character_fade and not context.auto_expand_width then
local fade_chars = 3 - remaining_width
if fade_chars > 0 then
try_fade_content(layer.left, fade_chars)
end
end
vim.list_extend(left, layer.left)
left_width = left_width + width
end
end
end
if remaining_width == 0 and not context.auto_expand_width then
i = 0
break
end
end
if remaining_width > 0 and #right > 0 then
table.insert(left, { text = string.rep(" ", remaining_width) })
end
local result = {}
vim.list_extend(result, left)
-- we do not pad between left and right side
if #right >= 1 then
right[1].no_padding = true
end
vim.list_extend(result, right)
context.merged_content = result
log.trace("wanted width: ", wanted_width, " actual width: ", context.container_width)
context.wanted_width = math.max(wanted_width, context.wanted_width)
end
M.render = function(config, node, state, available_width)
local context = {
wanted_width = 0,
max_width = 0,
grouped_by_zindex = {},
available_width = available_width,
left_padding = config.left_padding,
right_padding = config.right_padding,
enable_character_fade = config.enable_character_fade,
auto_expand_width = state.window.auto_expand_width and state.window.position ~= "float",
}
render_content(config, node, state, context)
calc_container_width(config, node, state, context)
merge_content(context)
if context.has_right_content then
state.has_right_content = true
end
-- we still want padding between this container and the previous component
if #context.merged_content > 0 then
context.merged_content[1].no_padding = false
end
return context.merged_content, context.wanted_width
end
return M

View File

@ -0,0 +1,231 @@
local vim = vim
local files_nesting = require("neo-tree.sources.common.file-nesting")
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local git = require("neo-tree.git")
local unused_to_produce_diagnostic = {}
local function sort_items(a, b)
if a.type == b.type then
return a.path < b.path
else
return a.type < b.type
end
end
local function sort_items_case_insensitive(a, b)
if a.type == b.type then
return a.path:lower() < b.path:lower()
else
return a.type < b.type
end
end
local function sort_function_is_valid(func)
if func == nil then
return false
end
local a = { type = "dir", path = "foo" }
local b = { type = "dir", path = "baz" }
local success, result = pcall(func, a, b)
if success and type(result) == "boolean" then
return true
end
log.error("sort function isn't valid ", result)
return false
end
local function deep_sort(tbl, sort_func)
if sort_func == nil then
local config = require("neo-tree").config
if sort_function_is_valid(config.sort_function) then
sort_func = config.sort_function
elseif config.sort_case_insensitive then
sort_func = sort_items_case_insensitive
else
sort_func = sort_items
end
end
table.sort(tbl, sort_func)
for _, item in pairs(tbl) do
if item.type == "directory" then
deep_sort(item.children, sort_func)
end
end
end
local create_item, set_parents
function create_item(context, path, _type, bufnr)
local parent_path, name = utils.split_path(path)
local id = path
if path == "[No Name]" and bufnr then
parent_path = context.state.path
name = "[No Name]"
id = tostring(bufnr)
else
-- avoid creating duplicate items
if context.folders[path] or context.nesting[path] then
return context.folders[path] or context.nesting[path]
end
end
if _type == nil then
local stat = vim.loop.fs_stat(path)
_type = stat and stat.type or "unknown"
end
local item = {
id = id,
name = name,
parent_path = parent_path,
path = path,
type = _type,
}
if item.type == "link" then
item.is_link = true
item.link_to = vim.loop.fs_realpath(path)
if item.link_to ~= nil then
item.type = vim.loop.fs_stat(item.link_to).type
end
end
if item.type == "directory" then
item.children = {}
item.loaded = false
context.folders[path] = item
if context.state.search_pattern then
table.insert(context.state.default_expanded_nodes, item.id)
end
else
item.base = item.name:match("^([-_,()%s%w%i]+)%.")
item.ext = item.name:match("%.([-_,()%s%w%i]+)$")
item.exts = item.name:match("^[-_,()%s%w%i]+%.(.*)")
if files_nesting.can_have_nesting(item) then
item.children = {}
context.nesting[path] = item
end
end
item.is_reveal_target = (path == context.path_to_reveal)
local state = context.state
local f = state.filtered_items
local is_not_root = not utils.is_subpath(path, context.state.path)
if f and is_not_root then
if f.never_show[name] then
item.filtered_by = item.filtered_by or {}
item.filtered_by.never_show = true
else
if utils.is_filtered_by_pattern(f.never_show_by_pattern, path, name) then
item.filtered_by = item.filtered_by or {}
item.filtered_by.never_show = true
end
end
if f.always_show[name] then
item.filtered_by = item.filtered_by or {}
item.filtered_by.always_show = true
end
if f.hide_by_name[name] then
item.filtered_by = item.filtered_by or {}
item.filtered_by.name = true
end
if utils.is_filtered_by_pattern(f.hide_by_pattern, path, name) then
item.filtered_by = item.filtered_by or {}
item.filtered_by.pattern = true
end
if f.hide_dotfiles and string.sub(name, 1, 1) == "." then
item.filtered_by = item.filtered_by or {}
item.filtered_by.dotfiles = true
end
if f.hide_hidden and utils.is_hidden(path) then
item.filtered_by = item.filtered_by or {}
item.filtered_by.hidden = true
end
-- NOTE: git_ignored logic moved to job_complete
end
set_parents(context, item)
if context.all_items == nil then
context.all_items = {}
end
if is_not_root then
table.insert(context.all_items, item)
end
return item
end
-- function to set (or create) parent folder
function set_parents(context, item)
-- we can get duplicate items if we navigate up with open folders
-- this is probably hacky, but it works
if context.item_exists[item.id] then
return
end
if not item.parent_path then
return
end
local nesting_parent_path = files_nesting.get_parent(item)
local nesting_parent = context.nesting[nesting_parent_path]
if
nesting_parent_path
and not nesting_parent
and utils.truthy(vim.loop.fs_stat(nesting_parent_path))
then
local success
success, nesting_parent = pcall(create_item, context, nesting_parent_path)
if not success then
log.error("error, creating item for ", nesting_parent_path)
end
end
local parent = context.folders[item.parent_path]
if not utils.truthy(item.parent_path) then
return
end
if parent == nil and nesting_parent == nil then
local success
success, parent = pcall(create_item, context, item.parent_path, "directory")
if not success then
log.error("error creating item for ", item.parent_path)
end
context.folders[parent.id] = parent
set_parents(context, parent)
end
if nesting_parent then
table.insert(nesting_parent.children, item)
item.is_nested = true
else
table.insert(parent.children, item)
end
context.item_exists[item.id] = true
if item.filtered_by == nil and type(parent.filtered_by) == "table" then
item.filtered_by = vim.deepcopy(parent.filtered_by)
end
end
---Create context to be used in other file-items functions.
---@param state table|nil The state of the file-items.
---@return table
local create_context = function(state)
local context = {}
-- Make the context a weak table so that it can be garbage collected
--setmetatable(context, { __mode = 'v' })
context.state = state
context.folders = {}
context.nesting = {}
context.item_exists = {}
context.all_items = {}
return context
end
return {
create_context = create_context,
create_item = create_item,
deep_sort = deep_sort,
}

View File

@ -0,0 +1,54 @@
local iter = require("plenary.iterators").iter
local utils = require("neo-tree.utils")
local Path = require("plenary.path")
-- File nesting a la JetBrains (#117).
local M = {}
M.config = {}
--- Checks if file-nesting module is enabled by config
---@return boolean
function M.is_enabled()
return next(M.config) ~= nil
end
--- Returns `item` nesting parent path if exists
---@return string?
function M.get_parent(item)
for base_exts, nesting_exts in pairs(M.config) do
for _, exts in ipairs(nesting_exts) do
if item.exts == exts then
local parent_id = utils.path_join(item.parent_path, item.base) .. "." .. base_exts
if Path:new(parent_id):exists() then
return parent_id
end
end
end
end
return nil
end
--- Checks if `item` have a valid nesting lookup
---@return boolean
function M.can_have_nesting(item)
return utils.truthy(M.config[item.exts])
end
--- Checks if `target` should be nested into `base`
---@return boolean
function M.should_nest_file(base, target)
local ext_lookup = M.config[base.exts]
return utils.truthy(
base.base == target.base and ext_lookup and iter(ext_lookup):find(target.exts)
)
end
---Setup the module with the given config
---@param config table
function M.setup(config)
M.config = config or {}
end
return M

View File

@ -0,0 +1,246 @@
-- The lua implementation of the fzy string matching algorithm
-- credits to: https://github.com/swarn/fzy-lua
--[[
The MIT License (MIT)
Copyright (c) 2020 Seth Warn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--]]
-- modified by: @pysan3 (2023)
local SCORE_GAP_LEADING = -0.005
local SCORE_GAP_TRAILING = -0.005
local SCORE_GAP_INNER = -0.01
local SCORE_MATCH_CONSECUTIVE = 1.0
local SCORE_MATCH_SLASH = 0.9
local SCORE_MATCH_WORD = 0.8
local SCORE_MATCH_CAPITAL = 0.7
local SCORE_MATCH_DOT = 0.6
local SCORE_MAX = math.huge
local SCORE_MIN = -math.huge
local MATCH_MAX_LENGTH = 1024
local M = {}
-- Return `true` if `needle` is a subsequence of `haystack`.
function M.has_match(needle, haystack, case_sensitive)
if not case_sensitive then
needle = string.lower(needle)
haystack = string.lower(haystack)
end
local j = 1
for i = 1, string.len(needle) do
j = string.find(haystack, needle:sub(i, i), j, true)
if not j then
return false
else
j = j + 1
end
end
return true
end
local function is_lower(c)
return c:match('%l')
end
local function is_upper(c)
return c:match('%u')
end
local function precompute_bonus(haystack)
local match_bonus = {}
local last_char = '/'
for i = 1, string.len(haystack) do
local this_char = haystack:sub(i, i)
if last_char == '/' or last_char == '\\' then
match_bonus[i] = SCORE_MATCH_SLASH
elseif last_char == '-' or last_char == '_' or last_char == ' ' then
match_bonus[i] = SCORE_MATCH_WORD
elseif last_char == '.' then
match_bonus[i] = SCORE_MATCH_DOT
elseif is_lower(last_char) and is_upper(this_char) then
match_bonus[i] = SCORE_MATCH_CAPITAL
else
match_bonus[i] = 0
end
last_char = this_char
end
return match_bonus
end
local function compute(needle, haystack, D, T, case_sensitive)
-- Note that the match bonuses must be computed before the arguments are
-- converted to lowercase, since there are bonuses for camelCase.
local match_bonus = precompute_bonus(haystack)
local n = string.len(needle)
local m = string.len(haystack)
if not case_sensitive then
needle = string.lower(needle)
haystack = string.lower(haystack)
end
-- Because lua only grants access to chars through substring extraction,
-- get all the characters from the haystack once now, to reuse below.
local haystack_chars = {}
for i = 1, m do
haystack_chars[i] = haystack:sub(i, i)
end
for i = 1, n do
D[i] = {}
T[i] = {}
local prev_score = SCORE_MIN
local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER
local needle_char = needle:sub(i, i)
for j = 1, m do
if needle_char == haystack_chars[j] then
local score = SCORE_MIN
if i == 1 then
score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j]
elseif j > 1 then
local a = T[i - 1][j - 1] + match_bonus[j]
local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE
score = math.max(a, b)
end
D[i][j] = score
prev_score = math.max(score, prev_score + gap_score)
T[i][j] = prev_score
else
D[i][j] = SCORE_MIN
prev_score = prev_score + gap_score
T[i][j] = prev_score
end
end
end
end
-- Compute a matching score for two strings.
--
-- Where `needle` is a subsequence of `haystack`, this returns a score
-- measuring the quality of their match. Better matches get higher scores.
--
-- `needle` must be a subsequence of `haystack`, the result is undefined
-- otherwise. Call `has_match()` before calling `score`.
--
-- returns `get_score_min()` where a or b are longer than `get_max_length()`
--
-- returns `get_score_min()` when a or b are empty strings.
--
-- returns `get_score_max()` when a and b are the same string.
--
-- When the return value is not covered by the above rules, it is a number
-- in the range (`get_score_floor()`, `get_score_ceiling()`)
function M.score(needle, haystack, case_sensitive)
local n = string.len(needle)
local m = string.len(haystack)
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then
return SCORE_MIN
elseif n == m then
return SCORE_MAX
else
local D = {}
local T = {}
compute(needle, haystack, D, T, case_sensitive)
return T[n][m]
end
end
-- Find the locations where fzy matched a string.
--
-- Returns {score, indices}, where indices is an array showing where each
-- character of the needle matches the haystack in the best match.
function M.score_and_positions(needle, haystack, case_sensitive)
local n = string.len(needle)
local m = string.len(haystack)
if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then
return SCORE_MIN, {}
elseif n == m then
local consecutive = {}
for i = 1, n do
consecutive[i] = i
end
return SCORE_MAX, consecutive
end
local D = {}
local T = {}
compute(needle, haystack, D, T, case_sensitive)
local positions = {}
local match_required = false
local j = m
for i = n, 1, -1 do
while j >= 1 do
if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == T[i][j]) then
match_required = (i ~= 1) and (j ~= 1) and (T[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE)
positions[i] = j
j = j - 1
break
else
j = j - 1
end
end
end
return T[n][m], positions
end
-- Return only the positions of a match.
function M.positions(needle, haystack, case_sensitive)
local _, positions = M.score_and_positions(needle, haystack, case_sensitive)
return positions
end
function M.get_score_min()
return SCORE_MIN
end
function M.get_score_max()
return SCORE_MAX
end
function M.get_max_length()
return MATCH_MAX_LENGTH
end
function M.get_score_floor()
return MATCH_MAX_LENGTH * SCORE_GAP_INNER
end
function M.get_score_ceiling()
return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE
end
function M.get_implementation_name()
return 'lua'
end
return M

View File

@ -0,0 +1,225 @@
---A generalization of the filter functionality to directly filter the
---source tree instead of relying on pre-filtered data, which is specific
---to the filesystem source.
local vim = vim
local Input = require("nui.input")
local event = require("nui.utils.autocmd").event
local popups = require("neo-tree.ui.popups")
local renderer = require("neo-tree.ui.renderer")
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local fzy = require("neo-tree.sources.common.filters.filter_fzy")
local M = {}
local cmds = {
move_cursor_down = function(state, scroll_padding)
renderer.focus_node(state, nil, true, 1, scroll_padding)
end,
move_cursor_up = function(state, scroll_padding)
renderer.focus_node(state, nil, true, -1, scroll_padding)
vim.cmd("redraw!")
end,
}
---Reset the current filter to the empty string.
---@param state any
---@param refresh boolean? whether to refresh the source tree
---@param open_current_node boolean? whether to open the current node
local reset_filter = function(state, refresh, open_current_node)
log.trace("reset_search")
if refresh == nil then
refresh = true
end
-- Cancel any pending search
require("neo-tree.sources.filesystem.lib.filter_external").cancel()
-- reset search state
if state.open_folders_before_search then
state.force_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
else
state.force_open_folders = nil
end
state.open_folders_before_search = nil
state.search_pattern = nil
if open_current_node then
local success, node = pcall(state.tree.get_node, state.tree)
if success and node then
local id = node:get_id()
renderer.position.set(state, id)
id = utils.remove_trailing_slash(id)
manager.navigate(state, nil, id, utils.wrap(pcall, renderer.focus_node, state, id, false))
end
elseif refresh then
manager.navigate(state)
else
state.tree = vim.deepcopy(state.orig_tree)
end
state.orig_tree = nil
end
---Show the filtered tree
---@param state any
---@param do_not_focus_window boolean? whether to focus the window
local show_filtered_tree = function(state, do_not_focus_window)
state.tree = vim.deepcopy(state.orig_tree)
state.tree:get_nodes()[1].search_pattern = state.search_pattern
local max_score, max_id = fzy.get_score_min(), nil
local function filter_tree(node_id)
local node = state.tree:get_node(node_id)
local path = node.extra.search_path or node.path
local should_keep = fzy.has_match(state.search_pattern, path)
if should_keep then
local score = fzy.score(state.search_pattern, path)
node.extra.fzy_score = score
if score > max_score then
max_score = score
max_id = node_id
end
end
if node:has_children() then
for _, child_id in ipairs(node:get_child_ids()) do
should_keep = filter_tree(child_id) or should_keep
end
end
if not should_keep then
state.tree:remove_node(node_id) -- TODO: this might not be efficient
end
return should_keep
end
if #state.search_pattern > 0 then
for _, root in ipairs(state.tree:get_nodes()) do
filter_tree(root:get_id())
end
end
manager.redraw(state.name)
if max_id then
renderer.focus_node(state, max_id, do_not_focus_window)
end
end
---Main entry point for the filter functionality.
---This will display a filter input popup and filter the source tree on change and on submit
---@param state table the source state
---@param search_as_you_type boolean? whether to filter as you type or only on submit
---@param keep_filter_on_submit boolean? whether to keep the filter on <CR> or reset it
M.show_filter = function(state, search_as_you_type, keep_filter_on_submit)
local winid = vim.api.nvim_get_current_win()
local height = vim.api.nvim_win_get_height(winid)
local scroll_padding = 3
-- setup the input popup options
local popup_msg = "Search:"
if search_as_you_type then
popup_msg = "Filter:"
end
local width = vim.fn.winwidth(0) - 2
local row = height - 3
if state.current_position == "float" then
scroll_padding = 0
width = vim.fn.winwidth(winid)
row = height - 2
vim.api.nvim_win_set_height(winid, row)
end
state.orig_tree = vim.deepcopy(state.tree)
local popup_options = popups.popup_options(popup_msg, width, {
relative = "win",
winid = winid,
position = {
row = row,
col = 0,
},
size = width,
})
local has_pre_search_folders = utils.truthy(state.open_folders_before_search)
if not has_pre_search_folders then
log.trace("No search or pre-search folders, recording pre-search folders now")
state.open_folders_before_search = renderer.get_expanded_nodes(state.tree)
end
local waiting_for_default_value = utils.truthy(state.search_pattern)
local input = Input(popup_options, {
prompt = " ",
default_value = state.search_pattern,
on_submit = function(value)
if value == "" then
reset_filter(state)
return
end
if search_as_you_type and not keep_filter_on_submit then
reset_filter(state, true, true)
return
end
-- do the search
state.search_pattern = value
show_filtered_tree(state, false)
end,
--this can be bad in a deep folder structure
on_change = function(value)
if not search_as_you_type then
return
end
-- apparently when a default value is set, on_change fires for every character
if waiting_for_default_value then
if #value < #state.search_pattern then
return
end
waiting_for_default_value = false
end
if value == state.search_pattern or value == nil then
return
end
-- finally do the search
log.trace("Setting search in on_change to: " .. value)
state.search_pattern = value
local len_to_delay = { [0] = 500, 500, 400, 200 }
local delay = len_to_delay[#value] or 100
utils.debounce(state.name .. "_filter", function()
show_filtered_tree(state, true)
end, delay, utils.debounce_strategy.CALL_LAST_ONLY)
end,
})
input:mount()
local restore_height = vim.schedule_wrap(function()
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_set_height(winid, height)
end
end)
-- create mappings and autocmd
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
input:map("i", "<esc>", function(bufnr)
vim.cmd("stopinsert")
input:unmount()
if utils.truthy(state.search_pattern) then
reset_filter(state, true)
end
restore_height()
end, { noremap = true })
local config = require("neo-tree").config
for lhs, cmd_name in pairs(config.filesystem.window.fuzzy_finder_mappings) do
local cmd = cmds[cmd_name]
if cmd then
input:map("i", lhs, utils.wrap(cmd, state, scroll_padding), { noremap = true })
else
log.warn(string.format("Invalid command in fuzzy_finder_mappings: %s = %s", lhs, cmd_name))
end
end
end
return M

View File

@ -0,0 +1,93 @@
local Popup = require("nui.popup")
local NuiLine = require("nui.line")
local utils = require("neo-tree.utils")
local popups = require("neo-tree.ui.popups")
local highlights = require("neo-tree.ui.highlights")
local M = {}
local add_text = function(text, highlight)
local line = NuiLine()
line:append(text, highlight)
return line
end
M.show = function(state)
local tree_width = vim.api.nvim_win_get_width(state.winid)
local keys = utils.get_keys(state.resolved_mappings, true)
local lines = { add_text("") }
lines[1] = add_text(" Press the corresponding key to execute the command.", "Comment")
lines[2] = add_text(" Press <Esc> to cancel.", "Comment")
lines[3] = add_text("")
local header = NuiLine()
header:append(string.format(" %14s", "KEY(S)"), highlights.ROOT_NAME)
header:append(" ", highlights.DIM_TEXT)
header:append("COMMAND", highlights.ROOT_NAME)
lines[4] = header
local max_width = #lines[1]:content()
for _, key in ipairs(keys) do
local value = state.resolved_mappings[key]
local nline = NuiLine()
nline:append(string.format(" %14s", key), highlights.FILTER_TERM)
nline:append(" -> ", highlights.DIM_TEXT)
nline:append(value.text, highlights.NORMAL)
local line = nline:content()
if #line > max_width then
max_width = #line
end
table.insert(lines, nline)
end
local width = math.min(60, max_width + 1)
if state.current_position == "right" then
col = vim.o.columns - tree_width - width - 1
else
col = tree_width - 1
end
local options = {
position = {
row = 2,
col = col,
},
size = {
width = width,
height = #keys + 5,
},
enter = true,
focusable = true,
zindex = 50,
relative = "editor",
}
local options = popups.popup_options("Neotree Help", width, options)
local popup = Popup(options)
popup:mount()
popup:map("n", "<esc>", function()
popup:unmount()
end, { noremap = true })
local event = require("nui.utils.autocmd").event
popup:on({ event.BufLeave, event.BufDelete }, function()
popup:unmount()
end, { once = true })
for _, key in ipairs(keys) do
-- map everything except for <escape>
if string.match(key:lower(), "^<esc") == nil then
local value = state.resolved_mappings[key]
popup:map("n", key, function()
popup:unmount()
vim.api.nvim_set_current_win(state.winid)
value.handler()
end)
end
end
for i, line in ipairs(lines) do
line:render(popup.bufnr, -1, i)
end
end
return M

View File

@ -0,0 +1,419 @@
local vim = vim
local utils = require("neo-tree.utils")
local highlights = require("neo-tree.ui.highlights")
local events = require("neo-tree.events")
local manager = require("neo-tree.sources.manager")
local log = require("neo-tree.log")
local renderer = require("neo-tree.ui.renderer")
local neo_tree_preview_namespace = vim.api.nvim_create_namespace("neo_tree_preview")
local function create_floating_preview_window(state)
local default_position = utils.resolve_config_option(state, "window.position", "left")
state.current_position = state.current_position or default_position
local winwidth = vim.api.nvim_win_get_width(state.winid)
local winheight = vim.api.nvim_win_get_height(state.winid)
local height = vim.o.lines - 4
local width = 120
local row, col = 0, 0
if state.current_position == "left" then
col = winwidth + 1
width = math.min(vim.o.columns - col, 120)
elseif state.current_position == "top" or state.current_position == "bottom" then
height = height - winheight
width = winwidth - 2
if state.current_position == "top" then
row = vim.api.nvim_win_get_height(state.winid) + 1
end
elseif state.current_position == "right" then
width = math.min(vim.o.columns - winwidth - 4, 120)
col = vim.o.columns - winwidth - width - 3
elseif state.current_position == "float" then
local pos = vim.api.nvim_win_get_position(state.winid)
-- preview will be same height and top as tree
row = pos[1] - 1
height = winheight
-- tree and preview window will be side by side and centered in the editor
width = math.min(vim.o.columns - winwidth - 4, 120)
local total_width = winwidth + width + 4
local margin = math.floor((vim.o.columns - total_width) / 2)
col = margin + winwidth + 2
-- move the tree window to make the combined layout centered
local popup = renderer.get_nui_popup(state.winid)
popup:update_layout({
relative = "editor",
position = {
row = row,
col = margin,
},
})
else
local cur_pos = state.current_position or "unknown"
log.error('Preview cannot be used when position = "' .. cur_pos .. '"')
return
end
local popups = require("neo-tree.ui.popups")
local options = popups.popup_options("Neo-tree Preview", width, {
ns_id = highlights.ns_id,
size = { height = height, width = width },
relative = "editor",
position = {
row = row,
col = col,
},
win_options = {
number = true,
winhighlight = "Normal:"
.. highlights.FLOAT_NORMAL
.. ",FloatBorder:"
.. highlights.FLOAT_BORDER,
},
})
options.zindex = 40
options.buf_options.filetype = "neo-tree-preview"
local NuiPopup = require("nui.popup")
local win = NuiPopup(options)
win:mount()
return win
end
local Preview = {}
local instance = nil
---Creates a new preview.
---@param state table The state of the source.
---@return table preview A new preview. A preview is a table consisting of the following keys:
-- active = boolean Whether the preview is active.
-- winid = number The id of the window being used to preview.
-- is_neo_tree_window boolean Whether the preview window belongs to neo-tree.
-- bufnr = number The buffer that is currently in the preview window.
-- start_pos = array or nil An array-like table specifying the (0-indexed) starting position of the previewed text.
-- end_pos = array or nil An array-like table specifying the (0-indexed) ending position of the preview text.
-- truth = table A table containing information to be restored when the preview ends.
-- events = array A list of events the preview is subscribed to.
--These keys should not be altered directly. Note that the keys `start_pos`, `end_pos` and `truth`
--may be inaccurate if `active` is false.
function Preview:new(state)
local preview = {}
preview.active = false
preview.config = vim.deepcopy(state.config)
setmetatable(preview, { __index = self })
preview:findWindow(state)
return preview
end
---Preview a buffer in the preview window and optionally reveal and highlight the previewed text.
---@param bufnr number? The number of the buffer to be previewed.
---@param start_pos table? The (0-indexed) starting position of the previewed text. May be absent.
---@param end_pos table? The (0-indexed) ending position of the previewed text. May be absent
function Preview:preview(bufnr, start_pos, end_pos)
if self.is_neo_tree_window then
log.warn("Could not find appropriate window for preview")
return
end
bufnr = bufnr or self.bufnr
if not self.active then
self:activate()
end
if not self.active then
return
end
if bufnr ~= self.bufnr then
self:setBuffer(bufnr)
end
self:clearHighlight()
self.bufnr = bufnr
self.start_pos = start_pos
self.end_pos = end_pos
self:reveal()
self:highlight()
end
---Reverts the preview and inactivates it, restoring the preview window to its previous state.
function Preview:revert()
self.active = false
self:unsubscribe()
self:clearHighlight()
if not renderer.is_window_valid(self.winid) then
self.winid = nil
return
end
if self.config.use_float then
vim.api.nvim_win_close(self.winid, true)
self.winid = nil
return
else
local foldenable = utils.get_value(self.truth, "options.foldenable", nil, false)
if foldenable ~= nil then
vim.api.nvim_win_set_option(self.winid, "foldenable", self.truth.options.foldenable)
end
vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 0)
end
local bufnr = self.truth.bufnr
if type(bufnr) ~= "number" then
return
end
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
self:setBuffer(bufnr)
self.bufnr = bufnr
if vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_call(self.winid, function()
vim.fn.winrestview(self.truth.view)
end)
end
vim.api.nvim_buf_set_option(self.bufnr, "bufhidden", self.truth.options.bufhidden)
end
---Subscribe to event and add it to the preview event list.
--@param source string? Name of the source to add the event to. Will use `events.subscribe` if nil.
--@param event table Event to subscribe to.
function Preview:subscribe(source, event)
if source == nil then
events.subscribe(event)
else
manager.subscribe(source, event)
end
self.events = self.events or {}
table.insert(self.events, { source = source, event = event })
end
---Unsubscribe to all events in the preview event list.
function Preview:unsubscribe()
if self.events == nil then
return
end
for _, event in ipairs(self.events) do
if event.source == nil then
events.unsubscribe(event.event)
else
manager.unsubscribe(event.source, event.event)
end
end
self.events = {}
end
---Finds the appropriate window and updates the preview accordingly.
---@param state table The state of the source.
function Preview:findWindow(state)
local winid, is_neo_tree_window
if self.config.use_float then
if
type(self.winid) == "number"
and vim.api.nvim_win_is_valid(self.winid)
and utils.is_floating(self.winid)
then
return
end
local win = create_floating_preview_window(state)
if not win then
self.active = false
return
end
winid = win.winid
is_neo_tree_window = false
else
winid, is_neo_tree_window = utils.get_appropriate_window(state)
self.bufnr = vim.api.nvim_win_get_buf(winid)
end
if winid == self.winid then
return
end
self.winid, self.is_neo_tree_window = winid, is_neo_tree_window
if self.active then
self:revert()
self:preview()
end
end
---Activates the preview, but does not populate the preview window,
function Preview:activate()
if self.active then
return
end
if not renderer.is_window_valid(self.winid) then
return
end
if self.config.use_float then
self.truth = {}
else
self.truth = {
bufnr = self.bufnr,
view = vim.api.nvim_win_call(self.winid, vim.fn.winsaveview),
options = {
bufhidden = vim.api.nvim_buf_get_option(self.bufnr, "bufhidden"),
foldenable = vim.api.nvim_win_get_option(self.winid, "foldenable"),
},
}
vim.api.nvim_buf_set_option(self.bufnr, "bufhidden", "hide")
vim.api.nvim_win_set_option(self.winid, "foldenable", false)
end
self.active = true
vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 1)
end
---Set the buffer in the preview window without executing BufEnter or BufWinEnter autocommands.
--@param bufnr number The buffer number of the buffer to set.
function Preview:setBuffer(bufnr)
local eventignore = vim.opt.eventignore
vim.opt.eventignore:append("BufEnter,BufWinEnter")
vim.api.nvim_win_set_buf(self.winid, bufnr)
if self.config.use_float then
-- I'm not sufe why float windows won;t show numbers without this
vim.api.nvim_win_set_option(self.winid, "number", true)
end
vim.opt.eventignore = eventignore
end
---Move the cursor to the previewed position and center the screen.
function Preview:reveal()
local pos = self.start_pos or self.end_pos
if not self.active or not self.winid or not pos then
return
end
vim.api.nvim_win_set_cursor(self.winid, { (pos[1] or 0) + 1, pos[2] or 0 })
vim.api.nvim_win_call(self.winid, function()
vim.cmd("normal! zz")
end)
end
---Highlight the previewed range
function Preview:highlight()
if not self.active or not self.bufnr then
return
end
local start_pos, end_pos = self.start_pos, self.end_pos
if not start_pos and not end_pos then
return
elseif not start_pos then
start_pos = end_pos
elseif not end_pos then
end_pos = start_pos
end
local highlight = function(line, col_start, col_end)
vim.api.nvim_buf_add_highlight(
self.bufnr,
neo_tree_preview_namespace,
highlights.PREVIEW,
line,
col_start,
col_end
)
end
local start_line, end_line = start_pos[1], end_pos[1]
local start_col, end_col = start_pos[2], end_pos[2]
if start_line == end_line then
highlight(start_line, start_col, end_col)
else
highlight(start_line, start_col, -1)
for line = start_line + 1, end_line - 1 do
highlight(line, 0, -1)
end
highlight(end_line, 0, end_col)
end
end
---Clear the preview highlight in the buffer currently in the preview window.
function Preview:clearHighlight()
if type(self.bufnr) == "number" and vim.api.nvim_buf_is_valid(self.bufnr) then
vim.api.nvim_buf_clear_namespace(self.bufnr, neo_tree_preview_namespace, 0, -1)
end
end
local toggle_state = false
Preview.hide = function()
toggle_state = false
if instance then
instance:revert()
end
instance = nil
end
Preview.is_active = function()
return instance and instance.active
end
Preview.show = function(state)
local node = state.tree:get_node()
if node.type == "directory" then
return
end
if instance then
instance:findWindow(state)
else
instance = Preview:new(state)
end
local extra = node.extra or {}
local position = extra.position
local end_position = extra.end_position
local path = node.path or node:get_id()
local bufnr = extra.bufnr or vim.fn.bufadd(path)
if bufnr and bufnr > 0 and instance then
instance:preview(bufnr, position, end_position)
end
end
Preview.toggle = function(state)
if toggle_state then
Preview.hide()
else
Preview.show(state)
if instance and instance.active then
toggle_state = true
else
Preview.hide()
return
end
local winid = state.winid
local source_name = state.name
local preview_event = {
event = events.VIM_CURSOR_MOVED,
handler = function()
if not toggle_state or vim.api.nvim_get_current_win() == instance.winid then
return
end
if vim.api.nvim_get_current_win() == winid then
log.debug("Cursor moved in tree window, updating preview")
Preview.show(state)
else
log.debug("Neo-tree window lost focus, disposing preview")
Preview.hide()
end
end,
id = "preview-event",
}
instance:subscribe(source_name, preview_event)
end
end
Preview.focus = function()
if Preview.is_active() then
vim.fn.win_gotoid(instance.winid)
end
end
return Preview

View File

@ -0,0 +1,69 @@
--This file should contain all commands meant to be used by mappings.
local cc = require("neo-tree.sources.common.commands")
local utils = require("neo-tree.utils")
local manager = require("neo-tree.sources.manager")
local inputs = require("neo-tree.ui.inputs")
local filters = require("neo-tree.sources.common.filters")
local vim = vim
local M = {}
local SOURCE_NAME = "document_symbols"
M.refresh = utils.wrap(manager.refresh, SOURCE_NAME)
M.redraw = utils.wrap(manager.redraw, SOURCE_NAME)
M.show_debug_info = function(state)
print(vim.inspect(state))
end
M.jump_to_symbol = function(state, node)
node = node or state.tree:get_node()
if node:get_depth() == 1 then
return
end
vim.api.nvim_set_current_win(state.lsp_winid)
vim.api.nvim_set_current_buf(state.lsp_bufnr)
local symbol_loc = node.extra.selection_range.start
vim.api.nvim_win_set_cursor(state.lsp_winid, { symbol_loc[1] + 1, symbol_loc[2] })
end
M.rename = function(state)
local node = state.tree:get_node()
if node:get_depth() == 1 then
return
end
local old_name = node.name
local callback = function(new_name)
if not new_name or new_name == "" or new_name == old_name then
return
end
M.jump_to_symbol(state, node)
vim.lsp.buf.rename(new_name)
M.refresh(state)
end
local msg = string.format('Enter new name for "%s":', old_name)
inputs.input(msg, old_name, callback)
end
M.open = M.jump_to_symbol
M.filter_on_submit = function(state)
filters.show_filter(state, true, true)
end
M.filter = function(state)
filters.show_filter(state, true)
end
cc._add_common_commands(M, "node") -- common tree commands
cc._add_common_commands(M, "^open") -- open commands
cc._add_common_commands(M, "^close_window$")
cc._add_common_commands(M, "source$") -- source navigation
cc._add_common_commands(M, "preview") -- preview
cc._add_common_commands(M, "^cancel$") -- cancel
cc._add_common_commands(M, "help") -- help commands
cc._add_common_commands(M, "with_window_picker$") -- open using window picker
cc._add_common_commands(M, "^toggle_auto_expand_width$")
return M

View File

@ -0,0 +1,41 @@
-- This file contains the built-in components. Each componment is a function
-- that takes the following arguments:
-- config: A table containing the configuration provided by the user
-- when declaring this component in their renderer config.
-- node: A NuiNode object for the currently focused node.
-- state: The current state of the source providing the items.
--
-- The function should return either a table, or a list of tables, each of which
-- contains the following keys:
-- text: The text to display for this item.
-- highlight: The highlight group to apply to this text.
local highlights = require("neo-tree.ui.highlights")
local common = require("neo-tree.sources.common.components")
local M = {}
M.icon = function(config, node, state)
return {
text = node:get_depth() == 1 and "" or node.extra.kind.icon,
highlight = node.extra.kind.hl,
}
end
M.kind_icon = M.icon
M.kind_name = function(config, node, state)
return {
text = node:get_depth() == 1 and "" or node.extra.kind.name,
highlight = node.extra.kind.hl,
}
end
M.name = function(config, node, state)
return {
text = node.name,
highlight = node.extra.kind.hl or highlights.FILE_NAME,
}
end
return vim.tbl_deep_extend("force", common, M)

View File

@ -0,0 +1,108 @@
--This file should have all functions that are in the public api and either set
--or read the state of this source.
local vim = vim
local manager = require("neo-tree.sources.manager")
local events = require("neo-tree.events")
local utils = require("neo-tree.utils")
local symbols = require("neo-tree.sources.document_symbols.lib.symbols_utils")
local renderer = require("neo-tree.ui.renderer")
local M = {
name = "document_symbols",
display_name = "  Symbols ",
}
local get_state = function()
return manager.get_state(M.name)
end
---Refresh the source with debouncing
---@param args { afile: string }
local refresh_debounced = function(args)
if utils.is_real_file(args.afile) == false then
return
end
utils.debounce(
"document_symbols_refresh",
utils.wrap(manager.refresh, M.name),
100,
utils.debounce_strategy.CALL_LAST_ONLY
)
end
---Internal function to follow the cursor
local follow_symbol = function()
local state = get_state()
if state.lsp_bufnr ~= vim.api.nvim_get_current_buf() then
return
end
local cursor = vim.api.nvim_win_get_cursor(state.lsp_winid)
local node_id = symbols.get_symbol_by_loc(state.tree, { cursor[1] - 1, cursor[2] })
if #node_id > 0 then
renderer.focus_node(state, node_id, true)
end
end
---Follow the cursor with debouncing
---@param args { afile: string }
local follow_debounced = function(args)
if utils.is_real_file(args.afile) == false then
return
end
utils.debounce(
"document_symbols_follow",
utils.wrap(follow_symbol, args.afile),
100,
utils.debounce_strategy.CALL_LAST_ONLY
)
end
---Navigate to the given path.
M.navigate = function(state)
state.lsp_winid, _ = utils.get_appropriate_window(state)
state.lsp_bufnr = vim.api.nvim_win_get_buf(state.lsp_winid)
state.path = vim.api.nvim_buf_get_name(state.lsp_bufnr)
symbols.render_symbols(state)
end
---Configures the plugin, should be called before the plugin is used.
---@param config table Configuration table containing any keys that the user
---wants to change from the defaults. May be empty to accept default values.
M.setup = function(config, global_config)
symbols.setup(config)
if config.before_render then
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
config.before_render(this_state)
end
end,
})
end
local refresh_events = {
events.VIM_BUFFER_ENTER,
events.VIM_INSERT_LEAVE,
events.VIM_TEXT_CHANGED_NORMAL,
}
for _, event in ipairs(refresh_events) do
manager.subscribe(M.name, {
event = event,
handler = refresh_debounced,
})
end
if config.follow_cursor then
manager.subscribe(M.name, {
event = events.VIM_CURSOR_MOVED,
handler = follow_debounced,
})
end
end
return M

View File

@ -0,0 +1,88 @@
---Utilities function to filter the LSP servers
local utils = require("neo-tree.utils")
---@alias LspRespRaw table<integer, { result: LspRespNode }>
local M = {}
---@alias FilterFn fun(client_name: string): boolean
---Filter clients
---@param filter_type "first" | "all"
---@param filter_fn FilterFn
---@param resp LspRespRaw
---@return table<string, LspRespNode>
local filter_clients = function(filter_type, filter_fn, resp)
if resp == nil or type(resp) ~= "table" then
return {}
end
filter_fn = filter_fn or function(client_name)
return true
end
local result = {}
for client_id, client_resp in pairs(resp) do
local client_name = vim.lsp.get_client_by_id(client_id).name
if filter_fn(client_name) and client_resp.result ~= nil then
result[client_name] = client_resp.result
if filter_type ~= "all" then
break
end
end
end
return result
end
---Filter only allowed clients
---@param allow_only string[] the list of clients to keep
---@return FilterFn
local allow_only = function(allow_only)
return function(client_name)
return vim.tbl_contains(allow_only, client_name)
end
end
---Ignore clients
---@param ignore string[] the list of clients to remove
---@return FilterFn
local ignore = function(ignore)
return function(client_name)
return not vim.tbl_contains(ignore, client_name)
end
end
---Main entry point for the filter
---@param resp LspRespRaw
---@return table<string, LspRespNode>
M.filter_resp = function(resp)
return {}
end
---Setup the filter accordingly to the config
---@see neo-tree-document-symbols-source for more details on options that the filter accepts
---@param cfg_flt "first" | "all" | { type: "first" | "all", fn: FilterFn, allow_only: string[], ignore: string[] }
M.setup = function(cfg_flt)
local filter_type = "first"
local filter_fn = nil
if type(cfg_flt) == "table" then
if cfg_flt.type == "all" then
filter_type = "all"
end
if cfg_flt.fn ~= nil then
filter_fn = cfg_flt.fn
elseif cfg_flt.allow_only then
filter_fn = allow_only(cfg_flt.allow_only)
elseif cfg_flt.ignore then
filter_fn = ignore(cfg_flt.ignore)
end
elseif cfg_flt == "all" then
filter_type = "all"
end
M.filter_resp = function(resp)
return filter_clients(filter_type, filter_fn, resp)
end
end
return M

View File

@ -0,0 +1,62 @@
---Helper module to render symbols' kinds
---Need to be initialized by calling M.setup()
local M = {}
local kinds_id_to_name = {
[0] = "Root",
[1] = "File",
[2] = "Module",
[3] = "Namespace",
[4] = "Package",
[5] = "Class",
[6] = "Method",
[7] = "Property",
[8] = "Field",
[9] = "Constructor",
[10] = "Enum",
[11] = "Interface",
[12] = "Function",
[13] = "Variable",
[14] = "Constant",
[15] = "String",
[16] = "Number",
[17] = "Boolean",
[18] = "Array",
[19] = "Object",
[20] = "Key",
[21] = "Null",
[22] = "EnumMember",
[23] = "Struct",
[24] = "Event",
[25] = "Operator",
[26] = "TypeParameter",
}
local kinds_map = {}
---Get how the kind with kind_id should be rendered
---@param kind_id integer the kind_id to be render
---@return table res of the form { name = kind_display_name, icon = kind_icon, hl = kind_hl }
M.get_kind = function(kind_id)
local kind_name = kinds_id_to_name[kind_id]
return vim.tbl_extend(
"force",
{ name = kind_name or ("Unknown: " .. kind_id), icon = "?", hl = "" },
kind_name and (kinds_map[kind_name] or {}) or kinds_map["Unknown"]
)
end
---Setup the module with custom kinds
---@param custom_kinds table additional kinds, should be of the form { [kind_id] = kind_name }
---@param kinds_display table mapping of kind_name to corresponding display name, icon and hl group
--- { [kind_name] = {
--- name = kind_display_name,
--- icon = kind_icon,
--- hl = kind_hl
--- }, }
M.setup = function(custom_kinds, kinds_display)
kinds_id_to_name = vim.tbl_deep_extend("force", kinds_id_to_name, custom_kinds or {})
kinds_map = kinds_display
end
return M

View File

@ -0,0 +1,210 @@
---Utilities functions for the document_symbols source
local renderer = require("neo-tree.ui.renderer")
local filters = require("neo-tree.sources.document_symbols.lib.client_filters")
local kinds = require("neo-tree.sources.document_symbols.lib.kinds")
local M = {}
---@alias Loc integer[] a location in a buffer {row, col}, 0-indexed
---@alias LocRange { start: Loc, ["end"]: Loc } a range consisting of two loc
---@class SymbolExtra
---@field bufnr integer the buffer containing the symbols,
---@field kind string the kind of each symbol
---@field selection_range LocRange the symbol's location
---@field position Loc start of symbol's definition
---@field end_position Loc start of symbol's definition
---@class SymbolNode see
---@field id string
---@field name string name of symbol
---@field path string buffer path - should all be the same
---@field type "root"|"symbol"
---@field children SymbolNode[]
---@field extra SymbolExtra additional info
---@alias LspLoc { line: integer, character: integer}
---@alias LspRange { start : LspLoc, ["end"]: LspLoc }
---@class LspRespNode
---@field name string
---@field detail string?
---@field kind integer
---@field tags any
---@field deprecated boolean?
---@field range LspRange
---@field selectionRange LspRange
---@field children LspRespNode[]
---Parse the LspRange
---@param range LspRange the LspRange object to parse
---@return LocRange range the parsed range
local parse_range = function(range)
return {
start = { range.start.line, range.start.character },
["end"] = { range["end"].line, range["end"].character },
}
end
---Compare two tuples of length 2 by first - second elements
---@param a Loc
---@param b Loc
---@return boolean
local loc_less_than = function(a, b)
if a[1] < b[1] then
return true
elseif a[1] == b[1] then
return a[2] <= b[2]
end
return false
end
---Check whether loc is contained in range, i.e range[1] <= loc <= range[2]
---@param loc Loc
---@param range LocRange
---@return boolean
M.is_loc_in_range = function(loc, range)
return loc_less_than(range[1], loc) and loc_less_than(loc, range[2])
end
---Get the the current symbol under the cursor
---@param tree any the Nui symbol tree
---@param loc Loc the cursor location {row, col} (0-index)
---@return string node_id
M.get_symbol_by_loc = function(tree, loc)
local function dfs(node)
local node_id = node:get_id()
if node:has_children() then
for _, child in ipairs(tree:get_nodes(node_id)) do
if M.is_loc_in_range(loc, { child.extra.position, child.extra.end_position }) then
return dfs(child)
end
end
end
return node_id
end
for _, root in ipairs(tree:get_nodes()) do
local node_id = dfs(root)
if node_id ~= root:get_id() then
return node_id
end
end
return ""
end
---Parse the LSP response into a tree. Each node on the tree follows
---the same structure as a NuiTree node, with the extra field
---containing additional information.
---@param resp_node LspRespNode the LSP response node
---@param id string the id of the current node
---@return SymbolNode symb_node the parsed tree
local function parse_resp(resp_node, id, state, parent_search_path)
-- parse all children
local children = {}
local search_path = parent_search_path .. "/" .. resp_node.name
for i, child in ipairs(resp_node.children or {}) do
local child_node = parse_resp(child, id .. "." .. i, state, search_path)
table.insert(children, child_node)
end
-- parse current node
local preview_range = parse_range(resp_node.range)
local symb_node = {
id = id,
name = resp_node.name,
type = "symbol",
path = state.path,
children = children,
extra = {
bufnr = state.lsp_bufnr,
kind = kinds.get_kind(resp_node.kind),
selection_range = parse_range(resp_node.selectionRange),
search_path = search_path,
-- detail = resp_node.detail,
position = preview_range.start,
end_position = preview_range["end"],
},
}
return symb_node
end
---Callback function for lsp request
---@param lsp_resp LspRespRaw the response of the lsp client
---@param state table the state of the source
local on_lsp_resp = function(lsp_resp, state)
if lsp_resp == nil or type(lsp_resp) ~= "table" then
return
end
-- filter the response to get only the desired LSP
local resp = filters.filter_resp(lsp_resp)
local bufname = state.path
local items = {}
-- parse each client's response
for client_name, client_result in pairs(resp) do
local symbol_list = {}
for i, resp_node in ipairs(client_result) do
table.insert(symbol_list, parse_resp(resp_node, #items .. "." .. i, state, "/"))
end
-- add the parsed response to the tree
local splits = vim.split(bufname, "/")
local filename = splits[#splits]
table.insert(items, {
id = "" .. #items,
name = string.format("SYMBOLS (%s) in %s", client_name, filename),
path = bufname,
type = "root",
children = symbol_list,
extra = { kind = kinds.get_kind(0), search_path = "/" },
})
end
renderer.show_nodes(items, state)
end
M.render_symbols = function(state)
local bufnr = state.lsp_bufnr
local bufname = state.path
-- if no client found, terminate
local client_found = false
for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
if client.server_capabilities.documentSymbolProvider then
client_found = true
break
end
end
if not client_found then
local splits = vim.split(bufname, "/")
renderer.show_nodes({
{
id = "0",
name = "No client found for " .. splits[#splits],
path = bufname,
type = "root",
children = {},
extra = { kind = kinds.get_kind(0), search_path = "/" },
},
}, state)
return
end
-- client found
vim.lsp.buf_request_all(
bufnr,
"textDocument/documentSymbol",
{ textDocument = vim.lsp.util.make_text_document_params(bufnr) },
function(resp)
on_lsp_resp(resp, state)
end
)
end
M.setup = function(config)
filters.setup(config.client_filters)
kinds.setup(config.custom_kinds, config.kinds)
end
return M

View File

@ -0,0 +1,248 @@
--This file should contain all commands meant to be used by mappings.
local cc = require("neo-tree.sources.common.commands")
local fs = require("neo-tree.sources.filesystem")
local utils = require("neo-tree.utils")
local filter = require("neo-tree.sources.filesystem.lib.filter")
local renderer = require("neo-tree.ui.renderer")
local log = require("neo-tree.log")
local M = {}
local refresh = function(state)
fs._navigate_internal(state, nil, nil, nil, false)
end
local redraw = function(state)
renderer.redraw(state)
end
M.add = function(state)
cc.add(state, utils.wrap(fs.show_new_children, state))
end
M.add_directory = function(state)
cc.add_directory(state, utils.wrap(fs.show_new_children, state))
end
M.clear_filter = function(state)
fs.reset_search(state, true)
end
M.copy = function(state)
cc.copy(state, utils.wrap(refresh, state))
end
---Marks node as copied, so that it can be pasted somewhere else.
M.copy_to_clipboard = function(state)
cc.copy_to_clipboard(state, utils.wrap(redraw, state))
end
M.copy_to_clipboard_visual = function(state, selected_nodes)
cc.copy_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state))
end
---Marks node as cut, so that it can be pasted (moved) somewhere else.
M.cut_to_clipboard = function(state)
cc.cut_to_clipboard(state, utils.wrap(redraw, state))
end
M.cut_to_clipboard_visual = function(state, selected_nodes)
cc.cut_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state))
end
M.move = function(state)
cc.move(state, utils.wrap(refresh, state))
end
---Pastes all items from the clipboard to the current directory.
M.paste_from_clipboard = function(state)
cc.paste_from_clipboard(state, utils.wrap(fs.show_new_children, state))
end
M.delete = function(state)
cc.delete(state, utils.wrap(refresh, state))
end
M.delete_visual = function(state, selected_nodes)
cc.delete_visual(state, selected_nodes, utils.wrap(refresh, state))
end
M.expand_all_nodes = function(state)
local toggle_dir_no_redraw = function(_state, node)
fs.toggle_directory(_state, node, nil, true, true)
end
cc.expand_all_nodes(state, toggle_dir_no_redraw)
end
---Shows the filter input, which will filter the tree.
M.filter_as_you_type = function(state)
filter.show_filter(state, true)
end
---Shows the filter input, which will filter the tree.
M.filter_on_submit = function(state)
filter.show_filter(state, false)
end
---Shows the filter input in fuzzy finder mode.
M.fuzzy_finder = function(state)
filter.show_filter(state, true, true)
end
---Shows the filter input in fuzzy finder mode.
M.fuzzy_finder_directory = function(state)
filter.show_filter(state, true, "directory")
end
---Shows the filter input in fuzzy sorter
M.fuzzy_sorter = function(state)
filter.show_filter(state, true, true, true)
end
---Shows the filter input in fuzzy sorter with only directories
M.fuzzy_sorter_directory = function(state)
filter.show_filter(state, true, "directory", true)
end
---Navigate up one level.
M.navigate_up = function(state)
local parent_path, _ = utils.split_path(state.path)
if not utils.truthy(parent_path) then
return
end
local path_to_reveal = nil
local node = state.tree:get_node()
if node then
path_to_reveal = node:get_id()
end
if state.search_pattern then
fs.reset_search(state, false)
end
log.debug("Changing directory to:", parent_path)
fs._navigate_internal(state, parent_path, path_to_reveal, nil, false)
end
local focus_next_git_modified = function(state, reverse)
local node = state.tree:get_node()
local current_path = node:get_id()
local g = state.git_status_lookup
if not utils.truthy(g) then
return
end
local paths = { current_path }
for path, status in pairs(g) do
if path ~= current_path and status and status ~= "!!" then
--don't include files not in the current working directory
if utils.is_subpath(state.path, path) then
table.insert(paths, path)
end
end
end
local sorted_paths = utils.sort_by_tree_display(paths)
if reverse then
sorted_paths = utils.reverse_list(sorted_paths)
end
local is_file = function(path)
local success, stats = pcall(vim.loop.fs_stat, path)
return (success and stats and stats.type ~= "directory")
end
local passed = false
local target = nil
for _, path in ipairs(sorted_paths) do
if target == nil and is_file(path) then
target = path
end
if passed then
if is_file(path) then
target = path
break
end
elseif path == current_path then
passed = true
end
end
local existing = state.tree:get_node(target)
if existing then
renderer.focus_node(state, target)
else
fs.navigate(state, state.path, target, nil, false)
end
end
M.next_git_modified = function(state)
focus_next_git_modified(state, false)
end
M.prev_git_modified = function(state)
focus_next_git_modified(state, true)
end
M.open = function(state)
cc.open(state, utils.wrap(fs.toggle_directory, state))
end
M.open_split = function(state)
cc.open_split(state, utils.wrap(fs.toggle_directory, state))
end
M.open_vsplit = function(state)
cc.open_vsplit(state, utils.wrap(fs.toggle_directory, state))
end
M.open_tabnew = function(state)
cc.open_tabnew(state, utils.wrap(fs.toggle_directory, state))
end
M.open_drop = function(state)
cc.open_drop(state, utils.wrap(fs.toggle_directory, state))
end
M.open_tab_drop = function(state)
cc.open_tab_drop(state, utils.wrap(fs.toggle_directory, state))
end
M.open_with_window_picker = function(state)
cc.open_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
end
M.split_with_window_picker = function(state)
cc.split_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
end
M.vsplit_with_window_picker = function(state)
cc.vsplit_with_window_picker(state, utils.wrap(fs.toggle_directory, state))
end
M.refresh = refresh
M.rename = function(state)
cc.rename(state, utils.wrap(refresh, state))
end
M.set_root = function(state)
local tree = state.tree
local node = tree:get_node()
if node.type == "directory" then
if state.search_pattern then
fs.reset_search(state, false)
end
fs._navigate_internal(state, node.id, nil, nil, false)
end
end
---Toggles whether hidden files are shown or not.
M.toggle_hidden = function(state)
state.filtered_items.visible = not state.filtered_items.visible
log.info("Toggling hidden files: " .. tostring(state.filtered_items.visible))
refresh(state)
end
---Toggles whether the tree is filtered by gitignore or not.
M.toggle_gitignore = function(state)
log.warn("`toggle_gitignore` has been removed, running toggle_hidden instead.")
M.toggle_hidden(state)
end
M.toggle_node = function(state)
cc.toggle_node(state, utils.wrap(fs.toggle_directory, state))
end
cc._add_common_commands(M)
return M

View File

@ -0,0 +1,40 @@
-- This file contains the built-in components. Each componment is a function
-- that takes the following arguments:
-- config: A table containing the configuration provided by the user
-- when declaring this component in their renderer config.
-- node: A NuiNode object for the currently focused node.
-- state: The current state of the source providing the items.
--
-- The function should return either a table, or a list of tables, each of which
-- contains the following keys:
-- text: The text to display for this item.
-- highlight: The highlight group to apply to this text.
local highlights = require("neo-tree.ui.highlights")
local common = require("neo-tree.sources.common.components")
local utils = require("neo-tree.utils")
local M = {}
M.current_filter = function(config, node, state)
local filter = node.search_pattern or ""
if filter == "" then
return {}
end
return {
{
text = "Find",
highlight = highlights.DIM_TEXT,
},
{
text = string.format('"%s"', filter),
highlight = config.highlight or highlights.FILTER_TERM,
},
{
text = "in",
highlight = highlights.DIM_TEXT,
},
}
end
return vim.tbl_deep_extend("force", common, M)

View File

@ -0,0 +1,431 @@
--This file should have all functions that are in the public api and either set
--or read the state of this source.
local vim = vim
local utils = require("neo-tree.utils")
local fs_scan = require("neo-tree.sources.filesystem.lib.fs_scan")
local renderer = require("neo-tree.ui.renderer")
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local git = require("neo-tree.git")
local glob = require("neo-tree.sources.filesystem.lib.globtopattern")
local M = {
name = "filesystem",
display_name = "  Files ",
}
local wrap = function(func)
return utils.wrap(func, M.name)
end
local get_state = function(tabid)
return manager.get_state(M.name, tabid)
end
-- TODO: DEPRECATED in 1.19, remove in 2.0
-- Leaving this here for now because it was mentioned in the help file.
M.reveal_current_file = function()
log.warn("DEPRECATED: use `neotree.sources.manager.reveal_current_file('filesystem')` instead")
return manager.reveal_current_file(M.name)
end
local follow_internal = function(callback, force_show, async)
log.trace("follow called")
if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then
return
end
local path_to_reveal = manager.get_path_to_reveal()
if not utils.truthy(path_to_reveal) then
return false
end
local state = get_state()
if state.current_position == "float" then
return false
end
if not state.path then
return false
end
local window_exists = renderer.window_exists(state)
if window_exists then
local node = state.tree and state.tree:get_node()
if node then
if node:get_id() == path_to_reveal then
-- already focused
return false
end
end
else
if not force_show then
return false
end
end
local is_in_path = path_to_reveal:sub(1, #state.path) == state.path
if not is_in_path then
return false
end
log.debug("follow file: ", path_to_reveal)
local show_only_explicitly_opened = function()
local eod = state.explicitly_opened_directories or {}
local expanded_nodes = renderer.get_expanded_nodes(state.tree)
local state_changed = false
for _, id in ipairs(expanded_nodes) do
local is_explicit = eod[id]
if not is_explicit then
local is_in_path = path_to_reveal:sub(1, #id) == id
if is_in_path then
is_explicit = true
end
end
if not is_explicit then
local node = state.tree:get_node(id)
if node then
node:collapse()
state_changed = true
end
end
if state_changed then
renderer.redraw(state)
end
end
end
state.position.is.restorable = false -- we will handle setting cursor position here
fs_scan.get_items(state, nil, path_to_reveal, function()
show_only_explicitly_opened()
renderer.focus_node(state, path_to_reveal, true)
if type(callback) == "function" then
callback()
end
end, async)
return true
end
M.follow = function(callback, force_show)
if vim.fn.bufname(0) == "COMMIT_EDITMSG" then
return false
end
if utils.is_floating() then
return false
end
utils.debounce("neo-tree-follow", function()
return follow_internal(callback, force_show)
end, 100, utils.debounce_strategy.CALL_LAST_ONLY)
end
M._navigate_internal = function(state, path, path_to_reveal, callback, async)
log.trace("navigate_internal", state.current_position, path, path_to_reveal)
state.dirty = false
local is_search = utils.truthy(state.search_pattern)
local path_changed = false
if not path and not state.bind_to_cwd then
path = state.path
end
if path == nil then
log.debug("navigate_internal: path is nil, using cwd")
path = manager.get_cwd(state)
end
if path ~= state.path then
log.debug("navigate_internal: path changed from ", state.path, " to ", path)
state.path = path
path_changed = true
end
if path_to_reveal then
renderer.position.set(state, path_to_reveal)
log.debug(
"navigate_internal: in path_to_reveal, state.position is ",
state.position.node_id,
", restorable = ",
state.position.is.restorable
)
fs_scan.get_items(state, nil, path_to_reveal, callback)
else
local is_current = state.current_position == "current"
local follow_file = state.follow_current_file
and not is_search
and not is_current
and manager.get_path_to_reveal()
local handled = false
if utils.truthy(follow_file) then
handled = follow_internal(callback, true, async)
end
if not handled then
local success, msg = pcall(renderer.position.save, state)
if success then
log.trace("navigate_internal: position saved")
else
log.trace("navigate_internal: FAILED to save position: ", msg)
end
fs_scan.get_items(state, nil, nil, callback, async)
end
end
if path_changed and state.bind_to_cwd then
manager.set_cwd(state)
end
local config = require("neo-tree").config
if config.enable_git_status and not is_search and config.git_status_async then
git.status_async(state.path, state.git_base, config.git_status_async_options)
end
end
---Navigate to the given path.
---@param path string? Path to navigate to. If empty, will navigate to the cwd.
---@param path_to_reveal string? Node to focus after the items are loaded.
---@param callback function? Callback to call after the items are loaded.
M.navigate = function(state, path, path_to_reveal, callback, async)
log.trace("navigate", path, path_to_reveal, async)
utils.debounce("filesystem_navigate", function()
M._navigate_internal(state, path, path_to_reveal, callback, async)
end, utils.debounce_strategy.CALL_FIRST_AND_LAST, 100)
end
M.reset_search = function(state, refresh, open_current_node)
log.trace("reset_search")
-- Cancel any pending search
require("neo-tree.sources.filesystem.lib.filter_external").cancel()
-- reset search state
state.fuzzy_finder_mode = nil
state.use_fzy = nil
state.fzy_sort_result_scores = nil
state.fzy_sort_file_list_cache = nil
state.sort_function_override = nil
if refresh == nil then
refresh = true
end
if state.open_folders_before_search then
state.force_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
else
state.force_open_folders = nil
end
state.search_pattern = nil
state.open_folders_before_search = nil
if open_current_node then
local success, node = pcall(state.tree.get_node, state.tree)
if success and node then
local path = node:get_id()
renderer.position.set(state, path)
if node.type == "directory" then
path = utils.remove_trailing_slash(path)
log.trace("opening directory from search: ", path)
M.navigate(state, nil, path, function()
pcall(renderer.focus_node, state, path, false)
end)
else
utils.open_file(state, path)
if
refresh
and state.current_position ~= "current"
and state.current_position ~= "float"
then
M.navigate(state, nil, path)
end
end
end
elseif refresh then
M.navigate(state)
end
end
M.show_new_children = function(state, node_or_path)
local node = node_or_path
if node_or_path == nil then
node = state.tree:get_node()
node_or_path = node:get_id()
elseif type(node_or_path) == "string" then
node = state.tree:get_node(node_or_path)
if node == nil then
local parent_path, _ = utils.split_path(node_or_path)
node = state.tree:get_node(parent_path)
if node == nil then
M.navigate(state, nil, node_or_path)
return
end
end
else
node = node_or_path
node_or_path = node:get_id()
end
if node.type ~= "directory" then
return
end
M.navigate(state, nil, node_or_path)
end
---Configures the plugin, should be called before the plugin is used.
---@param config table Configuration table containing any keys that the user
--wants to change from the defaults. May be empty to accept default values.
M.setup = function(config, global_config)
config.filtered_items = config.filtered_items or {}
config.enable_git_status = global_config.enable_git_status
for _, key in ipairs({ "hide_by_pattern", "never_show_by_pattern" }) do
local list = config.filtered_items[key]
if type(list) == "table" then
for i, pattern in ipairs(list) do
list[i] = glob.globtopattern(pattern)
end
end
end
for _, key in ipairs({ "hide_by_name", "always_show", "never_show" }) do
local list = config.filtered_items[key]
if type(list) == "table" then
config.filtered_items[key] = utils.list_to_dict(list)
end
end
--Configure events for before_render
if config.before_render then
--convert to new event system
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
config.before_render(this_state)
end
end,
})
elseif global_config.enable_git_status and global_config.git_status_async then
manager.subscribe(M.name, {
event = events.GIT_STATUS_CHANGED,
handler = wrap(manager.git_status_changed),
})
elseif global_config.enable_git_status then
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
state.git_status_lookup = git.status(state.git_base)
end
end,
})
end
-- Respond to git events from git_status source or Fugitive
if global_config.enable_git_status then
manager.subscribe(M.name, {
event = events.GIT_EVENT,
handler = function()
manager.refresh(M.name)
end,
})
end
--Configure event handlers for file changes
if config.use_libuv_file_watcher then
manager.subscribe(M.name, {
event = events.FS_EVENT,
handler = wrap(manager.refresh),
})
else
require("neo-tree.sources.filesystem.lib.fs_watch").unwatch_all()
if global_config.enable_refresh_on_write then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_CHANGED,
handler = function(arg)
local afile = arg.afile or ""
if utils.is_real_file(afile) then
log.trace("refreshing due to vim_buffer_changed event: ", afile)
manager.refresh("filesystem")
else
log.trace("Ignoring vim_buffer_changed event for non-file: ", afile)
end
end,
})
end
end
--Configure event handlers for cwd changes
if config.bind_to_cwd then
manager.subscribe(M.name, {
event = events.VIM_DIR_CHANGED,
handler = wrap(manager.dir_changed),
})
end
--Configure event handlers for lsp diagnostic updates
if global_config.enable_diagnostics then
manager.subscribe(M.name, {
event = events.VIM_DIAGNOSTIC_CHANGED,
handler = wrap(manager.diagnostics_changed),
})
end
--Configure event handlers for modified files
if global_config.enable_modified_markers then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_MODIFIED_SET,
handler = wrap(manager.opened_buffers_changed),
})
end
if global_config.enable_opened_markers then
for _, event in ipairs({ events.VIM_BUFFER_ADDED, events.VIM_BUFFER_DELETED }) do
manager.subscribe(M.name, {
event = event,
handler = wrap(manager.opened_buffers_changed),
})
end
end
-- Configure event handler for follow_current_file option
if config.follow_current_file then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_ENTER,
handler = function(args)
if utils.is_real_file(args.afile) then
M.follow()
end
end,
})
end
end
---Expands or collapses the current node.
M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive)
local tree = state.tree
if not node then
node = tree:get_node()
end
if node.type ~= "directory" then
return
end
state.explicitly_opened_directories = state.explicitly_opened_directories or {}
if node.loaded == false then
local id = node:get_id()
state.explicitly_opened_directories[id] = true
renderer.position.set(state, nil)
fs_scan.get_items(state, id, path_to_reveal, nil, false, recursive)
elseif node:has_children() then
local updated = false
if node:is_expanded() then
updated = node:collapse()
state.explicitly_opened_directories[node:get_id()] = false
else
updated = node:expand()
state.explicitly_opened_directories[node:get_id()] = true
end
if updated and not skip_redraw then
renderer.redraw(state)
end
if path_to_reveal then
renderer.focus_node(state, path_to_reveal)
end
elseif require("neo-tree").config.filesystem.scan_mode == "deep" then
node.empty_expanded = not node.empty_expanded
renderer.redraw(state)
end
end
return M

View File

@ -0,0 +1,233 @@
-- This file holds all code for the search function.
local vim = vim
local Input = require("nui.input")
local event = require("nui.utils.autocmd").event
local fs = require("neo-tree.sources.filesystem")
local popups = require("neo-tree.ui.popups")
local renderer = require("neo-tree.ui.renderer")
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local M = {}
local cmds = {
move_cursor_down = function(state, scroll_padding)
renderer.focus_node(state, nil, true, 1, scroll_padding)
end,
move_cursor_up = function(state, scroll_padding)
renderer.focus_node(state, nil, true, -1, scroll_padding)
vim.cmd("redraw!")
end,
}
local function create_input_mapping_handle(cmd, state, scroll_padding)
return function()
cmd(state, scroll_padding)
end
end
M.show_filter = function(state, search_as_you_type, fuzzy_finder_mode, use_fzy)
local popup_options
local winid = vim.api.nvim_get_current_win()
local height = vim.api.nvim_win_get_height(winid)
local scroll_padding = 3
local popup_msg = "Search:"
if search_as_you_type then
if fuzzy_finder_mode == "directory" then
popup_msg = "Filter Directories:"
else
popup_msg = "Filter:"
end
end
if state.current_position == "float" then
scroll_padding = 0
local width = vim.fn.winwidth(winid)
local row = height - 2
vim.api.nvim_win_set_height(winid, row)
popup_options = popups.popup_options(popup_msg, width, {
relative = "win",
winid = winid,
position = {
row = row,
col = 0,
},
size = width,
})
else
local width = vim.fn.winwidth(0) - 2
local row = height - 3
popup_options = popups.popup_options(popup_msg, width, {
relative = "win",
winid = winid,
position = {
row = row,
col = 0,
},
size = width,
})
end
local sort_by_score = function(a, b)
-- `state.fzy_sort_result_scores` should be defined in
-- `sources.filesystem.lib.filter_external.fzy_sort_files`
local result_scores = state.fzy_sort_result_scores or { foo = 0, baz = 0 }
local a_score = result_scores[a.path]
local b_score = result_scores[b.path]
if a_score == nil or b_score == nil then
log.debug(string.format([[Fzy: failed to compare %s: %s, %s: %s]], a.path, a_score, b.path, b_score))
local config = require("neo-tree").config
if config.sort_function ~= nil then
return config.sort_function(a, b)
end
return nil
end
return a_score > b_score
end
local select_first_file = function()
local is_file = function(node)
return node.type == "file"
end
local files = renderer.select_nodes(state.tree, is_file, 1)
if #files > 0 then
renderer.focus_node(state, files[1]:get_id(), true)
end
end
local has_pre_search_folders = utils.truthy(state.open_folders_before_search)
if not has_pre_search_folders then
log.trace("No search or pre-search folders, recording pre-search folders now")
state.open_folders_before_search = renderer.get_expanded_nodes(state.tree)
end
local waiting_for_default_value = utils.truthy(state.search_pattern)
local input = Input(popup_options, {
prompt = " ",
default_value = state.search_pattern,
on_submit = function(value)
if value == "" then
fs.reset_search(state)
else
if search_as_you_type and fuzzy_finder_mode then
fs.reset_search(state, true, true)
return
end
state.search_pattern = value
manager.refresh("filesystem", function()
-- focus first file
local nodes = renderer.get_all_visible_nodes(state.tree)
for _, node in ipairs(nodes) do
if node.type == "file" then
renderer.focus_node(state, node:get_id(), false)
break
end
end
end)
end
end,
--this can be bad in a deep folder structure
on_change = function(value)
if not search_as_you_type then
return
end
-- apparently when a default value is set, on_change fires for every character
if waiting_for_default_value then
if #value < #state.search_pattern then
return
else
waiting_for_default_value = false
end
end
if value == state.search_pattern then
return
elseif value == nil then
return
elseif value == "" then
if state.search_pattern == nil then
return
end
log.trace("Resetting search in on_change")
local original_open_folders = nil
if type(state.open_folders_before_search) == "table" then
original_open_folders = vim.deepcopy(state.open_folders_before_search, { noref = 1 })
end
fs.reset_search(state)
state.open_folders_before_search = original_open_folders
else
log.trace("Setting search in on_change to: " .. value)
state.search_pattern = value
state.fuzzy_finder_mode = fuzzy_finder_mode
if use_fzy then
state.sort_function_override = sort_by_score
state.use_fzy = true
end
local callback = select_first_file
if fuzzy_finder_mode == "directory" then
callback = nil
end
local len = #value
local delay = 500
if len > 3 then
delay = 100
elseif len > 2 then
delay = 200
elseif len > 1 then
delay = 400
end
utils.debounce("filesystem_filter", function()
fs._navigate_internal(state, nil, nil, callback)
end, delay, utils.debounce_strategy.CALL_LAST_ONLY)
end
end,
})
input:mount()
local restore_height = vim.schedule_wrap(function()
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_set_height(winid, height)
end
end)
input:map("i", "<esc>", function(bufnr)
vim.cmd("stopinsert")
input:unmount()
if fuzzy_finder_mode and utils.truthy(state.search_pattern) then
fs.reset_search(state, true)
end
restore_height()
end, { noremap = true })
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
input:on({ event.BufLeave, event.BufDelete }, function()
vim.cmd("stopinsert")
input:unmount()
-- If this was closed due to submit, that function will handle the reset_search
vim.defer_fn(function()
if fuzzy_finder_mode and utils.truthy(state.search_pattern) then
fs.reset_search(state, true)
end
end, 100)
restore_height()
end, { once = true })
if fuzzy_finder_mode then
local config = require("neo-tree").config
for lhs, cmd_name in pairs(config.filesystem.window.fuzzy_finder_mappings) do
local cmd = cmds[cmd_name]
if cmd then
input:map("i", lhs, create_input_mapping_handle(cmd, state, scroll_padding), { noremap = true })
else
log.warn(string.format('Invalid command in fuzzy_finder_mappings: %s = %s', lhs, cmd_name))
end
end
end
end
return M

View File

@ -0,0 +1,392 @@
local vim = vim
local log = require("neo-tree.log")
local Job = require("plenary.job")
local utils = require("neo-tree.utils")
local Queue = require("neo-tree.collections").Queue
local M = {}
local fd_supports_max_results = nil
local unpack = unpack or table.unpack
local test_for_max_results = function(cmd)
if fd_supports_max_results == nil then
if cmd == "fd" or cmd == "fdfind" then
--test if it supports the max-results option
local test = vim.fn.system(cmd .. " this_is_only_a_test --max-depth=1 --max-results=1")
if test:match("^error:") then
fd_supports_max_results = false
log.debug(cmd, "does NOT support max-results")
else
fd_supports_max_results = true
log.debug(cmd, "supports max-results")
end
end
end
end
local get_find_command = function(state)
if state.find_command then
test_for_max_results(state.find_command)
return state.find_command
end
if 1 == vim.fn.executable("fdfind") then
state.find_command = "fdfind"
elseif 1 == vim.fn.executable("fd") then
state.find_command = "fd"
elseif 1 == vim.fn.executable("find") and vim.fn.has("win32") == 0 then
state.find_command = "find"
elseif 1 == vim.fn.executable("where") then
state.find_command = "where"
end
test_for_max_results(state.find_command)
return state.find_command
end
local running_jobs = Queue:new()
local kill_job = function(job)
local pid = job.pid
job:shutdown()
if pid ~= nil and pid > 0 then
if utils.is_windows then
vim.fn.system("taskkill /F /T /PID " .. pid)
else
vim.fn.system("kill -9 " .. pid)
end
end
return true
end
M.cancel = function()
if running_jobs:is_empty() then
return
end
running_jobs:for_each(kill_job)
end
---@class FileTypes
---@field file boolean
---@field directory boolean
---@field symlink boolean
---@field socket boolean
---@field pipe boolean
---@field executable boolean
---@field empty boolean
---@field block boolean Only for `find`
---@field character boolean Only for `find`
---filter_files_external
-- Spawns a filter command based on `cmd`
---@param cmd string Command to execute. Use `get_find_command` most times.
---@param path string Base directory to start the search.
---@param glob string | nil If not nil, do glob search. Take precedence on `regex`
---@param regex string | nil If not nil, do regex search if command supports. if glob ~= nil, ignored
---@param full_path boolean If true, search agaist the absolute path
---@param types FileTypes | nil Return only true filetypes. If nil, all are returned.
---@param ignore { dotfiles: boolean?, gitignore: boolean? } If true, ignored from result. Default: false
---@param limit? integer | nil Maximim number of results. nil will return everything.
---@param find_args? string[] | table<string, string[]> Any additional options passed to command if any.
---@param on_insert? fun(err: string, line: string): any Executed for each line of stdout and stderr.
---@param on_exit? fun(return_val: table): any Executed at the end.
M.filter_files_external = function(
cmd,
path,
glob,
regex,
full_path,
types,
ignore,
limit,
find_args,
on_insert,
on_exit
)
if glob ~= nil and regex ~= nil then
local log_msg = string.format([[glob: %s, regex: %s]], glob, regex)
log.warn("both glob and regex are set. glob will take precedence. " .. log_msg)
end
ignore = ignore or {}
types = types or {}
limit = limit or math.huge -- math.huge == no limit
local file_type_map = {
file = "f",
directory = "d",
symlink = "l",
socket = "s",
pipe = "p",
executable = "x", -- only for `fd`
empty = "e", -- only for `fd`
block = "b", -- only for `find`
character = "c", -- only for `find`
}
local args = {}
local function append(...)
for _, v in pairs({ ... }) do
if v ~= nil then
args[#args + 1] = v
end
end
end
local function append_find_args()
if find_args then
if type(find_args) == "string" then
append(find_args)
elseif type(find_args) == "table" then
if find_args[1] then
append(unpack(find_args))
elseif find_args[cmd] then
append(unpack(find_args[cmd])) ---@diagnostic disable-line
end
elseif type(find_args) == "function" then
args = find_args(cmd, path, glob, args)
end
end
end
if cmd == "fd" or cmd == "fdfind" then
if not ignore.dotfiles then
append("--hidden")
end
if not ignore.gitignore then
append("--no-ignore")
end
append("--color", "never")
if fd_supports_max_results and 0 < limit and limit < math.huge then
append("--max-results", limit)
end
for k, v in pairs(types) do
if v and file_type_map[k] ~= nil then
append("--type", k)
end
end
if full_path then
append("--full-path")
if glob ~= nil then
local words = utils.split(glob, " ")
regex = ".*" .. table.concat(words, ".*") .. ".*"
glob = nil
end
end
if glob ~= nil then
append("--glob")
end
append_find_args()
append("--", glob or regex or "")
append(path)
elseif cmd == "find" then
append(path)
local file_types = {}
for k, v in pairs(types) do
if v and file_type_map[k] ~= nil then
file_types[#file_types + 1] = file_type_map[k]
end
end
if #file_types > 0 then
append("-type", table.concat(file_types, ","))
end
if types.empty then
append("-empty")
end
if types.executable then
append("-executable")
end
if not ignore.dotfiles then
append("-not", "-path", "*/.*")
end
if glob ~= nil and not full_path then
append("-iname", glob)
elseif glob ~= nil and full_path then
local words = utils.split(glob, " ")
regex = ".*" .. table.concat(words, ".*") .. ".*"
append("-regextype", "sed", "-regex", regex)
elseif regex ~= nil then
append("-regextype", "sed", "-regex", regex)
end
append_find_args()
elseif cmd == "fzf" then
-- This does not work yet, there's some kind of issue with how fzf uses stdout
error("fzf is not a supported find_command")
append_find_args()
append("--no-sort", "--no-expect", "--filter", glob or regex) -- using the raw term without glob patterns
elseif cmd == "where" then
append_find_args()
append("/r", path, glob or regex)
else
return { "No search command found!" }
end
if fd_supports_max_results then
limit = math.huge -- `fd` manages limit on its own
end
local item_count = 0
local job = Job:new({
command = cmd,
cwd = path,
args = args,
enable_recording = false,
on_stdout = function(err, line)
if item_count < limit and on_insert then
on_insert(err, line)
item_count = item_count + 1
end
end,
on_stderr = function(err, line)
if item_count < limit and on_insert then
on_insert(err or line, line)
-- item_count = item_count + 1
end
end,
on_exit = function(_, return_val)
if on_exit then
on_exit(return_val)
end
end,
})
-- This ensures that only one job is running at a time
running_jobs:for_each(kill_job)
running_jobs:add(job)
job:start()
end
local function fzy_sort_get_total_score(terms, path)
local fzy = require("neo-tree.sources.common.filters.filter_fzy")
local total_score = 0
for _, term in ipairs(terms) do -- spaces in `opts.term` are treated as `and`
local score = fzy.score(term, path)
if score == fzy.get_score_min() then -- if any not found, end searching
return 0
end
total_score = total_score + score
end
return total_score
end
local function modify_parent_scores(result_scores, path, score)
local parent, _ = utils.split_path(path)
while parent ~= nil do -- back propagate the score to its ancesters
if score > (result_scores[parent] or 0) then
result_scores[parent] = score
parent, _ = utils.split_path(parent)
else
break
end
end
end
M.fzy_sort_files = function(opts, state)
state = state or {}
local filters = opts.filtered_items
local limit = opts.limit or 100
local full_path_words = opts.find_by_full_path_words
local fuzzy_finder_mode = opts.fuzzy_finder_mode
local pwd = opts.path
if pwd:sub(-1) ~= "/" then
pwd = pwd .. "/"
end
local pwd_length = #pwd
local terms = {}
for term in string.gmatch(opts.term, "[^%s]+") do -- space split opts.term
terms[#terms + 1] = term
end
-- The base search is anything that contains the characters in the term
-- The fzy score is then used to sort the results
local chars = {}
local regex = ".*"
local chars_to_escape =
{ "%", "+", "-", "?", "[", "^", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#" }
for _, term in ipairs(terms) do
for c in term:gmatch(".") do
if not chars[c] then
chars[c] = true
if chars_to_escape[c] then
c = [[\]] .. c
end
regex = regex .. c .. ".*"
end
end
end
local result_counter = 0
local index = 1
state.fzy_sort_result_scores = {}
local function on_insert(err, path)
if not err then
local relative_path = path
if not full_path_words and #path > pwd_length and path:sub(1, pwd_length) == pwd then
relative_path = "./" .. path:sub(pwd_length + 1)
end
index = index + 1
if state.fzy_sort_result_scores == nil then
state.fzy_sort_result_scores = {}
end
state.fzy_sort_result_scores[path] = 0
local score = fzy_sort_get_total_score(terms, relative_path)
if score > 0 then
state.fzy_sort_result_scores[path] = score
result_counter = result_counter + 1
modify_parent_scores(state.fzy_sort_result_scores, path, score)
opts.on_insert(nil, path)
if result_counter >= limit then
vim.schedule(M.cancel)
end
end
end
end
M.filter_files_external(
get_find_command(state),
pwd,
nil,
regex,
true,
{ directory = fuzzy_finder_mode == "directory", file = fuzzy_finder_mode ~= "directory" },
{
dotfiles = not filters.visible and filters.hide_dotfiles,
gitignore = not filters.visible and filters.hide_gitignored,
},
nil,
opts.find_args,
on_insert,
opts.on_exit
)
end
M.find_files = function(opts)
local filters = opts.filtered_items
local full_path_words = opts.find_by_full_path_words
local regex, glob = nil, nil
local fuzzy_finder_mode = opts.fuzzy_finder_mode
glob = opts.term
if glob:sub(1) ~= "*" then
glob = "*" .. glob
end
if glob:sub(-1) ~= "*" then
glob = glob .. "*"
end
M.filter_files_external(
get_find_command(opts),
opts.path,
glob,
regex,
full_path_words,
{ directory = fuzzy_finder_mode == "directory" },
{
dotfiles = not filters.visible and filters.hide_dotfiles,
gitignore = not filters.visible and filters.hide_gitignored,
},
opts.limit or 200,
opts.find_args,
opts.on_insert,
opts.on_exit
)
end
return M

View File

@ -0,0 +1,591 @@
-- This file is for functions that mutate the filesystem.
-- This code started out as a copy from:
-- https://github.com/mhartington/dotfiles
-- and modified to fit neo-tree's api.
-- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua
local vim = vim
local api = vim.api
local loop = vim.loop
local scan = require("plenary.scandir")
local utils = require("neo-tree.utils")
local inputs = require("neo-tree.ui.inputs")
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local Path = require("plenary").path
local M = {}
local function clear_buffer(path)
local buf = utils.find_buffer_by_name(path)
if buf < 1 then
return
end
local alt = vim.fn.bufnr("#")
-- Check all windows to see if they are using the buffer
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then
-- if there is no alternate buffer yet, create a blank one now
if alt < 1 or alt == buf then
alt = vim.api.nvim_create_buf(true, false)
end
-- replace the buffer displayed in this window with the alternate buffer
vim.api.nvim_win_set_buf(win, alt)
end
end
local success, msg = pcall(vim.api.nvim_buf_delete, buf, { force = true })
if not success then
log.error("Could not clear buffer: ", msg)
end
end
---Opens new_buf in each window that has old_buf currently open.
---Useful during file rename.
---@param old_buf number
---@param new_buf number
local function replace_buffer_in_windows(old_buf, new_buf)
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == old_buf then
vim.api.nvim_win_set_buf(win, new_buf)
end
end
end
local function rename_buffer(old_path, new_path)
local force_save = function()
vim.cmd("silent! write!")
end
for _, buf in pairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf)
local new_buf_name = nil
if old_path == buf_name then
new_buf_name = new_path
elseif utils.is_subpath(old_path, buf_name) then
new_buf_name = new_path .. buf_name:sub(#old_path + 1)
end
if utils.truthy(new_buf_name) then
local new_buf = vim.fn.bufadd(new_buf_name)
vim.fn.bufload(new_buf)
vim.api.nvim_buf_set_option(new_buf, "buflisted", true)
replace_buffer_in_windows(buf, new_buf)
if vim.api.nvim_buf_get_option(buf, "buftype") == "" then
local modified = vim.api.nvim_buf_get_option(buf, "modified")
if modified then
local old_buffer_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, old_buffer_lines)
local msg = buf_name .. " has been modified. Save under new name? (y/n) "
inputs.confirm(msg, function(confirmed)
if confirmed then
vim.api.nvim_buf_call(new_buf, force_save)
log.trace("Force saving renamed buffer with changes")
else
vim.cmd("echohl WarningMsg")
vim.cmd(
[[echo "Skipping force save. You'll need to save it with `:w!` when you are ready to force writing with the new name."]]
)
vim.cmd("echohl NONE")
end
end)
end
end
vim.api.nvim_buf_delete(buf, { force = true })
end
end
end
end
local function create_all_parents(path)
local create_all_as_folders
function create_all_as_folders(in_path)
if not loop.fs_stat(in_path) then
local parent, _ = utils.split_path(in_path)
if parent then
create_all_as_folders(parent)
end
loop.fs_mkdir(in_path, 493)
end
end
local parent_path, _ = utils.split_path(path)
create_all_as_folders(parent_path)
end
-- Gets a non-existing filename from the user and executes the callback with it.
local function get_unused_name(
destination,
using_root_directory,
name_chosen_callback,
first_message
)
if loop.fs_stat(destination) then
local parent_path, name
if not using_root_directory then
parent_path, name = utils.split_path(destination)
elseif #using_root_directory > 0 then
parent_path = destination:sub(1, #using_root_directory)
name = destination:sub(#using_root_directory + 2)
else
parent_path = nil
name = destination
end
local message = first_message or name .. " already exists. Please enter a new name: "
inputs.input(message, name, function(new_name)
if new_name and string.len(new_name) > 0 then
local new_path = parent_path and parent_path .. utils.path_separator .. new_name or new_name
get_unused_name(new_path, using_root_directory, name_chosen_callback)
end
end)
else
name_chosen_callback(destination)
end
end
-- Move Node
M.move_node = function(source, destination, callback, using_root_directory)
log.trace(
"Moving node: ",
source,
" to ",
destination,
", using root directory: ",
using_root_directory
)
local _, name = utils.split_path(source)
get_unused_name(destination or source, using_root_directory, function(dest)
local function move_file()
create_all_parents(dest)
loop.fs_rename(source, dest, function(err)
if err then
log.error("Could not move the files from", source, "to", dest, ":", err)
return
end
vim.schedule(function()
rename_buffer(source, dest)
end)
vim.schedule(function()
events.fire_event(events.FILE_MOVED, {
source = source,
destination = dest,
})
if callback then
callback(source, dest)
end
end)
end)
end
local event_result = events.fire_event(events.BEFORE_FILE_MOVE, {
source = source,
destination = dest,
callback = move_file,
}) or {}
if event_result.handled then
return
end
move_file()
end, 'Move "' .. name .. '" to:')
end
---Plenary path.copy() when used to copy a recursive structure, can return a nested
-- table with for each file a Path instance and the success result.
---@param copy_result table The output of Path.copy()
---@param flat_result table Return value containing the flattened results
local function flatten_path_copy_result(flat_result, copy_result)
if not copy_result then
return
end
for k, v in pairs(copy_result) do
if type(v) == "table" then
flatten_path_copy_result(flat_result, v)
else
table.insert(flat_result, { destination = k.filename, success = v })
end
end
end
-- Check if all files were copied successfully, using the flattened copy result
local function check_path_copy_result(flat_result)
if not flat_result then
return
end
for _, file_result in ipairs(flat_result) do
if not file_result.success then
return false
end
end
return true
end
-- Copy Node
M.copy_node = function(source, _destination, callback, using_root_directory)
local _, name = utils.split_path(source)
get_unused_name(_destination or source, using_root_directory, function(destination)
local parent_path, _ = utils.split_path(destination)
if source == parent_path then
log.warn("Cannot copy a file/folder to itself")
return
end
local source_path = Path:new(source)
if source_path:is_file() then
-- When the source is a file, then Path.copy() currently doesn't create
-- the potential non-existing parent directories of the destination.
create_all_parents(destination)
end
local success, result = pcall(source_path.copy, source_path, {
destination = destination,
recursive = true,
parents = true,
})
if not success then
log.error("Could not copy the file(s) from", source, "to", destination, ":", result)
return
end
-- It can happen that the Path.copy() function returns successfully but
-- the copy action still failed. In this case the copy() result contains
-- a nested table of Path instances for each file copied, and the success
-- result.
local flat_result = {}
flatten_path_copy_result(flat_result, result)
if not check_path_copy_result(flat_result) then
log.error("Could not copy the file(s) from", source, "to", destination, ":", flat_result)
return
end
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(source, destination)
end
end)
end, 'Copy "' .. name .. '" to:')
end
--- Create a new directory
M.create_directory = function(in_directory, callback, using_root_directory)
local base
if type(using_root_directory) == "string" then
if in_directory == using_root_directory then
base = ""
elseif #using_root_directory > 0 then
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
else
base = in_directory .. utils.path_separator
end
else
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
using_root_directory = false
end
inputs.input("Enter name for new directory:", base, function(destinations)
if not destinations then
return
end
for _, destination in ipairs(utils.brace_expand(destinations)) do
if not destination or destination == base then
return
end
if using_root_directory then
destination = utils.path_join(using_root_directory, destination)
else
destination = vim.fn.fnamemodify(destination, ":p")
end
if loop.fs_stat(destination) then
log.warn("Directory already exists")
return
end
create_all_parents(destination)
loop.fs_mkdir(destination, 493)
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(destination)
end
end)
end
end)
end
--- Create Node
M.create_node = function(in_directory, callback, using_root_directory)
local base
if type(using_root_directory) == "string" then
if in_directory == using_root_directory then
base = ""
elseif #using_root_directory > 0 then
base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator
else
base = in_directory .. utils.path_separator
end
else
base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~")
using_root_directory = false
end
inputs.input(
'Enter name for new file or directory (dirs end with a "/"):',
base,
function(destinations)
if not destinations then
return
end
for _, destination in ipairs(utils.brace_expand(destinations)) do
if not destination or destination == base then
return
end
local is_dir = vim.endswith(destination, "/")
if using_root_directory then
destination = utils.path_join(using_root_directory, destination)
else
destination = vim.fn.fnamemodify(destination, ":p")
end
if loop.fs_stat(destination) then
log.warn("File already exists")
return
end
create_all_parents(destination)
if is_dir then
loop.fs_mkdir(destination, 493)
else
local open_mode = loop.constants.O_CREAT
+ loop.constants.O_WRONLY
+ loop.constants.O_TRUNC
local fd = loop.fs_open(destination, "w", open_mode)
if not fd then
if not loop.fs_stat(destination) then
api.nvim_err_writeln("Could not create file " .. destination)
return
else
log.warn("Failed to complete file creation of " .. destination)
end
else
loop.fs_chmod(destination, 420)
loop.fs_close(fd)
end
end
vim.schedule(function()
events.fire_event(events.FILE_ADDED, destination)
if callback then
callback(destination)
end
end)
end
end
)
end
-- Delete Node
M.delete_node = function(path, callback, noconfirm)
local _, name = utils.split_path(path)
local msg = string.format("Are you sure you want to delete '%s'?", name)
log.trace("Deleting node: ", path)
local _type = "unknown"
local stat = loop.fs_stat(path)
if stat then
_type = stat.type
if _type == "link" then
local link_to = loop.fs_readlink(path)
if not link_to then
log.error("Could not read link")
return
end
_type = loop.fs_stat(link_to)
end
if _type == "directory" then
local children = scan.scan_dir(path, {
hidden = true,
respect_gitignore = false,
add_dirs = true,
depth = 1,
})
if #children > 0 then
msg = "WARNING: Dir not empty! " .. msg
end
end
else
log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...")
-- Guess the type by whether it appears to have an extension
if path:match("%.(.+)$") then
_type = "file"
else
_type = "directory"
end
return
end
local do_delete = function(confirmed)
if not confirmed then
return
end
local function delete_dir(dir_path)
local handle = loop.fs_scandir(dir_path)
if type(handle) == "string" then
return api.nvim_err_writeln(handle)
end
while true do
local child_name, t = loop.fs_scandir_next(handle)
if not child_name then
break
end
local child_path = dir_path .. "/" .. child_name
if t == "directory" then
local success = delete_dir(child_path)
if not success then
log.error("failed to delete ", child_path)
return false
end
else
local success = loop.fs_unlink(child_path)
if not success then
return false
end
clear_buffer(child_path)
end
end
return loop.fs_rmdir(dir_path)
end
if _type == "directory" then
-- first try using native system commands, which are recursive
local success = false
if utils.is_windows then
local result = vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", path })
local error = vim.v.shell_error
if error ~= 0 then
log.debug("Could not delete directory '", path, "' with rmdir: ", result)
else
log.info("Deleted directory ", path)
success = true
end
else
local result = vim.fn.system({ "rm", "-Rf", path })
local error = vim.v.shell_error
if error ~= 0 then
log.debug("Could not delete directory '", path, "' with rm: ", result)
else
log.info("Deleted directory ", path)
success = true
end
end
-- Fallback to using libuv if native commands fail
if not success then
success = delete_dir(path)
if not success then
return api.nvim_err_writeln("Could not remove directory: " .. path)
end
end
else
local success = loop.fs_unlink(path)
if not success then
return api.nvim_err_writeln("Could not remove file: " .. path)
end
clear_buffer(path)
end
vim.schedule(function()
events.fire_event(events.FILE_DELETED, path)
if callback then
callback(path)
end
end)
end
if noconfirm then
do_delete(true)
else
inputs.confirm(msg, do_delete)
end
end
M.delete_nodes = function(paths_to_delete, callback)
local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?"
inputs.confirm(msg, function(confirmed)
if not confirmed then
return
end
for _, path in ipairs(paths_to_delete) do
M.delete_node(path, nil, true)
end
if callback then
vim.schedule(function()
callback(paths_to_delete[#paths_to_delete])
end)
end
end)
end
-- Rename Node
M.rename_node = function(path, callback)
local parent_path, name = utils.split_path(path)
local msg = string.format('Enter new name for "%s":', name)
inputs.input(msg, name, function(new_name)
-- If cancelled
if not new_name or new_name == "" then
log.info("Operation canceled")
return
end
local destination = parent_path .. utils.path_separator .. new_name
-- If aleady exists
if loop.fs_stat(destination) then
log.warn(destination, " already exists")
return
end
local complete = vim.schedule_wrap(function()
rename_buffer(path, destination)
events.fire_event(events.FILE_RENAMED, {
source = path,
destination = destination,
})
if callback then
callback(path, destination)
end
log.info("Renamed " .. new_name .. " successfully")
end)
local function fs_rename()
loop.fs_rename(path, destination, function(err)
if err then
log.warn("Could not rename the files")
return
else
complete()
end
end)
end
local event_result = events.fire_event(events.BEFORE_FILE_RENAME, {
source = path,
destination = destination,
callback = fs_rename,
}) or {}
if event_result.handled then
return
end
fs_rename()
end)
end
return M

View File

@ -0,0 +1,505 @@
-- This files holds code for scanning the filesystem to build the tree.
local uv = vim.loop
local renderer = require("neo-tree.ui.renderer")
local utils = require("neo-tree.utils")
local filter_external = require("neo-tree.sources.filesystem.lib.filter_external")
local file_items = require("neo-tree.sources.common.file-items")
local log = require("neo-tree.log")
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
local git = require("neo-tree.git")
local events = require("neo-tree.events")
local async = require("plenary.async")
local Path = require("plenary.path")
local os_sep = Path.path.sep
local M = {}
local on_directory_loaded = function(context, dir_path)
local state = context.state
local scanned_folder = context.folders[dir_path]
if scanned_folder then
scanned_folder.loaded = true
end
if state.use_libuv_file_watcher then
local root = context.folders[dir_path]
if root then
local target_path = root.is_link and root.link_to or root.path
local fs_watch_callback = vim.schedule_wrap(function(err, fname)
if err then
log.error("file_event_callback: ", err)
return
end
if context.is_a_never_show_file(fname) then
-- don't fire events for nodes that are designated as "never show"
return
else
events.fire_event(events.FS_EVENT, { afile = target_path })
end
end)
log.trace("Adding fs watcher for ", target_path)
fs_watch.watch_folder(target_path, fs_watch_callback)
end
end
end
local dir_complete = function(context, dir_path)
local paths_to_load = context.paths_to_load
local folders = context.folders
on_directory_loaded(context, dir_path)
-- check to see if there are more folders to load
local next_path = nil
while #paths_to_load > 0 and not next_path do
next_path = table.remove(paths_to_load)
-- ensure that the path is still valid
local success, result = pcall(vim.loop.fs_stat, next_path)
-- ensure that the result is a directory
if success and result and result.type == "directory" then
-- ensure that it is not already loaded
local existing = folders[next_path]
if existing and existing.loaded then
next_path = nil
end
else
-- if the path doesn't exist, skip it
next_path = nil
end
end
return next_path
end
local render_context = function(context)
local state = context.state
local root = context.root
local parent_id = context.parent_id
if not parent_id and state.use_libuv_file_watcher and state.enable_git_status then
log.trace("Starting .git folder watcher")
local path = root.path
if root.is_link then
path = root.link_to
end
fs_watch.watch_git_index(path, require("neo-tree").config.git_status_async)
end
fs_watch.updated_watched()
if root and root.children then
file_items.deep_sort(root.children, state.sort_function_override)
end
if parent_id then
-- lazy loading a child folder
renderer.show_nodes(root.children, state, parent_id, context.callback)
else
-- full render of the tree
renderer.show_nodes({ root }, state, nil, context.callback)
end
context.state = nil
context.callback = nil
context.all_items = nil
context.root = nil
context.parent_id = nil
context = nil
end
local job_complete = function(context)
local state = context.state
local parent_id = context.parent_id
if #context.all_items == 0 then
log.info("No items, skipping git ignored/status lookups")
render_context(context)
return
end
if state.filtered_items.hide_gitignored or state.enable_git_status then
if require("neo-tree").config.git_status_async then
git.mark_ignored(state, context.all_items, function(all_items)
if parent_id then
vim.list_extend(state.git_ignored, all_items)
else
state.git_ignored = all_items
end
vim.schedule(function()
render_context(context)
end)
end)
return
else
local all_items = git.mark_ignored(state, context.all_items)
if parent_id then
vim.list_extend(state.git_ignored, all_items)
else
state.git_ignored = all_items
end
end
end
render_context(context)
end
local function create_node(context, node)
local success3, item = pcall(file_items.create_item, context, node.path, node.type)
end
local function process_node(context, path)
on_directory_loaded(context, path)
end
local function get_children_sync(path)
local children = {}
local success, dir = pcall(vim.loop.fs_opendir, path, nil, 1000)
if not success then
log.error("Error opening dir:", dir)
end
local success2, stats = pcall(vim.loop.fs_readdir, dir)
if success2 and stats then
for _, stat in ipairs(stats) do
local child_path = utils.path_join(path, stat.name)
table.insert(children, { path = child_path, type = stat.type })
end
end
pcall(vim.loop.fs_closedir, dir)
return children
end
local function get_children_async(path, callback)
uv.fs_opendir(path, function(_, dir)
uv.fs_readdir(dir, function(_, stats)
local children = {}
if stats then
for _, stat in ipairs(stats) do
local child_path = utils.path_join(path, stat.name)
table.insert(children, { path = child_path, type = stat.type })
end
end
uv.fs_closedir(dir)
callback(children)
end)
end, 1000)
end
local function scan_dir_sync(context, path)
process_node(context, path)
local children = get_children_sync(path)
for _, child in ipairs(children) do
create_node(context, child)
if child.type == "directory" then
local grandchild_nodes = get_children_sync(child.path)
if
grandchild_nodes == nil
or #grandchild_nodes == 0
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
then
scan_dir_sync(context, child.path)
end
end
end
end
local function scan_dir_async(context, path, callback)
get_children_async(path, function(children)
for _, child in ipairs(children) do
create_node(context, child)
if child.type == "directory" then
local grandchild_nodes = get_children_sync(child.path)
if
grandchild_nodes == nil
or #grandchild_nodes == 0
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
then
scan_dir_sync(context, child.path)
end
end
end
process_node(context, path)
callback(path)
end)
end
-- async_scan scans all the directories in context.paths_to_load
-- and adds them as items to render in the UI.
local function async_scan(context, path)
log.trace("async_scan: ", path)
local scan_mode = require("neo-tree").config.filesystem.scan_mode
if scan_mode == "deep" then
local scan_tasks = {}
for _, p in ipairs(context.paths_to_load) do
local scan_task = async.wrap(function(callback)
scan_dir_async(context, p, callback)
end, 1)
table.insert(scan_tasks, scan_task)
end
async.util.run_all(
scan_tasks,
vim.schedule_wrap(function()
job_complete(context)
end)
)
return
end
-- scan_mode == "shallow"
context.directories_scanned = 0
context.directories_to_scan = #context.paths_to_load
context.on_exit = vim.schedule_wrap(function()
job_complete(context)
end)
-- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua
local function read_dir(current_dir, ctx)
uv.fs_opendir(current_dir, function(err, dir)
if err then
log.error(current_dir, ": ", err)
return
end
local function on_fs_readdir(err, entries)
if err then
log.error(current_dir, ": ", err)
return
end
if entries then
for _, entry in ipairs(entries) do
local success, item = pcall(
file_items.create_item,
ctx,
utils.path_join(current_dir, entry.name),
entry.type
)
if success then
if ctx.recursive and item.type == "directory" then
ctx.directories_to_scan = ctx.directories_to_scan + 1
table.insert(ctx.paths_to_load, item.path)
end
else
log.error("error creating item for ", path)
end
end
uv.fs_readdir(dir, on_fs_readdir)
return
end
uv.fs_closedir(dir)
on_directory_loaded(ctx, current_dir)
ctx.directories_scanned = ctx.directories_scanned + 1
if ctx.directories_scanned == #ctx.paths_to_load then
ctx.on_exit()
end
--local next_path = dir_complete(ctx, current_dir)
--if next_path then
-- local success, error = pcall(read_dir, next_path)
-- if not success then
-- log.error(next_path, ": ", error)
-- end
--else
-- on_exit()
--end
end
uv.fs_readdir(dir, on_fs_readdir)
end)
end
--local first = table.remove(context.paths_to_load)
--local success, err = pcall(read_dir, first)
--if not success then
-- log.error(first, ": ", err)
--end
for i = 1, context.directories_to_scan do
read_dir(context.paths_to_load[i], context)
end
end
local function sync_scan(context, path_to_scan)
log.trace("sync_scan: ", path_to_scan)
local scan_mode = require("neo-tree").config.filesystem.scan_mode
if scan_mode == "deep" then
for _, path in ipairs(context.paths_to_load) do
scan_dir_sync(context, path)
-- scan_dir(context, path)
end
job_complete(context)
else -- scan_mode == "shallow"
local success, dir = pcall(vim.loop.fs_opendir, path_to_scan, nil, 1000)
if not success then
log.error("Error opening dir:", dir)
end
local success2, stats = pcall(vim.loop.fs_readdir, dir)
if success2 and stats then
for _, stat in ipairs(stats) do
local path = utils.path_join(path_to_scan, stat.name)
local success3, item = pcall(file_items.create_item, context, path, stat.type)
if success3 then
if context.recursive and stat.type == "directory" then
table.insert(context.paths_to_load, path)
end
else
log.error("error creating item for ", path)
end
end
end
vim.loop.fs_closedir(dir)
local next_path = dir_complete(context, path_to_scan)
if next_path then
sync_scan(context, next_path)
else
job_complete(context)
end
end
end
M.get_items_sync = function(state, parent_id, path_to_reveal, callback)
return M.get_items(state, parent_id, path_to_reveal, callback, false)
end
M.get_items_async = function(state, parent_id, path_to_reveal, callback)
M.get_items(state, parent_id, path_to_reveal, callback, true)
end
M.get_items = function(state, parent_id, path_to_reveal, callback, async, recursive)
if state.async_directory_scan == "always" then
async = true
elseif state.async_directory_scan == "never" then
async = false
elseif type(async) == "nil" then
async = (state.async_directory_scan == "auto") or state.async_directory_scan
end
if not parent_id then
M.stop_watchers(state)
end
local context = file_items.create_context()
context.state = state
context.parent_id = parent_id
context.path_to_reveal = path_to_reveal
context.recursive = recursive
context.callback = callback
-- Create root folder
local root = file_items.create_item(context, parent_id or state.path, "directory")
root.name = vim.fn.fnamemodify(root.path, ":~")
root.loaded = true
root.search_pattern = state.search_pattern
context.root = root
context.folders[root.path] = root
state.default_expanded_nodes = state.force_open_folders or { state.path }
if state.search_pattern then
local search_opts = {
filtered_items = state.filtered_items,
find_command = state.find_command,
limit = state.search_limit or 50,
path = root.path,
term = state.search_pattern,
find_args = state.find_args,
find_by_full_path_words = state.find_by_full_path_words,
fuzzy_finder_mode = state.fuzzy_finder_mode,
on_insert = function(err, path)
if err then
log.debug(err)
else
file_items.create_item(context, path)
end
end,
on_exit = vim.schedule_wrap(function()
job_complete(context)
end),
}
if state.use_fzy then
filter_external.fzy_sort_files(search_opts, state)
else
-- Use the external command because the plenary search is slow
filter_external.find_files(search_opts)
end
else
-- In the case of a refresh or navigating up, we need to make sure that all
-- open folders are loaded.
local path = parent_id or state.path
context.paths_to_load = {}
if parent_id == nil then
if utils.truthy(state.force_open_folders) then
for _, f in ipairs(state.force_open_folders) do
table.insert(context.paths_to_load, f)
end
elseif state.tree then
context.paths_to_load = renderer.get_expanded_nodes(state.tree, state.path)
end
-- Ensure that there are no nested files in the list of folders to load
context.paths_to_load = vim.tbl_filter(function(p)
local stats = vim.loop.fs_stat(p)
return stats and stats.type == "directory"
end, context.paths_to_load)
if path_to_reveal then
-- be sure to load all of the folders leading up to the path to reveal
local path_to_reveal_parts = utils.split(path_to_reveal, utils.path_separator)
table.remove(path_to_reveal_parts) -- remove the file name
-- add all parent folders to the list of paths to load
utils.reduce(path_to_reveal_parts, "", function(acc, part)
local current_path = utils.path_join(acc, part)
if #current_path > #path then -- within current root
table.insert(context.paths_to_load, current_path)
table.insert(state.default_expanded_nodes, current_path)
end
return current_path
end)
context.paths_to_load = utils.unique(context.paths_to_load)
end
end
local filtered_items = state.filtered_items or {}
context.is_a_never_show_file = function(fname)
if fname then
local _, name = utils.split_path(fname)
if name then
if filtered_items.never_show and filtered_items.never_show[name] then
return true
end
if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then
return true
end
end
end
return false
end
table.insert(context.paths_to_load, path)
if async then
async_scan(context, path)
else
sync_scan(context, path)
end
end
end
M.stop_watchers = function(state)
if state.use_libuv_file_watcher and state.tree then
-- We are loaded a new root or refreshing, unwatch any folders that were
-- previously being watched.
local loaded_folders = renderer.select_nodes(state.tree, function(node)
return node.type == "directory" and node.loaded
end)
fs_watch.unwatch_git_index(state.path, require("neo-tree").config.git_status_async)
for _, folder in ipairs(loaded_folders) do
log.trace("Unwatching folder ", folder.path)
if folder.is_link then
fs_watch.unwatch_folder(folder.link_to)
else
fs_watch.unwatch_folder(folder:get_id())
end
end
else
log.debug(
"Not unwatching folders... use_libuv_file_watcher is ",
state.use_libuv_file_watcher,
" and state.tree is ",
utils.truthy(state.tree)
)
end
end
return M

View File

@ -0,0 +1,177 @@
local vim = vim
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local git = require("neo-tree.git")
local utils = require("neo-tree.utils")
local M = {}
local flags = {
watch_entry = false,
stat = false,
recursive = false,
}
local watched = {}
local get_dot_git_folder = function(path, callback)
if type(callback) == "function" then
git.get_repository_root(path, function(git_root)
if git_root then
local git_folder = utils.path_join(git_root, ".git")
local stat = vim.loop.fs_stat(git_folder)
if stat and stat.type == "directory" then
callback(git_folder, git_root)
end
else
callback(nil, nil)
end
end)
else
local git_root = git.get_repository_root(path)
if git_root then
local git_folder = utils.path_join(git_root, ".git")
local stat = vim.loop.fs_stat(git_folder)
if stat and stat.type == "directory" then
return git_folder, git_root
end
end
return nil, nil
end
end
M.show_watched = function()
local items = {}
for _, handle in pairs(watched) do
items[handle.path] = handle.references
end
log.info("Watched Folders: ", vim.inspect(items))
end
---Watch a directory for changes to it's children. Not recursive.
---@param path string The directory to watch.
---@param custom_callback? function The callback to call when a change is detected.
---@param allow_git_watch? boolean Allow watching of git folders.
M.watch_folder = function(path, custom_callback, allow_git_watch)
if not allow_git_watch then
if path:find("/%.git$") or path:find("/%.git/") then
-- git folders seem to throw off fs events constantly.
log.debug("watch_folder(path): Skipping git folder: ", path)
return
end
end
local h = watched[path]
if h == nil then
log.trace("Starting new fs watch on: ", path)
local callback = custom_callback
or vim.schedule_wrap(function(err, fname)
if fname and fname:match("^%.null[-]ls_.+") then
-- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
return
end
if err then
log.error("file_event_callback: ", err)
return
end
events.fire_event(events.FS_EVENT, { afile = path })
end)
h = {
handle = vim.loop.new_fs_event(),
path = path,
references = 0,
active = false,
callback = callback,
}
watched[path] = h
--w:start(path, flags, callback)
else
log.trace("Incrementing references for fs watch on: ", path)
end
h.references = h.references + 1
end
M.watch_git_index = function(path, async)
local function watch_git_folder(git_folder, git_root)
if git_folder then
local git_event_callback = vim.schedule_wrap(function(err, fname)
if fname and fname:match("^.+%.lock$") then
return
end
if fname and fname:match("^%._null-ls_.+") then
-- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075
return
end
if err then
log.error("git_event_callback: ", err)
return
end
events.fire_event(events.GIT_EVENT, { path = fname, repository = git_root })
end)
M.watch_folder(git_folder, git_event_callback, true)
end
end
if async then
get_dot_git_folder(path, watch_git_folder)
else
watch_git_folder(get_dot_git_folder(path))
end
end
M.updated_watched = function()
for path, w in pairs(watched) do
if w.references > 0 then
if not w.active then
log.trace("References added for fs watch on: ", path, ", starting.")
w.handle:start(path, flags, w.callback)
w.active = true
end
else
if w.active then
log.trace("No more references for fs watch on: ", path, ", stopping.")
w.handle:stop()
w.active = false
end
end
end
end
---Stop watching a directory. If there are no more references to the handle,
---it will be destroyed. Otherwise, the reference count will be decremented.
---@param path string The directory to stop watching.
M.unwatch_folder = function(path, callback_id)
local h = watched[path]
if h then
log.trace("Decrementing references for fs watch on: ", path, callback_id)
h.references = h.references - 1
else
log.trace("(unwatch_folder) No fs watch found for: ", path)
end
end
M.unwatch_git_index = function(path, async)
local function unwatch_git_folder(git_folder, _)
if git_folder then
M.unwatch_folder(git_folder)
end
end
if async then
get_dot_git_folder(path, unwatch_git_folder)
else
unwatch_git_folder(get_dot_git_folder(path))
end
end
---Stop watching all directories. This is the nuclear option and it affects all
---sources.
M.unwatch_all = function()
for _, h in pairs(watched) do
h.handle:stop()
h.handle = nil
end
watched = {}
end
return M

View File

@ -0,0 +1,157 @@
--(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
--Permission is hereby granted, free of charge, to any person obtaining a copy
--of this software and associated documentation files (the "Software"), to deal
--in the Software without restriction, including without limitation the rights
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
--The above copyright notice and this permission notice shall be included in
--all copies or substantial portions of the Software.
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
--THE SOFTWARE.
--(end license)
local M = { _TYPE = "module", _NAME = "globtopattern", _VERSION = "0.2.1.20120406" }
function M.globtopattern(g)
-- Some useful references:
-- - apr_fnmatch in Apache APR. For example,
-- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
-- which cites POSIX 1003.2-1992, section B.6.
local p = "^" -- pattern being built
local i = 0 -- index in g
local c -- char at index i in g.
-- unescape glob char
local function unescape()
if c == "\\" then
i = i + 1
c = g:sub(i, i)
if c == "" then
p = "[^]"
return false
end
end
return true
end
-- escape pattern char
local function escape(c)
return c:match("^%w$") and c or "%" .. c
end
-- Convert tokens at end of charset.
local function charset_end()
while 1 do
if c == "" then
p = "[^]"
return false
elseif c == "]" then
p = p .. "]"
break
else
if not unescape() then
break
end
local c1 = c
i = i + 1
c = g:sub(i, i)
if c == "" then
p = "[^]"
return false
elseif c == "-" then
i = i + 1
c = g:sub(i, i)
if c == "" then
p = "[^]"
return false
elseif c == "]" then
p = p .. escape(c1) .. "%-]"
break
else
if not unescape() then
break
end
p = p .. escape(c1) .. "-" .. escape(c)
end
elseif c == "]" then
p = p .. escape(c1) .. "]"
break
else
p = p .. escape(c1)
i = i - 1 -- put back
end
end
i = i + 1
c = g:sub(i, i)
end
return true
end
-- Convert tokens in charset.
local function charset()
i = i + 1
c = g:sub(i, i)
if c == "" or c == "]" then
p = "[^]"
return false
elseif c == "^" or c == "!" then
i = i + 1
c = g:sub(i, i)
if c == "]" then
-- ignored
else
p = p .. "[^"
if not charset_end() then
return false
end
end
else
p = p .. "["
if not charset_end() then
return false
end
end
return true
end
-- Convert tokens.
while 1 do
i = i + 1
c = g:sub(i, i)
if c == "" then
p = p .. "$"
break
elseif c == "?" then
p = p .. "."
elseif c == "*" then
p = p .. ".*"
elseif c == "[" then
if not charset() then
break
end
elseif c == "\\" then
i = i + 1
c = g:sub(i, i)
if c == "" then
p = p .. "\\$"
break
end
p = p .. escape(c)
else
p = p .. escape(c)
end
end
return p
end
return M

View File

@ -0,0 +1,71 @@
--This file should contain all commands meant to be used by mappings.
local vim = vim
local cc = require("neo-tree.sources.common.commands")
local utils = require("neo-tree.utils")
local manager = require("neo-tree.sources.manager")
local M = {}
local refresh = utils.wrap(manager.refresh, "git_status")
local redraw = utils.wrap(manager.redraw, "git_status")
-- ----------------------------------------------------------------------------
-- Common commands
-- ----------------------------------------------------------------------------
M.add = function(state)
cc.add(state, refresh)
end
M.add_directory = function(state)
cc.add_directory(state, refresh)
end
---Marks node as copied, so that it can be pasted somewhere else.
M.copy_to_clipboard = function(state)
cc.copy_to_clipboard(state, redraw)
end
M.copy_to_clipboard_visual = function(state, selected_nodes)
cc.copy_to_clipboard_visual(state, selected_nodes, redraw)
end
---Marks node as cut, so that it can be pasted (moved) somewhere else.
M.cut_to_clipboard = function(state)
cc.cut_to_clipboard(state, redraw)
end
M.cut_to_clipboard_visual = function(state, selected_nodes)
cc.cut_to_clipboard_visual(state, selected_nodes, redraw)
end
M.copy = function(state)
cc.copy(state, redraw)
end
M.move = function(state)
cc.move(state, redraw)
end
---Pastes all items from the clipboard to the current directory.
M.paste_from_clipboard = function(state)
cc.paste_from_clipboard(state, refresh)
end
M.delete = function(state)
cc.delete(state, refresh)
end
M.delete_visual = function(state, selected_nodes)
cc.delete_visual(state, selected_nodes, refresh)
end
M.refresh = refresh
M.rename = function(state)
cc.rename(state, refresh)
end
cc._add_common_commands(M)
return M

View File

@ -0,0 +1,44 @@
-- This file contains the built-in components. Each componment is a function
-- that takes the following arguments:
-- config: A table containing the configuration provided by the user
-- when declaring this component in their renderer config.
-- node: A NuiNode object for the currently focused node.
-- state: The current state of the source providing the items.
--
-- The function should return either a table, or a list of tables, each of which
-- contains the following keys:
-- text: The text to display for this item.
-- highlight: The highlight group to apply to this text.
local highlights = require("neo-tree.ui.highlights")
local common = require("neo-tree.sources.common.components")
local M = {}
M.name = function(config, node, state)
local highlight = config.highlight or highlights.FILE_NAME_OPENED
local name = node.name
if node.type == "directory" then
if node:get_depth() == 1 then
highlight = highlights.ROOT_NAME
if node:has_children() then
name = "GIT STATUS for " .. name
else
name = "GIT STATUS (working tree clean) for " .. name
end
else
highlight = highlights.DIRECTORY_NAME
end
elseif config.use_git_status_colors then
local git_status = state.components.git_status({}, node, state)
if git_status and git_status.highlight then
highlight = git_status.highlight
end
end
return {
text = name,
highlight = highlight,
}
end
return vim.tbl_deep_extend("force", common, M)

View File

@ -0,0 +1,94 @@
--This file should have all functions that are in the public api and either set
--or read the state of this source.
local vim = vim
local utils = require("neo-tree.utils")
local renderer = require("neo-tree.ui.renderer")
local items = require("neo-tree.sources.git_status.lib.items")
local events = require("neo-tree.events")
local manager = require("neo-tree.sources.manager")
local M = {
name = "git_status",
display_name = "  Git "
}
local wrap = function(func)
return utils.wrap(func, M.name)
end
local get_state = function()
return manager.get_state(M.name)
end
---Navigate to the given path.
---@param path string Path to navigate to. If empty, will navigate to the cwd.
M.navigate = function(state, path, path_to_reveal)
state.dirty = false
if path_to_reveal then
renderer.position.set(state, path_to_reveal)
end
items.get_git_status(state)
end
M.refresh = function()
manager.refresh(M.name)
end
---Configures the plugin, should be called before the plugin is used.
---@param config table Configuration table containing any keys that the user
--wants to change from the defaults. May be empty to accept default values.
M.setup = function(config, global_config)
if config.before_render then
--convert to new event system
manager.subscribe(M.name, {
event = events.BEFORE_RENDER,
handler = function(state)
local this_state = get_state()
if state == this_state then
config.before_render(this_state)
end
end,
})
end
if global_config.enable_refresh_on_write then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_CHANGED,
handler = function(args)
if utils.is_real_file(args.afile) then
M.refresh()
end
end,
})
end
if config.bind_to_cwd then
manager.subscribe(M.name, {
event = events.VIM_DIR_CHANGED,
handler = M.refresh,
})
end
if global_config.enable_diagnostics then
manager.subscribe(M.name, {
event = events.VIM_DIAGNOSTIC_CHANGED,
handler = wrap(manager.diagnostics_changed),
})
end
--Configure event handlers for modified files
if global_config.enable_modified_markers then
manager.subscribe(M.name, {
event = events.VIM_BUFFER_MODIFIED_SET,
handler = wrap(manager.opened_buffers_changed),
})
end
manager.subscribe(M.name, {
event = events.GIT_EVENT,
handler = M.refresh,
})
end
return M

View File

@ -0,0 +1,49 @@
local vim = vim
local renderer = require("neo-tree.ui.renderer")
local file_items = require("neo-tree.sources.common.file-items")
local log = require("neo-tree.log")
local git = require("neo-tree.git")
local M = {}
---Get a table of all open buffers, along with all parent paths of those buffers.
---The paths are the keys of the table, and all the values are 'true'.
M.get_git_status = function(state)
if state.loading then
return
end
state.loading = true
local status_lookup, project_root = git.status(state.git_base, true)
state.path = project_root or state.path or vim.fn.getcwd()
local context = file_items.create_context()
context.state = state
-- Create root folder
local root = file_items.create_item(context, state.path, "directory")
root.name = vim.fn.fnamemodify(root.path, ":~")
root.loaded = true
root.search_pattern = state.search_pattern
context.folders[root.path] = root
for path, status in pairs(status_lookup) do
local success, item = pcall(file_items.create_item, context, path, "file")
item.status = status
if success then
item.extra = {
git_status = status,
}
else
log.error("Error creating item for " .. path .. ": " .. item)
end
end
state.git_status_lookup = status_lookup
state.default_expanded_nodes = {}
for id, _ in pairs(context.folders) do
table.insert(state.default_expanded_nodes, id)
end
file_items.deep_sort(root.children, state.sort_function_override)
renderer.show_nodes({ root }, state)
state.loading = false
end
return M

View File

@ -0,0 +1,644 @@
--This file should have all functions that are in the public api and either set
--or read the state of this source.
local vim = vim
local utils = require("neo-tree.utils")
local fs_scan = require("neo-tree.sources.filesystem.lib.fs_scan")
local renderer = require("neo-tree.ui.renderer")
local inputs = require("neo-tree.ui.inputs")
local events = require("neo-tree.events")
local log = require("neo-tree.log")
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
local M = {}
local source_data = {}
local all_states = {}
local default_configs = {}
local get_source_data = function(source_name)
if source_name == nil then
error("get_source_data: source_name cannot be nil")
end
local sd = source_data[source_name]
if sd then
return sd
end
sd = {
name = source_name,
state_by_tab = {},
state_by_win = {},
subscriptions = {},
}
source_data[source_name] = sd
return sd
end
local function create_state(tabid, sd, winid)
local default_config = default_configs[sd.name]
local state = vim.deepcopy(default_config, { noref = 1 })
state.tabid = tabid
state.id = winid or tabid
state.dirty = true
state.position = {
is = { restorable = false },
}
state.git_base = "HEAD"
table.insert(all_states, state)
return state
end
M._get_all_states = function()
return all_states
end
M._for_each_state = function(source_name, action)
for _, state in ipairs(all_states) do
if source_name == nil or state.name == source_name then
action(state)
end
end
end
---For use in tests only, completely resets the state of all sources.
---This closes all windows as well since they would be broken by this action.
M._clear_state = function()
fs_watch.unwatch_all()
renderer.close_all_floating_windows()
for _, data in pairs(source_data) do
for _, state in pairs(data.state_by_tab) do
renderer.close(state)
end
for _, state in pairs(data.state_by_win) do
renderer.close(state)
end
end
source_data = {}
end
M.set_default_config = function(source_name, config)
if source_name == nil then
error("set_default_config: source_name cannot be nil")
end
default_configs[source_name] = config
local sd = get_source_data(source_name)
for tabid, tab_config in pairs(sd.state_by_tab) do
sd.state_by_tab[tabid] = vim.tbl_deep_extend("force", tab_config, config)
end
end
--TODO: we need to track state per window when working with netwrw style "current"
--position. How do we know which one to return when this is called?
M.get_state = function(source_name, tabid, winid)
if source_name == nil then
error("get_state: source_name cannot be nil")
end
tabid = tabid or vim.api.nvim_get_current_tabpage()
local sd = get_source_data(source_name)
if type(winid) == "number" then
local win_state = sd.state_by_win[winid]
if not win_state then
win_state = create_state(tabid, sd, winid)
sd.state_by_win[winid] = win_state
end
return win_state
else
local tab_state = sd.state_by_tab[tabid]
if tab_state and tab_state.winid then
-- just in case tab and window get tangled up, tab state replaces window
sd.state_by_win[tab_state.winid] = nil
end
if not tab_state then
tab_state = create_state(tabid, sd)
sd.state_by_tab[tabid] = tab_state
end
return tab_state
end
end
---Returns the state for the current buffer, assuming it is a neo-tree buffer.
---@param winid number|nil The window id to use, if nil, the current window is used.
---@return table|nil The state for the current buffer, or nil if it is not a
---neo-tree buffer.
M.get_state_for_window = function(winid)
local winid = winid or vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
local source_status, source_name = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_source")
local position_status, position = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_position")
if not source_status or not position_status then
return nil
end
local tabid = vim.api.nvim_get_current_tabpage()
if position == "current" then
return M.get_state(source_name, tabid, winid)
else
return M.get_state(source_name, tabid, nil)
end
end
M.get_path_to_reveal = function(include_terminals)
local win_id = vim.api.nvim_get_current_win()
local cfg = vim.api.nvim_win_get_config(win_id)
if cfg.relative > "" or cfg.external then
-- floating window, ignore
return nil
end
if vim.bo.filetype == "neo-tree" then
return nil
end
local path = vim.fn.expand("%:p")
if not utils.truthy(path) then
return nil
end
if not include_terminals and path:match("term://") then
return nil
end
return path
end
M.subscribe = function(source_name, event)
if source_name == nil then
error("subscribe: source_name cannot be nil")
end
local sd = get_source_data(source_name)
if not sd.subscriptions then
sd.subscriptions = {}
end
if not utils.truthy(event.id) then
event.id = sd.name .. "." .. event.event
end
log.trace("subscribing to event: " .. event.id)
sd.subscriptions[event] = true
events.subscribe(event)
end
M.unsubscribe = function(source_name, event)
if source_name == nil then
error("unsubscribe: source_name cannot be nil")
end
local sd = get_source_data(source_name)
log.trace("unsubscribing to event: " .. event.id or event.event)
if sd.subscriptions then
for sub, _ in pairs(sd.subscriptions) do
if sub.event == event.event and sub.id == event.id then
sd.subscriptions[sub] = false
events.unsubscribe(sub)
end
end
end
events.unsubscribe(event)
end
M.unsubscribe_all = function(source_name)
if source_name == nil then
error("unsubscribe_all: source_name cannot be nil")
end
local sd = get_source_data(source_name)
if sd.subscriptions then
for event, subscribed in pairs(sd.subscriptions) do
if subscribed then
events.unsubscribe(event)
end
end
end
sd.subscriptions = {}
end
M.close = function(source_name, at_position)
local state = M.get_state(source_name)
if at_position then
if state.current_position == at_position then
return renderer.close(state)
else
return false
end
else
return renderer.close(state)
end
end
M.close_all = function(at_position)
local tabid = vim.api.nvim_get_current_tabpage()
for source_name, _ in pairs(source_data) do
M._for_each_state(source_name, function(state)
if state.tabid == tabid then
if at_position then
if state.current_position == at_position then
log.trace("Closing " .. source_name .. " at position " .. at_position)
pcall(renderer.close, state)
end
else
log.trace("Closing " .. source_name)
pcall(renderer.close, state)
end
end
end)
end
end
M.close_all_except = function(except_source_name)
local tabid = vim.api.nvim_get_current_tabpage()
for source_name, _ in pairs(source_data) do
M._for_each_state(source_name, function(state)
if state.tabid == tabid and source_name ~= except_source_name then
log.trace("Closing " .. source_name)
pcall(renderer.close, state)
end
end)
end
end
---Redraws the tree with updated diagnostics without scanning the filesystem again.
M.diagnostics_changed = function(source_name, args)
if not type(args) == "table" then
error("diagnostics_changed: args must be a table")
end
M._for_each_state(source_name, function(state)
state.diagnostics_lookup = args.diagnostics_lookup
renderer.redraw(state)
end)
end
---Called by autocmds when the cwd dir is changed. This will change the root.
M.dir_changed = function(source_name)
M._for_each_state(source_name, function(state)
local cwd = M.get_cwd(state)
if state.path and cwd == state.path then
return
end
if renderer.window_exists(state) then
M.navigate(state, cwd)
else
state.path = nil
state.dirty = true
end
end)
end
--
---Redraws the tree with updated git_status without scanning the filesystem again.
M.git_status_changed = function(source_name, args)
if not type(args) == "table" then
error("git_status_changed: args must be a table")
end
M._for_each_state(source_name, function(state)
if utils.is_subpath(args.git_root, state.path) then
state.git_status_lookup = args.git_status
renderer.redraw(state)
end
end)
end
-- Vimscript functions like vim.fn.getcwd take tabpage number (tab position counting from left)
-- but API functions operate on tabpage id (as returned by nvim_tabpage_get_number). These values
-- get out of sync when tabs are being moved and we want to track state according to tabpage id.
local to_tabnr = function(tabid)
return tabid > 0 and vim.api.nvim_tabpage_get_number(tabid) or tabid
end
local get_params_for_cwd = function(state)
local tabid = state.tabid
-- the id is either the tabid for sidebars or the winid for splits
local winid = state.id == tabid and -1 or state.id
if state.cwd_target then
local target = state.cwd_target.sidebar
if state.current_position == "current" then
target = state.cwd_target.current
end
if target == "window" then
return winid, to_tabnr(tabid)
elseif target == "global" then
return -1, -1
elseif target == "none" then
return nil, nil
else -- default to tab
return -1, to_tabnr(tabid)
end
else
return winid, to_tabnr(tabid)
end
end
M.get_cwd = function(state)
local winid, tabnr = get_params_for_cwd(state)
local success, cwd = false, ""
if winid or tabnr then
success, cwd = pcall(vim.fn.getcwd, winid, tabnr)
end
if success then
return cwd
else
success, cwd = pcall(vim.fn.getcwd)
if success then
return cwd
else
return state.path
end
end
end
M.set_cwd = function(state)
if not state.path then
return
end
local winid, tabnr = get_params_for_cwd(state)
if winid == nil and tabnr == nil then
return
end
local _, cwd = pcall(vim.fn.getcwd, winid, tabnr)
if state.path ~= cwd then
if winid > 0 then
vim.cmd("lcd " .. state.path)
elseif tabnr > 0 then
vim.cmd("tcd " .. state.path)
else
vim.cmd("cd " .. state.path)
end
end
end
local dispose_state = function(state)
pcall(fs_scan.stop_watchers, state)
pcall(renderer.close, state)
source_data[state.name].state_by_tab[state.id] = nil
source_data[state.name].state_by_win[state.id] = nil
state.disposed = true
end
M.dispose = function(source_name, tabid)
for i, state in ipairs(all_states) do
if source_name == nil or state.name == source_name then
if not tabid or tabid == state.tabid then
log.trace(state.name, " disposing of tab: ", tabid)
dispose_state(state)
table.remove(all_states, i)
end
end
end
end
M.dispose_tab = function(tabid)
if not tabid then
error("dispose_tab: tabid cannot be nil")
end
for i, state in ipairs(all_states) do
if tabid == state.tabid then
log.trace(state.name, " disposing of tab: ", tabid, state.name)
dispose_state(state)
table.remove(all_states, i)
end
end
end
M.dispose_invalid_tabs = function()
-- Iterate in reverse because we are removing items during loop
for i = #all_states, 1, -1 do
local state = all_states[i]
-- if not valid_tabs[state.tabid] then
if not vim.api.nvim_tabpage_is_valid(state.tabid) then
log.trace(state.name, " disposing of tab: ", state.tabid, state.name)
dispose_state(state)
table.remove(all_states, i)
end
end
end
M.dispose_window = function(winid)
if not winid then
error("dispose_window: winid cannot be nil")
end
for i, state in ipairs(all_states) do
if state.id == winid then
log.trace(state.name, " disposing of window: ", winid, state.name)
dispose_state(state)
table.remove(all_states, i)
end
end
end
M.float = function(source_name)
local state = M.get_state(source_name)
state.current_position = "float"
local path_to_reveal = M.get_path_to_reveal()
M.navigate(source_name, state.path, path_to_reveal)
end
---Focus the window, opening it if it is not already open.
---@param source_name string Source name.
---@param path_to_reveal string|nil Node to focus after the items are loaded.
---@param callback function|nil Callback to call after the items are loaded.
M.focus = function(source_name, path_to_reveal, callback)
local state = M.get_state(source_name)
state.current_position = nil
if path_to_reveal then
M.navigate(source_name, state.path, path_to_reveal, callback)
else
if not state.dirty and renderer.window_exists(state) then
vim.api.nvim_set_current_win(state.winid)
else
M.navigate(source_name, state.path, nil, callback)
end
end
end
---Redraws the tree with updated modified markers without scanning the filesystem again.
M.opened_buffers_changed = function(source_name, args)
if not type(args) == "table" then
error("opened_buffers_changed: args must be a table")
end
if type(args.opened_buffers) == "table" then
M._for_each_state(source_name, function(state)
if utils.tbl_equals(args.opened_buffers, state.opened_buffers) then
-- no changes, no need to redraw
return
end
state.opened_buffers = args.opened_buffers
renderer.redraw(state)
end)
end
end
---Navigate to the given path.
---@param state_or_source_name string|table The state or source name to navigate.
---@param path string? Path to navigate to. If empty, will navigate to the cwd.
---@param path_to_reveal string? Node to focus after the items are loaded.
---@param callback function? Callback to call after the items are loaded.
---@param async boolean? Whether to load the items asynchronously, may not be respected by all sources.
M.navigate = function(state_or_source_name, path, path_to_reveal, callback, async)
require("neo-tree").ensure_config()
local state, source_name
if type(state_or_source_name) == "string" then
state = M.get_state(state_or_source_name)
source_name = state_or_source_name
elseif type(state_or_source_name) == "table" then
state = state_or_source_name
source_name = state.name
else
log.error("navigate: state_or_source_name must be a string or a table")
end
log.trace("navigate", source_name, path, path_to_reveal)
local mod = get_source_data(source_name).module
if not mod then
mod = require("neo-tree.sources." .. source_name)
end
mod.navigate(state, path, path_to_reveal, callback, async)
end
---Redraws the tree without scanning the filesystem again. Use this after
-- making changes to the nodes that would affect how their components are
-- rendered.
M.redraw = function(source_name)
M._for_each_state(source_name, function(state)
renderer.redraw(state)
end)
end
---Refreshes the tree by scanning the filesystem again.
M.refresh = function(source_name, callback)
if type(callback) ~= "function" then
callback = nil
end
local current_tabid = vim.api.nvim_get_current_tabpage()
log.trace(source_name, "refresh")
for i = 1, #all_states, 1 do
local state = all_states[i]
if state.tabid == current_tabid and state.path and renderer.window_exists(state) then
local success, err = pcall(M.navigate, state, state.path, nil, callback)
if not success then
log.error(err)
end
else
state.dirty = true
end
end
end
M.reveal_current_file = function(source_name, callback, force_cwd)
log.trace("Revealing current file")
local state = M.get_state(source_name)
state.current_position = nil
-- When events trigger that try to restore the position of the cursor in the tree window,
-- we want them to ignore this "iteration" as the user is trying to explicitly focus a
-- (potentially) different position/node
state.position.is.restorable = false
require("neo-tree").close_all_except(source_name)
local path = M.get_path_to_reveal()
if not path then
M.focus(source_name)
return
end
local cwd = state.path
if cwd == nil then
cwd = M.get_cwd(state)
end
if force_cwd then
if not utils.is_subpath(cwd, path) then
state.path, _ = utils.split_path(path)
end
elseif not utils.is_subpath(cwd, path) then
cwd, _ = utils.split_path(path)
inputs.confirm("File not in cwd. Change cwd to " .. cwd .. "?", function(response)
if response == true then
state.path = cwd
M.focus(source_name, path, callback)
else
M.focus(source_name, nil, callback)
end
end)
return
end
if path then
if not renderer.focus_node(state, path) then
M.focus(source_name, path, callback)
end
end
end
M.reveal_in_split = function(source_name, callback)
local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win())
state.current_position = "current"
local path_to_reveal = M.get_path_to_reveal()
if not path_to_reveal then
M.navigate(state, nil, nil, callback)
return
end
local cwd = state.path
if cwd == nil then
cwd = M.get_cwd(state)
end
if cwd and not utils.is_subpath(cwd, path_to_reveal) then
state.path, _ = utils.split_path(path_to_reveal)
end
M.navigate(state, state.path, path_to_reveal, callback)
end
---Opens the tree and displays the current path or cwd, without focusing it.
M.show = function(source_name)
local state = M.get_state(source_name)
state.current_position = nil
if not renderer.window_exists(state) then
local current_win = vim.api.nvim_get_current_win()
M.navigate(source_name, state.path, nil, function()
vim.api.nvim_set_current_win(current_win)
end)
end
end
M.show_in_split = function(source_name, callback)
local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win())
state.current_position = "current"
M.navigate(state, state.path, nil, callback)
end
M.validate_source = function(source_name, module)
if source_name == nil then
error("register_source: source_name cannot be nil")
end
if module == nil then
error("register_source: module cannot be nil")
end
if type(module) ~= "table" then
error("register_source: module must be a table")
end
local required_functions = {
"navigate",
"setup",
}
for _, name in ipairs(required_functions) do
if type(module[name]) ~= "function" then
error("Source " .. source_name .. " must have a " .. name .. " function")
end
end
end
---Configures the plugin, should be called before the plugin is used.
---@param source_name string Name of the source.
---@param config table Configuration table containing merged configuration for the source.
---@param global_config table Global configuration table, shared between all sources.
---@param module table Module containing the source's code.
M.setup = function(source_name, config, global_config, module)
log.debug(source_name, " setup ", config)
M.unsubscribe_all(source_name)
M.set_default_config(source_name, config)
if module == nil then
module = require("neo-tree.sources." .. source_name)
end
local success, err = pcall(M.validate_source, source_name, module)
if success then
success, err = pcall(module.setup, config, global_config)
if success then
get_source_data(source_name).module = module
else
log.error("Source " .. source_name .. " setup failed: " .. err)
end
else
log.error("Source " .. source_name .. " is invalid: " .. err)
end
end
return M

View File

@ -0,0 +1,271 @@
local log = require("neo-tree.log")
local utils = require("neo-tree.utils")
local vim = vim
local M = {}
---@type integer
M.ns_id = vim.api.nvim_create_namespace("neo-tree.nvim")
M.BUFFER_NUMBER = "NeoTreeBufferNumber"
M.CURSOR_LINE = "NeoTreeCursorLine"
M.DIM_TEXT = "NeoTreeDimText"
M.DIRECTORY_ICON = "NeoTreeDirectoryIcon"
M.DIRECTORY_NAME = "NeoTreeDirectoryName"
M.DOTFILE = "NeoTreeDotfile"
M.FADE_TEXT_1 = "NeoTreeFadeText1"
M.FADE_TEXT_2 = "NeoTreeFadeText2"
M.FILE_ICON = "NeoTreeFileIcon"
M.FILE_NAME = "NeoTreeFileName"
M.FILE_NAME_OPENED = "NeoTreeFileNameOpened"
M.FILTER_TERM = "NeoTreeFilterTerm"
M.FLOAT_BORDER = "NeoTreeFloatBorder"
M.FLOAT_NORMAL = "NeoTreeFloatNormal"
M.FLOAT_TITLE = "NeoTreeFloatTitle"
M.GIT_ADDED = "NeoTreeGitAdded"
M.GIT_CONFLICT = "NeoTreeGitConflict"
M.GIT_DELETED = "NeoTreeGitDeleted"
M.GIT_IGNORED = "NeoTreeGitIgnored"
M.GIT_MODIFIED = "NeoTreeGitModified"
M.GIT_RENAMED = "NeoTreeGitRenamed"
M.GIT_STAGED = "NeoTreeGitStaged"
M.GIT_UNTRACKED = "NeoTreeGitUntracked"
M.GIT_UNSTAGED = "NeoTreeGitUnstaged"
M.HIDDEN_BY_NAME = "NeoTreeHiddenByName"
M.INDENT_MARKER = "NeoTreeIndentMarker"
M.MESSAGE = "NeoTreeMessage"
M.MODIFIED = "NeoTreeModified"
M.NORMAL = "NeoTreeNormal"
M.NORMALNC = "NeoTreeNormalNC"
M.SIGNCOLUMN = "NeoTreeSignColumn"
M.STATUS_LINE = "NeoTreeStatusLine"
M.STATUS_LINE_NC = "NeoTreeStatusLineNC"
M.TAB_ACTIVE = "NeoTreeTabActive"
M.TAB_INACTIVE = "NeoTreeTabInactive"
M.TAB_SEPARATOR_ACTIVE = "NeoTreeTabSeparatorActive"
M.TAB_SEPARATOR_INACTIVE = "NeoTreeTabSeparatorInactive"
M.VERTSPLIT = "NeoTreeVertSplit"
M.WINSEPARATOR = "NeoTreeWinSeparator"
M.END_OF_BUFFER = "NeoTreeEndOfBuffer"
M.ROOT_NAME = "NeoTreeRootName"
M.SYMBOLIC_LINK_TARGET = "NeoTreeSymbolicLinkTarget"
M.TITLE_BAR = "NeoTreeTitleBar"
M.INDENT_MARKER = "NeoTreeIndentMarker"
M.EXPANDER = "NeoTreeExpander"
M.WINDOWS_HIDDEN = "NeoTreeWindowsHidden"
M.PREVIEW = "NeoTreePreview"
local function dec_to_hex(n, chars)
chars = chars or 6
local hex = string.format("%0" .. chars .. "x", n)
while #hex < chars do
hex = "0" .. hex
end
return hex
end
---If the given highlight group is not defined, define it.
---@param hl_group_name string The name of the highlight group.
---@param link_to_if_exists table A list of highlight groups to link to, in
--order of priority. The first one that exists will be used.
---@param background string|nil The background color to use, in hex, if the highlight group
--is not defined and it is not linked to another group.
---@param foreground string|nil The foreground color to use, in hex, if the highlight group
--is not defined and it is not linked to another group.
---@gui string|nil The gui to use, if the highlight group is not defined and it is not linked
--to another group.
---@return table table The highlight group values.
M.create_highlight_group = function(hl_group_name, link_to_if_exists, background, foreground, gui)
local success, hl_group = pcall(vim.api.nvim_get_hl_by_name, hl_group_name, true)
if not success or not hl_group.foreground or not hl_group.background then
for _, link_to in ipairs(link_to_if_exists) do
success, hl_group = pcall(vim.api.nvim_get_hl_by_name, link_to, true)
if success then
local new_group_has_settings = background or foreground or gui
local link_to_has_settings = hl_group.foreground or hl_group.background
if link_to_has_settings or not new_group_has_settings then
vim.cmd("highlight default link " .. hl_group_name .. " " .. link_to)
return hl_group
end
end
end
if type(background) == "number" then
background = dec_to_hex(background)
end
if type(foreground) == "number" then
foreground = dec_to_hex(foreground)
end
local cmd = "highlight default " .. hl_group_name
if background then
cmd = cmd .. " guibg=#" .. background
end
if foreground then
cmd = cmd .. " guifg=#" .. foreground
else
cmd = cmd .. " guifg=NONE"
end
if gui then
cmd = cmd .. " gui=" .. gui
end
vim.cmd(cmd)
return {
background = background and tonumber(background, 16) or nil,
foreground = foreground and tonumber(foreground, 16) or nil,
}
end
return hl_group
end
local faded_highlight_group_cache = {}
M.get_faded_highlight_group = function(hl_group_name, fade_percentage)
if type(hl_group_name) ~= "string" then
error("hl_group_name must be a string")
end
if type(fade_percentage) ~= "number" then
error("hl_group_name must be a number")
end
if fade_percentage < 0 or fade_percentage > 1 then
error("fade_percentage must be between 0 and 1")
end
local key = hl_group_name .. "_" .. tostring(math.floor(fade_percentage * 100))
if faded_highlight_group_cache[key] then
return faded_highlight_group_cache[key]
end
local normal = vim.api.nvim_get_hl_by_name("Normal", true)
if type(normal.foreground) ~= "number" then
if vim.api.nvim_get_option("background") == "dark" then
normal.foreground = 0xffffff
else
normal.foreground = 0x000000
end
end
if type(normal.background) ~= "number" then
if vim.api.nvim_get_option("background") == "dark" then
normal.background = 0x000000
else
normal.background = 0xffffff
end
end
local foreground = dec_to_hex(normal.foreground)
local background = dec_to_hex(normal.background)
local hl_group = vim.api.nvim_get_hl_by_name(hl_group_name, true)
if type(hl_group.foreground) == "number" then
foreground = dec_to_hex(hl_group.foreground)
end
if type(hl_group.background) == "number" then
background = dec_to_hex(hl_group.background)
end
local gui = {}
if hl_group.bold then
table.insert(gui, "bold")
end
if hl_group.italic then
table.insert(gui, "italic")
end
if hl_group.underline then
table.insert(gui, "underline")
end
if hl_group.undercurl then
table.insert(gui, "undercurl")
end
if #gui > 0 then
gui = table.concat(gui, ",")
else
gui = nil
end
local f_red = tonumber(foreground:sub(1, 2), 16)
local f_green = tonumber(foreground:sub(3, 4), 16)
local f_blue = tonumber(foreground:sub(5, 6), 16)
local b_red = tonumber(background:sub(1, 2), 16)
local b_green = tonumber(background:sub(3, 4), 16)
local b_blue = tonumber(background:sub(5, 6), 16)
local red = (f_red * fade_percentage) + (b_red * (1 - fade_percentage))
local green = (f_green * fade_percentage) + (b_green * (1 - fade_percentage))
local blue = (f_blue * fade_percentage) + (b_blue * (1 - fade_percentage))
local new_foreground =
string.format("%s%s%s", dec_to_hex(red, 2), dec_to_hex(green, 2), dec_to_hex(blue, 2))
M.create_highlight_group(key, {}, hl_group.background, new_foreground, gui)
faded_highlight_group_cache[key] = key
return key
end
M.setup = function()
-- Reset this here in case of color scheme change
faded_highlight_group_cache = {}
local normal_hl = M.create_highlight_group(M.NORMAL, { "Normal" })
local normalnc_hl = M.create_highlight_group(M.NORMALNC, { "NormalNC", M.NORMAL })
M.create_highlight_group(M.SIGNCOLUMN, { "SignColumn", M.NORMAL })
M.create_highlight_group(M.STATUS_LINE, { "StatusLine" })
M.create_highlight_group(M.STATUS_LINE_NC, { "StatusLineNC" })
M.create_highlight_group(M.VERTSPLIT, { "VertSplit" })
M.create_highlight_group(M.WINSEPARATOR, { "WinSeparator" })
M.create_highlight_group(M.END_OF_BUFFER, { "EndOfBuffer" })
local float_border_hl =
M.create_highlight_group(M.FLOAT_BORDER, { "FloatBorder" }, normalnc_hl.background, "444444")
M.create_highlight_group(M.FLOAT_NORMAL, { "NormalFloat", M.NORMAL })
M.create_highlight_group(M.FLOAT_TITLE, {}, float_border_hl.background, normal_hl.foreground)
local title_fg = normal_hl.background
if title_fg == float_border_hl.foreground then
title_fg = normal_hl.foreground
end
M.create_highlight_group(M.TITLE_BAR, {}, float_border_hl.foreground, title_fg)
M.create_highlight_group(M.BUFFER_NUMBER, { "SpecialChar" })
M.create_highlight_group(M.DIM_TEXT, {}, nil, "505050")
M.create_highlight_group(M.MESSAGE, {}, nil, "505050", "italic")
M.create_highlight_group(M.FADE_TEXT_1, {}, nil, "626262")
M.create_highlight_group(M.FADE_TEXT_2, {}, nil, "444444")
M.create_highlight_group(M.DOTFILE, {}, nil, "626262")
M.create_highlight_group(M.HIDDEN_BY_NAME, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.CURSOR_LINE, { "CursorLine" }, nil, nil, "bold")
M.create_highlight_group(M.DIRECTORY_NAME, { "Directory" }, "NONE", "NONE")
M.create_highlight_group(M.DIRECTORY_ICON, { "Directory" }, nil, "73cef4")
M.create_highlight_group(M.FILE_ICON, { M.DIRECTORY_ICON })
M.create_highlight_group(M.FILE_NAME, {}, "NONE", "NONE")
M.create_highlight_group(M.FILE_NAME_OPENED, {}, nil, nil, "bold")
M.create_highlight_group(M.SYMBOLIC_LINK_TARGET, { M.FILE_NAME })
M.create_highlight_group(M.FILTER_TERM, { "SpecialChar", "Normal" })
M.create_highlight_group(M.ROOT_NAME, {}, nil, nil, "bold,italic")
M.create_highlight_group(M.INDENT_MARKER, { M.DIM_TEXT })
M.create_highlight_group(M.EXPANDER, { M.DIM_TEXT })
M.create_highlight_group(M.MODIFIED, {}, nil, "d7d787")
M.create_highlight_group(M.WINDOWS_HIDDEN, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.PREVIEW, { "Search" }, nil, nil)
M.create_highlight_group(M.GIT_ADDED, { "GitGutterAdd", "GitSignsAdd" }, nil, "5faf5f")
M.create_highlight_group(M.GIT_DELETED, { "GitGutterDelete", "GitSignsDelete" }, nil, "ff5900")
M.create_highlight_group(M.GIT_MODIFIED, { "GitGutterChange", "GitSignsChange" }, nil, "d7af5f")
local conflict = M.create_highlight_group(M.GIT_CONFLICT, {}, nil, "ff8700", "italic,bold")
M.create_highlight_group(M.GIT_IGNORED, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.GIT_RENAMED, { M.GIT_MODIFIED }, nil, nil)
M.create_highlight_group(M.GIT_STAGED, { M.GIT_ADDED }, nil, nil)
M.create_highlight_group(M.GIT_UNSTAGED, { M.GIT_CONFLICT }, nil, nil)
M.create_highlight_group(M.GIT_UNTRACKED, {}, nil, conflict.foreground, "italic")
M.create_highlight_group(M.TAB_ACTIVE, {}, nil, nil, "bold")
M.create_highlight_group(M.TAB_INACTIVE, {}, "141414", "777777")
M.create_highlight_group(M.TAB_SEPARATOR_ACTIVE, {}, nil, "0a0a0a")
M.create_highlight_group(M.TAB_SEPARATOR_INACTIVE, {}, "141414", "101010")
end
return M

View File

@ -0,0 +1,80 @@
local vim = vim
local Input = require("nui.input")
local popups = require("neo-tree.ui.popups")
local utils = require("neo-tree.utils")
local M = {}
local should_use_popup_input = function()
local nt = require("neo-tree")
return utils.get_value(nt.config, "use_popups_for_input", true, false)
end
M.show_input = function(input, callback)
input:mount()
input:map("i", "<esc>", function()
vim.cmd("stopinsert")
input:unmount()
end, { noremap = true })
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
local event = require("nui.utils.autocmd").event
input:on({ event.BufLeave, event.BufDelete }, function()
input:unmount()
if callback then
callback()
end
end, { once = true })
end
M.input = function(message, default_value, callback, options, completion)
if should_use_popup_input() then
local popup_options = popups.popup_options(message, 10, options)
local input = Input(popup_options, {
prompt = " ",
default_value = default_value,
on_submit = callback,
})
M.show_input(input)
else
local opts = {
prompt = message .. " ",
default = default_value,
}
if completion then
opts.completion = completion
end
vim.ui.input(opts, callback)
end
end
M.confirm = function(message, callback)
if should_use_popup_input() then
local popup_options = popups.popup_options(message, 10)
local input = Input(popup_options, {
prompt = " y/n: ",
on_close = function()
callback(false)
end,
on_submit = function(value)
callback(value == "y" or value == "Y")
end,
})
M.show_input(input)
else
local opts = {
prompt = message .. " y/n: ",
}
vim.ui.input(opts, function(value)
callback(value == "y" or value == "Y")
end)
end
end
return M

View File

@ -0,0 +1,127 @@
local vim = vim
local Input = require("nui.input")
local NuiText = require("nui.text")
local NuiPopup = require("nui.popup")
local highlights = require("neo-tree.ui.highlights")
local log = require("neo-tree.log")
local M = {}
M.popup_options = function(title, min_width, override_options)
local min_width = min_width or 30
local width = string.len(title) + 2
local nt = require("neo-tree")
local popup_border_style = nt.config.popup_border_style
local popup_border_text = NuiText(" " .. title .. " ", highlights.FLOAT_TITLE)
local col = 0
-- fix popup position when using multigrid
local popup_last_col = vim.api.nvim_win_get_position(0)[2] + width + 2
if popup_last_col >= vim.o.columns then
col = vim.o.columns - popup_last_col
end
local popup_options = {
ns_id = highlights.ns_id,
relative = "cursor",
position = {
row = 1,
col = col,
},
size = width,
border = {
text = {
top = popup_border_text,
},
style = popup_border_style,
highlight = highlights.FLOAT_BORDER,
},
win_options = {
winhighlight = "Normal:"
.. highlights.FLOAT_NORMAL
.. ",FloatBorder:"
.. highlights.FLOAT_BORDER,
},
buf_options = {
bufhidden = "delete",
buflisted = false,
filetype = "neo-tree-popup",
},
}
if popup_border_style == "NC" then
local blank = NuiText(" ", highlights.TITLE_BAR)
popup_border_text = NuiText(" " .. title .. " ", highlights.TITLE_BAR)
popup_options.border = {
style = { "", blank, "", "", " ", "", " ", "" },
highlight = highlights.FLOAT_BORDER,
text = {
top = popup_border_text,
top_align = "left",
},
}
end
if override_options then
return vim.tbl_extend("force", popup_options, override_options)
else
return popup_options
end
end
M.alert = function(title, message, size)
local lines = {}
local max_line_width = title:len()
local add_line = function(line)
if not type(line) == "string" then
line = tostring(line)
end
if line:len() > max_line_width then
max_line_width = line:len()
end
table.insert(lines, line)
end
if type(message) == "table" then
for _, v in ipairs(message) do
add_line(v)
end
else
add_line(message)
end
add_line("")
add_line(" Press <Escape> or <Enter> to close")
local win_options = M.popup_options(title, 80)
win_options.zindex = 60
win_options.size = {
width = max_line_width + 4,
height = #lines + 1,
}
local win = NuiPopup(win_options)
win:mount()
local success, msg = pcall(vim.api.nvim_buf_set_lines, win.bufnr, 0, 0, false, lines)
if success then
win:map("n", "<esc>", function(bufnr)
win:unmount()
end, { noremap = true })
win:map("n", "<enter>", function(bufnr)
win:unmount()
end, { noremap = true })
local event = require("nui.utils.autocmd").event
win:on({ event.BufLeave, event.BufDelete }, function()
win:unmount()
end, { once = true })
-- why is this necessary?
vim.api.nvim_set_current_win(win.winid)
else
log.error(msg)
win:unmount()
end
end
return M

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,432 @@
local vim = vim
local utils = require("neo-tree.utils")
local log = require("neo-tree.log")
local manager = require("neo-tree.sources.manager")
local M = {}
---calc_click_id_from_source:
-- Calculates click_id that stores information of the source and window id
-- DANGER: Do not change this function unless you know what you are doing
---@param winid integer: window id of the window source_selector is placed
---@param source_index integer: index of the source
---@return integer
local calc_click_id_from_source = function(winid, source_index)
local base_number = #require("neo-tree").config.source_selector.sources + 1
return base_number * winid + source_index
end
---calc_source_from_click_id:
-- Calculates source index and window id from click_id. Paired with `M.calc_click_id_from_source`
-- DANGER: Do not change this function unless you know what you are doing
---@param click_id integer: click_id
---@return integer, integer
local calc_source_from_click_id = function(click_id)
local base_number = #require("neo-tree").config.source_selector.sources + 1
return math.floor(click_id / base_number), click_id % base_number
end
---sep_tbl:
-- Returns table expression of separator.
-- Converts to table expression if sep is string.
---@param sep string | table:
---@return table: `{ left = .., right = .., override = .. }`
local sep_tbl = function(sep)
if type(sep) == "nil" then
return {}
elseif type(sep) ~= "table" then
return { left = sep, right = sep, override = "active" }
end
return sep
end
-- Function below provided by @akinsho
-- https://github.com/nvim-neo-tree/neo-tree.nvim/pull/427#discussion_r924947766
-- truncate a string based on number of display columns/cells it occupies
-- so that multibyte characters are not broken up mid-character
---@param str string
---@param col_limit number
---@return string
local function truncate_by_cell(str, col_limit)
local api = vim.api
local fn = vim.fn
if str and str:len() == api.nvim_strwidth(str) then
return fn.strcharpart(str, 0, col_limit)
end
local short = fn.strcharpart(str, 0, col_limit)
if api.nvim_strwidth(short) > col_limit then
while api.nvim_strwidth(short) > col_limit do
short = fn.strcharpart(short, 0, fn.strchars(short) - 1)
end
end
return short
end
---get_separators
-- Returns information about separator on each tab.
---@param source_index integer: index of source
---@param active_index integer: index of active source. used to check if source is active and when `override = "active"`
---@param force_ignore_left boolean: overwrites calculated results with "" if set to true
---@param force_ignore_right boolean: overwrites calculated results with "" if set to true
---@return table: something like `{ left = "|", right = "|" }`
local get_separators = function(source_index, active_index, force_ignore_left, force_ignore_right)
local config = require("neo-tree").config
local is_active = source_index == active_index
local sep = sep_tbl(config.source_selector.separator)
if is_active then
sep = vim.tbl_deep_extend("force", sep, sep_tbl(config.source_selector.separator_active))
end
local show_left = sep.override == "left"
or (sep.override == "active" and source_index <= active_index)
or sep.override == nil
local show_right = sep.override == "right"
or (sep.override == "active" and source_index >= active_index)
or sep.override == nil
return {
left = (show_left and not force_ignore_left) and sep.left or "",
right = (show_right and not force_ignore_right) and sep.right or "",
}
end
---get_selector_tab_info:
-- Returns information to create a tab
---@param source_name string: name of source. should be same as names in `config.sources`
---@param source_index integer: index of source_name
---@param is_active boolean: whether this source is currently focused
---@param separator table: `{ left = .., right = .. }`: output from `get_separators()`
---@return table (see code): Note: `length`: length of whole tab (including seps), `text_length`: length of tab excluding seps
local get_selector_tab_info = function(source_name, source_index, is_active, separator)
local config = require("neo-tree").config
local separator_config = utils.resolve_config_option(config, "source_selector", nil)
if separator_config == nil then
log.warn("Cannot find source_selector config. `get_selector` abort.")
return {}
end
local source_config = config[source_name] or {}
local get_strlen = vim.api.nvim_strwidth
local text = separator_config.sources[source_index].display_name or source_config.display_name or source_name
local text_length = get_strlen(text)
if separator_config.tabs_min_width ~= nil and text_length < separator_config.tabs_min_width then
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_min_width)
text_length = separator_config.tabs_min_width
end
if separator_config.tabs_max_width ~= nil and text_length > separator_config.tabs_max_width then
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_max_width)
text_length = separator_config.tabs_max_width
end
local tab_hl = is_active and separator_config.highlight_tab_active
or separator_config.highlight_tab
local sep_hl = is_active and separator_config.highlight_separator_active
or separator_config.highlight_separator
return {
index = source_index,
is_active = is_active,
left = separator.left,
right = separator.right,
text = text,
tab_hl = tab_hl,
sep_hl = sep_hl,
length = text_length + get_strlen(separator.left) + get_strlen(separator.right),
text_length = text_length,
}
end
---text_with_hl:
-- Returns text with highlight syntax for winbar / statusline
---@param text string: text to highlight
---@param tab_hl string | nil: if nil, does nothing
---@return string: e.g. "%#HiName#text"
local text_with_hl = function(text, tab_hl)
if tab_hl == nil then
return text
end
return string.format("%%#%s#%s", tab_hl, text)
end
---add_padding:
-- Use for creating padding with highlight
---@param padding_legth number: number of padding. if float, value is rounded with `math.floor`
---@param padchar string | nil: if nil, " " (space) is used
---@return string
local add_padding = function(padding_legth, padchar)
if padchar == nil then
padchar = " "
end
return string.rep(padchar, math.floor(padding_legth))
end
---text_layout:
-- Add padding to fill `output_width`.
-- If `output_width` is less than `text_length`, text is truncated to fit `output_width`.
---@param text string:
---@param content_layout string: `"start", "center", "end"`: see `config.source_selector.tabs_layout` for more details
---@param output_width integer: exact `strdisplaywidth` of the output string
---@param trunc_char string | nil: Character used to indicate truncation. If nil, "…" (ellipsis) is used.
---@return string
local text_layout = function(text, content_layout, output_width, trunc_char)
if output_width < 1 then
return ""
end
local text_length = vim.fn.strdisplaywidth(text)
local pad_length = output_width - text_length
local left_pad, right_pad = 0, 0
if pad_length < 0 then
if output_width < 4 then
return truncate_by_cell(text, output_width)
else
return truncate_by_cell(text, output_width - 1) .. trunc_char
end
elseif content_layout == "start" then
left_pad, right_pad = 0, pad_length
elseif content_layout == "end" then
left_pad, right_pad = pad_length, 0
elseif content_layout == "center" then
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
end
return add_padding(left_pad) .. text .. add_padding(right_pad)
end
---render_tab:
-- Renders string to express one tab for winbar / statusline.
---@param left_sep string: left separator
---@param right_sep string: right separator
---@param sep_hl string: highlight of separators
---@param text string: text, mostly name of source in this case
---@param tab_hl string: highlight of text
---@param click_id integer: id passed to `___neotree_selector_click`, should be calculated with `M.calc_click_id_from_source`
---@return string: complete string to render one tab
local render_tab = function(left_sep, right_sep, sep_hl, text, tab_hl, click_id)
local res = "%" .. click_id .. "@v:lua.___neotree_selector_click@"
if left_sep ~= nil then
res = res .. text_with_hl(left_sep, sep_hl)
end
res = res .. text_with_hl(text, tab_hl)
if right_sep ~= nil then
res = res .. text_with_hl(right_sep, sep_hl)
end
return res
end
M.get_scrolled_off_node_text = function(state)
if state == nil then
state = require("neo-tree.sources.manager").get_state_for_window()
if state == nil then
return
end
end
local win_top_line = vim.fn.line("w0")
if win_top_line == nil or win_top_line == 1 then
return
end
local node = state.tree:get_node(win_top_line)
return "" .. vim.fn.fnamemodify(node.path, ":~:h")
end
M.get = function()
local state = require("neo-tree.sources.manager").get_state_for_window()
if state == nil then
return
else
local config = require("neo-tree").config
local scrolled_off =
utils.resolve_config_option(config, "source_selector.show_scrolled_off_parent_node", false)
if scrolled_off then
local node_text = M.get_scrolled_off_node_text(state)
if node_text ~= nil then
return node_text
end
end
return M.get_selector(state, vim.api.nvim_win_get_width(0))
end
end
---get_selector:
-- Does everything to generate the string for source_selector in winbar / statusline.
---@param state table:
---@param width integer: width of the entire window where the source_selector is displayed
---@return string | nil
M.get_selector = function(state, width)
local config = require("neo-tree").config
if config == nil then
log.warn("Cannot find config. `get_selector` abort.")
return nil
end
local winid = state.winid or vim.api.nvim_get_current_win()
-- load padding from config
local padding = config.source_selector.padding
if type(padding) == "number" then
padding = { left = padding, right = padding }
end
width = math.floor(width - padding.left - padding.right)
-- generate information of each tab (look `get_selector_tab_info` for type hint)
local tabs = {}
local sources = config.source_selector.sources
local active_index = #sources
local length_sum, length_active, length_separators = 0, 0, 0
for i, source_info in ipairs(sources) do
local is_active = source_info.source == state.name
if is_active then
active_index = i
end
local separator = get_separators(
i,
active_index,
config.source_selector.show_separator_on_edge == false and i == 1,
config.source_selector.show_separator_on_edge == false and i == #sources
)
local element = get_selector_tab_info(source_info.source, i, is_active, separator)
length_sum = length_sum + element.length
length_separators = length_separators + element.length - element.text_length
if is_active then
length_active = element.length
end
table.insert(tabs, element)
end
-- start creating string to display
local tabs_layout = config.source_selector.tabs_layout
local content_layout = config.source_selector.content_layout or "center"
local hl_background = config.source_selector.highlight_background
local trunc_char = config.source_selector.truncation_character or ""
local remaining_width = width - length_separators
local return_string = text_with_hl(add_padding(padding.left), hl_background)
if width < length_sum and config.source_selector.text_trunc_to_fit then -- not enough width
local each_width = math.floor(remaining_width / #tabs)
local remaining = remaining_width % each_width
tabs_layout = "start"
length_sum = width
for _, tab in ipairs(tabs) do
tab.text = text_layout( -- truncate text and pass it to "start"
tab.text,
"center",
each_width + (tab.is_active and remaining or 0),
trunc_char
)
end
end
if tabs_layout == "active" then
local active_tab_length = width - length_sum + length_active
for _, tab in ipairs(tabs) do
return_string = return_string
.. render_tab(
tab.left,
tab.right,
tab.sep_hl,
text_layout(
tab.text,
tab.is_active and content_layout or nil,
active_tab_length,
trunc_char
),
tab.tab_hl,
calc_click_id_from_source(winid, tab.index)
)
.. text_with_hl("", hl_background)
end
elseif tabs_layout == "equal" then
for _, tab in ipairs(tabs) do
return_string = return_string
.. render_tab(
tab.left,
tab.right,
tab.sep_hl,
text_layout(tab.text, content_layout, math.floor(remaining_width / #tabs), trunc_char),
tab.tab_hl,
calc_click_id_from_source(winid, tab.index)
)
.. text_with_hl("", hl_background)
end
else -- config.source_selector.tab_labels == "start", "end", "center"
-- calculate padding based on tabs_layout
local pad_length = width - length_sum
local left_pad, right_pad = 0, 0
if pad_length > 0 then
if tabs_layout == "start" then
left_pad, right_pad = 0, pad_length
elseif tabs_layout == "end" then
left_pad, right_pad = pad_length, 0
elseif tabs_layout == "center" then
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
end
end
for i, tab in ipairs(tabs) do
if width == 0 then
break
end
-- only render trunc_char if there is no space for the tab
local sep_length = tab.length - tab.text_length
if width <= sep_length + 1 then
return_string = return_string
.. text_with_hl(trunc_char .. add_padding(width - 1), hl_background)
width = 0
break
end
-- tab_length should not exceed width
local tab_length = width < tab.length and width or tab.length
width = width - tab_length
-- add padding for first and last tab
local tab_text = tab.text
if i == 1 then
tab_text = add_padding(left_pad) .. tab_text
tab_length = tab_length + left_pad
end
if i == #tabs then
tab_text = tab_text .. add_padding(right_pad)
tab_length = tab_length + right_pad
end
return_string = return_string
.. render_tab(
tab.left,
tab.right,
tab.sep_hl,
text_layout(tab_text, tabs_layout, tab_length - sep_length, trunc_char),
tab.tab_hl,
calc_click_id_from_source(winid, tab.index)
)
end
end
return return_string .. "%<%0@v:lua.___neotree_selector_click@"
end
---set_source_selector:
-- (public): Directly set source_selector to current window's winbar / statusline
---@param state table: state
---@return nil
M.set_source_selector = function(state)
local sel_config = utils.resolve_config_option(require("neo-tree").config, "source_selector", {})
if sel_config and sel_config.winbar then
vim.wo[state.winid].winbar = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
end
if sel_config and sel_config.statusline then
vim.wo[state.winid].statusline = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
end
end
-- @v:lua@ in the tabline only supports global functions, so this is
-- the only way to add click handlers without autoloaded vimscript functions
_G.___neotree_selector_click = function(id, _, _, _)
if id < 1 then
return
end
local sources = require("neo-tree").config.source_selector.sources
local winid, source_index = calc_source_from_click_id(id)
local state = manager.get_state_for_window(winid)
if state == nil then
log.warn("state not found for window ", winid, "; ignoring click")
return
end
require("neo-tree.command").execute({
source = sources[source_index].source,
position = state.current_position,
action = "focus",
})
end
return M

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
if exists('g:loaded_neo_tree')
finish
endif
let g:loaded_neo_tree = 1
if !exists('g:neo_tree_remove_legacy_commands')
command! -nargs=? NeoTreeClose lua require("neo-tree").close_all("<args>")
command! -nargs=? NeoTreeFloat lua require("neo-tree").float("<args>")
command! -nargs=? NeoTreeFocus lua require("neo-tree").focus("<args>")
command! -nargs=? NeoTreeShow lua require("neo-tree").show("<args>", true)
command! -bang NeoTreeReveal lua require("neo-tree").reveal_current_file("filesystem", false, "<bang>" == "!")
command! NeoTreeRevealInSplit lua require("neo-tree").reveal_in_split("filesystem", false)
command! NeoTreeShowInSplit lua require("neo-tree").show_in_split("filesystem", false)
command! -nargs=? NeoTreeFloatToggle lua require("neo-tree").float("<args>", true)
command! -nargs=? NeoTreeFocusToggle lua require("neo-tree").focus("<args>", true, true)
command! -nargs=? NeoTreeShowToggle lua require("neo-tree").show("<args>", true, true, true)
command! -bang NeoTreeRevealToggle lua require("neo-tree").reveal_current_file("filesystem", true, "<bang>" == "!")
command! NeoTreeRevealInSplitToggle lua require("neo-tree").reveal_in_split("filesystem", true)
command! NeoTreeShowInSplitToggle lua require("neo-tree").show_in_split("filesystem", true)
command! NeoTreePasteConfig lua require("neo-tree").paste_default_config()
command! -nargs=? NeoTreeSetLogLevel lua require("neo-tree").set_log_level("<args>")
command! NeoTreeLogs lua require("neo-tree").show_logs()
endif
command! -nargs=* -complete=custom,v:lua.require'neo-tree.command'.complete_args
\ Neotree lua require("neo-tree.command")._command(<f-args>)

38
bundle/neo-tree.nvim/release.sh vendored Normal file
View File

@ -0,0 +1,38 @@
#/bin/bash
REPO="nvim-neo-tree/neo-tree.nvim"
LAST_VERSION=$(curl --silent "https://api.github.com/repos/$REPO/releases/latest" | jq -r .tag_name)
echo "LAST_VERSION=$LAST_VERSION"
MAJOR=$(cut -d. -f1 <<<"$LAST_VERSION")
MINOR=$(cut -d. -f2 <<<"$LAST_VERSION")
echo
RELEASE_BRANCH="${1:-v${MAJOR}.x}"
echo "RELEASE_BRANCH=$RELEASE_BRANCH"
NEXT_VERSION=$MAJOR.$((MINOR+1))
NEW_VERSION="${2:-${NEXT_VERSION}}"
echo "NEW_VERSION=$NEW_VERSION"
echo
read -p "Are you sure you want to publish this release? " -n 1 -r
echo # (optional) move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
[[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 # handle exits from shell or function but don't exit interactive shell
fi
git fetch
git checkout main
git pull
echo "Merging to ${RELEASE_BRANCH}"
git checkout $RELEASE_BRANCH
git pull
if git merge --ff-only origin/main; then
git push
git tag -a $NEW_VERSION -m "Release ${NEW_VERSION}"
git push origin $NEW_VERSION
echo "Creating Release"
gh release create $NEW_VERSION --generate-notes
else
echo "RELEASE FAILED! Could not fast-forward release to $RELEASE_BRANCH"
fi
git checkout main

85
bundle/neo-tree.nvim/scripts/test.sh vendored Normal file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
luacov_dir=""
while [[ $# -gt 0 ]]; do
case "${1}" in
--clean)
shift
echo "[test] cleaning up environment"
rm -rf ./.testcache
echo "[test] envionment cleaned"
;;
*)
shift
;;
esac
done
function setup_environment() {
echo
echo "[test] setting up environment"
echo
local plugins_dir="./.testcache/site/pack/vendor/start"
if [[ ! -d "${plugins_dir}" ]]; then
mkdir -p "${plugins_dir}"
fi
if [[ ! -d "${plugins_dir}/nui.nvim" ]]; then
echo "[plugins] nui.nvim: installing..."
git clone https://github.com/MunifTanjim/nui.nvim "${plugins_dir}/nui.nvim"
echo "[plugins] nui.nvim: installed"
echo
fi
if [[ ! -d "${plugins_dir}/nvim-web-devicons" ]]; then
echo "[plugins] nvim-web-devicons: installing..."
git clone https://github.com/nvim-tree/nvim-web-devicons "${plugins_dir}/nvim-web-devicons"
echo "[plugins] nvim-web-devicons: installed"
echo
fi
if [[ ! -d "${plugins_dir}/plenary.nvim" ]]; then
echo "[plugins] plenary.nvim: installing..."
git clone https://github.com/nvim-lua/plenary.nvim "${plugins_dir}/plenary.nvim"
# this commit broke luacov
#git -C "${plugins_dir}/plenary.nvim" revert --no-commit 9069d14a120cadb4f6825f76821533f2babcab92
echo "[plugins] plenary.nvim: installed"
echo
fi
echo "[test] environment ready"
echo
}
function luacov_start() {
luacov_dir="$(dirname "$(luarocks which luacov 2>/dev/null | head -1)")"
if [[ "${luacov_dir}" == "." ]]; then
luacov_dir=""
fi
if test -n "${luacov_dir}"; then
rm -f luacov.*.out
export LUA_PATH=";;${luacov_dir}/?.lua"
fi
}
function luacov_end() {
if test -n "${luacov_dir}"; then
luacov
echo
tail -n +$(($(grep -n "^Summary$" luacov.report.out | cut -d":" -f1) - 1)) luacov.report.out
fi
}
setup_environment
#luacov_start
make test
#luacov_end

30
bundle/neo-tree.nvim/tests/mininit.lua vendored Normal file
View File

@ -0,0 +1,30 @@
-- Need the absolute path as when doing the testing we will issue things like `tcd` to change directory
-- to where our temporary filesystem lives
local root_dir = vim.fn.fnamemodify(vim.trim(vim.fn.system("git rev-parse --show-toplevel")), ":p")
package.path = string.format("%s;%s?.lua;%s?/init.lua", package.path, root_dir, root_dir)
vim.opt.packpath:prepend(string.format("%s", root_dir .. ".testcache/site"))
vim.opt.rtp = {
root_dir,
vim.env.VIMRUNTIME,
}
vim.cmd([[
filetype on
packadd plenary.nvim
packadd nui.nvim
packadd nvim-web-devicons
]])
vim.opt.swapfile = false
vim.cmd([[
runtime plugin/neo-tree.vim
]])
-- For debugging
P = function(...)
print(unpack(vim.tbl_map(vim.inspect, { ... })))
end

View File

@ -0,0 +1,102 @@
pcall(require, "luacov")
local Path = require("plenary.path")
local u = require("tests.utils")
local verify = require("tests.utils.verify")
local run_in_current_command = function(command, expected_tree_node)
local winid = vim.api.nvim_get_current_win()
vim.cmd(command)
verify.window_handle_is(winid)
verify.buf_name_endswith(string.format("neo-tree filesystem [%s]", winid), 1000)
if expected_tree_node then
verify.filesystem_tree_node_is(expected_tree_node, winid)
end
end
describe("Command", function()
local test = u.fs.init_test({
items = {
{
name = "foo",
type = "dir",
items = {
{
name = "bar",
type = "dir",
items = {
{ name = "baz1.txt", type = "file" },
{ name = "baz2.txt", type = "file", id = "deepfile2" },
},
},
},
},
{ name = "topfile1.txt", type = "file", id = "topfile1" },
{ name = "topfile2.txt", type = "file", id = "topfile2" },
},
})
test.setup()
local fs_tree = test.fs_tree
after_each(function()
u.clear_environment()
end)
describe("netrw style:", function()
it("`:Neotree current` should show neo-tree in current window", function()
local cmd = "Neotree current"
run_in_current_command(cmd)
end)
it(
"`:Neotree current reveal` should show neo-tree and reveal file in current window",
function()
local cmd = "Neotree current reveal"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_in_current_command(cmd, testfile)
end
)
it("`:Neotree current reveal toggle` should toggle neo-tree in current window", function()
local cmd = "Neotree current reveal toggle"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
local tree_winid = vim.api.nvim_get_current_win()
-- toggle OPEN
run_in_current_command(cmd, testfile)
-- toggle CLOSE
vim.cmd(cmd)
verify.window_handle_is(tree_winid)
verify.buf_name_is(testfile)
end)
it(
"`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is not a parent of file",
function()
vim.cmd("cd ~")
local testfile = fs_tree.lookup["deepfile2"].abspath
local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile
run_in_current_command(cmd, testfile)
end
)
it(
"`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is a parent of file",
function()
local testfile = fs_tree.lookup["deepfile2"].abspath
local testfile_dir = Path:new(testfile):parent().filename
vim.cmd(string.format("cd %s", testfile_dir))
local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile
run_in_current_command(cmd, testfile)
end
)
end)
test.teardown()
end)

View File

@ -0,0 +1,206 @@
pcall(require, "luacov")
local u = require("tests.utils")
local verify = require("tests.utils.verify")
local run_focus_command = function(command, expected_tree_node)
local winid = vim.api.nvim_get_current_win()
vim.cmd(command)
verify.window_handle_is_not(winid)
verify.buf_name_endswith("neo-tree filesystem [1]")
if expected_tree_node then
verify.filesystem_tree_node_is(expected_tree_node)
end
end
local run_show_command = function(command, expected_tree_node)
local starting_winid = vim.api.nvim_get_current_win()
local starting_bufname = vim.api.nvim_buf_get_name(0)
local expected_num_windows = #vim.api.nvim_list_wins() + 1
vim.cmd(command)
verify.eventually(500, function()
if #vim.api.nvim_list_wins() ~= expected_num_windows then
return false
end
if vim.api.nvim_get_current_win() ~= starting_winid then
return false
end
if vim.api.nvim_buf_get_name(0) ~= starting_bufname then
return false
end
if expected_tree_node then
verify.filesystem_tree_node_is(expected_tree_node)
end
return true
end, "Expected to see a new window without focusing it.")
end
describe("Command", function()
local test = u.fs.init_test({
items = {
{
name = "foo",
type = "dir",
items = {
{
name = "bar",
type = "dir",
items = {
{ name = "baz1.txt", type = "file" },
{ name = "baz2.txt", type = "file", id = "deepfile2" },
},
},
{ name = "foofile1.txt", type = "file" },
},
},
{ name = "topfile1.txt", type = "file", id = "topfile1" },
},
})
test.setup()
local fs_tree = test.fs_tree
after_each(function()
u.clear_environment()
end)
describe("with reveal:", function()
it("`:Neotree float reveal` should reveal the current file in the floating window", function()
local cmd = "Neotree float reveal"
local testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
it("`:Neotree reveal toggle` should toggle the reveal-state of the tree", function()
local cmd = "Neotree reveal toggle"
local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath
u.editfile(testfile)
-- toggle OPEN
run_focus_command(cmd, testfile)
local tree_winid = vim.api.nvim_get_current_win()
-- toggle CLOSE
vim.cmd(cmd)
verify.window_handle_is_not(tree_winid)
verify.buf_name_is(testfile)
-- toggle OPEN with a different file
testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
it(
"`:Neotree float reveal toggle` should toggle the reveal-state of the floating window",
function()
local cmd = "Neotree float reveal toggle"
local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath
u.editfile(testfile)
-- toggle OPEN
run_focus_command(cmd, testfile)
local tree_winid = vim.api.nvim_get_current_win()
-- toggle CLOSE
vim.cmd("Neotree float reveal toggle")
verify.window_handle_is_not(tree_winid)
verify.buf_name_is(testfile)
-- toggle OPEN
testfile = fs_tree.lookup["./foo/bar/baz2.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end
)
it("`:Neotree reveal` should reveal the current file in the sidebar", function()
local cmd = "Neotree reveal"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
end)
for _, follow_current_file in ipairs({ true, false }) do
require("neo-tree").setup({
filesystem = {
follow_current_file = follow_current_file,
},
})
describe(string.format("w/ follow_current_file=%s", follow_current_file), function()
describe("with show :", function()
it("`:Neotree show` should show the window without focusing", function()
local cmd = "Neotree show"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_show_command(cmd)
end)
it("`:Neotree show toggle` should retain the focused node on next show", function()
local cmd = "Neotree show toggle"
local topfile = fs_tree.lookup["topfile1"].abspath
local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
-- focus a sub node to see if state is retained
u.editfile(baz)
run_focus_command(":Neotree reveal", baz)
local expected_tree_node = baz
verify.after(500, function()
-- toggle CLOSE
vim.cmd(cmd)
-- toggle OPEN
u.editfile(topfile)
if follow_current_file then
expected_tree_node = topfile
end
run_show_command(cmd, expected_tree_node)
return true
end)
end)
end)
describe("with focus :", function()
it("`:Neotree focus` should show the window and focus it", function()
local cmd = "Neotree focus"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_focus_command(cmd)
end)
it("`:Neotree focus toggle` should retain the focused node on next focus", function()
local cmd = "Neotree focus toggle"
local topfile = fs_tree.lookup["topfile1"].abspath
local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
-- focus a sub node to see if state is retained
u.editfile(baz)
run_focus_command("Neotree reveal", baz)
local expected_tree_node = baz
verify.after(500, function()
-- toggle CLOSE
vim.cmd(cmd)
-- toggle OPEN
u.editfile(topfile)
if follow_current_file then
expected_tree_node = topfile
end
run_focus_command(cmd, expected_tree_node)
return true
end)
end)
end)
end)
end
test.teardown()
end)

View File

@ -0,0 +1,28 @@
pcall(require, "luacov")
describe("Event queue", function()
it("should return data when handled = true", function()
local events = require("neo-tree.events")
events.subscribe({
event = "test",
handler = function()
return { data = "first" }
end,
})
events.subscribe({
event = "test",
handler = function()
return { handled = true, data = "second" }
end,
})
events.subscribe({
event = "test",
handler = function()
return { data = "third" }
end,
})
local result = events.fire_event("test") or {}
local data = result.data
assert.are.same("second", data)
end)
end)

View File

@ -0,0 +1,146 @@
pcall(require, "luacov")
local ns_id = require("neo-tree.ui.highlights").ns_id
local u = require("tests.utils")
local config = {
renderers = {
directory = {
{
"container",
content = {
{ "indent", zindex = 10 },
{ "icon", zindex = 10 },
{ "name", zindex = 10 },
{ "name", zindex = 5, align = "right" },
},
},
},
file = {
{
"container",
content = {
{ "indent", zindex = 10 },
{ "icon", zindex = 10 },
{ "name", zindex = 10 },
{ "name", zindex = 20, align = "right" },
},
},
},
},
window = {
width = 40,
},
}
local config_right = {
renderers = {
directory = {
{
"container",
enable_character_fade = false,
content = {
{ "indent", zindex = 10, align = "right" },
{ "icon", zindex = 10, align = "right" },
{ "name", zindex = 10, align = "right" },
},
},
},
file = {
{
"container",
enable_character_fade = false,
content = {
{ "indent", zindex = 10, align = "right" },
{ "icon", zindex = 10, align = "right" },
{ "name", zindex = 10, align = "right" },
},
},
},
},
window = {
width = 40,
},
}
local test_dir = {
items = {
{
name = "foo",
type = "dir",
items = {
{
name = "bar",
type = "dir",
items = {
{ name = "bar1.txt", type = "file" },
{ name = "bar2.txt", type = "file" },
},
},
{ name = "foo1.lua", type = "file" },
},
},
{ name = "bazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbaz", type = "dir" },
{ name = "1.md", type = "file" },
},
}
describe("sources/components/container", function()
local req_switch = u.get_require_switch()
local test = u.fs.init_test(test_dir)
test.setup()
after_each(function()
if req_switch then
req_switch.restore()
end
u.clear_environment()
end)
describe("should expand to width", function()
for pow = 4, 8 do
it(2 ^ pow, function()
config.window.width = 2 ^ pow
require("neo-tree").setup(config)
vim.cmd([[Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
assert.equals(vim.bo.filetype, "neo-tree")
local width = vim.api.nvim_win_get_width(0)
local lines = vim.api.nvim_buf_get_lines(0, 2, -1, false)
for _, line in ipairs(lines) do
assert.is_true(#line >= width)
end
end)
end
end)
describe("right-align should matches width", function()
for pow = 4, 8 do
it(2 ^ pow, function()
config_right.window.width = 2 ^ pow
require("neo-tree").setup(config_right)
vim.cmd([[Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
assert.equals(vim.bo.filetype, "neo-tree")
local width = vim.api.nvim_win_get_width(0)
local lines = vim.api.nvim_buf_get_lines(0, 1, -1, false)
for _, line in ipairs(lines) do
line = vim.fn.trim(line, " ", 2)
assert.equals(width, vim.fn.strchars(line))
end
end)
end
end)
test.teardown()
end)

View File

@ -0,0 +1,204 @@
pcall(require, "luacov")
local u = require("tests.utils")
local verify = require("tests.utils.verify")
local run_focus_command = function(command, expected_tree_node)
local winid = vim.api.nvim_get_current_win()
vim.cmd(command)
verify.window_handle_is_not(winid)
verify.buf_name_endswith("neo-tree filesystem [1]")
if expected_tree_node then
verify.filesystem_tree_node_is(expected_tree_node)
end
end
local run_show_command = function(command, expected_tree_node)
local starting_winid = vim.api.nvim_get_current_win()
local starting_bufname = vim.api.nvim_buf_get_name(0)
local expected_num_windows = #vim.api.nvim_list_wins() + 1
vim.cmd(command)
verify.eventually(500, function()
if #vim.api.nvim_list_wins() ~= expected_num_windows then
return false
end
if vim.api.nvim_get_current_win() ~= starting_winid then
return false
end
if vim.api.nvim_buf_get_name(0) ~= starting_bufname then
return false
end
if expected_tree_node then
verify.filesystem_tree_node_is(expected_tree_node)
end
return true
end, "Expected to see a new window without focusing it.")
end
describe("Filesystem", function()
local test = u.fs.init_test({
items = {
{
name = "foo",
type = "dir",
items = {
{
name = "bar",
type = "dir",
items = {
{ name = "baz1.txt", type = "file" },
{ name = "baz2.txt", type = "file", id = "deepfile2" },
},
},
{ name = "foofile1.txt", type = "file" },
},
},
{ name = "topfile1.txt", type = "file", id = "topfile1" },
},
})
test.setup()
local fs_tree = test.fs_tree
after_each(function()
u.clear_environment()
end)
describe("reveal command", function()
it("should reveal the current file in the sidebar", function()
local cmd = "NeoTreeReveal"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
it("should reveal the current file in the floating window", function()
local cmd = "NeoTreeFloat"
local testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
it("should toggle the reveal-state of the tree", function()
local cmd = "NeoTreeRevealToggle"
local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath
u.editfile(testfile)
-- toggle OPEN
run_focus_command(cmd, testfile)
local tree_winid = vim.api.nvim_get_current_win()
-- toggle CLOSE
vim.cmd(cmd)
verify.window_handle_is_not(tree_winid)
verify.buf_name_is(testfile)
-- toggle OPEN with a different file
testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
it("should toggle the reveal-state of the floating window", function()
local cmd = "NeoTreeFloatToggle"
local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath
u.editfile(testfile)
-- toggle OPEN
run_focus_command(cmd, testfile)
local tree_winid = vim.api.nvim_get_current_win()
-- toggle CLOSE
vim.cmd("NeoTreeRevealToggle")
verify.window_handle_is_not(tree_winid)
verify.buf_name_is(testfile)
vim.wait(1000)
-- toggle OPEN
testfile = fs_tree.lookup["./foo/bar/baz2.txt"].abspath
u.editfile(testfile)
run_focus_command(cmd, testfile)
end)
end)
for _, follow_current_file in ipairs({ true, false }) do
require("neo-tree").setup({
filesystem = {
follow_current_file = follow_current_file,
},
})
describe(string.format("w/ follow_current_file=%s", follow_current_file), function()
describe("show command", function()
it("should show the window without focusing", function()
local cmd = "NeoTreeShow"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_show_command(cmd)
end)
it("should retain the focused node on next show", function()
local cmd = "NeoTreeShowToggle"
local topfile = fs_tree.lookup["topfile1"].abspath
local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
-- focus a sub node to see if state is retained
u.editfile(baz)
run_focus_command("NeoTreeReveal", baz)
local expected_tree_node = baz
verify.after(500, function()
-- toggle CLOSE
vim.cmd(cmd)
-- toggle OPEN
u.editfile(topfile)
if follow_current_file then
expected_tree_node = topfile
end
run_show_command(cmd, expected_tree_node)
return true
end)
end)
end)
describe("focus command", function()
it("should show the window and focus it", function()
local cmd = "NeoTreeFocus"
local testfile = fs_tree.lookup["topfile1"].abspath
u.editfile(testfile)
run_focus_command(cmd)
end)
it("should retain the focused node on next focus", function()
local cmd = "NeoTreeFocusToggle"
local topfile = fs_tree.lookup["topfile1"].abspath
local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath
-- focus a sub node to see if state is retained
u.editfile(baz)
run_focus_command("NeoTreeReveal", baz)
local expected_tree_node = baz
verify.after(500, function()
-- toggle CLOSE
vim.cmd(cmd)
-- toggle OPEN
u.editfile(topfile)
if follow_current_file then
expected_tree_node = topfile
end
run_focus_command(cmd, expected_tree_node)
return true
end)
end)
end)
end)
end
test.teardown()
end)

View File

@ -0,0 +1,92 @@
pcall(require, "luacov")
local u = require("tests.utils")
local verify = require("tests.utils.verify")
describe("Filesystem netrw hijack", function()
after_each(function()
u.clear_environment()
end)
it("does not interfere with netrw when disabled", function()
require("neo-tree").setup({
filesystem = {
hijack_netrw_behavior = "disabled",
window = {
position = "left",
},
},
})
vim.cmd("edit .")
assert(#vim.api.nvim_list_wins() == 1, "there should only be one window")
verify.after(100, function()
local name = vim.api.nvim_buf_get_name(0)
return name ~= "neo-tree filesystem [1]"
end, "the buffer should not be neo-tree")
end)
it("opens in sidebar when behavior is open_default", function()
local file = "Makefile"
vim.cmd("edit " .. file)
require("neo-tree").setup({
filesystem = {
hijack_netrw_behavior = "open_default",
window = {
position = "left",
},
},
})
vim.cmd("edit .")
verify.eventually(200, function()
return #vim.api.nvim_list_wins() == 2
end, "there should be two windows")
verify.buf_name_endswith("neo-tree filesystem [1]")
verify.eventually(100, function()
local expected_buf_name = "Makefile"
local buf_at_2 = vim.api.nvim_win_get_buf(vim.fn.win_getid(2))
local name_at_2 = vim.api.nvim_buf_get_name(buf_at_2)
if name_at_2:sub(-#expected_buf_name) == expected_buf_name then
return true
else
return false
end
end, file .. " is not at window 2")
end)
it("opens in in splits when behavior is open_current", function()
local file = "Makefile"
vim.cmd("edit " .. file)
require("neo-tree").setup({
filesystem = {
hijack_netrw_behavior = "open_current",
},
})
assert(#vim.api.nvim_list_wins() == 1, "Test should start with one window")
vim.cmd("edit .")
verify.eventually(200, function()
assert(#vim.api.nvim_list_wins() == 1, "`edit .` should not open a new window")
return vim.api.nvim_buf_get_option(0, "filetype") == "neo-tree"
end, "neotree is not the only window")
vim.cmd("split .")
verify.eventually(200, function()
if #vim.api.nvim_list_wins() ~= 2 then
return false
end
return vim.api.nvim_buf_get_option(0, "filetype") == "neo-tree"
end, "neotree is not in the second window")
end)
end)

View File

@ -0,0 +1,125 @@
pcall(require, "luacov")
local u = require("tests.utils")
local verify = require("tests.utils.verify")
local manager = require('neo-tree.sources.manager')
local get_dirs = function(winid)
winid = winid or vim.api.nvim_get_current_win()
local tabnr = vim.api.nvim_tabpage_get_number(vim.api.nvim_win_get_tabpage(winid))
local winnr = vim.api.nvim_win_get_number(winid)
return {
win = vim.fn.getcwd(winnr),
tab = vim.fn.getcwd(-1, tabnr),
global = vim.fn.getcwd(-1, -1),
}
end
local get_state_for_tab = function(tabid)
for _, state in ipairs(manager._get_all_states()) do
if state.tabid == tabid then
return state
end
end
end
local get_tabnr = function(tabid)
return vim.api.nvim_tabpage_get_number(tabid or vim.api.nvim_get_current_tabpage())
end
describe("Manager", function()
local test = u.fs.init_test({
items = {
{
name = "foo",
type = "dir",
items = {
{ name = "foofile1.txt", type = "file" },
},
},
{ name = "topfile1.txt", type = "file", id = "topfile1" },
},
})
test.setup()
local fs_tree = test.fs_tree
-- Just make sure we start all tests in the expected state
before_each(function()
u.eq(1, #vim.api.nvim_list_wins())
u.eq(1, #vim.api.nvim_list_tabpages())
vim.cmd.lcd(fs_tree.abspath)
vim.cmd.tcd(fs_tree.abspath)
vim.cmd.cd(fs_tree.abspath)
end)
after_each(function()
u.clear_environment()
end)
local setup_2_tabs = function()
-- create 2 tabs
local tab1 = vim.api.nvim_get_current_tabpage()
local win1 = vim.api.nvim_get_current_win()
vim.cmd.tabnew()
local tab2 = vim.api.nvim_get_current_tabpage()
local win2 = vim.api.nvim_get_current_win()
u.neq(tab2, tab1)
u.neq(win2, win1)
-- set different directories
vim.api.nvim_set_current_tabpage(tab2)
local base_dir = vim.fn.getcwd()
vim.cmd.tcd('foo')
local new_dir = vim.fn.getcwd()
-- open neo-tree
vim.api.nvim_set_current_tabpage(tab1)
vim.cmd.Neotree('show')
vim.api.nvim_set_current_tabpage(tab2)
vim.cmd.Neotree('show')
return {
tab1 = tab1,
tab2 = tab2,
win1 = win1,
win2 = win2,
tab1_dir = base_dir,
tab2_dir = new_dir,
}
end
it("should respect changed tab cwd", function()
local ctx = setup_2_tabs()
local state1 = get_state_for_tab(ctx.tab1)
local state2 = get_state_for_tab(ctx.tab2)
u.eq(ctx.tab1_dir, manager.get_cwd(state1))
u.eq(ctx.tab2_dir, manager.get_cwd(state2))
end)
it("should have correct tab cwd after tabs order is changed", function()
local ctx = setup_2_tabs()
-- tab numbers should be the same as ids
u.eq(1, get_tabnr(ctx.tab1))
u.eq(2, get_tabnr(ctx.tab2))
-- swap tabs
vim.cmd.tabfirst()
vim.cmd.tabmove('+1')
-- make sure tabs have been swapped
u.eq(2, get_tabnr(ctx.tab1))
u.eq(1, get_tabnr(ctx.tab2))
-- verify that tab dirs are the same as nvim tab cwd
local state1 = get_state_for_tab(ctx.tab1)
local state2 = get_state_for_tab(ctx.tab2)
u.eq(get_dirs(ctx.win1).tab, manager.get_cwd(state1))
u.eq(get_dirs(ctx.win2).tab, manager.get_cwd(state2))
end)
end)

View File

@ -0,0 +1,235 @@
pcall(require, "luacov")
local ns_id = require("neo-tree.ui.highlights").ns_id
local u = require("tests.utils")
describe("ui/icons", function()
local req_switch = u.get_require_switch()
local test = u.fs.init_test({
items = {
{
name = "foo",
type = "dir",
items = {
{
name = "bar",
type = "dir",
items = {
{ name = "bar1.txt", type = "file" },
{ name = "bar2.txt", type = "file" },
},
},
{ name = "foo1.lua", type = "file" },
},
},
{ name = "baz", type = "dir" },
{ name = "1.md", type = "file" },
},
})
test.setup()
local fs_tree = test.fs_tree
after_each(function()
if req_switch then
req_switch.restore()
end
u.clear_environment()
end)
describe("w/ default_config", function()
before_each(function()
require("neo-tree").setup({})
end)
it("works w/o nvim-web-devicons", function()
req_switch.disable_package("nvim-web-devicons")
vim.cmd([[:Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
u.assert_buf_lines(bufnr, {
string.format("  %s", fs_tree.abspath):sub(1, 42),
"  baz",
"  foo",
" * 1.md",
})
vim.api.nvim_win_set_cursor(winid, { 2, 0 })
u.feedkeys("<CR>")
vim.api.nvim_win_set_cursor(winid, { 3, 0 })
u.feedkeys("<CR>")
vim.wait(100)
u.assert_buf_lines(bufnr, {
string.format("  %s", fs_tree.abspath):sub(1, 42),
" ﰊ baz",
"  foo",
" │  bar",
" └ * foo1.lua",
" * 1.md",
})
u.assert_highlight(bufnr, ns_id, 1, "", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 2, "", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 4, "", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 5, "* ", "NeoTreeFileIcon")
end)
it("works w/ nvim-web-devicons", function()
vim.cmd([[:Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
u.assert_buf_lines(bufnr, {
vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40),
"  baz",
"  foo",
"  1.md",
})
vim.api.nvim_win_set_cursor(winid, { 2, 0 })
u.feedkeys("<CR>")
vim.api.nvim_win_set_cursor(winid, { 3, 0 })
u.feedkeys("<CR>")
vim.wait(100)
u.assert_buf_lines(bufnr, {
vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40),
" ﰊ baz",
"  foo",
" │  bar",
" └  foo1.lua",
"  1.md",
})
u.assert_highlight(bufnr, ns_id, 1, "", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 2, "", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 4, "", "NeoTreeDirectoryIcon")
local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, "")
u.eq(#extmarks, 1)
u.neq(extmarks[1][4].hl_group, "NeoTreeFileIcon")
end)
end)
describe("custom config", function()
local config
before_each(function()
config = {
default_component_configs = {
icon = {
folder_closed = "c",
folder_open = "o",
folder_empty = "e",
default = "f",
highlight = "TestNeoTreeFileIcon",
},
},
}
require("neo-tree").setup(config)
end)
it("works w/o nvim-web-devicons", function()
req_switch.disable_package("nvim-web-devicons")
vim.cmd([[:Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
u.assert_buf_lines(bufnr, {
string.format(" o %s", fs_tree.abspath):sub(1, 40),
" c baz",
" c foo",
" f 1.md",
})
vim.api.nvim_win_set_cursor(winid, { 2, 0 })
u.feedkeys("<CR>")
vim.api.nvim_win_set_cursor(winid, { 3, 0 })
u.feedkeys("<CR>")
vim.wait(100)
u.assert_buf_lines(bufnr, {
string.format(" o %s", fs_tree.abspath):sub(1, 40),
" e baz",
" o foo",
" │ c bar",
" └ f foo1.lua",
" f 1.md",
})
u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 5, "f ", config.default_component_configs.icon.highlight)
end)
it("works w/ nvim-web-devicons", function()
vim.cmd([[:Neotree focus]])
u.wait_for(function()
return vim.bo.filetype == "neo-tree"
end)
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
u.assert_buf_lines(bufnr, {
vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40),
" c baz",
" c foo",
"  1.md",
})
vim.api.nvim_win_set_cursor(winid, { 2, 0 })
u.feedkeys("<CR>")
vim.api.nvim_win_set_cursor(winid, { 3, 0 })
u.feedkeys("<CR>")
vim.wait(100)
u.assert_buf_lines(bufnr, {
vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40),
" e baz",
" o foo",
" │ c bar",
" └  foo1.lua",
"  1.md",
})
u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon")
u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon")
local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, "")
u.eq(#extmarks, 1)
u.neq(extmarks[1][4].hl_group, config.default_component_configs.icon.highlight)
end)
end)
test.teardown()
end)

94
bundle/neo-tree.nvim/tests/utils/fs.lua vendored Normal file
View File

@ -0,0 +1,94 @@
local Path = require("plenary.path")
local fs = {}
function fs.create_temp_dir()
-- Resolve for two reasons.
-- 1. Follow any symlinks which make comparing paths fail. (on macOS, TMPDIR can be under /var which is symlinked to
-- /private/var)
-- 2. Remove any double separators (on macOS TMPDIR can end in a trailing / which absolute doesn't remove, this should
-- be coverted by https://github.com/nvim-lua/plenary.nvim/issues/330).
local temp_dir = vim.fn.resolve(
Path:new(
vim.fn.fnamemodify(vim.fn.tempname(), ":h"),
string.format("neo-tree-test-%s", vim.fn.rand())
):absolute()
)
vim.fn.mkdir(temp_dir, "p")
return temp_dir
end
function fs.create_dir(path)
local abspath = Path:new(path):absolute()
vim.fn.mkdir(abspath, "p")
end
function fs.remove_dir(dir, recursive)
if vim.fn.isdirectory(dir) == 1 then
return vim.fn.delete(dir, recursive and "rf" or "d") == 0
end
return false
end
function fs.write_file(path, content)
local abspath = Path:new(path):absolute()
fs.create_dir(vim.fn.fnamemodify(abspath, ":h"))
vim.fn.writefile(content or {}, abspath)
end
function fs.create_fs_tree(fs_tree)
local function create_items(items, basedir, relative_root_path)
relative_root_path = relative_root_path or "."
for _, item in ipairs(items) do
local relative_path = relative_root_path .. "/" .. item.name
-- create lookups
fs_tree.lookup[relative_path] = item
if item.id then
fs_tree.lookup[item.id] = item
end
-- create actual files and directories
if item.type == "dir" then
item.abspath = Path:new(basedir, item.name):absolute()
fs.create_dir(item.abspath)
if item.items then
create_items(item.items, item.abspath, relative_path)
end
elseif item.type == "file" then
item.abspath = Path:new(basedir, item.name):absolute()
fs.write_file(item.abspath)
end
end
end
create_items(fs_tree.items, fs_tree.abspath)
return fs_tree
end
function fs.init_test(fs_tree)
fs_tree.lookup = {}
if not fs_tree.abspath then
fs_tree.abspath = fs.create_temp_dir()
end
local function setup()
fs.remove_dir(fs_tree.abspath, true)
fs.create_fs_tree(fs_tree)
vim.cmd("tcd " .. fs_tree.abspath)
end
local function teardown()
fs.remove_dir(fs_tree.abspath, true)
end
return {
fs_tree = fs_tree,
setup = setup,
teardown = teardown,
}
end
return fs

View File

@ -0,0 +1,183 @@
local mod = {
fs = require("tests.utils.fs"),
}
function mod.clear_environment()
-- Create fresh window
vim.cmd("top new | wincmd o")
local keepbufnr = vim.api.nvim_get_current_buf()
-- Clear ALL neo-tree state
require("neo-tree.sources.manager")._clear_state()
-- Cleanup any remaining buffers
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if bufnr ~= keepbufnr then
vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
assert(#vim.api.nvim_tabpage_list_wins(0) == 1, "Failed to properly clear tab")
assert(#vim.api.nvim_list_bufs() == 1, "Failed to properly clear buffers")
end
mod.editfile = function(testfile)
vim.cmd("e " .. testfile)
assert.are.same(
vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":p"),
vim.fn.fnamemodify(testfile, ":p")
)
end
function mod.eq(...)
return assert.are.same(...)
end
function mod.neq(...)
return assert["not"].are.same(...)
end
---@param keys string
---@param mode? string
function mod.feedkeys(keys, mode)
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), mode or "x", true)
end
---@param tbl table
---@param keys string[]
function mod.tbl_pick(tbl, keys)
if not keys or #keys == 0 then
return tbl
end
local new_tbl = {}
for _, key in ipairs(keys) do
new_tbl[key] = tbl[key]
end
return new_tbl
end
local orig_require = _G.require
-- can be used to enable/disable package
-- for specific tests
function mod.get_require_switch()
local disabled_packages = {}
local function fake_require(name)
if vim.tbl_contains(disabled_packages, name) then
return error("test: package disabled")
end
return orig_require(name)
end
return {
disable_package = function(name)
_G.require = fake_require
package.loaded[name] = nil
table.insert(disabled_packages, name)
end,
enable_package = function(name)
_G.require = fake_require
disabled_packages = vim.tbl_filter(function(package_name)
return package_name ~= name
end, disabled_packages)
end,
restore = function()
disabled_packages = {}
_G.require = orig_require
end,
}
end
---@param bufnr number
---@param lines string[]
---@param linenr_start? integer (1-indexed)
---@param linenr_end? integer (1-indexed, inclusive)
function mod.assert_buf_lines(bufnr, lines, linenr_start, linenr_end)
mod.eq(
vim.api.nvim_buf_get_lines(
bufnr,
linenr_start and linenr_start - 1 or 0,
linenr_end or -1,
false
),
lines
)
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param byte_start? integer (0-indexed)
---@param byte_end? integer (0-indexed, inclusive)
function mod.get_line_extmarks(bufnr, ns_id, linenr, byte_start, byte_end)
return vim.api.nvim_buf_get_extmarks(
bufnr,
ns_id,
{ linenr - 1, byte_start or 0 },
{ linenr - 1, byte_end and byte_end + 1 or -1 },
{ details = true }
)
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param text string
---@return table[]
---@return { byte_start: integer, byte_end: integer } info (byte range: 0-indexed, inclusive)
function mod.get_text_extmarks(bufnr, ns_id, linenr, text)
local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1]
local byte_start = string.find(line, text) -- 1-indexed
byte_start = byte_start - 1 -- 0-indexed
local byte_end = byte_start + #text - 1 -- inclusive
local extmarks = vim.api.nvim_buf_get_extmarks(
bufnr,
ns_id,
{ linenr - 1, byte_start },
{ linenr - 1, byte_end },
{ details = true }
)
return extmarks, { byte_start = byte_start, byte_end = byte_end }
end
---@param extmark table
---@param linenr number (1-indexed)
---@param text string
---@param hl_group string
function mod.assert_extmark(extmark, linenr, text, hl_group)
mod.eq(extmark[2], linenr - 1)
if text then
local start_col = extmark[3]
mod.eq(extmark[4].end_col - start_col, #text)
end
mod.eq(mod.tbl_pick(extmark[4], { "end_row", "hl_group" }), {
end_row = linenr - 1,
hl_group = hl_group,
})
end
---@param bufnr number
---@param ns_id integer
---@param linenr integer (1-indexed)
---@param text string
---@param hl_group string
function mod.assert_highlight(bufnr, ns_id, linenr, text, hl_group)
local extmarks, info = mod.get_text_extmarks(bufnr, ns_id, linenr, text)
mod.eq(#extmarks, 1)
mod.eq(extmarks[1][3], info.byte_start)
mod.assert_extmark(extmarks[1], linenr, text, hl_group)
end
---@param callback fun(): boolean
---@param options? { interval?: integer, timeout?: integer }
function mod.wait_for(callback, options)
options = options or {}
vim.wait(options.timeout or 1000, callback, options.interval or 100)
end
return mod

View File

@ -0,0 +1,106 @@
local verify = {}
verify.eventually = function(timeout, assertfunc, failmsg, ...)
local success, args = false, { ... }
vim.wait(timeout or 1000, function()
success = assertfunc(unpack(args))
return success
end)
assert(success, failmsg)
end
verify.after = function(timeout, assertfunc, failmsg)
vim.wait(timeout, function()
return false
end)
assert(assertfunc(), failmsg)
end
verify.bufnr_is = function(bufnr, timeout)
verify.eventually(timeout or 500, function()
return bufnr == vim.api.nvim_get_current_buf()
end, string.format("Current buffer is expected to be '%s' but is not", bufnr))
end
verify.bufnr_is_not = function(bufnr, timeout)
verify.eventually(timeout or 500, function()
return bufnr ~= vim.api.nvim_get_current_buf()
end, string.format("Current buffer is '%s' when expected to not be", bufnr))
end
verify.buf_name_endswith = function(buf_name, timeout)
verify.eventually(
timeout or 500,
function()
if buf_name == "" then
return true
end
local n = vim.api.nvim_buf_get_name(0)
if n:sub(-#buf_name) == buf_name then
return true
else
return false
end
end,
string.format("Current buffer name is expected to be end with '%s' but it does not", buf_name)
)
end
verify.buf_name_is = function(buf_name, timeout)
verify.eventually(timeout or 500, function()
return buf_name == vim.api.nvim_buf_get_name(0)
end, string.format("Current buffer name is expected to be '%s' but is not", buf_name))
end
verify.tree_focused = function(timeout)
verify.eventually(timeout or 1000, function()
return vim.api.nvim_buf_get_option(0, "filetype") == "neo-tree"
end, "Current buffer is not a 'neo-tree' filetype")
end
verify.tree_node_is = function(source_name, expected_node_id, winid, timeout)
verify.eventually(timeout or 500, function()
local state = require("neo-tree.sources.manager").get_state(source_name, nil, winid)
if not state.tree then
return false
end
local success, node = pcall(state.tree.get_node, state.tree)
if not success then
return false
end
if not node then
return false
end
local node_id = node:get_id()
if node_id == expected_node_id then
return true
end
return false
end, string.format("Tree node '%s' not focused", expected_node_id))
end
verify.filesystem_tree_node_is = function(expected_node_id, winid, timeout)
verify.tree_node_is("filesystem", expected_node_id, winid, timeout)
end
verify.buffers_tree_node_is = function(expected_node_id, winid, timeout)
verify.tree_node_is("buffers", expected_node_id, winid, timeout)
end
verify.git_status_tree_node_is = function(expected_node_id, winid, timeout)
verify.tree_node_is("git_status", expected_node_id, winid, timeout)
end
verify.window_handle_is = function(winid, timeout)
verify.eventually(timeout or 500, function()
return winid == vim.api.nvim_get_current_win()
end, string.format("Current window handle is expected to be '%s' but is not", winid))
end
verify.window_handle_is_not = function(winid, timeout)
verify.eventually(timeout or 500, function()
return winid ~= vim.api.nvim_get_current_win()
end, string.format("Current window handle is not expected to be '%s' but it is", winid))
end
return verify

10
bundle/nui.nvim/.codecov.yml vendored Normal file
View File

@ -0,0 +1,10 @@
coverage:
status:
project:
default:
informational: true
only_pulls: true
patch:
default:
informational: true
only_pulls: true

122
bundle/nui.nvim/.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,122 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: luacheck
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get Cache Key
id: luver-cache-key
env:
CI_RUNNER_OS: ${{ runner.os }}
CI_SECRETS_CACHE_VERSION: ${{ secrets.CACHE_VERSION }}
run: |
echo "::set-output name=value::${CI_RUNNER_OS}-luver-${CI_SECRETS_CACHE_VERSION}-$(date -u +%Y-%m-%d)"
shell: bash
- name: Setup Cache
uses: actions/cache@v2
with:
path: ~/.local/share/luver
key: ${{ steps.luver-cache-key.outputs.value }}
- name: Setup Lua
uses: MunifTanjim/luver-action@v1
with:
default: 5.1.5
lua_versions: 5.1.5
luarocks_versions: 5.1.5:3.8.0
- name: Setup luacheck
run: |
luarocks install luacheck
- name: Lint
run: ./scripts/lint.sh --no-cache
format:
name: stylua
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Check Format
uses: JohnnyMorganz/stylua-action@1.0.0
with:
version: 0.13.1
token: ${{ secrets.GITHUB_TOKEN }}
args: --color always --check lua/
test:
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get Cache Key
id: luver-cache-key
env:
CI_RUNNER_OS: ${{ runner.os }}
CI_SECRETS_CACHE_VERSION: ${{ secrets.CACHE_VERSION }}
run: |
echo "::set-output name=value::${CI_RUNNER_OS}-luver-${CI_SECRETS_CACHE_VERSION}-$(date -u +%Y-%m-%d)"
shell: bash
- name: Setup Cache
uses: actions/cache@v2
with:
path: ~/.local/share/luver
key: ${{ steps.luver-cache-key.outputs.value }}
- name: Setup Lua
uses: MunifTanjim/luver-action@v1
with:
default: 5.1.5
lua_versions: 5.1.5
luarocks_versions: 5.1.5:3.8.0
- name: Setup luacov
run: |
luarocks install luacov
- name: Setup Neovim
uses: MunifTanjim/setup-neovim-action@v1
with:
tag: nightly
- name: Run Tests
run: |
nvim --version
./scripts/test.sh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
verbose: true
release:
name: release
if: ${{ github.ref == 'refs/heads/main' }}
needs:
- lint
- format
- test
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
pull-requests: write
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: simple
package-name: nui.nvim
bump-minor-pre-major: true
pull-request-title-pattern: "chore: release ${version}"
include-v-in-tag: false
- name: Trigger Publish
if: ${{ steps.release.outputs.release_created }}
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ steps.release.outputs.tag_name }}
run: |
gh workflow run --repo ${GITHUB_REPOSITORY} publish.yml -f version=${TAG_NAME}

View File

@ -0,0 +1,33 @@
name: Publish
on:
push:
tags:
- "[0-1].[0-9]+.[0-9]+"
workflow_dispatch:
inputs:
version:
description: Version to publish
required: false
type: string
force:
description: Force publish
required: false
default: false
type: boolean
jobs:
publish:
name: publish
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: LuaRocks Publish
uses: MunifTanjim/luarocks-publish-action@v1
with:
lua_version: 5.1.5
luarocks_version: 3.9.1
version: ${{ inputs.version }}
api_key: ${{ secrets.LUAROCKS_API_KEY }}
force: ${{ inputs.force }}

5
bundle/nui.nvim/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.luacheckcache
luacov.*.out
.testcache

16
bundle/nui.nvim/.luacheckrc vendored Normal file
View File

@ -0,0 +1,16 @@
cache = ".luacheckcache"
-- https://luacheck.readthedocs.io/en/stable/warnings.html
ignore = {
"211/_.*",
"212/_.*",
"213/_.*",
}
include_files = { "*.luacheckrc", "lua/**/*.lua", "tests/**/*.lua" }
globals = { "vim" }
std = "luajit"
files["tests/helpers/**/*.lua"] = {
read_globals = { "assert", "describe" },
}
-- vim: set filetype=lua :

3
bundle/nui.nvim/.luacov vendored Normal file
View File

@ -0,0 +1,3 @@
include = {
"lua%/nui",
}

6
bundle/nui.nvim/.stylua.toml vendored Normal file
View File

@ -0,0 +1,6 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
no_call_parentheses = false

234
bundle/nui.nvim/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,234 @@
# Changelog
## 0.1.0 (2023-05-27)
### ⚠ BREAKING CHANGES
* **line:** change parameter order for methods.
* **text:** change parameter order for methods.
### Features
* accept multiple keys for keymap ([4998347](https://github.com/MunifTanjim/nui.nvim/commit/4998347f02fde115ad0c023b90be9c5654834635))
* add internal utils._.calculate_gap_width ([90d7285](https://github.com/MunifTanjim/nui.nvim/commit/90d7285c182b396c21ba69684068fd7d5d6eba79))
* add util to clear namespace for buffer ([6c63bf5](https://github.com/MunifTanjim/nui.nvim/commit/6c63bf5fd51076455d1b04f5c09d9841ab081263))
* **bar:** add lower-level `core.add_highlight` function ([20385a6](https://github.com/MunifTanjim/nui.nvim/commit/20385a698e8a5dd98ee7e63f16b700a10b921098))
* **bar:** add some lower-level `core.add_*` functions ([5d1ca66](https://github.com/MunifTanjim/nui.nvim/commit/5d1ca66829d8fac9965cd18fcc2cd9aa49ba1ea5))
* **bar:** initial implementation ([35758e9](https://github.com/MunifTanjim/nui.nvim/commit/35758e946a64376e0e9625a27469410b3d1f9223))
* **bar:** remove module ([0dc148c](https://github.com/MunifTanjim/nui.nvim/commit/0dc148c6ec06577fcf06cbab3b7dac96d48ba6be))
* **input:** add component Input ([7307c94](https://github.com/MunifTanjim/nui.nvim/commit/7307c94a7a0954f1c717e094c39772f28bb9d13f))
* **input:** move internal default_value and prompt prop ([595a2ea](https://github.com/MunifTanjim/nui.nvim/commit/595a2ea90f7d31bc23386ba67939117d45771035))
* **input:** support nui.text for options.prompt ([c380563](https://github.com/MunifTanjim/nui.nvim/commit/c38056355f5a72c7a1a005f7125afd62ed7b2083))
* **layout:** add method layout:update ([3f611a6](https://github.com/MunifTanjim/nui.nvim/commit/3f611a674254370d6d73f9a35a3eedbc8d07ee3b))
* **layout:** add some util for size and position ([cc3b970](https://github.com/MunifTanjim/nui.nvim/commit/cc3b970c1537de9562f82e5fa6aa126a629242d1))
* **layout:** feature guard lua autocmd api usage ([3dc6b89](https://github.com/MunifTanjim/nui.nvim/commit/3dc6b89bda8f1616a2638727eeb75a4a770e1c21))
* **layout:** initial implementation ([716c3f9](https://github.com/MunifTanjim/nui.nvim/commit/716c3f9d857b62a086b4f9123d0d34d1b7733922))
* **layout:** introduce layout type ([a5fd005](https://github.com/MunifTanjim/nui.nvim/commit/a5fd005263d238d2fbd6ee335e06139645f11fa9))
* **layout:** introduce split layout ([042cceb](https://github.com/MunifTanjim/nui.nvim/commit/042cceb497cc4cfa3ae735a5e7bc01b4b6f19ef1))
* **layout:** make window transparent ([2d33512](https://github.com/MunifTanjim/nui.nvim/commit/2d33512836cb603d58cb538e716a02c44547c6ab))
* **layout:** re-use windows for unchanged split layout box ([71ddaaf](https://github.com/MunifTanjim/nui.nvim/commit/71ddaafadfd8b3c21f809ea26fb4fe2a110bbf54))
* **layout:** support :show and :hide for float layout ([f572782](https://github.com/MunifTanjim/nui.nvim/commit/f572782bd8cc6b6b8671566b09146c4c09f20ae2))
* **layout:** support container component ([5bc7376](https://github.com/MunifTanjim/nui.nvim/commit/5bc737602e231111c44458c84e3eca05393f355f))
* **layout:** support o.grow factor for layout.box ([796dc82](https://github.com/MunifTanjim/nui.nvim/commit/796dc8293be59acc0c0d53b99e7dca7c32db4413))
* **layout:** support o.grow for layout.box ([2c6bac9](https://github.com/MunifTanjim/nui.nvim/commit/2c6bac942e3af35eb42165c923c0d3dc60a46430))
* **layout:** throw for empty box at init ([6e872b3](https://github.com/MunifTanjim/nui.nvim/commit/6e872b3bf28aaf52fd1b831ea0690aea205d6464))
* **layout:** tweak component wire up ([eed888e](https://github.com/MunifTanjim/nui.nvim/commit/eed888e47fa91980ce295dd347b213a102200b5a))
* **layout:** use backported autocmd methods ([4e21085](https://github.com/MunifTanjim/nui.nvim/commit/4e21085cd1b6be12da7ff0e7168e9100c51c363a))
* **layout:** wire up float layout components ([3aa617d](https://github.com/MunifTanjim/nui.nvim/commit/3aa617d9054e052cde68cd3141db4396af93a9cb))
* **line:** accept initial nui.text objects ([2f44cc9](https://github.com/MunifTanjim/nui.nvim/commit/2f44cc941cf1a129b6b6069a7489672b24cf0015))
* **line:** accept NuiText object as param for line:append() method ([34fd4bf](https://github.com/MunifTanjim/nui.nvim/commit/34fd4bfde84ff4b735a98dfb3508280d39e520f6))
* **line:** add method :width ([80122e5](https://github.com/MunifTanjim/nui.nvim/commit/80122e542fcebc361c4ca585be40a071c6e360be))
* **line:** add nui.line block ([1333fd0](https://github.com/MunifTanjim/nui.nvim/commit/1333fd07c57310d1421dcb337a21fbddb20d3c84))
* **line:** make ns_id required, update method signature ([5695bde](https://github.com/MunifTanjim/nui.nvim/commit/5695bde7ac9b8bcbb1df237a05169bad99458090))
* **line:** support nui.line in method :append ([401a69f](https://github.com/MunifTanjim/nui.nvim/commit/401a69f27ede2d5a9a725f4f45758708d3b72d09))
* make components extendable ([c75976e](https://github.com/MunifTanjim/nui.nvim/commit/c75976e823085218aeef4f5312a8c88e7d333358))
* **menu:** add component Menu ([dca0630](https://github.com/MunifTanjim/nui.nvim/commit/dca0630b0d7ab5dfcabaa70284c94c2e133b8200))
* **menu:** expose .tree ([d12a697](https://github.com/MunifTanjim/nui.nvim/commit/d12a6977846b2fa978bff89b439e509320854e10))
* **menu:** improve menu separator implementation ([0e05425](https://github.com/MunifTanjim/nui.nvim/commit/0e0542505369861fc22f6bff3ad2976bdef6d8f3))
* **menu:** move internal props ([0373a94](https://github.com/MunifTanjim/nui.nvim/commit/0373a94bc79725726aadcc1e0e26de159e3152e1))
* **menu:** pass self to on_change callback ([4438c5e](https://github.com/MunifTanjim/nui.nvim/commit/4438c5e53f8a5c834569309a5d3e276e6bf1abe9))
* **menu:** rename method menu:init to menu:new ([2825c3d](https://github.com/MunifTanjim/nui.nvim/commit/2825c3d60438bbed9c04f18f030ab3505249e8a7))
* **menu:** simplify automatic width calculation ([54cbaf5](https://github.com/MunifTanjim/nui.nvim/commit/54cbaf5d227cfedbb939d16b405116e4cea6e3fb))
* **menu:** support arbritary props for Menu.item ([77cefa6](https://github.com/MunifTanjim/nui.nvim/commit/77cefa67df7f282db20aa9afb5925aff79455dc2))
* **menu:** support nui.line for Menu.item ([51cbd0c](https://github.com/MunifTanjim/nui.nvim/commit/51cbd0ccc9410e317a947eea1e99966226a5f8b5))
* **menu:** support nui.text as separator char ([3029554](https://github.com/MunifTanjim/nui.nvim/commit/30295541c1bda2ab171ab7b684f790be3fbb60a7))
* **menu:** support nui.text for Menu.item ([13e557d](https://github.com/MunifTanjim/nui.nvim/commit/13e557d045b62efc6da87bdcc77503b5adf1db21))
* **menu:** support options.on_change ([db06fed](https://github.com/MunifTanjim/nui.nvim/commit/db06feddf324d4c6c8763fe9fca43b9140de84a9))
* **object:** add helper functions ([9531977](https://github.com/MunifTanjim/nui.nvim/commit/95319774b31558479c71e9b190870aae7bd8e49f))
* **object:** initial implementation ([194837f](https://github.com/MunifTanjim/nui.nvim/commit/194837ffc4a9c77dfcc18809c7e43a02b4d11e03))
* **popup:** add method .border:set_highlight ([acb72b1](https://github.com/MunifTanjim/nui.nvim/commit/acb72b150c7fdf01331b08460ec07fe7a81029b6))
* **popup:** add method popup:set_layout(config) ([006711f](https://github.com/MunifTanjim/nui.nvim/commit/006711f3ab4e8626f47b2b903759a9ac0e232fbd))
* **popup:** add method popup:set_position(position, relative) ([7ea1a6b](https://github.com/MunifTanjim/nui.nvim/commit/7ea1a6b910b1b33fceb33068df8f31e14ccceb1e))
* **popup:** add method popup:set_size(size) ([07b6e9a](https://github.com/MunifTanjim/nui.nvim/commit/07b6e9a90b58af8ca9c09f3695aa14603c947b61))
* **popup:** add method popup:unmap ([46bbf33](https://github.com/MunifTanjim/nui.nvim/commit/46bbf336e9068c73c4e810cb93c7a1f8197ebbc3))
* **popup:** add method popup.border:set_text(...) ([b82a5d3](https://github.com/MunifTanjim/nui.nvim/commit/b82a5d3bda43b5cd9b9dca39b782e86d2735933f))
* **popup:** add methods popup:hide() and popup:show() ([b3c706d](https://github.com/MunifTanjim/nui.nvim/commit/b3c706d4c30bd5cd8b05d10a2c8bf8fcd1f5cf7a))
* **popup:** add methods popup:on(...) and popup:off(...) ([0eb57a2](https://github.com/MunifTanjim/nui.nvim/commit/0eb57a2bdd565dfad09e4bda4293ef11c17d741f))
* **popup:** add option 'focusable' ([8339965](https://github.com/MunifTanjim/nui.nvim/commit/8339965e991ad54e6606ea22213996521701e293))
* **popup:** add option padding ([596cd77](https://github.com/MunifTanjim/nui.nvim/commit/596cd77a875eef1c6629e12074139e2ca0033e38))
* **popup:** add options 'buf_options' and 'win_options' ([163d99a](https://github.com/MunifTanjim/nui.nvim/commit/163d99a3fb9fe9a630c96bbf6159973885c3caf3))
* **popup:** add options.ns_id ([6f165aa](https://github.com/MunifTanjim/nui.nvim/commit/6f165aad4d2ac5a5a279a695ed247ec954cdcc4b))
* **popup:** add type annotation for bufnr,winid,ns_id ([c2d1f73](https://github.com/MunifTanjim/nui.nvim/commit/c2d1f73eb0ab02e90e8316a6e70158d2eec72153))
* **popup:** add type annotation for win_config ([f8ccc5c](https://github.com/MunifTanjim/nui.nvim/commit/f8ccc5cf8e8aec7ff34572b80e7b5d92b2d07556))
* **popup:** allow layout refresh when container size changes ([ee8d315](https://github.com/MunifTanjim/nui.nvim/commit/ee8d315456691fc1d05dcec465d95f8aec902541))
* **popup:** change behavior of padding ([9f29df4](https://github.com/MunifTanjim/nui.nvim/commit/9f29df4153da1483ad784a473f5bbcf02fcbbe3b))
* **popup:** clear namespace object on unmount ([58e06b0](https://github.com/MunifTanjim/nui.nvim/commit/58e06b0175cf22672d96a522343ec6ce017ba54c))
* **popup:** create buffer on initialization ([6b1deda](https://github.com/MunifTanjim/nui.nvim/commit/6b1deda411b96f0d694dead7cc12ab9db1dd9b65))
* **popup:** default border.text hl to FloatTitle ([4eaec2a](https://github.com/MunifTanjim/nui.nvim/commit/4eaec2ac66af2ca6ddddd3f665ad0909b90ae36a))
* **popup:** feature guard lua autocmd api usage ([6028584](https://github.com/MunifTanjim/nui.nvim/commit/60285847a4df99c2ab0ece3020eb930c81b09c41))
* **popup:** improve border highlight implementation ([2f58c40](https://github.com/MunifTanjim/nui.nvim/commit/2f58c406eb9cb5cedf5d82f9ce7c4f6efc418550))
* **popup:** improve cleanup ([220d4a4](https://github.com/MunifTanjim/nui.nvim/commit/220d4a4bbaa8f7dc80c0aa37e8377c7c150c6384))
* **popup:** merge internal position_meta into position ([bc2fc9c](https://github.com/MunifTanjim/nui.nvim/commit/bc2fc9c1b7b7241c4a0f34597f8ddc11c5f90844))
* **popup:** move internal buf_options and win_options ([3148908](https://github.com/MunifTanjim/nui.nvim/commit/31489084b7d3363c9a6f8f4d435374a25f4f636b))
* **popup:** move internal loading and mounted state ([4dc3214](https://github.com/MunifTanjim/nui.nvim/commit/4dc321448faaad171f0aab07d092a0e626fb1cbb))
* **popup:** move internal position state ([d229bbb](https://github.com/MunifTanjim/nui.nvim/commit/d229bbb4846bd4268ca1f6f67543294237ee0029))
* **popup:** move internal position_meta state ([cbbbe90](https://github.com/MunifTanjim/nui.nvim/commit/cbbbe90213091a462e71a1f102e0927ac11bac8a))
* **popup:** move internal size prop ([0a2fced](https://github.com/MunifTanjim/nui.nvim/commit/0a2fcedb97673ba478e26a0f59edbca950d15d31))
* **popup:** move internal win_enter prop ([f7736c9](https://github.com/MunifTanjim/nui.nvim/commit/f7736c90db395adc276519bd871bcec088bed908))
* **popup:** remove automatic cleanup ([06b48cf](https://github.com/MunifTanjim/nui.nvim/commit/06b48cf645971e1e1d598a083b98733890a82302))
* **popup:** remove method popup:on(...) ([c24b131](https://github.com/MunifTanjim/nui.nvim/commit/c24b13195c3e8c7c42d64736d96ded0ce7cb13ed))
* **popup:** rename method :set_layout to :update_layout ([90f59b0](https://github.com/MunifTanjim/nui.nvim/commit/90f59b035565e549e467fc414f1178d30c074ce9))
* **popup:** rename window to popup ([7097509](https://github.com/MunifTanjim/nui.nvim/commit/7097509b4b0fd21d870cea19c51389043c408449))
* **popup:** rework border internals ([8a776a2](https://github.com/MunifTanjim/nui.nvim/commit/8a776a2033220b16fa032b9d3590c75a178435ec))
* **popup:** simplify border highlight mechanism ([0ec30d9](https://github.com/MunifTanjim/nui.nvim/commit/0ec30d912c473139832d5c0bfd8ccfa675a48974))
* **popup:** simplify border.text ([674305a](https://github.com/MunifTanjim/nui.nvim/commit/674305a0cb1df2b2c0f51ade7081235755e93643))
* **popup:** support nui.text for simple border ([5f5a2e5](https://github.com/MunifTanjim/nui.nvim/commit/5f5a2e5284a08f930098383a6d0a00b861fc58d2))
* **popup:** support NuiText as border.text ([1248c67](https://github.com/MunifTanjim/nui.nvim/commit/1248c674a4a632ea7d45a76b1255df997379e116))
* **popup:** support option 'anchor' ([a86c733](https://github.com/MunifTanjim/nui.nvim/commit/a86c733e7d30596d80927b5eaf6869aa9a3d30af))
* **popup:** support set_size when unmounted ([fa86f85](https://github.com/MunifTanjim/nui.nvim/commit/fa86f8539373e1b76b93f95dfa36a5793ed4fe35))
* **popup:** support unmanaged buffer ([335415a](https://github.com/MunifTanjim/nui.nvim/commit/335415af52ea23d07433aa1f72f7c0d56c219316))
* **popup:** use backported autocmd methods ([372369d](https://github.com/MunifTanjim/nui.nvim/commit/372369dea4c059f831e540e34b9a49bf183c245b))
* **split:** add component Split ([bcb7382](https://github.com/MunifTanjim/nui.nvim/commit/bcb73828d54169f5ae9d141bca05fecb6aec5ec5))
* **split:** add method :update_layout ([32f44a6](https://github.com/MunifTanjim/nui.nvim/commit/32f44a610691ff2e22e60a8040801e047112806f))
* **split:** add method split:unmap ([e0444e0](https://github.com/MunifTanjim/nui.nvim/commit/e0444e020fd3ed336e2b6ba998ff7903ddb68ddd))
* **split:** create buffer on initialization ([0e36b78](https://github.com/MunifTanjim/nui.nvim/commit/0e36b78b836200ef83ef723c6a0ebb753ac8a11e))
* **split:** feature guard lua autocmd api usage ([d14daab](https://github.com/MunifTanjim/nui.nvim/commit/d14daab6710d1ebd7ec868f15bddde2bbd2c186f))
* **split:** improve cleanup ([36b0649](https://github.com/MunifTanjim/nui.nvim/commit/36b0649d7df3d1899d5aed8b27d22159cf058a2e))
* **split:** move internal buf_options and win_options ([6c9a3ee](https://github.com/MunifTanjim/nui.nvim/commit/6c9a3ee9fdb2c7a48cf13d9c82ee56822c5bfdb8))
* **split:** move internal loading and mounted state ([653199c](https://github.com/MunifTanjim/nui.nvim/commit/653199c635ad56c1e313e0890c6a72da8d3ee3bd))
* **split:** move internal props ([25e51eb](https://github.com/MunifTanjim/nui.nvim/commit/25e51eba14cdf2d445d0c0efde78d808741483cb))
* **split:** set buffer after .winid is set ([35091ca](https://github.com/MunifTanjim/nui.nvim/commit/35091ca0f8c766ea3542508f3ab9217a746cc5a0))
* **split:** store id internally ([96ef1cb](https://github.com/MunifTanjim/nui.nvim/commit/96ef1cb4e3c830dc79e18acac209ea5f7eb78829))
* **split:** support o.enter ([b75e2e6](https://github.com/MunifTanjim/nui.nvim/commit/b75e2e6d2a86a1105a756b894385d5fe836c3821))
* **split:** support o.ns_id ([28cafab](https://github.com/MunifTanjim/nui.nvim/commit/28cafab82f5d5ffc4321b9f82bae7b38951434d1))
* **split:** support o.relative.winid ([82851af](https://github.com/MunifTanjim/nui.nvim/commit/82851af021d651bb6bf6da0991c79f4c529ff37e))
* **split:** tweak split size handling for new window ([b681ab2](https://github.com/MunifTanjim/nui.nvim/commit/b681ab2a8a8750b37dcece4e87105a5671c39745))
* **split:** use backported autocmd methods ([6bd1d8a](https://github.com/MunifTanjim/nui.nvim/commit/6bd1d8af326c654a8b9d122c32a62fed5e55fe89))
* **text:** add method :new ([4ad7811](https://github.com/MunifTanjim/nui.nvim/commit/4ad781109a44bc00fa7d1208576f4086add27ad3))
* **text:** add method text:set(content, highlight?) ([eaf4844](https://github.com/MunifTanjim/nui.nvim/commit/eaf4844e9e84994705acc0347673a6b15e7c030f))
* **text:** add nui.text block ([a6df800](https://github.com/MunifTanjim/nui.nvim/commit/a6df800df514c2f0a9dd88d8db267ec5b8c8d68d))
* **text:** change highlight table key group-&gt;hl_group ([a8aaca1](https://github.com/MunifTanjim/nui.nvim/commit/a8aaca1578dff3504e78538b477910de344af0f8))
* **text:** make ns_id required, update method signature ([b63d199](https://github.com/MunifTanjim/nui.nvim/commit/b63d199ddfdc89457ae401ed0a5659fa1055c37c))
* **text:** preserve own extmark id ([26622d1](https://github.com/MunifTanjim/nui.nvim/commit/26622d147762f2212bf30e0792df1d0164a73cd9))
* **text:** remove method :new ([18a0390](https://github.com/MunifTanjim/nui.nvim/commit/18a0390d5caca31b310af41787e5ddd407c68783))
* **text:** return self from method text:set ([c5971ed](https://github.com/MunifTanjim/nui.nvim/commit/c5971ed28773cf9b6ce4ed8bfbc20266aa05b2a1))
* **text:** support cloning and use extmarks ([281d453](https://github.com/MunifTanjim/nui.nvim/commit/281d4535d046b1dd09d45bca8b0739bb16461b75))
* **text:** support extmark override when cloning ([878dfaf](https://github.com/MunifTanjim/nui.nvim/commit/878dfaf85fa19ff6811f19f1dad0bbfda77d5bbf))
* **tree:** add method node:get_child_ids ([bc05620](https://github.com/MunifTanjim/nui.nvim/commit/bc056204ff06b205dd3804474fc155180e704d47))
* **tree:** add method tree:get_nodes ([f85aedc](https://github.com/MunifTanjim/nui.nvim/commit/f85aedc1378acb375e4da0ec2220c1df0929e843))
* **tree:** add method tree:set_nodes(nodes, parent_id?) ([b25fab5](https://github.com/MunifTanjim/nui.nvim/commit/b25fab59d997cd9793bf7f42a1e41b9b7684d987))
* **tree:** add nui.tree block ([3bdfa78](https://github.com/MunifTanjim/nui.nvim/commit/3bdfa780fdba053358b85fd1f276052ae1455b61))
* **tree:** add o.bufnr and deprecate o.winid ([ce4869f](https://github.com/MunifTanjim/nui.nvim/commit/ce4869f97e4f3d4f9eb64d021fff775684ae8859))
* **tree:** clear namespace before render ([1f66cc7](https://github.com/MunifTanjim/nui.nvim/commit/1f66cc794bb6cad23f97b71e099cc32dd63c0614))
* **tree:** make node:has_children method work before init ([96f600b](https://github.com/MunifTanjim/nui.nvim/commit/96f600b58f128bde4a78a56c0a64d55664a43955))
* **tree:** move internal buf_options and win_options ([3a7c0c4](https://github.com/MunifTanjim/nui.nvim/commit/3a7c0c48b279ee0495c5ec61ca312fddd052195c))
* **tree:** move internal get_node_id and prepare_node functions ([1b9e046](https://github.com/MunifTanjim/nui.nvim/commit/1b9e04685097b93a4f8d01002adaaa5dcfb327af))
* **tree:** pass parent_node to prepare_node function ([559d33d](https://github.com/MunifTanjim/nui.nvim/commit/559d33dcace8016603129e63e5cb605aaa059c10))
* **tree:** rename options.ns to options.ns_id ([5db3901](https://github.com/MunifTanjim/nui.nvim/commit/5db390110bf9944b678c84cd7bcd2a28af712481))
* **tree:** return end linenr from tree:get_node method ([8ae5e31](https://github.com/MunifTanjim/nui.nvim/commit/8ae5e3106a0fa17144a0a086ccfce1e73a73f19f))
* **tree:** return linenr from tree:get_node method ([3b746d7](https://github.com/MunifTanjim/nui.nvim/commit/3b746d7b6f16818a970d1c4810261d18b958a956))
* **tree:** support linenr for method tree:get_node ([7fee7c6](https://github.com/MunifTanjim/nui.nvim/commit/7fee7c6c176e83806a144f7e0a8ac6251920a8a7))
* **tree:** support linenr_start for method :render ([afb9e5b](https://github.com/MunifTanjim/nui.nvim/commit/afb9e5b4512e17d879fb069e77de4141472d0fb9))
* **tree:** support multiline node ([4926ee9](https://github.com/MunifTanjim/nui.nvim/commit/4926ee9ba8fac49ad23cd695f1f5c0952c52dd4a))
* **tree:** support nil return for o.prepare_node ([5a79b1b](https://github.com/MunifTanjim/nui.nvim/commit/5a79b1b3b8231cfa33290d8d3c56d36b6496499e))
* use api-autocmd if available ([3f05d74](https://github.com/MunifTanjim/nui.nvim/commit/3f05d742b273ed4b48db4e5ab99c09b8b05cd537))
* use native keymap callback if supported ([ad2c05c](https://github.com/MunifTanjim/nui.nvim/commit/ad2c05c983dda8d423d952fce55eb3cf3966a1ea))
* **utils:** add autocmd ([1f51d5a](https://github.com/MunifTanjim/nui.nvim/commit/1f51d5a6735153ea5e3f9a1f68a12c9f1bce5681))
* **utils:** add buf_storage ([6300e3b](https://github.com/MunifTanjim/nui.nvim/commit/6300e3bdcc2fc38e361a48b76247c4524a0fb27a))
* **utils:** backport autocmd to nvim &lt; 0.7.x ([587a49f](https://github.com/MunifTanjim/nui.nvim/commit/587a49f90fb036a6d94904851dafdd53d1327fe0))
* **utils:** move keymap to utils ([56c2230](https://github.com/MunifTanjim/nui.nvim/commit/56c223041ad342b2a8d28d2bb1dbd88dc0d7839a))
* **utils:** update autocmd.event ([6f9153c](https://github.com/MunifTanjim/nui.nvim/commit/6f9153cc8462a3bb0a8a883694befa6554d31403))
* **window:** add method window:destroy() ([c51856d](https://github.com/MunifTanjim/nui.nvim/commit/c51856da53df1327a406e7983d5fd748486fc339))
* **window:** add method window:map(...) ([f69ee04](https://github.com/MunifTanjim/nui.nvim/commit/f69ee0498ecc57268d23a4b51516002404c3def1))
* **window:** add method window:on(event_name, handler) ([f36b496](https://github.com/MunifTanjim/nui.nvim/commit/f36b496b52aec95dc9e122830b0d2ff64eeb21f5))
* **window:** add method window:render() ([d1a047d](https://github.com/MunifTanjim/nui.nvim/commit/d1a047d22d0794943a08a2de598ac57c61fc4978))
* **window:** add option highlight ([c4bbe61](https://github.com/MunifTanjim/nui.nvim/commit/c4bbe6139f012aca971ea26721dc4c1942b71286))
* **window:** change default border to none ([8db2faa](https://github.com/MunifTanjim/nui.nvim/commit/8db2faa3dfa9b196fe2fd11311c534b37db2428a))
* **window:** enhanced border support ([a17070c](https://github.com/MunifTanjim/nui.nvim/commit/a17070c6b55ba36db82edef0c3af28ac0929e649))
* **window:** initial implementation ([19e4bb6](https://github.com/MunifTanjim/nui.nvim/commit/19e4bb669d2fdd168a6d0a3edebaca150f93678f))
* **window:** rename 'destroy' to 'unmount' ([c409518](https://github.com/MunifTanjim/nui.nvim/commit/c409518e6640e50dc754b199cd37246f60a3fdbc))
* **window:** rename 'render' to 'mount' ([66190d2](https://github.com/MunifTanjim/nui.nvim/commit/66190d237fc61db2135c0c19eb9091091d9090dc))
* **window:** simplify option relative ([5584892](https://github.com/MunifTanjim/nui.nvim/commit/55848921914083d57e178448f3714f3ba4d9fc75))
### Bug Fixes
* **bar:** rename tabnr to tabid in context ([f220495](https://github.com/MunifTanjim/nui.nvim/commit/f2204952ba670372de507bdd446c6a14f821ac73))
* **bar:** type for generator ([2a6533f](https://github.com/MunifTanjim/nui.nvim/commit/2a6533fb798efad7dd783311315bab8dc5eb381b))
* **input:** escape multi-byte chars in default value ([698e758](https://github.com/MunifTanjim/nui.nvim/commit/698e75814cd7c56b0dd8af4936bcef2d13807f3c))
* **input:** stopinsert on close ([971cca4](https://github.com/MunifTanjim/nui.nvim/commit/971cca41914aaefc9569b8a1904cf8c25cec5aa8))
* **input:** try to keep stable cursor position on parent window ([6f803e8](https://github.com/MunifTanjim/nui.nvim/commit/6f803e88093573f73d4ee6c0dfe0575df3f97a9f))
* **layout:** apply size for first child box in split layout ([dde3f89](https://github.com/MunifTanjim/nui.nvim/commit/dde3f89b74b726eacfa6bea82c895af265573fe4))
* **layout:** float position calculation for child with complex border ([257da38](https://github.com/MunifTanjim/nui.nvim/commit/257da38029d3859ed111804f9d4e95b0fa993a31))
* **layout:** focus on relative.win for split ':update' ([6e8f9a0](https://github.com/MunifTanjim/nui.nvim/commit/6e8f9a0f280fada3f8ce694a42f8376a31e9776e))
* **layout:** o.relative for split layout ([bf5900f](https://github.com/MunifTanjim/nui.nvim/commit/bf5900f1b60bf6499755ac92315181a24a87a577))
* **layout:** preserve split win_options 'winfixheight' and 'winfixwidth' ([ecd9def](https://github.com/MunifTanjim/nui.nvim/commit/ecd9def93891b9260b15b5fcef542eaabf4145c9))
* **layout:** process float layout box change ([51721a4](https://github.com/MunifTanjim/nui.nvim/commit/51721a409794bf2e432e99acc21b635102fedcea))
* **layout:** process split layout box change ([b12db53](https://github.com/MunifTanjim/nui.nvim/commit/b12db5321c194c10eb34e610fb76ce2c058853fc))
* **layout:** typo in update_layout_config util ([d5d3d6c](https://github.com/MunifTanjim/nui.nvim/commit/d5d3d6ce542b3921f8a8da213ef7b0841f6a4adc))
* luacheck lint warnings ([e7dd31c](https://github.com/MunifTanjim/nui.nvim/commit/e7dd31c4135389e460a686d48d10ed532ca5234e))
* **menu:** error with fallback separator char ([76fc8ed](https://github.com/MunifTanjim/nui.nvim/commit/76fc8edf771adff74aba513bd0f62e984996ef88))
* **popup:** add border.style default value ([a07b754](https://github.com/MunifTanjim/nui.nvim/commit/a07b754552008012f2d7d3602b7a233a29d92c66))
* **popup:** border highlight for type:complex ([151d593](https://github.com/MunifTanjim/nui.nvim/commit/151d593b28911d61b10e1a8ba14f9e4c755141aa))
* **popup:** border padding with style=shadow ([37e0511](https://github.com/MunifTanjim/nui.nvim/commit/37e0511f189cd19eabd0e71841f10c3a39bbb62d))
* **popup:** check bufnr is valid before clearing namespace ([62facd3](https://github.com/MunifTanjim/nui.nvim/commit/62facd37e0dd8196212399a897374f689886f500))
* **popup:** check if border bufnr is valid before clearing namespace ([b99e6cb](https://github.com/MunifTanjim/nui.nvim/commit/b99e6cb13dc51768abc1c4c8585045a0c0459ef1))
* **popup:** do better mount/unmount handling ([7622fcf](https://github.com/MunifTanjim/nui.nvim/commit/7622fcf3dc4cffffc666d9f1f4e646168f640a2a))
* **popup:** do buf_storage.cleanup after buffer wipeout ([644e595](https://github.com/MunifTanjim/nui.nvim/commit/644e595a3862ca45de71ff14fa465019ecfc17ad))
* **popup:** do BufWinEnter autocmd after self.winid is set ([4c77e3a](https://github.com/MunifTanjim/nui.nvim/commit/4c77e3a064b7b0fdfb5f2729500a81b431ff86f8))
* **popup:** do not reset win_options on update_layout call ([1f43b13](https://github.com/MunifTanjim/nui.nvim/commit/1f43b13d133eb4b4f53a4485379d9afa58808389))
* **popup:** handle border padding without text ([1f9aebc](https://github.com/MunifTanjim/nui.nvim/commit/1f9aebcaca1f43c311ba16a2aad9170d597640a8))
* **popup:** handle border:set_text properly ([e78c822](https://github.com/MunifTanjim/nui.nvim/commit/e78c822378c102978792465dbf93b8ffb27a4cc8))
* **popup:** handle map as border.style ([26a0eea](https://github.com/MunifTanjim/nui.nvim/commit/26a0eeaca8890b74d53a91f84520abea94fcf8ee))
* **popup:** handle various mix of border and padding ([49a155f](https://github.com/MunifTanjim/nui.nvim/commit/49a155f3bbacb90a8b2df320f5b127b568083cd6))
* **popup:** highlight for simple border ([3ff3d26](https://github.com/MunifTanjim/nui.nvim/commit/3ff3d2628f3c14633792f702b3cfea377addcf47))
* **popup:** ignore WinClosed from other popup ([a501202](https://github.com/MunifTanjim/nui.nvim/commit/a5012020a48f5740bf30bb3468ca532cb7627997))
* **popup:** manual doautocmd BufWinEnter ([0807a9c](https://github.com/MunifTanjim/nui.nvim/commit/0807a9ca3274d5f89d02802aa00dac9fc2649864))
* **popup:** position relative to buffer position ([f369333](https://github.com/MunifTanjim/nui.nvim/commit/f36933397a0689a96106d9aa74db320286f5ffda))
* **popup:** properly apply winhighlight ([4396e44](https://github.com/MunifTanjim/nui.nvim/commit/4396e4427dc7ec80c58575096fdcad46d336a7ac))
* **popup:** remove mutation in border:get() ([1179f2e](https://github.com/MunifTanjim/nui.nvim/commit/1179f2e3f5245ab585e1154478f45bab1cf41867))
* **popup:** respect 'enter' option on subsequent mounts ([602e4d8](https://github.com/MunifTanjim/nui.nvim/commit/602e4d885fda5da9f015345fecb8c745ffedbf52))
* **popup:** set noautocmd for opening border window ([4715f60](https://github.com/MunifTanjim/nui.nvim/commit/4715f6092443f0b8fb9a3bcb0cfd03202bb03477))
* **popup:** set win_config.win explicitly if applicable ([d50d84a](https://github.com/MunifTanjim/nui.nvim/commit/d50d84a340a3088b8560bf74fa3d6f6ad0556134))
* **popup:** store winid for parent window ([03131f8](https://github.com/MunifTanjim/nui.nvim/commit/03131f8aa1873d08009ca214727a2c699d73dfe6))
* **popup:** take border size_delta into account when calculating position ([e67310b](https://github.com/MunifTanjim/nui.nvim/commit/e67310b23d21ebe8b12d9dbadb3dfa562dda5057))
* **popup:** track mounted state ([c1db7f8](https://github.com/MunifTanjim/nui.nvim/commit/c1db7f8377dfce4f724f35eec74868a15443dccf))
* **popup:** update position handling ([d87b561](https://github.com/MunifTanjim/nui.nvim/commit/d87b56193991e102516689f394101d1367bf8ef5))
* **popup:** update state and buffer handling for hide/show ([02e9262](https://github.com/MunifTanjim/nui.nvim/commit/02e9262d2d1820c2ed573f3a09848affc6526704))
* **popup:** use copy of border styles table item ([abd1a4a](https://github.com/MunifTanjim/nui.nvim/commit/abd1a4a23d0bbc0d0d4bc8faaa240330b44a4d5c))
* **popup:** use popup winhighlight for border ([cf67636](https://github.com/MunifTanjim/nui.nvim/commit/cf676363181aae149271226531abca341e583997))
* **split:** check bufnr is valid before clearing namespace ([e9889bb](https://github.com/MunifTanjim/nui.nvim/commit/e9889bbd9919544697d497537acacd9c67d0de99))
* **split:** do buf_storage.cleanup after buffer wipeout ([5bf5d62](https://github.com/MunifTanjim/nui.nvim/commit/5bf5d62531b473b01cae65ea389d4f788fb2341f))
* **split:** do not update position when unchanged ([6d86148](https://github.com/MunifTanjim/nui.nvim/commit/6d86148ad43554f51caa82e99d63a57be1654182))
* **split:** set size after open window ([c45ad68](https://github.com/MunifTanjim/nui.nvim/commit/c45ad685cb156174761f912c7697fdb6fd5a3b50))
* **split:** skip manual buf/win removal when pending quit ([cc76e6f](https://github.com/MunifTanjim/nui.nvim/commit/cc76e6ff13629b18d3dedfadd4f52e35ff085700))
* **split:** tweak container size relative to editor ([120fe69](https://github.com/MunifTanjim/nui.nvim/commit/120fe69bc4d96a13f89c2450ceb27fcd921b77ae))
* **split:** use self.winid for WinClosed ([747c20d](https://github.com/MunifTanjim/nui.nvim/commit/747c20d10a42f22a0125ee2e7397aee4c2422ffe))
* support nui.text for internal alignment util ([915fabe](https://github.com/MunifTanjim/nui.nvim/commit/915fabe334639c384ed5c66005046b96d4e8d30a))
* **text:** fix :highlight extmark.end_col ([3775746](https://github.com/MunifTanjim/nui.nvim/commit/3775746f8324db1ff5068eaf10f321db5e566611))
* **tree:** fix calculation for method :render ([70f2dad](https://github.com/MunifTanjim/nui.nvim/commit/70f2dadb73b5aa15727ec8f7a620818997505be5))
* **tree:** pass ns_id correctly to nui.line in :render method ([70fc6b6](https://github.com/MunifTanjim/nui.nvim/commit/70fc6b66c651538a78d393cf9fc830c81e919ca8))
* **tree:** remove children recursively in method remove_node ([4939282](https://github.com/MunifTanjim/nui.nvim/commit/4939282919885e1c83aff68ecb35b3cadf6015a9))
* **tree:** remove id from .nodes.root_ids on tree:remove_node ([792caa3](https://github.com/MunifTanjim/nui.nvim/commit/792caa3c1dc3efb22385b3b363c32567ba9cb059))
* **tree:** set default buf_options.bufhidden=hide ([7c7bdf4](https://github.com/MunifTanjim/nui.nvim/commit/7c7bdf40219bc274a849fa241daa47872c809fd8))
* **tree:** set default buf_options.undolevels=0 ([42552b3](https://github.com/MunifTanjim/nui.nvim/commit/42552b3797c3452c5c94e0c84a04fbda9591b9d1))
* vim.schedule QuitPre event callback ([d147222](https://github.com/MunifTanjim/nui.nvim/commit/d147222a1300901656f3ebd5b95f91732785a329))
* **window:** cleanup properly ([dbc8185](https://github.com/MunifTanjim/nui.nvim/commit/dbc81850faeeb70c303cdf3c50e0a4b6bbf154bd))
* **window:** set zindex properly ([2d427f7](https://github.com/MunifTanjim/nui.nvim/commit/2d427f7f9bac670523de2906776117192f243d8c))
* **window:** use 0-indexed position ([fbf96df](https://github.com/MunifTanjim/nui.nvim/commit/fbf96df95e437687206b9213924372c13c328b7a))
### Continuous Integration
* introduce automated release ([70560c4](https://github.com/MunifTanjim/nui.nvim/commit/70560c4e7b36ff974e7136d08aa4022e79a002f4))

21
bundle/nui.nvim/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Munif Tanjim
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

345
bundle/nui.nvim/README.md vendored Normal file
View File

@ -0,0 +1,345 @@
![GitHub Workflow Status: CI](https://img.shields.io/github/actions/workflow/status/MunifTanjim/nui.nvim/ci.yml?branch=main&label=CI&style=for-the-badge)
[![Coverage](https://img.shields.io/codecov/c/gh/MunifTanjim/nui.nvim/master?style=for-the-badge)](https://codecov.io/gh/MunifTanjim/nui.nvim)
[![Version](https://img.shields.io/luarocks/v/MunifTanjim/nui.nvim?color=%232c3e67&style=for-the-badge)](https://luarocks.org/modules/MunifTanjim/nui.nvim)
![License](https://img.shields.io/github/license/MunifTanjim/nui.nvim?color=%23000080&style=for-the-badge)
# nui.nvim
UI Component Library for Neovim.
## Requirements
- [Neovim 0.5.0](https://github.com/neovim/neovim/releases/tag/v0.5.0)
## Installation
Install the plugins with your preferred plugin manager. For example, with [`vim-plug`](https://github.com/junegunn/vim-plug):
```vim
Plug 'MunifTanjim/nui.nvim'
```
## Blocks
### [NuiText](lua/nui/text)
Quickly add highlighted text on the buffer.
**[Check Detailed Documentation for `nui.text`](lua/nui/text)**
**[Check Wiki Page for `nui.text`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.text)**
### [NuiLine](lua/nui/line)
Quickly add line containing highlighted text chunks on the buffer.
**[Check Detailed Documentation for `nui.line`](lua/nui/line)**
**[Check Wiki Page for `nui.line`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.line)**
### [NuiTree](lua/nui/tree)
Quickly render tree-like structured content on the buffer.
**[Check Detailed Documentation for `nui.tree`](lua/nui/tree)**
**[Check Wiki Page for `nui.tree`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.tree)**
## Components
### [Layout](lua/nui/layout)
![Layout GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/layout.gif)
```lua
local Popup = require("nui.popup")
local Layout = require("nui.layout")
local popup_one, popup_two = Popup({
enter = true,
border = "single",
}), Popup({
border = "double",
})
local layout = Layout(
{
position = "50%",
size = {
width = 80,
height = "60%",
},
},
Layout.Box({
Layout.Box(popup_one, { size = "40%" }),
Layout.Box(popup_two, { size = "60%" }),
}, { dir = "row" })
)
local current_dir = "row"
popup_one:map("n", "r", function()
if current_dir == "col" then
layout:update(Layout.Box({
Layout.Box(popup_one, { size = "40%" }),
Layout.Box(popup_two, { size = "60%" }),
}, { dir = "row" }))
current_dir = "row"
else
layout:update(Layout.Box({
Layout.Box(popup_two, { size = "60%" }),
Layout.Box(popup_one, { size = "40%" }),
}, { dir = "col" }))
current_dir = "col"
end
end, {})
layout:mount()
```
**[Check Detailed Documentation for `nui.layout`](lua/nui/layout)**
**[Check Wiki Page for `nui.layout`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.layout)**
### [Popup](lua/nui/popup)
![Popup GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/popup.gif)
```lua
local Popup = require("nui.popup")
local event = require("nui.utils.autocmd").event
local popup = Popup({
enter = true,
focusable = true,
border = {
style = "rounded",
},
position = "50%",
size = {
width = "80%",
height = "60%",
},
})
-- mount/open the component
popup:mount()
-- unmount component when cursor leaves buffer
popup:on(event.BufLeave, function()
popup:unmount()
end)
-- set content
vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, { "Hello World" })
```
**[Check Detailed Documentation for `nui.popup`](lua/nui/popup)**
**[Check Wiki Page for `nui.popup`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.popup)**
### [Input](lua/nui/input)
![Input GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/input.gif)
```lua
local Input = require("nui.input")
local event = require("nui.utils.autocmd").event
local input = Input({
position = "50%",
size = {
width = 20,
},
border = {
style = "single",
text = {
top = "[Howdy?]",
top_align = "center",
},
},
win_options = {
winhighlight = "Normal:Normal,FloatBorder:Normal",
},
}, {
prompt = "> ",
default_value = "Hello",
on_close = function()
print("Input Closed!")
end,
on_submit = function(value)
print("Input Submitted: " .. value)
end,
})
-- mount/open the component
input:mount()
-- unmount component when cursor leaves buffer
input:on(event.BufLeave, function()
input:unmount()
end)
```
**[Check Detailed Documentation for `nui.input`](lua/nui/input)**
**[Check Wiki Page for `nui.input`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.input)**
### [Menu](lua/nui/menu)
![Menu GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/menu.gif)
```lua
local Menu = require("nui.menu")
local event = require("nui.utils.autocmd").event
local menu = Menu({
position = "50%",
size = {
width = 25,
height = 5,
},
border = {
style = "single",
text = {
top = "[Choose-an-Element]",
top_align = "center",
},
},
win_options = {
winhighlight = "Normal:Normal,FloatBorder:Normal",
},
}, {
lines = {
Menu.item("Hydrogen (H)"),
Menu.item("Carbon (C)"),
Menu.item("Nitrogen (N)"),
Menu.separator("Noble-Gases", {
char = "-",
text_align = "right",
}),
Menu.item("Helium (He)"),
Menu.item("Neon (Ne)"),
Menu.item("Argon (Ar)"),
},
max_width = 20,
keymap = {
focus_next = { "j", "<Down>", "<Tab>" },
focus_prev = { "k", "<Up>", "<S-Tab>" },
close = { "<Esc>", "<C-c>" },
submit = { "<CR>", "<Space>" },
},
on_close = function()
print("Menu Closed!")
end,
on_submit = function(item)
print("Menu Submitted: ", item.text)
end,
})
-- mount the component
menu:mount()
```
**[Check Detailed Documentation for `nui.menu`](lua/nui/menu)**
**[Check Wiki Page for `nui.menu`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.menu)**
### [Split](lua/nui/split)
![Split GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/split.gif)
```lua
local Split = require("nui.split")
local event = require("nui.utils.autocmd").event
local split = Split({
relative = "editor",
position = "bottom",
size = "20%",
})
-- mount/open the component
split:mount()
-- unmount component when cursor leaves buffer
split:on(event.BufLeave, function()
split:unmount()
end)
```
**[Check Detailed Documentation for `nui.split`](lua/nui/split)**
**[Check Wiki Page for `nui.split`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.split)**
## Extendibility
Each of the [blocks](#blocks) and [components](#components) can be extended to add new
methods or change their behaviors.
```lua
local Timer = Popup:extend("Timer")
function Timer:init(popup_options)
local options = vim.tbl_deep_extend("force", popup_options or {}, {
border = "double",
focusable = false,
position = { row = 0, col = "100%" },
size = { width = 10, height = 1 },
win_options = {
winhighlight = "Normal:Normal,FloatBorder:SpecialChar",
},
})
Timer.super.init(self, options)
end
function Timer:countdown(time, step, format)
local function draw_content(text)
local gap_width = 10 - vim.api.nvim_strwidth(text)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {
string.format(
"%s%s%s",
string.rep(" ", math.floor(gap_width / 2)),
text,
string.rep(" ", math.ceil(gap_width / 2))
),
})
end
self:mount()
local remaining_time = time
draw_content(format(remaining_time))
vim.fn.timer_start(step, function()
remaining_time = remaining_time - step
draw_content(format(remaining_time))
if remaining_time <= 0 then
self:unmount()
end
end, { ["repeat"] = math.ceil(remaining_time / step) })
end
local timer = Timer()
timer:countdown(10000, 1000, function(time)
return tostring(time / 1000) .. "s"
end)
```
#### `nui.object`
A small object library is bundled with `nui.nvim`. It is, more or less, a clone of the
[`kikito/middleclass`](https://github.com/kikito/middleclass) library.
[Check Wiki Page for `nui.object`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.object)
## License
Licensed under the MIT License. Check the [LICENSE](./LICENSE) file for details.

102
bundle/nui.nvim/lua/nui/input/README.md vendored Normal file
View File

@ -0,0 +1,102 @@
# Input
Input is an abstraction layer on top of Popup.
It uses prompt buffer (check `:h prompt-buffer`) for its popup window.
```lua
local Input = require("nui.input")
local event = require("nui.utils.autocmd").event
local popup_options = {
relative = "cursor",
position = {
row = 1,
col = 0,
},
size = 20,
border = {
style = "rounded",
text = {
top = "[Input]",
top_align = "left",
},
},
win_options = {
winhighlight = "Normal:Normal",
},
}
local input = Input(popup_options, {
prompt = "> ",
default_value = "42",
on_close = function()
print("Input closed!")
end,
on_submit = function(value)
print("Value submitted: ", value)
end,
on_change = function(value)
print("Value changed: ", value)
end,
})
```
If you provide the `on_change` function, it'll be run everytime value changes.
Pressing `<CR>` runs the `on_submit` callback function and closes the window.
Pressing `<C-c>` runs the `on_close` callback function and closes the window.
Of course, you can override the default keymaps and add more. For example:
```lua
-- unmount input by pressing `<Esc>` in normal mode
input:map("n", "<Esc>", function()
input:unmount()
end, { noremap = true })
```
You can manipulate the assocciated buffer and window using the
`split.bufnr` and `split.winid` properties.
**NOTE**: the first argument accepts options for `nui.popup` component.
## Options
### `prompt`
**Type:** `string` or `NuiText`
Prefix in the input.
### `default_value`
**Type:** `string`
Default value placed in the input on mount
### `on_close`
Callback function, called when input is closed.
### `on_submit`
Callback function, called when input value is submitted.
### `on_change`
Callback function, called when input value is changed.
### `disable_cursor_position_patch`
By default, `nui.input` will try to make sure the cursor on parent window is not
moved after input is submitted/closed. If you want to disable this behavior
for some reason, you can set `disable_cursor_position_patch` to `true`.
## Methods
Methods from `nui.popup` are also available for `nui.input`.
## Wiki Page
You can find additional documentation/examples/guides/tips-n-tricks in [nui.input wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.input).

143
bundle/nui.nvim/lua/nui/input/init.lua vendored Normal file
View File

@ -0,0 +1,143 @@
local Popup = require("nui.popup")
local Text = require("nui.text")
local defaults = require("nui.utils").defaults
local is_type = require("nui.utils").is_type
local event = require("nui.utils.autocmd").event
-- exiting insert mode places cursor one character backward,
-- so patch the cursor position to one character forward
-- when unmounting input.
---@param target_cursor number[]
---@param force? boolean
local function patch_cursor_position(target_cursor, force)
local cursor = vim.api.nvim_win_get_cursor(0)
if target_cursor[2] == cursor[2] and force then
-- didn't exit insert mode yet, but it's gonna
vim.api.nvim_win_set_cursor(0, { cursor[1], cursor[2] + 1 })
elseif target_cursor[2] - 1 == cursor[2] then
-- already exited insert mode
vim.api.nvim_win_set_cursor(0, { cursor[1], cursor[2] + 1 })
end
end
---@alias nui_input_internal nui_popup_internal|{ default_value: string, prompt: NuiText }
---@class NuiInput: NuiPopup
---@field private _ nui_input_internal
local Input = Popup:extend("NuiInput")
---@param popup_options table
---@param options table
function Input:init(popup_options, options)
popup_options.enter = true
popup_options.buf_options = defaults(popup_options.buf_options, {})
popup_options.buf_options.buftype = "prompt"
if not is_type("table", popup_options.size) then
popup_options.size = {
width = popup_options.size,
}
end
popup_options.size.height = 1
Input.super.init(self, popup_options)
self._.default_value = defaults(options.default_value, "")
self._.prompt = Text(defaults(options.prompt, ""))
self._.disable_cursor_position_patch = defaults(options.disable_cursor_position_patch, false)
local props = {}
self.input_props = props
props.on_submit = function(value)
local target_cursor = vim.api.nvim_win_get_cursor(self._.position.win)
local prompt_normal_mode = vim.fn.mode() == "n"
self:unmount()
vim.schedule(function()
if prompt_normal_mode then
-- NOTE: on prompt-buffer normal mode <CR> causes neovim to enter insert mode.
-- ref: https://github.com/neovim/neovim/blob/d8f5f4d09078/src/nvim/normal.c#L5327-L5333
vim.api.nvim_command("stopinsert")
end
if not self._.disable_cursor_position_patch then
patch_cursor_position(target_cursor, prompt_normal_mode)
end
if options.on_submit then
options.on_submit(value)
end
end)
end
props.on_close = function()
local target_cursor = vim.api.nvim_win_get_cursor(self._.position.win)
self:unmount()
vim.schedule(function()
if vim.fn.mode() == "i" then
vim.api.nvim_command("stopinsert")
end
if not self._.disable_cursor_position_patch then
patch_cursor_position(target_cursor)
end
if options.on_close then
options.on_close()
end
end)
end
if options.on_change then
props.on_change = function()
local value_with_prompt = vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, false)[1]
local value = string.sub(value_with_prompt, self._.prompt:length() + 1)
options.on_change(value)
end
end
end
function Input:mount()
local props = self.input_props
Input.super.mount(self)
if props.on_change then
vim.api.nvim_buf_attach(self.bufnr, false, {
on_lines = props.on_change,
})
end
if #self._.default_value then
self:on(event.InsertEnter, function()
vim.api.nvim_feedkeys(self._.default_value, "t", true)
end, { once = true })
end
vim.fn.prompt_setprompt(self.bufnr, self._.prompt:content())
if self._.prompt:length() > 0 then
vim.schedule(function()
self._.prompt:highlight(self.bufnr, self.ns_id, 1, 0)
end)
end
vim.fn.prompt_setcallback(self.bufnr, props.on_submit)
vim.fn.prompt_setinterrupt(self.bufnr, props.on_close)
vim.api.nvim_command("startinsert!")
end
---@alias NuiInput.constructor fun(popup_options: table, options: table): NuiInput
---@type NuiInput|NuiInput.constructor
local NuiInput = Input
return NuiInput

240
bundle/nui.nvim/lua/nui/layout/README.md vendored Normal file
View File

@ -0,0 +1,240 @@
# Layout
Layout is a helper component for creating complex layout by automatically
handling the calculation for position and size of other components.
**Example**
```lua
local Layout = require("nui.layout")
local Popup = require("nui.popup")
local top_popup = Popup({ border = "single" })
local bottom_popup = Popup({ border = "double" })
local layout = Layout(
{
position = "50%",
size = {
width = 80,
height = 40,
},
},
Layout.Box({
Layout.Box(top_popup, { size = "40%" }),
Layout.Box(bottom_popup, { size = "60%" }),
}, { dir = "col" })
)
layout:mount()
```
_Signature:_ `Layout(options, box)` or `Layout(component, box)`
`component` can be `Popup` or `Split`.
## Options
### `relative`
**Type:** `string` or `table`
This option affects how `position` and `size` are calculated.
**Examples**
Relative to cursor on current window:
```lua
relative = "cursor",
```
Relative to the current editor screen:
```lua
relative = "editor",
```
Relative to the current window (_default_):
```lua
relative = "win",
```
Relative to the window with specific id:
```lua
relative = {
type = "win",
winid = 5,
},
```
Relative to the buffer position:
```lua
relative = {
type = "buf",
-- zero-indexed
position = {
row = 5,
col = 5,
},
},
```
---
### `position`
**Type:** `number` or `percentage string` or `table`
Position is calculated from the top-left corner.
If `position` is `number` or `percentage string`, it applies to both `row` and `col`.
Or you can pass a table to set them separately.
For `percentage string`, position is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, `percentage string` is not allowed.
**Examples**
```lua
position = 50,
```
```lua
position = "50%",
```
```lua
position = {
row = 30,
col = 20,
},
```
```lua
position = {
row = "20%",
col = "50%",
},
```
---
### `size`
**Type:** `number` or `percentage string` or `table`
Determines the size of the layout.
If `size` is `number` or `percentage string`, it applies to both `width` and `height`.
You can also pass a table to set them separately.
For `percentage string`, `size` is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, window size is considered.
**Examples**
```lua
size = 50,
```
```lua
size = "50%",
```
```lua
size = {
width = 80,
height = 40,
},
```
```lua
size = {
width = "80%",
height = "60%",
},
```
## Layout.Box
_Signature:_ `Layout.Box(box, options)`
**Parameters**
| Name | Type | Description |
| --------- | ------------------------------ | ----------------------------------------- |
| `box` | `Layout.Box[]` / nui component | list of `Layout.Box` or any nui component |
| `options` | `table` | box options |
`options` is a `table` having the following keys:
| Key | Type | Description |
| ------ | ----------------------------- | ------------------------------------------------------ |
| `dir` | `"col"` / `"row"` (_default_) | arrangement direction, only if `box` is `Layout.Box[]` |
| `grow` | `number` | growth factor to fill up the box free space |
| `size` | `number` / `string` / `table` | optional if `grow` is present |
## Methods
### `layout:mount`
_Signature:_ `layout:mount()`
Mounts the layout with all the components.
**Examples**
```lua
layout:mount()
```
### `layout:unmount`
_Signature:_ `layout:unmount()`
Unmounts the layout with all the components.
**Examples**
```lua
layout:unmount()
```
### `layout:hide`
_Signature:_ `layout:hide()`
Hides the layout with all the components. Preserves the buffer (related content, autocmds and keymaps).
### `layout:show`
_Signature:_ `layout:show()`
Shows the hidden layout with all the components.
### `layout:update`
_Signature:_ `layout:update(config, box?)` or `layout:update(box?)`
**Parameters**
`config` is a `table` having the following keys:
| Key | Type |
| ---------- | ------------------ |
| `relative` | `string` / `table` |
| `position` | `string` / `table` |
| `size` | `string` / `table` |
`box` is a `table` returned by `Layout.Box`.
They are the same options used for layout initialization.
## Wiki Page
You can find additional documentation/examples/guides/tips-n-tricks in
[nui.layout wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.layout).

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