├── .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 | 
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 | 
169 | 
170 | 
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 |
--------------------------------------------------------------------------------