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