├── .gitignore ├── autoload ├── macrobatics │ ├── clap.vim │ └── fzf.vim └── macrobatics.vim ├── License.md ├── doc ├── tags └── macrobatics.txt ├── plugin └── macrobatics.vim └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | # Should never check in any included builds 8 | /includedBuilds.txt 9 | 10 | -------------------------------------------------------------------------------- /autoload/macrobatics/clap.vim: -------------------------------------------------------------------------------- 1 | 2 | let s:values = v:null 3 | let s:sink = v:null 4 | 5 | function macrobatics#clap#isAvailable() 6 | return !empty(globpath(&runtimepath, "plugin/clap.vim", 1)) 7 | endfunction 8 | 9 | function macrobatics#clap#makeChoice(values, sink) 10 | let s:sink = a:sink 11 | let s:values = a:values 12 | Clap macrobatics 13 | endfunction 14 | 15 | function s:clapPlaySink(choice) 16 | call s:sink(a:choice) 17 | endfunction 18 | 19 | function s:clapSource() 20 | return s:values 21 | endfunction 22 | 23 | let g:clap_provider_macrobatics = { 24 | \ 'source': function('s:clapSource'), 25 | \ 'sink': function('s:clapPlaySink') 26 | \ } 27 | -------------------------------------------------------------------------------- /autoload/macrobatics/fzf.vim: -------------------------------------------------------------------------------- 1 | 2 | function! s:getDefaultFzfOptions() 3 | if has('nvim') || version >= 802 4 | return {'window': {'width': 0.75, 'height': 0.6}} 5 | endif 6 | return {'down': '40%'} 7 | endfunction 8 | 9 | let s:fzfOpts = get(g:, 'Mac_FzfOptions', s:getDefaultFzfOptions()) 10 | let s:sink2 = v:null 11 | 12 | function macrobatics#fzf#isAvailable() 13 | return !empty(globpath(&runtimepath, "plugin/fzf.vim", 1)) 14 | endfunction 15 | 16 | " Use an intermediate sink so we can redraw so that the fzf popup goes away 17 | function! s:sink1(choice) 18 | redraw 19 | call s:sink2(a:choice) 20 | endfunction 21 | 22 | function macrobatics#fzf#makeChoice(values, sink) 23 | let s:sink2 = a:sink 24 | let opts = { 25 | \ 'source': a:values, 26 | \ 'sink': function('s:sink1') 27 | \ } 28 | call fzf#run(extend(copy(s:fzfOpts), opts)) 29 | endfunction 30 | 31 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Steve Vermeulen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/tags: -------------------------------------------------------------------------------- 1 | g:Mac_DefaultRegister macrobatics.txt /*g:Mac_DefaultRegister* 2 | g:Mac_DisplayMacroMaxWidth macrobatics.txt /*g:Mac_DisplayMacroMaxWidth* 3 | g:Mac_MaxItems macrobatics.txt /*g:Mac_MaxItems* 4 | g:Mac_NamedMacroFileExtension macrobatics.txt /*g:Mac_NamedMacroFileExtension* 5 | g:Mac_NamedMacroFuzzySearcher macrobatics.txt /*g:Mac_NamedMacroFuzzySearcher* 6 | g:Mac_NamedMacroParameters macrobatics.txt /*g:Mac_NamedMacroParameters* 7 | g:Mac_NamedMacroParametersByFileType macrobatics.txt /*g:Mac_NamedMacroParametersByFileType* 8 | g:Mac_NamedMacrosDirectory macrobatics.txt /*g:Mac_NamedMacrosDirectory* 9 | g:Mac_SavePersistently macrobatics.txt /*g:Mac_SavePersistently* 10 | macrobatics macrobatics.txt /*macrobatics* 11 | macrobatics-configuration macrobatics.txt /*macrobatics-configuration* 12 | macrobatics-editing macrobatics.txt /*macrobatics-editing* 13 | macrobatics-faq macrobatics.txt /*macrobatics-faq* 14 | macrobatics-features macrobatics.txt /*macrobatics-features* 15 | macrobatics-file-type-macros macrobatics.txt /*macrobatics-file-type-macros* 16 | macrobatics-history macrobatics.txt /*macrobatics-history* 17 | macrobatics-installation macrobatics.txt /*macrobatics-installation* 18 | macrobatics-macro-parameters macrobatics.txt /*macrobatics-macro-parameters* 19 | macrobatics-moving-registers macrobatics.txt /*macrobatics-moving-registers* 20 | macrobatics-named-macros macrobatics.txt /*macrobatics-named-macros* 21 | macrobatics-persistent-history macrobatics.txt /*macrobatics-persistent-history* 22 | macrobatics-playback macrobatics.txt /*macrobatics-playback* 23 | macrobatics-recommended-configuration macrobatics.txt /*macrobatics-recommended-configuration* 24 | macrobatics-recording macrobatics.txt /*macrobatics-recording* 25 | macrobatics.txt macrobatics.txt /*macrobatics.txt* 26 | -------------------------------------------------------------------------------- /plugin/macrobatics.vim: -------------------------------------------------------------------------------- 1 | 2 | if exists('g:Mac_Initialized') 3 | finish 4 | endif 5 | let g:Mac_Initialized = 1 6 | 7 | nnoremap (Mac_Play) :call macrobatics#play(v:register, v:count) 8 | nnoremap (Mac_RecordNew) macrobatics#recordNew(v:register) 9 | 10 | nnoremap (Mac_Append) :call macrobatics#append(v:register, v:count) 11 | nnoremap (Mac_Prepend) :call macrobatics#prepend(v:register, v:count) 12 | 13 | nnoremap (Mac_CopyCurrentMacroToRegister) :call macrobatics#copyCurrentMacroToRegister(v:count, v:register) 14 | 15 | nnoremap (Mac_NameCurrentMacro) :call macrobatics#nameCurrentMacro() 16 | nnoremap (Mac_NameCurrentMacroForFileType) :call macrobatics#nameCurrentMacroForFileType() 17 | nnoremap (Mac_NameCurrentMacroForCurrentSession) :call macrobatics#nameCurrentMacroForCurrentSession() 18 | 19 | nnoremap (Mac_RotateBack) :call macrobatics#rotate(v:count > 0 ? v:count : 1) 20 | nnoremap (Mac_RotateForward) :call macrobatics#rotate(v:count > 0 ? -v:count : -1) 21 | 22 | nnoremap (Mac_SearchForNamedMacroAndSelect) :call macrobatics#searchThenSelectNamedMacro() 23 | nnoremap (Mac_SearchForNamedMacroAndPlay) :call macrobatics#searchThenPlayNamedMacro(v:count) 24 | nnoremap (Mac_SearchForNamedMacroAndOverwrite) :call macrobatics#searchAndOverwriteNamedMacro() 25 | nnoremap (Mac_SearchForNamedMacroAndDelete) :call macrobatics#searchAndDeleteNamedMacro() 26 | nnoremap (Mac_SearchForNamedMacroAndRename) :call macrobatics#searchAndRenameNamedMacro() 27 | 28 | command! -nargs=0 DisplayMacroHistory call macrobatics#displayHistory() 29 | command! -nargs=0 DisplayNamedMacros call macrobatics#displayNamedMacros() 30 | command! -nargs=0 ClearMacroHistory call macrobatics#clearHistory() 31 | 32 | " Deprecated 33 | command! -nargs=0 Macros call macrobatics#displayHistory() 34 | command! -nargs=0 ClearMacros call macrobatics#clearHistory() 35 | 36 | augroup _Macrobatics 37 | au! 38 | autocmd VimEnter * call macrobatics#onVimEnter() 39 | augroup END 40 | 41 | -------------------------------------------------------------------------------- /doc/macrobatics.txt: -------------------------------------------------------------------------------- 1 | *macrobatics.txt* provides additional features for playing / recording / editing macros 2 | 3 | Author: Steve Vermeulen 4 | License: MIT 5 | 6 | INTRODUCTION *macrobatics* 7 | 8 | Macrobatics is a plugin for vim/neovim with the goal of making macros easier to use. 9 | 10 | FEATURES *macrobatics-features* 11 | 12 | - Macro history, which can be navigated to play previously recorded macros 13 | - Repeatable macros with the `.` operator 14 | - Edit existing macros by appending or prepending content to it 15 | - Named macros (saved persistently) 16 | - Parameterized macros 17 | - File type specific macros 18 | - Written in pure vim-script 19 | - Nested macros (create macros that play other macros) 20 | 21 | INSTALLATION *macrobatics-installation* 22 | 23 | Install into vim using your preferred plugin manager (eg. vim-plug). 24 | 25 | Note that in order for macros to be repeatable with the `.` key, you will need to also install [tpope/vim-repeat](https://github.com/tpope/vim-repeat). 26 | 27 | Note also that this plugin contains no default mappings and will have no effect until you add your own maps to one of the `` bindings. 28 | 29 | For example, to add just the most basic functionality: 30 | > 31 | " Use to override the default bindings which wait for another key press 32 | nmap q (Mac_Play) 33 | nmap gq (Mac_RecordNew) 34 | < 35 | We choose `q` here because we don't need it anymore when using this plugin. Of course you might not want these specific bindings so you can use what makes sense for your config. 36 | 37 | RECORDING *macrobatics-recording* 38 | 39 | With the above mappings, you can then press `gq` in Vim to begin recording a new macro. 40 | 41 | However - Note that this mapping works differently than Vim's default way of recording a macro with the `q` key. Unlike `q`, which is immediately followed by the register you want to record the macro to, `gq` will always record to the same register unless a register is explicitly given (eg. `"xgq` to record the macro to the `x` register). By default this register is `m` however this can be changed in your |macrobatics-configuration|. 42 | 43 | It works this way just because specifying the register this way is more consistent with other actions in Vim like delete, yank, etc. 44 | 45 | You can then stop recording by pressing the same keys again (`gq`) 46 | 47 | PLAYBACK AND REPEAT *macrobatics-playback* 48 | 49 | Again assuming the above plug mappings, you can replay the current macro by pressing `q`. Similar to `gq`, you can also pass a register to use using the standard Vim convention (eg. `"xq` to execute the macro stored in the `x` register). And when a register is not specified, it will play whatever macro is stored in the default macro register (`m` by default but also can be changed in your |macrobatics-configuration| 50 | 51 | Assuming vim-repeat (https://github.com/tpope/vim-repeat) is installed, after playback or recording, you can use the standard repeat operator `.` to replay the same macro again in a different spot. Or, you can also execute `q` / `"xq` again for the same effect. 52 | 53 | You can also pass a count to the play command to immediately repeat the macro a given number of times. 54 | 55 | NAVIGATING HISTORY *macrobatics-history* 56 | 57 | To view the current history of macros, you can execute `:DisplayMacroHistory`. By default the history contains a maximum of 10 items, however this can be changed in |macrobatics-configuration|. You might also consider adding a binding for this: 58 | > 59 | nmap md :DisplayMacroHistory 60 | < 61 | You will notice that the current macro is displayed alongside the `m` letter (the default value for `g:Mac_DefaultRegister`) and the rest are displayed as indexes into the history buffer. 62 | 63 | To navigate the history, you can add bindings similar to the following to your `.vimrc`: 64 | > 65 | nmap [m (Mac_RotateBack) 66 | nmap ]m (Mac_RotateForward) 67 | < 68 | Then if you execute `[m` or `]m` you should see a preview of the newly selected macro in status bar. Note that you can also pass a count to the `[m` or `]m` commands. 69 | 70 | EDITING MACROS *macrobatics-editing* 71 | 72 | In many cases, after recording a macro, you realize that you would like to tweak it slightly, usually by either inserting something in the beginning or adding something to the end. Macrobatics provides two bindings to make this process very easy. For example, you could add the following bindings to your `.vimrc`: 73 | > 74 | nmap ma (Mac_Append) 75 | nmap mp (Mac_Prepend) 76 | < 77 | Then, you can append behaviour to the current macro by pressing `ma`. This will play the current macro and then immediately enter record mode to record any new content to the end of it. 78 | 79 | The prepend `mp` command works similarly except that it will enter record mode immediately, and then play the previous macro immediately after the recording is stopped. 80 | 81 | Then in both cases, the macro will be updated to contain the new change. 82 | 83 | NAMED MACROS *macrobatics-named-macros* 84 | 85 | If you find yourself re-using a macro quite often, then you might consider giving it a name, and maybe even adding a direct key mapping for it. You can do this by first adding the following mapping or similar to your `.vimrc`: 86 | > 87 | nmap mn (Mac_NameCurrentMacro) 88 | < 89 | Now, every time you create a new macro that you want to name, you can execute `mn`, and you will then be prompted to type in a name for it. Then, to add a mapping for it, you can add the following to your `.vimrc`: 90 | > 91 | nnoremap mf :call macrobatics#playNamedMacro('foo') 92 | < 93 | Where `foo` is the name that you typed into the prompt, and `tm` is the keys that you want to use for your custom macro. 94 | 95 | In many cases, you will have named macros that you don't use enough to justify adding an entirely new key binding. In these cases, it's helpful to be able to play the named macro by searching through the list of named macros whenever you need it instead. This is often easier than needing to remember a key binding for something you rarely use. You can do this by adding the following maps or similar to your `.vimrc`: 96 | > 97 | " me = macro execute 98 | nmap me (Mac_SearchForNamedMacroAndPlay) 99 | < 100 | Note that in order for these maps to work, you must either have fzf.vim (https://github.com/junegunn/fzf.vim) or vim-clap (https://github.com/liuchengxu/vim-clap) installed. If you would prefer using another fuzzy list plugin, feel free to create a github issue for it at https://github.com/svermeulen/vim-macrobatics/issues/new. 101 | 102 | Now, you can execute `me`, to directly choose the named macro you want to play! Note that you can also pass a count to this command. 103 | 104 | In some cases you might want to just select a named macro rather than playing it directly. You can do that as well with the following mapping: 105 | > 106 | " ms = macro select 107 | nmap ms (Mac_SearchForNamedMacroAndSelect) 108 | < 109 | Then you can execute `ms` to set the current macro to the chosen named macro. This is especially useful when you want to edit a named macro by appending or prepending to it (or simply overwriting it entirely). You can do this by naming it again using the same name. 110 | 111 | RECOMMENDED CONFIGURATION *macrobatics-recommended-configuration* 112 | 113 | If you decide to adopt all the recommended bindings discussed above, you can include the following in your `.vimrc`: 114 | > 115 | " Use to override the default bindings which wait for another key press 116 | nmap q (Mac_Play) 117 | nmap gq (Mac_RecordNew) 118 | 119 | nmap md :DisplayMacroHistory 120 | 121 | nmap [m (Mac_RotateBack) 122 | nmap ]m (Mac_RotateForward) 123 | 124 | nmap ma (Mac_Append) 125 | nmap mp (Mac_Prepend) 126 | 127 | " me = macro execute named 128 | nmap me (Mac_SearchForNamedMacroAndPlay) 129 | 130 | nmap ms (Mac_SearchForNamedMacroAndSelect) 131 | 132 | nmap mng (Mac_NameCurrentMacro) 133 | nmap mnf (Mac_NameCurrentMacroForFileType) 134 | < 135 | 136 | CONFIGURATION *macrobatics-configuration* 137 | 138 | This is the default configuration: 139 | > 140 | let g:Mac_DefaultRegister = 'm' 141 | let g:Mac_MaxItems = 10 142 | let g:Mac_SavePersistently = 0 143 | let g:Mac_DisplayMacroMaxWidth = 80 144 | let g:Mac_NamedMacroFileExtension = '.bin' 145 | let g:Mac_NamedMacroFuzzySearcher = v:null 146 | let g:Mac_NamedMacrosDirectory = "~/.config/macrobatics" 147 | " Note that for windows, the default is actually this: 148 | " let g:Mac_NamedMacrosDirectory = "~/AppData/Local/macrobatics" 149 | let g:Mac_NamedMacroParameters = {} 150 | let g:Mac_NamedMacroParametersByFileType = {} 151 | < 152 | Note that including these lines in your `.vimrc` will have zero effect, because these are already the default values. So you'll only need to include the lines which you customize. 153 | 154 | The values are: 155 | 156 | *g:Mac_DefaultRegister* - The default register that macros get stored to when an explicit register is not given. 157 | 158 | *g:Mac_MaxItems* - The number of macros to store in the history buffer. This will also control the number of rows displayed when executing the `:Macros` command 159 | 160 | *g:Mac_SavePersistently* - When true, the macro history will be preserved even when restarting Vim. Note: Requires Neovim. See here for details. Default: `0`. Note that this setting is only necessary for macros that are in the history buffer. Macros that you've assigned to a specific register should be automatically restored as part of built-in Vim behaviour. 161 | 162 | *g:Mac_DisplayMacroMaxWidth* - When macros are displayed by executing the `:Macros` command or when navigating history, this value will control the length at which the displayed macro is truncated at to fit on the screen. 163 | 164 | *g:Mac_NamedMacroFileExtension* - The file extension used for the macro files stored inside directory `g:Mac_NamedMacrosDirectory` 165 | 166 | *g:Mac_NamedMacroFuzzySearcher* - The type of search to use when selecting or executing named macros. Currently, valid values are 'clap' (which will use https://github.com/liuchengxu/vim-clap) and 'fzf' (which will use https://github.com/junegunn/fzf.vim) 167 | 168 | *g:Mac_NamedMacrosDirectory* - The directory to store the files associated with |macrobatics-named-macros| 169 | 170 | *g:Mac_NamedMacroParameters* - The list of |macrobatics-named-macros| associated with any macros that you want to be parameterized. 171 | 172 | *g:Mac_NamedMacroParametersByFileType* - The list of |macrobatics-named-parameters| associated with any filetype specific macros that you want to be parameterized. 173 | 174 | FILE TYPE MACROS *macrobatics-file-type-macros* 175 | 176 | In many cases you will be making macros that only apply to certain file types. You could make these named macros in the way described above, but then they would be listed for all file types. Also, you might want to use the same name for different macros depending on the file type (eg. "rename method", "create class", etc.). For these cases you can use file-specific macros. 177 | 178 | First, you will need a mapping to name the macro for the specific file type: 179 | > 180 | " nmg = name macro global 181 | nmap mng (Mac_NameCurrentMacro) 182 | " nmf = name macro file type 183 | nmap mnf (Mac_NameCurrentMacroForFileType) 184 | < 185 | Note here that we have changed the keys we used with `Mac_NameCurrentMacro` from `mn` to `mng`. 186 | 187 | Now, when we record a named macro that is file-type-specific, we can execute `mnf` and it will save to a file-type specific directory. 188 | 189 | We can then execute `ms` or `me` (assuming default mappings) and we will get both the global list of macros as well as any file-type specific macros to choose from. 190 | 191 | PERSISTENT/SHARED HISTORY *macrobatics-persistent-history* 192 | 193 | When |g:Mac_SavePersistently| is set to `1`, the macro history will be saved persistently by taking advantage of Neovim's "ShaDa" feature. Note that since ShaDa support only exists in Neovim this feature is not available for Vim. 194 | 195 | You can also use this feature to sync the macro history across multiple running instances of Vim by updating Neovim's shada file. For example, if you execute |:wshada| in the first instance and then |:rshada| in the second instance, the second instance will be synced with the macro history in the first instance. If this becomes a common operation you might consider using key bindings for this. 196 | 197 | Note also that the `!` option must be added to Neovims |shada| setting for this feature to work. For example: `set shada=!,'100,<50,s10,h` (see `:h 'shada'` for details) 198 | 199 | MACRO PARAMETERS *macrobatics-macro-parameters* 200 | 201 | Macrobatics also has built in support for using 'named parameters' with your named macros. How this works is that before recording the macro, you save parameter values into vim registers, then make use of those registers during the recording. Then, before re-playing the the macro, macrobatics will prompt the user to fill in a value for these paramters. 202 | 203 | For example, let's say you have a macro that renames the current method that you are in, and every time you run it, you want the user to supply the new name for the method. You can do this by doing the following: 204 | 205 | * Fill in a temporary value for the 'n' register that will represent the new name for the method (eg. by executing `"nyiw`) 206 | * Record the macro, making use of the 'n' register to replace the current method name 207 | * Name the current macro `rename-current-method` (see |macrobatics-named-macros|). It is now stored persistently into the macros folder. 208 | * Add the following to your `.vimrc`: 209 | > 210 | let g:Mac_NamedMacroParameters = { 211 | \ 'rename-current-method': { 'n': 'New Name' } 212 | \ } 213 | < 214 | * Restart vim, or re-source your `.vimrc` 215 | * Play the `rename-current-method` macro 216 | * You should then be prompted for a "New Name" value. The 'n' register will then be set to whatever you type here, and then the macro will be executed. 217 | 218 | Note that you can use any register in place of 'n' here, including the default `"` register. 219 | 220 | You can also add parameter information to filetype specific macros. For example: 221 | > 222 | let g:Mac_NamedMacroParametersByFileType = { 223 | \ 'js': { 224 | \ 'rename-current-method': { 'n': 'New Method Name' }, 225 | \ 'create-method': { 'n': 'Method Name' }, 226 | \ }, 227 | \ 'py': { 228 | \ 'rename-current-method': { 'n': 'New Method Name' }, 229 | \ 'create-method': { 'n': 'Method Name' }, 230 | \ }, 231 | \ } 232 | < 233 | 234 | MOVING REGISTERS *macrobatics-moving-registers* 235 | 236 | In some cases you might find yourself making use of multiple macros at once. In this case, it can be cumbersome to navigate the macro buffer history back and forth every time you want to swap the active macro between indexes in the history buffer. A better way to handle this case is to save one or more of these macros to named registers and execute them that way instead. Macrobatics provides a shortcut mapping that can do this. For example, if you add the following to your `.vimrc`: 237 | > 238 | " mc = macro copy 239 | nmap mc (Mac_CopyCurrentMacroToRegister) 240 | < 241 | Then, the next time you want to give a name to the active macro, you can execute `"xmc` where `x` is the register you want to associate with the active macro. You can then record some number of new macros by executing `gq`, while also having access to the `x` macro (which you can replay by executing `"xq`). 242 | 243 | Note that in addition to replaying the `x` macro with `"xq`, you can also re-record with `"xgq`, append with `"xma`, or prepend with `"xmp`. 244 | 245 | Note also that you might consider naming the current macro (see |macrobatics-named-macros|) instead. However, this can still be useful when juggling multiple temporary maps at once that you don't need to use again. 246 | 247 | FAQ *macrobatics-faq* 248 | 249 | Q: How do I select a specific macro from the history after executing `:DisplayMacroHistory`? 250 | 251 | A: > 252 | The easiest way to do this is to execute `x[m` where `x` is the number associated with the macro as displayed by `:DisplayMacroHistory` 253 | 254 | 255 | Q: The repeat button '.' doesn't work when executed immediately after undo 256 | 257 | A: > 258 | This is due to a bug with tpope/vim-repeat (https://github.com/tpope/vim-repeat/pull/66). You can use my fork (https://github.com/svermeulen/vim-repeat) instead which contains the fix while we wait for approval. 259 | 260 | 261 | Q: Can I execute a macro from within a macro? 262 | 263 | A: > 264 | Yes! This can be quite useful. You can do this by either triggering a named macro via a key binding, or by triggering another macro that is stored in a different register than the current macro. 265 | 266 | 267 | Q: Why did my macro stop working suddenly? 268 | 269 | A: > 270 | This was probably because a mapping that was used inside the macro was changed. One of the dangers of using macros is that it uses "recursive" mappings. In other words, macros depend heavily on the current key bindings in place at the time the macro was recorded. If you later modify one of the bindings that was used inside the macro, the macro will break. In this case you will need to re-record the macro. 271 | 272 | 273 | Q: Why should I use a named macro for a custom key map? Why can't I just directly map to the contents of the macro register? 274 | 275 | A: 276 | > 277 | Yes, this approach usually works as well. Assuming the macro you want to bind is stored in the `m` register, you can accomplish this by adding the following to your `.vimrc`: 278 | 279 | nmap t [MACRO CONTENTS] 280 | 281 | Note that we need to use nmap here in case our macro uses any non-default mappings. To actually fill in the value for `[MACRO CONTENTS]`, you can paste from the `m` register like this: 282 | 283 | nmap t ^R^Rm 284 | 285 | We type `^R^Rm` to paste the raw values from the macro. Alternatively, you could create a function for your macro instead: 286 | 287 | function s:doSomething() 288 | normal [MACRO CONTENTS] 289 | endfunction 290 | 291 | nnoremap t :call doSomething() 292 | 293 | However, dependending on your platform and the types of key presses used during the macro, it may not be possible to represent the macro correctly as text inside your `.vimrc`. This is why it's often easier and more reliable to use named macros instead (see |macrobatics-named-macros|) which do not suffer from this problem (because named macros are stored into binary files) 294 | 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Macrobatics.vim 5 | 6 | Macrobatics is a plugin for vim/neovim with the goal of making vim macros easier to use. 7 | 8 | ## Features 9 | 10 | * Macro history, which can be navigated to play previously recorded macros 11 | * Repeatable macros with the `.` operator 12 | * Edit existing macros by appending or prepending content to it 13 | * Named macros (saved persistently) 14 | * Parameterized macros 15 | * File type specific macros 16 | * Written in pure vim-script 17 | * Nested macros (create macros that play other macros) 18 | 19 | ## Installation 20 | 21 | Install into vim using your preferred plugin manager (eg. [vim-plug](https://github.com/junegunn/vim-plug)). 22 | 23 | Note that in order for macros to be repeatable with the `.` key, you will need to also install [tpope/vim-repeat](https://github.com/tpope/vim-repeat). 24 | 25 | Note also that this plugin contains no default mappings and will have no effect until you add your own maps to one of the `` bindings. 26 | 27 | For example, to add just the most basic functionality: 28 | 29 | ```viml 30 | " Use to override the default bindings which wait for another key press 31 | nmap q (Mac_Play) 32 | nmap gq (Mac_RecordNew) 33 | ``` 34 | 35 | We choose `q` here because we don't need it anymore when using this plugin. Of course you might not want these specific bindings so you can use what makes sense for your config. 36 | 37 | ## Recording 38 | 39 | With the above mappings, you can then press `gq` in Vim to begin recording a new macro. 40 | 41 | However - Note that this mapping works differently than Vim's default way of recording a macro with the `q` key. Unlike `q`, which is immediately followed by the register you want to record the macro to, `gq` will always record to the same register unless a register is explicitly given (eg. `"xgq` to record the macro to the `x` register). By default this register is `m` however this is [configurable](#configuration). 42 | 43 | It works this way just because specifying the register this way is more consistent with other actions in Vim like delete, yank, etc. 44 | 45 | You can then stop recording by pressing the same keys again (`gq`) 46 | 47 | ## Playback and repeat 48 | 49 | Again assuming the above plug mappings, you can replay the current macro by pressing `q`. Similar to `gq`, you can also pass a register to use using the standard Vim convention (eg. `"xq` to execute the macro stored in the `x` register). And when a register is not specified, it will play whatever macro is stored in the default macro register (`m` by default but also [configurable](#configuration)) 50 | 51 | Assuming [vim-repeat](https://github.com/tpope/vim-repeat) is installed, after playback or recording, you can use the standard repeat operator `.` to replay the same macro again in a different spot. Or, you can also execute `q` / `"xq` again for the same effect. 52 | 53 | You can also pass a count to the play command to immediately repeat the macro a given number of times. 54 | 55 | ## Navigating history 56 | 57 | To view the current history of macros, you can execute `:DisplayMacroHistory`. By default the history contains a maximum of 10 items, however this is [configurable](#configuration). You might also consider adding a binding for this: 58 | 59 | ```viml 60 | nmap mh :DisplayMacroHistory 61 | ``` 62 | 63 | You will notice that the current macro is displayed alongside the `m` letter (the default value for `g:Mac_DefaultRegister`) and the rest are displayed as indexes into the history buffer. 64 | 65 | To navigate the history, you can add bindings similar to the following to your `.vimrc`: 66 | 67 | ```viml 68 | nmap [m (Mac_RotateBack) 69 | nmap ]m (Mac_RotateForward) 70 | ``` 71 | 72 | Then if you execute `[m` or `]m` you should see a preview of the newly selected macro in status bar. Note that you can also pass a count to the `[m` or `]m` commands. 73 | 74 | ## Editing Macros 75 | 76 | In many cases, after recording a macro, you realize that you would like to tweak it slightly, usually by either inserting something in the beginning or adding something to the end. Macrobatics provides two bindings to make this process very easy. For example, you could add the following bindings to your `.vimrc`: 77 | 78 | ```viml 79 | nmap ma (Mac_Append) 80 | nmap mp (Mac_Prepend) 81 | ``` 82 | 83 | Then, you can append behaviour to the current macro by pressing `ma`. This will play the current macro and then immediately enter record mode to record any new content to the end of it. 84 | 85 | The prepend `mp` command works similarly except that it will enter record mode immediately, and then play the previous macro immediately after the recording is stopped. 86 | 87 | Then in both cases, the macro will be updated to contain the new change. 88 | 89 | ## Named Macros 90 | 91 | If you find yourself re-using a macro quite often, then you might consider giving it a name, and maybe even adding a direct key mapping for it. You can do this by first adding the following mapping or similar to your `.vimrc`: 92 | 93 | ```viml 94 | nmap mn (Mac_NameCurrentMacro) 95 | ``` 96 | 97 | Now, every time you create a new macro that you want to name, you can execute `mn`, and you will then be prompted to type in a name for it. You will also be prompted for whether you want to [named parameters](#parameterized-macros) to which you can respond no for now. 98 | 99 | Then, to add a mapping for it, you can add the following to your `.vimrc`: 100 | 101 | ```viml 102 | nnoremap mf :call macrobatics#playNamedMacro('foo') 103 | ``` 104 | 105 | Where `foo` is the name that you typed into the prompt, and `tm` is the keys that you want to use for your custom macro. 106 | 107 | ## Playing/Selecting Named Macros Directly 108 | 109 | In many cases, you will have named macros that you don't use enough to justify adding an entirely new key binding. In these cases, it's helpful to be able to play the named macro by searching through the list of named macros whenever you need it instead. This is often easier than needing to remember a key binding for something you rarely use. You can do this by adding the following maps or similar to your `.vimrc`: 110 | 111 | ```viml 112 | " me = macro execute 113 | nmap me (Mac_SearchForNamedMacroAndPlay) 114 | ``` 115 | 116 | Note that in order for these maps to work, you must either have [fzf.vim](https://github.com/junegunn/fzf.vim) or [vim-clap](https://github.com/liuchengxu/vim-clap) installed. If you would prefer using another fuzzy list plugin, feel free to [create a github issue for it](https://github.com/svermeulen/vim-macrobatics/issues/new). 117 | 118 | Now, you can execute `me`, to directly choose the named macro you want to play! Note that you can also pass a count to this command. 119 | 120 | In some cases you might want to just select a named macro rather than playing it directly. You can do that as well with the following mapping: 121 | 122 | ```viml 123 | " ms = macro select 124 | nmap ms (Mac_SearchForNamedMacroAndSelect) 125 | ``` 126 | 127 | Then you can execute `ms` to set the current macro to the chosen named macro. This is especially useful when you want to edit a named macro by appending or prepending to it (or simply overwriting it entirely). You can do this by naming it again using the same name as described above. 128 | 129 | ## Updating Named Macros 130 | 131 | In many cases you'll want to update some of your saved macros. You could do this by executing `(Mac_NameCurrentMacro)` and typing in the exact name of the macro, but this can be error prone. As an alternative, you can use the fuzzy finder popup to select the macro that you intend to overwrite: 132 | 133 | ```viml 134 | " mo = macro overwrite 135 | nmap mo (Mac_SearchForNamedMacroAndOverwrite) 136 | ``` 137 | 138 | ## Deleting Named Macros 139 | 140 | Simiarly, you might want to bind a key to delete saved macros using the fuzzy finder: 141 | 142 | ```viml 143 | " md = macro delete 144 | nmap md (Mac_SearchForNamedMacroAndDelete) 145 | ``` 146 | 147 | Or rename the macro: 148 | 149 | ```viml 150 | " mr = macro rename 151 | nmap mr (Mac_SearchForNamedMacroAndRename) 152 | ``` 153 | 154 | ## Recommended configuration 155 | 156 | If you decide to adopt all the recommended bindings discussed above, you can include the following in your `.vimrc`: 157 | 158 | ```viml 159 | " Use to override the default bindings which wait for another key press 160 | nmap q (Mac_Play) 161 | nmap gq (Mac_RecordNew) 162 | 163 | nmap mh :DisplayMacroHistory 164 | 165 | nmap [m (Mac_RotateBack) 166 | nmap ]m (Mac_RotateForward) 167 | 168 | nmap ma (Mac_Append) 169 | nmap mp (Mac_Prepend) 170 | 171 | nmap mng (Mac_NameCurrentMacro) 172 | nmap mnf (Mac_NameCurrentMacroForFileType) 173 | nmap mns (Mac_NameCurrentMacroForCurrentSession) 174 | 175 | nmap mo (Mac_SearchForNamedMacroAndOverwrite) 176 | nmap mr (Mac_SearchForNamedMacroAndRename) 177 | nmap md (Mac_SearchForNamedMacroAndDelete) 178 | nmap me (Mac_SearchForNamedMacroAndPlay) 179 | nmap ms (Mac_SearchForNamedMacroAndSelect) 180 | ``` 181 | 182 | ## Configuration 183 | 184 | This is the default configuration: 185 | 186 | ```viml 187 | let g:Mac_DefaultRegister = 'm' 188 | let g:Mac_PlayByNameRegister = 'n' 189 | let g:Mac_MaxItems = 10 190 | let g:Mac_SavePersistently = 0 191 | let g:Mac_DisplayMacroMaxWidth = 80 192 | let g:Mac_NamedMacroFileExtension = '.bin' 193 | let g:Mac_NamedMacroFuzzySearcher = v:null 194 | let g:Mac_NamedMacrosDirectory = "~/.config/macrobatics" 195 | " Note that for windows, the default is actually this: 196 | " let g:Mac_NamedMacrosDirectory = "~/AppData/Local/macrobatics" 197 | let g:Mac_NamedMacroParameters = {} 198 | let g:Mac_NamedMacroParametersByFileType = {} 199 | let g:Mac_FzfOptions = {'window': {'width': 0.75, 'height': 0.6}} 200 | " Or for vim < 8: 201 | " let g:Mac_FzfOptions = {'down': '40%'} 202 | ``` 203 | 204 | Note that including these lines in your `.vimrc` will have zero effect, because these are already the default values. So you'll only need to include the lines which you customize. 205 | 206 | The values are: 207 | * `g:Mac_DefaultRegister` - The default register that macros get stored to when an explicit register is not given. 208 | * `g:Mac_PlayByNameRegister` - The default register that macros get stored to when triggering named macros via either `macrobatics#playNamedMacro` or `(Mac_SearchForNamedMacroAndPlay)`. This is a different value from `Mac_DefaultRegister` so that `macrobatics#playNamedMacro` or `(Mac_SearchForNamedMacroAndPlay)` can themselves appear inside other named macros, without clobbering the current macro register. 209 | * `g:Mac_MaxItems` - The number of macros to store in the history buffer. This will also control the number of rows displayed when executing the `:Macros` command 210 | * `g:Mac_SavePersistently` - When true, the macro history will be preserved even when restarting Vim. Note: Requires Neovim. See here for details. Default: `0`. Note that this setting is only necessary for macros that are in the history buffer. Macros that you've assigned to a specific register should be automatically restored as part of built-in Vim behaviour. 211 | * `g:Mac_DisplayMacroMaxWidth` - When macros are displayed by executing the `:Macros` command or when navigating history, this value will control the length at which the displayed macro is truncated at to fit on the screen. 212 | * `g:Mac_NamedMacroFileExtension` - The file extension used for the macro files stored inside directory `g:Mac_NamedMacrosDirectory` 213 | * `g:Mac_NamedMacroFuzzySearcher` - The type of search to use when selecting or executing named macros. Currently, valid values are 'clap' (which will use [vim-clap](https://github.com/liuchengxu/vim-clap)) and 'fzf' (which will use [fzf.vim](https://github.com/junegunn/fzf.vim)). You can also define your own custom fuzzy searcher, by adding a file to vim's runtime path at `autoload/macrobatics/X.vim` and then use 'X' for `g:Mac_NamedMacroFuzzySearcher`. See `autoload/macrobatics/fzf.vim` for an example. 214 | * `g:Mac_NamedMacrosDirectory` - The directory to store the files associated with [named macros](#named-macros) 215 | * `g:Mac_NamedMacroParameters` - The list of [named parameters](#parameterized-macros) associated with any macros that you want to be parameterized. 216 | * `g:Mac_NamedMacroParametersByFileType` - The list of [named parameters](#parameterized-macros) associated with any filetype specific macros that you want to be parameterized. 217 | * `g:Mac_FzfOptions` - The options used to configure the fzf window that is used to choose named macros / named parameters. 218 | 219 | ## FAQ 220 | 221 | * ### _How do I select a specific macro from the history after executing `:DisplayMacroHistory`?_ 222 | 223 | The easiest way to do this is to execute `x[m` where `x` is the number associated with the macro as displayed by `:DisplayMacroHistory` 224 | 225 | 226 | * ### _The repeat button '.' doesn't work when executed immediately after undo_ 227 | 228 | This is due to a [bug with tpope/vim-repeat](https://github.com/tpope/vim-repeat/pull/66). If this issue bothers you, you can use [my fork](https://github.com/svermeulen/vim-repeat) of vim-repeat instead which contains the fix. 229 | 230 | 231 | * ### _Can I execute a macro from within a macro?_ 232 | 233 | Yes! This can be quite useful. You can do this by either triggering a named macro via a key binding, or by triggering another macro that is stored in a different register than the current macro. 234 | 235 | 236 | * ### _Why did my macro stop working suddenly?_ 237 | 238 | This was probably because a mapping that was used inside the macro was changed. One of the dangers of using macros is that it uses "recursive" mappings. In other words, macros depend heavily on the current key bindings in place at the time the macro was recorded. If you later modify one of the bindings that was used inside the macro, the macro will break. In this case you will need to re-record the macro. 239 | 240 | 241 | * ### _Why should I use a named macro for a custom key map? Why can't I just directly map to the contents of the macro register?_ 242 | 243 | Yes, this approach usually works as well. Assuming the macro you want to bind is stored in the `m` register, you can accomplish this by adding the following to your `.vimrc`: 244 | 245 | ```viml 246 | nmap t [MACRO CONTENTS] 247 | ``` 248 | 249 | Note that we need to use nmap here in case our macro uses any non-default mappings. To actually fill in the value for `[MACRO CONTENTS]`, you can paste from the `m` register like this: 250 | 251 | ```viml 252 | nmap t ^R^Rm 253 | ``` 254 | 255 | We type `^R^Rm` to paste the raw values from the macro. Alternatively, you could create a function for your macro instead: 256 | 257 | ```viml 258 | function s:doSomething() 259 | normal [MACRO CONTENTS] 260 | endfunction 261 | 262 | nnoremap t :call doSomething() 263 | ``` 264 | 265 | However, depending on your platform and the types of key presses used during the macro, it may not be possible to represent the macro correctly as text inside your `.vimrc`. This is why it's often easier and more reliable to use [named macros instead](#named-macros) which do not suffer from this problem (because named macros are stored into binary files) 266 | 267 | # Advanced Topics 268 | 269 | ## Temporary Named Macros 270 | 271 | In some cases you might have a macro that is very ad-hoc and only really needed during your current Vim session. In these cases you don't want to save it as a global macro, so you can use a binding like the following instead: 272 | 273 | ```viml 274 | nmap mns (Mac_NameCurrentMacroForCurrentSession) 275 | ``` 276 | 277 | ## File Type Macros 278 | 279 | In many cases you will be making macros that only apply to certain file types. You could make these named macros in the way described above, but then they would be listed for all file types. Also, you might want to use the same name for different macros depending on the file type (eg. "rename method", "create class", etc.). For these cases you can use file-specific macros. 280 | 281 | First, you will need a mapping to name the macro for the specific file type: 282 | 283 | ```viml 284 | " nmg = name macro global 285 | nmap mng (Mac_NameCurrentMacro) 286 | " nmf = name macro file type 287 | nmap mnf (Mac_NameCurrentMacroForFileType) 288 | ``` 289 | 290 | Note here that we have changed the keys we used with `Mac_NameCurrentMacro` from `mn` to `mng`. 291 | 292 | Now, when we record a named macro that is file-type-specific, we can execute `mnf` and it will save to a file-type specific directory. 293 | 294 | We can then execute `ms` or `me` (assuming recommended mappings) and we will get both the global list of macros as well as any file-type specific macros to choose from. 295 | 296 | ## Persistent/Shared History 297 | 298 | When `g:Mac_SavePersistently` is set to `1`, the macro history will be saved persistently by taking advantage of Neovim's "ShaDa" feature. Note that since ShaDa support only exists in Neovim this feature is not available for Vim. 299 | 300 | You can also use this feature to sync the macro history across multiple running instances of Vim by updating Neovim's shada file. For example, if you execute `:wshada` in the first instance and then `:rshada` in the second instance, the second instance will be synced with the macro history in the first instance. If this becomes a common operation you might consider using key bindings for this. 301 | 302 | Note also that the `!` option must be added to Neovims `shada` setting for this feature to work. For example: `set shada=!,'100,<50,s10,h` (see `:h 'shada'` for details) 303 | 304 | ## Nested Named Macros 305 | 306 | If you use this plugin for awhile, you might have some number of bindings that looks like this: 307 | 308 | ```viml 309 | nnoremap tf :call macrobatics#playNamedMacro('foo') 310 | nnoremap tb :call macrobatics#playNamedMacro('bar') 311 | ``` 312 | 313 | In some cases, you might even record macros where you trigger these bindings. This is one way to trigger nested named macros. Note that this should Just Work when doing it this way. 314 | 315 | Often though, you won't have keys mapped to named macros and will rely instead on the fuzzy finder popup to select them when you need them. You might then wonder, can you record a macro in which you trigger the fuzzy finder popup, select another macro, and execute that? The answer is yes, with an important caveat: You have to type in the exact name of the macro for this to work. In other words, when recording a macro where you execute either `(Mac_SearchForNamedMacroAndSelect)` or `(Mac_SearchForNamedMacroAndPlay)`, you cannot rely on the fuzzy search selection. You have to type in the exact name and then hit enter. If you follow this rule, and then play back the macro, you can have as many nested macros as you like. 316 | 317 | ## Parameterized Macros 318 | 319 | Macrobatics also has built in support for using 'named parameters' with your named macros. How this works is that when recording the macro, you make use of certain register values as part of the macro. Then, when the macro is named and saved, you can indicate which registers need to be filled in before the macro is played. For example, when executing `mng` (assuming the default mappings from above), after you choose a name for your macro, it will then also prompt you with the text "Add parameters?". If you type `Y`, you can then indicate which registers need to be filled in and also give a name for each required parameter. 320 | 321 | Then, the next time you execute this macro, you will be prompted to fill in values for all these parameters. 322 | 323 | ## Specifying Parameter Values 324 | 325 | Instead of always relying on the user to provide values for macro parameters, you can also provide these values via script as well. For example: 326 | 327 | ```viml 328 | nnoremap mf :call macrobatics#playNamedMacro('foo', 1, {'e', 'bar'}) 329 | ``` 330 | 331 | In the above case, we are playing the macro named 'foo' 1 time and overridding the parameter with register 'e' to have value 'bar'. Note that the user will be prompted for any other registers not provideded. 332 | 333 | ## Specifying Parameter Values Option 2 334 | 335 | One problem with providing parameter values as part of the call to `playNamedMacro` is that these parameter values will not be used when the macro is executed via the fuzzy search prompt. Therefore an alternative way to specify parameter information is given below: 336 | 337 | For example, let's say you have a macro that renames the current method that you are in, and every time you run it, you want the user to supply the new name for the method. You can add this macro by doing the following: 338 | 339 | * Fill in a temporary value for the 'x' register that will represent the new name for the method (eg. by executing `"nyiw`) 340 | * Record the macro, making use of the 'x' register to replace the current method name 341 | * Name the current macro `rename-current-method` as [described above](#named-macros). It is now stored persistently into the macros folder. 342 | * Add the following to your `.vimrc`: 343 | ```viml 344 | let g:Mac_NamedMacroParameters = { 345 | \ 'rename-current-method': [ { 'register': 'x', 'name': 'New Name' } ] 346 | \ } 347 | ``` 348 | * Restart vim, or re-source your `.vimrc` 349 | * Play the `rename-current-method` macro 350 | * You should then be prompted for a "New Name" value. The 'x' register will then be set to whatever you type here, and then the macro will be executed. 351 | 352 | Note that you can use any register in place of 'x' here, including the default `"` register. 353 | 354 | You can also add parameter information to filetype specific macros. For example: 355 | ```viml 356 | let g:Mac_NamedMacroParametersByFileType = { 357 | \ 'javascript': { 358 | \ 'rename-current-method': [ {'register': 'x', 'name': 'New Method Name'} ], 359 | \ 'create-method': [ {'register': 'x', 'name': 'Method Name'} ], 360 | \ }, 361 | \ 'python': { 362 | \ 'rename-current-method': [ {'register': 'x', 'name': 'New Method Name'} ], 363 | \ 'create-method': [ {'register': 'x', 'name': 'Method Name'} ], 364 | \ }, 365 | \ } 366 | ``` 367 | 368 | In many cases you will just need to assign a name to the register, and then let macrobatics prompt the user for the value, as shown above. However, there are some cases where it is more useful to let the user choose from a list of pre-defined values, or have the value for a register come from a custom vimscript function that you define yourself. You can refer to the following examples to learn how these kinds of parameterized macros can be defined: 369 | 370 | ```viml 371 | " An example of using a hard-coded value 372 | let g:Mac_NamedMacroParameters = { 373 | \ 'my-custom-macro1': [{ 374 | \ 'register': 'x', 375 | \ 'name': 'foo', 376 | \ 'value': 'bar' 377 | \ }], 378 | \ } 379 | 380 | " An example of using a list of values 381 | " This will trigger either fzf or vim-clap to choose a value in the given list 382 | let g:Mac_NamedMacroParameters = { 383 | \ 'my-custom-macro2': [{ 384 | \ 'register': 'x', 385 | \ 'name': 'foo', 386 | \ 'choices': ['bar', 'qux', 'gorp'] 387 | \ }], 388 | \ } 389 | 390 | " An example of using a custom function to retrieve the value 391 | function! s:getFoo(argName) 392 | return 'bar' 393 | endfunction 394 | 395 | let g:Mac_NamedMacroParameters = { 396 | \ 'my-custom-macro3': [{ 397 | \ 'register': 'x', 398 | \ 'name': 'foo', 399 | \ 'valueProvider': function('s:getFoo') 400 | \ }], 401 | \ } 402 | 403 | " An example of using a custom async function to retrieve the value 404 | " Note that we can also just not call the sink function which will just cancel 405 | function! s:getFoo(argName, sink) 406 | call a:sink('bar') 407 | endfunction 408 | 409 | let g:Mac_NamedMacroParameters = { 410 | \ 'my-custom-macro3': [{ 411 | \ 'register': 'x', 412 | \ 'name': 'foo', 413 | \ 'is_async': 1, 414 | \ 'valueProvider': function('s:getFoo') 415 | \ }], 416 | \ } 417 | 418 | " An example of using a custom function to retrieve the list of choices 419 | " This will trigger either fzf or vim-clap to choose a value in the given list 420 | function! s:getFooChoices(argName) 421 | return ['foo', 'bar', 'qux'] 422 | endfunction 423 | 424 | let g:Mac_NamedMacroParameters = { 425 | \ 'my-custom-macro3': [{ 426 | \ 'register': 'x', 427 | \ 'name': 'foo', 428 | \ 'choicesProvider': function('s:getFooChoices') 429 | \ }], 430 | \ } 431 | 432 | " An example of using a custom async function to retrieve the list of choices 433 | " This will trigger either fzf or vim-clap to choose a value in the given list 434 | " Note that we can also just not call the sink function which will just cancel 435 | function! s:getFooChoices(argName, sink) 436 | call a:sink(['foo', 'bar', 'qux']) 437 | endfunction 438 | 439 | let g:Mac_NamedMacroParameters = { 440 | \ 'my-custom-macro3': [{ 441 | \ 'register': 'x', 442 | \ 'name': 'foo', 443 | \ 'is_async': 1, 444 | \ 'choicesProvider': function('s:getFooChoices') 445 | \ }], 446 | \ } 447 | ``` 448 | 449 | ## Moving registers 450 | 451 | In some cases you might find yourself making use of multiple macros at once. In this case, it can be cumbersome to navigate the macro buffer history back and forth every time you want to swap the active macro between indexes in the history buffer. A better way to handle this case is to save one or more of these macros to named registers and execute them that way instead. Macrobatics provides a shortcut mapping that can do this. For example, if you add the following to your `.vimrc`: 452 | 453 | ```viml 454 | " mc = macro copy 455 | nmap mc (Mac_CopyCurrentMacroToRegister) 456 | ``` 457 | 458 | Then, the next time you want to give a name to the active macro, you can execute `"xmc` where `x` is the register you want to associate with the active macro. You can then record some number of new macros by executing `gq`, while also having access to the `x` macro (which you can replay by executing `"xq`). 459 | 460 | Note that in addition to replaying the `x` macro with `"xq`, you can also re-record with `"xgq`, append with `"xma`, or prepend with `"xmp`. 461 | 462 | Note also that you might consider [naming the current macro](#named-macros) instead. However, this can still be useful when juggling multiple temporary maps at once that you don't need to use again. 463 | 464 | -------------------------------------------------------------------------------- /autoload/macrobatics.vim: -------------------------------------------------------------------------------- 1 | 2 | let s:defaultMacroReg = get(g:, 'Mac_DefaultRegister', 'm') 3 | let s:playByNameMacroReg = get(g:, 'Mac_PlayByNameRegister', 'n') 4 | let s:maxItems = get(g:, 'Mac_MaxItems', 10) 5 | let s:saveHistoryToShada = get(g:, 'Mac_SavePersistently', 0) 6 | let s:displayMacroMaxWidth = get(g:, 'Mac_DisplayMacroMaxWidth', 80) 7 | let s:macroFileExtension = get(g:, 'Mac_NamedMacroFileExtension', '.bin') 8 | let s:macroParameterInfoFileExtension = get(g:, 'Mac_NamedMacroParameterInfoFileExtension', '.txt') 9 | let s:fuzzySearcher = get(g:, 'Mac_NamedMacroFuzzySearcher', v:null) 10 | let s:globalNamedMacrosSaveDirectory = v:null 11 | let s:defaultFuzzySearchers = ['fzf', 'clap'] 12 | let s:previousCompleteOpt=v:null 13 | let s:autoFinishRecordAfterPlay = 0 14 | let s:namedMacroCache = {} 15 | let s:namedMacrosForSession = {} 16 | let s:namedMacroParamInfosForSession = {} 17 | let s:macrosInProgress = 0 18 | let s:repeatMacro = v:null 19 | let s:isRecording = 0 20 | let s:recordInfo = v:null 21 | let s:queuedMacroInfo = v:null 22 | 23 | nnoremap (Mac__OnPlayMacroCompleted) :call onPlayMacroCompleted() 24 | nnoremap (Mac__RepeatLast) :call repeatLast() 25 | 26 | if s:saveHistoryToShada 27 | if !exists("g:MACROBATICS_HISTORY") 28 | let g:MACROBATICS_HISTORY = [] 29 | endif 30 | 31 | if !has("nvim") 32 | echoerr "Neovim is required when setting g:Mac_SavePersistently to 1" 33 | elseif &shada !~ '\V!' 34 | echoerr "Must enable global variable support by including ! in the shada property when setting g:Mac_SavePersistently to 1. See macrobatics documentation for details or run :help 'shada'." 35 | endif 36 | else 37 | let s:history = [] 38 | " If the setting is off then clear it to not keep taking up space 39 | let g:MACROBATICS_HISTORY = [] 40 | endif 41 | 42 | function! macrobatics#getRecordRegister() 43 | return s:defaultMacroReg 44 | endfunction 45 | 46 | function! macrobatics#isRecording() 47 | return s:isRecording 48 | endfunction 49 | 50 | function! macrobatics#isPlayingMacro() 51 | return s:macrosInProgress > 0 52 | endfunction 53 | 54 | function! macrobatics#getHistory() 55 | if s:saveHistoryToShada 56 | return g:MACROBATICS_HISTORY 57 | endif 58 | 59 | return s:history 60 | endfunction 61 | 62 | function! macrobatics#setCurrent(entry) 63 | call s:updateMacroReg(s:defaultMacroReg, a:entry) 64 | call s:addToHistory(a:entry) 65 | endfunction 66 | 67 | function! macrobatics#displayNamedMacros() 68 | echohl WarningMsg | echo "--- Named Macros ---" | echohl None 69 | for name in macrobatics#getNamedMacros() 70 | echo name 71 | endfor 72 | endfunction 73 | 74 | function! macrobatics#displayHistory() 75 | echohl WarningMsg | echo "--- Macro History ---" | echohl None 76 | let i = 0 77 | for macro in macrobatics#getHistory() 78 | call s:displayMacro(macro, i) 79 | let i += 1 80 | endfor 81 | endfunction 82 | 83 | function! s:getNamedMacrosInfo() 84 | let result = [] 85 | let nameSet = {} 86 | 87 | for name in keys(s:namedMacrosForSession) 88 | if !has_key(nameSet, name) 89 | call add(result, {'name': name, 'type':'s'}) 90 | let nameSet[name] = 1 91 | endif 92 | endfor 93 | 94 | let dirMap = { 95 | \ 'b': s:getBufferLocalNamedMacrosDirs(), 96 | \ 'f': s:getFileTypeNamedMacrosDirs(), 97 | \ 'g': [macrobatics#getGlobalNamedMacrosDir()] 98 | \ } 99 | 100 | for dirType in keys(dirMap) 101 | for dir in dirMap[dirType] 102 | for filePath in globpath(dir, "*" . s:macroFileExtension, 0, 1) 103 | let name = s:getMacroNameFromPath(filePath) 104 | if !has_key(nameSet, name) 105 | call add(result, {'name': name, 'type':dirType}) 106 | let nameSet[name] = 1 107 | endif 108 | endfor 109 | endfor 110 | endfor 111 | return result 112 | endfunction 113 | 114 | function! macrobatics#getNamedMacros() 115 | let result = [] 116 | for info in s:getNamedMacrosInfo() 117 | call add(result, info.name) 118 | endfor 119 | return result 120 | endfunction 121 | 122 | function! macrobatics#copyCurrentMacroToRegister(cnt, reg) 123 | if a:cnt == 0 124 | let content = getreg(s:defaultMacroReg) 125 | else 126 | let history = macrobatics#getHistory() 127 | let content = history[a:cnt] 128 | endif 129 | 130 | call s:updateMacroReg(a:reg, content) 131 | call s:echo("Stored to '%s' register: %s", a:reg, s:formatMacro(content)) 132 | endfunction 133 | 134 | function! macrobatics#getGlobalNamedMacrosDir() 135 | if s:globalNamedMacrosSaveDirectory is v:null 136 | let s:globalNamedMacrosSaveDirectory = s:chooseGlobalMacroSaveDirectory() 137 | endif 138 | return s:globalNamedMacrosSaveDirectory 139 | endfunction 140 | 141 | function! macrobatics#saveCurrentMacroToDirectory(dirPath) 142 | call s:saveCurrentMacroToDirectory(resolve(expand(a:dirPath))) 143 | endfunction 144 | 145 | function! macrobatics#nameCurrentMacroForCurrentSession() 146 | let name = input('Macro Name:') 147 | if len(name) == 0 148 | echo "Save macro cancelled" 149 | return 150 | endif 151 | " Without this the echo below appears on the same line as input 152 | echo "\r" 153 | let overwriteParams = v:true 154 | if has_key(s:namedMacrosForSession, name) 155 | if confirm(printf( 156 | \ "Found existing macro with name '%s'. Overwrite?", name), 157 | \ "&Yes\n&No", 2, "Question") != 1 158 | " Any response except yes is viewed as a cancel 159 | echo "Save macro cancelled" 160 | return 161 | endif 162 | if has_key(s:namedMacroParamInfosForSession, name) 163 | let choice = confirm("Re-use previously saved parameter settings?", "&Yes\n&No", 2, "Question") 164 | if choice == 0 165 | echo "Save macro cancelled" 166 | return 167 | endif 168 | if choice == 1 169 | let overwriteParams = v:false 170 | endif 171 | endif 172 | endif 173 | let s:namedMacrosForSession[name] = getreg(s:defaultMacroReg) 174 | if overwriteParams 175 | let s:namedMacroParamInfosForSession[name] = s:promptForParameterInfo() 176 | endif 177 | call s:echo("Saved macro with name '%s'", name) 178 | endfunction 179 | 180 | function! macrobatics#nameCurrentMacroForFileType() 181 | let saveDir = s:getFileTypeNamedMacrosDirs()[0] 182 | call s:saveCurrentMacroToDirectory(saveDir) 183 | endfunction 184 | 185 | function! macrobatics#renameNamedMacro(macroName) 186 | let newName = input('New Name:', a:macroName) 187 | echo "\r" 188 | if len(newName) == 0 189 | " View this as a cancel 190 | echo "Save macro cancelled" 191 | return 192 | endif 193 | if has_key(s:namedMacrosForSession, a:macroName) 194 | if has_key(s:namedMacrosForSession, newName) && confirm(printf( 195 | \ "Found existing macro with name '%s'. Overwrite?", newName), 196 | \ "&Yes\n&No", 2, "Question") != 1 197 | " Any response except yes is viewed as a cancel 198 | echo "Save macro cancelled" 199 | return 200 | endif 201 | let s:namedMacrosForSession[newName] = s:namedMacrosForSession[a:macroName] 202 | let s:namedMacroParamInfosForSession[newName] = s:namedMacroParamInfosForSession[a:macroName] 203 | unlet s:namedMacrosForSession[a:macroName] 204 | unlet s:namedMacroParamInfosForSession[a:macroName] 205 | return 206 | endif 207 | let macroDir = s:findNamedMacroDir(a:macroName) 208 | let newFilePath = s:constructMacroPath(macroDir, newName) 209 | if filereadable(newFilePath) && confirm( 210 | \ printf("Found existing macro with name '%s'. Overwrite?", newName), 211 | \ "&Yes\n&No", 2, "Question") != 1 212 | " Any response except yes is viewed as a cancel 213 | echo "Save macro cancelled" 214 | return 215 | endif 216 | let dataFilePath = s:constructMacroPath(macroDir, a:macroName) 217 | let paramInfoListFilePath = s:constructMacroParameterFilePath(macroDir, a:macroName) 218 | call s:assert(filereadable(dataFilePath), "File '%s' is not readable", dataFilePath) 219 | let macroData = s:loadNamedMacroData(dataFilePath) 220 | let paramInfoList = s:tryLoadNamedMacroParameterInfoFromFile(paramInfoListFilePath) 221 | call delete(dataFilePath) 222 | call delete(paramInfoListFilePath) 223 | call s:saveMacroFile(macroData, newFilePath) 224 | if (!(paramInfoList is v:null)) 225 | let newParamInfoListFilePath = s:constructMacroParameterFilePath(macroDir, newName) 226 | call s:saveMacroParameterFile(paramInfoList, newParamInfoListFilePath) 227 | endif 228 | call s:echo("Renamed macro from '%s' to '%s'", a:macroName, newName) 229 | endfunction 230 | 231 | function! macrobatics#overwriteNamedMacro(macroName) 232 | if has_key(s:namedMacrosForSession, a:macroName) 233 | let paramInfo = s:namedMacroParamInfosForSession[a:macroName] 234 | let overwriteParams = v:true 235 | if len(paramInfo) > 0 236 | let choice = confirm("Re-use previously saved parameter settings?", "&Yes\n&No", 2, "Question") 237 | if choice == 0 238 | echo "Save macro cancelled" 239 | return 240 | endif 241 | if choice == 1 242 | let overwriteParams = v:false 243 | endif 244 | endif 245 | if overwriteParams 246 | let s:namedMacroParamInfosForSession[a:macroName] = s:promptForParameterInfo() 247 | endif 248 | let s:namedMacrosForSession[a:macroName] = getreg(s:defaultMacroReg) 249 | return 250 | endif 251 | let macroDir = s:findNamedMacroDir(a:macroName) 252 | let filePath = s:constructMacroPath(macroDir, a:macroName) 253 | call s:assert(filereadable(filePath), "File '%s' is not readable", filePath) 254 | let paramFilePath = s:constructMacroParameterFilePath(macroDir, a:macroName) 255 | let overwriteParamFile = v:true 256 | if filereadable(paramFilePath) 257 | let choice = confirm("Re-use previously saved parameter settings?", "&Yes\n&No", 2, "Question") 258 | if choice == 0 259 | echo "Save macro cancelled" 260 | return 261 | endif 262 | if choice == 1 263 | let overwriteParamFile = v:false 264 | endif 265 | endif 266 | let macroData = getreg(s:defaultMacroReg) 267 | call s:saveMacroFile(macroData, filePath) 268 | if overwriteParamFile 269 | call s:saveMacroParameterFile(s:promptForParameterInfo(), paramFilePath) 270 | endif 271 | call s:echo("Updated macro with name '%s'", a:macroName) 272 | endfunction 273 | 274 | function! macrobatics#deleteNamedMacro(macroName) 275 | if has_key(s:namedMacrosForSession, a:macroName) 276 | unlet s:namedMacrosForSession[a:macroName] 277 | unlet s:namedMacroParamInfosForSession[a:macroName] 278 | return 279 | endif 280 | let macroDir = s:findNamedMacroDir(a:macroName) 281 | let paramFilePath = s:constructMacroParameterFilePath(macroDir, a:macroName) 282 | if filereadable(paramFilePath) 283 | call delete(paramFilePath) 284 | endif 285 | let filePath = s:constructMacroPath(macroDir, a:macroName) 286 | if filereadable(filePath) 287 | call delete(filePath) 288 | call s:echo("Deleted macro with name '%s'", a:macroName) 289 | endif 290 | endfunction 291 | 292 | function! macrobatics#searchAndRenameNamedMacro() 293 | call s:chooseNamedMacro({choice -> macrobatics#renameNamedMacro(choice)}) 294 | endfunction 295 | 296 | function! macrobatics#searchAndDeleteNamedMacro() 297 | call s:chooseNamedMacro({choice -> macrobatics#deleteNamedMacro(choice)}) 298 | endfunction 299 | 300 | function! macrobatics#searchAndOverwriteNamedMacro() 301 | call s:chooseNamedMacro({choice -> macrobatics#overwriteNamedMacro(choice)}) 302 | endfunction 303 | 304 | function! macrobatics#nameCurrentMacro() 305 | call s:saveCurrentMacroToDirectory(macrobatics#getGlobalNamedMacrosDir()) 306 | endfunction 307 | 308 | function! s:makeChoice(values, sink) 309 | if macrobatics#isPlayingMacro() 310 | " Require that they type it in exactly when recording, for this to work 311 | call a:sink(input('')) 312 | else 313 | let makeChoiceArgs = v:null 314 | 315 | if type(a:values) == type({}) 316 | let makeChoiceArgs = [keys(a:values), {choice -> a:sink(a:values[choice])}] 317 | else 318 | let makeChoiceArgs = [a:values, a:sink] 319 | endif 320 | 321 | call call("macrobatics#" . s:getFuzzySearchMethod() . "#makeChoice", makeChoiceArgs) 322 | endif 323 | endfunction 324 | 325 | function! s:chooseNamedMacro(sink) 326 | let map = {} 327 | for info in s:getNamedMacrosInfo() 328 | let displayName = "[" . info.type . "] " . info.name 329 | let map[displayName] = info.name 330 | endfor 331 | call s:makeChoice(map, a:sink) 332 | endfunction 333 | 334 | function! macrobatics#searchThenPlayNamedMacro(cnt) 335 | let playCount = a:cnt > 0 ? a:cnt : 1 336 | call s:chooseNamedMacro({choice -> macrobatics#playNamedMacro(choice, playCount)}) 337 | endfunction 338 | 339 | function! macrobatics#searchThenSelectNamedMacro() 340 | call s:chooseNamedMacro(function('macrobatics#selectNamedMacro')) 341 | endfunction 342 | 343 | function! s:updateMacroReg(reg, value) 344 | " It's important that we always set in charwise mode, otherwise it can add unnecessary 345 | " newline characters to the end of the macro, when it ends with a ^M character 346 | call setreg(a:reg, a:value, 'c') 347 | endfunction 348 | 349 | function s:paramValueSink(reg, value) 350 | call s:assert(len(a:reg) == 1, "Expected register value for macro parameter") 351 | call s:updateMacroReg(a:reg, a:value) 352 | call s:queuedMacroNext() 353 | endfunction 354 | 355 | function s:queuedMacroNext() 356 | let info = s:queuedMacroInfo 357 | call s:assert(!(info is v:null)) 358 | 359 | if len(info.paramInputQueue) == 0 360 | call s:updateMacroReg(info.destinationRegister, info.macroData) 361 | if info.destinationRegister == s:defaultMacroReg 362 | call s:addToHistory(info.macroData) 363 | endif 364 | if (info.autoplay) 365 | call macrobatics#play(info.destinationRegister, info.playCount) 366 | endif 367 | return 368 | endif 369 | 370 | let paramInfo = remove(info.paramInputQueue, 0) 371 | 372 | call s:assert(type(paramInfo) == v:t_dict, 373 | \ "Expected named parameter info for macro '%s' to be type dictionary", info.macroName) 374 | 375 | if has_key(paramInfo, 'value') 376 | call s:paramValueSink(paramInfo.register, paramInfo.value) 377 | elseif has_key(paramInfo, 'valueProvider') 378 | if get(paramInfo, 'is_async', 0) 379 | call paramInfo.valueProvider(paramInfo.name, {choice -> s:paramValueSink(paramInfo.register, choice)}) 380 | else 381 | call s:paramValueSink(paramInfo.register, paramInfo.valueProvider(paramInfo.name)) 382 | endif 383 | elseif has_key(paramInfo, 'choices') 384 | call s:echo("Choose value for '%s'", paramInfo.name) 385 | call s:makeChoice(paramInfo.choices, {choice -> s:paramValueSink(paramInfo.register, choice)}) 386 | elseif has_key(paramInfo, 'choicesProvider') 387 | if get(paramInfo, 'is_async', 0) 388 | call s:echo("Choose value for '%s'", paramInfo.name) 389 | call paramInfo.choicesProvider(paramInfo.name, { 390 | \ choices -> s:makeChoice( 391 | \ choices, {choice -> s:paramValueSink(paramInfo.register, choice)})}) 392 | else 393 | call s:makeChoice(paramInfo.choicesProvider(paramInfo.name), 394 | \ {choice -> s:paramValueSink(paramInfo.register, choice)}) 395 | endif 396 | elseif has_key(paramInfo, 'name') 397 | let paramValue = input(paramInfo.name . ": ") 398 | if len(paramValue) == 0 399 | call s:echo("Cancelled macro '%s'", info.macroName) 400 | return 401 | endif 402 | call s:assert(paramInfo.register != info.destinationRegister, "Macro parameter register cannot be the same as the macro register") 403 | call s:paramValueSink(paramInfo.register, paramValue) 404 | else 405 | call s:assert(0, 406 | \ "Unexpected value for macro '%s' and register '%s'", info.macroName, paramInfo.register) 407 | endif 408 | endfunction 409 | 410 | function! macrobatics#playNamedMacro(name, ...) 411 | let playCount = a:0 ? a:1 : 1 412 | let paramOverrides = a:0 > 1 ? a:2 : v:null 413 | call s:processNamedMacro(a:name, 1, s:playByNameMacroReg, paramOverrides, playCount) 414 | endfunction 415 | 416 | function! s:getPlayMacroInfoForName(name) 417 | if has_key(s:namedMacrosForSession, a:name) 418 | return { 419 | \ 'data': s:namedMacrosForSession[a:name], 420 | \ 'paramInfoList': s:namedMacroParamInfosForSession[a:name], 421 | \ } 422 | endif 423 | 424 | let macroDir = s:findNamedMacroDir(a:name) 425 | let dataFilePath = s:constructMacroPath(macroDir, a:name) 426 | let paramInfoListFilePath = s:constructMacroParameterFilePath(macroDir, a:name) 427 | let cache = s:getMacroCacheForDir(macroDir) 428 | let cachedInfo = get(cache, a:name, v:null) 429 | if cachedInfo is v:null 430 | call s:assert(filereadable(dataFilePath), 431 | \ "Could not find macro with name '%s'!", a:name) 432 | let cachedInfo = { 433 | \ 'data':s:loadNamedMacroData(dataFilePath), 434 | \ 'timestamp':getftime(dataFilePath), 435 | \ 'paramInfoListFromFile':s:tryLoadNamedMacroParameterInfoFromFile(paramInfoListFilePath), 436 | \ } 437 | let cache[a:name] = cachedInfo 438 | else 439 | let currentFileTime = getftime(dataFilePath) 440 | " Auto reload if the file is changed 441 | " This would occur when over-writing from the same or different vim instance 442 | if filereadable(dataFilePath) && cachedInfo.timestamp != currentFileTime 443 | let cachedInfo.data = s:loadNamedMacroData(dataFilePath) 444 | let cachedInfo.paramInfoListFromFile = s:tryLoadNamedMacroParameterInfoFromFile(paramInfoListFilePath) 445 | let cachedInfo.timestamp = currentFileTime 446 | endif 447 | endif 448 | let result = {'data': cachedInfo.data} 449 | " Reload from settings every time 450 | let result.paramInfoList = s:tryGetMacroParametersInfoFromSettings(a:name) 451 | if result.paramInfoList is v:null 452 | let result.paramInfoList = cachedInfo.paramInfoListFromFile 453 | if result.paramInfoList is v:null 454 | let result.paramInfoList = [] 455 | endif 456 | endif 457 | return result 458 | endfunction 459 | 460 | function s:tryGetMacroParametersInfoFromSettings(name) 461 | let bufferLocalMap = get(b:, 'Mac_NamedMacroParameters', {}) 462 | if has_key(bufferLocalMap, a:name) 463 | return bufferLocalMap[a:name] 464 | endif 465 | 466 | let fileTypeMap = get(g:, 'Mac_NamedMacroParametersByFileType', {}) 467 | for fileType in s:getCurrentFileTypes() 468 | let fileTypeParamMap = get(fileTypeMap, fileType, v:null) 469 | if !(fileTypeParamMap is v:null) && has_key(fileTypeParamMap, a:name) 470 | return fileTypeParamMap[a:name] 471 | endif 472 | endfor 473 | 474 | let globalMap = get(g:, 'Mac_NamedMacroParameters', {}) 475 | return get(globalMap, a:name, v:null) 476 | endfunction 477 | 478 | function! s:processNamedMacro(macroName, autoplay, destinationRegister, paramOverrides, cnt) 479 | let playInfo = s:getPlayMacroInfoForName(a:macroName) 480 | 481 | " deepcopy is necessary due to the param override below 482 | let paramInfoList = deepcopy(playInfo.paramInfoList) 483 | 484 | if (!(a:paramOverrides is v:null)) 485 | for regOverride in keys(a:paramOverrides) 486 | let found = v:false 487 | for paramInfo in paramInfoList 488 | if paramInfo.register == regOverride 489 | let paramInfo.value = a:paramOverrides[regOverride] 490 | let found = v:true 491 | break 492 | endif 493 | endfor 494 | call s:assert(found, "Unable to substitute parameter value in macro for register '" .. regOverride .. "'") 495 | endfor 496 | endif 497 | 498 | let s:queuedMacroInfo = { 499 | \ 'macroData': playInfo.data, 500 | \ 'macroName': a:macroName, 501 | \ 'autoplay': a:autoplay, 502 | \ 'playCount': a:cnt, 503 | \ 'destinationRegister': a:destinationRegister, 504 | \ 'paramInputQueue': paramInfoList, 505 | \ } 506 | 507 | call s:queuedMacroNext() 508 | endfunction 509 | 510 | function! macrobatics#selectNamedMacro(name) 511 | call s:processNamedMacro(a:name, 0, s:defaultMacroReg, v:null, 0) 512 | endfunction 513 | 514 | function! macrobatics#onVimEnter() 515 | " This should still work when saving persisently since it should be a no-op 516 | call s:addToHistory(getreg(s:defaultMacroReg)) 517 | endfunction 518 | 519 | function! macrobatics#clearHistory() 520 | let history = macrobatics#getHistory() 521 | let previousSize = len(history) 522 | call remove(history, 0, -1) 523 | call s:addToHistory(getreg(s:defaultMacroReg)) 524 | call s:echo("Cleared macro history of %s entries", previousSize) 525 | endfunction 526 | 527 | function! macrobatics#rotate(offset) 528 | let history = macrobatics#getHistory() 529 | 530 | if empty(history) || a:offset == 0 531 | return 532 | endif 533 | 534 | " If the default register has contents different than the first entry in our history, 535 | " then it must have changed through a delete operation or directly via setreg etc. 536 | " In this case, don't rotate and instead just update the default register 537 | if history[0] != getreg(s:defaultMacroReg) 538 | call s:updateMacroReg(s:defaultMacroReg, history[0]) 539 | return 540 | endif 541 | 542 | let actualOffset = float2nr(fmod(a:offset, len(history))) 543 | " Mod to save ourselves some work 544 | let offsetLeft = actualOffset 545 | 546 | while offsetLeft != 0 547 | if offsetLeft > 0 548 | let l:entry = remove(history, 0) 549 | call add(history, l:entry) 550 | let offsetLeft -= 1 551 | elseif offsetLeft < 0 552 | let l:entry = remove(history, -1) 553 | call insert(history, l:entry) 554 | let offsetLeft += 1 555 | endif 556 | endwhile 557 | 558 | call s:updateMacroReg(s:defaultMacroReg, history[0]) 559 | call s:echo("Current Macro: %s", s:formatMacro(history[0])) 560 | endfunction 561 | 562 | function! macrobatics#onRecordingComplete(_) 563 | 564 | if (s:recordInfo is v:null) 565 | call s:resetPopupMenu() 566 | " This can happen when repeat.vim is not installed, so just do nothing in this case 567 | return 568 | endif 569 | 570 | let info = s:recordInfo 571 | let info.recordContent = getreg(info.reg) 572 | if !(info.appendContents is v:null) 573 | let s:autoFinishRecordAfterPlay = 1 574 | call s:updateMacroReg(info.reg, info.appendContents) 575 | call macrobatics#play(info.reg, 1) 576 | else 577 | call s:onRecordingFullyComplete() 578 | endif 579 | endfunction 580 | 581 | function! macrobatics#recordNew(reg) 582 | if s:isRecording 583 | let s:isRecording = 0 584 | " We use onRecordingComplete here instead of play because we don't actually 585 | " want to run the macro again after it is recorded 586 | set opfunc=macrobatics#onRecordingComplete 587 | return "qg@l" 588 | endif 589 | 590 | let recordReg = s:getMacroRegister(a:reg) 591 | call s:setRecordInfo(recordReg, v:null, v:null) 592 | 593 | call s:temporarilyDisablePopupMenu() 594 | let s:isRecording = 1 595 | return "q" . recordReg 596 | endfunction 597 | 598 | function! macrobatics#append(reg, cnt) 599 | call s:assert(!s:isRecording) 600 | call s:assert(a:cnt == 0 || a:cnt == 1) 601 | 602 | let recordReg = s:getMacroRegister(a:reg) 603 | call s:setRecordInfo(recordReg, getreg(recordReg), v:null) 604 | 605 | call s:temporarilyDisablePopupMenu() 606 | let s:isRecording = 1 607 | " I don't know why this works and yet 608 | " call feedkeys("@" . recordReg . "q" . recordReg, 'n') does not 609 | " and neither does changing the map to be an 610 | " and then returning "@" . recordReg . "q" . recordReg 611 | " Also, for some reason, reversing these lines and removing the 'i' 612 | " works too 613 | call feedkeys("@" . recordReg, 'ni') 614 | call feedkeys("q" . recordReg, 'ni') 615 | endfunction 616 | 617 | function! macrobatics#prepend(reg, cnt) 618 | call s:assert(!s:isRecording) 619 | call s:assert(a:cnt == 0 || a:cnt == 1) 620 | 621 | let recordReg = s:getMacroRegister(a:reg) 622 | 623 | call s:setRecordInfo(recordReg, v:null, getreg(recordReg)) 624 | 625 | call s:temporarilyDisablePopupMenu() 626 | let s:isRecording = 1 627 | call feedkeys("q" . recordReg, 'n') 628 | endfunction 629 | 630 | function! macrobatics#play(reg, cnt) 631 | let playInfo = s:createPlayInfo( 632 | \ s:getMacroRegister(a:reg), a:cnt > 0 ? a:cnt : 1) 633 | 634 | if s:macrosInProgress == 0 635 | let s:repeatMacro = playInfo 636 | endif 637 | 638 | let s:macrosInProgress += 1 639 | 640 | " We need to use 'i' to allow nested macros to work 641 | " Also note that using `normal! @` instead of feedkeys 642 | " Doesn't work sometimes 643 | call feedkeys(playInfo.cnt . "@" . playInfo.reg, 'ni') 644 | " Don't need to use i here though, because we only want this to run at the very end 645 | " Since otherwise it will overwrite s:repeatMacro 646 | call feedkeys("\(Mac__OnPlayMacroCompleted)", 'm') 647 | endfunction 648 | 649 | function! s:assert(value, ...) 650 | if !a:value 651 | if len(a:000) == 0 652 | throw 'Assert hit inside vim-macrobatics plugin' 653 | else 654 | throw call('printf', a:000) 655 | endif 656 | endif 657 | endfunction 658 | 659 | function! s:getDefaultReg() 660 | let clipboardFlags = split(&clipboard, ',') 661 | if index(clipboardFlags, 'unnamedplus') >= 0 662 | return "+" 663 | elseif index(clipboardFlags, 'unnamed') >= 0 664 | return "*" 665 | else 666 | return "\"" 667 | endif 668 | endfunction 669 | 670 | function! s:onPlayMacroCompleted() 671 | let s:macrosInProgress -= 1 672 | 673 | call s:assert(s:macrosInProgress >= 0) 674 | 675 | if s:macrosInProgress == 0 676 | if s:autoFinishRecordAfterPlay 677 | let s:autoFinishRecordAfterPlay = 0 678 | call s:assert(!(s:recordInfo is v:null)) 679 | call s:assert(!(s:recordInfo.appendContents is v:null)) 680 | call s:onRecordingFullyComplete() 681 | else 682 | call s:markForRepeat() 683 | endif 684 | endif 685 | endfunction 686 | 687 | function! s:formatMacro(macro) 688 | let result = strtrans(a:macro) 689 | if len(result) > s:displayMacroMaxWidth 690 | return result[: s:displayMacroMaxWidth] . '…' 691 | endif 692 | return result 693 | endfunction 694 | 695 | function! s:displayMacro(macro, index) 696 | if a:index == 0 697 | echohl Directory | echo 'm ' 698 | else 699 | echohl Directory | echo printf("%-4d", a:index) 700 | endif 701 | 702 | echohl None | echon s:formatMacro(a:macro) 703 | echohl None 704 | endfunction 705 | 706 | function! s:getMacroRegister(requestedReg) 707 | if a:requestedReg == "_" || a:requestedReg == s:getDefaultReg() 708 | return s:defaultMacroReg 709 | endif 710 | return a:requestedReg 711 | endfunction 712 | 713 | function! s:setRecordInfo(reg, prependContents, appendContents) 714 | call s:assert(s:recordInfo is v:null) 715 | let s:recordInfo = {'reg': a:reg, 'prependContents': a:prependContents, 'appendContents': a:appendContents, 'previousContents': getreg(a:reg)} 716 | endfunction 717 | 718 | function! s:removeFromHistory(entry) 719 | let history = macrobatics#getHistory() 720 | 721 | let i = 0 722 | for candidate in history 723 | if candidate == a:entry 724 | call remove(history, i) 725 | return 1 726 | endif 727 | let i += 1 728 | endfor 729 | return 0 730 | endfunction 731 | 732 | function! s:addToHistory(entry) 733 | let history = macrobatics#getHistory() 734 | 735 | if len(history) == 0 || history[0] != a:entry 736 | call s:removeFromHistory(a:entry) 737 | call insert(history, a:entry, 0) 738 | if len(history) > s:maxItems 739 | call remove(history, s:maxItems, -1) 740 | endif 741 | endif 742 | endfunction 743 | 744 | function! s:constructMacroPath(directoryPath, name) 745 | return a:directoryPath . '/' . a:name . s:macroFileExtension 746 | endfunction 747 | 748 | function! s:constructMacroParameterFilePath(directoryPath, name) 749 | return a:directoryPath . '/' . a:name . s:macroParameterInfoFileExtension 750 | endfunction 751 | 752 | function! s:getMacroNameFromPath(filePath) 753 | let matchIndex = match(a:filePath, '\v[\\/]\zs[^\\/]*' . s:macroFileExtension . '$') 754 | call s:assert(matchIndex != -1) 755 | return strpart(a:filePath, matchIndex, len(a:filePath) - matchIndex - len(s:macroFileExtension)) 756 | endfunction 757 | 758 | " This was copied from coc.nvim 759 | function! s:chooseGlobalMacroSaveDirectory() 760 | let saveDir = get(g:, 'Mac_NamedMacrosDirectory', v:null) 761 | if saveDir is v:null 762 | if exists('$XDG_CONFIG_HOME') 763 | let saveDir = resolve($XDG_CONFIG_HOME."/macrobatics") 764 | else 765 | if has('win32') || has('win64') 766 | let saveDir = resolve(expand('~/AppData/Local/macrobatics')) 767 | else 768 | let saveDir = resolve(expand('~/.config/macrobatics')) 769 | endif 770 | endif 771 | else 772 | let saveDir = resolve(expand(saveDir)) 773 | endif 774 | return saveDir 775 | endfunction 776 | 777 | function s:getCurrentFileTypes() 778 | return split(&ft, '\.') 779 | endfunction 780 | 781 | function! s:getFileTypeNamedMacrosDirs() 782 | let dirs = [] 783 | for ft in s:getCurrentFileTypes() 784 | call add(dirs, macrobatics#getGlobalNamedMacrosDir() . "/filetype/" . ft) 785 | endfor 786 | return dirs 787 | endfunction 788 | 789 | function! s:getBufferLocalNamedMacrosDirs() 790 | return get(b:, 'Mac_NamedMacrosDirectories', []) 791 | endfunction 792 | 793 | function! s:getNamedMacrosDirs() 794 | " Place buffer local dirs first so they override global macros 795 | return s:getBufferLocalNamedMacrosDirs() + s:getFileTypeNamedMacrosDirs() + [macrobatics#getGlobalNamedMacrosDir()] 796 | endfunction 797 | 798 | function s:echo(...) 799 | echo call('printf', a:000) 800 | endfunction 801 | 802 | function s:echom(...) 803 | echom call('printf', a:000) 804 | endfunction 805 | 806 | function! s:saveMacroFile(macroData, filePath) 807 | call writefile([a:macroData], a:filePath, 'b') 808 | endfunction 809 | 810 | function! s:saveMacroParameterFile(paramInfo, filePath) 811 | call delete(a:filePath) 812 | if len(a:paramInfo) > 0 813 | call writefile([string(a:paramInfo)], a:filePath) 814 | endif 815 | endfunction 816 | 817 | function! s:promptForParameterInfo() 818 | let result = [] 819 | while v:true 820 | let prompt = "" 821 | if len(result) == 0 822 | let prompt = "Add parameters?" 823 | else 824 | let prompt = "Add another parameter?" 825 | endif 826 | 827 | let choice = confirm(prompt, "&Yes\n&No", 2, "Question") 828 | 829 | if choice == 0 830 | return v:null 831 | endif 832 | 833 | if choice != 1 834 | break 835 | endif 836 | 837 | let paramInfo = {} 838 | 839 | while v:true 840 | let paramInfo.register = input('Parameter Register: ') 841 | 842 | if len(paramInfo.register) == 0 843 | " interpret this as a cancel 844 | return v:null 845 | endif 846 | 847 | if len(paramInfo.register) != 1 848 | echo "\nInvalid register given. Please try again" 849 | else 850 | break 851 | endif 852 | endwhile 853 | 854 | let paramInfo.name = input('Parameter Name: ') 855 | 856 | if len(paramInfo.name) == 0 857 | " interpret this as a cancel 858 | return v:null 859 | endif 860 | 861 | call add(result, paramInfo) 862 | endwhile 863 | return result 864 | endfunction 865 | 866 | function! s:saveCurrentMacroToDirectory(dirPath) 867 | let name = input('Macro Name:') 868 | if len(name) == 0 869 | echo "Save macro cancelled" 870 | " View this as a cancel 871 | return 872 | endif 873 | " Without this the echo below appears on the same line as input 874 | echo "\r" 875 | " Ensure directory exists 876 | call mkdir(a:dirPath, "p", 0755) 877 | let dataFilePath = s:constructMacroPath(a:dirPath, name) 878 | let paramFilePath = s:constructMacroParameterFilePath(a:dirPath, name) 879 | let overwriteParamFile = v:true 880 | if filereadable(dataFilePath) 881 | if confirm( 882 | \ printf("Found existing macro with name '%s'. Overwrite?", name), 883 | \ "&Yes\n&No", 2, "Question") != 1 884 | " Any response except yes is viewed as a cancel 885 | echo "Save macro cancelled" 886 | return 887 | endif 888 | if filereadable(paramFilePath) 889 | let choice = confirm("Re-use previously saved parameter settings?", "&Yes\n&No", 2, "Question") 890 | if choice == 0 891 | echo "Save macro cancelled" 892 | return 893 | endif 894 | if choice == 1 895 | let overwriteParamFile = v:false 896 | endif 897 | endif 898 | endif 899 | let macroData = getreg(s:defaultMacroReg) 900 | call s:saveMacroFile(macroData, dataFilePath) 901 | if overwriteParamFile 902 | call s:saveMacroParameterFile(s:promptForParameterInfo(), paramFilePath) 903 | endif 904 | call s:echo("Saved macro with name '%s'", name) 905 | endfunction 906 | 907 | function! s:getFuzzySearchMethod() 908 | if s:fuzzySearcher is v:null 909 | for fuzzyName in s:defaultFuzzySearchers 910 | if call("macrobatics#" . fuzzyName . "#isAvailable", []) 911 | let s:fuzzySearcher = fuzzyName 912 | break 913 | endif 914 | endfor 915 | 916 | call s:assert(!(s:fuzzySearcher is v:null), 917 | \ "Could not find an available fuzzy searcher for macrobatics! " 918 | \ . "This can also be set explicitly with 'Mac_NamedMacroFuzzySearcher'. " 919 | \ . "See documentation for details.") 920 | endif 921 | 922 | return s:fuzzySearcher 923 | endfunction 924 | 925 | function s:tryLoadNamedMacroParameterInfoFromFile(filePath) 926 | if !filereadable(a:filePath) 927 | return v:null 928 | endif 929 | let lines = readfile(a:filePath) 930 | call s:assert(len(lines) == 1) 931 | return eval(lines[0]) 932 | endfunction 933 | 934 | function s:loadNamedMacroData(filePath) 935 | let macroDataList = readfile(a:filePath, 'b') 936 | call s:assert(len(macroDataList) == 1, "Invalid content found in macro file '%s'. Please re-record macro and try again.", a:filePath) 937 | return macroDataList[0] 938 | endfunction 939 | 940 | function s:findNamedMacroDir(name) 941 | for macroDir in s:getNamedMacrosDirs() 942 | let filePath = s:constructMacroPath(macroDir, a:name) 943 | if filereadable(filePath) 944 | return macroDir 945 | endif 946 | endfor 947 | return v:null 948 | endfunction 949 | 950 | function! s:getMacroCacheForDir(dirPath) 951 | let cache = get(s:namedMacroCache, a:dirPath, v:null) 952 | if cache is v:null 953 | let cache = {} 954 | let s:namedMacroCache[a:dirPath] = cache 955 | endif 956 | return cache 957 | endfunction 958 | 959 | function! s:onRecordingFullyComplete() 960 | call s:resetPopupMenu() 961 | let info = s:recordInfo 962 | let s:recordInfo = v:null 963 | let fullContent = info.recordContent 964 | if !(info.prependContents is v:null) 965 | let fullContent = info.prependContents . fullContent 966 | endif 967 | if !(info.appendContents is v:null) 968 | let fullContent = fullContent . info.appendContents 969 | endif 970 | if fullContent == '' 971 | " In this case, reset the macro register and do not add to history 972 | " View this as a cancel 973 | call s:updateMacroReg(info.reg, info.previousContents) 974 | else 975 | call s:updateMacroReg(info.reg, fullContent) 976 | if !(info.prependContents is v:null) || !(info.appendContents is v:null) 977 | call s:removeFromHistory(info.previousContents) 978 | endif 979 | call s:addToHistory(fullContent) 980 | let s:repeatMacro = s:createPlayInfo(info.reg, 1) 981 | call s:markForRepeat() 982 | endif 983 | endfunction 984 | 985 | function! s:markForRepeat() 986 | silent! call repeat#set("\(Mac__RepeatLast)") 987 | " Force disable the logic in vim-repeat that waits for CursorMove 988 | " This cause a bug where if you make a change immediately after recording a macro 989 | " and then attempt to repeat that change it will repeat the macro instead 990 | " Not sure why this logic is necessary in vim-repeat 991 | augroup repeat_custom_motion 992 | autocmd! 993 | augroup END 994 | endfunction 995 | 996 | function! s:resetPopupMenu() 997 | call s:assert(!(s:previousCompleteOpt is v:null)) 998 | exec "set completeopt=" . s:previousCompleteOpt 999 | let s:previousCompleteOpt=v:null 1000 | endfunction 1001 | 1002 | function! s:temporarilyDisablePopupMenu() 1003 | let s:previousCompleteOpt=&completeopt 1004 | set completeopt=noselect 1005 | endfunction 1006 | 1007 | function! s:createPlayInfo(reg, cnt) 1008 | return { 1009 | \ 'reg': s:getMacroRegister(a:reg), 1010 | \ 'cnt': a:cnt > 0 ? a:cnt : 1 1011 | \ } 1012 | endfunction 1013 | 1014 | function! s:repeatLast() 1015 | call macrobatics#play(s:repeatMacro.reg, s:repeatMacro.cnt) 1016 | endfunction 1017 | --------------------------------------------------------------------------------