├── README.md ├── UNLICENSE ├── snippets.kak └── test.kak_ /README.md: -------------------------------------------------------------------------------- 1 | # kakoune-snippets 2 | 3 | **Disclaimer**: I've recently downsized the plugin and removed functionality. This is both for making the plugin more manageable, and so that I actually want to use and work on it. If you are interested in using (or forking) this previous state, you have my blessing (not that you need it). The latest commit of this state is 9c96e64a567ae5cb16d47cf9d3a56189f77c430c. 4 | 5 | (Yet another) [kakoune](http://kakoune.org) plugin for handling snippets. 6 | 7 | [![demo](https://asciinema.org/a/217470.png)](https://asciinema.org/a/217470) 8 | 9 | ## Setup 10 | 11 | Add `snippets.kak` to your autoload dir: `~/.config/kak/autoload/`, or source it manually. 12 | 13 | This plugin requires the kakoune version `2022.10.31`. 14 | 15 | ## Usage 16 | 17 | The extension is configured via two options: 18 | * `snippets` `[str-list]` is a list of {name, trigger, command} tuples. The name is the identifier of the snippet, the trigger is a short string that identifies the snippet, and the command is what gets `eval`'d when the snippet is activated. In practice it's just a flat list that looks like `snip1-name` `snip1-trigger` `snip1-command` `snip2-name` `snip2-trigger` `snip2-command`... 19 | * `snippets_auto_expand` `[bool]` controls whether triggers are automatically expanded when they are typed in insert mode. `true` by default. 20 | 21 | Snippets can be selected manually with the commands `snippets` and `snippets-menu`. 22 | 23 | At any moment, the `snippets-info` command can be used to show the available snippets and their respective triggers. 24 | 25 | ### Triggers 26 | 27 | Snippets can be executed when a certain string is written directly in the buffer with the help of triggers. To each snippet is associated a regex which we call a trigger. 28 | 29 | Triggers can be automatically expanded by setting `snippets_auto_expand` to true, or they can be expanded manually by using the `snippets-expand-trigger` command. By default, this command tries to expand the current selection if it is a trigger, but you can also pass it an argument to select a different part of the buffer. 30 | 31 | The option `snippets_triggers_regex` can be used to help select triggers. It's a simple alternation of all triggers as a single regex. 32 | 33 | For example, this call will try to select a trigger on the current line and expand it. If it fails, the selection stays unmodified. 34 | ``` 35 | snippets-expand-trigger %{ 36 | reg / "%opt{snippets_triggers_regex}" 37 | # select to the beginning of the line, and then subselect for one of the triggers 38 | exec 'hGhs' 39 | } 40 | ``` 41 | 42 | If a snippet does not have a trigger (i.e. it's empty), you won't be able to use it via expansion, but the basic commands `snippets` and `snippets-menu` can still be used. 43 | 44 | ### Defining your own snippets 45 | 46 | Snippet commands are just regular kakoune command, so you can do just about anything in them. 47 | 48 | Ideally, your snippet command should work in both Insert and Normal mode, so that it can be used via auto-expansion and manual snippet call (be careful about this [kakoune issue](https://github.com/mawww/kakoune/issues/1916)). 49 | 50 | ### `snippets-insert` 51 | 52 | `snippets-insert` is a builtin command of the script that can be used to insert text with proper indentation and optionally move/create cursors. It accepts one argument which is the snippet to be inserted at the cursor(s). 53 | 54 | Tabs should be used for indentation when defining snippets, they will be automatically converted to the appropriate indentation level (depending on `indentwidth`) 55 | 56 | The snippet supports custom syntax to define cursor placeholders, which define the resulting selections after expansion. A cursor placeholder is defined with `${}` (empty selection) or `${text}` (default selection text). `$n` will be transformed into newlines. To use a literal `$` inside a snippet or a literal `}` inside a placeholder, double it up (`$$` and `}}`). 57 | 58 | When a snippet is inserted with `snippets-insert`, all placeholders are selected. If there are none, the entire snippet is selected. 59 | 60 | ## Changelog 61 | 62 | * Removed `snippets-directory.kak` and numbered placeholder support from `snippets-insert` (see disclaimer at the top) 63 | * `${indent}` has been removed in favor of changing leading tabs to the preferred indentation 64 | * any value can now be used as a trigger. They're regexes, so escape them accordingly 65 | * implicit `\b` are not inserted anymore before and after triggers. The internal option `%opt{snippets_expand_triggers}` has been renamed to `%opt{snippets_triggers_regex}` 66 | * `snippets_triggers` and `snippets` have been merged into a single option 67 | * triggers can now be manually expanded by calling the `snippets-expand-trigger` command on a valid trigger 68 | * `snippets_auto_expand` is now a boolean that controls whether auto-expansion of triggers is enabled 69 | * `snippets_auto_expand` was renamed to `snippets_triggers` 70 | 71 | ## FAQ 72 | 73 | ### What's the performance impact of the extension? 74 | 75 | If you use the auto-expansion feature, a runtime hook is run on each Insert mode key press. It only uses a shell scope in case of a match, and stops early otherwise. 76 | If you don't use it, there is no runtime cost (except when executing a snippet of course). 77 | 78 | ### What's with escaping, what kind of characters can I use and not use? 79 | 80 | You should be able to use anything. Triggers are currently restricted to at most 10 characters (at least for auto-expansion), but the number is arbitrary and we could raise it. 81 | 82 | ### My snippets are expanding too greedily. If I type 'before', I don't want my 'for' snippet to be expanded. 83 | 84 | You should use a stricter trigger for the snippet. For example, `\bfor` will only expand if `for` starts at a word boundary. Similarly, you can use `^` to match the start of a line. 85 | 86 | ### How did you do the demo? 87 | 88 | It's done using kitty's remote control features, a 'manuscript' and a script to bridge the two. I'll upload them at some point. 89 | 90 | ## Tests 91 | 92 | The `test.kak_` file contains tests for the plugin. To execute these tests, simply run `kak -n -e 'source test.kak_ ; quit'`: if the kakoune instance stays open, the tests have somehow failed and the current state can be inspected. 93 | 94 | ## Similar extensions 95 | 96 | https://github.com/alexherbo2/snippets.kak 97 | https://github.com/shachaf/kak/blob/master/scripts/snippet.kak 98 | 99 | ## License 100 | 101 | Unlicense 102 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /snippets.kak: -------------------------------------------------------------------------------- 1 | provide-module snippets '' 2 | require-module snippets 3 | 4 | declare-option -hidden regex snippets_triggers_regex "\A\z" # doing \A\z will always fail 5 | 6 | hook global WinSetOption 'snippets=$' %{ 7 | set window snippets_triggers_regex "\A\z" 8 | } 9 | 10 | hook global WinSetOption 'snippets=.+$' %{ 11 | set window snippets_triggers_regex %sh{ 12 | eval set -- "$kak_quoted_opt_snippets" 13 | if [ $(($#%3)) -ne 0 ]; then printf '\A\z'; exit; fi 14 | res="" 15 | while [ $# -ne 0 ]; do 16 | if [ -n "$2" ]; then 17 | if [ -z "$res" ]; then 18 | res="$2" 19 | else 20 | res="$res|$2" 21 | fi 22 | fi 23 | shift 3 24 | done 25 | if [ -z "$res" ]; then 26 | printf '\A\z' 27 | else 28 | printf '(?:%s)' "$res" 29 | fi 30 | } 31 | } 32 | 33 | define-command snippets-expand-trigger -params ..1 %{ 34 | eval -save-regs '/snc' %{ 35 | # -draft so that we don't modify anything in case of failure 36 | eval -draft %{ 37 | # ideally early out in here to avoid going to the (expensive) shell scope 38 | eval %arg{1} 39 | # this shell scope generates a block that looks like this 40 | # except with single quotes instead of %{..} 41 | # 42 | # try %{ 43 | # reg / "\Atrig1\z" 44 | # exec -draft d 45 | # reg c "snipcommand1" 46 | # } catch %{ 47 | # reg / "\Atrig2\z" 48 | # exec -draft d 49 | # reg c "snipcommand2" 50 | # } catch %{ 51 | # .. 52 | # } 53 | 54 | eval %sh{ 55 | quadrupleupsinglequotes() 56 | { 57 | rest="$1" 58 | while :; do 59 | beforequote="${rest%%"'"*}" 60 | if [ "$rest" = "$beforequote" ]; then 61 | printf %s "$rest" 62 | break 63 | fi 64 | printf "%s''''" "$beforequote" 65 | rest="${rest#*"'"}" 66 | done 67 | } 68 | 69 | eval set -- "$kak_quoted_opt_snippets" 70 | if [ $(($#%3)) -ne 0 ]; then exit; fi 71 | first=0 72 | while [ $# -ne 0 ]; do 73 | if [ -z "$2" ]; then 74 | shift 3 75 | continue 76 | fi 77 | if [ $first -eq 0 ]; then 78 | printf "try '\n" 79 | first=1 80 | else 81 | printf "' catch '\n" 82 | fi 83 | # put the trigger into %reg{/} as \Atrig\z 84 | printf "reg / ''\\\A" 85 | # we're at two levels of nested single quotes (one for try ".." catch "..", one for reg "..") 86 | # in the arbitrary user input (snippet trigger and snippet name) 87 | quadrupleupsinglequotes "$2" 88 | printf "\\\z''\n" 89 | printf "exec -draft d\n" 90 | printf "reg n ''" 91 | quadrupleupsinglequotes "$1" 92 | printf "''\n" 93 | printf "reg c ''" 94 | quadrupleupsinglequotes "$3" 95 | printf "''\n" 96 | shift 3 97 | done 98 | printf "'" 99 | } 100 | # preserve the selections generated by the snippet, since -draft will discard them 101 | eval %reg{c} 102 | reg s %val{selections_desc} 103 | } 104 | eval select %reg{s} 105 | echo "Snippet '%reg{n}' expanded" 106 | } 107 | } 108 | 109 | hook global WinSetOption 'snippets_auto_expand=false$' %{ 110 | rmhooks window snippets-auto-expand 111 | } 112 | hook global WinSetOption 'snippets_auto_expand=true$' %{ 113 | rmhooks window snippets-auto-expand 114 | hook -group snippets-auto-expand window InsertChar .* %{ 115 | try %{ 116 | snippets-expand-trigger %{ # no need to save-regs '/', since expand-trigger does that for us 117 | reg / "(%opt{snippets_triggers_regex})|." 118 | exec ';' 119 | reg / "\A(%opt{snippets_triggers_regex})\z" 120 | exec '' 121 | } 122 | } 123 | } 124 | } 125 | 126 | declare-option str-list snippets 127 | # this one must be declared after the hook, otherwise it might not be enabled right away 128 | declare-option bool snippets_auto_expand true 129 | 130 | define-command snippets-impl -hidden -params 1.. %{ 131 | eval %sh{ 132 | use=$1 133 | shift 1 134 | index=4 135 | while [ $# -ne 0 ]; do 136 | if [ "$1" = "$use" ]; then 137 | printf "eval %%arg{%s}" "$index" 138 | exit 139 | fi 140 | index=$((index + 3)) 141 | shift 3 142 | done 143 | printf "fail 'Snippet not found'" 144 | } 145 | } 146 | 147 | define-command snippets -params 1 -shell-script-candidates %{ 148 | eval set -- "$kak_quoted_opt_snippets" 149 | if [ $(($#%3)) -ne 0 ]; then exit; fi 150 | while [ $# -ne 0 ]; do 151 | printf '%s\n' "$1" 152 | shift 3 153 | done 154 | } %{ 155 | snippets-impl %arg{1} %opt{snippets} 156 | } 157 | 158 | define-command snippets-info %{ 159 | info -title Snippets %sh{ 160 | eval set -- "$kak_quoted_opt_snippets" 161 | if [ $(($#%3)) -ne 0 ]; then printf "Invalid 'snippets' value"; exit; fi 162 | if [ $# -eq 0 ]; then printf 'No snippets defined'; exit; fi 163 | maxtriglen=0 164 | while [ $# -ne 0 ]; do 165 | if [ ${#2} -gt $maxtriglen ]; then 166 | maxtriglen=${#2} 167 | fi 168 | shift 3 169 | done 170 | eval set -- "$kak_quoted_opt_snippets" 171 | while [ $# -ne 0 ]; do 172 | if [ $maxtriglen -eq 0 ]; then 173 | printf '%s\n' "$1" 174 | else 175 | if [ "$2" = "" ]; then 176 | printf "%${maxtriglen}s %s\n" "" "$1" 177 | else 178 | printf "%${maxtriglen}s ➡ %s\n" "$2" "$1" 179 | fi 180 | fi 181 | shift 3 182 | done 183 | } 184 | } 185 | 186 | define-command snippets-insert -hidden -params 1 %< 187 | eval -save-regs 's' %< 188 | eval -draft -save-regs '"' %< 189 | # paste the snippet 190 | reg dquote %arg{1} 191 | exec P 192 | 193 | # replace $n with newlines 194 | eval -draft -verbatim try %< 195 | # select $n and $$ (to avoid transforming escaped $) 196 | exec 's\$\$|\$n\$nc 197 | ' 198 | > 199 | 200 | # replace leading tabs with the appropriate indent 201 | try %< 202 | reg dquote %sh< 203 | if [ $kak_opt_indentwidth -eq 0 ]; then 204 | printf '\t' 205 | else 206 | printf "%${kak_opt_indentwidth}s" 207 | fi 208 | > 209 | exec -draft 's\A\t+s.R' 210 | > 211 | 212 | # align everything with the current line 213 | eval -draft -itersel -save-regs '"' %< 214 | try %< 215 | exec -draft -save-regs '/' '),xs^\s+y' 216 | exec -draft ')P' 217 | > 218 | > 219 | 220 | reg s %val{selections_desc} 221 | # process placeholders 222 | try %< 223 | # select all placeholders ${..} and escaped-$ (== $$) 224 | exec 's\$\$|\$\{(\}\}|[^}])*\}' 225 | # nonsense test text to check the regex 226 | # qwldqwld {qldwlqwld} qlwdl$qwld {qwdlqwld}}qwdlqwldl} 227 | # lqlwdl$qwldlqwdl$qwdlqwld {qwd$$lqwld} $qwdlqwld$ 228 | # ${asd.as.d.} lqwdlqwld $$${as.dqdqw} 229 | 230 | # remove one $ from all $$, and leading $ from ${..} 231 | exec -draft ';d' 232 | # unselect the $ 233 | exec '\A\$\z' 234 | # we're left with only {..} placeholders, process them... 235 | eval reg dquote %sh< 236 | eval set -- "$kak_quoted_selections" 237 | for sel do 238 | # remove trailing } 239 | sel="${sel%\}}" 240 | # and leading { 241 | sel="${sel#{}" 242 | # de-double }} 243 | tmp="$sel" 244 | sel="" 245 | while true; do 246 | case "$tmp" in 247 | *}}*) 248 | sel="${sel}${tmp%%\}\}*}}" 249 | tmp=${tmp#*\}\}} 250 | ;; 251 | *) 252 | sel="${sel}${tmp}" 253 | break 254 | ;; 255 | esac 256 | done 257 | # and quote the result in '..', with escaping (== doubling of ') 258 | tmp="$sel" 259 | sel="" 260 | while true; do 261 | case "$tmp" in 262 | *\'*) 263 | sel="${sel}${tmp%%\'*}''" 264 | tmp=${tmp#*\'} 265 | ;; 266 | *) 267 | sel="${sel}${tmp}" 268 | break 269 | ;; 270 | esac 271 | done 272 | # all done, print it 273 | # nothing like some good old posix-shell text processing 274 | printf "'%s' " "$sel" 275 | done 276 | > 277 | exec R 278 | reg s %val{selections_desc} 279 | > 280 | > 281 | try %{ select %reg{s} } 282 | > 283 | > 284 | 285 | define-command snippets-menu-impl -hidden -params .. %{ 286 | eval %sh{ 287 | if [ $# -eq 0 ]; then 288 | printf 'fail "No snippets defined"' 289 | exit 290 | fi 291 | if [ $(($#%3)) -ne 0 ]; then 292 | exit 293 | fi 294 | printf 'menu' 295 | i=1 296 | while [ $# -ne 0 ]; do 297 | printf ' %%arg{%s}' $i 298 | printf ' "snippets %%arg{%s}"' $i 299 | i=$((i+3)) 300 | shift 3 301 | done 302 | } 303 | } 304 | 305 | define-command snippets-menu %{ 306 | require-module menu 307 | snippets-menu-impl %opt{snippets} 308 | } 309 | -------------------------------------------------------------------------------- /test.kak_: -------------------------------------------------------------------------------- 1 | try %{ 2 | require-module snippets 3 | } catch %{ 4 | source snippets.kak 5 | require-module snippets 6 | } 7 | 8 | define-command assert-selections-are -params 1 %{ 9 | eval %sh{ 10 | if [ "$1" != "$kak_quoted_selections" ]; then 11 | printf 'bad "Check failed"' 12 | fi 13 | } 14 | } 15 | 16 | edit -scratch *snippets-test-1* 17 | 18 | 19 | # basic snippet 20 | set -add buffer snippets 'snip1' 'trig1' %{ snippets-insert %{foo bar} } 21 | exec -with-hooks 'itrig1' 22 | assert-selections-are "'foo bar'" 23 | exec '%d' 24 | 25 | # wrong trigger 26 | exec -with-hooks 'itrig2' 27 | exec '%' 28 | assert-selections-are "'trig2 29 | '" 30 | exec '%d' 31 | 32 | # multi-selection 33 | exec -with-hooks '3otrig1' 34 | assert-selections-are "'foo bar' 'foo bar' 'foo bar'" 35 | exec '%d' 36 | 37 | # adjacent multi-selection 38 | exec 'ixHs' 39 | assert-selections-are "' ' ' '" 40 | exec -with-hooks 'itrig1' 41 | assert-selections-are "'foo bar' 'foo bar'" 42 | exec '%H' 43 | assert-selections-are "'foo bar foo bar '" 44 | exec '%d' 45 | 46 | # snippet with placeholders 47 | set -add buffer snippets 'snip2' 'trig2' %{ snippets-insert %{${foo} ${bar} ${baz}} } 48 | exec -with-hooks 'itrig2' 49 | assert-selections-are "'foo' 'bar' 'baz'" 50 | exec '%H' 51 | assert-selections-are "'foo bar baz'" 52 | exec '%d' 53 | 54 | # placeholders + multi-selection 55 | exec -with-hooks '3otrig2' 56 | assert-selections-are "'foo' 'bar' 'baz' 'foo' 'bar' 'baz' 'foo' 'bar' 'baz'" 57 | exec '%d' 58 | 59 | # snippet with empty placeholders 60 | set -add buffer snippets 'snip3' 'trig3' %{ snippets-insert %{foo ${} ${} bar} } 61 | exec -with-hooks 'itrig3' 62 | assert-selections-are "' ' ' '" 63 | exec '%H' 64 | assert-selections-are "'foo bar'" 65 | exec '%d' 66 | 67 | # snippet with empty placeholders + multi-selection 68 | exec -with-hooks '3otrig3' 69 | assert-selections-are "' ' ' ' ' ' ' ' ' ' ' '" 70 | exec '%d' 71 | 72 | # snippet with escaped placeholders 73 | set -add buffer snippets 'snip4' 'trig4' %< snippets-insert % > 74 | exec -with-hooks 'itrig4' 75 | assert-selections-are "'{bar}'" 76 | exec '%H' 77 | assert-selections-are "'foo ${} {bar}'" 78 | exec '%d' 79 | 80 | # snippet alignment after insertion 81 | set -add buffer snippets 'snip5' 'trig5' %< snippets-insert % > 84 | exec -with-hooks 'i trig5' 85 | exec '%H' 86 | assert-selections-are "' a 87 | b 88 | c'" 89 | exec '%d' 90 | 91 | # respect indentwidth for indentation 92 | set -add buffer snippets 'snip6' 'trig6' %< snippets-insert % > 94 | set-option buffer indentwidth 0 95 | exec -with-hooks 'itrig6' 96 | exec '%H' 97 | assert-selections-are "'a 98 | b'" # should contain a tab 99 | exec '%d' 100 | set-option buffer indentwidth 8 101 | exec -with-hooks 'itrig6' 102 | exec '%H' 103 | assert-selections-are "'a 104 | b'" # should contain 8 spaces 105 | exec '%d' 106 | set-option buffer indentwidth 4 107 | 108 | # check transforming $n into newline 109 | set -add buffer snippets 'snip7' 'trig7' %< snippets-insert % > 110 | exec -with-hooks 'itrig7' 111 | assert-selections-are "'foo 112 | bar 113 | baz'" 114 | exec '%d' 115 | 116 | # end-to-end test with multiple features 117 | set -add buffer snippets 'snip8' 'trig8' %< snippets-insert % > 118 | exec -with-hooks '2o trig8' 119 | assert-selections-are "';' ';' ')' ' 120 | ' ';' ';' ')' ' 121 | '" 122 | exec '%H' 123 | assert-selections-are "' 124 | for (; ; ) { 125 | $ 126 | } 127 | for (; ; ) { 128 | $ 129 | }'" 130 | exec '%d' 131 | 132 | delete-buffer 133 | --------------------------------------------------------------------------------