├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── markdownlint.yml │ ├── nvim-type-check.yml │ ├── panvimdoc.yml │ ├── pr-title.yml │ ├── stale-bot.yml │ └── stylua.yml ├── .gitignore ├── .luarc.json ├── .markdownlint.yaml ├── .stylua.toml ├── Justfile ├── LICENSE ├── README.md ├── doc └── nvim-recorder.txt └── lua └── recorder.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 100 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 3 10 | tab_width = 3 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,yaml,scm,cff}] 14 | indent_style = space 15 | indent_size = 2 16 | tab_width = 2 17 | 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | tab_width = 4 22 | 23 | [*.md] 24 | indent_size = 4 25 | tab_width = 4 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository 2 | 3 | custom: https://www.paypal.me/ChrisGrieser 4 | ko_fi: pseudometa 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: textarea 7 | id: bug-description 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain your problem. 18 | - type: textarea 19 | id: reproduction-steps 20 | attributes: 21 | label: To Reproduce 22 | description: Steps to reproduce the problem 23 | placeholder: | 24 | For example: 25 | 1. Go to '...' 26 | 2. Click on '...' 27 | 3. Scroll down to '...' 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: version-info 32 | attributes: 33 | label: neovim version 34 | render: Text 35 | validations: 36 | required: true 37 | - type: checkboxes 38 | id: checklist 39 | attributes: 40 | label: Make sure you have done the following 41 | options: 42 | - label: I have updated to the latest version of the plugin. 43 | required: true 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: "I have read the plugin's documentation." 12 | required: true 13 | - label: The feature would be useful to more users than just me. 14 | required: true 15 | - type: textarea 16 | id: feature-requested 17 | attributes: 18 | label: Feature Requested 19 | description: A clear and concise description of the feature. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: screenshot 24 | attributes: 25 | label: Relevant Screenshot 26 | description: If applicable, add screenshots or a screen recording to help explain the request. 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(dependabot): " 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What problem does this PR solve? 2 | 3 | ## How does the PR solve it? 4 | 5 | ## Checklist 6 | - [ ] Used only `camelCase` variable names. 7 | - [ ] If functionality is added or modified, also made respective changes to the 8 | `README.md` (the `.txt` file is auto-generated and does not need to be 9 | modified). 10 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Markdownlint check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.md" 8 | - ".github/workflows/markdownlint.yml" 9 | - ".markdownlint.*" # markdownlint config files 10 | pull_request: 11 | paths: 12 | - "**.md" 13 | 14 | jobs: 15 | markdownlint: 16 | name: Markdownlint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: DavidAnson/markdownlint-cli2-action@v20 21 | with: 22 | globs: "**/*.md" 23 | -------------------------------------------------------------------------------- /.github/workflows/nvim-type-check.yml: -------------------------------------------------------------------------------- 1 | name: nvim type check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | build: 12 | name: nvim type check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: stevearc/nvim-typecheck-action@v2 17 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - README.md 8 | - .github/workflows/panvimdoc.yml 9 | workflow_dispatch: {} # allows manual execution 10 | 11 | permissions: 12 | contents: write 13 | 14 | #─────────────────────────────────────────────────────────────────────────────── 15 | 16 | jobs: 17 | docs: 18 | runs-on: ubuntu-latest 19 | name: README.md to vimdoc 20 | steps: 21 | - uses: actions/checkout@v4 22 | - run: git pull # fix failure when multiple commits are pushed in succession 23 | - run: mkdir -p doc 24 | 25 | - name: panvimdoc 26 | uses: kdheepak/panvimdoc@main 27 | with: 28 | vimdoc: ${{ github.event.repository.name }} 29 | version: "Neovim" 30 | demojify: true 31 | treesitter: true 32 | 33 | - run: git pull 34 | - name: push changes 35 | uses: stefanzweifel/git-auto-commit-action@v5 36 | with: 37 | commit_message: "chore: auto-generate vimdocs" 38 | branch: ${{ github.head_ref }} 39 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-pull-request: 17 | name: Check PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | requireScope: false 25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital 26 | types: | # add `improv` to the list of allowed types 27 | improv 28 | fix 29 | feat 30 | refactor 31 | build 32 | ci 33 | style 34 | test 35 | chore 36 | perf 37 | docs 38 | break 39 | revert 40 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale bot 2 | on: 3 | schedule: 4 | - cron: "18 04 * * 3" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close stale issues 15 | uses: actions/stale@v9 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | # DOCS https://github.com/actions/stale#all-options 20 | days-before-stale: 180 21 | days-before-close: 7 22 | stale-issue-label: "Stale" 23 | stale-issue-message: | 24 | This issue has been automatically marked as stale. 25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity, and will not be monitored. 28 | -------------------------------------------------------------------------------- /.github/workflows/stylua.yml: -------------------------------------------------------------------------------- 1 | name: Stylua check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | stylua: 12 | name: Stylua 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: JohnnyMorganz/stylua-action@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | version: latest 20 | args: --check . 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # help-tags auto-generated by lazy.nvim 2 | doc/tags 3 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime.version": "LuaJIT", 3 | "diagnostics.globals": ["vim"] 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Defaults https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 2 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 3 | #─────────────────────────────────────────────────────────────────────────────── 4 | 5 | # MODIFIED SETTINGS 6 | blanks-around-headings: 7 | lines_below: 0 # space waster 8 | ul-style: { style: sublist } 9 | 10 | # not autofixable 11 | ol-prefix: { style: ordered } 12 | line-length: 13 | tables: false 14 | code_blocks: false 15 | no-inline-html: 16 | allowed_elements: [img, details, summary, kbd, a, br] 17 | 18 | #───────────────────────────────────────────────────────────────────────────── 19 | # DISABLED 20 | ul-indent: false # not compatible with using tabs 21 | no-hard-tabs: false # taken care of by editorconfig 22 | blanks-around-lists: false # space waster 23 | first-line-heading: false # e.g., ignore-comments 24 | no-emphasis-as-heading: false # sometimes useful 25 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/JohnnyMorganz/StyLua#options 2 | column_width = 100 3 | indent_type = "Tabs" 4 | indent_width = 3 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "NoSingleTable" 7 | collapse_simple_statement = "Always" 8 | 9 | [sort_requires] 10 | enabled = true 11 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set quiet := true 2 | 3 | masonPath := "$HOME/.local/share/nvim/mason/bin/" 4 | 5 | #─────────────────────────────────────────────────────────────────────────────── 6 | 7 | stylua: 8 | #!/usr/bin/env zsh 9 | {{ masonPath }}/stylua --check --output-format=summary . && return 0 10 | {{ masonPath }}/stylua . 11 | echo "\nFiles formatted." 12 | 13 | lua_ls_check: 14 | {{ masonPath }}/lua-language-server --check . 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christopher Grieser 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nvim-recorder 📹 3 | 4 | 5 | badge 6 | 7 | Enhance the usage of macros in Neovim. 8 | 9 | 10 | 11 | - [Features](#features) 12 | - [Setup](#setup) 13 | * [Installation](#installation) 14 | * [Configuration](#configuration) 15 | * [Status Line Components](#status-line-components) 16 | - [Basic Usage](#basic-usage) 17 | - [Advanced Usage](#advanced-usage) 18 | * [Performance Optimizations](#performance-optimizations) 19 | * [Macro Breakpoints](#macro-breakpoints) 20 | * [Lazy-loading the plugin](#lazy-loading-the-plugin) 21 | - [About the developer](#about-the-developer) 22 | 23 | 24 | 25 | ## Features 26 | - __Simplified controls__: One key to start and stop recording, a second key for 27 | playing the macro. Instead of `qa … q @a @@`, you just do `q … q Q Q`.[^1] 28 | - __Macro Breakpoints__ for easier debugging of macros. Breakpoints can also be 29 | set after the recording and are automatically ignored when triggering a macro 30 | with a count. 31 | - __Status line components__: Particularly useful if you use `cmdheight=0` where 32 | the recording status is not visible. 33 | - __Macro-to-Mapping__: Copy a macro, so you can save it as a mapping. 34 | - __Various quality-of-life features__: notifications with macro content, the 35 | ability to cancel a recording, a command to edit macros, 36 | - __Performance Optimizations for large macros__: When the macro is triggered 37 | with a high count, temporarily enable some performance improvements. 38 | - Uses up-to-date nvim features like `vim.notify`. This means you can get 39 | confirmation notices with plugins like 40 | [nvim-notify](https://github.com/rcarriga/nvim-notify). 41 | 42 | ## Setup 43 | 44 | ### Installation 45 | 46 | ```lua 47 | -- lazy.nvim 48 | { 49 | "chrisgrieser/nvim-recorder", 50 | dependencies = "rcarriga/nvim-notify", -- optional 51 | opts = {}, -- required even with default settings, since it calls `setup()` 52 | }, 53 | 54 | -- packer 55 | use { 56 | "chrisgrieser/nvim-recorder", 57 | requires = "rcarriga/nvim-notify", -- optional 58 | config = function() require("recorder").setup() end, 59 | } 60 | ``` 61 | 62 | Calling `setup()` (or `lazy`'s `opts`) is __required__. 63 | 64 | ### Configuration 65 | 66 | ```lua 67 | -- default values 68 | require("recorder").setup { 69 | -- Named registers where macros are saved (single lowercase letters only). 70 | -- The first register is the default register used as macro-slot after 71 | -- startup. 72 | slots = { "a", "b" }, 73 | 74 | -- specify one of options: 75 | -- [static] -> use static slots, this is default behaviour 76 | -- [rotate] -> rotates through letters specified in slots[] 77 | dynamicSlots = "static", 78 | 79 | mapping = { 80 | startStopRecording = "q", 81 | playMacro = "Q", 82 | switchSlot = "", 83 | editMacro = "cq", 84 | deleteAllMacros = "dq", 85 | yankMacro = "yq", 86 | -- ⚠️ this should be a string you don't use in insert mode during a macro 87 | addBreakPoint = "##", 88 | }, 89 | 90 | -- Clears all macros-slots on startup. 91 | clear = false, 92 | 93 | -- Log level used for non-critical notifications; mostly relevant for nvim-notify. 94 | -- (Note that by default, nvim-notify does not show the levels `trace` & `debug`.) 95 | logLevel = vim.log.levels.INFO, -- :help vim.log.levels 96 | 97 | -- If enabled, only essential notifications are sent. 98 | -- If you do not use a plugin like nvim-notify, set this to `true` 99 | -- to remove otherwise annoying messages. 100 | lessNotifications = false, 101 | 102 | -- Use nerdfont icons in the status bar components and keymap descriptions 103 | useNerdfontIcons = true, 104 | 105 | -- Performance optimizations for macros with high count. When `playMacro` is 106 | -- triggered with a count higher than the threshold, nvim-recorder 107 | -- temporarily changes changes some settings for the duration of the macro. 108 | performanceOpts = { 109 | countThreshold = 100, 110 | lazyredraw = true, -- enable lazyredraw (see `:h lazyredraw`) 111 | noSystemClipboard = true, -- remove `+`/`*` from clipboard option 112 | autocmdEventsIgnore = { -- temporarily ignore these autocmd events 113 | "TextChangedI", 114 | "TextChanged", 115 | "InsertLeave", 116 | "InsertEnter", 117 | "InsertCharPre", 118 | }, 119 | }, 120 | 121 | -- [experimental] partially share keymaps with nvim-dap. 122 | -- (See README for further explanations.) 123 | dapSharedKeymaps = false, 124 | } 125 | ``` 126 | 127 | If you want to handle multiple macros or use `cmdheight=0`, it is recommended to 128 | also set up the status line components: 129 | 130 | ### Status Line Components 131 | 132 | ```lua 133 | -- Indicates whether you are currently recording. Useful if you are using 134 | -- `cmdheight=0`, where recording-status is not visible. 135 | require("recorder").recordingStatus() 136 | 137 | -- Displays non-empty macro-slots (registers) and indicates the selected ones. 138 | -- Only displayed when *not* recording. Slots with breakpoints get an extra `#`. 139 | require("recorder").displaySlots() 140 | ``` 141 | 142 | > [!TIP] 143 | > Use with the config `clear = true` to see recordings you made this session. 144 | 145 | Example for adding the status line components to [lualine](https://github.com/nvim-lualine/lualine.nvim): 146 | 147 | ```lua 148 | lualine_y = { 149 | { require("recorder").displaySlots }, 150 | }, 151 | lualine_z = { 152 | { require("recorder").recordingStatus }, 153 | }, 154 | ``` 155 | 156 | > [!TIP] 157 | > Put the components in different status line segments, so they have 158 | > a different color, making the recording status more distinguishable 159 | > from saved recordings 160 | 161 | ## Basic Usage 162 | - `startStopRecording`: Starts recording to the current macro slot (so you do 163 | not need to specify a register). Press again to end the recording. 164 | - `playMacro`: Plays the macro in the current slot (without the need to specify 165 | a register). 166 | - `switchSlot`: Cycles through the registers you specified in the configuration. 167 | Also show a notification with the slot and its content. (The currently 168 | selected slot can be seen in the [status line 169 | component](#status-line-components).) 170 | - `editMacro`: Edit the macro recorded in the active slot. (Be aware that these 171 | are the keystrokes in "encoded" form.) 172 | - `yankMacro`: Copies the current macro in decoded form that can be used to 173 | create a mapping from it. Breakpoints are removed from the copied macro. 174 | - `deleteAllMacros`: Copies the current macro in decoded form that can be used to 175 | 176 | > [!TIP] 177 | > For recursive macros (playing a macro inside a macro), you can still use 178 | > the default command `@a`. 179 | 180 | ## Advanced Usage 181 | 182 | ### Performance Optimizations 183 | Running macros with a high count can be demanding on the system and result in 184 | lags. For this reason, `nvim-recorder` provides some performance optimizations 185 | that are temporarily enabled when a macro with a high count is run. 186 | 187 | Note that these optimizations do have some potential drawbacks. 188 | - [`lazyredraw`](https://neovim.io/doc/user/options.html#'lazyredraw') disables 189 | redrawing of the screen, which makes it harder to notice edge cases not 190 | considered in the macro. It may also appear as if the screen is frozen for a 191 | while. 192 | - Disabling the system clipboard is mostly safe, if you do not intend to copy 193 | content to it with the macro. 194 | - Ignoring auto-commands is not recommended, when you rely on certain plugin 195 | functionality during the macro, since it can potentially disrupt those 196 | plugins' effect. 197 | 198 | ### Macro Breakpoints 199 | `nvim-recorder` allows you to set breakpoints in your macros which can be 200 | helpful for debugging macros. Breakpoints are automatically ignored when you 201 | trigger the macro with a count. 202 | 203 | __Setting Breakpoints__ 204 | - *During a recording:* press the `addBreakPoint` key (default: `##`) in normal 205 | mode 206 | - *After a recording:* use `editMacro` and add or remove the `##` manually. 207 | 208 | __Playing Macros with Breakpoints__ 209 | - Using the `playMacro` key, the macro automatically stops at the next 210 | breakpoint. The next time you press `playMacro`, the next segment of the macro 211 | is played. 212 | - Starting a new recording, editing a macro, yanking a macro, or switching macro 213 | slot all reset the sequence, meaning that `playMacro` starts from the 214 | beginning again. 215 | 216 | > [!TIP] 217 | > You can do other things in between playing segments of the macro, like 218 | > moving a few characters to the left or right. That way you can also use 219 | > breakpoints to manually correct irregularities. 220 | 221 | __Ignoring Breakpoints__ 222 | When you play the macro with a *count* (for example `50Q`), breakpoints are 223 | automatically ignored. 224 | 225 | > [!TIP] 226 | > Add a count of 1 (`1Q`) to play a macro once and still ignore breakpoints. 227 | 228 | __Shared Keybindings with `nvim-dap`__ 229 | If you are using [nvim-dap](https://github.com/mfussenegger/nvim-dap), you can 230 | use `dapSharedKeymaps = true` to set up the following shared keybindings: 231 | 1. `addBreakPoint` maps to `dap.toggle_breakpoint()` outside 232 | a recording. During a recording, it adds a macro breakpoint instead. 233 | 2. `playMacro` maps to `dap.continue()` if there is at least one 234 | DAP-breakpoint. If there is no DAP-breakpoint, plays the current 235 | macro-slot instead. 236 | 237 | Note that this feature is experimental, since the [respective API from nvim-dap 238 | is non-public and can be changed without deprecation 239 | notice](https://github.com/mfussenegger/nvim-dap/discussions/810#discussioncomment-4623606). 240 | 241 | ### Lazy-loading the plugin 242 | `nvim-recorder` is best lazy-loaded on the mappings for `startStopRecording` and 243 | `playMacro`. However, adding the status line components to `lualine` will cause the 244 | plugin to load before you start or play a recording. 245 | 246 | To avoid this, the statusline components need to be loaded only in the plugin's 247 | `config`. The drawback of this method is that no component is shown when until 248 | you start or play a recording (which you can completely disregard when you set 249 | `clear = true`, though). 250 | 251 | Nonetheless, the plugin is pretty lightweight (~400 lines of code), so not 252 | lazy-loading it should not have a big impact. 253 | 254 | ```lua 255 | -- minimal config for lazy-loading with lazy.nvim 256 | { 257 | "chrisgrieser/nvim-recorder", 258 | dependencies = "rcarriga/nvim-notify", 259 | keys = { 260 | -- these must match the keys in the mapping config below 261 | { "q", desc = " Start Recording" }, 262 | { "Q", desc = " Play Recording" }, 263 | }, 264 | config = function() 265 | require("recorder").setup({ 266 | mapping = { 267 | startStopRecording = "q", 268 | playMacro = "Q", 269 | }, 270 | }) 271 | 272 | local lualineZ = require("lualine").get_config().sections.lualine_z or {} 273 | local lualineY = require("lualine").get_config().sections.lualine_y or {} 274 | table.insert(lualineZ, { require("recorder").recordingStatus }) 275 | table.insert(lualineY, { require("recorder").displaySlots }) 276 | 277 | require("lualine").setup { 278 | tabline = { 279 | lualine_y = lualineY, 280 | lualine_z = lualineZ, 281 | }, 282 | } 283 | end, 284 | }, 285 | ``` 286 | 287 | ## About the developer 288 | In my day job, I am a sociologist studying the social mechanisms underlying the 289 | digital economy. For my PhD project, I investigate the governance of the app 290 | economy and how software ecosystems manage the tension between innovation and 291 | compatibility. If you are interested in this subject, feel free to get in touch. 292 | 293 | - [Website](https://chris-grieser.de/) 294 | - [Mastodon](https://pkm.social/@pseudometa) 295 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 296 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 297 | 298 | Buy Me a Coffee at ko-fi.com 301 | 302 | [^1]: As opposed to vim, Neovim already allows you to use `Q` to [play the last 303 | recorded macro](https://neovim.io/doc/user/repeat.html#Q). Considering this, 304 | the simplified controls really only save you one keystroke for one-off 305 | macros. However, as opposed to Neovim's built-in controls, you can still 306 | keep using `Q` for playing the not-most-recently recorded macro. 307 | -------------------------------------------------------------------------------- /doc/nvim-recorder.txt: -------------------------------------------------------------------------------- 1 | *nvim-recorder.txt* For Neovim Last change: 2025 May 03 2 | 3 | ============================================================================== 4 | Table of Contents *nvim-recorder-table-of-contents* 5 | 6 | 1. nvim-recorder |nvim-recorder-nvim-recorder-| 7 | - Features |nvim-recorder-nvim-recorder--features| 8 | - Setup |nvim-recorder-nvim-recorder--setup| 9 | - Basic Usage |nvim-recorder-nvim-recorder--basic-usage| 10 | - Advanced Usage |nvim-recorder-nvim-recorder--advanced-usage| 11 | - About the developer |nvim-recorder-nvim-recorder--about-the-developer| 12 | 13 | ============================================================================== 14 | 1. nvim-recorder *nvim-recorder-nvim-recorder-* 15 | 16 | 17 | 18 | Enhancethe usage of macros in Neovim. 19 | 20 | - |nvim-recorder-features| 21 | - |nvim-recorder-setup| 22 | - |nvim-recorder-installation| 23 | - |nvim-recorder-configuration| 24 | - |nvim-recorder-status-line-components| 25 | - |nvim-recorder-basic-usage| 26 | - |nvim-recorder-advanced-usage| 27 | - |nvim-recorder-performance-optimizations| 28 | - |nvim-recorder-macro-breakpoints| 29 | - |nvim-recorder-lazy-loading-the-plugin| 30 | - |nvim-recorder-about-the-developer| 31 | 32 | 33 | FEATURES *nvim-recorder-nvim-recorder--features* 34 | 35 | - **Simplified controls**One key to start and stop recording, a second key for 36 | playing the macro. Instead of `qa … q @a @@`, you just do `q … q Q Q`. 37 | - **Macro Breakpoints** for easier debugging of macros. Breakpoints can also be 38 | set after the recording and are automatically ignored when triggering a macro 39 | with a count. 40 | - **Status line components**Particularly useful if you use `cmdheight=0` where 41 | the recording status is not visible. 42 | - **Macro-to-Mapping**Copy a macro, so you can save it as a mapping. 43 | - **Various quality-of-life features**notifications with macro content, the 44 | ability to cancel a recording, a command to edit macros, 45 | - **Performance Optimizations for large macros**When the macro is triggered 46 | with a high count, temporarily enable some performance improvements. 47 | - Uses up-to-date nvim features like `vim.notify`. This means you can get 48 | confirmation notices with plugins like 49 | nvim-notify . 50 | 51 | 52 | SETUP *nvim-recorder-nvim-recorder--setup* 53 | 54 | 55 | INSTALLATION ~ 56 | 57 | >lua 58 | -- lazy.nvim 59 | { 60 | "chrisgrieser/nvim-recorder", 61 | dependencies = "rcarriga/nvim-notify", -- optional 62 | opts = {}, -- required even with default settings, since it calls `setup()` 63 | }, 64 | 65 | -- packer 66 | use { 67 | "chrisgrieser/nvim-recorder", 68 | requires = "rcarriga/nvim-notify", -- optional 69 | config = function() require("recorder").setup() end, 70 | } 71 | < 72 | 73 | Calling `setup()` (or `lazy`’s `opts`) is **required**. 74 | 75 | 76 | CONFIGURATION ~ 77 | 78 | >lua 79 | -- default values 80 | require("recorder").setup { 81 | -- Named registers where macros are saved (single lowercase letters only). 82 | -- The first register is the default register used as macro-slot after 83 | -- startup. 84 | slots = { "a", "b" }, 85 | 86 | -- specify one of options: 87 | -- [static] -> use static slots, this is default behaviour 88 | -- [rotate] -> rotates through letters specified in slots[] 89 | dynamicSlots = "static", 90 | 91 | mapping = { 92 | startStopRecording = "q", 93 | playMacro = "Q", 94 | switchSlot = "", 95 | editMacro = "cq", 96 | deleteAllMacros = "dq", 97 | yankMacro = "yq", 98 | -- ⚠️ this should be a string you don't use in insert mode during a macro 99 | addBreakPoint = "##", 100 | }, 101 | 102 | -- Clears all macros-slots on startup. 103 | clear = false, 104 | 105 | -- Log level used for non-critical notifications; mostly relevant for nvim-notify. 106 | -- (Note that by default, nvim-notify does not show the levels `trace` & `debug`.) 107 | logLevel = vim.log.levels.INFO, -- :help vim.log.levels 108 | 109 | -- If enabled, only essential notifications are sent. 110 | -- If you do not use a plugin like nvim-notify, set this to `true` 111 | -- to remove otherwise annoying messages. 112 | lessNotifications = false, 113 | 114 | -- Use nerdfont icons in the status bar components and keymap descriptions 115 | useNerdfontIcons = true, 116 | 117 | -- Performance optimizations for macros with high count. When `playMacro` is 118 | -- triggered with a count higher than the threshold, nvim-recorder 119 | -- temporarily changes changes some settings for the duration of the macro. 120 | performanceOpts = { 121 | countThreshold = 100, 122 | lazyredraw = true, -- enable lazyredraw (see `:h lazyredraw`) 123 | noSystemClipboard = true, -- remove `+`/`*` from clipboard option 124 | autocmdEventsIgnore = { -- temporarily ignore these autocmd events 125 | "TextChangedI", 126 | "TextChanged", 127 | "InsertLeave", 128 | "InsertEnter", 129 | "InsertCharPre", 130 | }, 131 | }, 132 | 133 | -- [experimental] partially share keymaps with nvim-dap. 134 | -- (See README for further explanations.) 135 | dapSharedKeymaps = false, 136 | } 137 | < 138 | 139 | If you want to handle multiple macros or use `cmdheight=0`, it is recommended 140 | to also set up the status line components: 141 | 142 | 143 | STATUS LINE COMPONENTS ~ 144 | 145 | >lua 146 | -- Indicates whether you are currently recording. Useful if you are using 147 | -- `cmdheight=0`, where recording-status is not visible. 148 | require("recorder").recordingStatus() 149 | 150 | -- Displays non-empty macro-slots (registers) and indicates the selected ones. 151 | -- Only displayed when *not* recording. Slots with breakpoints get an extra `#`. 152 | require("recorder").displaySlots() 153 | < 154 | 155 | 156 | [!TIP] Use with the config `clear = true` to see recordings you made this 157 | session. 158 | Example for adding the status line components to lualine 159 | 160 | 161 | >lua 162 | lualine_y = { 163 | { require("recorder").displaySlots }, 164 | }, 165 | lualine_z = { 166 | { require("recorder").recordingStatus }, 167 | }, 168 | < 169 | 170 | 171 | [!TIP] Putthe components in different status line segments, so they have a 172 | different color, making the recording status more distinguishable from saved 173 | recordings 174 | 175 | BASIC USAGE *nvim-recorder-nvim-recorder--basic-usage* 176 | 177 | - `startStopRecording`Starts recording to the current macro slot (so you do 178 | not need to specify a register). Press again to end the recording. 179 | - `playMacro`Plays the macro in the current slot (without the need to specify 180 | a register). 181 | - `switchSlot`Cycles through the registers you specified in the configuration. 182 | Also show a notification with the slot and its content. (The currently 183 | selected slot can be seen in the |nvim-recorder-status-line-component|.) 184 | - `editMacro`Edit the macro recorded in the active slot. (Be aware that these 185 | are the keystrokes in "encoded" form.) 186 | - `yankMacro`Copies the current macro in decoded form that can be used to 187 | create a mapping from it. Breakpoints are removed from the copied macro. 188 | - `deleteAllMacros`Copies the current macro in decoded form that can be used to 189 | 190 | 191 | [!TIP] For recursive macros (playing a macro inside a macro), you can still use 192 | the default command `@a`. 193 | 194 | ADVANCED USAGE *nvim-recorder-nvim-recorder--advanced-usage* 195 | 196 | 197 | PERFORMANCE OPTIMIZATIONS ~ 198 | 199 | Running macros with a high count can be demanding on the system and result in 200 | lags. For this reason, `nvim-recorder` provides some performance optimizations 201 | that are temporarily enabled when a macro with a high count is run. 202 | 203 | Note that these optimizations do have some potential drawbacks. - 204 | |`lazyredraw`| disables redrawing of the screen, which makes it harder to 205 | notice edge cases not considered in the macro. It may also appear as if the 206 | screen is frozen for a while. - Disabling the system clipboard is mostly safe, 207 | if you do not intend to copy content to it with the macro. - Ignoring 208 | auto-commands is not recommended, when you rely on certain plugin functionality 209 | during the macro, since it can potentially disrupt those plugins’ effect. 210 | 211 | 212 | MACRO BREAKPOINTS ~ 213 | 214 | `nvim-recorder` allows you to set breakpoints in your macros which can be 215 | helpful for debugging macros. Breakpoints are automatically ignored when you 216 | trigger the macro with a count. 217 | 218 | **Setting Breakpoints** - _During a recording:_ press the `addBreakPoint` key 219 | (default: `##`) in normal mode - _After a recording:_ use `editMacro` and add 220 | or remove the `##` manually. 221 | 222 | **Playing Macros with Breakpoints** - Using the `playMacro` key, the macro 223 | automatically stops at the next breakpoint. The next time you press 224 | `playMacro`, the next segment of the macro is played. - Starting a new 225 | recording, editing a macro, yanking a macro, or switching macro slot all reset 226 | the sequence, meaning that `playMacro` starts from the beginning again. 227 | 228 | 229 | [!TIP] You can do other things in between playing segments of the macro, like 230 | moving a few characters to the left or right. That way you can also use 231 | breakpoints to manually correct irregularities. 232 | **Ignoring Breakpoints** When you play the macro with a _count_ (for example 233 | `50Q`), breakpoints are automatically ignored. 234 | 235 | 236 | [!TIP] Add a count of 1 (`1Q`) to play a macro once and still ignore 237 | breakpoints. 238 | **Shared Keybindings with nvim-dap** If you are using nvim-dap 239 | , you can use `dapSharedKeymaps = 240 | true` to set up the following shared keybindings: 1. `addBreakPoint` maps to 241 | `dap.toggle_breakpoint()` outside a recording. During a recording, it adds a 242 | macro breakpoint instead. 2. `playMacro` maps to `dap.continue()` if there is 243 | at least one DAP-breakpoint. If there is no DAP-breakpoint, plays the current 244 | macro-slot instead. 245 | 246 | Note that this feature is experimental, since the respective API from nvim-dap 247 | is non-public and can be changed without deprecation notice 248 | . 249 | 250 | 251 | LAZY-LOADING THE PLUGIN ~ 252 | 253 | `nvim-recorder` is best lazy-loaded on the mappings for `startStopRecording` 254 | and `playMacro`. However, adding the status line components to `lualine` will 255 | cause the plugin to load before you start or play a recording. 256 | 257 | To avoid this, the statusline components need to be loaded only in the 258 | plugin’s `config`. The drawback of this method is that no component is shown 259 | when until you start or play a recording (which you can completely disregard 260 | when you set `clear = true`, though). 261 | 262 | Nonetheless, the plugin is pretty lightweight (~400 lines of code), so not 263 | lazy-loading it should not have a big impact. 264 | 265 | >lua 266 | -- minimal config for lazy-loading with lazy.nvim 267 | { 268 | "chrisgrieser/nvim-recorder", 269 | dependencies = "rcarriga/nvim-notify", 270 | keys = { 271 | -- these must match the keys in the mapping config below 272 | { "q", desc = " Start Recording" }, 273 | { "Q", desc = " Play Recording" }, 274 | }, 275 | config = function() 276 | require("recorder").setup({ 277 | mapping = { 278 | startStopRecording = "q", 279 | playMacro = "Q", 280 | }, 281 | }) 282 | 283 | local lualineZ = require("lualine").get_config().sections.lualine_z or {} 284 | local lualineY = require("lualine").get_config().sections.lualine_y or {} 285 | table.insert(lualineZ, { require("recorder").recordingStatus }) 286 | table.insert(lualineY, { require("recorder").displaySlots }) 287 | 288 | require("lualine").setup { 289 | tabline = { 290 | lualine_y = lualineY, 291 | lualine_z = lualineZ, 292 | }, 293 | } 294 | end, 295 | }, 296 | < 297 | 298 | 299 | ABOUT THE DEVELOPER *nvim-recorder-nvim-recorder--about-the-developer* 300 | 301 | In my day job, I am a sociologist studying the social mechanisms underlying the 302 | digital economy. For my PhD project, I investigate the governance of the app 303 | economy and how software ecosystems manage the tension between innovation and 304 | compatibility. If you are interested in this subject, feel free to get in 305 | touch. 306 | 307 | - Website 308 | - Mastodon 309 | - ResearchGate 310 | - LinkedIn 311 | 312 | 313 | 314 | Generated by panvimdoc 315 | 316 | vim:tw=78:ts=8:noet:ft=help:norl: 317 | -------------------------------------------------------------------------------- /lua/recorder.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local fn = vim.fn 4 | local v = vim.v 5 | local opt = vim.opt 6 | local keymap = vim.keymap.set 7 | 8 | -- internal vars 9 | local config, macroRegs, slotIndex, defaultLogLevel, breakCounter, firstRun 10 | 11 | -- Use this function to normalize keycodes (which can have multiple 12 | -- representations, e.g. or ). 13 | ---@param mapping string 14 | local normalizeKeycodes = function(mapping) 15 | return fn.keytrans(vim.api.nvim_replace_termcodes(mapping, true, true, true)) 16 | end 17 | 18 | local getMacro = function(reg) 19 | -- Some keys (e.g. ) have different representations when they are recorded 20 | -- versus when they are a result of vim.api.nvim_replace_termcodes (for example). 21 | -- This ensures that whenever we are manually doing something with register contents, 22 | -- they are always consistent. 23 | return vim.api.nvim_replace_termcodes(fn.keytrans(vim.fn.getreg(reg)), true, true, true) 24 | end 25 | local setMacro = function(reg, recording) vim.fn.setreg(reg, recording, "c") end 26 | 27 | -- vars which can be set by the user 28 | local toggleKey, breakPointKey, dapSharedKeymaps, lessNotifications, useNerdfontIcons 29 | local perf = {} 30 | 31 | -------------------------------------------------------------------------------- 32 | 33 | ---@param msg string 34 | ---@param level? 0|1|2|3|4 vim.log.levels 35 | ---@param importance "essential"|"nonessential" 36 | ---@param extraOpts? table 37 | local function notify(msg, importance, level, extraOpts) 38 | if importance == "nonessential" and lessNotifications then return end 39 | if not level then level = defaultLogLevel end 40 | local opts = vim.tbl_deep_extend("force", { title = "nvim-recorder" }, extraOpts or {}) 41 | vim.notify(msg, level, opts) 42 | end 43 | 44 | ---@return boolean 45 | local function isRecording() return fn.reg_recording() ~= "" end 46 | 47 | ---@return boolean 48 | local function isPlaying() return fn.reg_executing() ~= "" end 49 | 50 | ---runs `:normal` natively with bang 51 | ---@param cmdStr string 52 | local function normal(cmdStr) vim.cmd.normal { cmdStr, bang = true } end 53 | 54 | -------------------------------------------------------------------------------- 55 | -- COMMANDS 56 | 57 | -- start/stop recording macro into the current slot 58 | local function toggleRecording() 59 | if config.dynamicSlots == "rotate" and not firstRun and not isRecording() then 60 | slotIndex = slotIndex + 1 61 | if slotIndex > #macroRegs then slotIndex = 1 end 62 | end 63 | if firstRun then firstRun = false end 64 | local reg = macroRegs[slotIndex] 65 | 66 | -- start recording 67 | if not isRecording() then 68 | breakCounter = 0 -- reset break points 69 | normal("q" .. reg) 70 | notify("Recording to [" .. reg .. "]…", "essential") 71 | return 72 | end 73 | 74 | -- stop recording 75 | local prevRec = getMacro(macroRegs[slotIndex]) 76 | normal("q") 77 | 78 | -- NOTE the macro key records itself, so it has to be removed from the 79 | -- register. As this function has to know the variable length of the 80 | -- LHS key that triggered it, it has to be passed in via .setup()-function 81 | local decodedToggleKey = vim.api.nvim_replace_termcodes(toggleKey, true, true, true) 82 | local recording = getMacro(reg):sub(1, -1 * (#decodedToggleKey + 1)) 83 | setMacro(reg, recording) 84 | 85 | local justRecorded = fn.keytrans(getMacro(reg)) 86 | if justRecorded == "" then 87 | if config.dynamicSlots == "rotate" then slotIndex = slotIndex - 1 end 88 | setMacro(reg, prevRec) 89 | notify("Recording aborted.\n(Previous recording is kept.)", "essential") 90 | elseif not lessNotifications then 91 | notify("Recorded [" .. reg .. "]:\n" .. justRecorded, "nonessential") 92 | end 93 | end 94 | 95 | ---play the macro recorded in current slot 96 | local function playRecording() 97 | local reg = macroRegs[slotIndex] 98 | local macro = getMacro(reg) 99 | 100 | -- Guard Clause 1: Toggle Breakpoint instead of Macro 101 | -- WARN undocumented and prone to change https://github.com/mfussenegger/nvim-dap/discussions/810#discussioncomment-4623606 102 | if dapSharedKeymaps then 103 | -- nested to avoid requiring `dap` for lazyloading 104 | local dapBreakpointsExist = next(require("dap.breakpoints").get()) ~= nil 105 | if dapBreakpointsExist then 106 | require("dap").continue() 107 | return 108 | end 109 | end 110 | 111 | -- Guard Clause 2: Recursively play macro 112 | if isRecording() then 113 | -- stylua: ignore 114 | notify( 115 | "Playing the macro while it is recording would cause recursion problems. Aborting.\n" .. 116 | "(You can still use recursive macros by using `@" .. reg .. "`)", 117 | "essential", 118 | vim.log.levels.ERROR 119 | ) 120 | normal("q") -- end recording 121 | setMacro(reg, "") -- empties macro since the recursion has been recorded there 122 | return 123 | end 124 | 125 | -- Guard Clause 3: Slot is empty 126 | if macro == "" then 127 | notify("Macro Slot [" .. reg .. "] is empty.", "essential", vim.log.levels.WARN) 128 | return 129 | end 130 | 131 | -- EXECUTE MACRO 132 | local countGiven = v.count ~= 0 133 | local hasBreakPoints = fn.keytrans(macro):find(vim.pesc(breakPointKey)) 134 | local usePerfOptimizations = v.count1 >= perf.countThreshold 135 | 136 | -- macro (w/ breakpoints) 137 | if hasBreakPoints and not countGiven then 138 | breakCounter = breakCounter + 1 139 | 140 | local macroParts = {} 141 | for _, macroPart in ipairs(vim.split(fn.keytrans(macro), vim.pesc(breakPointKey), {})) do 142 | table.insert(macroParts, vim.api.nvim_replace_termcodes(macroPart, true, true, true)) 143 | end 144 | 145 | local partialMacro = macroParts[breakCounter] 146 | 147 | setMacro(reg, partialMacro) 148 | normal("@" .. reg) 149 | setMacro(reg, macro) -- restore original macro for all other purposes like prewing slots 150 | 151 | if breakCounter ~= #macroParts then 152 | notify("Reached Breakpoint #" .. tostring(breakCounter), "essential") 153 | else 154 | notify("Reached end of macro", "essential") 155 | breakCounter = 0 156 | end 157 | 158 | -- macro (w/ perf optimizations) 159 | elseif usePerfOptimizations then 160 | -- message to avoid confusion by the user due to performance optimizations 161 | local msg = "Running macro with performance optimizations…" 162 | if perf.lazyredraw then 163 | msg = msg 164 | .. "\nnvim might appear to freeze due to lazy redrawing. \nThis is to be expected and not a bug." 165 | end 166 | notify(msg, "nonessential", nil, { animate = false }) -- no animation as macro will be blocking 167 | 168 | local original = {} 169 | if perf.lazyredraw then 170 | original.lazyredraw = opt.lazyredraw:get() ---@diagnostic disable-line: undefined-field 171 | opt.lazyredraw = true 172 | end 173 | if perf.noSystemclipboard then 174 | original.clipboard = opt.clipboard:get() ---@diagnostic disable-line: undefined-field 175 | opt.clipboard = "" 176 | end 177 | original.eventignore = opt.eventignore:get() 178 | opt.eventignore = perf.autocmdEventsIgnore 179 | 180 | -- if notification is shown, defer to ensure it is displayed 181 | local count = v.count1 -- counts needs to be saved due to scoping by defer_fn 182 | vim.defer_fn(function() 183 | normal(count .. "@" .. reg) 184 | 185 | if perf.lazyredraw then vim.opt.lazyredraw = original.lazyredraw end 186 | if perf.noSystemclipboard then opt.clipboard = original.clipboard end 187 | opt.eventignore = original.eventignore 188 | end, 500) 189 | 190 | -- macro (regular) 191 | else 192 | normal(v.count1 .. "@" .. reg) 193 | end 194 | end 195 | 196 | ---changes the active slot 197 | local function switchMacroSlot() 198 | slotIndex = slotIndex + 1 199 | breakCounter = 0 -- reset breakpoint counter 200 | 201 | if slotIndex > #macroRegs then slotIndex = 1 end 202 | local reg = macroRegs[slotIndex] 203 | local currentMacro = fn.keytrans(getMacro(reg)) 204 | local msg = " Now using macro slot [" .. reg .. "]" 205 | if currentMacro ~= "" then 206 | msg = msg .. ".\n" .. currentMacro 207 | else 208 | msg = msg .. "\n(empty)" 209 | end 210 | notify(msg, "nonessential") 211 | end 212 | 213 | ---edit the current slot 214 | local function editMacro() 215 | breakCounter = 0 -- reset breakpoint counter 216 | local reg = macroRegs[slotIndex] 217 | local macroContent = getMacro(reg) 218 | local inputConfig = { 219 | prompt = "Edit Macro [" .. reg .. "]:", 220 | default = macroContent, 221 | } 222 | vim.ui.input(inputConfig, function(editedMacro) 223 | if not editedMacro then return end -- cancellation 224 | setMacro(reg, editedMacro) 225 | notify("Edited Macro [" .. reg .. "]:\n" .. editedMacro, "nonessential") 226 | end) 227 | end 228 | 229 | ---@param mode? "silent" 230 | local function deleteAllMacros(mode) 231 | breakCounter = 0 -- reset breakpoint counter 232 | for _, reg in pairs(macroRegs) do 233 | setMacro(reg, "") 234 | end 235 | if mode ~= "silent" then notify("All macros deleted.", "nonessential") end 236 | end 237 | 238 | local function yankMacro() 239 | breakCounter = 0 240 | local reg = macroRegs[slotIndex] 241 | local macroContent = fn.keytrans(getMacro(reg)) 242 | if macroContent == "" then 243 | notify( 244 | "Nothing to copy, macro slot [" .. reg .. "] is still empty.", 245 | "essential", 246 | vim.log.levels.WARN 247 | ) 248 | return 249 | end 250 | -- remove breakpoints when yanking the macro 251 | macroContent = macroContent:gsub(vim.pesc(breakPointKey), "") 252 | 253 | local clipboardOpt = opt.clipboard:get() ---@diagnostic disable-line: undefined-field 254 | local useSystemClipb = #clipboardOpt > 0 and clipboardOpt[1]:find("unnamed") 255 | local copyToReg = useSystemClipb and "+" or '"' 256 | 257 | fn.setreg(copyToReg, macroContent) 258 | notify("Copied Macro [" .. reg .. "]:\n" .. macroContent, "nonessential") 259 | end 260 | 261 | local function addBreakPoint() 262 | if isRecording() then 263 | -- INFO nothing happens, but the key is still recorded in the macro 264 | notify("Macro breakpoint added.", "essential") 265 | elseif not isPlaying() and not dapSharedKeymaps then 266 | notify("Cannot insert breakpoint outside of a recording.", "essential", vim.log.levels.WARN) 267 | elseif not isPlaying() and dapSharedKeymaps then 268 | -- only test for dap here to not interfere with user lazyloading 269 | if require("dap") then require("dap").toggle_breakpoint() end 270 | end 271 | end 272 | 273 | -------------------------------------------------------------------------------- 274 | -- CONFIG 275 | 276 | ---@class configObj 277 | ---@field slots string[] named register slots 278 | ---@field dynamicSlots string 2 states we could choose from: 279 | ---static -> use static slots 280 | ---rotate -> through letters specified in slots[] if end is encountered it goes(overwrite) from start 281 | ---@field clear boolean whether to clear slots/registers on setup 282 | ---@field timeout number Default timeout for notification 283 | ---@field mapping maps individual mappings 284 | ---@field logLevel integer log level (vim.log.levels) 285 | ---@field lessNotifications boolean plugin is less verbose, shows only essential or critical notifications 286 | ---@field performanceOpts perfOpts various performance options 287 | ---@field dapSharedKeymaps boolean (experimental) partially share keymaps with dap 288 | ---@field useNerdfontIcons boolean currently only relevant for status bar components 289 | 290 | ---@class perfOpts 291 | ---@field countThreshold number if count used is higher than threshold, the following performance optimizations are applied 292 | ---@field lazyredraw boolean :h lazyredraw 293 | ---@field noSystemClipboard boolean no `*` or `+` in clipboard https://vi.stackexchange.com/a/31888 294 | ---@field autocmdEventsIgnore string[] list of autocmd events to ignore 295 | 296 | ---@class maps 297 | ---@field startStopRecording string 298 | ---@field playMacro string 299 | ---@field editMacro string 300 | ---@field yankMacro string 301 | ---@field deleteAllMacros string 302 | ---@field switchSlot string 303 | ---@field addBreakPoint string 304 | 305 | ---@param userConfig configObj 306 | function M.setup(userConfig) 307 | -- initialize values on plugin load 308 | slotIndex = 1 309 | breakCounter = 0 310 | firstRun = true 311 | 312 | local defaultConfig = { 313 | slots = { "a", "b" }, 314 | dynamicSlots = "static", 315 | mapping = { 316 | startStopRecording = "q", 317 | playMacro = "Q", 318 | switchSlot = "", 319 | editMacro = "cq", 320 | deleteAllMacros = "dq", 321 | yankMacro = "yq", 322 | addBreakPoint = "##", 323 | }, 324 | dapSharedKeymaps = false, 325 | clear = false, 326 | logLevel = vim.log.levels.INFO, 327 | lessNotifications = false, 328 | useNerdfontIcons = true, 329 | performanceOpts = { 330 | countThreshold = 100, 331 | lazyredraw = true, 332 | noSystemClipboard = true, 333 | -- stylua: ignore 334 | autocmdEventsIgnore = { "TextChangedI", "TextChanged", "InsertLeave", "InsertEnter", "InsertCharPre" }, 335 | }, 336 | } 337 | config = vim.tbl_deep_extend("keep", userConfig, defaultConfig) 338 | 339 | -- settings to be used globally 340 | perf = config.performanceOpts 341 | useNerdfontIcons = config.useNerdfontIcons 342 | lessNotifications = config.lessNotifications 343 | defaultLogLevel = config.logLevel 344 | 345 | -- validate macro slots 346 | macroRegs = config.slots 347 | for _, reg in pairs(macroRegs) do 348 | if not (reg:find("^%l$")) then 349 | notify( 350 | ('"%s" is an invalid slot. Choose only named registers (a-z).'):format(reg), 351 | "essential", 352 | vim.log.levels.ERROR 353 | ) 354 | return 355 | end 356 | end 357 | 358 | -- clear macro slots 359 | if config.clear then deleteAllMacros("silent") end 360 | 361 | -- setup keymaps 362 | toggleKey = config.mapping.startStopRecording 363 | breakPointKey = normalizeKeycodes(config.mapping.addBreakPoint) 364 | local icon = config.useNerdfontIcons and " " or "" 365 | local dapSharedIcon = config.useNerdfontIcons and " / " or "" 366 | 367 | keymap("n", toggleKey, toggleRecording, { desc = icon .. "Start/Stop Recording" }) 368 | keymap("n", config.mapping.switchSlot, switchMacroSlot, { desc = icon .. "Switch Macro Slot" }) 369 | keymap("n", config.mapping.editMacro, editMacro, { desc = icon .. "Edit Macro" }) 370 | keymap("n", config.mapping.yankMacro, yankMacro, { desc = icon .. "Yank Macro" }) 371 | -- stylua: ignore 372 | keymap("n", config.mapping.deleteAllMacros, deleteAllMacros, { desc = icon .. "Delete All Macros" }) 373 | 374 | -- (experimental) if true, nvim-recorder and dap will use shared keymaps: 375 | -- 1) `addBreakPoint` will map to `dap.toggle_breakpoint()` outside 376 | -- a recording. During a recording, it will add a macro breakpoint instead. 377 | -- 2) `playMacro` will map to `dap.continue()` if there is at least one 378 | -- dap-breakpoint. If there is no dap breakpoint, will play the current 379 | -- macro-slot instead 380 | dapSharedKeymaps = config.dapSharedKeymaps or false 381 | local breakPointDesc = dapSharedKeymaps and dapSharedIcon .. "Breakpoint" 382 | or icon .. "Insert Macro Breakpoint." 383 | keymap("n", breakPointKey, addBreakPoint, { desc = breakPointDesc }) 384 | local playDesc = dapSharedKeymaps and dapSharedIcon .. "Continue/Play" or icon .. "Play Macro" 385 | keymap("n", config.mapping.playMacro, playRecording, { desc = playDesc }) 386 | end 387 | 388 | -------------------------------------------------------------------------------- 389 | -- STATUS LINE COMPONENTS 390 | 391 | ---returns recording status for status line plugins (e.g., used with cmdheight=0) 392 | ---@return string 393 | function M.recordingStatus() 394 | if not isRecording() then return "" end 395 | local icon = useNerdfontIcons and " " or "" 396 | return icon .. "Recording… [" .. macroRegs[slotIndex] .. "]" 397 | end 398 | 399 | ---returns non-empty for status line plugins. 400 | ---@return string 401 | function M.displaySlots() 402 | if isRecording() then return "" end 403 | local out = {} 404 | 405 | for _, reg in pairs(macroRegs) do 406 | local empty = getMacro(reg) == "" 407 | local active = macroRegs[slotIndex] == reg 408 | local hasBreakPoints = getMacro(reg):find(vim.pesc(breakPointKey)) 409 | local bpIcon = hasBreakPoints and "!" or "" 410 | 411 | if empty and active then 412 | table.insert(out, "[ ]") 413 | elseif not empty and active then 414 | table.insert(out, "[" .. reg .. bpIcon .. "]") 415 | elseif not empty and not active then 416 | table.insert(out, reg .. bpIcon) 417 | end 418 | end 419 | 420 | local output = table.concat(out) 421 | if output == "[ ]" then return "" end 422 | local icon = useNerdfontIcons and "󰃽 " or "RECs " 423 | return icon .. output 424 | end 425 | 426 | -------------------------------------------------------------------------------- 427 | 428 | return M 429 | --------------------------------------------------------------------------------