├── .gitignore ├── README.md ├── doc ├── nvim-traveller.txt └── tags └── lua ├── nvim-traveller.lua └── nvim-traveller ├── fm-globals.lua ├── fm-location.lua ├── fm-popup.lua ├── fm-shell.lua ├── fm-theming.lua ├── navigation.lua ├── persist-data.lua └── plugin-telescope.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | debug.log 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-traveller 2 | A file manager inside Neovim. take a look at github.com/norlock/nvim-traveller-buffers for a good 3 | complementary plugin for buffers 4 | 5 | ### What makes this file manager different than others? 6 | 7 | I want to put the emphasis on multi-project use, having a polished experience inside Neovim. Take a 8 | look at the showcase to see how it can enhance your workflow for multi-project use. 9 | The idea is that you don't have to open new terminals and navigate to the desired locations only to open up another instance of Neovim. 10 | 11 | If for instance you are creating a frontend application and want to see what kind of parameters your 12 | request needs to have. You can navigate inside Neovim quickly and open the backend project. You 13 | share the buffers so yanking / pasting is very convenient. It also makes sure cwd is always correct 14 | so your plugins will work. 15 | 16 | If for example you need to tail some log file of your backend you can open a real terminal (or 17 | terminal tab) from inside Neovim at the correct location. 18 | 19 | ## Features 20 | - [x] Fast navigation through directories 21 | - [x] Open files in buffer/tab/split/vsplit 22 | - [x] Open a Neovim terminal tab with the navigated directory 23 | - [x] Open a real terminal with the navigated directory 24 | - [x] Create files or directories with one command 25 | - [x] Delete directories or files 26 | - [x] Easy to cancel navigation or commands 27 | - [x] Move or rename an item 28 | - [x] Follows symlinks 29 | - [x] Toggle hidden files 30 | - [x] Use git rm if possible 31 | - [x] Use git mv if possible 32 | - [x] Telescope integration with directories 33 | - [x] Show last visited directories 34 | - [x] Opening terminal at desired location 35 | - [x] Change cwd to git root if possible (optional) 36 | - [x] Change cwd to traveller (optional) 37 | - [x] Navigate to home directory with a hotkey 38 | - [x] Being able to select items 39 | - [x] Being able to delete selected items (using git rm if possible) 40 | - [x] Being able to move / copy selected items 41 | - [ ] Use git mv if possible 42 | - [x] Project buffers (see nvim-traveller-buffers) 43 | - [x] Selection feedback window in the bottom 44 | - [x] Resize windows if needed 45 | - [x] Help menu in popup 46 | - [ ] Custom keymapping 47 | - [x] Docs 48 | - [x] Open binaries with open 49 | - [ ] Optional: FZF/(Other fuzzy file searcher) if there is demand for it 50 | - [ ] Optional: being able to pass stringed cmds "test file.lua" 51 | - [ ] Optional: Support for Windows (if there is demand for it) 52 | - [ ] Optional: Custom directory for telescope global search 53 | 54 | ## Showcase 55 | 56 | https://github.com/Norlock/nvim-traveller/assets/7510943/ccaa83ce-593c-4dde-8bb6-a0b612a67d4b 57 | 58 | ## Startup 59 | 60 | Install using packer: 61 | ```lua 62 | use 'nvim-lua/plenary.nvim', 63 | use 'nvim-telescope/telescope.nvim', tag = '0.1.2', 64 | use 'norlock/nvim-traveller', 65 | ``` 66 | 67 | Install using vim-plug: 68 | ```viml 69 | Plug 'nvim-lua/plenary.nvim' 70 | Plug 'nvim-telescope/telescope.nvim', { 'tag': '0.1.2' } 71 | Plug 'norlock/nvim-traveller' 72 | ``` 73 | 74 | ## Requires 75 | - Telescope plugin 76 | - fd - https://github.com/sharkdp/fd 77 | 78 | ## Usage 79 | 80 | Lua: 81 | ```lua 82 | local traveller = require('nvim-traveller').setup({ 83 | show_hidden = false, 84 | mappings = { 85 | -- directories overview 86 | directories_tab = "", 87 | directories_delete = "" 88 | } 89 | }) 90 | 91 | vim.keymap.set('n', '-', traveller.open_navigation, {}) 92 | -- Opens quick directory search (Tab to display all directories) 93 | vim.keymap.set('n', 'd', traveller.last_directories_search, {}) 94 | vim.keymap.set('n', 'o', traveller.open_terminal, {}) 95 | 96 | Viml: 97 | ```viml 98 | nnoremap - lua require('nvim-traveller').open_navigation() 99 | -- Opens quick directory search (Tab to display all directories) 100 | nnoremap d lua require('nvim-traveller').last_directories_search() 101 | nnoremap o lua require('nvim-traveller').open_terminal() 102 | ``` 103 | 104 | - When navigation is openend press ? for more info 105 | -------------------------------------------------------------------------------- /doc/nvim-traveller.txt: -------------------------------------------------------------------------------- 1 | Nvim-traveller - *traveller* 2 | 3 | A file manager made for multiproject purpose, so you don't have to close your neovim instance. 4 | 5 | Problem: 6 | 1 - 7 | You are working on a project with a frontend and backed. You find out that you 8 | need an extra field in your api call response, what do you do? Most people will 9 | open another terminal or tmux window and (cd/fzf/z) to the directory they need 10 | to go. Than they open neovim with the name of the file. 11 | 12 | Already you are spending mental capacity trying to navigate to that other 13 | project. You also can't use the buffer of each other session (unless you use 14 | something like nvr), if you just for example want to copy some fields or 15 | function names. 16 | 17 | 2 - 18 | You navigated to some directory and realised you want to tail the log file 19 | there. Opening a terminal and jump towards the same directory is taking time and 20 | mental resources. 21 | 22 | Solution: 23 | Use the telescope directory search customization by Nvim-traveller. Open the 24 | terminal (=) or press f/a to start the fuzzy search. Nvim-traveller will 25 | automatically update cwd to the current file, so all your plugins work. 26 | 27 | Help - *traveller-help* 28 | 29 | To see help press "?" after openning the navigation buffer. 30 | 31 | Github - *traveller-github* 32 | 33 | For more information navigate to: 34 | https://github.com/Norlock/nvim-traveller 35 | -------------------------------------------------------------------------------- /doc/tags: -------------------------------------------------------------------------------- 1 | traveller nvim-traveller.txt /*traveller* 2 | traveller-github nvim-traveller.txt /*traveller-github* 3 | traveller-help nvim-traveller.txt /*traveller-help* 4 | -------------------------------------------------------------------------------- /lua/nvim-traveller.lua: -------------------------------------------------------------------------------- 1 | local NavigationState = require("nvim-traveller.navigation") 2 | local fm_telescope = require("nvim-traveller.plugin-telescope") 3 | local path = require("plenary.path") 4 | local fm_shell = require("nvim-traveller.fm-shell") 5 | local fm_globals = require("nvim-traveller.fm-globals") 6 | 7 | local state = {} 8 | local M = {} 9 | 10 | ---@class ModOptions 11 | ---@field show_hidden boolean show hidden by default or not (default is true) 12 | ---@field mappings table 13 | local ModOptions = {} 14 | 15 | function M.open_navigation() 16 | if state.is_initialized then 17 | if vim.api.nvim_get_current_buf() ~= state.buf_id then 18 | state:init() 19 | state:open_navigation() 20 | end 21 | else 22 | state = NavigationState:new() 23 | state:open_navigation() 24 | end 25 | end 26 | 27 | function M.git_root() 28 | fm_globals.set_cwd_to_git_root() 29 | end 30 | 31 | function M.all_directories_search() 32 | if not state.is_initialized then 33 | state = NavigationState:new() 34 | end 35 | 36 | fm_telescope:directories_search(state, false) 37 | end 38 | 39 | function M.last_directories_search() 40 | if not state.is_initialized then 41 | state = NavigationState:new() 42 | end 43 | 44 | fm_telescope:directories_search(state, true) 45 | end 46 | 47 | function M.open_terminal() 48 | local fd = vim.fn.expand('%:p:h') 49 | 50 | if vim.fn.isdirectory(fd) == 1 then 51 | local abs = path:new(fd):absolute() 52 | fm_shell.open_terminal(abs) 53 | end 54 | end 55 | 56 | ---Setup global options 57 | ---@param options ModOptions 58 | function M.setup(options) 59 | --vim.api.nvim_create_autocmd("VimEnter", { 60 | -- callback = function() 61 | -- vim.api.nvim_del_augroup_by_name("FileExplorer") 62 | 63 | -- local fn = vim.fn.expand('%:t') 64 | -- local filetype = vim.bo.filetype 65 | -- if filetype == "netrw" or fn == "" then 66 | -- vim.bo.buftype = "nofile" 67 | -- vim.bo.bufhidden = "wipe" 68 | -- vim.bo.buflisted = false 69 | -- M.open_navigation() 70 | -- end 71 | -- end 72 | --}) 73 | 74 | NavigationState:set_mod_options(options or {}) 75 | fm_telescope.set_mod_options(options or {}) 76 | 77 | return M 78 | end 79 | 80 | return M 81 | -------------------------------------------------------------------------------- /lua/nvim-traveller/fm-globals.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | os = vim.loop.os_uname().sysname, 3 | only_stderr = " > /dev/null", 4 | only_stdout = " 2> /dev/null", 5 | } 6 | 7 | function M.is_item_directory(item) 8 | local ending = "/" 9 | return item:sub(- #ending) == ending 10 | end 11 | 12 | local log_path = vim.fn.stdpath('log') .. '/nvim-traveller.log' 13 | 14 | function M.debug(val, label) 15 | local filewrite = io.open(log_path, "a+") 16 | 17 | if filewrite == nil then 18 | print("Can't open debug file") 19 | return 20 | end 21 | 22 | if label ~= nil then 23 | filewrite:write("--" .. label .. "\n") 24 | end 25 | 26 | filewrite:write(vim.inspect(val) .. "\n\n") 27 | filewrite:close() 28 | end 29 | 30 | M.debug("Opening Neovim " .. os.date('%Y-%m-%d %H:%M:%S')) 31 | 32 | function M.round(num) 33 | local fraction = num % 1 34 | if 0.5 < fraction then 35 | return math.ceil(num) 36 | else 37 | return math.floor(num) 38 | end 39 | end 40 | 41 | function M.split(str, sep) 42 | local parts = {} 43 | for part in string.gmatch(str, "([^" .. sep .. "]+)") do 44 | table.insert(parts, part) 45 | end 46 | return parts 47 | end 48 | 49 | function M.trim(str) 50 | return str:match("^%s*(.-)%s*$") 51 | end 52 | 53 | function M.sanitize(str) 54 | return M.trim('"' .. str .. '"') 55 | end 56 | 57 | function M.item_is_part_of_git_repo(dir_path, item) 58 | local sh_cmd = "cd " .. 59 | M.sanitize(dir_path) .. " && git ls-files --error-unmatch " .. M.sanitize(item) .. M.only_stderr 60 | return #vim.fn.systemlist(sh_cmd) == 0 61 | end 62 | 63 | function M.get_git_root(dir_path) 64 | local sh_cmd = "cd " .. dir_path .. " && git rev-parse --show-toplevel" .. M.only_stdout 65 | return vim.fn.systemlist(sh_cmd)[1] 66 | end 67 | 68 | function M.set_cwd_to_git_root() 69 | local git_root = M.get_git_root(vim.fn.expand('%:p:h')) 70 | 71 | if git_root ~= nil then 72 | vim.api.nvim_set_current_dir(git_root); 73 | end 74 | end 75 | 76 | function M.directory_is_inside_a_git_repo(dir_path) 77 | local sh_cmd = "cd " .. dir_path .. " && git rev-parse --is-inside-work-tree" .. M.only_stderr 78 | return #vim.fn.systemlist(sh_cmd) == 0 79 | end 80 | 81 | function M.get_home_directory() 82 | return vim.fn.expand("$HOME") .. "/" 83 | end 84 | 85 | function M.list_contains(list, input) 86 | for _, dir in pairs(list) do 87 | if dir == input then return true end 88 | end 89 | return false 90 | end 91 | 92 | ---@param target table 93 | ---@param other table 94 | ---@return table 95 | function M.concat_table(target, other) 96 | for i = 1, #other do 97 | target[#target + 1] = other[i] 98 | end 99 | return target 100 | end 101 | 102 | return M 103 | -------------------------------------------------------------------------------- /lua/nvim-traveller/fm-location.lua: -------------------------------------------------------------------------------- 1 | ---@class Location 2 | ---@field dir_path string 3 | ---@field item_name string 4 | local Location = {} 5 | 6 | ---Create new location 7 | ---@param dir_path string 8 | ---@param item_name string 9 | ---@return Location 10 | function Location:new(dir_path, item_name) 11 | local o = {} 12 | setmetatable(o, self) 13 | self.__index = self 14 | 15 | o.dir_path = dir_path 16 | o.item_name = item_name 17 | 18 | return o 19 | end 20 | 21 | ---@return string 22 | function Location:to_path() 23 | return self.dir_path .. self.item_name 24 | end 25 | 26 | return Location 27 | -------------------------------------------------------------------------------- /lua/nvim-traveller/fm-popup.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | local fm_theming = require("nvim-traveller.fm-theming") 3 | local fm_shell = require("nvim-traveller.fm-shell") 4 | 5 | ---@class Popup 6 | ---@field win_id integer 7 | ---@field buf_id integer 8 | ---@field buf_content string[] 9 | ---@field buffer_options table 10 | Popup = {} 11 | 12 | ---@return Popup 13 | function Popup:new() 14 | local o = {} 15 | setmetatable(o, self) 16 | self.__index = self 17 | 18 | local buf_id = vim.api.nvim_create_buf(false, true) 19 | o.buf_id = buf_id 20 | o.buf_content = {} 21 | o.buffer_options = { silent = true, buffer = buf_id } 22 | 23 | return o 24 | end 25 | 26 | function Popup:close() 27 | if vim.api.nvim_win_is_valid(self.win_id) then 28 | vim.api.nvim_win_close(self.win_id, false) 29 | end 30 | end 31 | 32 | function Popup:set_keymap(mode, lhs, rhs) 33 | vim.keymap.set(mode, lhs, rhs, self.buffer_options) 34 | end 35 | 36 | function Popup:set_buffer_content(buf_content) 37 | self.buf_content = buf_content 38 | 39 | vim.api.nvim_buf_set_option(self.buf_id, 'modifiable', true) 40 | vim.api.nvim_buf_set_lines(self.buf_id, 0, -1, true, self.buf_content) 41 | vim.api.nvim_buf_set_option(self.buf_id, 'modifiable', false) 42 | end 43 | 44 | function Popup:init_cmd_variant(title, buf_content) 45 | vim.api.nvim_create_autocmd({ "BufWinLeave", "BufLeave", "BufHidden" }, { 46 | buffer = self.buf_id, 47 | callback = function() 48 | vim.cmd('stopinsert') 49 | self:close() 50 | end 51 | }) 52 | 53 | local ui = vim.api.nvim_list_uis()[1] 54 | local width = fm_globals.round(ui.width * 0.6) 55 | local height = 1 56 | 57 | local win_options = { 58 | relative = 'editor', 59 | width = width, 60 | height = height, 61 | col = fm_globals.round((ui.width - width) * 0.5), 62 | row = fm_globals.round((ui.height - height) * 0.2), 63 | anchor = 'NW', 64 | style = 'minimal', 65 | border = 'rounded', 66 | title = title, 67 | title_pos = 'left', 68 | noautocmd = true, 69 | } 70 | 71 | vim.api.nvim_buf_set_lines(self.buf_id, 0, -1, true, buf_content) 72 | 73 | self.win_id = vim.api.nvim_open_win(self.buf_id, true, win_options) 74 | fm_theming.add_cmd_popup_theming(self) 75 | end 76 | 77 | local function create_help_window_options() 78 | local ui = vim.api.nvim_list_uis()[1] 79 | local width = fm_globals.round(ui.width * 0.9) 80 | local height = fm_globals.round(ui.height * 0.8) 81 | 82 | return { 83 | relative = 'editor', 84 | width = width, 85 | height = height, 86 | col = (ui.width - width) * 0.5, 87 | row = (ui.height - height) * 0.2, 88 | anchor = 'NW', 89 | style = 'minimal', 90 | border = 'rounded', 91 | title = ' Help ', 92 | title_pos = 'center', 93 | noautocmd = true, 94 | } 95 | end 96 | 97 | local function create_feedback_window_options(related_win_id, title, buf_content) 98 | local win_width = vim.api.nvim_win_get_width(related_win_id) 99 | local win_height = vim.api.nvim_win_get_height(related_win_id) 100 | local height = #buf_content 101 | 102 | return { 103 | relative = 'win', 104 | win = related_win_id, 105 | width = win_width, 106 | height = height, 107 | row = win_height - height - 1, 108 | col = -1, 109 | anchor = 'NW', 110 | style = 'minimal', 111 | border = 'single', 112 | title = title, 113 | title_pos = "right", 114 | noautocmd = true, 115 | } 116 | end 117 | 118 | local function create_selection_window_options(related_win_id) 119 | local win_width = vim.api.nvim_win_get_width(related_win_id) 120 | local win_height = vim.api.nvim_win_get_height(related_win_id) 121 | local height = 1 122 | 123 | return { 124 | relative = 'win', 125 | win = related_win_id, 126 | width = win_width, 127 | height = height, 128 | row = win_height - height, 129 | col = -1, 130 | anchor = 'NW', 131 | style = 'minimal', 132 | border = 'none', 133 | noautocmd = true, 134 | } 135 | end 136 | 137 | ---@param buf_content string[] 138 | ---@param win_options any 139 | function Popup:init_info_variant(buf_content, win_options) 140 | vim.api.nvim_create_autocmd({ "BufLeave" }, { 141 | buffer = self.buf_id, 142 | callback = function() 143 | self:close() 144 | end 145 | }) 146 | 147 | self.win_id = vim.api.nvim_open_win(self.buf_id, true, win_options) 148 | self.buffer_options = { silent = true, buffer = self.buf_id } 149 | 150 | vim.keymap.set('n', '', function() self:close() end, self.buffer_options) 151 | vim.keymap.set('n', 'q', function() self:close() end, self.buffer_options) 152 | 153 | self:set_buffer_content(buf_content) 154 | end 155 | 156 | ---@param buf_content string[] 157 | ---@param win_options any 158 | function Popup:init_status_variant(buf_content, win_options) 159 | self.win_id = vim.api.nvim_open_win(self.buf_id, false, win_options) 160 | self.buffer_options = { silent = true, buffer = self.buf_id } 161 | 162 | vim.keymap.set('n', '', function() self:close() end, self.buffer_options) 163 | vim.keymap.set('n', 'q', function() self:close() end, self.buffer_options) 164 | 165 | self:set_buffer_content(buf_content) 166 | end 167 | 168 | ---@param current_location Location 169 | ---@param mv_cmd mv_cmd 170 | ---@return string 171 | function Popup:create_mv_cmd(current_location, mv_cmd) 172 | local user_input = vim.api.nvim_buf_get_lines(self.buf_id, 0, 1, false)[1] 173 | return fm_shell.create_mv_cmd(current_location, user_input, mv_cmd) 174 | end 175 | 176 | ---@param dir_path string 177 | ---@return string 178 | function Popup:create_new_items_cmd(dir_path) 179 | local user_input = vim.api.nvim_buf_get_lines(self.buf_id, 0, 1, false) 180 | return fm_shell.create_new_items_cmd(dir_path, user_input) 181 | end 182 | 183 | ---@param nav_state any 184 | ---@return string[] 185 | local function create_selection_buf_content(nav_state) 186 | return { " " .. #nav_state.selection .. " items selected: [u] undo, [pm] paste as move, " 187 | .. "[pc] paste as copy)" } 188 | end 189 | ---updates text for popups with 190 | ---@param nav_state NavigationState 191 | function Popup:update_status_text(nav_state) 192 | local buf_content = create_selection_buf_content(nav_state) 193 | self:set_buffer_content(buf_content) 194 | end 195 | 196 | local M = {} 197 | 198 | ---@param parent_win_id integer 199 | ---@param buf_content string[] 200 | ---@return Popup 201 | function M.create_delete_item_popup(parent_win_id, buf_content) 202 | local popup = Popup:new() 203 | 204 | local title = 'Confirm (Enter), cancel (Esc / q)' 205 | local window_options = create_feedback_window_options(parent_win_id, title, buf_content) 206 | popup:init_info_variant(buf_content, window_options) 207 | 208 | fm_theming.add_info_popup_theming(popup) 209 | 210 | return popup 211 | end 212 | 213 | function M.create_items_popup() 214 | local popup = Popup:new() 215 | popup:init_cmd_variant(' Create (separate by space) ', {}) 216 | 217 | popup:set_keymap('i', '', function() popup:close() end) 218 | vim.cmd("startinsert") 219 | 220 | return popup 221 | end 222 | 223 | ---comment 224 | ---@param location Location 225 | ---@return Popup 226 | function M.create_move_popup(location) 227 | local popup = Popup:new() 228 | popup:init_cmd_variant(' Move (mv) ', { location.dir_path .. location.item_name }) 229 | 230 | vim.api.nvim_win_set_cursor(popup.win_id, { 1, #location.dir_path }) 231 | popup:set_keymap('n', '', function() popup:close() end) 232 | popup:set_keymap('n', '', function() popup:close() end) 233 | 234 | return popup 235 | end 236 | 237 | ---@param nav_state NavigationState 238 | function M.create_selection_popup(nav_state) 239 | local popup = Popup:new() 240 | 241 | local buf_content = create_selection_buf_content(nav_state) 242 | local window_opts = create_selection_window_options(nav_state.win_id) 243 | 244 | popup:init_status_variant(buf_content, window_opts) 245 | fm_theming.add_status_popup_theming(popup) 246 | 247 | return popup 248 | end 249 | 250 | function M.create_help_popup() 251 | local popup = Popup:new() 252 | 253 | local buf_content = { 254 | " -- Navigation", 255 | " [h / ] Navigate to parent", 256 | " [l / / ] Navigate to directory or open item", 257 | " [q / ] Close popup / navigation", 258 | " [.] Toggle hidden or all files", 259 | " [gh] Navigate to the home directory", 260 | " [g/] Navigate to the root directory", 261 | " ", 262 | " -- Commands", 263 | " [t] Open file as tab", 264 | " [s] Open file as split", 265 | " [v] Open file as vsplit", 266 | " [os] Open terminal in Neovim", 267 | " [ot] Open terminal (using $TERM)", 268 | " [c] Create items (e.g.: test.lua lua/ lua/some_file.lua)", 269 | " [dd] Delete item / Delete selection", 270 | " [m] Move or rename item (e.g.: .. will move to parent)", 271 | " [f] Toggle telescope find_files inside directory", 272 | " [a] Toggle telescope live_grep inside directory", 273 | " ", 274 | " -- Selection", 275 | " [y] Yank item (add / remove to selection)", 276 | " [pm] Paste as move", 277 | " [pc] Paste as copy", 278 | " [u] Undo selection", 279 | " ", 280 | " -- Telescope search directory", 281 | " [ / t / v / s] Open directory in traveller", 282 | " [] Toggle all directories / last used ones", 283 | " [] Remove directory from the last used list", 284 | } 285 | 286 | local function init() 287 | local window_options = create_help_window_options() 288 | popup:init_info_variant(buf_content, window_options) 289 | fm_theming.add_help_popup_theming(popup) 290 | fm_theming.theme_help_content(popup) 291 | end 292 | 293 | init() 294 | 295 | vim.api.nvim_create_autocmd("VimResized", { 296 | buffer = popup.buf_id, 297 | callback = function() 298 | popup:close() 299 | init() 300 | end 301 | }) 302 | end 303 | 304 | return M 305 | -------------------------------------------------------------------------------- /lua/nvim-traveller/fm-shell.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | 3 | local M = {} 4 | 5 | ---@alias mv_cmd 'mv' | 'git mv' 6 | ---@param src_location Location 7 | ---@param dst_str string 8 | ---@param mv_cmd mv_cmd 9 | ---@return string 10 | function M.create_mv_cmd(src_location, dst_str, mv_cmd) 11 | local dir_path = src_location.dir_path 12 | local item_name = src_location.item_name 13 | 14 | local sh_cmd_prefix = table.concat({ "cd", dir_path, "&&", mv_cmd, fm_globals.sanitize(item_name) }, " ") 15 | 16 | local sanitize = fm_globals.sanitize(dst_str) 17 | return sh_cmd_prefix .. " " .. sanitize 18 | end 19 | 20 | ---@param state NavigationState 21 | ---@return string[] 22 | function M.create_mv_cmds_selection(state) 23 | local sh_cmds = {} 24 | 25 | -- TODO try to use git mv cmd as well 26 | for _, event in pairs(state.selection) do 27 | local sanitize_src = fm_globals.sanitize(event.dir_path .. event.item_name) 28 | local sanitize_dst = fm_globals.sanitize(state.dir_path) 29 | 30 | local cmd = { "mv", sanitize_src, sanitize_dst, fm_globals.only_stderr } 31 | 32 | table.insert(sh_cmds, table.concat(cmd, " ")) 33 | end 34 | return sh_cmds 35 | end 36 | 37 | ---@param state NavigationState 38 | ---@return string[] 39 | function M.create_cp_cmds_selection(state) 40 | local sh_cmds = {} 41 | for _, event in pairs(state.selection) do 42 | local sanitize_src = fm_globals.sanitize(event.dir_path .. event.item_name) 43 | local sanitize_dst = fm_globals.sanitize(state.dir_path) 44 | 45 | local cp_prefix = "cp" 46 | if fm_globals.is_item_directory(event.item_name) then 47 | cp_prefix = cp_prefix .. " -r" 48 | end 49 | 50 | local cmd = { cp_prefix, sanitize_src, sanitize_dst, fm_globals.only_stderr } 51 | 52 | table.insert(sh_cmds, table.concat(cmd, " ")) 53 | end 54 | return sh_cmds 55 | end 56 | 57 | local function get_rm_cmd(dir_path, item_name) 58 | local function get_rm_cmd_parts() 59 | if fm_globals.item_is_part_of_git_repo(dir_path, item_name) then 60 | return "cd " .. dir_path .. " && git rm", fm_globals.sanitize(item_name) 61 | else 62 | return "rm", fm_globals.sanitize(dir_path .. item_name) 63 | end 64 | end 65 | 66 | local rm_prefix, rm_suffix = get_rm_cmd_parts() 67 | 68 | if fm_globals.is_item_directory(item_name) then 69 | return rm_prefix .. " -rf " .. rm_suffix 70 | else 71 | return rm_prefix .. " " .. rm_suffix 72 | end 73 | end 74 | 75 | ---@param state NavigationState 76 | ---@return string[] 77 | function M.create_rm_cmds(state) 78 | local sh_cmds = {} 79 | 80 | if #state.selection ~= 0 then 81 | for _, event in pairs(state.selection) do 82 | table.insert(sh_cmds, get_rm_cmd(event.dir_path, event.item_name)) 83 | end 84 | else 85 | local dir_path = state:get_relative_path() 86 | local item_name = state:get_cursor_item() 87 | table.insert(sh_cmds, get_rm_cmd(dir_path, item_name)) 88 | end 89 | 90 | return sh_cmds 91 | end 92 | 93 | ---Creates new items through touch or mkdir 94 | ---@param dir_path string 95 | ---@param user_input string 96 | ---@return string 97 | function M.create_new_items_cmd(dir_path, user_input) 98 | local parts = fm_globals.split(user_input[1], " ") 99 | 100 | --local cmd = sh_cmd 101 | local touch_cmds = {} 102 | local mkdir_cmds = {} 103 | 104 | for _, item in ipairs(parts) do 105 | if fm_globals.is_item_directory(item) then 106 | table.insert(mkdir_cmds, dir_path .. item) 107 | else 108 | table.insert(touch_cmds, dir_path .. item) 109 | end 110 | end 111 | 112 | local mkdr_sh_cmd = "mkdir -p" 113 | local touch_sh_cmd = "touch" 114 | local has_mkdir_cmds = #mkdir_cmds ~= 0 115 | local has_touch_cmds = #touch_cmds ~= 0 116 | 117 | if has_mkdir_cmds then 118 | for _, item in pairs(mkdir_cmds) do 119 | mkdr_sh_cmd = mkdr_sh_cmd .. " " .. item 120 | end 121 | end 122 | 123 | if has_touch_cmds then 124 | for _, item in pairs(touch_cmds) do 125 | touch_sh_cmd = touch_sh_cmd .. " " .. item 126 | end 127 | end 128 | 129 | if has_mkdir_cmds then 130 | if has_touch_cmds then 131 | return mkdr_sh_cmd .. " && " .. touch_sh_cmd 132 | else 133 | return mkdr_sh_cmd 134 | end 135 | else 136 | return touch_sh_cmd 137 | end 138 | end 139 | 140 | function M.open_terminal() 141 | local term = vim.fn.expand("$TERM") 142 | 143 | local cwd = vim.fn.expand("%:p:h"); 144 | 145 | vim.fn.jobstart(term, { cwd = cwd, detach = true }) 146 | end 147 | 148 | function M.open_shell(rel_path) 149 | vim.cmd("tabe") 150 | vim.cmd("terminal cd " .. rel_path .. " && $SHELL") 151 | vim.cmd("startinsert") 152 | end 153 | 154 | function M.is_file_binary(file_path) 155 | local output = vim.fn.systemlist("file --mime " .. file_path .. " | grep charset=binary") 156 | 157 | return #output ~= 0 and not output[1]:find("inode/x-empty;", nil, true) 158 | end 159 | 160 | return M 161 | -------------------------------------------------------------------------------- /lua/nvim-traveller/fm-theming.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | 3 | local M = { 4 | navigation_ns_id = vim.api.nvim_create_namespace("TravellerNavigation"), 5 | popup_ns_id = vim.api.nvim_create_namespace("TravellerInfo"), 6 | help_ns_id = vim.api.nvim_create_namespace("TravellerHelp"), 7 | status_ns_id = vim.api.nvim_create_namespace("TravellerStatus") 8 | } 9 | 10 | ---@param state NavigationState 11 | function M.remove_navigation(state) 12 | vim.api.nvim_win_set_hl_ns(state.win_id, 0) 13 | end 14 | 15 | ---@param state NavigationState 16 | function M.add_navigation_theming(state) 17 | vim.opt.cursorline = true 18 | 19 | local cursor_line_hl = vim.api.nvim_get_hl(0, { name = 'CursorLine' }) 20 | cursor_line_hl.bold = true 21 | 22 | vim.api.nvim_set_hl(M.navigation_ns_id, 'CursorLine', cursor_line_hl) 23 | vim.api.nvim_win_set_hl_ns(state.win_id, M.navigation_ns_id) 24 | end 25 | 26 | ---@param state Popup 27 | function M.add_cmd_popup_theming(state) 28 | vim.api.nvim_set_hl(M.popup_ns_id, 'FloatTitle', { link = "Question" }) 29 | vim.api.nvim_set_hl(M.popup_ns_id, 'FloatBorder', {}) 30 | vim.api.nvim_set_hl(M.popup_ns_id, 'NormalFloat', {}) 31 | 32 | vim.api.nvim_win_set_hl_ns(state.win_id, M.popup_ns_id) 33 | end 34 | 35 | ---@param state Popup 36 | function M.add_info_popup_theming(state) 37 | local hlBorder = { 38 | link = "Question", 39 | } 40 | 41 | vim.api.nvim_set_hl(M.popup_ns_id, 'FloatBorder', hlBorder) 42 | vim.api.nvim_set_hl(M.popup_ns_id, 'FloatTitle', hlBorder) 43 | vim.api.nvim_set_hl(M.popup_ns_id, 'NormalFloat', { italic = true }) 44 | 45 | vim.api.nvim_win_set_hl_ns(state.win_id, M.popup_ns_id) 46 | end 47 | 48 | ---@param state Popup 49 | function M.add_status_popup_theming(state) 50 | local hl_question = vim.api.nvim_get_hl(0, { name = "Question" }) 51 | hl_question.reverse = true 52 | 53 | vim.api.nvim_set_hl(M.status_ns_id, 'NormalFloat', hl_question) 54 | 55 | vim.api.nvim_win_set_hl_ns(state.win_id, M.status_ns_id) 56 | end 57 | 58 | ---@param state Popup 59 | function M.add_help_popup_theming(state) 60 | vim.api.nvim_set_hl(M.help_ns_id, 'FloatBorder', {}) 61 | vim.api.nvim_set_hl(M.help_ns_id, 'NormalFloat', {}) 62 | 63 | vim.api.nvim_win_set_hl_ns(state.win_id, M.help_ns_id) 64 | end 65 | 66 | ---Themes the buffer 67 | ---@param state NavigationState 68 | function M.theme_buffer_content(state) 69 | vim.api.nvim_buf_clear_namespace(state.buf_id, M.navigation_ns_id, 0, -1) 70 | 71 | if #state.buf_content == 0 then 72 | vim.opt_local.cursorline = false 73 | local ui = vim.api.nvim_list_uis()[1] 74 | local text = "Traveller - (Empty directory)" 75 | local width = #text 76 | local center = fm_globals.round((ui.width - width) * 0.5) - 2 77 | 78 | vim.api.nvim_buf_set_extmark(state.buf_id, M.navigation_ns_id, 0, 0, { 79 | id = 1, 80 | end_row = 0, 81 | virt_text = { { text, "Comment" } }, 82 | virt_text_win_col = center, 83 | }) 84 | else 85 | vim.cmd("set cursorline<") 86 | end 87 | 88 | for i, item_name in ipairs(state.buf_content) do 89 | if fm_globals.is_item_directory(item_name) then 90 | vim.api.nvim_buf_add_highlight(state.buf_id, M.navigation_ns_id, "Directory", i - 1, 0, -1) 91 | end 92 | 93 | if state:is_selected(item_name) then 94 | vim.api.nvim_buf_add_highlight(state.buf_id, M.navigation_ns_id, "Special", i - 1, 0, -1) 95 | end 96 | end 97 | end 98 | 99 | ---@param state Popup 100 | function M.theme_help_content(state) 101 | local function add_hl(hl_group, i, col_start, col_end) 102 | vim.api.nvim_buf_add_highlight( 103 | state.buf_id, M.help_ns_id, hl_group, i - 1, col_start, col_end 104 | ) 105 | end 106 | 107 | local function hl_comment(i, line) 108 | local trim = fm_globals.trim(line) 109 | if string.sub(trim, 1, 2) == "--" then 110 | add_hl('Title', i, 0, -1) 111 | end 112 | end 113 | 114 | local function hl_keymap(i, line) 115 | local start_column = line:find('%[') 116 | local end_column = line:find('%]') 117 | 118 | if start_column ~= nil and end_column ~= nil then 119 | add_hl('SpecialChar', i, start_column, end_column - 1) 120 | end 121 | end 122 | 123 | local function hl_slash(line_idx, line) 124 | local columns = {} 125 | 126 | local slash_byte = string.byte("/") 127 | 128 | for i = 1, line:len(), 1 do 129 | local char_byte = line:byte(i) 130 | 131 | if slash_byte == char_byte then 132 | table.insert(columns, i) 133 | end 134 | end 135 | 136 | for _, column in ipairs(columns) do 137 | add_hl('@conditional', line_idx, column - 1, column) 138 | end 139 | end 140 | 141 | for i, line in ipairs(state.buf_content) do 142 | hl_comment(i, line) 143 | hl_keymap(i, line) 144 | hl_slash(i, line) 145 | end 146 | end 147 | 148 | return M 149 | -------------------------------------------------------------------------------- /lua/nvim-traveller/navigation.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | local fm_theming = require("nvim-traveller.fm-theming") 3 | local fm_popup = require("nvim-traveller.fm-popup") 4 | local fm_telescope = require("nvim-traveller.plugin-telescope") 5 | local fm_shell = require("nvim-traveller.fm-shell") 6 | 7 | local path = require("plenary.path") 8 | local Location = require("nvim-traveller.fm-location") 9 | 10 | local log = require("plenary.log"):new() 11 | log.level = "debug"; 12 | 13 | local item_cmd = { 14 | open = 'e', 15 | openTab = 'tabe', 16 | vSplit = 'vsplit', 17 | hSplit = 'split', 18 | } 19 | 20 | ---@type ModOptions 21 | local mod_options 22 | 23 | ---@class NavigationState 24 | ---@field win_id number 25 | ---@field buf_id number 26 | ---@field dir_path string 27 | ---@field show_hidden boolean 28 | ---@field is_initialized boolean 29 | ---@field history Location[] 30 | ---@field selection Location[] 31 | ---@field buf_content table 32 | local NavigationState = { 33 | is_initialized = false 34 | } 35 | 36 | ---Create new navigation state 37 | ---@param options any 38 | ---@return NavigationState 39 | function NavigationState:new(options) 40 | local o = {} 41 | setmetatable(o, self) 42 | self.__index = self 43 | 44 | o:init(options) 45 | 46 | return o 47 | end 48 | 49 | ---@param opts ModOptions 50 | function NavigationState:set_mod_options(opts) 51 | mod_options = opts 52 | end 53 | 54 | function NavigationState:init(options) 55 | local function get_dir_path() 56 | local fd = vim.fn.expand('%:p:h') 57 | if vim.fn.isdirectory(fd) == 1 then 58 | return fd .. "/" 59 | else 60 | return vim.fn.expand('$HOME') .. "/" 61 | end 62 | end 63 | 64 | options = options or {} 65 | 66 | self.dir_path = options.dir_path or get_dir_path() 67 | self.win_id = vim.api.nvim_get_current_win() 68 | self.buf_id = vim.api.nvim_create_buf(false, true) 69 | self.show_hidden = self.show_hidden or mod_options.show_hidden 70 | self.is_initialized = true 71 | self.history = {} 72 | self.selection = options.selection or {} 73 | self.buf_content = {} 74 | 75 | vim.bo[self.buf_id].bufhidden = "wipe" 76 | 77 | vim.api.nvim_create_autocmd({ "BufHidden" }, { 78 | buffer = self.buf_id, 79 | callback = function() 80 | self:close_status_popup() 81 | fm_theming.remove_navigation(self) 82 | end 83 | }) 84 | end 85 | 86 | function NavigationState:toggle_hidden() 87 | self.show_hidden = not self.show_hidden 88 | self:reload_buffer() 89 | end 90 | 91 | function NavigationState:get_current_location() 92 | local item_name = self:get_cursor_item() 93 | return Location:new(self.dir_path, item_name) 94 | end 95 | 96 | function NavigationState:get_relative_path() 97 | local rel = path:new(self.dir_path):make_relative() 98 | 99 | if fm_globals.is_item_directory(rel) then 100 | return rel 101 | else 102 | return rel .. "/" 103 | end 104 | end 105 | 106 | function NavigationState:get_absolute_path() 107 | local abs = path:new(self.dir_path):absolute() 108 | 109 | if fm_globals.is_item_directory(abs) then 110 | return abs 111 | else 112 | return abs .. "/" 113 | end 114 | end 115 | 116 | function NavigationState:paste_selection(copy) 117 | local sh_cmds 118 | if copy then 119 | sh_cmds = fm_shell.create_cp_cmds_selection(self) 120 | else 121 | sh_cmds = fm_shell.create_mv_cmds_selection(self) 122 | end 123 | 124 | fm_globals.debug(sh_cmds) 125 | local errors = {} 126 | 127 | for _, cmd in pairs(sh_cmds) do 128 | local output = vim.fn.systemlist(cmd) 129 | if #output ~= 0 then 130 | fm_globals.concat_table(errors, output) 131 | end 132 | end 133 | 134 | self:undo_selection() 135 | self:reload_buffer() 136 | 137 | if #errors ~= 0 then 138 | fm_globals.debug(errors) 139 | fm_popup.create_info_popup(errors, self.win_id, 'Command failed (Esc / q)') 140 | end 141 | end 142 | 143 | function NavigationState:close_status_popup() 144 | if self.status_popup then 145 | self.status_popup:close() 146 | self.status_popup = nil; 147 | end 148 | end 149 | 150 | function NavigationState:init_status_popup() 151 | local has_selection = #self.selection ~= 0 152 | 153 | if self.status_popup then 154 | if has_selection then 155 | self.status_popup:update_status_text(self) 156 | else 157 | self:close_status_popup() 158 | end 159 | elseif has_selection then 160 | self.status_popup = fm_popup.create_selection_popup(self) 161 | end 162 | end 163 | 164 | function NavigationState:add_to_selection() 165 | local event = self:get_current_location() 166 | local selection_index = self:get_selection_index(event.item_name) 167 | 168 | if selection_index == -1 then 169 | table.insert(self.selection, event) 170 | else 171 | table.remove(self.selection, selection_index) 172 | end 173 | 174 | fm_theming.theme_buffer_content(self) 175 | self:init_status_popup() 176 | end 177 | 178 | function NavigationState:undo_selection() 179 | self.selection = {} 180 | fm_theming.theme_buffer_content(self) 181 | self:close_status_popup() 182 | end 183 | 184 | ---Checks if in selection 185 | ---@param item_name string 186 | ---@return boolean 187 | function NavigationState:is_selected(item_name) 188 | return self:get_selection_index(item_name) ~= -1 189 | end 190 | 191 | ---Checks if in selection 192 | ---@param item_name string 193 | ---@return number 194 | function NavigationState:get_selection_index(item_name) 195 | for i, location in ipairs(self.selection) do 196 | if location.dir_path == self.dir_path and location.item_name == item_name then 197 | return i 198 | end 199 | end 200 | return -1 201 | end 202 | 203 | ---@return string 204 | function NavigationState:get_cursor_item() 205 | local cursor = vim.api.nvim_win_get_cursor(0) 206 | return self.buf_content[cursor[1]] 207 | end 208 | 209 | function NavigationState:set_buffer_content(new_dir_path) 210 | --assert(fm_globals.is_item_directory(new_dir_path), "Passed path is not a directory") 211 | 212 | local function get_buffer_content() 213 | local function get_cmd() 214 | if self.show_hidden then 215 | return "ls -pAL" 216 | else 217 | return "ls -pL" 218 | end 219 | end 220 | 221 | vim.api.nvim_set_current_dir(new_dir_path) 222 | 223 | return vim.fn.systemlist(get_cmd()) 224 | end 225 | 226 | self.buf_content = get_buffer_content() 227 | self.dir_path = vim.fn.getcwd() .. "/" 228 | 229 | vim.api.nvim_buf_set_option(self.buf_id, 'modifiable', true) 230 | vim.api.nvim_buf_set_lines(self.buf_id, 0, -1, true, self.buf_content) 231 | vim.api.nvim_buf_set_option(self.buf_id, 'modifiable', false) 232 | 233 | fm_theming.theme_buffer_content(self) 234 | 235 | local function set_window_cursor() 236 | for i, buf_item in ipairs(self.buf_content) do 237 | for _, event in ipairs(self.history) do 238 | if self.dir_path == event.dir_path and buf_item == event.item_name then 239 | vim.api.nvim_win_set_cursor(self.win_id, { i, 0 }) 240 | return 241 | end 242 | end 243 | end 244 | vim.api.nvim_win_set_cursor(self.win_id, { 1, 0 }) 245 | end 246 | 247 | set_window_cursor() 248 | 249 | vim.cmd("nohlsearch") 250 | end 251 | 252 | function NavigationState:reload_buffer() 253 | self:set_buffer_content(self.dir_path) 254 | end 255 | 256 | ---@param self NavigationState 257 | ---@param dir_path string 258 | function NavigationState:reload_navigation(dir_path) 259 | if not fm_globals.is_item_directory(dir_path) then 260 | dir_path = dir_path .. "/" 261 | end 262 | 263 | self:init({ dir_path = dir_path, selection = self.selection }) 264 | self:open_navigation() 265 | end 266 | 267 | function NavigationState:navigate_to_parent() 268 | if self.dir_path == "/" then 269 | return 270 | end 271 | 272 | local function get_parent_location() 273 | local parts = fm_globals.split(self.dir_path, "/") 274 | local item_name = table.remove(parts, #parts) 275 | 276 | local dir_path = "/" 277 | for _, value in ipairs(parts) do 278 | dir_path = dir_path .. value .. "/" 279 | end 280 | 281 | return Location:new(dir_path, item_name .. "/") 282 | end 283 | 284 | local function get_history_index(cmp_path) 285 | for i, event in ipairs(self.history) do 286 | if event.dir_path == cmp_path then 287 | return i 288 | end 289 | end 290 | return -1 291 | end 292 | 293 | local function update_history_location(event) 294 | local his_index = get_history_index(event.dir_path) 295 | 296 | if his_index == -1 then 297 | table.insert(self.history, event) 298 | else 299 | self.history[his_index].item_name = event.item_name 300 | end 301 | end 302 | 303 | local current_location = self:get_current_location() 304 | local parent_location = get_parent_location() 305 | 306 | update_history_location(current_location) 307 | update_history_location(parent_location) 308 | 309 | self:set_buffer_content("..") 310 | end 311 | 312 | function NavigationState:open_navigation() 313 | -- Needs to happen here before new buffer gets loaded 314 | local fn = vim.fn.expand('%:t') 315 | 316 | vim.api.nvim_set_current_buf(self.buf_id) 317 | vim.cmd("file Traveller") 318 | 319 | fm_theming.add_navigation_theming(self) 320 | self:init_status_popup() 321 | 322 | local buffer_options = { silent = true, buffer = self.buf_id } 323 | 324 | local function action_on_item(cmd_str) 325 | local item = self:get_cursor_item() 326 | 327 | if item == nil then 328 | return 329 | end 330 | 331 | local abs_path = path:new(self.dir_path .. item):absolute() 332 | 333 | if fm_globals.is_item_directory(item) then 334 | if cmd_str == item_cmd.open then 335 | self:set_buffer_content(item) 336 | end 337 | elseif fm_shell.is_file_binary(abs_path) and false then 338 | vim.fn.jobstart("open " .. abs_path, { detach = true }) 339 | else 340 | local file_rel = path:new(abs_path):make_relative() 341 | vim.cmd(cmd_str .. "./" .. item) 342 | fm_globals.set_cwd_to_git_root(); 343 | end 344 | end 345 | 346 | ---@param popup Popup 347 | ---@param sh_cmd string 348 | local function confirm_callback(popup, sh_cmd) 349 | local output = vim.fn.systemlist(sh_cmd .. fm_globals.only_stderr) 350 | self:reload_buffer() 351 | popup:close() 352 | 353 | if #output ~= 0 then 354 | fm_globals.debug(output) 355 | fm_popup.create_info_popup(output, self.win_id, 'Command failed (Esc / q)') 356 | end 357 | end 358 | 359 | local function create_items_popup() 360 | local popup = fm_popup.create_items_popup() 361 | 362 | local function confirm_mkdir_callback() 363 | confirm_callback(popup, popup:create_new_items_cmd(self.dir_path)) 364 | end 365 | 366 | popup:set_keymap('i', '', confirm_mkdir_callback) 367 | end 368 | 369 | local function create_move_popup() 370 | local current_location = self:get_current_location() 371 | local popup = fm_popup.create_move_popup(current_location) 372 | 373 | local function confirm_move_callback() 374 | -- Tries git mv first, if fails fallsback to mv. 375 | local sh_cmd = popup:create_mv_cmd(current_location, "git mv") 376 | local output = vim.fn.systemlist(sh_cmd .. fm_globals.only_stderr) 377 | 378 | if #output ~= 0 then 379 | fm_globals.debug(output) 380 | confirm_callback(popup, popup:create_mv_cmd(current_location, "mv")) 381 | else 382 | self:reload_buffer() 383 | popup:close() 384 | end 385 | end 386 | 387 | popup:set_keymap('i', '', confirm_move_callback) 388 | popup:set_keymap('n', '', confirm_move_callback) 389 | end 390 | 391 | local function delete_item() 392 | local sh_cmds = fm_shell.create_rm_cmds(self) 393 | local popup = fm_popup.create_delete_item_popup(self.win_id, sh_cmds) 394 | 395 | local function confirm_delete_callback() 396 | local errors = {} 397 | 398 | for _, sh_cmd in pairs(sh_cmds) do 399 | local output = vim.fn.systemlist(sh_cmd .. fm_globals.only_stderr) 400 | 401 | if #output ~= 0 then 402 | fm_globals.concat_table(errors, output) 403 | end 404 | end 405 | 406 | self:undo_selection() 407 | self:reload_buffer() 408 | popup:close() 409 | 410 | if #errors ~= 0 then 411 | fm_globals.debug(errors) 412 | fm_popup.create_info_popup(errors, self.win_id, 'Command failed (Esc / q)') 413 | end 414 | end 415 | 416 | popup:set_keymap('n', '', confirm_delete_callback) 417 | end 418 | 419 | local function navigate_to_root_directory() 420 | self:set_buffer_content("/") 421 | end 422 | 423 | local function navigate_to_home_directory() 424 | self:set_buffer_content(vim.fn.expand('$HOME') .. "/") 425 | end 426 | 427 | local function close() 428 | fm_globals.set_cwd_to_git_root(); 429 | vim.cmd("silent! e #") 430 | end 431 | 432 | vim.keymap.set('n', 'q', close, buffer_options) 433 | vim.keymap.set('n', '', close, buffer_options) 434 | vim.keymap.set('n', '', function() action_on_item(item_cmd.open) end, buffer_options) 435 | vim.keymap.set('n', 'l', function() action_on_item(item_cmd.open) end, buffer_options) 436 | vim.keymap.set('n', '', function() action_on_item(item_cmd.open) end, buffer_options) 437 | vim.keymap.set('n', 'v', function() action_on_item(item_cmd.vSplit) end, buffer_options) 438 | vim.keymap.set('n', 's', function() action_on_item(item_cmd.hSplit) end, buffer_options) 439 | vim.keymap.set('n', 't', function() action_on_item(item_cmd.openTab) end, buffer_options) 440 | 441 | vim.keymap.set('n', 'ot', function() 442 | fm_shell.open_terminal() 443 | end, buffer_options) 444 | 445 | vim.keymap.set('n', 'os', function() 446 | fm_shell.open_shell(self:get_relative_path()) 447 | end, buffer_options) 448 | 449 | vim.keymap.set('n', 'c', create_items_popup, buffer_options) 450 | vim.keymap.set('n', 'm', create_move_popup, buffer_options) 451 | vim.keymap.set('n', 'dd', delete_item, buffer_options) 452 | vim.keymap.set('n', '', "", buffer_options) 453 | vim.keymap.set('n', '', function() self:navigate_to_parent() end, buffer_options) 454 | vim.keymap.set('n', 'h', function() self:navigate_to_parent() end, buffer_options) 455 | vim.keymap.set('n', '?', function() fm_popup.create_help_popup() end, buffer_options) 456 | vim.keymap.set('n', '.', function() self:toggle_hidden() end, buffer_options) 457 | vim.keymap.set('n', 'y', function() self:add_to_selection() end, buffer_options) 458 | vim.keymap.set('n', 'u', function() self:undo_selection() end, buffer_options) 459 | vim.keymap.set('n', 'pm', function() self:paste_selection(false) end, buffer_options) 460 | vim.keymap.set('n', 'pc', function() self:paste_selection(true) end, buffer_options) 461 | vim.keymap.set('n', 'gh', navigate_to_home_directory, buffer_options) 462 | vim.keymap.set('n', 'g/', navigate_to_root_directory, buffer_options) 463 | 464 | 465 | -- Plugin integration 466 | vim.keymap.set('n', 'f', function() fm_telescope:find_files(self) end, buffer_options) 467 | vim.keymap.set('n', 'a', function() fm_telescope:live_grep(self) end, buffer_options) 468 | 469 | if fn ~= "" then 470 | table.insert(self.history, Location:new(self.dir_path, fn)) 471 | end 472 | 473 | self:reload_buffer() 474 | end 475 | 476 | return NavigationState 477 | -------------------------------------------------------------------------------- /lua/nvim-traveller/persist-data.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | 3 | local M = {} 4 | 5 | local data_path = vim.fn.stdpath('data') .. '/nvim-traveller.json' 6 | 7 | local function retrieve_data() 8 | fm_globals.debug("history test"); 9 | if vim.fn.filereadable(data_path) == 0 then 10 | local filewrite = io.open(data_path, "w") 11 | 12 | if filewrite == nil then 13 | fm_globals.debug("Can't write data") 14 | return {} 15 | end 16 | 17 | filewrite:write("[]") -- empty JSON array 18 | filewrite:close() 19 | return {} 20 | end 21 | 22 | local file_output = vim.fn.readfile(data_path) 23 | fm_globals.debug(file_output, "history"); 24 | local json_str = "" 25 | 26 | for _, item in pairs(file_output) do 27 | json_str = json_str .. item 28 | end 29 | 30 | return vim.fn.json_decode(json_str) 31 | end 32 | 33 | local history = retrieve_data() 34 | 35 | local function update_history(dir_path) 36 | local function compare(a, b) 37 | return b.last_used < a.last_used 38 | end 39 | 40 | for _, item in pairs(history) do 41 | if item.dir_path == dir_path then 42 | item.last_used = os.time() 43 | table.sort(history, compare) 44 | return item 45 | end 46 | end 47 | 48 | table.insert(history, { 49 | dir_path = dir_path, 50 | last_used = os.time() 51 | }) 52 | 53 | table.sort(history, compare) 54 | 55 | while 15 < #history do 56 | table.remove(history, #history) 57 | end 58 | end 59 | 60 | local function persist() 61 | local json = vim.fn.json_encode(history) 62 | local filewrite = io.open(data_path, "w+") 63 | 64 | if filewrite == nil then 65 | fm_globals.debug(data_path, "Can't open data file") 66 | return 67 | end 68 | 69 | filewrite:write(json) 70 | filewrite:close() 71 | end 72 | 73 | function M.store_data(dir_path) 74 | update_history(dir_path) 75 | persist() 76 | end 77 | 78 | function M.remove(dir_path) 79 | local function get_index() 80 | for i, item in ipairs(history) do 81 | if item.dir_path == dir_path then 82 | return i 83 | end 84 | end 85 | end 86 | 87 | table.remove(history, get_index()) 88 | 89 | persist() 90 | return M.last_used_dirs() 91 | end 92 | 93 | function M.last_used_dirs() 94 | local dirs = {} 95 | 96 | for _, item in pairs(history) do 97 | table.insert(dirs, item.dir_path) 98 | end 99 | 100 | return dirs 101 | end 102 | 103 | return M 104 | -------------------------------------------------------------------------------- /lua/nvim-traveller/plugin-telescope.lua: -------------------------------------------------------------------------------- 1 | local fm_globals = require("nvim-traveller.fm-globals") 2 | local Job = require('plenary.job') 3 | local persist = require("nvim-traveller.persist-data") 4 | local mod_options = {} 5 | 6 | local M = {} 7 | 8 | if package.loaded["telescope"] then 9 | M.builtin = require("telescope.builtin") 10 | M.pickers = require("telescope.pickers") 11 | M.finders = require("telescope.finders") 12 | M.config = require("telescope.config").values 13 | M.actions = require("telescope.actions") 14 | M.action_state = require("telescope.actions.state") 15 | M.themes = require("telescope.themes") 16 | end 17 | 18 | ---@param state NavigationState 19 | function M:find_files(state) 20 | if self.builtin == nil then 21 | return 22 | end 23 | 24 | M.builtin.find_files({ cwd = state.dir_path }) 25 | end 26 | 27 | function M.set_mod_options(options) 28 | mod_options = options 29 | end 30 | 31 | ---@param state NavigationState 32 | function M:live_grep(state) 33 | if self.builtin == nil then 34 | return 35 | end 36 | 37 | self.builtin.live_grep({ cwd = state.dir_path }) 38 | end 39 | 40 | ---@param state NavigationState 41 | function M:directories_search(state, show_last_used) 42 | local search_dir = fm_globals.get_home_directory() 43 | local last_used_dirs = persist.last_used_dirs() 44 | fm_globals.debug(last_used_dirs, "test"); 45 | 46 | if #last_used_dirs == 0 then 47 | show_last_used = false 48 | end 49 | 50 | local all_dirs = {} 51 | 52 | local function get_results() 53 | if show_last_used then 54 | return last_used_dirs 55 | else 56 | return all_dirs 57 | end 58 | end 59 | 60 | local function attach_mappings(_, map) 61 | local actions = self.actions 62 | local action_state = self.action_state 63 | local mappings = mod_options.mappings or {} 64 | 65 | map('i', mappings.directories_tab or "", function() 66 | show_last_used = not show_last_used 67 | 68 | self.picker:refresh(self.finders.new_table({ 69 | results = get_results() 70 | })) 71 | end) 72 | 73 | map('i', mod_options.directories_delete or "", function() 74 | local selected_dir = action_state.get_selected_entry()[1] 75 | 76 | if selected_dir == nil or not show_last_used then 77 | return 78 | end 79 | 80 | last_used_dirs = persist.remove(selected_dir) 81 | 82 | self.picker:refresh(self.finders.new_table({ 83 | results = last_used_dirs 84 | })) 85 | end) 86 | 87 | local function execute_item(opts, callback) 88 | local selected_entry = action_state.get_selected_entry() 89 | 90 | if #selected_entry == 0 then 91 | return 92 | end 93 | 94 | actions.close(opts) 95 | 96 | callback() 97 | 98 | local selected_item = selected_entry[1] 99 | local dir_path = search_dir .. selected_item 100 | state:reload_navigation(dir_path) 101 | persist.store_data(selected_item) 102 | 103 | if #state.selection == 0 and #state.buf_content ~= 0 then 104 | self:find_files(state) 105 | end 106 | end 107 | 108 | actions.toggle_selection:replace(function() end) 109 | actions.select_all:replace(function() end) 110 | 111 | actions.select_tab:replace(function(opts) 112 | execute_item(opts, function() 113 | vim.cmd("tabnew") 114 | vim.bo.bufhidden = "hide" 115 | vim.bo.buflisted = false 116 | end) 117 | end) 118 | 119 | actions.select_vertical:replace(function(opts) 120 | execute_item(opts, function() 121 | vim.cmd("vsplit") 122 | end) 123 | end) 124 | 125 | actions.select_horizontal:replace(function(opts) 126 | execute_item(opts, function() 127 | vim.cmd("split") 128 | end) 129 | end) 130 | 131 | actions.select_default:replace(function(opts) 132 | execute_item(opts, function() end) 133 | end) 134 | 135 | return true 136 | end 137 | 138 | local opts = M.themes.get_dropdown({ 139 | attach_mappings = attach_mappings, 140 | }) 141 | 142 | Job:new({ 143 | command = 'fd', 144 | args = { "--type", "directory", "--base-directory", search_dir }, 145 | cwd = search_dir, 146 | on_stdout = function(_, line) 147 | table.insert(all_dirs, line) 148 | end, 149 | }):sync() 150 | 151 | Job:new({ 152 | command = 'fd', 153 | args = { "--type", "directory", "--base-directory", search_dir, ".", ".config" }, 154 | cwd = search_dir, 155 | on_stdout = function(_, line) 156 | table.insert(all_dirs, line) 157 | end, 158 | }):sync() 159 | 160 | self.picker = self.pickers.new(opts, { 161 | prompt_title = "Directories (Tab)", 162 | finder = self.finders.new_table({ 163 | results = get_results() 164 | }), 165 | sorter = self.config.file_sorter(opts), 166 | }) 167 | 168 | self.picker:find() 169 | end 170 | 171 | return M 172 | --------------------------------------------------------------------------------