├── .gitignore ├── README.md ├── assets ├── buffers.jpg ├── statusline.jpg └── tabs.jpg ├── buffalo-nvim.png ├── lua └── buffalo │ ├── api.lua │ ├── dev.lua │ ├── init.lua │ ├── ui.lua │ └── utils.lua └── plugin ├── buffalo.lua └── buffalo.vim /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pheon-Dev/buffalo-nvim/86cc767848fe747c28b226af0d35049fd5faf288/.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Linux](https://img.shields.io/badge/Linux-%23.svg?logo=linux&color=FCC624&logoColor=black) 2 | ![macOS](https://img.shields.io/badge/macOS-%23.svg?logo=apple&color=000000&logoColor=white) 3 | ![Windows](https://img.shields.io/badge/Windows-%23.svg?logo=windows&color=0078D6&logoColor=white) 4 | 5 |

6 | buffalo-nvim 7 |

8 | 9 |

10 | buffalo-nvim 11 |

12 | 13 | This is a [harpoon](https://github.com/ThePrimeagen/harpoon) like plugin that provides an interface 14 | to navigate through buffers or tabs. 15 | 16 | > Their respective totals can be displayed on the statusline, tabline or winbar. 17 | 18 |
19 |

20 | NOTE: 21 |

22 |
Please note that this plugin is still in its early development stages. Breaking changes are to be expected!
23 | 24 |
25 |
26 | BUFFERS 27 |
28 |

29 | buffalo-buffers 30 |

31 | 32 |
33 |
34 | TABS 35 |
36 |

37 | buffalo-tabs 38 |

39 | 40 |
41 |
42 | STATUSLINE 43 |
44 | 45 |

46 | buffalo-statusline 47 |

48 |

49 | tabs: 4 | buffers: 7 [lualine] 50 |

51 | 52 | ## Installation 53 | 54 | Using [Lazy](https://github.com/folke/lazy.nvim) 55 | 56 | ```lua 57 | { 58 | 'Pheon-Dev/buffalo-nvim' 59 | } 60 | ``` 61 | 62 | Using [packer.nvim](https://github.com/wbthomason/packer.nvim) 63 | 64 | ```lua 65 | use 'Pheon-Dev/buffalo-nvim' 66 | ``` 67 | 68 | Using [vim-plug](https://github.com/junegunn/vim-plug) 69 | 70 | ```vim 71 | Plug 'Pheon-Dev/buffalo-nvim' 72 | ``` 73 | 74 | ## Setup 75 | 76 | ```lua 77 | -- default config 78 | require('buffalo').setup({}) 79 | ``` 80 | 81 | ## Usage 82 | 83 | ```lua 84 | -- Keymaps 85 | local opts = { noremap = true } 86 | local map = vim.keymap.set 87 | local buffalo = require("buffalo.ui") 88 | 89 | -- buffers 90 | map({ 't', 'n' }, '', buffalo.toggle_buf_menu, opts) 91 | 92 | map('n', '', buffalo.nav_buf_next, opts) 93 | map('n', '', buffalo.nav_buf_prev, opts) 94 | 95 | -- tabpages 96 | map({ 't', 'n' }, '', buffalo.toggle_tab_menu, opts) 97 | 98 | map('n', '', buffalo.nav_tab_next, opts) 99 | map('n', '', buffalo.nav_tab_prev, opts) 100 | 101 | -- Example in lualine 102 | ... 103 | sections = { 104 | ... 105 | lualine_x = { 106 | { 107 | function() 108 | local buffers = require("buffalo").buffers() 109 | local tabpages = require("buffalo").tabpages() 110 | return "󱂬 " .. buffers .. " 󰓩 " .. tabpages 111 | end, 112 | color = { fg = "#ffaa00", bg = "#24273a",}, 113 | } 114 | }, 115 | ... 116 | }, 117 | ... 118 | ``` 119 | 120 | --- 121 | 122 | ## Config 123 | 124 | ```lua 125 | require("buffalo").setup({ 126 | tab_commands = { -- use default neovim commands for tabs e.g `tablast`, `tabnew` etc 127 | next = { -- you can use any unique name e.g `tabnext`, `tab_next`, `next`, `random` etc 128 | key = "", 129 | command = "tabnext" 130 | }, 131 | close = { 132 | key = "c", 133 | command = "tabclose" 134 | }, 135 | dd = { 136 | key = "dd", 137 | command = "tabclose" 138 | }, 139 | new = { 140 | key = "n", 141 | command = "tabnew" 142 | } 143 | }, 144 | buffer_commands = { -- use default neovim commands for buffers e.g `bd`, `edit` 145 | edit = { 146 | key = "", 147 | command = "edit" 148 | }, 149 | vsplit = { 150 | key = "v", 151 | command = "vsplit" 152 | }, 153 | split = { 154 | key = "h", 155 | command = "split" 156 | } 157 | buffer_delete = { 158 | key = "d", 159 | command = "bd" 160 | } 161 | }, 162 | general_commands = { 163 | cycle = true, -- cycle through buffers or tabs 164 | exit_menu = "x", -- similar to 'q' and '' 165 | }, 166 | go_to = { 167 | enabled = true, 168 | go_to_tab = "%s", 169 | go_to_buffer = "", 170 | }, 171 | filter = { 172 | enabled = true, 173 | filter_tabs = "", 174 | filter_buffers = "", 175 | }, 176 | ui = { 177 | width = 60, 178 | height = 10, 179 | row = 2, 180 | col = 2, 181 | borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, 182 | } 183 | }) 184 | ``` 185 | 186 | --- 187 | 188 | ## Tips 189 | 190 | - Hit any number on the menu to navigate to that buffer or tab without having to scroll. 191 | - Use normal keymap defaults for neovim e.g `dd` to delete a buffer, on the open menu. 192 | 193 | --- 194 | 195 | ## Highlights 196 | 197 | - `BuffaloBorder` 198 | - `BuffaloWindow` 199 | - `BuffaloBuffersModified` 200 | - `BuffaloBuffersCurrentLine` 201 | - `BuffaloTabsCurrentLine` 202 | 203 | --- 204 | 205 | ## Acknowledgement 206 | 207 | - ThePrimeagen's [Harpoon](https://github.com/ThePrimeagen/harpoon) 208 | - J-Morano's [Buffer Manager](https://github.com/j-morano/buffer_manager.nvim) 209 | 210 | --- 211 | 212 | ## Contributions 213 | 214 | - PRs and Issues are always welcome. 215 | -------------------------------------------------------------------------------- /assets/buffers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pheon-Dev/buffalo-nvim/86cc767848fe747c28b226af0d35049fd5faf288/assets/buffers.jpg -------------------------------------------------------------------------------- /assets/statusline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pheon-Dev/buffalo-nvim/86cc767848fe747c28b226af0d35049fd5faf288/assets/statusline.jpg -------------------------------------------------------------------------------- /assets/tabs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pheon-Dev/buffalo-nvim/86cc767848fe747c28b226af0d35049fd5faf288/assets/tabs.jpg -------------------------------------------------------------------------------- /buffalo-nvim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pheon-Dev/buffalo-nvim/86cc767848fe747c28b226af0d35049fd5faf288/buffalo-nvim.png -------------------------------------------------------------------------------- /lua/buffalo/api.lua: -------------------------------------------------------------------------------- 1 | local api = {} 2 | 3 | function api.get_tabs() 4 | return vim.api.nvim_list_tabpages() 5 | end 6 | 7 | function api.get_tab_wins(tabid) 8 | local wins = vim.api.nvim_tabpage_list_wins(tabid) 9 | return vim.tbl_filter(api.is_not_float_win, wins) 10 | end 11 | 12 | function api.get_current_tab() 13 | return vim.api.nvim_get_current_tabpage() 14 | end 15 | 16 | function api.get_tab_current_win(tabid) 17 | return vim.api.nvim_tabpage_get_win(tabid) 18 | end 19 | 20 | function api.get_tab_number(tabid) 21 | local valid = vim.api.nvim_tabpage_is_valid(tabid) 22 | if not valid then return -1 end 23 | return vim.api.nvim_tabpage_get_number(tabid) 24 | end 25 | 26 | function api.get_wins() 27 | local wins = vim.api.nvim_list_wins() 28 | return vim.tbl_filter(api.is_not_float_win, wins) 29 | end 30 | 31 | function api.get_win_tab(winid) 32 | return vim.api.nvim_win_get_tabpage(winid) 33 | end 34 | 35 | function api.is_float_win(winid) 36 | return vim.api.nvim_win_get_config(winid).relative ~= '' 37 | end 38 | 39 | function api.is_not_float_win(winid) 40 | return vim.api.nvim_win_get_config(winid).relative == '' 41 | end 42 | 43 | function api.get_win_buf(winid) 44 | return vim.api.nvim_win_get_buf(winid) 45 | end 46 | 47 | function api.get_buf_type(bufid) 48 | return vim.api.nvim_buf_get_option(bufid, 'buftype') 49 | end 50 | 51 | function api.get_buf_is_changed(bufid) 52 | return vim.fn.getbufinfo(bufid)[1].changed == 1 53 | end 54 | 55 | return api 56 | -------------------------------------------------------------------------------- /lua/buffalo/dev.lua: -------------------------------------------------------------------------------- 1 | -- Don't include this file, we should manually include it via 2 | -- require("buffalo.dev").reload(); 3 | -- 4 | -- A quick mapping can be setup using something like: 5 | -- :nmap rr :lua require("buffalo.dev").reload() 6 | local M = {} 7 | 8 | function M.reload() 9 | require("plenary.reload").reload_module("buffalo") 10 | end 11 | 12 | local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" } 13 | local function set_log_level() 14 | local log_level = vim.env.BUFFALO_LOG or vim.g.buffalo_log_level 15 | 16 | for _, level in pairs(log_levels) do 17 | if level == log_level then 18 | return log_level 19 | end 20 | end 21 | 22 | return "warn" -- default, if user hasn't set to one from log_levels 23 | end 24 | 25 | local log_level = set_log_level() 26 | M.log = require("plenary.log").new({ 27 | plugin = "buffalo", 28 | level = log_level, 29 | }) 30 | 31 | local log_key = os.time() 32 | 33 | local function override(key) 34 | local fn = M.log[key] 35 | M.log[key] = function(...) 36 | fn(log_key, ...) 37 | end 38 | end 39 | 40 | for _, v in pairs(log_levels) do 41 | override(v) 42 | end 43 | 44 | function M.get_log_key() 45 | return log_key 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /lua/buffalo/init.lua: -------------------------------------------------------------------------------- 1 | local Dev = require("buffalo.dev") 2 | local api = require("buffalo.api") 3 | local log = Dev.log 4 | local buffer_is_valid = require("buffalo.utils").buffer_is_valid 5 | local merge_tables = require("buffalo.utils").merge_tables 6 | -- 7 | local M = {} 8 | 9 | M.marks = {} 10 | M.tab_marks = {} 11 | 12 | M.Config = M.Config or {} 13 | 14 | function M.buffers() 15 | local bufs = vim.api.nvim_list_bufs() 16 | bufs = vim.tbl_filter(function(buf) 17 | local is_loaded = vim.api.nvim_buf_is_loaded(buf) 18 | local is_listed = vim.fn.buflisted(buf) == 1 19 | 20 | if not (is_loaded and is_listed) then 21 | return false 22 | end 23 | 24 | return true 25 | end, bufs) 26 | local count = 0 27 | for _, buf in pairs(bufs) do 28 | count = count + 1 29 | end 30 | return count 31 | end 32 | 33 | function M.tabpages() 34 | local tabs = api.get_tabs() 35 | local count = 0 36 | for _, tab in pairs(tabs) do 37 | count = count + 1 38 | end 39 | return count 40 | end 41 | 42 | function M.init_buffers() 43 | local buffers = vim.api.nvim_list_bufs() 44 | 45 | for idx = 1, #buffers do 46 | local buf_id = buffers[idx] 47 | local buf_name = vim.api.nvim_buf_get_name(buf_id) 48 | local filename = buf_name 49 | -- if buffer is listed, then add to contents and marks 50 | if buffer_is_valid(buf_id, buf_name) then 51 | table.insert( 52 | M.marks, 53 | { 54 | filename = filename, 55 | buf_id = buf_id, 56 | } 57 | ) 58 | end 59 | end 60 | end 61 | 62 | function M.setup(config) 63 | log.trace("setup(): Setting up...") 64 | 65 | if not config then 66 | config = {} 67 | end 68 | 69 | local default_config = { 70 | tab_commands = { 71 | edit = { 72 | key = "", 73 | command = "tabnext" 74 | } 75 | }, 76 | buffer_commands = { 77 | edit = { 78 | key = "", 79 | command = "edit" 80 | } 81 | }, 82 | general_commands = { 83 | cycle = true, 84 | exit_menu = "q", 85 | }, 86 | go_to = { 87 | enabled = true, 88 | go_to_tab = "%s", 89 | go_to_buffer = "", 90 | }, 91 | filter = { 92 | enabled = true, 93 | filter_tabs = "", 94 | filter_buffers = "", 95 | }, 96 | ui = { 97 | width = 60, 98 | height = 10, 99 | row = 2, 100 | col = 2, 101 | borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, 102 | } 103 | } 104 | 105 | local complete_config = merge_tables(default_config, config) 106 | 107 | M.Config = complete_config 108 | log.debug("setup(): Config", M.Config) 109 | end 110 | 111 | function M.get_config() 112 | log.trace("get_config()") 113 | return M.Config or {} 114 | end 115 | 116 | M.setup() 117 | 118 | M.init_buffers() 119 | 120 | return M 121 | -------------------------------------------------------------------------------- /lua/buffalo/ui.lua: -------------------------------------------------------------------------------- 1 | local Path = require("plenary.path") 2 | local buffalo = require("buffalo") 3 | local popup = require("plenary.popup") 4 | local utils = require("buffalo.utils") 5 | local log = require("buffalo.dev").log 6 | local marks = require("buffalo").marks 7 | local api = require("buffalo.api") 8 | 9 | local M = {} 10 | 11 | Buffalo_win_id = nil 12 | Buffalo_bufh = nil 13 | Buffalo_Tabs_win_id = nil 14 | Buffalo_Tabs_bufh = nil 15 | 16 | local initial_marks = {} 17 | local config = buffalo.get_config() 18 | 19 | local function close_menu(force_save) 20 | force_save = force_save or false 21 | 22 | vim.api.nvim_win_close(Buffalo_win_id, true) 23 | 24 | Buffalo_win_id = nil 25 | Buffalo_bufh = nil 26 | end 27 | 28 | local function close_tabs_menu(force_save) 29 | force_save = force_save or false 30 | 31 | vim.api.nvim_win_close(Buffalo_Tabs_win_id, true) 32 | 33 | Buffalo_Tabs_win_id = nil 34 | Buffalo_Tabs_bufh = nil 35 | end 36 | local opts = { noremap = true } 37 | local map = vim.keymap.set 38 | 39 | local function create_window(title) 40 | log.trace("_create_window()") 41 | 42 | local width = config.ui.width or 60 43 | local height = config.ui.height or 10 44 | local row = config.ui.row or 2 45 | local col = config.ui.col or 2 46 | 47 | local borderchars = config.ui.borderchars or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 48 | local bufnr = vim.api.nvim_create_buf(false, false) 49 | 50 | local Buffalo_win_id, win = popup.create(bufnr, { 51 | title = "Buffalo [" .. title .. "]", 52 | highlight = "BuffaloWindow", 53 | titlehighlight = "BuffaloTitle", 54 | line = math.floor(((vim.o.lines - height) / row) - 1), 55 | col = math.floor((vim.o.columns - width) / col), 56 | minwidth = width, 57 | minheight = height, 58 | borderchars = borderchars, 59 | }) 60 | 61 | vim.api.nvim_win_set_option( 62 | win.border.win_id, 63 | "winhl", 64 | "Normal:BuffaloBorder" 65 | ) 66 | 67 | return { 68 | bufnr = bufnr, 69 | win_id = Buffalo_win_id, 70 | } 71 | end 72 | 73 | 74 | local function string_starts(string, start) 75 | return string.sub(string, 1, string.len(start)) == start 76 | end 77 | 78 | local function can_be_deleted(bufname, bufnr) 79 | return ( 80 | vim.api.nvim_buf_is_valid(bufnr) 81 | and (not string_starts(bufname, "term://")) 82 | and (not vim.bo[bufnr].modified) 83 | and bufnr ~= -1 84 | ) 85 | end 86 | 87 | local function is_buffer_in_marks(bufnr) 88 | for _, mark in pairs(marks) do 89 | if mark.buf_id == bufnr then 90 | return true 91 | end 92 | end 93 | return false 94 | end 95 | 96 | local function get_mark_by_name(name, specific_marks) 97 | local ref_name = nil 98 | for _, mark in pairs(specific_marks) do 99 | ref_name = mark.filename 100 | if string_starts(mark.filename, "term://") then 101 | ref_name = utils.get_short_term_name(mark.filename) 102 | else 103 | ref_name = utils.normalize_path(mark.filename) 104 | end 105 | if name == ref_name then 106 | return mark 107 | end 108 | end 109 | return nil 110 | end 111 | 112 | local function update_buffers() 113 | for _, mark in pairs(initial_marks) do 114 | if not is_buffer_in_marks(mark.buf_id) then 115 | if can_be_deleted(mark.filename, mark.buf_id) then 116 | vim.api.nvim_buf_clear_namespace(mark.buf_id, -1, 1, -1) 117 | vim.api.nvim_buf_delete(mark.buf_id, {}) 118 | end 119 | end 120 | end 121 | 122 | for idx, mark in pairs(marks) do 123 | local bufnr = vim.fn.bufnr(mark.filename) 124 | if bufnr == -1 then 125 | vim.cmd("badd " .. mark.filename) 126 | marks[idx].buf_id = vim.fn.bufnr(mark.filename) 127 | end 128 | end 129 | end 130 | 131 | local function remove_mark(idx) 132 | marks[idx] = nil 133 | if idx < #marks then 134 | for i = idx, #marks do 135 | marks[i] = marks[i + 1] 136 | end 137 | end 138 | end 139 | 140 | local function update_marks() 141 | for idx, mark in pairs(marks) do 142 | if not utils.buffer_is_valid(mark.buf_id, mark.filename) then 143 | remove_mark(idx) 144 | end 145 | end 146 | for _, buf in pairs(vim.api.nvim_list_bufs()) do 147 | local bufname = vim.api.nvim_buf_get_name(buf) 148 | if utils.buffer_is_valid(buf, bufname) and not is_buffer_in_marks(buf) then 149 | table.insert(marks, { 150 | filename = bufname, 151 | buf_id = buf, 152 | }) 153 | end 154 | end 155 | end 156 | 157 | function M.toggle_buf_menu() 158 | log.trace("toggle_buf_menu()") 159 | if Buffalo_win_id ~= nil and vim.api.nvim_win_is_valid(Buffalo_win_id) then 160 | if vim.api.nvim_buf_get_changedtick(vim.fn.bufnr()) > 0 then 161 | M.on_menu_save() 162 | end 163 | close_menu(true) 164 | update_buffers() 165 | return 166 | end 167 | local current_buf_id = -1 168 | current_buf_id = vim.fn.bufnr() 169 | 170 | local win_info = create_window("buffers") 171 | local contents = {} 172 | initial_marks = {} 173 | 174 | Buffalo_win_id = win_info.win_id 175 | Buffalo_bufh = win_info.bufnr 176 | 177 | update_marks() 178 | 179 | local current_buf_line = 1 180 | local line = 1 181 | local modified_lines = {} 182 | for idx, mark in pairs(marks) do 183 | if vim.fn.buflisted(mark.buf_id) ~= 1 then 184 | marks[idx] = nil 185 | else 186 | local current_mark = marks[idx] 187 | initial_marks[idx] = { 188 | filename = current_mark.filename, 189 | buf_id = current_mark.buf_id, 190 | } 191 | if vim.bo[current_mark.buf_id].modified then 192 | table.insert(modified_lines, line) 193 | end 194 | if current_mark.buf_id == current_buf_id then 195 | current_buf_line = line 196 | end 197 | local display_filename = current_mark.filename 198 | display_filename = utils.normalize_path(display_filename) 199 | contents[line] = string.format("%s", display_filename) 200 | line = line + 1 201 | end 202 | end 203 | 204 | vim.api.nvim_set_option_value("number", true, { win = Buffalo_win_id }) 205 | vim.api.nvim_buf_set_name(Buffalo_bufh, "buffalo-buffers") 206 | vim.api.nvim_buf_set_lines(Buffalo_bufh, 0, #contents, false, contents) 207 | vim.api.nvim_buf_set_option(Buffalo_bufh, "filetype", "buffalo") 208 | vim.api.nvim_buf_set_option(Buffalo_bufh, "buftype", "acwrite") 209 | vim.api.nvim_buf_set_option(Buffalo_bufh, "bufhidden", "delete") 210 | vim.cmd(string.format(":call cursor(%d, %d)", current_buf_line, 1)) 211 | vim.api.nvim_buf_set_keymap( 212 | Buffalo_bufh, 213 | "n", 214 | "q", 215 | "lua require('buffalo.ui').toggle_buf_menu()", 216 | { silent = true } 217 | ) 218 | vim.api.nvim_buf_set_keymap( 219 | Buffalo_bufh, 220 | "n", 221 | config.general_commands.exit_menu, 222 | "lua require('buffalo.ui').toggle_buf_menu()", 223 | { silent = true } 224 | ) 225 | vim.api.nvim_buf_set_keymap( 226 | Buffalo_bufh, 227 | "n", 228 | "", 229 | "lua require('buffalo.ui').toggle_buf_menu()", 230 | { silent = true } 231 | ) 232 | for _, value in pairs(config.buffer_commands) do 233 | if type(value.command) == "string" then 234 | vim.api.nvim_buf_set_keymap( 235 | Buffalo_bufh, 236 | "n", 237 | value.key, 238 | "lua require('buffalo.ui').select_menu_item('" .. value.command .. "')", 239 | {} 240 | ) 241 | end 242 | if type(value.command) == "function" then 243 | vim.keymap.set( 244 | "n", 245 | value.key, 246 | value.command, 247 | {buffer = Buffalo_bufh } 248 | ) 249 | end 250 | end 251 | vim.cmd( 252 | string.format( 253 | "autocmd BufModifiedSet set nomodified", 254 | Buffalo_bufh 255 | ) 256 | ) 257 | vim.cmd( 258 | "autocmd BufLeave ++nested ++once silent" .. 259 | " lua require('buffalo.ui').toggle_buf_menu()" 260 | ) 261 | vim.cmd( 262 | string.format( 263 | "autocmd BufWriteCmd " .. 264 | " lua require('buffalo.ui').on_menu_save()", 265 | Buffalo_bufh 266 | ) 267 | ) 268 | local str = "1234567890" 269 | 270 | for i = 1, #str do 271 | local c = str:sub(i, i) 272 | vim.api.nvim_buf_set_keymap( 273 | Buffalo_bufh, 274 | "n", 275 | c, 276 | string.format( 277 | "%s lua require('buffalo.ui')" .. 278 | ".select_menu_item()", 279 | i 280 | ), 281 | {} 282 | ) 283 | end 284 | 285 | 286 | for _, modified_line in pairs(modified_lines) do 287 | vim.api.nvim_buf_add_highlight( 288 | Buffalo_bufh, 289 | -1, 290 | "BuffaloBuffersModified", 291 | modified_line - 1, 292 | 0, 293 | -1 294 | ) 295 | end 296 | vim.api.nvim_buf_add_highlight( 297 | Buffalo_bufh, 298 | -1, 299 | "BuffaloBuffersCurrentLine", 300 | current_buf_line - 1, 301 | 0, 302 | -1 303 | ) 304 | end 305 | 306 | function M.toggle_tab_menu() 307 | log.trace("toggle_tab_menu()") 308 | if Buffalo_Tabs_win_id ~= nil and vim.api.nvim_win_is_valid(Buffalo_Tabs_win_id) then 309 | close_tabs_menu(true) 310 | return 311 | end 312 | local tabid = api.get_current_tab() 313 | local current_tab_id = api.get_tab_number(tabid) 314 | local tabs = api.get_tabs() 315 | 316 | local win_info = create_window("tabpages") 317 | local contents = {} 318 | 319 | Buffalo_Tabs_win_id = win_info.win_id 320 | Buffalo_Tabs_bufh = win_info.bufnr 321 | 322 | local current_tab_line = 1 323 | 324 | for idx = 1, #tabs do 325 | local current_tab = api.get_tab_number(idx) 326 | if current_tab == current_tab_id then 327 | current_tab_line = idx 328 | end 329 | 330 | if current_tab == 0 then 331 | return 332 | end 333 | if current_tab > 0 then 334 | local twins = api.get_tab_wins(idx) 335 | local window = #twins > 1 and "[ " .. #twins .. " windows ]" or "[ " .. #twins .. " window ]" 336 | contents[idx] = string.format("Tab %s %s", current_tab, window) 337 | else 338 | contents[idx] = string.format("Tab [ deleted ]") 339 | end 340 | end 341 | 342 | vim.api.nvim_set_option_value("number", true, { win = Buffalo_Tabs_win_id }) 343 | 344 | vim.api.nvim_buf_set_name(Buffalo_Tabs_bufh, "buffalo-tabs") 345 | vim.api.nvim_buf_set_lines(Buffalo_Tabs_bufh, 0, #contents, false, contents) 346 | vim.api.nvim_buf_set_option(Buffalo_Tabs_bufh, "filetype", "buffalo") 347 | vim.api.nvim_buf_set_option(Buffalo_Tabs_bufh, "buftype", "acwrite") 348 | vim.api.nvim_buf_set_option(Buffalo_Tabs_bufh, "bufhidden", "delete") 349 | vim.cmd(string.format(":call cursor(%d, %d)", current_tab_line, 1)) 350 | vim.api.nvim_buf_set_keymap( 351 | Buffalo_Tabs_bufh, 352 | "n", 353 | config.general_commands.exit_menu, 354 | "lua require('buffalo.ui').toggle_tab_menu()", 355 | { silent = true } 356 | ) 357 | vim.api.nvim_buf_set_keymap( 358 | Buffalo_Tabs_bufh, 359 | "n", 360 | "q", 361 | "lua require('buffalo.ui').toggle_tab_menu()", 362 | { silent = true } 363 | ) 364 | vim.api.nvim_buf_set_keymap( 365 | Buffalo_Tabs_bufh, 366 | "n", 367 | "", 368 | "lua require('buffalo.ui').toggle_tab_menu()", 369 | { silent = true } 370 | ) 371 | for _, value in pairs(config.tab_commands) do 372 | vim.api.nvim_buf_set_keymap( 373 | Buffalo_Tabs_bufh, 374 | "n", 375 | value.key, 376 | "lua require('buffalo.ui').select_tab_menu_item('" .. value.command .. "')", 377 | {} 378 | ) 379 | end 380 | vim.cmd( 381 | string.format( 382 | "autocmd BufModifiedSet set nomodified", 383 | Buffalo_Tabs_bufh 384 | ) 385 | ) 386 | vim.cmd( 387 | "autocmd BufLeave ++nested ++once silent" .. 388 | " lua require('buffalo.ui').toggle_tab_menu()" 389 | ) 390 | vim.cmd( 391 | string.format( 392 | "autocmd BufWriteCmd " .. 393 | " lua require('buffalo.ui').on_menu_save()", 394 | Buffalo_Tabs_bufh 395 | ) 396 | ) 397 | local str = "1234567890" 398 | 399 | for i = 1, #str do 400 | local c = str:sub(i, i) 401 | vim.api.nvim_buf_set_keymap( 402 | Buffalo_Tabs_bufh, 403 | "n", 404 | c, 405 | string.format( 406 | "%s lua require('buffalo.ui')" .. 407 | ".select_tab_menu_item()", 408 | i 409 | ), 410 | {} 411 | ) 412 | end 413 | vim.api.nvim_buf_add_highlight( 414 | Buffalo_Tabs_bufh, 415 | -1, 416 | "BuffaloTabsCurrentLine", 417 | current_tab_line - 1, 418 | 0, 419 | -1 420 | ) 421 | end 422 | 423 | function M.select_tab_menu_item(command) 424 | local idx = vim.fn.line(".") 425 | close_tabs_menu(true) 426 | M.nav_tab(idx, command) 427 | end 428 | 429 | function M.select_menu_item(command) 430 | local idx = vim.fn.line(".") 431 | if vim.api.nvim_buf_get_changedtick(vim.fn.bufnr()) > 0 then 432 | M.on_menu_save() 433 | end 434 | close_menu(true) 435 | M.nav_buf(idx, command) 436 | update_buffers() 437 | end 438 | 439 | local function get_menu_items() 440 | log.trace("_get_menu_items()") 441 | local lines = vim.api.nvim_buf_get_lines(Buffalo_bufh, 0, -1, true) 442 | local indices = {} 443 | 444 | for _, line in pairs(lines) do 445 | if not utils.is_white_space(line) then 446 | table.insert(indices, line) 447 | end 448 | end 449 | 450 | return indices 451 | end 452 | 453 | local function set_mark_list(new_list) 454 | log.trace("set_mark_list(): New list:", new_list) 455 | 456 | local original_marks = utils.deep_copy(marks) 457 | marks = {} 458 | for _, v in pairs(new_list) do 459 | if type(v) == "string" then 460 | local filename = v 461 | local buf_id = nil 462 | local current_mark = get_mark_by_name(filename, original_marks) 463 | if current_mark then 464 | filename = current_mark.filename 465 | buf_id = current_mark.buf_id 466 | else 467 | buf_id = vim.fn.bufnr(v) 468 | end 469 | table.insert(marks, { 470 | filename = filename, 471 | buf_id = buf_id, 472 | }) 473 | end 474 | end 475 | end 476 | 477 | function M.on_menu_save() 478 | log.trace("on_menu_save()") 479 | set_mark_list(get_menu_items()) 480 | end 481 | 482 | function M.nav_tab(id, command) 483 | log.trace("nav_buf(): Navigating to", id) 484 | 485 | if command == nil or command == "tabnext" then 486 | local tabid = api.get_tab_number(id) 487 | if tabid ~= -1 then 488 | vim.cmd(tabid .. "tabnext") 489 | end 490 | elseif command == "tabclose" then 491 | -- vim.api.nvim_tabpage_del_var(id, "buffalo") 492 | vim.cmd(id .. "tabclose") 493 | else 494 | vim.cmd(id .. command) 495 | end 496 | end 497 | 498 | function M.nav_buf(id, command) 499 | log.trace("nav_buf(): Navigating to", id) 500 | update_marks() 501 | 502 | local mark = marks[id] 503 | if not mark then 504 | return 505 | end 506 | if command == nil or command == "edit" then 507 | local bufnr = vim.fn.bufnr(mark.filename) 508 | if bufnr ~= -1 then 509 | vim.cmd("buffer " .. bufnr) 510 | else 511 | vim.cmd("edit " .. mark.filename) 512 | end 513 | else 514 | vim.cmd(command .. " " .. mark.filename) 515 | end 516 | end 517 | 518 | local function get_current_buf_line() 519 | local current_buf_id = vim.fn.bufnr() 520 | for idx, mark in pairs(marks) do 521 | if mark.buf_id == current_buf_id then 522 | return idx 523 | end 524 | end 525 | log.error("get_current_buf_line(): Could not find current buffer in marks") 526 | return -1 527 | end 528 | 529 | function M.nav_tab_next() 530 | log.trace("nav_tab_next()") 531 | vim.cmd("tabnext") 532 | end 533 | 534 | function M.nav_tab_prev() 535 | log.trace("nav_tab_prev()") 536 | vim.cmd("tabprev") 537 | end 538 | 539 | function M.nav_buf_next() 540 | log.trace("nav_buf_next()") 541 | update_marks() 542 | local current_buf_line = get_current_buf_line() 543 | if current_buf_line == -1 then 544 | return 545 | end 546 | local next_buf_line = current_buf_line + 1 547 | if next_buf_line > #marks then 548 | if config.general_commands.cycle then 549 | M.nav_buf(1) 550 | end 551 | else 552 | M.nav_buf(next_buf_line) 553 | end 554 | end 555 | 556 | function M.nav_buf_prev() 557 | log.trace("nav_buf_prev()") 558 | update_marks() 559 | local current_buf_line = get_current_buf_line() 560 | if current_buf_line == -1 then 561 | return 562 | end 563 | local prev_buf_line = current_buf_line - 1 564 | if prev_buf_line < 1 then 565 | if config.general_commands.cycle then 566 | M.nav_buf(#marks) 567 | end 568 | else 569 | M.nav_buf(prev_buf_line) 570 | end 571 | end 572 | 573 | function M.location_window(options) 574 | local default_options = { 575 | relative = "editor", 576 | style = "minimal", 577 | width = 30, 578 | height = 15, 579 | row = 2, 580 | col = 2, 581 | } 582 | options = vim.tbl_extend("keep", options, default_options) 583 | 584 | local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) 585 | local win_id = vim.api.nvim_open_win(bufnr, true, options) 586 | 587 | return { 588 | bufnr = bufnr, 589 | win_id = win_id, 590 | } 591 | end 592 | 593 | function M.save_menu_to_file(filename) 594 | log.trace("save_menu_to_file()") 595 | if filename == nil or filename == "" then 596 | filename = vim.fn.input("Enter filename: ") 597 | if filename == "" then 598 | return 599 | end 600 | end 601 | local file = io.open(filename, "w") 602 | if file == nil then 603 | log.error("save_menu_to_file(): Could not open file for writing") 604 | return 605 | end 606 | for _, mark in pairs(marks) do 607 | file:write(Path:new(mark.filename):absolute() .. "\n") 608 | end 609 | file:close() 610 | end 611 | 612 | function M.load_menu_from_file(filename) 613 | log.trace("load_menu_from_file()") 614 | if filename == nil or filename == "" then 615 | filename = vim.fn.input("Enter filename: ") 616 | if filename == "" then 617 | return 618 | end 619 | end 620 | local file = io.open(filename, "r") 621 | if file == nil then 622 | log.error("load_menu_from_file(): Could not open file for reading") 623 | return 624 | end 625 | local lines = {} 626 | for line in file:lines() do 627 | table.insert(lines, line) 628 | end 629 | file:close() 630 | set_mark_list(lines) 631 | update_buffers() 632 | end 633 | 634 | local go_to = config.go_to 635 | if go_to.enabled then 636 | local keys = "1234567890" 637 | 638 | for i = 1, #keys do 639 | local buffer = keys:sub(i, i) 640 | map( 641 | 'n', 642 | string.format(go_to.go_to_buffer, buffer), 643 | function() M.nav_buf(i) end, 644 | opts 645 | ) 646 | end 647 | 648 | for i = 1, #keys do 649 | local tab = keys:sub(i, i) 650 | map( 651 | 'n', 652 | string.format(go_to.go_to_tab, tab), 653 | function() M.nav_tab(i) end, 654 | opts 655 | ) 656 | end 657 | end 658 | 659 | local filter = config.filter 660 | if filter.enabled then 661 | map({ 't', 'n' }, filter.filter_tabs, function() 662 | M.toggle_tab_menu() 663 | 664 | vim.defer_fn(function() 665 | vim.fn.feedkeys('/') 666 | end, 50) 667 | end, opts) 668 | 669 | map({ 't', 'n' }, filter.filter_buffers, function() 670 | M.toggle_buf_menu() 671 | 672 | vim.defer_fn(function() 673 | vim.fn.feedkeys('/') 674 | end, 50) 675 | end, opts) 676 | end 677 | return M 678 | -------------------------------------------------------------------------------- /lua/buffalo/utils.lua: -------------------------------------------------------------------------------- 1 | local Path = require("plenary.path") 2 | 3 | local M = {} 4 | 5 | 6 | function M.project_key() 7 | return vim.loop.cwd() 8 | end 9 | 10 | function M.normalize_path(item) 11 | if string.find(item, ".*:///.*") ~= nil then 12 | return Path:new(item) 13 | end 14 | return Path:new(Path:new(item):absolute()):make_relative(M.project_key()) 15 | end 16 | 17 | function M.get_file_name(file) 18 | return file:match("[^/\\]*$") 19 | end 20 | 21 | local function key_in_table(key, table) 22 | for k, _ in pairs(table) do 23 | if k == key then 24 | return true 25 | end 26 | end 27 | return false 28 | end 29 | 30 | 31 | function M.get_short_file_name(file, current_short_fns) 32 | local short_name = nil 33 | -- Get normalized file path 34 | file = M.normalize_path(file) 35 | -- Get all folders in the file path 36 | local folders = {} 37 | -- Convert file to string 38 | local file_str = tostring(file) 39 | for folder in string.gmatch(file_str, "([^/]+)") do 40 | -- insert firts char only 41 | table.insert(folders, folder) 42 | end 43 | -- File to string 44 | file = tostring(file) 45 | -- Count the number of slashes in the relative file path 46 | local slash_count = 0 47 | for _ in string.gmatch(file, "/") do 48 | slash_count = slash_count + 1 49 | end 50 | if slash_count == 0 then 51 | short_name = M.get_file_name(file) 52 | else 53 | -- Return the file name preceded by the number of slashes 54 | short_name = slash_count .. "|" .. M.get_file_name(file) 55 | end 56 | -- Check if the file name is already in the list of short file names 57 | -- If so, return the short file name with one number in front of it 58 | local i = 1 59 | while key_in_table(short_name, current_short_fns) do 60 | local folder = folders[i] 61 | if folder == nil then 62 | folder = i 63 | end 64 | short_name = short_name .. " (" .. folder .. ")" 65 | i = i + 1 66 | end 67 | return short_name 68 | end 69 | 70 | function M.get_short_term_name(term_name) 71 | return term_name:gsub("://.*//", ":") 72 | end 73 | 74 | function M.absolute_path(item) 75 | return Path:new(item):absolute() 76 | end 77 | 78 | function M.is_white_space(str) 79 | return str:gsub("%s", "") == "" 80 | end 81 | 82 | function M.buffer_is_valid(buf_id, buf_name) 83 | return 1 == vim.fn.buflisted(buf_id) 84 | and buf_name ~= "" 85 | end 86 | 87 | function M.tab_is_valid(tab_id, tab_name) 88 | -- return 1 == vim.api.nvim_tabpage_is_valid(tab_id) 89 | -- and tab_name ~= "" 90 | local valid = vim.api.nvim_tabpage_is_valid(tab_id) 91 | if not valid then return -1 end 92 | return tab_name ~= "" 93 | end 94 | 95 | -- tbl_deep_extend does not work the way you would think 96 | local function merge_table_impl(t1, t2) 97 | for k, v in pairs(t2) do 98 | if type(v) == "table" then 99 | if type(t1[k]) == "table" then 100 | merge_table_impl(t1[k], v) 101 | else 102 | t1[k] = v 103 | end 104 | else 105 | t1[k] = v 106 | end 107 | end 108 | end 109 | 110 | 111 | function M.merge_tables(...) 112 | local out = {} 113 | for i = 1, select("#", ...) do 114 | merge_table_impl(out, select(i, ...)) 115 | end 116 | return out 117 | end 118 | 119 | function M.deep_copy(obj, seen) 120 | -- Handle non-tables and previously-seen tables. 121 | if type(obj) ~= 'table' then return obj end 122 | if seen and seen[obj] then return seen[obj] end 123 | 124 | -- New table; mark it as seen and copy recursively. 125 | local s = seen or {} 126 | local res = {} 127 | s[obj] = res 128 | for k, v in pairs(obj) do res[M.deep_copy(k, s)] = M.deep_copy(v, s) end 129 | return setmetatable(res, getmetatable(obj)) 130 | end 131 | 132 | return M 133 | -------------------------------------------------------------------------------- /plugin/buffalo.lua: -------------------------------------------------------------------------------- 1 | if vim.g.buffalo_loaded ~= nil then 2 | return 3 | end 4 | vim.g.buffalo_loaded = 1 5 | -------------------------------------------------------------------------------- /plugin/buffalo.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_buffalo') | finish | endif 2 | 3 | function! s:complete(...) 4 | return "buffers" 5 | endfunction 6 | 7 | command! -nargs=1 -complete=custom,s:complete Buffalo lua require'buffalo'.() 8 | 9 | let g:loaded_buffalo = 1 10 | 11 | --------------------------------------------------------------------------------