├── .gitignore ├── LICENSE.md ├── README.md ├── lua └── scratchpad.lua └── plugin └── scratchpad.vim /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | temp/ 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 Fraser 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScratchPad 2 | 3 | A snazzy neovim plugin to centre your buffer by creating a persistent 4 | scratchpad off to the left. 5 | 6 | ![scratchpad-vid](https://raw.githubusercontent.com/FraserLee/readme_resources/main/resize.gif) 7 | 8 |

9 | # Installation 10 | 11 | If you're reading this you've probably already got a plugin manager. If not, I 12 | recommend [Vim-Plug](https://github.com/junegunn/vim-plug), but they're essentially 13 | interchangeable. Add the appropriate line in the appropriate spot in your 14 | `.vimrc` file. 15 | 16 | ```vim 17 | " vim-plug 18 | Plug 'FraserLee/ScratchPad' 19 | 20 | " vundle 21 | Plugin 'FraserLee/ScratchPad' 22 | 23 | " packer.nvim 24 | use 'FraserLee/ScratchPad' 25 | 26 | " etc... 27 | ``` 28 | 29 | Run your version of `:PlugInstall` and things should be good to go. 30 | 31 |



32 | # Usage 33 | 34 | ```vim 35 | nnoremap cc ScratchPad 36 | ``` 37 | --- 38 | 39 | By default, all scratchpad windows point to one underlying file 40 | (`~/.scratchpad` unless changed). They'll auto-save when modified, 41 | reload if the file is changed, and automatically close when all other 42 | windows are gone. 43 | 44 | I tend to use them as the digital equivalent of the sticky notes that coat 45 | all objects vaguely proximate to my desk, but that's not a requirement. 46 | 47 | - `:ScratchPad` to toggle the scratchpad 48 | - `:ScratchPad open` opens a new scratchpad 49 | - `:ScratchPad close` closes all scratchpads in the current tab 50 | 51 |



52 | # Configuration 53 | 54 | By default, the scratchpad will auto-open when you open vim, and automatically 55 | open / close / resize itself as the window size (and spilt) changes. 56 | 57 | 58 | Disable scratchpad on startup: 59 | ```vim 60 | let g:scratchpad_autostart = 0 61 | ``` 62 | 63 | Disable automatic resizing: 64 | ```vim 65 | let g:scratchpad_autosize = 0 66 | ``` 67 | 68 | ### Automatic Size Junk 69 | 70 | The assumed width of code, as per what will be centred on screen. Set this to the same 71 | thing as any sort of colour column. 72 | 73 | ```vim 74 | let g:scratchpad_textwidth = 80 " (80 is the default) 75 | ``` 76 | 77 | The minimum width of a ScratchPad before it will - if autosize is enabled - 78 | close itself. 79 | 80 | ```vim 81 | let g:scratchpad_minwidth = 12 82 | ``` 83 | 84 | ## File Locations 85 | 86 | Change the scratchpad file by 87 | ```vim 88 | let g:scratchpad_location = '~/.scratchpad' 89 | ``` 90 | 91 | Auto-focus when opening a scratchpad window: 92 | ```vim 93 | let g:scratchpad_autofocus = 1 94 | ``` 95 | 96 | ### Daily ScratchPad 97 | Instead of having one ScratchPad have a fresh one for each day. 98 | The old ScratchPads are saved as well. Disabled by default. 99 | 100 | Enable daily scratchpad 101 | ```vim 102 | let g:scratchpad_daily = 1 103 | ``` 104 | 105 | Change the daily scratchpad directory 106 | ```vim 107 | let g:scratchpad_daily_location = '~/.daily_scratchpad' 108 | ``` 109 | 110 | Change the daily scratchpad file name format using [lua os date](https://www.lua.org/pil/22.1.html) 111 | ```vim 112 | let g:scratchpad_daily_format = '%Y-%m-%d' 113 | ``` 114 | 115 | --- 116 | 117 | Edit colour with 118 | ```vim 119 | hi ScratchPad ctermfg=X ctermbg=Y 120 | ``` 121 | 122 | 123 |



124 | # Making Stuff Look (somewhat) Decent 125 | 126 | I've added a line to disable the 127 | [virtual-text colour column](https://github.com/lukas-reineke/virt-column.nvim) 128 | in scratchpad buffers if that plugin's found, since I think these two pair 129 | pretty well together. If you want to get something looking similar to the 130 | screenshots, here's a start. 131 | 132 | ```vim 133 | call plug#begin('~/.vim/plugged') 134 | Plug 'morhetz/gruvbox' 135 | Plug 'fraserlee/ScratchPad' 136 | Plug 'lukas-reineke/virt-column.nvim' 137 | call plug#end() 138 | 139 | " ------------------------------ SETUP --------------------------------------- 140 | 141 | se nu " Turn on line numbers 142 | se colorcolumn=80 " Set the colour-column to 80 143 | 144 | noremap 145 | let mapleader=" " 146 | 147 | " cc to toggle ScratchPad 148 | nnoremap cc ScratchPad 149 | 150 | lua << EOF 151 | require('virt-column').setup{ char = '|' } 152 | EOF 153 | 154 | " -------------------------- COLOUR SCHEME ----------------------------------- 155 | 156 | colorscheme gruvbox 157 | let g:gruvbox_contrast_dark = 'hard' 158 | se background=dark 159 | 160 | " Set the colourcolumn background to the background colour, 161 | " foreground to the same as the window split colour 162 | 163 | execute "hi ColorColumn ctermbg=" . 164 | \matchstr(execute('hi Normal'), 'ctermbg=\zs\S*') 165 | hi! link VirtColumn VertSplit 166 | ``` 167 | 168 | ![1](https://raw.githubusercontent.com/FraserLee/readme_resources/main/screenshot%201.png) 169 | ![2](https://raw.githubusercontent.com/FraserLee/readme_resources/main/screenshot%202.png) 170 | ![3](https://raw.githubusercontent.com/FraserLee/readme_resources/main/screenshot%203.png) 171 | -------------------------------------------------------------------------------- /lua/scratchpad.lua: -------------------------------------------------------------------------------- 1 | local M = { enabled = false, prev_win = 0 } 2 | 3 | local api = vim.api 4 | local fn = vim.fn 5 | 6 | 7 | -- general module entry-point 8 | function M.invoke(...) 9 | local args = {...} 10 | 11 | if #args == 0 or string.lower(args[1]) == 'toggle' then 12 | M.toggle() 13 | elseif string.lower(args[1]) == 'open' then 14 | M.open() 15 | elseif string.lower(args[1]) == 'close' then 16 | M.close() 17 | elseif string.lower(args[1]) == 'auto' then 18 | M.auto() 19 | else 20 | print("Invalid argument. Usage:\n \n :ScratchPad [toggle|open|close|auto]\n") 21 | end 22 | end 23 | 24 | 25 | ------------------------- Internal Functions -------------------------------- 26 | 27 | -- check if a window is a scratchpad, win_id = 0 -> current window 28 | local function is_scratchpad(win_id) 29 | local win_var = fn.getwinvar(win_id, 'is_scratchpad') 30 | return type(win_var) == 'boolean' and win_var 31 | end 32 | 33 | 34 | -- returns a list of all (non-floating) windows open on the current tab 35 | local function windows() 36 | 37 | local tab_id = api.nvim_get_current_tabpage() 38 | local win_ids = api.nvim_tabpage_list_wins(tab_id) 39 | 40 | local solid_win_ids = {} 41 | 42 | for _, win_id in ipairs(win_ids) do 43 | 44 | -- ignore floating windows, check `:h api-floatwin` to see where this 45 | -- line is from 46 | 47 | if vim.api.nvim_win_get_config(win_id).relative == '' then 48 | table.insert(solid_win_ids, win_id) 49 | end 50 | end 51 | 52 | return solid_win_ids 53 | end 54 | 55 | 56 | -- returns list of (scratchpads, non-scratchpads) on current tab 57 | local function partition() 58 | 59 | local scratchpads = {} 60 | local non_scratchpads = {} 61 | 62 | for _, win_id in ipairs(windows()) do 63 | if is_scratchpad(win_id) then 64 | table.insert(scratchpads, win_id) 65 | else 66 | table.insert(non_scratchpads, win_id) 67 | end 68 | end 69 | 70 | return scratchpads, non_scratchpads 71 | end 72 | 73 | 74 | -- returns number of (scratchpads, non-scratchpads) on current tab 75 | local function count() 76 | local c = 0 77 | local window_list = windows() 78 | for _, win_id in ipairs(window_list) do 79 | if is_scratchpad(win_id) then c = c + 1 end 80 | end 81 | 82 | return c, #window_list - c 83 | end 84 | 85 | 86 | -- returns the number of distinct vertical lines of windows - window stacks are counted as one 87 | local function splits() 88 | 89 | -- (note: mildly naïve behaviour in the case in a 2x2 split, vsplit major, with the hsplits misaligned) 90 | local win_columns = {} 91 | local split_count = 0 92 | 93 | for _, win_id in ipairs(windows()) do 94 | if not is_scratchpad(win_id) then 95 | local _, col = unpack(api.nvim_win_get_position(win_id)) 96 | if not win_columns[col] then 97 | win_columns[col] = true 98 | split_count = split_count + 1 99 | end 100 | end 101 | end 102 | 103 | return split_count 104 | end 105 | 106 | 107 | -- given a scratchpad and a non-scratchpad, set sizes so the non-scratchpad is 108 | -- centred with reference to the box of the two. If keep_open is false, the 109 | -- scratchpad might be closed if things are too tight. 110 | local function set_size(non_scratchpad, scratchpad, keep_open) 111 | 112 | if non_scratchpad == nil then -- no non-scratchpad passed -> find the widest one and use that 113 | local _, non_scratchpads = partition() 114 | local widest = non_scratchpads[1] 115 | local width = fn.getwininfo(widest)[1].width 116 | 117 | for _, win_id in ipairs(non_scratchpads) do 118 | local c_width = fn.getwininfo(win_id)[1].width 119 | if c_width > width then 120 | width = c_width 121 | widest = win_id 122 | end 123 | end 124 | non_scratchpad = widest 125 | end 126 | 127 | 128 | local win_info = fn.getwininfo(non_scratchpad)[1] 129 | local total_width = win_info.width + fn.getwininfo(scratchpad)[1].width 130 | local total_text = total_width - win_info.textoff 131 | 132 | -- if the scratchpad is too thin, possibly close it 133 | if total_text < vim.g.scratchpad_textwidth + 2 * vim.g.scratchpad_minwidth and not keep_open then 134 | M.close() 135 | return 136 | end 137 | 138 | local excess = total_text - vim.g.scratchpad_textwidth 139 | local excess_left = math.max(math.floor(excess / 2), vim.g.scratchpad_minwidth) 140 | 141 | api.nvim_win_set_width(scratchpad, excess_left) 142 | api.nvim_win_set_width(non_scratchpad, total_width - excess_left) 143 | end 144 | 145 | 146 | ---------------------------- Public Functions ------------------------------- 147 | 148 | -- toggle the scratchpad 149 | function M.toggle() 150 | local pad_count, _ = count() 151 | 152 | if pad_count == 0 then 153 | M.enabled = true 154 | M.open() 155 | else 156 | M.enabled = false 157 | M.close() 158 | end 159 | end 160 | 161 | 162 | -- open a scratchpad window 163 | function M.open() 164 | local main_win_id = fn.win_getid() 165 | M.prev_win = main_win_id 166 | local en_cache = M.enabled 167 | M.enabled = false 168 | 169 | -- open a buffer. No existing scratchpads -> open at far-left of window, otherwise auto-place 170 | local n_scratchpads, _ = count() 171 | local prefix = '' 172 | if n_scratchpads == 0 then prefix = 'topleft ' end 173 | 174 | local location = vim.g.scratchpad_location 175 | if vim.g.scratchpad_daily == 1 then 176 | location = vim.g.scratchpad_daily_location .. '/' 177 | .. os.date(vim.g.scratchpad_daily_format) 178 | end 179 | 180 | api.nvim_command(prefix .. 'vsplit ' .. location) 181 | 182 | api.nvim_win_set_var(0, 'is_scratchpad', true) 183 | 184 | -- set the window sizes 185 | if n_scratchpads == 0 then 186 | set_size(nil, fn.win_getid(), true) 187 | else 188 | set_size(main_win_id, fn.win_getid(), true) 189 | end 190 | 191 | -- setup the autocommand that will close the scratchpad 192 | api.nvim_command('autocmd BufEnter lua require"scratchpad".check_if_should_close()') 193 | 194 | -- setup automatic writing 195 | api.nvim_command('setlocal autowrite') 196 | api.nvim_command('setlocal autowriteall') 197 | api.nvim_command('setlocal autoread') 198 | api.nvim_command('autocmd InsertLeave,TextChanged :w') 199 | api.nvim_command('setlocal noswapfile') 200 | 201 | -- set the filetype, syntax 202 | api.nvim_command('setlocal filetype=scratchpad') 203 | api.nvim_command('syntax match ScratchPad /.*/') 204 | 205 | -- disable virtual-text colour-column in scratchpad if lukas-reineke/virt-column.nvim is loaded 206 | local hasVC, VC = pcall(require, 'virt-column') 207 | if hasVC then 208 | VC.setup_buffer(api.nvim_get_current_buf(), { 209 | char = ' ', 210 | virtcolumn = '', 211 | }) 212 | end 213 | 214 | if vim.g.scratchpad_autofocus ~= 1 then 215 | -- set the cursor back to the main window 216 | api.nvim_set_current_win(main_win_id) 217 | end 218 | 219 | M.enabled = en_cache 220 | end 221 | 222 | -- close all scratchpads on current tab 223 | function M.close() 224 | 225 | -- there's this thing - possibly a bug, possibly intended behaviour - where 226 | -- if the current window doesn't have a FileName (i.e. was probably created 227 | -- by another plugin for temporary use, or possibly with :enew), then 228 | -- api.nvim_win_close() on an unrelated window will throw up an error. 229 | -- I first check for that here. 230 | 231 | if api.nvim_win_is_valid(M.prev_win) then 232 | local main_buf_id = api.nvim_win_get_buf(M.prev_win) 233 | local main_buf_name = api.nvim_buf_get_name(main_buf_id) 234 | if main_buf_name == '' then 235 | print('scratchpad: main window has no FileName, cannot close scratchpads') 236 | return 237 | end 238 | end 239 | 240 | 241 | 242 | for _, win_id in ipairs(windows()) do 243 | if win_id == M.prev_win then api.nvim_set_current_win(M.prev_win) end 244 | if is_scratchpad(win_id) then 245 | local buf_id = api.nvim_win_get_buf(win_id) 246 | -- close the window 247 | api.nvim_win_close(win_id, false) 248 | -- also close the underlying buffer 249 | -- (still accessible through `:bnext`, `:bprev`) 250 | api.nvim_command('bdelete ' .. buf_id) 251 | end 252 | end 253 | end 254 | 255 | 256 | -- autocommand, runs on entering a scratchpad buffer: 257 | -- close this scratchpad if all the windows are scratchpads 258 | function M.check_if_should_close() 259 | local _, non_scratchpads = count() 260 | 261 | if non_scratchpads == 0 then 262 | api.nvim_command(':q') -- necessary to close the potentially last buffer 263 | end 264 | end 265 | 266 | 267 | -- autocommand, if enabled this runs on whenever it might be necessary to resize the scratchpads 268 | function M.auto() 269 | -- if we're disabled, or currently in a scratchpad, do nothing 270 | if not M.enabled or is_scratchpad(0) then return end 271 | 272 | local s_count, _ = count() 273 | 274 | if splits() > 1 then -- more than one vertical split -> close scratchpad 275 | if s_count > 0 then M.close() end 276 | return 277 | end 278 | 279 | M.prev_win = fn.win_getid() 280 | 281 | if s_count > 1 then -- more than one scratchpad -> close and re-open 282 | 283 | M.close() 284 | M.open() 285 | 286 | elseif s_count == 1 then -- one scratchpad -> resize it (with respect to the widest non-scratchpad) 287 | 288 | local scratchpads, _ = partition() 289 | set_size(nil, scratchpads[1], false) 290 | 291 | else -- no scratchpads -> open one if there's enough space 292 | 293 | local win_info = fn.getwininfo(api.nvim_get_current_win())[1] 294 | local win_text_width = win_info.width - win_info.textoff 295 | if win_text_width > vim.g.scratchpad_textwidth + 2 * vim.g.scratchpad_minwidth then 296 | M.open() 297 | end 298 | end 299 | end 300 | 301 | return M 302 | -------------------------------------------------------------------------------- /plugin/scratchpad.vim: -------------------------------------------------------------------------------- 1 | " define a command to act as a bridge between vim and lua 2 | command! -nargs=* ScratchPad lua require('scratchpad').invoke() 3 | 4 | " defaults 5 | let g:scratchpad_autosize = get(g:, 'scratchpad_autosize', 1) 6 | let g:scratchpad_autostart = get(g:, 'scratchpad_autostart', 1) 7 | let g:scratchpad_autofocus = get(g:, 'scratchpad_autofocus', 0) 8 | 9 | let g:scratchpad_textwidth = get(g:, 'scratchpad_textwidth', 80) 10 | let g:scratchpad_minwidth = get(g:, 'scratchpad_minwidth', 12) 11 | 12 | let g:scratchpad_location = get(g:, 'scratchpad_location', '~/.scratchpad') 13 | 14 | let g:scratchpad_daily = get(g:, 'scratchpad_daily', 0) 15 | let g:scratchpad_daily_location = get(g:, 'scratchpad_daily_location', '~/.daily_scratchpad') 16 | let g:scratchpad_daily_format = get(g:, 'scratchpad_daily_format', '%Y-%m-%d') 17 | 18 | " setup auto-resize, auto-start commands 19 | autocmd BufEnter,VimResized * if g:scratchpad_autosize | execute 'lua require("scratchpad").auto()' | endif 20 | autocmd VimEnter * if g:scratchpad_autostart | execute ':ScratchPad' | endif 21 | 22 | hi ScratchPad ctermfg=239 guifg=#4e4e4e 23 | --------------------------------------------------------------------------------