Repository: chrisgrieser/nvim-recorder
Branch: main
Commit: 7acba6f7bbaf
Files: 23
Total size: 48.3 KB
Directory structure:
gitextract_zqjnb51d/
├── .editorconfig
├── .emmyrc.json
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── nvim-type-check.yml
│ ├── panvimdoc.yml
│ ├── pr-title.yml
│ ├── rumdl-lint.yml
│ ├── stale-bot.yml
│ └── stylua.yml
├── .gitignore
├── .harper-dictionary.txt
├── .luarc.jsonc
├── .rumdl.toml
├── .stylua.toml
├── LICENSE
├── README.md
├── doc/
│ └── nvim-recorder.txt
└── lua/
└── recorder.lua
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
max_line_length = 100
end_of_line = lf
charset = utf-8
insert_final_newline = true
indent_style = tab
indent_size = 3
tab_width = 3
trim_trailing_whitespace = true
[*.{yml,yaml,scm,cff}]
indent_style = space
indent_size = 2
tab_width = 2
[*.py]
indent_style = space
indent_size = 4
tab_width = 4
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
================================================
FILE: .emmyrc.json
================================================
{
"runtime": {
"version": "LuaJIT",
"requirePattern": ["lua/?.lua", "lua/?/init.lua"]
},
"workspace": {
"library": ["$VIMRUNTIME"]
},
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json"
}
================================================
FILE: .github/FUNDING.yml
================================================
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github: chrisgrieser
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: textarea
id: bug-description
attributes:
label: Bug Description
description: A clear and concise description of the bug.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Relevant Screenshot
description: If applicable, add screenshots or a screen recording to help explain your problem.
- type: textarea
id: reproduction-steps
attributes:
label: To Reproduce
description: Steps to reproduce the problem
placeholder: |
For example:
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
validations:
required: true
- type: textarea
id: version-info
attributes:
label: neovim version
render: Text
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Make sure you have done the following
options:
- label: I have updated to the latest version of the plugin.
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature request
description: Suggest an idea
title: "Feature Request: "
labels: ["enhancement"]
body:
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: "I have read the plugin's documentation."
required: true
- label: The feature would be useful to more users than just me.
required: true
- type: textarea
id: feature-requested
attributes:
label: Feature Requested
description: A clear and concise description of the feature.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Relevant Screenshot
description: If applicable, add screenshots or a screen recording to help explain the request.
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore(dependabot): "
================================================
FILE: .github/pull_request_template.md
================================================
## Problem statement
## Proposed solution
## AI usage disclosure
## Checklist
- [ ] Variable names follow `camelCase` convention.
- [ ] All AI-generated code has been reviewed by a human.
- [ ] The `README.md` has been updated for any new or modified functionality
(the `.txt` file is auto-generated and does not need to be modified).
================================================
FILE: .github/workflows/nvim-type-check.yml
================================================
name: nvim type check
on:
push:
branches: [main]
paths: ["**.lua"]
pull_request:
paths: ["**.lua"]
jobs:
build:
name: nvim type check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: stevearc/nvim-typecheck-action@v2
================================================
FILE: .github/workflows/panvimdoc.yml
================================================
name: panvimdoc
on:
push:
branches: [main]
paths:
- README.md
- .github/workflows/panvimdoc.yml
workflow_dispatch: {} # allows manual execution
permissions:
contents: write
#───────────────────────────────────────────────────────────────────────────────
jobs:
docs:
runs-on: ubuntu-latest
name: README.md to vimdoc
steps:
- uses: actions/checkout@v6
- run: git pull # fix failure when multiple commits are pushed in succession
- run: mkdir -p doc
- name: panvimdoc
uses: kdheepak/panvimdoc@main
with:
vimdoc: ${{ github.event.repository.name }}
version: "Neovim"
demojify: true
treesitter: true
- run: git pull
- name: push changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: auto-generate vimdocs"
branch: ${{ github.head_ref }}
================================================
FILE: .github/workflows/pr-title.yml
================================================
name: PR title
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
- ready_for_review
permissions:
pull-requests: read
jobs:
semantic-pull-request:
name: Check PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
requireScope: false
subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital
types: | # add `improv` to the list of allowed types
improv
fix
feat
refactor
build
ci
style
test
chore
perf
docs
break
revert
================================================
FILE: .github/workflows/rumdl-lint.yml
================================================
name: Markdown linting via rumdl
on:
push:
branches: [main]
paths:
- "**/*.md"
- ".github/workflows/rumdl-lint.yml"
- ".rumdl.toml"
pull_request:
paths:
- "**/*.md"
jobs:
rumdl:
name: rumdl
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: rvben/rumdl@v0
with:
report-type: annotations
================================================
FILE: .github/workflows/stale-bot.yml
================================================
name: Stale bot
on:
schedule:
- cron: "18 04 * * 3"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Close stale issues
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# DOCS https://github.com/actions/stale#all-options
days-before-stale: 180
days-before-close: 7
stale-issue-label: "Stale"
stale-issue-message: |
This issue has been automatically marked as stale.
**If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open.
close-issue-message: |
This issue has been closed due to inactivity, and will not be monitored.
================================================
FILE: .github/workflows/stylua.yml
================================================
name: Stylua check
on:
push:
branches: [main]
paths: ["**.lua"]
pull_request:
paths: ["**.lua"]
jobs:
stylua:
name: Stylua
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: JohnnyMorganz/stylua-action@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --check .
================================================
FILE: .gitignore
================================================
# help-tags auto-generated by lazy.nvim
doc/tags
================================================
FILE: .harper-dictionary.txt
================================================
DAP
================================================
FILE: .luarc.jsonc
================================================
{
"runtime.version": "LuaJIT",
"workspace.library": ["$VIMRUNTIME/lua"], // nvim-lua runtime
"diagnostics": {
"unusedLocalExclude": ["_*"], // allow `_varname` for unused variables
"groupFileStatus": {
"luadoc": "Any", // require stricter annotations
"conventions": "Any" // disallow global variables
}
},
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json"
}
================================================
FILE: .rumdl.toml
================================================
# DOCS https://github.com/rvben/rumdl/blob/main/docs/global-settings.md
[global]
line-length = 80
disable = ["blanks-around-lists"] # rule of proximity
# ------------------------------------------------------------------------------
[ul-style]
style = "dash" # GitHub default & quicker to type
[ul-indent]
indent = 4 # consistent with .editorconfig
[line-length]
code-blocks = false
reflow = true # enable auto-formatting
[blanks-around-headings]
lines-below = 0 # rule of proximity
[ol-prefix]
style = "ordered"
[no-inline-html]
allowed-elements = ["a", "img", "kbd"] # badges
[emphasis-style]
style = "asterisk" # better than underscore, since not considered a word-char
[strong-style]
style = "asterisk"
[table-format]
enabled = true # opt-in rule
[heading-capitalization]
enabled = true # opt-in rule
style = "sentence_case"
ignore-words = ["nvim", "Obsidian", "Alfred"]
[toc-validation]
enabled = true # opt-in rule
# ------------------------------------------------------------------------------
[per-file-ignores]
".github/pull_request_template.md" = ["first-line-h1"]
================================================
FILE: .stylua.toml
================================================
# https://github.com/JohnnyMorganz/StyLua#options
#───────────────────────────────────────────────────────────────────────────────
syntax = "LuaJIT" # needed to support `::labels::`
column_width = 100
line_endings = "Unix"
indent_type = "Tabs"
indent_width = 3
quote_style = "AutoPreferDouble"
call_parentheses = "NoSingleTable"
collapse_simple_statement = "Always"
sort_requires.enabled = true
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Christopher Grieser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Nvim-recorder 📹
Enhance the usage of macros in Neovim.
- [Features](#features)
- [Setup](#setup)
- [Installation](#installation)
- [Configuration](#configuration)
- [Status line components](#status-line-components)
- [Basic usage](#basic-usage)
- [Advanced usage](#advanced-usage)
- [Performance optimizations](#performance-optimizations)
- [Macro breakpoints](#macro-breakpoints)
- [Lazy-loading the plugin](#lazy-loading-the-plugin)
- [About the developer](#about-the-developer)
## Features
- **Simplified controls**: One key to start and stop recording, a second key for
playing the macro. Instead of `qa … q @a @@`, you just do `q … q Q Q`.[^1]
- **Macro Breakpoints** for easier debugging of macros. Breakpoints can also be
set after the recording and are automatically ignored when triggering a macro
with a count.
- **Status line components**: Particularly useful if you use `cmdheight=0` where
the recording status is not visible.
- **Macro-to-Mapping**: Copy a macro, so you can save it as a mapping.
- **Various quality-of-life features**: notifications with macro content, the
ability to cancel a recording, a command to edit macros,
- **Performance Optimizations for large macros**: When the macro is triggered
with a high count, temporarily enable some performance improvements.
- Uses up-to-date nvim features like `vim.notify`. This means you can get
confirmation notices with plugins like
[nvim-notify](https://github.com/rcarriga/nvim-notify).
## Setup
### Installation
```lua
-- lazy.nvim
{
"chrisgrieser/nvim-recorder",
dependencies = "rcarriga/nvim-notify", -- optional
opts = {}, -- required even with default settings, since it calls `setup()`
},
-- packer
use {
"chrisgrieser/nvim-recorder",
requires = "rcarriga/nvim-notify", -- optional
config = function() require("recorder").setup() end,
}
```
Calling `setup()` (or `lazy`'s `opts`) is **required**.
### Configuration
```lua
-- default values
require("recorder").setup {
-- Named registers where macros are saved (single lowercase letters only).
-- The first register is the default register used as macro-slot after
-- startup.
slots = { "a", "b" },
-- specify one of options:
-- [static] -> use static slots, this is default behaviour
-- [rotate] -> rotates through letters specified in slots[]
dynamicSlots = "static",
mapping = {
startStopRecording = "q",
playMacro = "Q",
switchSlot = "",
editMacro = "cq",
deleteAllMacros = "dq",
yankMacro = "yq",
-- ⚠️ this should be a string you don't use in insert mode during a macro
addBreakPoint = "##",
},
-- Clears all macros-slots on startup.
clear = false,
-- Log level used for non-critical notifications; mostly relevant for nvim-notify.
-- (Note that by default, nvim-notify does not show the levels `trace` & `debug`.)
logLevel = vim.log.levels.INFO, -- :help vim.log.levels
-- If enabled, only essential notifications are sent.
-- If you do not use a plugin like nvim-notify, set this to `true`
-- to remove otherwise annoying messages.
lessNotifications = false,
-- Use nerdfont icons in the status bar components and keymap descriptions
useNerdfontIcons = true,
-- Performance optimizations for macros with high count. When `playMacro` is
-- triggered with a count higher than the threshold, nvim-recorder
-- temporarily changes changes some settings for the duration of the macro.
performanceOpts = {
countThreshold = 100,
lazyredraw = true, -- enable lazyredraw (see `:h lazyredraw`)
noSystemClipboard = true, -- remove `+`/`*` from clipboard option
autocmdEventsIgnore = { -- temporarily ignore these autocmd events
"TextChangedI",
"TextChanged",
"InsertLeave",
"InsertEnter",
"InsertCharPre",
},
},
-- [experimental] partially share keymaps with nvim-dap.
-- (See README for further explanations.)
dapSharedKeymaps = false,
}
```
If you want to handle multiple macros or use `cmdheight=0`, it is recommended to
also set up the status line components:
### Status line components
```lua
-- Indicates whether you are currently recording. Useful if you are using
-- `cmdheight=0`, where recording-status is not visible.
require("recorder").recordingStatus()
-- Displays non-empty macro-slots (registers) and indicates the selected ones.
-- Only displayed when *not* recording. Slots with breakpoints get an extra `#`.
require("recorder").displaySlots()
```
> [!TIP]
> Use with the config `clear = true` to see recordings you made this session.
Example for adding the status line components to [lualine](https://github.com/nvim-lualine/lualine.nvim):
```lua
lualine_y = {
{ require("recorder").displaySlots },
},
lualine_z = {
{ require("recorder").recordingStatus },
},
```
> [!TIP]
> Put the components in different status line segments, so they have
> a different color, making the recording status more distinguishable
> from saved recordings
## Basic usage
- `startStopRecording`: Starts recording to the current macro slot (so you do
not need to specify a register). Press again to end the recording.
- `playMacro`: Plays the macro in the current slot (without the need to specify
a register).
- `switchSlot`: Cycles through the registers you specified in the configuration.
Also show a notification with the slot and its content. (The currently
selected slot can be seen in the [status line
component](#status-line-components).)
- `editMacro`: Edit the macro recorded in the active slot. (Be aware that these
are the keystrokes in "encoded" form.)
- `yankMacro`: Copies the current macro in decoded form that can be used to
create a mapping from it. Breakpoints are removed from the copied macro.
- `deleteAllMacros`: Copies the current macro in decoded form that can be used
to
> [!TIP]
> For recursive macros (playing a macro inside a macro), you can still use
> the default command `@a`.
## Advanced usage
### Performance optimizations
Running macros with a high count can be demanding on the system and result in
lags. For this reason, `nvim-recorder` provides some performance optimizations
that are temporarily enabled when a macro with a high count is run.
Note that these optimizations do have some potential drawbacks.
- [`lazyredraw`](https://neovim.io/doc/user/options.html#'lazyredraw') disables
redrawing of the screen, which makes it harder to notice edge cases not
considered in the macro. It may also appear as if the screen is frozen for a
while.
- Disabling the system clipboard is mostly safe, if you do not intend to copy
content to it with the macro.
- Ignoring auto-commands is not recommended, when you rely on certain plugin
functionality during the macro, since it can potentially disrupt those
plugins' effect.
### Macro breakpoints
`nvim-recorder` allows you to set breakpoints in your macros which can be
helpful for debugging macros. Breakpoints are automatically ignored when you
trigger the macro with a count.
**Setting Breakpoints**
- *During a recording:* press the `addBreakPoint` key (default: `##`) in normal
mode
- *After a recording:* use `editMacro` and add or remove the `##` manually.
**Playing Macros with Breakpoints**
- Using the `playMacro` key, the macro automatically stops at the next
breakpoint. The next time you press `playMacro`, the next segment of the macro
is played.
- Starting a new recording, editing a macro, yanking a macro, or switching macro
slot all reset the sequence, meaning that `playMacro` starts from the
beginning again.
> [!TIP]
> You can do other things in between playing segments of the macro, like
> moving a few characters to the left or right. That way you can also use
> breakpoints to manually correct irregularities.
**Ignoring Breakpoints**
When you play the macro with a *count* (for example `50Q`), breakpoints are
automatically ignored.
> [!TIP]
> Add a count of 1 (`1Q`) to play a macro once and still ignore breakpoints.
**Shared Keybindings with `nvim-dap`**
If you are using [nvim-dap](https://github.com/mfussenegger/nvim-dap), you can
use `dapSharedKeymaps = true` to set up the following shared keybindings:
1. `addBreakPoint` maps to `dap.toggle_breakpoint()` outside
a recording. During a recording, it adds a macro breakpoint instead.
2. `playMacro` maps to `dap.continue()` if there is at least one
DAP-breakpoint. If there is no DAP-breakpoint, plays the current
macro-slot instead.
Note that this feature is experimental, since the [respective API from nvim-dap
is non-public and can be changed without deprecation
notice](https://github.com/mfussenegger/nvim-dap/discussions/810#discussioncomment-4623606).
### Lazy-loading the plugin
`nvim-recorder` is best lazy-loaded on the mappings for `startStopRecording` and
`playMacro`. However, adding the status line components to `lualine` will cause
the plugin to load before you start or play a recording.
To avoid this, the status line components need to be loaded only in the plugin's
`config`. The drawback of this method is that no component is shown when until
you start or play a recording (which you can completely disregard when you set
`clear = true`, though).
Nonetheless, the plugin is pretty lightweight (~400 lines of code), so not
lazy-loading it should not have a big impact.
```lua
-- minimal config for lazy-loading with lazy.nvim
{
"chrisgrieser/nvim-recorder",
dependencies = "rcarriga/nvim-notify",
keys = {
-- these must match the keys in the mapping config below
{ "q", desc = " Start Recording" },
{ "Q", desc = " Play Recording" },
},
config = function()
require("recorder").setup({
mapping = {
startStopRecording = "q",
playMacro = "Q",
},
})
local lualineZ = require("lualine").get_config().sections.lualine_z or {}
local lualineY = require("lualine").get_config().sections.lualine_y or {}
table.insert(lualineZ, { require("recorder").recordingStatus })
table.insert(lualineY, { require("recorder").displaySlots })
require("lualine").setup {
tabline = {
lualine_y = lualineY,
lualine_z = lualineZ,
},
}
end,
},
```
## About the developer
In my day job, I am a sociologist studying the social mechanisms underlying the
digital economy. For my PhD project, I investigate the governance of the app
economy and how software ecosystems manage the tension between innovation and
compatibility. If you are interested in this subject, feel free to get in touch.
- [Website](https://chris-grieser.de/)
- [Mastodon](https://pkm.social/@pseudometa)
- [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser)
- [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/)
If you find this project helpful, you can support me via [🩷 GitHub
Sponsors](https://github.com/sponsors/chrisgrieser?frequency=one-time).
[^1]: As opposed to vim, Neovim already allows you to use `Q` to [play the last
recorded macro](https://neovim.io/doc/user/repeat.html#Q). Considering this,
the simplified controls really only save you one keystroke for one-off
macros. However, as opposed to Neovim's built-in controls, you can still
keep using `Q` for playing the not-most-recently recorded macro.
================================================
FILE: doc/nvim-recorder.txt
================================================
*nvim-recorder.txt* For Neovim Last change: 2026 February 06
==============================================================================
Table of Contents *nvim-recorder-table-of-contents*
1. Nvim-recorder |nvim-recorder-nvim-recorder-|
- Features |nvim-recorder-nvim-recorder--features|
- Setup |nvim-recorder-nvim-recorder--setup|
- Basic usage |nvim-recorder-nvim-recorder--basic-usage|
- Advanced usage |nvim-recorder-nvim-recorder--advanced-usage|
- About the developer |nvim-recorder-nvim-recorder--about-the-developer|
==============================================================================
1. Nvim-recorder *nvim-recorder-nvim-recorder-*
Enhance the usage of macros in Neovim.
- |nvim-recorder-features|
- |nvim-recorder-setup|
- |nvim-recorder-installation|
- |nvim-recorder-configuration|
- |nvim-recorder-status-line-components|
- |nvim-recorder-basic-usage|
- |nvim-recorder-advanced-usage|
- |nvim-recorder-performance-optimizations|
- |nvim-recorder-macro-breakpoints|
- |nvim-recorder-lazy-loading-the-plugin|
- |nvim-recorder-about-the-developer|
FEATURES *nvim-recorder-nvim-recorder--features*
- **Simplified controls**: One key to start and stop recording, a second key for
playing the macro. Instead of `qa … q @a @@`, you just do `q … q Q Q`.
- **Macro Breakpoints** for easier debugging of macros. Breakpoints can also be
set after the recording and are automatically ignored when triggering a macro
with a count.
- **Status line components**: Particularly useful if you use `cmdheight=0` where
the recording status is not visible.
- **Macro-to-Mapping**: Copy a macro, so you can save it as a mapping.
- **Various quality-of-life features**: notifications with macro content, the
ability to cancel a recording, a command to edit macros,
- **Performance Optimizations for large macros**: When the macro is triggered
with a high count, temporarily enable some performance improvements.
- Uses up-to-date nvim features like `vim.notify`. This means you can get
confirmation notices with plugins like
nvim-notify .
SETUP *nvim-recorder-nvim-recorder--setup*
INSTALLATION ~
>lua
-- lazy.nvim
{
"chrisgrieser/nvim-recorder",
dependencies = "rcarriga/nvim-notify", -- optional
opts = {}, -- required even with default settings, since it calls `setup()`
},
-- packer
use {
"chrisgrieser/nvim-recorder",
requires = "rcarriga/nvim-notify", -- optional
config = function() require("recorder").setup() end,
}
<
Calling `setup()` (or `lazy`’s `opts`) is **required**.
CONFIGURATION ~
>lua
-- default values
require("recorder").setup {
-- Named registers where macros are saved (single lowercase letters only).
-- The first register is the default register used as macro-slot after
-- startup.
slots = { "a", "b" },
-- specify one of options:
-- [static] -> use static slots, this is default behaviour
-- [rotate] -> rotates through letters specified in slots[]
dynamicSlots = "static",
mapping = {
startStopRecording = "q",
playMacro = "Q",
switchSlot = "",
editMacro = "cq",
deleteAllMacros = "dq",
yankMacro = "yq",
-- ⚠️ this should be a string you don't use in insert mode during a macro
addBreakPoint = "##",
},
-- Clears all macros-slots on startup.
clear = false,
-- Log level used for non-critical notifications; mostly relevant for nvim-notify.
-- (Note that by default, nvim-notify does not show the levels `trace` & `debug`.)
logLevel = vim.log.levels.INFO, -- :help vim.log.levels
-- If enabled, only essential notifications are sent.
-- If you do not use a plugin like nvim-notify, set this to `true`
-- to remove otherwise annoying messages.
lessNotifications = false,
-- Use nerdfont icons in the status bar components and keymap descriptions
useNerdfontIcons = true,
-- Performance optimizations for macros with high count. When `playMacro` is
-- triggered with a count higher than the threshold, nvim-recorder
-- temporarily changes changes some settings for the duration of the macro.
performanceOpts = {
countThreshold = 100,
lazyredraw = true, -- enable lazyredraw (see `:h lazyredraw`)
noSystemClipboard = true, -- remove `+`/`*` from clipboard option
autocmdEventsIgnore = { -- temporarily ignore these autocmd events
"TextChangedI",
"TextChanged",
"InsertLeave",
"InsertEnter",
"InsertCharPre",
},
},
-- [experimental] partially share keymaps with nvim-dap.
-- (See README for further explanations.)
dapSharedKeymaps = false,
}
<
If you want to handle multiple macros or use `cmdheight=0`, it is recommended
to also set up the status line components:
STATUS LINE COMPONENTS ~
>lua
-- Indicates whether you are currently recording. Useful if you are using
-- `cmdheight=0`, where recording-status is not visible.
require("recorder").recordingStatus()
-- Displays non-empty macro-slots (registers) and indicates the selected ones.
-- Only displayed when *not* recording. Slots with breakpoints get an extra `#`.
require("recorder").displaySlots()
<
[!TIP] Use with the config `clear = true` to see recordings you made this
session.
Example for adding the status line components to lualine
:
>lua
lualine_y = {
{ require("recorder").displaySlots },
},
lualine_z = {
{ require("recorder").recordingStatus },
},
<
[!TIP] Put the components in different status line segments, so they have a
different color, making the recording status more distinguishable from saved
recordings
BASIC USAGE *nvim-recorder-nvim-recorder--basic-usage*
- `startStopRecording`: Starts recording to the current macro slot (so you do
not need to specify a register). Press again to end the recording.
- `playMacro`: Plays the macro in the current slot (without the need to specify
a register).
- `switchSlot`: Cycles through the registers you specified in the configuration.
Also show a notification with the slot and its content. (The currently
selected slot can be seen in the |nvim-recorder-status-line-component|.)
- `editMacro`: Edit the macro recorded in the active slot. (Be aware that these
are the keystrokes in "encoded" form.)
- `yankMacro`: Copies the current macro in decoded form that can be used to
create a mapping from it. Breakpoints are removed from the copied macro.
- `deleteAllMacros`: Copies the current macro in decoded form that can be used
to
[!TIP] For recursive macros (playing a macro inside a macro), you can still use
the default command `@a`.
ADVANCED USAGE *nvim-recorder-nvim-recorder--advanced-usage*
PERFORMANCE OPTIMIZATIONS ~
Running macros with a high count can be demanding on the system and result in
lags. For this reason, `nvim-recorder` provides some performance optimizations
that are temporarily enabled when a macro with a high count is run.
Note that these optimizations do have some potential drawbacks. -
|`lazyredraw`| disables redrawing of the screen, which makes it harder to
notice edge cases not considered in the macro. It may also appear as if the
screen is frozen for a while. - Disabling the system clipboard is mostly safe,
if you do not intend to copy content to it with the macro. - Ignoring
auto-commands is not recommended, when you rely on certain plugin functionality
during the macro, since it can potentially disrupt those plugins’ effect.
MACRO BREAKPOINTS ~
`nvim-recorder` allows you to set breakpoints in your macros which can be
helpful for debugging macros. Breakpoints are automatically ignored when you
trigger the macro with a count.
**Setting Breakpoints** - _During a recording:_ press the `addBreakPoint` key
(default: `##`) in normal mode - _After a recording:_ use `editMacro` and add
or remove the `##` manually.
**Playing Macros with Breakpoints** - Using the `playMacro` key, the macro
automatically stops at the next breakpoint. The next time you press
`playMacro`, the next segment of the macro is played. - Starting a new
recording, editing a macro, yanking a macro, or switching macro slot all reset
the sequence, meaning that `playMacro` starts from the beginning again.
[!TIP] You can do other things in between playing segments of the macro, like
moving a few characters to the left or right. That way you can also use
breakpoints to manually correct irregularities.
**Ignoring Breakpoints** When you play the macro with a _count_ (for example
`50Q`), breakpoints are automatically ignored.
[!TIP] Add a count of 1 (`1Q`) to play a macro once and still ignore
breakpoints.
**Shared Keybindings with nvim-dap** If you are using nvim-dap
, you can use `dapSharedKeymaps =
true` to set up the following shared keybindings: 1. `addBreakPoint` maps to
`dap.toggle_breakpoint()` outside a recording. During a recording, it adds a
macro breakpoint instead. 2. `playMacro` maps to `dap.continue()` if there is
at least one DAP-breakpoint. If there is no DAP-breakpoint, plays the current
macro-slot instead.
Note that this feature is experimental, since the respective API from nvim-dap
is non-public and can be changed without deprecation notice
.
LAZY-LOADING THE PLUGIN ~
`nvim-recorder` is best lazy-loaded on the mappings for `startStopRecording`
and `playMacro`. However, adding the status line components to `lualine` will
cause the plugin to load before you start or play a recording.
To avoid this, the status line components need to be loaded only in the
plugin’s `config`. The drawback of this method is that no component is shown
when until you start or play a recording (which you can completely disregard
when you set `clear = true`, though).
Nonetheless, the plugin is pretty lightweight (~400 lines of code), so not
lazy-loading it should not have a big impact.
>lua
-- minimal config for lazy-loading with lazy.nvim
{
"chrisgrieser/nvim-recorder",
dependencies = "rcarriga/nvim-notify",
keys = {
-- these must match the keys in the mapping config below
{ "q", desc = " Start Recording" },
{ "Q", desc = " Play Recording" },
},
config = function()
require("recorder").setup({
mapping = {
startStopRecording = "q",
playMacro = "Q",
},
})
local lualineZ = require("lualine").get_config().sections.lualine_z or {}
local lualineY = require("lualine").get_config().sections.lualine_y or {}
table.insert(lualineZ, { require("recorder").recordingStatus })
table.insert(lualineY, { require("recorder").displaySlots })
require("lualine").setup {
tabline = {
lualine_y = lualineY,
lualine_z = lualineZ,
},
}
end,
},
<
ABOUT THE DEVELOPER *nvim-recorder-nvim-recorder--about-the-developer*
In my day job, I am a sociologist studying the social mechanisms underlying the
digital economy. For my PhD project, I investigate the governance of the app
economy and how software ecosystems manage the tension between innovation and
compatibility. If you are interested in this subject, feel free to get in
touch.
- Website
- Mastodon
- ResearchGate
- LinkedIn
If you find this project helpful, you can support me via GitHub Sponsors
.
Generated by panvimdoc
vim:tw=78:ts=8:noet:ft=help:norl:
================================================
FILE: lua/recorder.lua
================================================
local M = {}
local fn = vim.fn
local v = vim.v
local opt = vim.opt
local keymap = vim.keymap.set
-- internal vars
local config, macroRegs, slotIndex, defaultLogLevel, breakCounter, firstRun
-- Use this function to normalize keycodes (which can have multiple
-- representations, e.g. or ).
---@param mapping string
local normalizeKeycodes = function(mapping)
return fn.keytrans(vim.api.nvim_replace_termcodes(mapping, true, true, true))
end
local getMacro = function(reg)
-- Some keys (e.g. ) have different representations when they are recorded
-- versus when they are a result of vim.api.nvim_replace_termcodes (for example).
-- This ensures that whenever we are manually doing something with register contents,
-- they are always consistent.
return vim.api.nvim_replace_termcodes(fn.keytrans(vim.fn.getreg(reg)), true, true, true)
end
local setMacro = function(reg, recording) vim.fn.setreg(reg, recording, "c") end
-- vars which can be set by the user
local toggleKey, breakPointKey, dapSharedKeymaps, lessNotifications, useNerdfontIcons
local perf = {}
--------------------------------------------------------------------------------
---@param msg string
---@param level? 0|1|2|3|4 vim.log.levels
---@param importance "essential"|"nonessential"
---@param extraOpts? table
local function notify(msg, importance, level, extraOpts)
if importance == "nonessential" and lessNotifications then return end
if not level then level = defaultLogLevel end
local opts = vim.tbl_deep_extend("force", { title = "nvim-recorder" }, extraOpts or {})
vim.notify(msg, level, opts)
end
---@return boolean
local function isRecording() return fn.reg_recording() ~= "" end
---@return boolean
local function isPlaying() return fn.reg_executing() ~= "" end
---runs `:normal` natively with bang
---@param cmdStr string
local function normal(cmdStr) vim.cmd.normal { cmdStr, bang = true } end
--------------------------------------------------------------------------------
-- COMMANDS
-- start/stop recording macro into the current slot
local function toggleRecording()
if config.dynamicSlots == "rotate" and not firstRun and not isRecording() then
slotIndex = slotIndex + 1
if slotIndex > #macroRegs then slotIndex = 1 end
end
if firstRun then firstRun = false end
local reg = macroRegs[slotIndex]
-- start recording
if not isRecording() then
breakCounter = 0 -- reset break points
normal("q" .. reg)
notify("Recording to [" .. reg .. "]…", "essential")
return
end
-- stop recording
local prevRec = getMacro(macroRegs[slotIndex])
normal("q")
-- NOTE the macro key records itself, so it has to be removed from the
-- register. As this function has to know the variable length of the
-- LHS key that triggered it, it has to be passed in via .setup()-function
local decodedToggleKey = vim.api.nvim_replace_termcodes(toggleKey, true, true, true)
local recording = getMacro(reg):sub(1, -1 * (#decodedToggleKey + 1))
setMacro(reg, recording)
local justRecorded = fn.keytrans(getMacro(reg))
if justRecorded == "" then
if config.dynamicSlots == "rotate" then slotIndex = slotIndex - 1 end
setMacro(reg, prevRec)
notify("Recording aborted.\n(Previous recording is kept.)", "essential")
elseif not lessNotifications then
notify("Recorded [" .. reg .. "]:\n" .. justRecorded, "nonessential")
end
end
---play the macro recorded in current slot
local function playRecording()
local reg = macroRegs[slotIndex]
local macro = getMacro(reg)
-- Guard Clause 1: Toggle Breakpoint instead of Macro
-- WARN undocumented and prone to change https://github.com/mfussenegger/nvim-dap/discussions/810#discussioncomment-4623606
if dapSharedKeymaps then
-- nested to avoid requiring `dap` for lazyloading
local dapBreakpointsExist = next(require("dap.breakpoints").get()) ~= nil
if dapBreakpointsExist then
require("dap").continue()
return
end
end
-- Guard Clause 2: Recursively play macro
if isRecording() then
-- stylua: ignore
notify(
"Playing the macro while it is recording would cause recursion problems. Aborting.\n" ..
"(You can still use recursive macros by using `@" .. reg .. "`)",
"essential",
vim.log.levels.ERROR
)
normal("q") -- end recording
setMacro(reg, "") -- empties macro since the recursion has been recorded there
return
end
-- Guard Clause 3: Slot is empty
if macro == "" then
notify("Macro Slot [" .. reg .. "] is empty.", "essential", vim.log.levels.WARN)
return
end
-- EXECUTE MACRO
local countGiven = v.count ~= 0
local hasBreakPoints = fn.keytrans(macro):find(vim.pesc(breakPointKey))
local usePerfOptimizations = v.count1 >= perf.countThreshold
-- macro (w/ breakpoints)
if hasBreakPoints and not countGiven then
breakCounter = breakCounter + 1
local macroParts = {}
for _, macroPart in ipairs(vim.split(fn.keytrans(macro), vim.pesc(breakPointKey), {})) do
table.insert(macroParts, vim.api.nvim_replace_termcodes(macroPart, true, true, true))
end
local partialMacro = macroParts[breakCounter]
setMacro(reg, partialMacro)
normal("@" .. reg)
setMacro(reg, macro) -- restore original macro for all other purposes like prewing slots
if breakCounter ~= #macroParts then
notify("Reached Breakpoint #" .. tostring(breakCounter), "essential")
else
notify("Reached end of macro", "essential")
breakCounter = 0
end
-- macro (w/ perf optimizations)
elseif usePerfOptimizations then
-- message to avoid confusion by the user due to performance optimizations
local msg = "Running macro with performance optimizations…"
if perf.lazyredraw then
msg = msg
.. "\nnvim might appear to freeze due to lazy redrawing. \nThis is to be expected and not a bug."
end
notify(msg, "nonessential", nil, { animate = false }) -- no animation as macro will be blocking
local original = {}
if perf.lazyredraw then
original.lazyredraw = opt.lazyredraw:get() ---@diagnostic disable-line: undefined-field
opt.lazyredraw = true
end
if perf.noSystemclipboard then
original.clipboard = opt.clipboard:get() ---@diagnostic disable-line: undefined-field
opt.clipboard = ""
end
original.eventignore = opt.eventignore:get()
opt.eventignore = perf.autocmdEventsIgnore
-- if notification is shown, defer to ensure it is displayed
local count = v.count1 -- counts needs to be saved due to scoping by defer_fn
vim.defer_fn(function()
normal(count .. "@" .. reg)
if perf.lazyredraw then vim.opt.lazyredraw = original.lazyredraw end
if perf.noSystemclipboard then opt.clipboard = original.clipboard end
opt.eventignore = original.eventignore
end, 500)
-- macro (regular)
else
normal(v.count1 .. "@" .. reg)
end
end
---changes the active slot
local function switchMacroSlot()
slotIndex = slotIndex + 1
breakCounter = 0 -- reset breakpoint counter
if slotIndex > #macroRegs then slotIndex = 1 end
local reg = macroRegs[slotIndex]
local currentMacro = fn.keytrans(getMacro(reg))
local msg = " Now using macro slot [" .. reg .. "]"
if currentMacro ~= "" then
msg = msg .. ".\n" .. currentMacro
else
msg = msg .. "\n(empty)"
end
notify(msg, "nonessential")
end
---edit the current slot
local function editMacro()
breakCounter = 0 -- reset breakpoint counter
local reg = macroRegs[slotIndex]
local macroContent = fn.keytrans(getMacro(reg))
local inputConfig = {
prompt = "Edit Macro [" .. reg .. "]:",
default = macroContent,
}
vim.ui.input(inputConfig, function(editedMacro)
if not editedMacro then return end -- cancellation
setMacro(reg, vim.api.nvim_replace_termcodes(editedMacro, true, true, true))
notify("Edited Macro [" .. reg .. "]:\n" .. editedMacro, "nonessential")
end)
end
---@param mode? "silent"
local function deleteAllMacros(mode)
breakCounter = 0 -- reset breakpoint counter
for _, reg in pairs(macroRegs) do
setMacro(reg, "")
end
if mode ~= "silent" then notify("All macros deleted.", "nonessential") end
end
local function yankMacro()
breakCounter = 0
local reg = macroRegs[slotIndex]
local macroContent = fn.keytrans(getMacro(reg))
if macroContent == "" then
notify(
"Nothing to copy, macro slot [" .. reg .. "] is still empty.",
"essential",
vim.log.levels.WARN
)
return
end
-- remove breakpoints when yanking the macro
macroContent = macroContent:gsub(vim.pesc(breakPointKey), "")
local clipboardOpt = opt.clipboard:get() ---@diagnostic disable-line: undefined-field
local useSystemClipb = #clipboardOpt > 0 and clipboardOpt[1]:find("unnamed")
local copyToReg = useSystemClipb and "+" or '"'
fn.setreg(copyToReg, macroContent)
notify("Copied Macro [" .. reg .. "]:\n" .. macroContent, "nonessential")
end
local function addBreakPoint()
if isRecording() then
-- INFO nothing happens, but the key is still recorded in the macro
notify("Macro breakpoint added.", "essential")
elseif not isPlaying() and not dapSharedKeymaps then
notify("Cannot insert breakpoint outside of a recording.", "essential", vim.log.levels.WARN)
elseif not isPlaying() and dapSharedKeymaps then
-- only test for dap here to not interfere with user lazyloading
if require("dap") then require("dap").toggle_breakpoint() end
end
end
--------------------------------------------------------------------------------
-- CONFIG
---@class configObj
---@field slots string[] named register slots
---@field dynamicSlots string 2 states we could choose from:
---static -> use static slots
---rotate -> through letters specified in slots[] if end is encountered it goes(overwrite) from start
---@field clear boolean whether to clear slots/registers on setup
---@field timeout number Default timeout for notification
---@field mapping maps individual mappings
---@field logLevel integer log level (vim.log.levels)
---@field lessNotifications boolean plugin is less verbose, shows only essential or critical notifications
---@field performanceOpts perfOpts various performance options
---@field dapSharedKeymaps boolean (experimental) partially share keymaps with dap
---@field useNerdfontIcons boolean currently only relevant for status bar components
---@class perfOpts
---@field countThreshold number if count used is higher than threshold, the following performance optimizations are applied
---@field lazyredraw boolean :h lazyredraw
---@field noSystemClipboard boolean no `*` or `+` in clipboard https://vi.stackexchange.com/a/31888
---@field autocmdEventsIgnore string[] list of autocmd events to ignore
---@class maps
---@field startStopRecording string
---@field playMacro string
---@field editMacro string
---@field yankMacro string
---@field deleteAllMacros string
---@field switchSlot string
---@field addBreakPoint string
---@param userConfig configObj
function M.setup(userConfig)
-- initialize values on plugin load
slotIndex = 1
breakCounter = 0
firstRun = true
local defaultConfig = {
slots = { "a", "b" },
dynamicSlots = "static",
mapping = {
startStopRecording = "q",
playMacro = "Q",
switchSlot = "",
editMacro = "cq",
deleteAllMacros = "dq",
yankMacro = "yq",
addBreakPoint = "##",
},
dapSharedKeymaps = false,
clear = false,
logLevel = vim.log.levels.INFO,
lessNotifications = false,
useNerdfontIcons = true,
performanceOpts = {
countThreshold = 100,
lazyredraw = true,
noSystemClipboard = true,
-- stylua: ignore
autocmdEventsIgnore = { "TextChangedI", "TextChanged", "InsertLeave", "InsertEnter", "InsertCharPre" },
},
}
config = vim.tbl_deep_extend("keep", userConfig, defaultConfig)
-- settings to be used globally
perf = config.performanceOpts
useNerdfontIcons = config.useNerdfontIcons
lessNotifications = config.lessNotifications
defaultLogLevel = config.logLevel
-- validate macro slots
macroRegs = config.slots
for _, reg in pairs(macroRegs) do
if not (reg:find("^%l$")) then
notify(
('"%s" is an invalid slot. Choose only named registers (a-z).'):format(reg),
"essential",
vim.log.levels.ERROR
)
return
end
end
-- clear macro slots
if config.clear then deleteAllMacros("silent") end
-- setup keymaps
toggleKey = config.mapping.startStopRecording
breakPointKey = normalizeKeycodes(config.mapping.addBreakPoint)
local icon = config.useNerdfontIcons and " " or ""
local dapSharedIcon = config.useNerdfontIcons and " / " or ""
keymap("n", toggleKey, toggleRecording, { desc = icon .. "Start/Stop Recording" })
keymap("n", config.mapping.switchSlot, switchMacroSlot, { desc = icon .. "Switch Macro Slot" })
keymap("n", config.mapping.editMacro, editMacro, { desc = icon .. "Edit Macro" })
keymap("n", config.mapping.yankMacro, yankMacro, { desc = icon .. "Yank Macro" })
-- stylua: ignore
keymap("n", config.mapping.deleteAllMacros, deleteAllMacros, { desc = icon .. "Delete All Macros" })
-- (experimental) if true, nvim-recorder and dap will use shared keymaps:
-- 1) `addBreakPoint` will map to `dap.toggle_breakpoint()` outside
-- a recording. During a recording, it will add a macro breakpoint instead.
-- 2) `playMacro` will map to `dap.continue()` if there is at least one
-- dap-breakpoint. If there is no dap breakpoint, will play the current
-- macro-slot instead
dapSharedKeymaps = config.dapSharedKeymaps or false
local breakPointDesc = dapSharedKeymaps and dapSharedIcon .. "Breakpoint"
or icon .. "Insert Macro Breakpoint."
keymap("n", breakPointKey, addBreakPoint, { desc = breakPointDesc })
local playDesc = dapSharedKeymaps and dapSharedIcon .. "Continue/Play" or icon .. "Play Macro"
keymap("n", config.mapping.playMacro, playRecording, { desc = playDesc })
end
--------------------------------------------------------------------------------
-- STATUS LINE COMPONENTS
---returns recording status for status line plugins (e.g., used with cmdheight=0)
---@return string
function M.recordingStatus()
if not isRecording() then return "" end
local icon = useNerdfontIcons and " " or ""
return icon .. "Recording… [" .. macroRegs[slotIndex] .. "]"
end
---returns non-empty for status line plugins.
---@return string
function M.displaySlots()
if isRecording() then return "" end
local out = {}
for _, reg in pairs(macroRegs) do
local empty = getMacro(reg) == ""
local active = macroRegs[slotIndex] == reg
local hasBreakPoints = getMacro(reg):find(vim.pesc(breakPointKey))
local bpIcon = hasBreakPoints and "!" or ""
if empty and active then
table.insert(out, "[ ]")
elseif not empty and active then
table.insert(out, "[" .. reg .. bpIcon .. "]")
elseif not empty and not active then
table.insert(out, reg .. bpIcon)
end
end
local output = table.concat(out)
if output == "[ ]" then return "" end
local icon = useNerdfontIcons and " " or "RECs "
return icon .. output
end
--------------------------------------------------------------------------------
return M