Refacto: rewrite everything

- The tree is created with libuv functions, which makes it blazingly fast.
- The tree may now be faster than any other vim trees, it can handle directories with thousands of files without any latency at all (tested on 40K files, works flawlessly).
- More solid logic for opening and closing the tree.
- tree state is remembered (closing / opening a folder keeps opened subdirectories open)
- detection of multiple git projects in the tree
- more icon support
- smart rendering
- smart updates
- ms windows support
- gx replacement function running xdg-open on linux, open on macos
This commit is contained in:
kiyan42 2020-05-19 19:46:45 +02:00
parent afc86a9623
commit e0bfcb4a6f
18 changed files with 1169 additions and 1029 deletions

View File

@ -1,5 +1,5 @@
Tree.lua is a simple tree for neovim
Copyright © 2012 Yazdani Kiyan
Copyright © 2019 Yazdani Kiyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -2,13 +2,23 @@
## Notice
- This plugin does not work on windows.
This plugin doesn't support windows. \
This plugin requires [neovim nightly](https://github.com/neovim/neovim/wiki/Installing-Neovim). \
You can switch to commit `afc86a9` if you use neovim 0.4.x. \
Note that the old version has less features and is much slower than the new one.
## Install
Install with [vim-plug](https://github.com/junegunn/vim-plug):
```vim
" master (neovim git)
Plug 'kyazdani42/nvim-web-devicons' " for file icons
Plug 'kyazdani42/nvim-tree.lua'
" old version that runs on neovim 0.4.x
Plug 'kyazdani42/nvim-tree.lua' { 'commit': 'afc86a9' }
" for icons in old version
Plug 'ryanoasis/vim-devicons'
```
## Setup
@ -16,11 +26,10 @@ Plug 'kyazdani42/nvim-tree.lua'
```vim
let g:lua_tree_side = 'right' | 'left' "left by default
let g:lua_tree_size = 40 "30 by default
let g:lua_tree_ignore = [ '.git', 'node_modules', '.cache' ] "empty by default, not working on mac atm
let g:lua_tree_ignore = [ '.git', 'node_modules', '.cache' ] "empty by default
let g:lua_tree_auto_open = 1 "0 by default, opens the tree when typing `vim $DIR` or `vim`
let g:lua_tree_auto_close = 1 "0 by default, closes the tree when it's the last window
let g:lua_tree_follow = 1 "0 by default, this option will bind BufEnter to the LuaTreeFindFile command
" :help LuaTreeFindFile for more info
let g:lua_tree_follow = 1 "0 by default, this option allows the cursor to be updated when entering a buffer
let g:lua_tree_show_icons = {
\ 'git': 1,
\ 'folders': 0,
@ -28,7 +37,7 @@ let g:lua_tree_show_icons = {
\}
"If 0, do not show the icons for one of 'git' 'folder' and 'files'
"1 by default, notice that if 'files' is 1, it will only display
"if web-devicons is installed and on your runtimepath
"if nvim-web-devicons is installed and on your runtimepath
" You can edit keybindings be defining this variable
" You don't have to define all keys.
@ -44,14 +53,27 @@ let g:lua_tree_bindings = {
\ 'rename': 'r'
\ }
" default will show icon by default if no icon is provided
" default shows no icon by default
let g:lua_tree_icons = {
\ 'default': '',
\ 'git': {
\ 'unstaged': "✗",
\ 'staged': "✓",
\ 'unmerged': "═",
\ 'renamed': "➜",
\ 'untracked': "★"
\ }
\ }
nnoremap <C-n> :LuaTreeToggle<CR>
nnoremap <leader>r :LuaTreeRefresh<CR>
nnoremap <leader>n :LuaTreeFindFile<CR>
" LuaTreeOpen and LuaTreeClose are also available if you need them
set termguicolors " this variable must be enabled for colors to be applied properly
" a list of groups can be found at `:help lua_tree_highlight`
highlight LuaTreeFolderName guibg=cyan gui=bold,underline
highlight LuaTreeFolderIcon guibg=blue
```
@ -60,33 +82,33 @@ highlight LuaTreeFolderIcon guibg=blue
- move around like in any vim buffer
- `<CR>` on `..` will cd in the above directory
- `.` will cd in the directory under the cursor
- type `a` to add a file
- type `a` to add a file. Adding a directory requires leaving a leading `/` at the end of the path.
> you can add multiple directories by doing foo/bar/baz/f and it will add foo bar and baz directories and f as a file
- type `r` to rename a file
- type `d` to delete a file (will prompt for confirmation)
- if the file is a directory, `<CR>` will open the directory
- otherwise it will open the file in the buffer near the tree
- if the file is a symlink, `<CR>` will follow the symlink
- if the file is a directory, `<CR>` will open the directory otherwise it will open the file in the buffer near the tree
- if the file is a symlink, `<CR>` will follow the symlink (if the target is a file)
- type `<C-v>` will open the file in a vertical split
- type `<C-x>` will open the file in a horizontal split
- type `<C-t>` will open the file in a new tab
- type `gx` to open the file with the `open` command on MACOS and `xdg-open` in linux
- Double left click acts like `<CR>`
- Double right click acts like `.`
## Note
This plugin is very fast because it uses the `libuv` `scandir` and `scandir_next` functions instead of spawning an `ls` process which can get slow on large files when combining with `stat` to get file informations.
## Features
- [x] Open file in current buffer or in split with FzF like bindings (`<CR>`, `<C-v>`, `<C-x>`, `<C-t>`)
- [x] File icons with vim-devicons
- [x] Syntax highlighting ([exa](https://github.com/ogham/exa) like)
- [x] Change directory with `.`
- [x] Add / Rename / delete files
- [x] Git integration
- [x] Mouse support
- Open file in current buffer or in split with FzF like bindings (`<CR>`, `<C-v>`, `<C-x>`, `<C-t>`)
- File icons with nvim-web-devicons
- Syntax highlighting ([exa](https://github.com/ogham/exa) like)
- Change directory with `.`
- Add / Rename / delete files
- Git integration
- Mouse support
- It's fast
## Screenshot
![alt text](.github/screenshot.png?raw=true "file explorer")
## TODO
- Tree creation could be async
- bufferize tree
- better default colors (use vim highlight groups)

View File

@ -7,6 +7,8 @@ Author: Yazdani Kiyan <yazdani.kiyan@protonmail.com>
==============================================================================
INTRODUCTION *nvim-tree-introduction*
This file explorer doesn't work on windows and requires neovim `nightly`
==============================================================================
QUICK START *nvim-tree-quickstart*
@ -19,6 +21,14 @@ open the tree with :LuaTreeToggle
==============================================================================
COMMANDS *nvim-tree-commands*
|:LuaTreeOpen| *:LuaTreeOpen*
opens the tree
|:LuaTreeClose| *:LuaTreeClose*
closes the tree
|:LuaTreeToggle| *:LuaTreeToggle*
open or close the tree
@ -32,7 +42,8 @@ refresh the tree
The command will change the cursor in the tree for the current bufname.
It will also open the leafs of the tree leading to the file in the buffer
(if you opened a file with something else than the LuaTree, like `fzf`)
(if you opened a file with something else than the LuaTree, like `fzf` or
`:split`)
==============================================================================
OPTIONS *nvim-tree-options*
@ -48,8 +59,8 @@ where the window will open (default to 'left')
|g:lua_tree_ignore| *g:lua_tree_ignore*
An array of strings that the tree won't display.
Each pattern is passed into the 'ls' function as `--ignore=PATTERN`
An array of strings that the tree won't load and display.
useful to hide large data/cache folders.
>
example: let g:lua_tree_ignore = [ '.git', 'node_modules' ]
@ -66,12 +77,30 @@ can disable icons per type:
\}
Can be one of `1` and `0` for each key. By default the tree will try
to render the icons. The `icons` key can only work if `vim-devicons`
to render the icons. The `icons` key can only work if `nvim-web-devicons`
is installed and in your |runtimepath|
(https://github.com/kyazdani42/nvim-web-devicons)
|g:lua_tree_icons| *g:lua_tree_icons*
You can set some icons for the git status and the default icon that shows
when no icon is found for a file.
>
let g:lua_tree_icons = {
\ 'default': '',
\ 'git': {
\ 'unstaged': "✗",
\ 'staged': "✓",
\ 'unmerged': "═",
\ 'renamed': "➜",
\ 'untracked': "★"
\ }
\ }
<
|g:lua_tree_follow| *g:lua_tree_follow*
Can be `0` or `1`. When `1`, will bind |:LuaTreeFindFile| to |BufEnter|
Can be `0` or `1`. When `1`, will update the cursor to update to the correct
location in the tree on |BufEnter|.
Default is 0
|g:lua_tree_auto_open| *g:lua_tree_auto_open*
@ -105,6 +134,8 @@ INFORMATIONS *nvim-tree-info*
- type '<C-v>' will open the file in a vertical split
- type '<C-x>' will open the file in a horizontal split
- type '<C-t>' will open the file in a new tab
- type 'gx' to open the file with the `open` command on macos and `xdg-open`
on linux.
- Double left click acts like '<CR>'
- Double right click acts like '.'
@ -130,8 +161,7 @@ default keybindings will be applied to undefined keys.
File icons with vim-devicons.
Uses other type of icons so a good font support is recommended.
If the tree renders weird glyphs, install correct fonts or try to change
your terminal.
If the tree renders weird glyphs, install the correct fonts.
Syntax highlighting uses g:terminal_color_ from colorschemes, fallbacks to
ugly colors otherwise.
@ -142,6 +172,7 @@ Git integration tells when a file is:
- ★ new file
- ✓ ✗ partially staged
- ✓ ★ new file staged
- ✓ ★ ✗ new file staged and has unstaged modifications
- ═ merging
- ➜ renamed

View File

@ -1,10 +1,13 @@
:LuaTreeClose nvim-tree-lua.txt /*:LuaTreeClose*
:LuaTreeFindFile nvim-tree-lua.txt /*:LuaTreeFindFile*
:LuaTreeOpen nvim-tree-lua.txt /*:LuaTreeOpen*
:LuaTreeRefresh nvim-tree-lua.txt /*:LuaTreeRefresh*
:LuaTreeToggle nvim-tree-lua.txt /*:LuaTreeToggle*
g:lua_tree_auto_close nvim-tree-lua.txt /*g:lua_tree_auto_close*
g:lua_tree_auto_open nvim-tree-lua.txt /*g:lua_tree_auto_open*
g:lua_tree_bindings nvim-tree-lua.txt /*g:lua_tree_bindings*
g:lua_tree_follow nvim-tree-lua.txt /*g:lua_tree_follow*
g:lua_tree_icons nvim-tree-lua.txt /*g:lua_tree_icons*
g:lua_tree_ignore nvim-tree-lua.txt /*g:lua_tree_ignore*
g:lua_tree_show_icons nvim-tree-lua.txt /*g:lua_tree_show_icons*
g:lua_tree_side nvim-tree-lua.txt /*g:lua_tree_side*

View File

@ -1,11 +1,33 @@
local api = vim.api
local get_colors = require 'lib/config'.get_colors
local colors = get_colors()
local M = {}
local function create_hl()
local function get_color_from_hl(hl_name, fallback)
local id = vim.api.nvim_get_hl_id_by_name(hl_name)
if not id then return fallback end
local hl = vim.api.nvim_get_hl_by_id(id, true)
if not hl or not hl.foreground then return fallback end
return hl.foreground
end
local function get_colors()
return {
red = vim.g.terminal_color_1 or get_color_from_hl('Keyword', 'Red'),
green = vim.g.terminal_color_2 or get_color_from_hl('Character', 'Green'),
yellow = vim.g.terminal_color_3 or get_color_from_hl('PreProc', 'Yellow'),
blue = vim.g.terminal_color_4 or get_color_from_hl('Include', 'Blue'),
purple = vim.g.terminal_color_5 or get_color_from_hl('Define', 'Purple'),
cyan = vim.g.terminal_color_6 or get_color_from_hl('Conditional', 'Cyan'),
dark_red = vim.g.terminal_color_9 or get_color_from_hl('Keyword', 'DarkRed'),
orange = vim.g.terminal_color_11 or get_color_from_hl('Number', 'Orange'),
}
end
local function get_hl_groups()
local colors = get_colors()
return {
Symlink = { gui = 'bold', fg = colors.cyan },
FolderIcon = { fg = '#90a4ae' },
@ -13,14 +35,22 @@ local function create_hl()
ExecFile = { gui = 'bold', fg = colors.green },
SpecialFile = { gui = 'bold,underline', fg = colors.yellow },
ImageFile = { gui = 'bold', fg = colors.purple },
MarkdownFile = { fg = colors.purple },
GitDirty = { fg = colors.dark_red },
GitStaged = { fg = colors.green },
GitMerge = { fg = colors.orange },
GitRenamed = { fg = colors.purple },
GitNew = { fg = colors.yellow },
-- TODO: remove those when we add this to nvim-web-devicons
MarkdownIcon = { fg = colors.purple },
LicenseIcon = { fg = colors.yellow },
YamlIcon = { fg = colors.yellow },
TomlIcon = { fg = colors.yellow },
GitignoreIcon = { fg = colors.yellow },
JsonIcon = { fg = colors.yellow },
LuaIcon = { fg = '#42a5f5' },
GoIcon = { fg = '#7Fd5EA' },
PythonIcon = { fg = colors.green },
ShellIcon = { fg = colors.green },
JavascriptIcon = { fg = colors.yellow },
@ -30,18 +60,44 @@ local function create_hl()
RustIcon = { fg = colors.orange },
VimIcon = { fg = colors.green },
TypescriptIcon = { fg = colors.blue },
GitDirty = { fg = colors.dark_red },
GitStaged = { fg = colors.green },
GitMerge = { fg = colors.orange },
GitRenamed = { fg = colors.purple },
GitNew = { fg = colors.yellow }
}
end
local HIGHLIGHTS = create_hl()
-- TODO: remove those when we add this to nvim-web-devicons
M.hl_groups = {
['LICENSE'] = 'LicenseIcon';
['license'] = 'LicenseIcon';
['vim'] = 'VimIcon';
['.vimrc'] = 'VimIcon';
['c'] = 'CIcon';
['cpp'] = 'CIcon';
['python'] = 'PythonIcon';
['lua'] = 'LuaIcon';
['rs'] = 'RustIcon';
['sh'] = 'ShellIcon';
['csh'] = 'ShellIcon';
['zsh'] = 'ShellIcon';
['bash'] = 'ShellIcon';
['md'] = 'MarkdownIcon';
['json'] = 'JsonIcon';
['toml'] = 'TomlIcon';
['go'] = 'GoIcon';
['yaml'] = 'YamlIcon';
['yml'] = 'YamlIcon';
['conf'] = 'GitignoreIcon';
['javascript'] = 'JavascriptIcon';
['typescript'] = 'TypescriptIcon';
['jsx'] = 'ReactIcon';
['tsx'] = 'ReactIcon';
['htm'] = 'HtmlIcon';
['html'] = 'HtmlIcon';
['slim'] = 'HtmlIcon';
['haml'] = 'HtmlIcon';
['ejs'] = 'HtmlIcon';
}
local LINKS = {
local function get_links()
return {
FolderName = 'Directory',
Normal = 'Normal',
EndOfBuffer = 'EndOfBuffer',
@ -49,16 +105,17 @@ local LINKS = {
VertSplit = 'VertSplit',
CursorColumn = 'CursorColumn'
}
end
function M.init_colors()
colors = get_colors()
HIGHLIGHTS = create_hl()
for k, d in pairs(HIGHLIGHTS) do
function M.setup()
local higlight_groups = get_hl_groups()
for k, d in pairs(higlight_groups) do
local gui = d.gui or 'NONE'
api.nvim_command('hi def LuaTree'..k..' gui='..gui..' guifg='..d.fg)
end
for k, d in pairs(LINKS) do
local links = get_links()
for k, d in pairs(links) do
api.nvim_command('hi def link LuaTree'..k..' '..d)
end
end

View File

@ -1,57 +1,50 @@
local api = vim.api
local M = {}
local function get(var, fallback)
if api.nvim_call_function('exists', { var }) == 1 then
return api.nvim_get_var(var)
else
return fallback
function M.get_icon_state()
local show_icons = vim.g.lua_tree_show_icons or { git = 1, folders = 1, files = 1 }
local icons = {
default = nil,
git_icons = {
unstaged = "",
staged = "",
unmerged = "",
renamed = "",
untracked = ""
}
}
local user_icons = vim.g.lua_tree_icons
if user_icons then
if user_icons.default then
icons.default = user_icons.default
end
for key, val in pairs(user_icons.git) do
if icons.git_icons[key] then
icons.git_icons[key] = val
end
end
end
local function get_color_from_hl(hl_name, fallback)
local id = api.nvim_get_hl_id_by_name(hl_name)
if not id then return fallback end
local hl = api.nvim_get_hl_by_id(id, true)
if not hl or not hl.foreground then return fallback end
return hl.foreground
end
local HAS_DEV_ICONS = api.nvim_call_function('exists', { "*WebDevIconsGetFileTypeSymbol" }) == 1
local show_icons = get('lua_tree_show_icons', { git = 1, folders = 1, files = 1 })
M.SHOW_FILE_ICON = HAS_DEV_ICONS and show_icons.files == 1
M.SHOW_FOLDER_ICON = show_icons.folders == 1
M.SHOW_GIT_ICON = show_icons.git == 1
function M.get_colors()
return {
red = get('terminal_color_1', get_color_from_hl('Keyword', 'Red')),
green = get('terminal_color_2', get_color_from_hl('Character', 'Green')),
yellow = get('terminal_color_3', get_color_from_hl('PreProc', 'Yellow')),
blue = get('terminal_color_4', get_color_from_hl('Include', 'Blue')),
purple = get('terminal_color_5', get_color_from_hl('Define', 'Purple')),
cyan = get('terminal_color_6', get_color_from_hl('Conditional', 'Cyan')),
orange = get('terminal_color_11', get_color_from_hl('Number', 'Orange')),
dark_red = get('terminal_color_9', get_color_from_hl('Keyword', 'DarkRed')),
show_file_icon = show_icons.files == 1 and vim.g.nvim_web_devicons == 1,
show_folder_icon = show_icons.folders == 1,
show_git_icon = show_icons.git == 1,
icons = icons
}
end
local keybindings = get('lua_tree_bindings', {});
M.bindings = {
function M.get_bindings()
local keybindings = vim.g.lua_tree_bindings or {}
return {
edit = keybindings.edit or '<CR>',
edit_vsplit = keybindings.edit_vsplit or '<C-v>',
edit_split = keybindings.edit_split or '<C-x>',
edit_tab = keybindings.edit_tab or '<C-t>',
cd = keybindings.cd or '.',
cd = keybindings.cd or '<C-]>',
create = keybindings.create or 'a',
remove = keybindings.remove or 'd',
rename = keybindings.rename or 'r',
}
end
return M

View File

@ -1,183 +0,0 @@
local api = vim.api
local config = require 'lib/config'
local M = {}
local function get_padding(depth)
local str = ""
while 0 < depth do
str = str .. " "
depth = depth - 1
end
return str
end
local function default_icons(_, isdir, open)
if isdir == true and config.SHOW_FOLDER_ICON then
if open == true then return "" end
return ""
end
return ""
end
local function create_matcher(arr)
return function(name)
for _, n in pairs(arr) do
if name:match(n) then return true end
end
return false
end
end
local is_special = create_matcher({
'README',
'readme',
'Makefile',
'Cargo%.toml',
})
local is_pic = create_matcher({
'%.jpe?g$',
'%.png',
'%.gif'
})
local function is_executable(name)
return api.nvim_call_function('executable', { name }) == 1
end
local function dev_icons(pathname, isdir, open)
if isdir == true or is_special(pathname) == true or is_executable(pathname) == true or is_pic(pathname) == true then
return default_icons(pathname, isdir, open)
end
local icon = api.nvim_call_function('WebDevIconsGetFileTypeSymbol', { pathname, isdir })
if icon == "" then return "" end
return icon .. " "
end
local function get_icon_func_gen()
if config.SHOW_FILE_ICON then
return dev_icons
else
return default_icons
end
end
local get_icon = get_icon_func_gen()
function M.format_tree(tree)
local dirs = {}
for i, node in pairs(tree) do
local padding = get_padding(node.depth)
local git = node.git
local icon = ""
local name = node.name
if node.link == true then
name = name .. '' .. node.linkto
elseif node.icon == true then
icon = get_icon(node.path .. node.name, node.dir, node.open)
end
dirs[i] = padding .. icon .. git .. name
end
return dirs
end
local HIGHLIGHT_ICON_GROUPS = {
['^LICENSE$'] = 'LicenseIcon';
['^%.?vimrc$'] = 'VimIcon';
['%.vim$'] = 'VimIcon';
['%.c$'] = 'CIcon';
['%.cpp$'] = 'CIcon';
['%.cxx$'] = 'CIcon';
['%.h$'] = 'CIcon';
['%.hpp$'] = 'CIcon';
['%.py$'] = 'PythonIcon';
['%.lua$'] = 'LuaIcon';
['%.rs$'] = 'RustIcon';
['%.[cz]?sh$'] = 'ShellIcon';
['%.md$'] = 'MarkdownIcon';
['%.json$'] = 'JsonIcon';
['%.toml$'] = 'TomlIcon';
['%.yml$'] = 'YamlIcon';
['%.gitignore$'] = 'GitignoreIcon';
['%.js$'] = 'JavascriptIcon';
['%.ts$'] = 'TypescriptIcon';
['%.[tj]sx$'] = 'ReactIcon';
['%.html?$'] = 'HtmlIcon';
}
local function highlight_line(buffer)
local function highlight(group, line, from, to)
api.nvim_buf_add_highlight(buffer, -1, group, line, from, to)
end
return function(line, node)
local text_start = node.depth * 2
local gitlen = string.len(node.git)
if node.name == '..' then
highlight('LuaTreeFolderName', line, 0, -1)
elseif node.dir == true then
if config.SHOW_FOLDER_ICON then
text_start = text_start + 4
highlight('LuaTreeFolderIcon', line, 0, text_start)
end
highlight('LuaTreeFolderName', line, text_start + gitlen, -1)
elseif node.link == true then
highlight('LuaTreeSymlink', line, 0, -1)
elseif is_special(node.name) == true then
highlight('LuaTreeSpecialFile', line, text_start + gitlen, -1)
elseif is_executable(node.path .. node.name) then
highlight('LuaTreeExecFile', line, text_start + gitlen, -1)
elseif is_pic(node.path .. node.name) then
highlight('LuaTreeImageFile', line, text_start + gitlen, -1)
elseif config.SHOW_FILE_ICON then
for k, v in pairs(HIGHLIGHT_ICON_GROUPS) do
if node.name:match(k) ~= nil then
text_start = text_start + 4
highlight('LuaTree' .. v, line, 0, text_start)
break
end
end
end
if node.git == '' then return end
if node.git == '' then
highlight('LuaTreeGitDirty', line, text_start, text_start + gitlen)
elseif node.git == '' then
highlight('LuaTreeGitStaged', line, text_start, text_start + gitlen)
elseif node.git == '✓★ ' then
highlight('LuaTreeGitStaged', line, text_start, text_start + 3)
highlight('LuaTreeGitNew', line, text_start + 3, text_start + gitlen)
elseif node.git == '✓✗ ' then
highlight('LuaTreeGitStaged', line, text_start, text_start + 3)
highlight('LuaTreeGitDirty', line, text_start + 3, text_start + gitlen)
elseif node.git == '' then
highlight('LuaTreeGitMerge', line, text_start, text_start + gitlen)
elseif node.git == '' then
highlight('LuaTreeGitRenamed', line, text_start, text_start + gitlen)
elseif node.git == '' then
highlight('LuaTreeGitNew', line, text_start, text_start + gitlen)
end
end
end
function M.highlight_buffer(buffer, tree)
local highlight = highlight_line(buffer)
for i, node in pairs(tree) do
highlight(i - 1, node)
end
end
return M

View File

@ -1,77 +1,169 @@
local api = vim.api
local luv = vim.loop
local open_mode = luv.constants.O_CREAT + luv.constants.O_WRONLY + luv.constants.O_TRUNC
local M = {}
function M.get_cwd() return luv.cwd() end
function M.is_dir(path)
local stat = luv.fs_lstat(path)
return stat and stat.type == 'directory' or false
local function clear_prompt()
vim.api.nvim_command('normal :esc<CR>')
end
function M.is_symlink(path)
local stat = luv.fs_lstat(path)
return stat and stat.type == 'link' or false
local function refresh_tree()
vim.api.nvim_command(":LuaTreeRefresh")
end
function M.link_to(path)
return luv.fs_readlink(path) or ''
end
function M.check_dir_access(path)
if luv.fs_access(path, 'R') == true then
return true
local function create_file(file)
luv.fs_open(file, "w", open_mode, vim.schedule_wrap(function(err, fd)
if err then
api.nvim_err_writeln('Could not create file '..file)
else
api.nvim_err_writeln('Permission denied: ' .. path)
return false
-- FIXME: i don't know why but libuv keeps creating file with executable permissions
-- this is why we need to chmod to default file permissions
luv.fs_chmod(file, 0644)
luv.fs_close(fd)
api.nvim_out_write('File '..file..' was properly created\n')
refresh_tree()
end
end
local handle = nil
local function run_process(opt, err, cb)
handle = luv.spawn(opt.cmd, { args = opt.args }, vim.schedule_wrap(function(code)
handle:close()
if code ~= 0 then
return api.nvim_err_writeln(err)
end
cb()
end))
end
function M.rm(path, cb)
local opt = { cmd='rm', args = {'-rf', path } };
run_process(opt, 'Error removing '..path, cb)
local function get_num_entries(iter)
i = 0
for _ in iter do
i = i + 1
end
return i
end
function M.create(node)
if node.name == '..' then return end
function M.rename(file, new_path, cb)
local opt = { cmd='mv', args = {file, new_path } };
run_process(opt, 'Error renaming '..file..' to '..new_path, cb)
end
function M.create(path, file, folders, cb)
local opt_file = nil
local file_path = nil
if file ~= nil then
file_path = path..folders..file
opt_file = { cmd='touch', args = {file_path} }
end
if folders ~= "" then
local folder_path = path..folders
local opt = {cmd = 'mkdir', args = {'-p', folder_path }}
run_process(opt, 'Error creating folder '..folder_path, function()
if opt_file then
run_process(opt, 'Error creating file '..file_path, cb)
local add_into
if node.entries ~= nil then
add_into = node.absolute_path..'/'
else
cb()
add_into = node.absolute_path:sub(0, -(#node.name + 1))
end
end)
elseif opt_file then
run_process(opt_file, 'Error creating file '..file_path, cb)
local ans = vim.fn.input('Create file '..add_into)
clear_prompt()
if not ans or #ans == 0 then return end
if not ans:match('/') then
return create_file(add_into..ans)
end
-- create a foler for each element until / and create a file when element is not ending with /
-- if element is ending with / and it's the last element, we need to manually refresh
local relpath = ''
local idx = 0
local num_entries = get_num_entries(ans:gmatch('[^/]+/?'))
for path in ans:gmatch('[^/]+/?') do
idx = idx + 1
relpath = relpath..path
if relpath:match('.*/$') then
local success = luv.fs_mkdir(add_into..relpath, 493)
if not success then
api.nvim_err_writeln('Could not create folder '..add_into..relpath)
return
end
if idx == num_entries then
api.nvim_out_write('Folder '..add_into..relpath..' was properly created\n')
refresh_tree()
end
else
create_file(add_into..relpath)
end
end
end
local remove_ok = true
local function remove_callback(name, absolute_path)
return function(err, success)
if err ~= nil then
api.nvim_err_writeln(err)
remove_ok = false
elseif not success then
remove_ok = false
api.nvim_err_writeln('Could not remove '..name)
else
api.nvim_out_write(name..' has been removed\n')
for _, buf in pairs(api.nvim_list_bufs()) do
if api.nvim_buf_get_name(buf) == absolute_path then
api.nvim_command(':bd! '..buf)
end
end
end
end
end
local function remove_dir(cwd)
local handle = luv.fs_scandir(cwd)
if type(handle) == 'string' then
return api.nvim_err_writeln(handle)
end
while true do
local name, t = luv.fs_scandir_next(handle)
if not name then break end
local new_cwd = cwd..'/'..name
if t == 'directory' then
remove_dir(new_cwd)
else
luv.fs_unlink(new_cwd, vim.schedule_wrap(remove_callback(new_cwd, new_cwd)))
end
if not remove_ok then return end
end
luv.fs_rmdir(cwd, vim.schedule_wrap(remove_callback(cwd, cwd)))
end
function M.remove(node)
if node.name == '..' then return end
local ans = vim.fn.input("Remove " ..node.name.. " ? y/n: ")
clear_prompt()
if ans:match('^y') then
remove_ok = true
if node.entries ~= nil then
remove_dir(node.absolute_path)
else
luv.fs_unlink(node.absolute_path, vim.schedule_wrap(
remove_callback(node.name, node.absolute_path)
))
end
refresh_tree()
end
end
local function rename_callback(node, new_name)
return function(err, success)
if err ~= nil then
api.nvim_err_writeln(err)
elseif not success then
api.nvim_err_writeln('Could not rename '..node.absolute_path..' to '..new_name)
else
api.nvim_out_write(node.absolute_path..''..new_name..'\n')
for _, buf in pairs(api.nvim_list_bufs()) do
if api.nvim_buf_get_name(buf) == node.absolute_path then
api.nvim_buf_set_name(buf, new_name)
end
end
refresh_tree()
end
end
end
function M.rename(node)
if node.name == '..' then return end
local ans = vim.fn.input("Rename " ..node.name.. " to ", node.absolute_path)
clear_prompt()
if not ans or #ans == 0 then return end
luv.fs_rename(node.absolute_path, ans, vim.schedule_wrap(rename_callback(node, ans)))
end
return M

View File

@ -1,68 +0,0 @@
local api = vim.api
local fs = require 'lib/fs'
local update_view = require 'lib/winutils'.update_view
local refresh_tree = require 'lib/state'.refresh_tree
local refresh_git = require 'lib/git'.refresh_git
local M = {}
local function input(v)
local param
if type(v) == 'string' then param = { v } else param = v end
return api.nvim_call_function('input', param)
end
local function clear_prompt()
api.nvim_command('normal :<esc>')
end
function M.create_file(path)
local new_file = input("Create file: " .. path)
local file = nil
if not new_file:match('.*/$') then
file = new_file:reverse():gsub('/.*$', ''):reverse()
new_file = new_file:gsub('[^/]*$', '')
end
local folders = ""
if #new_file ~= 0 then
for p in new_file:gmatch('[^/]*') do
if p and p ~= "" then
folders = folders .. p .. '/'
end
end
end
clear_prompt()
fs.create(path, file, folders, function()
refresh_git()
refresh_tree()
update_view()
end)
end
function M.remove_file(filename, path)
local ans = input("Remove " .. filename .. " ? y/n: ")
clear_prompt()
if ans == "y" then
fs.rm(path .. filename, function()
refresh_git()
refresh_tree()
update_view()
end)
end
end
function M.rename_file(filename, path)
local new_path = input({"Rename file " .. filename .. ": ", path .. filename})
clear_prompt()
fs.rename(path .. filename, new_path, function()
refresh_git()
refresh_tree()
update_view()
end)
end
return M

View File

@ -1,79 +0,0 @@
local api = vim.api
local config = require 'lib/config'
local utils = require'lib.utils'
local M = {}
local function system(v) return api.nvim_call_function('system', { v }) end
local function systemlist(v) return api.nvim_call_function('systemlist', { v }) end
local function is_git_repo()
local is_git = system('git rev-parse')
return is_git:match('fatal') == nil
end
local IS_GIT_REPO = is_git_repo()
local function set_git_status()
if IS_GIT_REPO == false then return '' end
return systemlist('git status --porcelain=v1')
end
local GIT_STATUS = set_git_status()
function M.refresh_git()
if IS_GIT_REPO == false then return false end
GIT_STATUS = set_git_status()
return true
end
function M.force_refresh_git()
IS_GIT_REPO = is_git_repo()
M.refresh_git()
end
local function is_folder_dirty(relpath)
for _, status in pairs(GIT_STATUS) do
if status:match(utils.path_to_matching_str(relpath)) ~= nil then
return true
end
end
end
local function create_git_checker(pattern)
return function(relpath)
for _, status in pairs(GIT_STATUS) do
local ret = status:match('^.. .*' .. utils.path_to_matching_str(relpath))
if ret ~= nil and ret:match(pattern) ~= nil then return true end
end
return false
end
end
local unstaged = create_git_checker('^ ')
local staged = create_git_checker('^M ')
local staged_new = create_git_checker('^A ')
local staged_mod = create_git_checker('^MM')
local unmerged = create_git_checker('^[U ][U ]')
local renamed = create_git_checker('^R')
local untracked = create_git_checker('^%?%?')
function M.get_git_attr(path, is_dir)
if IS_GIT_REPO == false or not config.SHOW_GIT_ICON then return '' end
if is_dir then
if is_folder_dirty(path) == true then return '' end
else
if unstaged(path) then return ''
elseif staged(path) then return ''
elseif staged_new(path) then return '✓★ '
elseif staged_mod(path) then return '✓✗ '
elseif unmerged(path) then return ''
elseif renamed(path) then return ''
elseif untracked(path) then return ''
end
end
return ''
end
return M

241
lua/lib/populate.lua Normal file
View File

@ -0,0 +1,241 @@
local config = require'lib.config'
local icon_config = config.get_icon_state()
local api = vim.api
local luv = vim.loop
local M = {}
local function path_to_matching_str(path)
return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)')
end
local function dir_new(cwd, name)
local absolute_path = cwd..'/'..name
local stat = luv.fs_stat(absolute_path)
return {
name = name,
absolute_path = absolute_path,
-- TODO: last modified could also involve atime and ctime
last_modified = stat.mtime.sec,
match_name = path_to_matching_str(name),
match_path = path_to_matching_str(absolute_path),
open = false,
entries = {}
}
end
local function file_new(cwd, name)
local absolute_path = cwd..'/'..name
local is_exec = luv.fs_access(absolute_path, 'X')
return {
name = name,
absolute_path = absolute_path,
executable = is_exec,
extension = vim.fn.fnamemodify(name, ':e') or "",
match_name = path_to_matching_str(name),
match_path = path_to_matching_str(absolute_path),
}
end
local function link_new(cwd, name)
local absolute_path = cwd..'/'..name
local link_to = luv.fs_realpath(absolute_path)
return {
name = name,
absolute_path = absolute_path,
link_to = link_to,
match_name = path_to_matching_str(name),
match_path = path_to_matching_str(absolute_path),
}
end
local function gen_ignore_check()
local ignore_list = {}
if vim.g.lua_tree_ignore and #vim.g.lua_tree_ignore > 0 then
for _, entry in pairs(vim.g.lua_tree_ignore) do
ignore_list[entry] = true
end
end
return function(path)
return ignore_list[path] == true
end
end
local should_ignore = gen_ignore_check()
function M.refresh_entries(entries, cwd)
local handle = luv.fs_scandir(cwd)
if type(handle) == 'string' then
api.nvim_err_writeln(handle)
return
end
local named_entries = {}
local cached_entries = {}
local entries_idx = {}
for i, node in ipairs(entries) do
cached_entries[i] = node.name
entries_idx[node.name] = i
named_entries[node.name] = node
end
local dirs = {}
local links = {}
local files = {}
local new_entries = {}
while true do
local name, t = luv.fs_scandir_next(handle)
if not name then break end
if should_ignore(name) then goto continue end
if t == 'directory' then
table.insert(dirs, name)
new_entries[name] = true
elseif t == 'file' then
table.insert(files, name)
new_entries[name] = true
elseif t == 'link' then
table.insert(links, name)
new_entries[name] = true
end
::continue::
end
local all = {
{ entries = dirs, fn = dir_new },
{ entries = links, fn = link_new },
{ entries = files, fn = file_new }
}
local prev = nil
for _, e in ipairs(all) do
for _, name in ipairs(e.entries) do
if not named_entries[name] then
local n = e.fn(cwd, name)
local idx = 1
if prev then
idx = entries_idx[prev] + 1
end
table.insert(entries, idx, n)
entries_idx[name] = idx
cached_entries[idx] = name
end
prev = name
end
end
local idx = 1
for _, name in ipairs(cached_entries) do
if not new_entries[name] then
table.remove(entries, idx, idx + 1)
else
idx = idx + 1
end
end
end
function M.populate(entries, cwd)
local handle = luv.fs_scandir(cwd)
if type(handle) == 'string' then
api.nvim_err_writeln(handle)
return
end
local dirs = {}
local links = {}
local files = {}
while true do
local name, t = luv.fs_scandir_next(handle)
if not name then break end
if t == 'directory' then
table.insert(dirs, name)
elseif t == 'file' then
table.insert(files, name)
elseif t == 'link' then
table.insert(links, name)
end
end
-- Create Nodes --
for _, dirname in ipairs(dirs) do
local dir = dir_new(cwd, dirname)
if not should_ignore(dir.name) and luv.fs_access(dir.absolute_path, 'R') then
table.insert(entries, dir)
end
end
for _, linkname in ipairs(links) do
local link = link_new(cwd, linkname)
if not should_ignore(link.name) then
table.insert(entries, link)
end
end
for _, filename in ipairs(files) do
local file = file_new(cwd, filename)
if not should_ignore(file.name) then
table.insert(entries, file)
end
end
if not icon_config.show_git_icon then
return
end
M.update_git_status(entries, cwd)
end
function M.update_git_status(entries, cwd)
local git_root = vim.fn.system('cd '..cwd..' && git rev-parse --show-toplevel')
if not git_root or #git_root == 0 or git_root:match('fatal: not a git repository') then
return
end
git_root = git_root:sub(0, -2)
local git_statuslist = vim.fn.systemlist('cd '..cwd..' && git status --porcelain=v1')
local git_status = {}
for _, v in pairs(git_statuslist) do
local head = v:sub(0, 2)
local body = v:sub(4, -1)
if body:match('%->') ~= nil then
body = body:gsub('^.* %-> ', '')
end
git_status[body] = head
end
local matching_cwd = path_to_matching_str(git_root..'/')
for _, node in pairs(entries) do
local relpath = node.absolute_path:gsub(matching_cwd, '')
if node.entries ~= nil then
relpath = relpath..'/'
node.git_status = nil
end
local status = git_status[relpath]
if status then
node.git_status = status
elseif node.entries ~= nil then
local matcher = '^'..path_to_matching_str(relpath)
for key, _ in pairs(git_status) do
if key:match(matcher) then
node.git_status = 'dirty'
break
end
end
else
node.git_status = nil
end
end
end
return M

183
lua/lib/renderer.lua Normal file
View File

@ -0,0 +1,183 @@
local colors = require'lib.colors'
local config = require'lib.config'
local api = vim.api
local lines = {}
local hl = {}
local index = 0
local namespace_id = api.nvim_create_namespace('LuaTreeHighlights')
local icon_state = config.get_icon_state()
local get_folder_icon = function() return "" end
local set_folder_hl = function(index, depth, git_icon_len)
table.insert(hl, {'LuaTreeFolderName', index, depth+git_icon_len, -1})
end
if icon_state.show_folder_icon then
get_folder_icon = function(open)
if open then
return ""
else
return ""
end
end
set_folder_hl = function(index, depth, icon_len, name_len)
table.insert(hl, {'LuaTreeFolderName', index, depth+icon_len, depth+icon_len+name_len})
table.insert(hl, {'LuaTreeFolderIcon', index, depth, depth+icon_len})
end
end
local get_file_icon = function() return "" end
if icon_state.show_file_icon then
local web_devicons = require'nvim-web-devicons'
get_file_icon = function(fname, extension, index, depth)
local icon, hl_group = web_devicons.get_icon(fname, extension)
-- TODO: remove this hl_group and make this in nvim-web-devicons
if #extension == 0 then
hl_group = colors.hl_groups[fname]
else
hl_group = colors.hl_groups[extension]
end
if hl_group and icon then
table.insert(hl, { 'LuaTree'..hl_group, index, depth, depth + #icon })
return icon.." "
else
return icon_state.icons.default and icon_state.icons.default.." " or ""
end
end
end
local get_git_icons = function() return "" end
local git_icon_state = {}
if icon_state.show_git_icon then
git_icon_state = {
["M "] = { { icon = icon_state.icons.git_icons.staged, hl = "LuaTreeGitStaged" } },
[" M"] = { { icon = icon_state.icons.git_icons.unstaged, hl = "LuaTreeGitDirty" } },
["MM"] = {
{ icon = icon_state.icons.git_icons.staged, hl = "LuaTreeGitStaged" },
{ icon = icon_state.icons.git_icons.unstaged, hl = "LuaTreeGitDirty" }
},
["A "] = {
{ icon = icon_state.icons.git_icons.staged, hl = "LuaTreeGitStaged" },
{ icon = icon_state.icons.git_icons.untracked, hl = "LuaTreeGitNew" }
},
["AM"] = {
{ icon = icon_state.icons.git_icons.staged, hl = "LuaTreeGitStaged" },
{ icon = icon_state.icons.git_icons.untracked, hl = "LuaTreeGitNew" },
{ icon = icon_state.icons.git_icons.unstaged, hl = "LuaTreeGitDirty" }
},
["??"] = { { icon = icon_state.icons.git_icons.untracked, hl = "LuaTreeGitNew" } },
["R "] = { { icon = icon_state.icons.git_icons.renamed, hl = "LuaTreeGitRenamed" } },
["UU"] = { { icon = icon_state.icons.git_icons.unmerged, hl = "LuaTreeGitMerge" } },
dirty = { { icon = icon_state.icons.git_icons.unstaged, hl = "LuaTreeGitDirty" } },
}
get_git_icons = function(node, index, depth, icon_len)
local git_status = node.git_status
if not git_status then return "" end
local icon = ""
local icons = git_icon_state[git_status]
for _, v in ipairs(icons) do
table.insert(hl, { v.hl, index, depth+icon_len+#icon, depth+icon_len+#icon+#v.icon })
icon = icon..v.icon.." "
end
return icon
end
end
local picture = {
jpg = true,
jpeg = true,
png = true,
gif = true,
}
local special = {
["Cargo.toml"] = true,
Makefile = true,
["README.md"] = true,
["readme.md"] = true,
}
local function update_draw_data(tree, depth)
if tree.cwd and tree.cwd ~= '/' then
table.insert(lines, "..")
table.insert(hl, {'LuaTreeFolderName', index, 0, 2})
index = 1
end
for _, node in ipairs(tree.entries) do
local padding = string.rep(" ", depth)
if node.entries then
local icon = get_folder_icon(node.open)
local git_icon = get_git_icons(node, index, depth+#node.name, #icon+1)
set_folder_hl(index, depth, #icon, #node.name)
index = index + 1
if node.open then
table.insert(lines, padding..icon..node.name.." "..git_icon)
update_draw_data(node, depth + 2)
else
table.insert(lines, padding..icon..node.name.." "..git_icon)
end
elseif node.link_to then
table.insert(hl, { 'LuaTreeSymlink', index, depth, -1 })
table.insert(lines, padding..node.name..""..node.link_to)
index = index + 1
else
local icon
local git_icons
if special[node.name] then
icon = ""
git_icons = get_git_icons(node, index, depth, 0)
table.insert(hl, {'LuaTreeSpecialFile', index, depth+#git_icons, -1})
else
icon = get_file_icon(node.name, node.extension, index, depth)
git_icons = get_git_icons(node, index, depth, #icon)
end
table.insert(lines, padding..icon..git_icons..node.name)
if node.executable then
table.insert(hl, {'LuaTreeExecFile', index, depth+#icon+#git_icons, -1 })
elseif picture[node.extension] then
table.insert(hl, {'LuaTreeImageFile', index, depth+#icon+#git_icons, -1 })
end
index = index + 1
end
end
end
local M = {}
function M.draw(tree, reload)
api.nvim_buf_set_option(tree.bufnr, 'modifiable', true)
local cursor = api.nvim_win_get_cursor(tree.winnr)
if reload then
index = 0
lines = {}
hl = {}
update_draw_data(tree, 0)
end
api.nvim_buf_set_lines(tree.bufnr, 0, -1, false, lines)
M.render_hl(tree.bufnr)
if #lines > cursor[1] then
api.nvim_win_set_cursor(tree.winnr, cursor)
end
api.nvim_buf_set_option(tree.bufnr, 'modifiable', false)
end
function M.render_hl(bufnr)
api.nvim_buf_clear_namespace(bufnr, namespace_id, 0, -1)
for _, data in ipairs(hl) do
api.nvim_buf_add_highlight(bufnr, namespace_id, data[1], data[2], data[3], data[4])
end
end
return M

View File

@ -1,179 +0,0 @@
local api = vim.api
local utils = require'lib.utils'
local gitutils = require 'lib.git'
local fs = require 'lib.fs'
local M = {}
local ROOT_PATH = fs.get_cwd() .. '/'
function M.set_root_path(path)
ROOT_PATH = path
end
function M.get_root_path()
return ROOT_PATH
end
local Tree = {}
local IGNORE_LIST = ""
local MACOS = api.nvim_call_function('has', { 'macunix' }) == 1
-- --ignore does not work with mac ls
if not MACOS and api.nvim_call_function('exists', { 'g:lua_tree_ignore' }) == 1 then
local ignore_patterns = api.nvim_get_var('lua_tree_ignore')
if type(ignore_patterns) == 'table' then
for _, pattern in pairs(ignore_patterns) do
IGNORE_LIST = IGNORE_LIST .. '--ignore='..pattern..' '
end
end
end
local function list_dirs(path)
return api.nvim_call_function('systemlist', { 'ls -A '..IGNORE_LIST..path })
end
local function sort_dirs(dirs)
local sorted_tree = {}
for _, node in pairs(dirs) do
if node.dir == true then
table.insert(sorted_tree, 1, node)
else
table.insert(sorted_tree, node)
end
end
return sorted_tree
end
local function create_nodes(path, relpath, depth, dirs)
local tree = {}
if not path:find('^.*/$') then path = path .. '/' end
if not relpath:find('^.*/$') and depth > 0 then relpath = relpath .. '/' end
for i, name in pairs(dirs) do
local full_path = path..name
local dir = fs.is_dir(full_path)
local link = fs.is_symlink(full_path)
local linkto = link == true and fs.link_to(full_path) or nil
local rel_path = relpath ..name
tree[i] = {
path = path,
relpath = rel_path,
link = link,
linkto = linkto,
name = name,
depth = depth,
dir = dir,
open = false,
icon = true,
git = gitutils.get_git_attr(rel_path, dir)
}
end
return sort_dirs(tree)
end
function M.init_tree()
Tree = create_nodes(ROOT_PATH, '', 0, list_dirs(ROOT_PATH))
if ROOT_PATH ~= '/' then
table.insert(Tree, 1, {
path = ROOT_PATH,
name = '..',
depth = 0,
dir = true,
open = false,
icon = false,
git = ''
})
end
end
function M.refresh_tree()
local cache = {}
for _, v in pairs(Tree) do
if v.dir == true and v.open == true then
table.insert(cache, v.path .. v.name)
end
end
M.init_tree()
for i, node in pairs(Tree) do
if node.dir == true then
for _, path in pairs(cache) do
if node.path .. node.name == path then
node.open = true
local dirs = list_dirs(path)
for j, n in pairs(create_nodes(path, node.relpath, node.depth + 1, dirs)) do
table.insert(Tree, i + j, n)
end
end
end
end
end
end
local function clone(obj)
if type(obj) ~= 'table' then return obj end
local res = {}
for k, v in pairs(obj) do res[clone(k)] = clone(v) end
return res
end
function M.find_file(path)
local relpath = string.sub(path, #ROOT_PATH + 1, -1)
local tree_copy = clone(Tree)
for i, node in pairs(tree_copy) do
if node.relpath and relpath:find(utils.path_to_matching_str(node.relpath)) then
if node.relpath == relpath then
Tree = clone(tree_copy)
return i
end
if node.dir and not node.open then
local dirpath = node.path .. node.name
node.open = true
local dirs = list_dirs(dirpath)
for j, n in pairs(create_nodes(dirpath, node.relpath, node.depth + 1, dirs)) do
table.insert(tree_copy, i + j, n)
end
end
end
end
return nil
end
function M.open_dir(tree_index)
local node = Tree[tree_index];
node.open = not node.open
if node.open == false then
local next_index = tree_index + 1;
local next_node = Tree[next_index]
while next_node ~= nil and next_node.depth > node.depth do
table.remove(Tree, next_index)
next_node = Tree[next_index]
end
else
local dirlist = list_dirs(tostring(node.path .. node.name))
local child_dirs = create_nodes(node.path .. node.name .. '/', node.relpath, node.depth + 1, dirlist)
for i, n in pairs(child_dirs) do
table.insert(Tree, tree_index + i, n)
end
end
end
function M.get_tree()
return Tree
end
return M

270
lua/lib/tree.lua Normal file
View File

@ -0,0 +1,270 @@
local api = vim.api
local luv = vim.loop
local renderer = require'lib.renderer'
local config = require'lib.config'
local pops = require'lib.populate'
local populate = pops.populate
local refresh_entries = pops.refresh_entries
local update_git = pops.update_git_status
local M = {}
M.Tree = {
entries = {},
buf_name = 'LuaTree',
cwd = nil,
win_width = vim.g.lua_tree_width or 30,
loaded = false,
side = 'H',
bufnr = nil,
winnr = nil,
buf_options = {
'nowrap', 'sidescroll=5', 'nospell', 'nolist', 'nofoldenable',
'foldmethod=manual', 'foldcolumn=0', 'nonumber',
'noswapfile', 'splitbelow', 'noruler', 'noshowmode', 'noshowcmd'
}
}
if vim.g.lua_tree_side == 'right' then
M.Tree.side = 'L'
end
function M.init(with_open, with_render)
M.Tree.cwd = luv.cwd()
populate(M.Tree.entries, M.Tree.cwd, M.Tree)
local stat = luv.fs_stat(M.Tree.cwd)
M.Tree.last_modified = stat.mtime.sec
if with_open then
M.open()
end
if with_render then
renderer.draw(M.Tree, true)
M.Tree.loaded = true
end
end
local function get_node_at_line(line)
local index = 2
local function iter(entries)
for _, node in ipairs(entries) do
if index == line then
return node
end
index = index + 1
if node.open == true then
local child = iter(node.entries)
if child ~= nil then return child end
end
end
end
return iter
end
function M.get_node_at_cursor()
local cursor = api.nvim_win_get_cursor(M.Tree.winnr)
local line = cursor[1]
if line == 1 and M.Tree.cwd ~= "/" then
return { name = ".." }
end
if M.Tree.cwd == "/" then
line = line + 1
end
return get_node_at_line(line)(M.Tree.entries)
end
function M.unroll_dir(node)
node.open = not node.open
if #node.entries > 0 then
renderer.draw(M.Tree, true)
else
populate(node.entries, node.absolute_path)
renderer.draw(M.Tree, true)
end
end
local function refresh_git(node)
update_git(node.entries, node.absolute_path or node.cwd)
for _, entry in pairs(node.entries) do
if entry.entries ~= nil then
refresh_git(entry)
end
end
end
-- TODO update only entries where directory has changed
local function refresh_nodes(node)
refresh_entries(node.entries, node.absolute_path or node.cwd)
for _, entry in ipairs(node.entries) do
if entry.entries and entry.open then
refresh_nodes(entry)
end
end
end
function M.refresh_tree()
local stat = luv.fs_stat(M.Tree.cwd)
-- if stat.mtime.sec ~= M.Tree.last_modified then
refresh_nodes(M.Tree)
-- end
if config.get_icon_state().show_git_icon then
refresh_git(M.Tree)
end
if M.Tree.winnr ~= nil then
renderer.draw(M.Tree, true)
end
end
function M.set_index_and_redraw(fname)
local i
if M.Tree.cwd == '/' then
i = 0
else
i = 1
end
local reload = false
local function iter(entries)
for _, entry in ipairs(entries) do
i = i + 1
if entry.absolute_path == fname then
return i
end
if fname:match(entry.match_path..'/') ~= nil then
if #entry.entries == 0 then
reload = true
populate(entry.entries, entry.absolute_path)
end
if entry.open == false then
reload = true
entry.open = true
end
if iter(entry.entries) ~= nil then
return i
end
elseif entry.open == true then
iter(entry.entries)
end
end
end
local index = iter(M.Tree.entries)
if index then
api.nvim_win_set_cursor(M.Tree.winnr, {index, 0})
end
renderer.draw(M.Tree, reload)
return index
end
function M.open_file(mode, filename)
if vim.g.lua_tree_side == 'right' then
api.nvim_command('noautocmd wincmd h')
else
api.nvim_command('noautocmd wincmd l')
end
api.nvim_command(string.format("%s %s", mode, filename))
end
function M.change_dir(foldername)
api.nvim_command('cd '..foldername)
M.Tree.entries = {}
M.init(false, M.Tree.bufnr ~= nil)
end
local function set_mappings()
local buf = M.Tree.bufnr
local bindings = config.get_bindings()
local mappings = {
['<2-LeftMouse>'] = 'on_keypress("edit")';
['<2-RightMouse>'] = 'on_keypress("cd")';
[bindings.cd] = 'on_keypress("cd")';
[bindings.edit] = 'on_keypress("edit")';
[bindings.edit_vsplit] = 'on_keypress("vsplit")';
[bindings.edit_split] = 'on_keypress("split")';
[bindings.edit_tab] = 'on_keypress("tabnew")';
[bindings.create] = 'on_keypress("create")';
[bindings.remove] = 'on_keypress("remove")';
[bindings.rename] = 'on_keypress("rename")';
gx = "xdg_open()";
}
for k,v in pairs(mappings) do
api.nvim_buf_set_keymap(buf, 'n', k, ':lua require"tree".'..v..'<cr>', {
nowait = true, noremap = true, silent = true
})
end
end
local function create_buf()
local options = {
bufhidden = 'delete';
buftype = 'nofile';
modifiable = false;
}
M.Tree.bufnr = api.nvim_create_buf(false, true)
api.nvim_buf_set_name(M.Tree.bufnr, M.Tree.buf_name)
api.nvim_buf_set_option(M.Tree.bufnr, 'filetype', M.Tree.buf_name)
for opt, val in pairs(options) do
api.nvim_buf_set_option(M.Tree.bufnr, opt, val)
end
for _, opt in pairs(M.Tree.buf_options) do
api.nvim_command('setlocal '..opt)
end
if M.Tree.side == 'L' then
api.nvim_command('setlocal nosplitright')
else
api.nvim_command('setlocal splitright')
end
set_mappings()
end
local function create_win()
api.nvim_command("vsplit")
api.nvim_command("wincmd "..M.Tree.side)
api.nvim_command("vertical resize "..M.Tree.win_width)
M.Tree.winnr = api.nvim_get_current_win()
api.nvim_win_set_option(M.Tree.winnr, 'relativenumber', false)
api.nvim_win_set_option(M.Tree.winnr, 'winfixwidth', true)
api.nvim_win_set_option(M.Tree.winnr, 'winfixheight', true)
api.nvim_command('setlocal winhighlight+=EndOfBuffer:LuaTreeEndOfBuffer,Normal:LuaTreeNormal,CursorLine:LuaTreeCursorLine,VertSplit:LuaTreeVertSplit')
end
function M.close()
api.nvim_win_close(M.Tree.winnr, true)
M.Tree.winnr = nil
M.Tree.bufnr = nil
end
function M.open()
create_buf()
create_win()
api.nvim_win_set_buf(M.Tree.winnr, M.Tree.bufnr)
renderer.draw(M.Tree, not M.Tree.loaded)
M.Tree.loaded = true
end
function M.win_open()
for _, win in pairs(api.nvim_list_wins()) do
if win == M.Tree.winnr then
return true
end
end
return false
end
return M

View File

@ -1,7 +0,0 @@
local M = {}
function M.path_to_matching_str(path)
return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)')
end
return M

View File

@ -1,164 +0,0 @@
local api = vim.api
local libformat = require 'lib/format'
local format = libformat.format_tree
local highlight = libformat.highlight_buffer
local stateutils = require 'lib/state'
local get_tree = stateutils.get_tree
local bindings = require 'lib/config'.bindings
local M = {
BUF_NAME = 'LuaTree'
}
function M.get_buf()
local regex = '.*'..M.BUF_NAME..'$';
for _, win in pairs(api.nvim_list_wins()) do
local buf = api.nvim_win_get_buf(win)
local buf_name = api.nvim_buf_get_name(buf)
if string.match(buf_name, regex) ~= nil then return buf end
end
return nil
end
function M.get_win()
local regex = '.*'..M.BUF_NAME..'$';
for _, win in pairs(api.nvim_list_wins()) do
local buf_name = api.nvim_buf_get_name(api.nvim_win_get_buf(win))
if string.match(buf_name, regex) ~= nil then return win end
end
return nil
end
local BUF_OPTIONS = {
'nowrap', 'sidescroll=5', 'nospell', 'nolist', 'nofoldenable',
'foldmethod=manual', 'foldcolumn=0', 'nonumber', 'norelativenumber',
'winfixwidth', 'winfixheight', 'noswapfile', 'splitbelow', 'noruler',
'noshowmode', 'noshowcmd'
}
local WIN_WIDTH = 30
local SIDE = 'H'
if api.nvim_call_function('exists', { 'g:lua_tree_width' }) == 1 then
WIN_WIDTH = api.nvim_get_var('lua_tree_width')
end
if api.nvim_call_function('exists', { 'g:lua_tree_side' }) == 1 then
if api.nvim_get_var('lua_tree_side') == 'right' then
SIDE = 'L'
end
end
function M.open()
local options = {
bufhidden = 'wipe';
buftype = 'nowrite';
modifiable = false;
}
local buf = api.nvim_create_buf(false, true)
api.nvim_buf_set_name(buf, M.BUF_NAME)
api.nvim_buf_set_option(buf, 'filetype', M.BUF_NAME)
for opt, val in pairs(options) do
api.nvim_buf_set_option(buf, opt, val)
end
api.nvim_command('vsplit')
api.nvim_command('wincmd '..SIDE)
api.nvim_command('vertical resize '..WIN_WIDTH)
api.nvim_win_set_buf(0, buf)
api.nvim_command('setlocal winhighlight=EndOfBuffer:LuaTreeEndOfBuffer,Normal:LuaTreeNormal,CursorLine:LuaTreeCursorLine,VertSplit:LuaTreeVertSplit')
for _, opt in pairs(BUF_OPTIONS) do
api.nvim_command('setlocal '..opt)
end
if SIDE == 'L' then
api.nvim_command('setlocal nosplitright')
else
api.nvim_command('setlocal splitright')
end
end
function M.replace_tree()
local win = M.get_win()
if not win then return end
local tree_position = api.nvim_win_get_position(win)
local win_width = api.nvim_win_get_width(win)
if win_width == WIN_WIDTH then
if SIDE == 'H' and tree_position[2] == 0 then return end
local columns = api.nvim_get_option('columns')
if SIDE == 'L' and tree_position[2] ~= columns - win_width then return end
end
local current_win = api.nvim_get_current_win()
api.nvim_set_current_win(win)
api.nvim_command('wincmd '..SIDE)
api.nvim_command('vertical resize '..WIN_WIDTH)
api.nvim_set_current_win(current_win)
end
function M.close()
local win = M.get_win()
if not win then return end
api.nvim_win_close(win, true)
end
function M.update_view(update_cursor)
local buf = M.get_buf();
if not buf then return end
local cursor = api.nvim_win_get_cursor(0)
local tree = get_tree()
api.nvim_buf_set_option(buf, 'modifiable', true)
api.nvim_buf_set_lines(buf, 0, -1, false, format(tree))
highlight(buf, tree)
api.nvim_buf_set_option(buf, 'modifiable', false)
if update_cursor == true then
api.nvim_win_set_cursor(0, cursor)
end
end
function M.set_mappings()
local buf = M.get_buf()
if not buf then return end
local mappings = {
['<2-LeftMouse>'] = 'open_file("edit")';
['<2-RightMouse>'] = 'open_file("chdir")';
[bindings.edit] = 'open_file("edit")';
[bindings.edit_vsplit] = 'open_file("vsplit")';
[bindings.edit_split] = 'open_file("split")';
[bindings.edit_tab] = 'open_file("tabnew")';
[bindings.cd] = 'open_file("chdir")';
[bindings.create] = 'edit_file("create")';
[bindings.remove] = 'edit_file("remove")';
[bindings.rename] = 'edit_file("rename")';
}
for k,v in pairs(mappings) do
api.nvim_buf_set_keymap(buf, 'n', k, ':lua require"tree".'..v..'<cr>', {
nowait = true, noremap = true, silent = true
})
end
end
function M.is_win_open()
return M.get_buf() ~= nil
end
return M

View File

@ -1,206 +1,150 @@
local luv = vim.loop
local tree = require'lib.tree'
local colors = require'lib.colors'
local renderer = require'lib.renderer'
local fs = require'lib.fs'
local api = vim.api
local fs_update = require 'lib/fs_update'
local create_file = fs_update.create_file
local rename_file = fs_update.rename_file
local remove_file = fs_update.remove_file
local fs = require 'lib/fs'
local check_dir_access = fs.check_dir_access
local is_dir = fs.is_dir
local get_cwd = fs.get_cwd
local state = require 'lib/state'
local get_tree = state.get_tree
local init_tree = state.init_tree
local open_dir = state.open_dir
local refresh_tree = state.refresh_tree
local set_root_path = state.set_root_path
local find_file = state.find_file
local winutils = require 'lib/winutils'
local update_view = winutils.update_view
local is_win_open = winutils.is_win_open
local close = winutils.close
local open = winutils.open
local set_mappings = winutils.set_mappings
local get_win = winutils.get_win
local git = require 'lib/git'
local refresh_git = git.refresh_git
local force_refresh_git = git.force_refresh_git
local colors = require 'lib/colors'
colors.init_colors()
local M = {}
M.replace_tree = winutils.replace_tree
init_tree()
function M.toggle()
if is_win_open() == true then
local wins = api.nvim_list_wins()
if #wins > 1 then close() end
if tree.win_open() then
tree.close()
else
open()
update_view()
set_mappings()
tree.open()
end
end
local MOVE_TO = 'l'
if api.nvim_call_function('exists', { 'g:lua_tree_side' }) == 1 then
if api.nvim_get_var('lua_tree_side') == 'right' then
MOVE_TO = 'h'
function M.close()
if tree.win_open() then
tree.close()
end
end
local function create_new_buf(open_type, bufname)
if open_type == 'edit' or open_type == 'split' then
api.nvim_command('wincmd '..MOVE_TO..' | '..open_type..' '..bufname)
elseif open_type == 'vsplit' then
local windows = api.nvim_list_wins();
api.nvim_command(#windows..'wincmd '..MOVE_TO..' | vsplit '..bufname)
elseif open_type == 'tabnew' then
api.nvim_command('tabnew '..bufname)
function M.open()
if not tree.win_open() then
tree.open()
end
end
function M.open_file(open_type)
local tree_index = api.nvim_win_get_cursor(0)[1]
local tree = get_tree()
local node = tree[tree_index]
function M.on_keypress(mode)
local node = tree.get_node_at_cursor()
if not node then return end
if node.name == '..' then
api.nvim_command('cd '..node.path..'/..')
local new_path = get_cwd()
if new_path ~= '/' then
new_path = new_path .. '/'
if mode == 'create' then
return fs.create(node)
elseif mode == 'remove' then
return fs.remove(node)
elseif mode == 'rename' then
return fs.rename(node)
end
set_root_path(new_path)
force_refresh_git()
init_tree(new_path)
update_view()
if node.name == ".." then
return tree.change_dir("..")
elseif mode == "cd" and node.entries ~= nil then
return tree.change_dir(node.absolute_path)
elseif mode == "cd" then
return
end
elseif open_type == 'chdir' then
if node.dir == false or check_dir_access(node.path .. node.name) == false then return end
api.nvim_command('cd ' .. node.path .. node.name)
local new_path = get_cwd() .. '/'
set_root_path(new_path)
force_refresh_git()
init_tree(new_path)
update_view()
elseif node.link == true then
local link_to_dir = is_dir(node.linkto)
if link_to_dir == true and check_dir_access(node.linkto) == false then return end
if link_to_dir == true then
api.nvim_command('cd ' .. node.linkto)
local new_path = get_cwd() .. '/'
set_root_path(new_path)
force_refresh_git()
init_tree(new_path)
update_view()
if node.link_to then
local stat = luv.fs_stat(node.link_to)
if stat.type == 'directory' then return end
tree.open_file(mode, node.link_to)
elseif node.entries ~= nil then
tree.unroll_dir(node)
else
create_new_buf(open_type, node.link_to);
end
elseif node.dir == true then
if check_dir_access(node.path .. node.name) == false then return end
open_dir(tree_index)
update_view(true)
else
create_new_buf(open_type, node.path .. node.name);
end
end
function M.edit_file(edit_type)
local tree = get_tree()
local tree_index = api.nvim_win_get_cursor(0)[1]
local node = tree[tree_index]
if edit_type == 'create' then
if node.dir == true then
create_file(node.path .. node.name .. '/')
else
create_file(node.path)
end
elseif edit_type == 'remove' then
remove_file(node.name, node.path)
elseif edit_type == 'rename' then
rename_file(node.name, node.path)
tree.open_file(mode, node.absolute_path)
end
end
function M.refresh()
if refresh_git() == true then
refresh_tree()
update_view()
tree.refresh_tree()
end
function M.on_enter()
local bufnr = api.nvim_get_current_buf()
local bufname = api.nvim_buf_get_name(bufnr)
local stats = luv.fs_stat(bufname)
local is_dir = stats and stats.type == 'directory'
if is_dir then
api.nvim_command('cd '..bufname)
end
local should_open = vim.g.lua_tree_auto_open == 1 and (bufname == '' or is_dir)
colors.setup()
tree.init(should_open, should_open)
end
local function is_file_readable(fname)
local stat = luv.fs_stat(fname)
if not stat or not stat.type == 'file' or not luv.fs_access(fname, 'R') then return false end
return true
end
local function find_file()
if not tree.win_open() then return end
local bufname = api.nvim_buf_get_name(api.nvim_get_current_buf())
if not is_file_readable(bufname) then return end
tree.set_index_and_redraw(bufname)
end
local function on_leave()
if #api.nvim_list_wins() == 1 and tree.win_open() then
api.nvim_command(':qa!')
end
end
function M.check_windows_and_close()
local wins = api.nvim_list_wins()
local function update_root_dir()
local bufname = api.nvim_buf_get_name(api.nvim_get_current_buf())
if not is_file_readable(bufname) or not tree.Tree.cwd then return end
if #wins == 1 and is_win_open() then
api.nvim_command('q!')
end
end
function M.navigate_to_buffer_dir(bufname)
local new_path = get_cwd()
if new_path ~= '/' then
new_path = new_path .. '/'
end
if new_path == state.get_root_path() then
-- this logic is a hack
-- depending on vim-rooter or autochdir, it would not behave the same way when those two are not enabled
-- until i implement multiple workspaces/project, it should stay like this
if bufname:match(tree.Tree.cwd:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)')) ~= nil then
return
end
set_root_path(new_path)
init_tree()
local new_cwd = luv.cwd()
if tree.Tree.cwd == new_cwd then return end
tree.change_dir(new_cwd)
end
function M.check_buffer_and_open()
local bufname = api.nvim_buf_get_name(0)
if bufname == '' then
M.toggle()
elseif is_dir(bufname) then
api.nvim_command('cd ' .. bufname)
local new_path = get_cwd()
if new_path ~= '/' then
new_path = new_path .. '/'
end
set_root_path(new_path)
init_tree()
M.toggle()
else
M.navigate_to_buffer_dir()
end
function M.buf_enter()
if vim.g.lua_tree_auto_close ~= 0 then
on_leave()
end
function M.find()
local line = find_file(api.nvim_buf_get_name(0))
if not line then return end
update_view()
local win = get_win()
if win then
api.nvim_win_set_cursor(win, { line, 0 })
update_root_dir()
if vim.g.lua_tree_follow ~= 0 then
find_file()
end
end
function M.reset_highlight()
colors.init_colors()
update_view()
colors.setup()
renderer.render_hl(tree.Tree.bufnr)
end
function M.xdg_open()
local node = tree.get_node_at_cursor()
-- TODO: this should open symlink targets
if not node or node.entries or node.link_to then return end
local cmd
if vim.fn.has('unix') == 1 then
cmd = 'xdg-open'
else
cmd = 'open'
end
vim.loop.spawn(cmd, {args={node.absolute_path}}, vim.schedule_wrap(function(code)
if code ~= 0 then
api.nvim_err_writeln("Could not open "..node.absolute_path)
end
end))
end
return M

View File

@ -10,32 +10,16 @@ hi def link LuaTreePopup Normal
augroup LuaTree
au BufWritePost * lua require'tree'.refresh()
if get(g:, 'lua_tree_auto_close') != 0
au BufEnter * lua require'tree'.check_windows_and_close()
endif
if get(g:, 'lua_tree_auto_open') != 0
au VimEnter * lua require'tree'.check_buffer_and_open()
endif
if get(g:, 'lua_tree_follow') != 0
au BufEnter * :LuaTreeFindFile
endif
au BufEnter * lua require'tree'.navigate_to_buffer_dir()
au BufEnter * lua require'tree'.buf_enter()
au VimEnter * lua require'tree'.on_enter()
au ColorScheme * lua require'tree'.reset_highlight()
augroup end
" TODO: WinEnter is not the right autocommand for this task,
" but we do not have LayoutChange or WinMove kind of option atm,
" so this is deactivated by default to avoid messing up users workflows
" au WinEnter * lua require'tree'.replace_tree()
command! LuaTreeOpen lua require'tree'.open()
command! LuaTreeClose lua require'tree'.close()
command! LuaTreeToggle lua require'tree'.toggle()
command! LuaTreeRefresh lua require'tree'.refresh()
command! LuaTreeFindFile lua require'tree'.find()
command! LuaTreeFindFile lua require'tree'.find_file()
let &cpo = s:save_cpo
unlet s:save_cpo