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 📹 badge 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