diff --git a/LICENSE b/LICENSE index 7161aeafcbd..4f0c63edf0e 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 53d76545220..93535dc7a9c 100644 --- a/README.md +++ b/README.md @@ -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 :LuaTreeToggle nnoremap r :LuaTreeRefresh nnoremap n :LuaTreeFindFile +" 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 - `` 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, `` will open the directory -- otherwise it will open the file in the buffer near the tree -- if the file is a symlink, `` will follow the symlink +- if the file is a directory, `` will open the directory otherwise it will open the file in the buffer near the tree +- if the file is a symlink, `` will follow the symlink (if the target is a file) - type `` will open the file in a vertical split - type `` will open the file in a horizontal split - type `` 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 `` - 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 (``, ``, ``, ``) -- [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 (``, ``, ``, ``) +- 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) diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index c601373fd7f..9ecbee5799a 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -7,6 +7,8 @@ Author: Yazdani Kiyan ============================================================================== 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 '' will open the file in a vertical split - type '' will open the file in a horizontal split - type '' 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 '' - 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. @@ -140,8 +170,9 @@ Git integration tells when a file is: - ✗ unstaged or folder is dirty - ✓ staged - ★ new file -- ✓✗ partially staged -- ✓★ new file staged +- ✓ ✗ partially staged +- ✓ ★ new file staged +- ✓ ★ ✗ new file staged and has unstaged modifications - ═ merging - ➜ renamed diff --git a/doc/tags b/doc/tags index 9631073f7e4..516eddf1b48 100644 --- a/doc/tags +++ b/doc/tags @@ -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* diff --git a/lua/lib/colors.lua b/lua/lib/colors.lua index 135af70cc58..297577d5a03 100644 --- a/lua/lib/colors.lua +++ b/lua/lib/colors.lua @@ -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,35 +60,62 @@ 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() - -local LINKS = { - FolderName = 'Directory', - Normal = 'Normal', - EndOfBuffer = 'EndOfBuffer', - CursorLine = 'CursorLine', - VertSplit = 'VertSplit', - CursorColumn = 'CursorColumn' +-- 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'; } -function M.init_colors() - colors = get_colors() - HIGHLIGHTS = create_hl() - for k, d in pairs(HIGHLIGHTS) do +local function get_links() + return { + FolderName = 'Directory', + Normal = 'Normal', + EndOfBuffer = 'EndOfBuffer', + CursorLine = 'CursorLine', + VertSplit = 'VertSplit', + CursorColumn = 'CursorColumn' + } +end + +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 diff --git a/lua/lib/config.lua b/lua/lib/config.lua index 136a66f031b..8531664f9c1 100644 --- a/lua/lib/config.lua +++ b/lua/lib/config.lua @@ -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 - 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 +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 hl = api.nvim_get_hl_by_id(id, true) - if not hl or not hl.foreground then return fallback end + 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 - return hl.foreground + return { + 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 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() +function M.get_bindings() + local keybindings = vim.g.lua_tree_bindings or {} 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')), + edit = keybindings.edit or '', + edit_vsplit = keybindings.edit_vsplit or '', + edit_split = keybindings.edit_split or '', + edit_tab = keybindings.edit_tab or '', + cd = keybindings.cd or '', + create = keybindings.create or 'a', + remove = keybindings.remove or 'd', + rename = keybindings.rename or 'r', } end -local keybindings = get('lua_tree_bindings', {}); - -M.bindings = { - edit = keybindings.edit or '', - edit_vsplit = keybindings.edit_vsplit or '', - edit_split = keybindings.edit_split or '', - edit_tab = keybindings.edit_tab or '', - cd = keybindings.cd or '.', - create = keybindings.create or 'a', - remove = keybindings.remove or 'd', - rename = keybindings.rename or 'r', -} - return M diff --git a/lua/lib/format.lua b/lua/lib/format.lua deleted file mode 100644 index 269eb0639fe..00000000000 --- a/lua/lib/format.lua +++ /dev/null @@ -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 diff --git a/lua/lib/fs.lua b/lua/lib/fs.lua index 5331df53488..6ec7923969b 100644 --- a/lua/lib/fs.lua +++ b/lua/lib/fs.lua @@ -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 +local function clear_prompt() + vim.api.nvim_command('normal :esc') +end -function M.is_dir(path) - local stat = luv.fs_lstat(path) - return stat and stat.type == 'directory' or false +local function refresh_tree() + vim.api.nvim_command(":LuaTreeRefresh") end -function M.is_symlink(path) - local stat = luv.fs_lstat(path) - return stat and stat.type == 'link' or false +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 + -- 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)) end -function M.link_to(path) - return luv.fs_readlink(path) or '' +local function get_num_entries(iter) + i = 0 + for _ in iter do + i = i + 1 + end + return i end -function M.check_dir_access(path) - if luv.fs_access(path, 'R') == true then - return true +function M.create(node) + if node.name == '..' then return end + + local add_into + if node.entries ~= nil then + add_into = node.absolute_path..'/' else - api.nvim_err_writeln('Permission denied: ' .. path) - return false + add_into = node.absolute_path:sub(0, -(#node.name + 1)) end -end -local handle = nil + 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 -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) + -- 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 - cb() - end)) + end end -function M.rm(path, cb) - local opt = { cmd='rm', args = {'-rf', path } }; - run_process(opt, 'Error removing '..path, cb) +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 -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) + 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.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} } +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 - 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) - else - cb() +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 - end) - elseif opt_file then - run_process(opt_file, 'Error creating file '..file_path, cb) + 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 diff --git a/lua/lib/fs_update.lua b/lua/lib/fs_update.lua deleted file mode 100644 index 4511899dabe..00000000000 --- a/lua/lib/fs_update.lua +++ /dev/null @@ -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 :') -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 diff --git a/lua/lib/git.lua b/lua/lib/git.lua deleted file mode 100644 index fa824d6c46b..00000000000 --- a/lua/lib/git.lua +++ /dev/null @@ -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 diff --git a/lua/lib/populate.lua b/lua/lib/populate.lua new file mode 100644 index 00000000000..f8ee64e19fb --- /dev/null +++ b/lua/lib/populate.lua @@ -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 diff --git a/lua/lib/renderer.lua b/lua/lib/renderer.lua new file mode 100644 index 00000000000..78b5825adf3 --- /dev/null +++ b/lua/lib/renderer.lua @@ -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 diff --git a/lua/lib/state.lua b/lua/lib/state.lua deleted file mode 100644 index 1ac48c641bc..00000000000 --- a/lua/lib/state.lua +++ /dev/null @@ -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 diff --git a/lua/lib/tree.lua b/lua/lib/tree.lua new file mode 100644 index 00000000000..32787594ea6 --- /dev/null +++ b/lua/lib/tree.lua @@ -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..'', { + 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 diff --git a/lua/lib/utils.lua b/lua/lib/utils.lua deleted file mode 100644 index 37f63a5825e..00000000000 --- a/lua/lib/utils.lua +++ /dev/null @@ -1,7 +0,0 @@ -local M = {} - -function M.path_to_matching_str(path) - return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)') -end - -return M diff --git a/lua/lib/winutils.lua b/lua/lib/winutils.lua deleted file mode 100644 index 5095ca3e465..00000000000 --- a/lua/lib/winutils.lua +++ /dev/null @@ -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..'', { - nowait = true, noremap = true, silent = true - }) - end -end - -function M.is_win_open() - return M.get_buf() ~= nil -end - -return M diff --git a/lua/tree.lua b/lua/tree.lua index 6890254a855..f2f32935e22 100644 --- a/lua/tree.lua +++ b/lua/tree.lua @@ -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] - - if node.name == '..' then - api.nvim_command('cd '..node.path..'/..') +function M.on_keypress(mode) + local node = tree.get_node_at_cursor() + if not node then return end - local new_path = get_cwd() - if new_path ~= '/' then - new_path = new_path .. '/' - end + 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() - - 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() - else - create_new_buf(open_type, node.link_to); - end + 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 node.dir == true then - if check_dir_access(node.path .. node.name) == false then return end - open_dir(tree_index) - update_view(true) + 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.path .. node.name); + tree.open_file(mode, node.absolute_path) 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] +function M.refresh() + tree.refresh_tree() +end - 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) +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 -function M.refresh() - if refresh_git() == true then - refresh_tree() - update_view() - 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 -function M.check_windows_and_close() - local wins = api.nvim_list_wins() +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 - if #wins == 1 and is_win_open() then - api.nvim_command('q!') - end + tree.set_index_and_redraw(bufname) end -function M.navigate_to_buffer_dir(bufname) - local new_path = get_cwd() - if new_path ~= '/' then - new_path = new_path .. '/' +local function on_leave() + if #api.nvim_list_wins() == 1 and tree.win_open() then + api.nvim_command(':qa!') end - if new_path == state.get_root_path() then +end + +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 + + -- 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() -end + local new_cwd = luv.cwd() + if tree.Tree.cwd == new_cwd then return 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) + tree.change_dir(new_cwd) +end - local new_path = get_cwd() - if new_path ~= '/' then - new_path = new_path .. '/' - end - set_root_path(new_path) - init_tree() +function M.buf_enter() + if vim.g.lua_tree_auto_close ~= 0 then + on_leave() + end - M.toggle() - else - M.navigate_to_buffer_dir() + update_root_dir() + if vim.g.lua_tree_follow ~= 0 then + find_file() end end -function M.find() - local line = find_file(api.nvim_buf_get_name(0)) - if not line then return end +function M.reset_highlight() + colors.setup() + renderer.render_hl(tree.Tree.bufnr) +end - update_view() +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 win = get_win() - if win then - api.nvim_win_set_cursor(win, { line, 0 }) + local cmd + if vim.fn.has('unix') == 1 then + cmd = 'xdg-open' + else + cmd = 'open' end -end - -function M.reset_highlight() - colors.init_colors() - update_view() + 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 diff --git a/plugin/tree.vim b/plugin/tree.vim index af5e2c6a41d..4fbdfd16b9f 100644 --- a/plugin/tree.vim +++ b/plugin/tree.vim @@ -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