├── images ├── bind.png ├── outline.gif ├── preview.png ├── winbar.png └── outline-main.png ├── plugin └── plugin │ └── outline.vim ├── lua ├── split.lua └── outline.lua └── README.md /images/bind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djancyp/outline/HEAD/images/bind.png -------------------------------------------------------------------------------- /plugin/plugin/outline.vim: -------------------------------------------------------------------------------- 1 | command BSOpen execute "lua require'outline'.open()" 2 | -------------------------------------------------------------------------------- /images/outline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djancyp/outline/HEAD/images/outline.gif -------------------------------------------------------------------------------- /images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djancyp/outline/HEAD/images/preview.png -------------------------------------------------------------------------------- /images/winbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djancyp/outline/HEAD/images/winbar.png -------------------------------------------------------------------------------- /images/outline-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Djancyp/outline/HEAD/images/outline-main.png -------------------------------------------------------------------------------- /lua/split.lua: -------------------------------------------------------------------------------- 1 | local function is_match_empty(pat, plain) 2 | return not not string.find('', pat, nil, plain) 3 | end 4 | 5 | --- String split into table 6 | function string:split(sep, plain) 7 | local str = self 8 | local b, res = 0, {} 9 | sep = sep or '%s+' 10 | 11 | assert(type(sep) == 'string') 12 | assert(type(str) == 'string') 13 | 14 | if #sep == 0 then 15 | for i = 1, #str do 16 | res[#res + 1] = string.sub(str, i, i) 17 | end 18 | return res 19 | end 20 | 21 | assert(not is_match_empty(sep, plain), 'delimiter can not match empty string') 22 | 23 | while b <= #str do 24 | local e, e2 = string.find(str, sep, b, plain) 25 | if e then 26 | res[#res + 1] = string.sub(str, b, e-1) 27 | b = e2 + 1 28 | if b > #str then res[#res + 1] = "" end 29 | else 30 | res[#res + 1] = string.sub(str, b) 31 | break 32 | end 33 | end 34 | return res 35 | end 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outline ( Another Nvim Buffer Management with Winbar ) 2 | 3 | Outline is a simple(not so simple) Nvim buffer management plugin written in Lua. 4 | ![](https://github.com/Djancyp/outline/blob/main/images/outline.gif) 5 | # What you will get with this plugin. 6 | 7 | - Uses Nvim winbar to show buffer name and file change indicator. 8 | // image 9 | - Buffer management ui. 10 | - Buffer switch. 11 | - Buffer status. 12 | - Buffer close. 13 | - Buffer Preview. 14 | - Buffer bind shortcut key. 15 | 16 | - ![Buffer main](/images/outline-main.png) 17 | - ![Status line](/images/winbar.png) 18 | - ![Previw](/images/preview.png) 19 | - ![Key binding](/images/bind.png) 20 | 21 | ## requirements 22 | - Neovim Nightly ≥ v0.8 - Winbar support 23 | - A patched [nerd font](https://www.nerdfonts.com/) for the buffer icons 24 | - [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) for filetype icons (recommended) 25 | ## Install 26 | 27 | ```lua 28 | use {"Djancyp/outline"} 29 | 30 | require('outline').setup() 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```lua 36 | // Toggle Buffer tab 37 | :BSOpen 38 | // Recommended key binding shift+c 39 | 40 | ``` 41 | ## Keys 42 | ``` 43 | | Key | Action | 44 | | -------------- | ------------------------------- | 45 | | q,ESC,Ctrl-c | exit outline window | 46 | | j or | navigate down | 47 | | k or | navigate up | 48 | | D | close buffer | 49 | | `` | jump to buffer | 50 | | s | open buffer in horizontal split | 51 | | v | open buffer in vertical split | 52 | | | open preview for buffer | 53 | | | bind buffer to a key | 54 | ``` 55 | 56 | ## Contributing 57 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 58 | 59 | Please make sure to update tests as appropriate. 60 | ## License 61 | [MIT](https://choosealicense.com/licenses/mit/) 62 | -------------------------------------------------------------------------------- /lua/outline.lua: -------------------------------------------------------------------------------- 1 | --TODO: 2 | -- * Add icon colors 3 | -- * Add sort options 4 | -- * Add Options for configuring the outline 5 | local api = vim.api 6 | local cmd = vim.api.nvim_create_autocmd 7 | local ui = api.nvim_list_uis()[1] 8 | local popup = require("plenary.popup") 9 | require 'split' 10 | local M = {} 11 | 12 | function M.setup(opt) 13 | M.main_win = nil 14 | M.main_buf = nil 15 | M.main_win_width = 50 16 | M.main_win_height = 20 17 | M.main_win_style = "minimal" 18 | M.main_win_relavent = "win" 19 | M.main_win_border = 'double' 20 | M.main_col = ui.width / 2 - M.main_win_width / 2 21 | M.main_row = ui.height / 2 - M.main_win_height / 2 22 | 23 | -- Preview mode window 24 | M.preview_win = nil 25 | M.preview_buf = nil 26 | M.preview_win_width = ui.width / 2 27 | M.preview_win_height = ui.height / 2 28 | M.preview_win_style = "minimal" 29 | M.preview_win_relavent = "win" 30 | M.preview_win_border = 'double' 31 | M.preview_col = M.main_win_width / 2 - M.preview_win_width / 2 32 | M.preview_row = M.main_win_height / 2 - M.preview_win_height / 2 33 | M.custom_keys = {} 34 | 35 | end 36 | 37 | function M.open() 38 | M.back_win = api.nvim_get_current_win() 39 | if not M.main_buf and not M.main_win then 40 | M.main_buf = api.nvim_create_buf(false, true) 41 | local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 42 | local win_id, win = popup.create(M.main_buf, { 43 | title = "Outline - Buffers", 44 | highlight = "OutlineWindow", 45 | line = math.floor(((vim.o.lines - 5) / 2) - 1), 46 | col = math.floor((vim.o.columns - M.main_win_width) / 2), 47 | minwidth = M.main_win_width, 48 | minheight = 5, 49 | borderchars = borderchars, 50 | }) 51 | M.main_win = win_id 52 | M.build_win() 53 | M.setKeys(M.back_win, M.main_buf) 54 | M.add_custom_keys() 55 | else 56 | xpcall(function() 57 | api.nvim_win_close(M.main_win, false) 58 | api.nvim_buf_delete(M.main_buf, {}) 59 | M.main_win = nil 60 | M.main_buf = nil 61 | end, function() 62 | M.main_win = nil 63 | M.main_buf = nil 64 | M.open() 65 | end) 66 | end 67 | end 68 | 69 | function M.add_custom_keys() 70 | for k, v in pairs(M.custom_keys) do 71 | api.nvim_buf_set_keymap(M.main_buf, 'n', v.key, 72 | string.format([[:lua require'outline'.set_saved_buffer(%s,%s)]], M.back_win, tonumber(v.buffer)), 73 | { nowait = true, noremap = true, silent = true }) 74 | end 75 | end 76 | 77 | function M.set_saved_buffer(win, buf) 78 | api.nvim_win_set_buf(win, tonumber(buf)) 79 | M.close() 80 | end 81 | 82 | function M.openPreview(buf) 83 | M.preview_buf = api.nvim_create_buf(false, true) 84 | -- rount float to int 85 | M.preview_win_width = math.floor(M.preview_win_width) 86 | M.preview_win_height = math.floor(M.preview_win_height) 87 | local cursor_pos = api.nvim_win_get_cursor(M.main_win) 88 | cursor_pos[1] = cursor_pos[1] - 1 89 | local lines = api.nvim_buf_get_lines(buf, cursor_pos[1], -1, false)[1] 90 | local buffer = tonumber(lines:split(" ")[1]) 91 | M.preview_buf = buffer 92 | -- M.preview_buf = api.nvim_create_buf(false, true) 93 | 94 | local width = M.preview_win_width 95 | local height = M.preview_win_height 96 | local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 97 | local win_id, win = popup.create(M.preview_buf, { 98 | title = "Outline - Buffers", 99 | highlight = "OutlineWindow", 100 | line = math.floor(((vim.o.lines - height) / 2) - 1), 101 | col = math.floor((vim.o.columns - width) / 2), 102 | maxwidth = width, 103 | maxheight = height, 104 | borderchars = borderchars, 105 | minheight = height, 106 | minwidth = width 107 | }) 108 | 109 | M.preview_win = win_id 110 | api.nvim_buf_set_option(M.preview_buf, 'modifiable', false) 111 | M.setPreviewKeys(M.preview_buf) 112 | end 113 | 114 | function M.setPreviewKeys(buf) 115 | api.nvim_buf_set_keymap(0, 'n', 'q', ':lua require"outline".close_preview()', 116 | { nowait = true, noremap = true, silent = true }) 117 | end 118 | 119 | function M.close_preview() 120 | api.nvim_win_close(M.preview_win, false) 121 | api.nvim_buf_delete(M.preview_buf, {}) 122 | M.preview_win = nil 123 | M.preview_buf = nil 124 | api.nvim_set_current_win(M.main_win) 125 | end 126 | 127 | function M.close() 128 | if M.main_win then 129 | api.nvim_win_close(M.main_win, false) 130 | api.nvim_buf_delete(M.main_buf, {}) 131 | M.main_win = nil 132 | M.main_buf = nil 133 | if M.preview_buf ~= nil then 134 | M.close_preview() 135 | end 136 | end 137 | end 138 | 139 | function M.setKeys(win, buf) 140 | -- Basic window buffer configuration 141 | api.nvim_buf_set_keymap(buf, 'n', '', 142 | string.format([[:lua require'outline'.set_buffer(%s,%s, 'window', vim.v.count)]], win, buf), 143 | { nowait = true, noremap = true, silent = true }) 144 | api.nvim_buf_set_keymap(buf, 'n', 's', 145 | string.format([[:lua require'outline'.set_buffer(%s,%s, 'hsplit', vim.v.count)]], win, buf), 146 | { nowait = true, noremap = true, silent = true }) 147 | api.nvim_buf_set_keymap(buf, 'n', 'v', 148 | string.format([[:lua require'outline'.set_buffer(%s,%s, 'vsplit', vim.v.count)]], win, buf), 149 | { nowait = true, noremap = true, silent = true }) 150 | api.nvim_buf_set_keymap(buf, 'n', 'P', 151 | string.format([[:lua require'outline'.openPreview(%s)]], buf), 152 | { nowait = true, noremap = true, silent = true }) 153 | api.nvim_buf_set_keymap(buf, 'n', 'D', 154 | string.format([[:lua require'outline'.close_buffer(%s)]], buf), 155 | { nowait = true, noremap = true, silent = true }) 156 | api.nvim_buf_set_keymap(buf, 'n', 'B', 157 | string.format([[:lua require'outline'.open_input_window(%s)]], buf), 158 | { nowait = true, noremap = true, silent = true }) 159 | -- Navigation keymaps 160 | api.nvim_buf_set_keymap(buf, 'n', 'q', ':lua require"outline".close()', 161 | { nowait = true, noremap = true, silent = true }) 162 | api.nvim_buf_set_keymap(buf, 'n', '', ':lua require"outline".close()', 163 | { nowait = true, noremap = true, silent = true }) 164 | api.nvim_buf_set_keymap(buf, 'n', '', 'j', 165 | { nowait = true, noremap = true, silent = true }) 166 | api.nvim_buf_set_keymap(buf, 'n', '', 'k', 167 | { nowait = true, noremap = true, silent = true }) 168 | -- vim.cmd(string.format("au CursorMoved if line(\".\") == 1 | call feedkeys('j', 'n') | endif", buf)) 169 | end 170 | 171 | function M.build_win() 172 | api.nvim_buf_set_option(M.main_buf, "modifiable", true) 173 | M.list_buffers() 174 | api.nvim_buf_set_option(M.main_buf, "modifiable", false) 175 | end 176 | 177 | function M.list_buffers() 178 | --get open buffe 179 | local buffers = api.nvim_list_bufs() 180 | local buffer_names = {} 181 | table.sort(buffers) 182 | local current_buffer = api.nvim_get_current_buf() 183 | for _, buffer in ipairs(buffers) do 184 | --check if buffers are avtive 185 | if api.nvim_buf_is_loaded(buffer) then 186 | local buffer_name = api.nvim_buf_get_name(buffer) 187 | -- check if buffer has changed 188 | if buffer_name == "" or nil then goto continue end 189 | 190 | local buffer_changed = api.nvim_buf_get_option(buffer, 'modified') 191 | local buffer_id = api.nvim_buf_get_number(buffer) 192 | 193 | local active_buff = "" 194 | if buffer_id == current_buffer then 195 | active_buff = "" 196 | end 197 | local buffer_icon = buffer_changed and '﨣' or '' 198 | 199 | local max_width = M.main_win_width - #buffer_name - 20 200 | local buffer_name_width = string.len(buffer_name) 201 | if buffer_name_width > max_width then 202 | buffer_name = "..." .. string.sub(buffer_name, 1 - max_width) 203 | end 204 | for b, bind in pairs(M.custom_keys) do 205 | if bind.buffer == buffer_id then 206 | buffer_name = string.format("%s %s", bind.key .. " ", buffer_name) 207 | end 208 | end 209 | buffer_names[#buffer_names + 1] = string.format("%s %s %s %s", buffer_id, buffer_name, active_buff, buffer_icon) 210 | ::continue:: 211 | end 212 | end 213 | if #buffer_names ~= 0 then 214 | api.nvim_buf_set_lines(M.main_buf, 0, #(buffer_names), false, buffer_names) 215 | end 216 | end 217 | 218 | function M.set_buffer(win, buf, opt) 219 | local cursor_pos = api.nvim_win_get_cursor(M.main_win) 220 | cursor_pos[1] = cursor_pos[1] - 1 221 | local lines = api.nvim_buf_get_lines(buf, cursor_pos[1], -1, false)[1] 222 | local buffer = tonumber(lines:split(" ")[1]) 223 | --check if window is split 224 | if opt == 'window' then 225 | api.nvim_win_set_buf(win, tonumber(buffer)) 226 | elseif opt == 'hsplit' then 227 | api.nvim_command('vsplit') 228 | api.nvim_win_set_buf(api.nvim_get_current_win(), buffer) 229 | elseif opt == 'vsplit' then 230 | api.nvim_command('split') 231 | api.nvim_win_set_buf(api.nvim_get_current_win(), buffer) 232 | end 233 | M.close() 234 | end 235 | 236 | function M.close_buffer(buf) 237 | local cursor_pos = api.nvim_win_get_cursor(M.main_win) 238 | local lines = api.nvim_buf_get_lines(buf, cursor_pos[1] - 1, -1, false)[1] 239 | local buffer = tonumber(lines:split(' ')[1]) 240 | -- close buffer 241 | vim.cmd(string.format('bd %s', buffer)) 242 | -- reset the buffer loader 243 | M.close() 244 | M.open() 245 | end 246 | 247 | function M.open_input_window() 248 | M.input_buf = api.nvim_create_buf(false, true) 249 | local ok, result = pcall(vim.api.nvim_buf_get_var, M.input_buf, 'lsp_enabled') 250 | local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 251 | local win_id, win = popup.create(M.input_buf, { 252 | title = "Input Keybinding:", 253 | highlight = "OutlineWindow", 254 | line = math.floor(((vim.o.lines - 1) / 2) - 1), 255 | col = math.floor((vim.o.columns - 20) / 2), 256 | minwidth = 20, 257 | height = 1, 258 | borderchars = borderchars, 259 | }) 260 | M.input_win = win_id 261 | M.set_input_keys(M.input_buf) 262 | -- turn off lsp for this buffer 263 | api.nvim_buf_set_option(M.input_buf, 'omnifunc', 'v:lua.vim.lsp.omnifunc') 264 | api.nvim_win_set_option(M.input_win, 'cursorline', true) 265 | api.nvim_set_current_win(M.input_win) 266 | api.nvim_win_set_cursor(M.input_win, { 1, 0 }) 267 | api.nvim_command('startinsert') 268 | api.nvim_buf_set_option(M.input_buf, 'modifiable', true) 269 | end 270 | 271 | function M.set_input_keys(buf) 272 | 273 | api.nvim_buf_set_keymap(buf, 'i', '', ':lua require"outline".bind_key_to_buffer()', 274 | { nowait = true, noremap = true, silent = true }) 275 | api.nvim_buf_set_keymap(buf, 'i', '', ':lua require"outline".close_input_window()', 276 | { nowait = true, noremap = true, silent = true }) 277 | api.nvim_buf_set_keymap(buf, 'i', 'q', ':lua require"outline".close_input_window()', 278 | { nowait = true, noremap = true, silent = true }) 279 | api.nvim_buf_set_keymap(buf, 'i', '', ':lua require"outline".close_input_window()', 280 | { nowait = true, noremap = true, silent = true }) 281 | end 282 | 283 | function M.close_input_window() 284 | api.nvim_win_close(M.input_win, true) 285 | M.input_buf = nil 286 | M.input_win = nil 287 | end 288 | 289 | function M.bind_key_to_buffer() 290 | --get current line from window 291 | local main_cursor_pos = api.nvim_win_get_cursor(M.main_win) 292 | main_cursor_pos[1] = main_cursor_pos[1] - 1 293 | local lines = api.nvim_buf_get_lines(M.main_buf, main_cursor_pos[1], -1, false)[1] 294 | local buffer = tonumber(lines:split(" ")[1]) 295 | local cursor_pos = api.nvim_win_get_cursor(M.input_win) 296 | local key = api.nvim_buf_get_lines(M.input_buf, cursor_pos[1] - 1, -1, false)[1] 297 | api.nvim_buf_set_keymap(M.main_buf, 'n', key, 298 | string.format([[:lua require'outline'.set_buffer(%s,%s, 'window', vim.v.count)]], M.back_win, M.main_buf), 299 | { nowait = true, noremap = true, silent = true }) 300 | -- add to custom keybindings 301 | -- check if buffer is already in custom keybindings 302 | -- if not add its 303 | for _, v in pairs(M.custom_keys) do 304 | if v.key == key then 305 | vim.notify('Key already exists') 306 | api.nvim_command('startinsert') 307 | return 308 | else if v.buffer == buffer then 309 | v.key = key 310 | vim.notify('Buffer binding changed.') 311 | M.close_input_window() 312 | M.close() 313 | M.open() 314 | 315 | return 316 | end 317 | end 318 | end 319 | M.custom_keys[#M.custom_keys + 1] = { 320 | key = key, 321 | buffer = buffer, 322 | window = M.back_win, 323 | opt = 'window' 324 | } 325 | M.close_input_window() 326 | vim.notify('Buffer binding added.') 327 | M.close() 328 | M.open() 329 | end 330 | 331 | return M 332 | --------------------------------------------------------------------------------