├── LICENSE ├── README.md ├── doc └── mini-operators.txt └── lua └── mini └── operators.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Evgeni Chasnovski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

mini.operators

2 | 3 | ### Text edit operators 4 | 5 | See more details in [Features](#features) and [Documentation](doc/mini-operators.txt). 6 | 7 | --- 8 | 9 | > [!NOTE] 10 | > This was previously hosted at a personal `echasnovski` GitHub account. It was transferred to a dedicated organization to improve long term project stability. See more details [here](https://github.com/nvim-mini/mini.nvim/discussions/1970). 11 | 12 | ⦿ This is a part of [mini.nvim](https://nvim-mini.org/mini.nvim) library. Please use [this link](https://nvim-mini.org/mini.nvim/readmes/mini-operators) if you want to mention this module. 13 | 14 | ⦿ All contributions (issues, pull requests, discussions, etc.) are done inside of 'mini.nvim'. 15 | 16 | ⦿ See [whole library documentation](https://nvim-mini.org/mini.nvim/doc/mini-nvim) to learn about general design principles, disable/configuration recipes, and more. 17 | 18 | ⦿ See [MiniMax](https://nvim-mini.org/MiniMax) for a full config example that uses this module. 19 | 20 | --- 21 | 22 | If you want to help this project grow but don't know where to start, check out [contributing guides of 'mini.nvim'](https://nvim-mini.org/mini.nvim/CONTRIBUTING) or leave a Github star for 'mini.nvim' project and/or any its standalone Git repositories. 23 | 24 | ## Demo 25 | 26 | 27 | https://github.com/nvim-mini/mini.nvim/assets/24854248/8a3656c4-c92a-4d9f-9711-8d6a751b3e5a 28 | 29 | ## Features 30 | 31 | - Operators: 32 | - Evaluate text and replace with output. 33 | - Exchange text regions. 34 | - Multiply (duplicate) text. 35 | - Replace text with register. 36 | - Sort text. 37 | 38 | - Automated configurable mappings to operate on textobject, line, selection. Can be disabled in favor of more control with `MiniOperators.make_mappings()`. 39 | 40 | - All operators support `[count]` and dot-repeat. 41 | 42 | For more information see theses parts of help: 43 | - `:h MiniOperators-overview` 44 | - `:h MiniOperators.config` 45 | 46 | ## Installation 47 | 48 | This plugin can be installed as part of 'mini.nvim' library (**recommended**) or as a standalone Git repository. 49 | 50 | There are two branches to install from: 51 | 52 | - `main` (default, **recommended**) will have latest development version of plugin. All changes since last stable release should be perceived as being in beta testing phase (meaning they already passed alpha-testing and are moderately settled). 53 | - `stable` will be updated only upon releases with code tested during public beta-testing phase in `main` branch. 54 | 55 | Here are code snippets for some common installation methods (use only one): 56 | 57 |
58 | With mini.deps 59 | 60 | - 'mini.nvim' library: 61 | 62 | | Branch | Code snippet | 63 | |--------|-----------------------------------------------| 64 | | Main | *Follow recommended ‘mini.deps’ installation* | 65 | | Stable | *Follow recommended ‘mini.deps’ installation* | 66 | 67 | - Standalone plugin: 68 | 69 | | Branch | Code snippet | 70 | |--------|---------------------------------------------------------------------| 71 | | Main | `add(‘nvim-mini/mini.operators’)` | 72 | | Stable | `add({ source = ‘nvim-mini/mini.operators’, checkout = ‘stable’ })` | 73 | 74 |
75 | 76 |
77 | With folke/lazy.nvim 78 | 79 | - 'mini.nvim' library: 80 | 81 | | Branch | Code snippet | 82 | |--------|-----------------------------------------------| 83 | | Main | `{ 'nvim-mini/mini.nvim', version = false },` | 84 | | Stable | `{ 'nvim-mini/mini.nvim', version = '*' },` | 85 | 86 | - Standalone plugin: 87 | 88 | | Branch | Code snippet | 89 | |--------|----------------------------------------------------| 90 | | Main | `{ 'nvim-mini/mini.operators', version = false },` | 91 | | Stable | `{ 'nvim-mini/mini.operators', version = '*' },` | 92 | 93 |
94 | 95 |
96 | With junegunn/vim-plug 97 | 98 | - 'mini.nvim' library: 99 | 100 | | Branch | Code snippet | 101 | |--------|------------------------------------------------------| 102 | | Main | `Plug 'nvim-mini/mini.nvim'` | 103 | | Stable | `Plug 'nvim-mini/mini.nvim', { 'branch': 'stable' }` | 104 | 105 | - Standalone plugin: 106 | 107 | | Branch | Code snippet | 108 | |--------|-----------------------------------------------------------| 109 | | Main | `Plug 'nvim-mini/mini.operators'` | 110 | | Stable | `Plug 'nvim-mini/mini.operators', { 'branch': 'stable' }` | 111 | 112 |
113 | 114 | **Important**: don't forget to call `require('mini.operators').setup()` to enable its functionality. 115 | 116 | **Note**: if you are on Windows, there might be problems with too long file paths (like `error: unable to create file : Filename too long`). Try doing one of the following: 117 | 118 | - Enable corresponding git global config value: `git config --system core.longpaths true`. Then try to reinstall. 119 | - Install plugin in other place with shorter path. 120 | 121 | ## Default config 122 | 123 | ```lua 124 | -- No need to copy this inside `setup()`. Will be used automatically. 125 | { 126 | -- Each entry configures one operator. 127 | -- `prefix` defines keys mapped during `setup()`: in Normal mode 128 | -- to operate on textobject and line, in Visual - on selection. 129 | 130 | -- Evaluate text and replace with output 131 | evaluate = { 132 | prefix = 'g=', 133 | 134 | -- Function which does the evaluation 135 | func = nil, 136 | }, 137 | 138 | -- Exchange text regions 139 | exchange = { 140 | -- NOTE: Default `gx` is remapped to `gX` 141 | prefix = 'gx', 142 | 143 | -- Whether to reindent new text to match previous indent 144 | reindent_linewise = true, 145 | }, 146 | 147 | -- Multiply (duplicate) text 148 | multiply = { 149 | prefix = 'gm', 150 | 151 | -- Function which can modify text before multiplying 152 | func = nil, 153 | }, 154 | 155 | -- Replace text with register 156 | replace = { 157 | -- NOTE: Default `gr*` LSP mappings are removed 158 | prefix = 'gr', 159 | 160 | -- Whether to reindent new text to match previous indent 161 | reindent_linewise = true, 162 | }, 163 | 164 | -- Sort text 165 | sort = { 166 | prefix = 'gs', 167 | 168 | -- Function which does the sort 169 | func = nil, 170 | } 171 | } 172 | ``` 173 | 174 | ## Similar plugins 175 | 176 | - [gbprod/substitute.nvim](https://github.com/gbprod/substitute.nvim) 177 | - [svermeulen/vim-subversive](https://github.com/svermeulen/vim-subversive) 178 | - [tommcdo/vim-exchange](https://github.com/tommcdo/vim-exchange) 179 | - [christoomey/vim-sort-motion](https://github.com/christoomey/vim-sort-motion) 180 | -------------------------------------------------------------------------------- /doc/mini-operators.txt: -------------------------------------------------------------------------------- 1 | *mini.operators* Text edit operators 2 | 3 | MIT License Copyright (c) 2023 Evgeni Chasnovski 4 | 5 | ------------------------------------------------------------------------------ 6 | *MiniOperators* 7 | Features: 8 | - Operators: 9 | - Evaluate text and replace with output. 10 | - Exchange text regions. 11 | - Multiply (duplicate) text. 12 | - Replace text with register. 13 | - Sort text. 14 | 15 | - Automated configurable mappings to operate on textobject, line, selection. 16 | Can be disabled in favor of more control with |MiniOperators.make_mappings()|. 17 | 18 | - All operators support |[count]| and dot-repeat. 19 | 20 | See |MiniOperators-overview| and |MiniOperators.config| for more details. 21 | 22 | # Setup ~ 23 | 24 | This module needs a setup with `require('mini.operators').setup({})` (replace 25 | `{}` with your `config` table). It will create global Lua table `MiniOperators` 26 | which you can use for scripting or manually (with `:lua MiniOperators.*`). 27 | 28 | See |MiniOperators.config| for available config settings. 29 | 30 | You can override runtime config settings (but not `config.mappings`) locally 31 | to buffer inside `vim.b.minioperators_config` which should have same structure 32 | as `MiniOperators.config`. See |mini.nvim-buffer-local-config| for more details. 33 | 34 | # Comparisons ~ 35 | 36 | - [gbprod/substitute.nvim](https://github.com/gbprod/substitute.nvim): 37 | - Has "replace" and "exchange" variants, but not others from this module. 38 | - Has "replace/substitute" over range functionality, while this module 39 | does not by design (it is similar to |:s| functionality while not 40 | offering significantly lower mental complexity). 41 | - "Replace" highlights pasted text, while in this module it doesn't. 42 | - "Exchange" doesn't work across buffers, while in this module it does. 43 | 44 | - [svermeulen/vim-subversive](https://github.com/svermeulen/vim-subversive): 45 | - Main inspiration for "replace" functionality, so they are mostly similar 46 | for this operator. 47 | - Has "replace/substitute" over range functionality, while this module 48 | does not by design. 49 | 50 | - [tommcdo/vim-exchange](https://github.com/tommcdo/vim-exchange): 51 | - Main inspiration for "exchange" functionality, so they are mostly 52 | similar for this operator. 53 | - Doesn't work across buffers, while this module does. 54 | 55 | - [christoomey/vim-sort-motion](https://github.com/christoomey/vim-sort-motion): 56 | - Uses |:sort| for linewise sorting, while this module uses consistent 57 | sorting algorithm (by default, see |MiniOperators.default_sort_func()|). 58 | - Sorting algorithm can't be customized, while this module allows this 59 | (see `sort.func` in |MiniOperators.config|). 60 | - For charwise region uses only commas as separators, while this module 61 | can also separate by semicolon or whitespace (by default, 62 | see |MiniOperators.default_sort_func()|). 63 | 64 | # Highlight groups ~ 65 | 66 | - `MiniOperatorsExchangeFrom` - first region to exchange. 67 | 68 | To change any highlight group, set it directly with |nvim_set_hl()|. 69 | 70 | # Disabling ~ 71 | 72 | To disable main functionality, set `vim.g.minioperators_disable` (globally) or 73 | `vim.b.minioperators_disable` (for a buffer) to `true`. Considering high number 74 | of different scenarios and customization intentions, writing exact rules 75 | for disabling module's functionality is left to user. See 76 | |mini.nvim-disabling-recipes| for common recipes. 77 | 78 | ------------------------------------------------------------------------------ 79 | *MiniOperators-overview* 80 | Operator defines an action that will be performed on a textobject, motion, 81 | or visual selection (similar to |d|, |c|, etc.). When makes sense, it can also 82 | respect supplied register (like "replace" operator). 83 | 84 | This module implements each operator in a separate dedicated function 85 | (like |MiniOperators.replace()| for "replace" operator). Each such function 86 | takes `mode` as argument and acts depending on it: 87 | 88 | - If `mode` is `nil` (or not explicitly supplied), it sets |'operatorfunc'| 89 | to this dedicated function and returns `g@` assuming being called from 90 | expression mapping. See |:map-operator| and |:map-expression| for more details. 91 | 92 | - If `mode` is "char", "line", or "block", it acts as `operatorfunc` and performs 93 | action for region between |`[| and |`]| marks. 94 | 95 | - If `mode` is "visual", it performs action for region between |`<| and |`>| marks. 96 | 97 | For more details about specific operator, see help for its function: 98 | 99 | - Evaluate: |MiniOperators.evaluate()| 100 | - Exchange: |MiniOperators.exchange()| 101 | - Multiply: |MiniOperators.multiply()| 102 | - Replace: |MiniOperators.replace()| 103 | - Sort: |MiniOperators.sort()| 104 | 105 | # Mappings ~ 106 | *MiniOperators-mappings* 107 | 108 | All operators are automatically mapped during |MiniOperators.setup()| execution. 109 | Mappings keys are deduced from `prefix` field of corresponding `config` entry. 110 | All built-in conflicting mappings are removed (like |gra|, |grn| in Neovim>=0.11). 111 | Both |gx| and |v_gx| are remapped to `gX` (if that is not already taken). 112 | 113 | For each operator the following mappings are created: 114 | 115 | - In Normal mode to operate on textobject. Uses `prefix` directly. 116 | - In Normal mode to operate on line. Appends to `prefix` the last character. 117 | This aligns with |operator-doubled| and established patterns for operators 118 | with more than two characters, like |guu|, |gUU|, etc. 119 | - In Visual mode to operate on visual selection. Uses `prefix` directly. 120 | 121 | Example of default mappings for "replace": 122 | - `gr` in Normal mode for operating on textobject. 123 | Example of usage: `griw` replaces "inner word" with default register. 124 | - `grr` in Normal mode for operating on line. 125 | Example of usage: `grr` replaces current line. 126 | - `gr` in Visual mode for operating on visual selection. 127 | Example of usage: `viw` selects "inner word" and `gr` replaces it. 128 | 129 | There are two suggested ways to customize mappings: 130 | 131 | - Change `prefix` in |MiniOperators.setup()| call. For example, doing >lua 132 | 133 | require('mini.operators').setup({ replace = { prefix = 'cr' } }) 134 | < 135 | will make mappings for `cr` / `crr` / `cr` instead of `gr` / `grr` / `gr`. 136 | 137 | - Disable automated mapping creation by supplying empty string as prefix and 138 | use |MiniOperators.make_mappings()| directly. For example: >lua 139 | 140 | -- Disable automated creation of "replace" 141 | local operators = require('mini.operators') 142 | operators.setup({ replace = { prefix = '' } }) 143 | 144 | -- Make custom mappings 145 | operators.make_mappings( 146 | 'replace', 147 | { textobject = 'cr', line = 'crr', selection = 'cr' } 148 | ) 149 | < 150 | ------------------------------------------------------------------------------ 151 | *MiniOperators.setup()* 152 | `MiniOperators.setup`({config}) 153 | Module setup 154 | 155 | Parameters ~ 156 | {config} `(table|nil)` Module config table. See |MiniOperators.config|. 157 | 158 | Usage ~ 159 | >lua 160 | require('mini.operators').setup() -- use default config 161 | -- OR 162 | require('mini.operators').setup({}) -- replace {} with your config table 163 | < 164 | ------------------------------------------------------------------------------ 165 | *MiniOperators.config* 166 | `MiniOperators.config` 167 | Defaults ~ 168 | >lua 169 | MiniOperators.config = { 170 | -- Each entry configures one operator. 171 | -- `prefix` defines keys mapped during `setup()`: in Normal mode 172 | -- to operate on textobject and line, in Visual - on selection. 173 | 174 | -- Evaluate text and replace with output 175 | evaluate = { 176 | prefix = 'g=', 177 | 178 | -- Function which does the evaluation 179 | func = nil, 180 | }, 181 | 182 | -- Exchange text regions 183 | exchange = { 184 | -- NOTE: Default `gx` is remapped to `gX` 185 | prefix = 'gx', 186 | 187 | -- Whether to reindent new text to match previous indent 188 | reindent_linewise = true, 189 | }, 190 | 191 | -- Multiply (duplicate) text 192 | multiply = { 193 | prefix = 'gm', 194 | 195 | -- Function which can modify text before multiplying 196 | func = nil, 197 | }, 198 | 199 | -- Replace text with register 200 | replace = { 201 | -- NOTE: Default `gr*` LSP mappings are removed 202 | prefix = 'gr', 203 | 204 | -- Whether to reindent new text to match previous indent 205 | reindent_linewise = true, 206 | }, 207 | 208 | -- Sort text 209 | sort = { 210 | prefix = 'gs', 211 | 212 | -- Function which does the sort 213 | func = nil, 214 | } 215 | } 216 | < 217 | # Evaluate ~ 218 | 219 | `evaluate.prefix` is a string used to automatically infer operator mappings keys 220 | during |MiniOperators.setup()|. See |MiniOperators-mappings|. 221 | 222 | `evaluate.func` is a function used to actually evaluate text region. 223 | If `nil` (default), |MiniOperators.default_evaluate_func()| is used. 224 | 225 | This function will take content table representing selected text as input 226 | and should return array of lines as output (each item per line). 227 | Content table has fields `lines`, array of region lines, and `submode`, 228 | one of `v`, `V`, `\22` (escaped ``) for charwise, linewise, and blockwise. 229 | 230 | To customize evaluation per language, set `evaluate.func` in buffer-local 231 | config (`vim.b.minioperators_config`; see |mini.nvim-buffer-local-config|). 232 | 233 | # Exchange ~ 234 | 235 | `exchange.prefix` is a string used to automatically infer operator mappings keys 236 | during |MiniOperators.setup()|. See |MiniOperators-mappings|. 237 | 238 | Note: default value "gx" overrides |gx| / |v_gx|. Instead they are remapped 239 | to `gX` (if that is not already taken). To keep using `gx` with built-in 240 | feature (open URL at cursor) choose different `config.prefix`. 241 | 242 | `exchange.reindent_linewise` is a boolean indicating whether newly put linewise 243 | text should preserve indent of replaced text. In other words, if `false`, 244 | regions are exchanged preserving their indents; if `true` - without them. 245 | 246 | # Multiply ~ 247 | 248 | `multiply.prefix` is a string used to automatically infer operator mappings keys 249 | during |MiniOperators.setup()|. See |MiniOperators-mappings|. 250 | 251 | `multiply.func` is a function used to optionally update multiplied text. 252 | If `nil` (default), text used as is. 253 | 254 | Takes content table as input (see "Evaluate" section) and should return 255 | array of lines as output. 256 | 257 | # Replace ~ 258 | 259 | `replace.prefix` is a string used to automatically infer operator mappings keys 260 | during |MiniOperators.setup()|. See |MiniOperators-mappings|. 261 | 262 | `replace.reindent_linewise` is a boolean indicating whether newly put linewise 263 | text should preserve indent of replaced text. 264 | 265 | # Sort ~ 266 | 267 | `sort.prefix` is a string used to automatically infer operator mappings keys 268 | during |MiniOperators.setup()|. See |MiniOperators-mappings|. 269 | 270 | `sort.func` is a function used to actually sort text region. 271 | If `nil` (default), |MiniOperators.default_sort_func()| is used. 272 | 273 | Takes content table as input (see "Evaluate" section) and should return 274 | array of lines as output. 275 | 276 | Example of `sort.func` which asks user for custom delimiter for charwise region: >lua 277 | 278 | local sort_func = function(content) 279 | local opts = {} 280 | if content.submode == 'v' then 281 | -- Ask for delimiter to be treated as is (not as Lua pattern) 282 | local delimiter = vim.fn.input('Sort delimiter: ') 283 | -- Treat surrounding whitespace as part of split 284 | opts.split_patterns = { '%s*' .. vim.pesc(delimiter) .. '%s*' } 285 | end 286 | return MiniOperators.default_sort_func(content, opts) 287 | end 288 | 289 | require('mini.operators').setup({ sort = { func = sort_func } }) 290 | < 291 | ------------------------------------------------------------------------------ 292 | *MiniOperators.evaluate()* 293 | `MiniOperators.evaluate`({mode}) 294 | Evaluate text and replace with output 295 | 296 | It replaces the region with the output of `config.evaluate.func`. 297 | By default it is |MiniOperators.default_evaluate_func()| which evaluates 298 | text as Lua code depending on the region submode. 299 | 300 | Parameters ~ 301 | {mode} `(string|nil)` One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 302 | 303 | ------------------------------------------------------------------------------ 304 | *MiniOperators.exchange()* 305 | `MiniOperators.exchange`({mode}) 306 | Exchange text regions 307 | 308 | Has two-step logic: 309 | - First call remembers the region as the one to be exchanged and highlights it 310 | with `MiniOperatorsExchangeFrom` highlight group. 311 | - Second call performs the exchange. Basically, a two substeps action: 312 | "yank both regions" and replace each one with another. 313 | 314 | Notes: 315 | - Use `` to stop exchanging after the first step. 316 | 317 | - Exchanged regions can have different (char,line,block)-wise submodes. 318 | 319 | - Works with most cases of intersecting regions, but not officially supported. 320 | 321 | Parameters ~ 322 | {mode} `(string|nil)` One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 323 | 324 | ------------------------------------------------------------------------------ 325 | *MiniOperators.multiply()* 326 | `MiniOperators.multiply`({mode}) 327 | Multiply (duplicate) text 328 | 329 | Copies a region (without affecting registers) and puts it directly after. 330 | 331 | Notes: 332 | - Supports two types of |[count]|: `[count1]gm[count2][textobject]` with default 333 | `config.multiply.prefix` makes `[count1]` copies of region defined by 334 | `[count2][textobject]`. Example: `2gm3aw` - 2 copies of `3aw`. 335 | 336 | - |[count]| for "line" mapping (`gmm` by default) is treated as `[count1]` from 337 | previous note. 338 | 339 | - Advantages of using this instead of "yank" + "paste": 340 | - Doesn't modify any register, while separate steps need some register to 341 | hold multiplied text. 342 | - In most cases separate steps would be "yank" + "move cursor" + "paste", 343 | while "multiply" makes it at once. 344 | 345 | Parameters ~ 346 | {mode} `(string|nil)` One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 347 | 348 | ------------------------------------------------------------------------------ 349 | *MiniOperators.replace()* 350 | `MiniOperators.replace`({mode}) 351 | Replace text with register 352 | 353 | Notes: 354 | - Supports two types of |[count]|: `[count1]gr[count2][textobject]` with default 355 | `config.replace.prefix` puts `[count1]` contents of register over region defined 356 | by `[count2][textobject]`. Example: `2gr3aw` - 2 register contents over `3aw`. 357 | 358 | - |[count]| for "line" mapping (`grr` by default) is treated as `[count1]` from 359 | previous note. 360 | 361 | - Advantages of using this instead of "visually select" + "paste with |v_P|": 362 | - As operator it is dot-repeatable which has cumulative gain in case of 363 | multiple replacing is needed. 364 | - Can automatically reindent. 365 | 366 | Parameters ~ 367 | {mode} `(string|nil)` One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 368 | 369 | ------------------------------------------------------------------------------ 370 | *MiniOperators.sort()* 371 | `MiniOperators.sort`({mode}) 372 | Sort text 373 | 374 | It replaces the region with the output of `config.sort.func`. 375 | By default it is |MiniOperators.default_sort_func()| which sorts the text 376 | depending on submode. 377 | 378 | Notes: 379 | - "line" mapping is charwise (as there is not much sense in sorting 380 | linewise a single line). This also results into no |[count]| support. 381 | 382 | Parameters ~ 383 | {mode} `(string|nil)` One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 384 | 385 | ------------------------------------------------------------------------------ 386 | *MiniOperators.make_mappings()* 387 | `MiniOperators.make_mappings`({operator_name}, {lhs_tbl}) 388 | Make operator mappings 389 | 390 | Parameters ~ 391 | {operator_name} `(string)` Name of existing operator from this module. 392 | {lhs_tbl} `(table)` Table with mappings keys. Should have these fields: 393 | - `(string)` - Normal mode mapping to operate on textobject. 394 | - `(string)` - Normal mode mapping to operate on line. 395 | Usually an alias for textobject mapping followed by |_|. 396 | For "sort" it operates charwise on whole line without left and right 397 | whitespace (as there is not much sense in sorting linewise a single line). 398 | - `(string)` - Visual mode mapping to operate on selection. 399 | 400 | Supply empty string to not create particular mapping. Note: creating `line` 401 | mapping needs `textobject` mapping to be set. 402 | 403 | Usage ~ 404 | >lua 405 | require('mini.operators').make_mappings( 406 | 'replace', 407 | { textobject = 'cr', line = 'crr', selection = 'cr' } 408 | ) 409 | < 410 | ------------------------------------------------------------------------------ 411 | *MiniOperators.default_evaluate_func()* 412 | `MiniOperators.default_evaluate_func`({content}) 413 | Default evaluate function 414 | 415 | Evaluate text as Lua code and return object from last line (like if last 416 | line is prepended with `return` if it is not already). 417 | 418 | Behavior depends on region submode: 419 | 420 | - For charwise and linewise regions, text evaluated as is. 421 | 422 | - For blockwise region, lines are evaluated per line using only first lines 423 | of outputs. This allows separate execution of lines in order to provide 424 | something different compared to linewise region. 425 | 426 | Parameters ~ 427 | {content} `(table)` Table with the following fields: 428 | - `(table)` - array with content lines. 429 | - `(string)` - region submode. One of `'v'`, `'V'`, `''` (escaped). 430 | 431 | ------------------------------------------------------------------------------ 432 | *MiniOperators.default_sort_func()* 433 | `MiniOperators.default_sort_func`({content}, {opts}) 434 | Default sort function 435 | 436 | Sort text based on region submode: 437 | 438 | - For charwise region, split by separator pattern, sort parts, merge back 439 | with separators. Actual pattern is inferred based on the array of patterns 440 | from `opts.split_patterns`: whichever element is present in the text is 441 | used, preferring the earlier one if several are present. 442 | Example: sorting "c, b; a" line with default `opts.split_patterns` results 443 | into "b; a, c" as it is split only by comma. 444 | 445 | - For linewise and blockwise regions sort lines as is. 446 | 447 | Notes: 448 | - Sort is done with |table.sort()| on an array of lines, which doesn't treat 449 | whitespace or digits specially. Use |:sort| for more complicated tasks. 450 | 451 | - Pattern is allowed to be an empty string in which case split results into 452 | all characters as parts. 453 | 454 | - Pad pattern in `split_patterns` with `%s*` to include whitespace into separator. 455 | Example: line "b _ a" with "_" pattern will be sorted as " a_b " (because 456 | it is split as "b ", "_", " a" ) while with "%s*_%s*" pattern it results 457 | into "a _ b" (split as "b", " _ ", "a"). 458 | 459 | Parameters ~ 460 | {content} `(table)` Table with the following fields: 461 | - `(table)` - array with content lines. 462 | - `(string)` - region submode. One of `'v'`, `'V'`, `''` (escaped). 463 | {opts} `(table|nil)` Options. Possible fields: 464 | - `(function)` - compare function compatible with |table.sort()|. 465 | Default: direct compare with `<`. 466 | - `(table)` - array of split Lua patterns to be used for 467 | charwise submode. Order is important. 468 | Default: `{ '%s*,%s*', '%s*;%s*', '%s+', '' }`. 469 | 470 | 471 | vim:tw=78:ts=8:noet:ft=help:norl: -------------------------------------------------------------------------------- /lua/mini/operators.lua: -------------------------------------------------------------------------------- 1 | --- *mini.operators* Text edit operators 2 | --- 3 | --- MIT License Copyright (c) 2023 Evgeni Chasnovski 4 | 5 | --- Features: 6 | --- - Operators: 7 | --- - Evaluate text and replace with output. 8 | --- - Exchange text regions. 9 | --- - Multiply (duplicate) text. 10 | --- - Replace text with register. 11 | --- - Sort text. 12 | --- 13 | --- - Automated configurable mappings to operate on textobject, line, selection. 14 | --- Can be disabled in favor of more control with |MiniOperators.make_mappings()|. 15 | --- 16 | --- - All operators support |[count]| and dot-repeat. 17 | --- 18 | --- See |MiniOperators-overview| and |MiniOperators.config| for more details. 19 | --- 20 | --- # Setup ~ 21 | --- 22 | --- This module needs a setup with `require('mini.operators').setup({})` (replace 23 | --- `{}` with your `config` table). It will create global Lua table `MiniOperators` 24 | --- which you can use for scripting or manually (with `:lua MiniOperators.*`). 25 | --- 26 | --- See |MiniOperators.config| for available config settings. 27 | --- 28 | --- You can override runtime config settings (but not `config.mappings`) locally 29 | --- to buffer inside `vim.b.minioperators_config` which should have same structure 30 | --- as `MiniOperators.config`. See |mini.nvim-buffer-local-config| for more details. 31 | --- 32 | --- # Comparisons ~ 33 | --- 34 | --- - [gbprod/substitute.nvim](https://github.com/gbprod/substitute.nvim): 35 | --- - Has "replace" and "exchange" variants, but not others from this module. 36 | --- - Has "replace/substitute" over range functionality, while this module 37 | --- does not by design (it is similar to |:s| functionality while not 38 | --- offering significantly lower mental complexity). 39 | --- - "Replace" highlights pasted text, while in this module it doesn't. 40 | --- - "Exchange" doesn't work across buffers, while in this module it does. 41 | --- 42 | --- - [svermeulen/vim-subversive](https://github.com/svermeulen/vim-subversive): 43 | --- - Main inspiration for "replace" functionality, so they are mostly similar 44 | --- for this operator. 45 | --- - Has "replace/substitute" over range functionality, while this module 46 | --- does not by design. 47 | --- 48 | --- - [tommcdo/vim-exchange](https://github.com/tommcdo/vim-exchange): 49 | --- - Main inspiration for "exchange" functionality, so they are mostly 50 | --- similar for this operator. 51 | --- - Doesn't work across buffers, while this module does. 52 | --- 53 | --- - [christoomey/vim-sort-motion](https://github.com/christoomey/vim-sort-motion): 54 | --- - Uses |:sort| for linewise sorting, while this module uses consistent 55 | --- sorting algorithm (by default, see |MiniOperators.default_sort_func()|). 56 | --- - Sorting algorithm can't be customized, while this module allows this 57 | --- (see `sort.func` in |MiniOperators.config|). 58 | --- - For charwise region uses only commas as separators, while this module 59 | --- can also separate by semicolon or whitespace (by default, 60 | --- see |MiniOperators.default_sort_func()|). 61 | --- 62 | --- # Highlight groups ~ 63 | --- 64 | --- - `MiniOperatorsExchangeFrom` - first region to exchange. 65 | --- 66 | --- To change any highlight group, set it directly with |nvim_set_hl()|. 67 | --- 68 | --- # Disabling ~ 69 | --- 70 | --- To disable main functionality, set `vim.g.minioperators_disable` (globally) or 71 | --- `vim.b.minioperators_disable` (for a buffer) to `true`. Considering high number 72 | --- of different scenarios and customization intentions, writing exact rules 73 | --- for disabling module's functionality is left to user. See 74 | --- |mini.nvim-disabling-recipes| for common recipes. 75 | ---@tag MiniOperators 76 | 77 | --- Operator defines an action that will be performed on a textobject, motion, 78 | --- or visual selection (similar to |d|, |c|, etc.). When makes sense, it can also 79 | --- respect supplied register (like "replace" operator). 80 | --- 81 | --- This module implements each operator in a separate dedicated function 82 | --- (like |MiniOperators.replace()| for "replace" operator). Each such function 83 | --- takes `mode` as argument and acts depending on it: 84 | --- 85 | --- - If `mode` is `nil` (or not explicitly supplied), it sets |'operatorfunc'| 86 | --- to this dedicated function and returns `g@` assuming being called from 87 | --- expression mapping. See |:map-operator| and |:map-expression| for more details. 88 | --- 89 | --- - If `mode` is "char", "line", or "block", it acts as `operatorfunc` and performs 90 | --- action for region between |`[| and |`]| marks. 91 | --- 92 | --- - If `mode` is "visual", it performs action for region between |`<| and |`>| marks. 93 | --- 94 | --- For more details about specific operator, see help for its function: 95 | --- 96 | --- - Evaluate: |MiniOperators.evaluate()| 97 | --- - Exchange: |MiniOperators.exchange()| 98 | --- - Multiply: |MiniOperators.multiply()| 99 | --- - Replace: |MiniOperators.replace()| 100 | --- - Sort: |MiniOperators.sort()| 101 | --- 102 | --- # Mappings ~ 103 | --- *MiniOperators-mappings* 104 | --- 105 | --- All operators are automatically mapped during |MiniOperators.setup()| execution. 106 | --- Mappings keys are deduced from `prefix` field of corresponding `config` entry. 107 | --- All built-in conflicting mappings are removed (like |gra|, |grn| in Neovim>=0.11). 108 | --- Both |gx| and |v_gx| are remapped to `gX` (if that is not already taken). 109 | --- 110 | --- For each operator the following mappings are created: 111 | --- 112 | --- - In Normal mode to operate on textobject. Uses `prefix` directly. 113 | --- - In Normal mode to operate on line. Appends to `prefix` the last character. 114 | --- This aligns with |operator-doubled| and established patterns for operators 115 | --- with more than two characters, like |guu|, |gUU|, etc. 116 | --- - In Visual mode to operate on visual selection. Uses `prefix` directly. 117 | --- 118 | --- Example of default mappings for "replace": 119 | --- - `gr` in Normal mode for operating on textobject. 120 | --- Example of usage: `griw` replaces "inner word" with default register. 121 | --- - `grr` in Normal mode for operating on line. 122 | --- Example of usage: `grr` replaces current line. 123 | --- - `gr` in Visual mode for operating on visual selection. 124 | --- Example of usage: `viw` selects "inner word" and `gr` replaces it. 125 | --- 126 | --- There are two suggested ways to customize mappings: 127 | --- 128 | --- - Change `prefix` in |MiniOperators.setup()| call. For example, doing >lua 129 | --- 130 | --- require('mini.operators').setup({ replace = { prefix = 'cr' } }) 131 | --- < 132 | --- will make mappings for `cr` / `crr` / `cr` instead of `gr` / `grr` / `gr`. 133 | --- 134 | --- - Disable automated mapping creation by supplying empty string as prefix and 135 | --- use |MiniOperators.make_mappings()| directly. For example: >lua 136 | --- 137 | --- -- Disable automated creation of "replace" 138 | --- local operators = require('mini.operators') 139 | --- operators.setup({ replace = { prefix = '' } }) 140 | --- 141 | --- -- Make custom mappings 142 | --- operators.make_mappings( 143 | --- 'replace', 144 | --- { textobject = 'cr', line = 'crr', selection = 'cr' } 145 | --- ) 146 | --- < 147 | ---@tag MiniOperators-overview 148 | 149 | ---@alias __operators_mode string|nil One of `nil`, `'char'`, `'line'`, `'block'`, `'visual'`. 150 | ---@alias __operators_content table Table with the following fields: 151 | --- - `(table)` - array with content lines. 152 | --- - `(string)` - region submode. One of `'v'`, `'V'`, `''` (escaped). 153 | 154 | ---@diagnostic disable:undefined-field 155 | ---@diagnostic disable:discard-returns 156 | ---@diagnostic disable:unused-local 157 | ---@diagnostic disable:cast-local-type 158 | 159 | -- Module definition ========================================================== 160 | local MiniOperators = {} 161 | local H = {} 162 | 163 | --- Module setup 164 | --- 165 | ---@param config table|nil Module config table. See |MiniOperators.config|. 166 | --- 167 | ---@usage >lua 168 | --- require('mini.operators').setup() -- use default config 169 | --- -- OR 170 | --- require('mini.operators').setup({}) -- replace {} with your config table 171 | --- < 172 | MiniOperators.setup = function(config) 173 | -- Export module 174 | _G.MiniOperators = MiniOperators 175 | 176 | -- Setup config 177 | config = H.setup_config(config) 178 | 179 | -- Apply config 180 | H.apply_config(config) 181 | 182 | -- Define behavior 183 | H.create_autocommands() 184 | 185 | -- Create default highlighting 186 | H.create_default_hl() 187 | end 188 | 189 | --stylua: ignore 190 | --- Defaults ~ 191 | ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) 192 | ---@text # Evaluate ~ 193 | --- 194 | --- `evaluate.prefix` is a string used to automatically infer operator mappings keys 195 | --- during |MiniOperators.setup()|. See |MiniOperators-mappings|. 196 | --- 197 | --- `evaluate.func` is a function used to actually evaluate text region. 198 | --- If `nil` (default), |MiniOperators.default_evaluate_func()| is used. 199 | --- 200 | --- This function will take content table representing selected text as input 201 | --- and should return array of lines as output (each item per line). 202 | --- Content table has fields `lines`, array of region lines, and `submode`, 203 | --- one of `v`, `V`, `\22` (escaped ``) for charwise, linewise, and blockwise. 204 | --- 205 | --- To customize evaluation per language, set `evaluate.func` in buffer-local 206 | --- config (`vim.b.minioperators_config`; see |mini.nvim-buffer-local-config|). 207 | --- 208 | --- # Exchange ~ 209 | --- 210 | --- `exchange.prefix` is a string used to automatically infer operator mappings keys 211 | --- during |MiniOperators.setup()|. See |MiniOperators-mappings|. 212 | --- 213 | --- Note: default value "gx" overrides |gx| / |v_gx|. Instead they are remapped 214 | --- to `gX` (if that is not already taken). To keep using `gx` with built-in 215 | --- feature (open URL at cursor) choose different `config.prefix`. 216 | --- 217 | --- `exchange.reindent_linewise` is a boolean indicating whether newly put linewise 218 | --- text should preserve indent of replaced text. In other words, if `false`, 219 | --- regions are exchanged preserving their indents; if `true` - without them. 220 | --- 221 | --- # Multiply ~ 222 | --- 223 | --- `multiply.prefix` is a string used to automatically infer operator mappings keys 224 | --- during |MiniOperators.setup()|. See |MiniOperators-mappings|. 225 | --- 226 | --- `multiply.func` is a function used to optionally update multiplied text. 227 | --- If `nil` (default), text used as is. 228 | --- 229 | --- Takes content table as input (see "Evaluate" section) and should return 230 | --- array of lines as output. 231 | --- 232 | --- # Replace ~ 233 | --- 234 | --- `replace.prefix` is a string used to automatically infer operator mappings keys 235 | --- during |MiniOperators.setup()|. See |MiniOperators-mappings|. 236 | --- 237 | --- `replace.reindent_linewise` is a boolean indicating whether newly put linewise 238 | --- text should preserve indent of replaced text. 239 | --- 240 | --- # Sort ~ 241 | --- 242 | --- `sort.prefix` is a string used to automatically infer operator mappings keys 243 | --- during |MiniOperators.setup()|. See |MiniOperators-mappings|. 244 | --- 245 | --- `sort.func` is a function used to actually sort text region. 246 | --- If `nil` (default), |MiniOperators.default_sort_func()| is used. 247 | --- 248 | --- Takes content table as input (see "Evaluate" section) and should return 249 | --- array of lines as output. 250 | --- 251 | --- Example of `sort.func` which asks user for custom delimiter for charwise region: >lua 252 | --- 253 | --- local sort_func = function(content) 254 | --- local opts = {} 255 | --- if content.submode == 'v' then 256 | --- -- Ask for delimiter to be treated as is (not as Lua pattern) 257 | --- local delimiter = vim.fn.input('Sort delimiter: ') 258 | --- -- Treat surrounding whitespace as part of split 259 | --- opts.split_patterns = { '%s*' .. vim.pesc(delimiter) .. '%s*' } 260 | --- end 261 | --- return MiniOperators.default_sort_func(content, opts) 262 | --- end 263 | --- 264 | --- require('mini.operators').setup({ sort = { func = sort_func } }) 265 | --- < 266 | MiniOperators.config = { 267 | -- Each entry configures one operator. 268 | -- `prefix` defines keys mapped during `setup()`: in Normal mode 269 | -- to operate on textobject and line, in Visual - on selection. 270 | 271 | -- Evaluate text and replace with output 272 | evaluate = { 273 | prefix = 'g=', 274 | 275 | -- Function which does the evaluation 276 | func = nil, 277 | }, 278 | 279 | -- Exchange text regions 280 | exchange = { 281 | -- NOTE: Default `gx` is remapped to `gX` 282 | prefix = 'gx', 283 | 284 | -- Whether to reindent new text to match previous indent 285 | reindent_linewise = true, 286 | }, 287 | 288 | -- Multiply (duplicate) text 289 | multiply = { 290 | prefix = 'gm', 291 | 292 | -- Function which can modify text before multiplying 293 | func = nil, 294 | }, 295 | 296 | -- Replace text with register 297 | replace = { 298 | -- NOTE: Default `gr*` LSP mappings are removed 299 | prefix = 'gr', 300 | 301 | -- Whether to reindent new text to match previous indent 302 | reindent_linewise = true, 303 | }, 304 | 305 | -- Sort text 306 | sort = { 307 | prefix = 'gs', 308 | 309 | -- Function which does the sort 310 | func = nil, 311 | } 312 | } 313 | --minidoc_afterlines_end 314 | 315 | --- Evaluate text and replace with output 316 | --- 317 | --- It replaces the region with the output of `config.evaluate.func`. 318 | --- By default it is |MiniOperators.default_evaluate_func()| which evaluates 319 | --- text as Lua code depending on the region submode. 320 | --- 321 | ---@param mode __operators_mode 322 | MiniOperators.evaluate = function(mode) 323 | if H.is_disabled() or not vim.bo.modifiable then return '' end 324 | 325 | -- If used without arguments inside expression mapping, set it as 326 | -- 'operatorfunc' and call it again as a result of expression mapping. 327 | if mode == nil then 328 | vim.o.operatorfunc = 'v:lua.MiniOperators.evaluate' 329 | return 'g@' 330 | end 331 | 332 | local evaluate_func = H.get_config().evaluate.func or MiniOperators.default_evaluate_func 333 | local data = H.get_region_data(mode) 334 | if data == nil then return end 335 | data.reindent_linewise = true 336 | H.apply_content_func(evaluate_func, data) 337 | end 338 | 339 | --- Exchange text regions 340 | --- 341 | --- Has two-step logic: 342 | --- - First call remembers the region as the one to be exchanged and highlights it 343 | --- with `MiniOperatorsExchangeFrom` highlight group. 344 | --- - Second call performs the exchange. Basically, a two substeps action: 345 | --- "yank both regions" and replace each one with another. 346 | --- 347 | --- Notes: 348 | --- - Use `` to stop exchanging after the first step. 349 | --- 350 | --- - Exchanged regions can have different (char,line,block)-wise submodes. 351 | --- 352 | --- - Works with most cases of intersecting regions, but not officially supported. 353 | --- 354 | ---@param mode __operators_mode 355 | MiniOperators.exchange = function(mode) 356 | if H.is_disabled() or not vim.bo.modifiable then return '' end 357 | 358 | -- If used without arguments inside expression mapping, set it as 359 | -- 'operatorfunc' and call it again as a result of expression mapping. 360 | if mode == nil then 361 | vim.o.operatorfunc = 'v:lua.MiniOperators.exchange' 362 | return 'g@' 363 | end 364 | 365 | -- Depending on present cache data, perform exchange step 366 | if not H.exchange_has_step_one() then 367 | -- Store data about first region 368 | H.cache.exchange.step_one = H.exchange_set_region_extmark(mode, true) 369 | if H.cache.exchange.step_one == nil then return end 370 | 371 | -- Temporarily remap `` to stop the exchange 372 | H.exchange_set_stop_mapping() 373 | else 374 | -- Store data about second region 375 | H.cache.exchange.step_two = H.exchange_set_region_extmark(mode, false) 376 | if H.cache.exchange.step_two == nil then return end 377 | 378 | -- Do exchange 379 | H.exchange_do() 380 | 381 | -- Stop exchange 382 | H.exchange_stop() 383 | end 384 | end 385 | 386 | --- Multiply (duplicate) text 387 | --- 388 | --- Copies a region (without affecting registers) and puts it directly after. 389 | --- 390 | --- Notes: 391 | --- - Supports two types of |[count]|: `[count1]gm[count2][textobject]` with default 392 | --- `config.multiply.prefix` makes `[count1]` copies of region defined by 393 | --- `[count2][textobject]`. Example: `2gm3aw` - 2 copies of `3aw`. 394 | --- 395 | --- - |[count]| for "line" mapping (`gmm` by default) is treated as `[count1]` from 396 | --- previous note. 397 | --- 398 | --- - Advantages of using this instead of "yank" + "paste": 399 | --- - Doesn't modify any register, while separate steps need some register to 400 | --- hold multiplied text. 401 | --- - In most cases separate steps would be "yank" + "move cursor" + "paste", 402 | --- while "multiply" makes it at once. 403 | --- 404 | ---@param mode __operators_mode 405 | MiniOperators.multiply = function(mode) 406 | if H.is_disabled() or not vim.bo.modifiable then return '' end 407 | 408 | -- If used without arguments inside expression mapping, set it as 409 | -- 'operatorfunc' and call it again as a result of expression mapping. 410 | if mode == nil then 411 | vim.o.operatorfunc = 'v:lua.MiniOperators.multiply' 412 | H.cache.multiply = { count = vim.v.count1 } 413 | 414 | -- Reset count to allow two counts: first for paste, second for textobject 415 | return vim.api.nvim_replace_termcodes('redrawg@', true, true, true) 416 | end 417 | 418 | local count = mode == 'visual' and vim.v.count1 or H.cache.multiply.count 419 | local data = H.get_region_data(mode) 420 | if data == nil then return end 421 | local mark_from, mark_to, submode = data.mark_from, data.mark_to, data.submode 422 | 423 | H.with_temp_context({ registers = { 'x', '"' } }, function() 424 | -- Yank to temporary "x" register 425 | local yank_data = { mark_from = mark_from, mark_to = mark_to, submode = submode, mode = mode, register = 'x' } 426 | H.do_between_marks('y', yank_data) 427 | 428 | -- Modify lines in "x" register 429 | local func = H.get_config().multiply.func or function(content) return content.lines end 430 | local x_reginfo = vim.fn.getreginfo('x') 431 | x_reginfo.regcontents = func({ lines = x_reginfo.regcontents, submode = submode }) 432 | vim.fn.setreg('x', x_reginfo) 433 | 434 | -- Adjust cursor for a proper paste 435 | local ref_coords = H.multiply_get_ref_coords(mark_from, mark_to, submode) 436 | vim.api.nvim_win_set_cursor(0, ref_coords) 437 | 438 | -- Paste after textobject from temporary register 439 | H.cmd_normal(count .. '"xp') 440 | 441 | -- Adjust cursor to be at start of pasted text. Not in linewise mode as it 442 | -- already is at first non-blank, while this moves to first column. 443 | if submode ~= 'V' then vim.cmd('normal! `[') end 444 | end) 445 | end 446 | 447 | --- Replace text with register 448 | --- 449 | --- Notes: 450 | --- - Supports two types of |[count]|: `[count1]gr[count2][textobject]` with default 451 | --- `config.replace.prefix` puts `[count1]` contents of register over region defined 452 | --- by `[count2][textobject]`. Example: `2gr3aw` - 2 register contents over `3aw`. 453 | --- 454 | --- - |[count]| for "line" mapping (`grr` by default) is treated as `[count1]` from 455 | --- previous note. 456 | --- 457 | --- - Advantages of using this instead of "visually select" + "paste with |v_P|": 458 | --- - As operator it is dot-repeatable which has cumulative gain in case of 459 | --- multiple replacing is needed. 460 | --- - Can automatically reindent. 461 | --- 462 | ---@param mode __operators_mode 463 | MiniOperators.replace = function(mode) 464 | if H.is_disabled() or not vim.bo.modifiable then return '' end 465 | 466 | -- If used without arguments inside expression mapping, set it as 467 | -- 'operatorfunc' and call it again as a result of expression mapping. 468 | if mode == nil then 469 | vim.o.operatorfunc = 'v:lua.MiniOperators.replace' 470 | H.cache.replace = { count = vim.v.count1, register = vim.v.register } 471 | 472 | -- Reset count to allow two counts: first for paste, second for textobject 473 | return vim.api.nvim_replace_termcodes('redrawg@', true, true, true) 474 | end 475 | 476 | -- Do replace 477 | -- - Compute `count` and `register` prior getting region data because it 478 | -- invalidates them for active Visual mode 479 | local count = mode == 'visual' and vim.v.count1 or H.cache.replace.count 480 | local register = mode == 'visual' and vim.v.register or H.cache.replace.register 481 | local data = H.get_region_data(mode) 482 | if data == nil then return '' end 483 | data.count = count 484 | data.register = register 485 | data.reindent_linewise = H.get_config().replace.reindent_linewise 486 | 487 | H.replace_do(data) 488 | 489 | return '' 490 | end 491 | 492 | --- Sort text 493 | --- 494 | --- It replaces the region with the output of `config.sort.func`. 495 | --- By default it is |MiniOperators.default_sort_func()| which sorts the text 496 | --- depending on submode. 497 | --- 498 | --- Notes: 499 | --- - "line" mapping is charwise (as there is not much sense in sorting 500 | --- linewise a single line). This also results into no |[count]| support. 501 | --- 502 | ---@param mode __operators_mode 503 | MiniOperators.sort = function(mode) 504 | if H.is_disabled() or not vim.bo.modifiable then return '' end 505 | 506 | -- If used without arguments inside expression mapping, set it as 507 | -- 'operatorfunc' and call it again as a result of expression mapping. 508 | if mode == nil then 509 | vim.o.operatorfunc = 'v:lua.MiniOperators.sort' 510 | return 'g@' 511 | end 512 | 513 | local sort_func = H.get_config().sort.func or MiniOperators.default_sort_func 514 | H.apply_content_func(sort_func, H.get_region_data(mode)) 515 | end 516 | 517 | --- Make operator mappings 518 | --- 519 | ---@param operator_name string Name of existing operator from this module. 520 | ---@param lhs_tbl table Table with mappings keys. Should have these fields: 521 | --- - `(string)` - Normal mode mapping to operate on textobject. 522 | --- - `(string)` - Normal mode mapping to operate on line. 523 | --- Usually an alias for textobject mapping followed by |_|. 524 | --- For "sort" it operates charwise on whole line without left and right 525 | --- whitespace (as there is not much sense in sorting linewise a single line). 526 | --- - `(string)` - Visual mode mapping to operate on selection. 527 | --- 528 | --- Supply empty string to not create particular mapping. Note: creating `line` 529 | --- mapping needs `textobject` mapping to be set. 530 | --- 531 | ---@usage >lua 532 | --- require('mini.operators').make_mappings( 533 | --- 'replace', 534 | --- { textobject = 'cr', line = 'crr', selection = 'cr' } 535 | --- ) 536 | --- < 537 | MiniOperators.make_mappings = function(operator_name, lhs_tbl) 538 | -- Validate arguments 539 | if not (type(operator_name) == 'string' and MiniOperators[operator_name] ~= nil) then 540 | H.error('`operator_name` should be a valid operator name.') 541 | end 542 | local is_keys_tbl = type(lhs_tbl) == 'table' 543 | and type(lhs_tbl.textobject) == 'string' 544 | and type(lhs_tbl.line) == 'string' 545 | and type(lhs_tbl.selection) == 'string' 546 | if not is_keys_tbl then H.error('`lhs_tbl` should be a valid table of keys.') end 547 | 548 | if lhs_tbl.line ~= '' and lhs_tbl.textobject == '' then 549 | H.error('Creating mapping for `line` needs mapping for `textobject`.') 550 | end 551 | 552 | -- Make mappings 553 | local operator_desc = operator_name:sub(1, 1):upper() .. operator_name:sub(2) 554 | 555 | local expr_opts = { expr = true, replace_keycodes = false, desc = operator_desc } 556 | H.map('n', lhs_tbl.textobject, string.format('v:lua.MiniOperators.%s()', operator_name), expr_opts) 557 | 558 | local rhs = lhs_tbl.textobject .. '_' 559 | -- - Make `sort()` line mapping to be charwise 560 | if operator_name == 'sort' then rhs = '^' .. lhs_tbl.textobject .. 'g_' end 561 | H.map('n', lhs_tbl.line, rhs, { remap = true, desc = operator_desc .. ' line' }) 562 | 563 | local visual_rhs = string.format([[lua MiniOperators.%s('visual')]], operator_name) 564 | H.map('x', lhs_tbl.selection, visual_rhs, { desc = operator_desc .. ' selection' }) 565 | end 566 | 567 | --- Default evaluate function 568 | --- 569 | --- Evaluate text as Lua code and return object from last line (like if last 570 | --- line is prepended with `return` if it is not already). 571 | --- 572 | --- Behavior depends on region submode: 573 | --- 574 | --- - For charwise and linewise regions, text evaluated as is. 575 | --- 576 | --- - For blockwise region, lines are evaluated per line using only first lines 577 | --- of outputs. This allows separate execution of lines in order to provide 578 | --- something different compared to linewise region. 579 | --- 580 | ---@param content __operators_content 581 | MiniOperators.default_evaluate_func = function(content) 582 | if not H.is_content(content) then H.error('`content` should be a content table.') end 583 | 584 | local lines, submode = content.lines, content.submode 585 | 586 | -- In non-blockwise mode return the result of the last line 587 | if submode ~= H.submode_keys.block then return H.eval_lua_lines(lines) end 588 | 589 | -- In blockwise selection evaluate and return each line separately 590 | return vim.tbl_map(function(l) return H.eval_lua_lines({ l })[1] end, lines) 591 | end 592 | 593 | --- Default sort function 594 | --- 595 | --- Sort text based on region submode: 596 | --- 597 | --- - For charwise region, split by separator pattern, sort parts, merge back 598 | --- with separators. Actual pattern is inferred based on the array of patterns 599 | --- from `opts.split_patterns`: whichever element is present in the text is 600 | --- used, preferring the earlier one if several are present. 601 | --- Example: sorting "c, b; a" line with default `opts.split_patterns` results 602 | --- into "b; a, c" as it is split only by comma. 603 | --- 604 | --- - For linewise and blockwise regions sort lines as is. 605 | --- 606 | --- Notes: 607 | --- - Sort is done with |table.sort()| on an array of lines, which doesn't treat 608 | --- whitespace or digits specially. Use |:sort| for more complicated tasks. 609 | --- 610 | --- - Pattern is allowed to be an empty string in which case split results into 611 | --- all characters as parts. 612 | --- 613 | --- - Pad pattern in `split_patterns` with `%s*` to include whitespace into separator. 614 | --- Example: line "b _ a" with "_" pattern will be sorted as " a_b " (because 615 | --- it is split as "b ", "_", " a" ) while with "%s*_%s*" pattern it results 616 | --- into "a _ b" (split as "b", " _ ", "a"). 617 | --- 618 | ---@param content __operators_content 619 | ---@param opts table|nil Options. Possible fields: 620 | --- - `(function)` - compare function compatible with |table.sort()|. 621 | --- Default: direct compare with `<`. 622 | --- - `(table)` - array of split Lua patterns to be used for 623 | --- charwise submode. Order is important. 624 | --- Default: `{ '%s*,%s*', '%s*;%s*', '%s+', '' }`. 625 | MiniOperators.default_sort_func = function(content, opts) 626 | if not H.is_content(content) then H.error('`content` should be a content table.') end 627 | 628 | opts = vim.tbl_deep_extend('force', { compare_fun = nil, split_patterns = nil }, opts or {}) 629 | 630 | local compare_fun = opts.compare_fun or function(a, b) return a < b end 631 | if not vim.is_callable(compare_fun) then H.error('`opts.compare_fun` should be callable.') end 632 | 633 | local split_patterns = opts.split_patterns or { '%s*,%s*', '%s*;%s*', '%s+', '' } 634 | if not H.islist(split_patterns) then H.error('`opts.split_patterns` should be array.') end 635 | 636 | -- Prepare lines to sort 637 | local lines, submode = content.lines, content.submode 638 | 639 | if submode ~= 'v' then 640 | table.sort(lines, compare_fun) 641 | return lines 642 | end 643 | 644 | local parts, seps = H.sort_charwise_split(lines, split_patterns) 645 | table.sort(parts, compare_fun) 646 | return H.sort_charwise_unsplit(parts, seps) 647 | end 648 | 649 | -- Helper data ================================================================ 650 | -- Module default config 651 | H.default_config = vim.deepcopy(MiniOperators.config) 652 | 653 | -- Namespaces 654 | H.ns_id = { 655 | exchange = vim.api.nvim_create_namespace('MiniOperatorsExchange'), 656 | } 657 | 658 | -- Cache for all operators 659 | H.cache = { 660 | exchange = {}, 661 | multiply = {}, 662 | replace = {}, 663 | } 664 | 665 | -- Submode keys for 666 | H.submode_keys = { 667 | char = 'v', 668 | line = 'V', 669 | block = vim.api.nvim_replace_termcodes('', true, true, true), 670 | } 671 | 672 | -- Helper functionality ======================================================= 673 | -- Settings ------------------------------------------------------------------- 674 | H.setup_config = function(config) 675 | H.check_type('config', config, 'table', true) 676 | config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {}) 677 | 678 | H.check_type('evaluate', config.evaluate, 'table') 679 | H.check_type('evaluate.prefix', config.evaluate.prefix, 'string') 680 | H.check_type('evaluate.func', config.evaluate.func, 'function', true) 681 | 682 | H.check_type('exchange', config.exchange, 'table') 683 | H.check_type('exchange.prefix', config.exchange.prefix, 'string') 684 | H.check_type('exchange.reindent_linewise', config.exchange.reindent_linewise, 'boolean') 685 | 686 | H.check_type('multiply', config.multiply, 'table') 687 | H.check_type('multiply.prefix', config.multiply.prefix, 'string') 688 | H.check_type('multiply.func', config.multiply.func, 'function', true) 689 | 690 | H.check_type('replace', config.replace, 'table') 691 | H.check_type('replace.prefix', config.replace.prefix, 'string') 692 | H.check_type('replace.reindent_linewise', config.replace.reindent_linewise, 'boolean') 693 | 694 | H.check_type('sort', config.sort, 'table') 695 | H.check_type('sort.prefix', config.sort.prefix, 'string') 696 | H.check_type('sort.func', config.sort.func, 'function', true) 697 | 698 | return config 699 | end 700 | 701 | H.apply_config = function(config) 702 | MiniOperators.config = config 703 | 704 | local remove_lsp_mapping = function(mode, lhs) 705 | local map_desc = vim.fn.maparg(lhs, mode, false, true).desc 706 | if map_desc == nil or string.find(map_desc, 'vim%.lsp') == nil then return end 707 | vim.keymap.del(mode, lhs) 708 | end 709 | 710 | local remap_builtin_gx = function(mode) 711 | if vim.fn.maparg('gX', mode) ~= '' then return end 712 | local keymap = vim.fn.maparg('gx', mode, false, true) 713 | local rhs = keymap.callback or keymap.rhs 714 | if rhs == nil or (keymap.desc or ''):find('URI under cursor') == nil then return end 715 | vim.keymap.set(mode, 'gX', rhs, { desc = keymap.desc }) 716 | end 717 | 718 | -- Make mappings 719 | local map_all = function(operator_name) 720 | -- Map only valid LHS 721 | local prefix = config[operator_name].prefix 722 | if type(prefix) ~= 'string' or prefix == '' then return end 723 | 724 | -- Remove conflicting built-in mappings 725 | if prefix == 'gr' and vim.fn.has('nvim-0.11') == 1 then 726 | remove_lsp_mapping('n', 'gra') 727 | remove_lsp_mapping('x', 'gra') 728 | remove_lsp_mapping('n', 'gri') 729 | remove_lsp_mapping('n', 'grn') 730 | remove_lsp_mapping('n', 'grr') 731 | remove_lsp_mapping('n', 'grt') 732 | end 733 | 734 | if prefix == 'gx' and vim.fn.has('nvim-0.10') == 1 then 735 | remap_builtin_gx('n') 736 | remap_builtin_gx('x') 737 | end 738 | 739 | local lhs_tbl = { 740 | textobject = prefix, 741 | line = prefix .. vim.fn.strcharpart(prefix, vim.fn.strchars(prefix) - 1, 1), 742 | selection = prefix, 743 | } 744 | MiniOperators.make_mappings(operator_name, lhs_tbl) 745 | end 746 | 747 | map_all('evaluate') 748 | map_all('exchange') 749 | map_all('multiply') 750 | map_all('replace') 751 | map_all('sort') 752 | end 753 | 754 | H.is_disabled = function() return vim.g.minioperators_disable == true or vim.b.minioperators_disable == true end 755 | 756 | H.get_config = function(config) 757 | return vim.tbl_deep_extend('force', MiniOperators.config, vim.b.minioperators_config or {}, config or {}) 758 | end 759 | 760 | H.create_autocommands = function() 761 | local gr = vim.api.nvim_create_augroup('MiniOperators', {}) 762 | vim.api.nvim_create_autocmd('ColorScheme', { group = gr, callback = H.create_default_hl, desc = 'Ensure colors' }) 763 | end 764 | 765 | H.create_default_hl = function() 766 | vim.api.nvim_set_hl(0, 'MiniOperatorsExchangeFrom', { default = true, link = 'IncSearch' }) 767 | end 768 | 769 | -- Evaluate ------------------------------------------------------------------- 770 | H.eval_lua_lines = function(lines) 771 | -- Copy to not modify input 772 | local lines_copy, n = vim.deepcopy(lines), #lines 773 | lines_copy[n] = (lines_copy[n]:find('^%s*return%s+') == nil and 'return ' or '') .. lines_copy[n] 774 | 775 | local str_to_eval = table.concat(lines_copy, '\n') 776 | 777 | -- Allow returning tuple with any value(s) being `nil` 778 | return H.inspect_objects(assert(loadstring(str_to_eval))()) 779 | end 780 | 781 | H.inspect_objects = function(...) 782 | local objects = {} 783 | -- Not using `{...}` because it removes `nil` input 784 | for i = 1, select('#', ...) do 785 | local v = select(i, ...) 786 | table.insert(objects, vim.inspect(v)) 787 | end 788 | 789 | return vim.split(table.concat(objects, '\n'), '\n') 790 | end 791 | 792 | -- Exchange ------------------------------------------------------------------- 793 | H.exchange_do = function() 794 | local step_one, step_two = H.cache.exchange.step_one, H.cache.exchange.step_two 795 | 796 | -- Do nothing if regions are the same 797 | if H.exchange_is_same_steps(step_one, step_two) then return end 798 | 799 | -- Save temporary registers 800 | local reg_one, reg_two = vim.fn.getreginfo('a'), vim.fn.getreginfo('b') 801 | 802 | -- Create step temporary contexts (data that should not change) 803 | local context_one = { buf_id = step_one.buf_id, marks = { 'x', 'y' }, registers = { '"' } } 804 | local context_two = { buf_id = step_two.buf_id, marks = { 'x', 'y' }, registers = { '"' } } 805 | 806 | -- Put regions into registers. NOTE: do it before actual exchange to allow 807 | -- intersecting regions. 808 | local populating_register = function(step, register) 809 | return function() 810 | H.exchange_set_step_marks(step, { 'x', 'y' }) 811 | local yank_data = 812 | { mark_from = 'x', mark_to = 'y', submode = step.submode, mode = step.mode, register = register } 813 | H.do_between_marks('y', yank_data) 814 | end 815 | end 816 | 817 | H.with_temp_context(context_one, populating_register(step_one, 'a')) 818 | H.with_temp_context(context_two, populating_register(step_two, 'b')) 819 | 820 | -- Sequentially replace 821 | local replacing = function(step, register) 822 | return function() 823 | H.exchange_set_step_marks(step, { 'x', 'y' }) 824 | 825 | local replace_data = { 826 | count = 1, 827 | mark_from = 'x', 828 | mark_to = 'y', 829 | mode = step.mode, 830 | register = register, 831 | reindent_linewise = H.get_config().exchange.reindent_linewise, 832 | submode = step.submode, 833 | } 834 | H.replace_do(replace_data) 835 | end 836 | end 837 | 838 | H.with_temp_context(context_one, replacing(step_one, 'b')) 839 | H.with_temp_context(context_two, replacing(step_two, 'a')) 840 | 841 | -- Restore temporary registers 842 | vim.fn.setreg('a', reg_one) 843 | vim.fn.setreg('b', reg_two) 844 | end 845 | 846 | H.exchange_has_step_one = function() 847 | local step_one = H.cache.exchange.step_one 848 | if type(step_one) ~= 'table' then return false end 849 | 850 | if not vim.api.nvim_buf_is_valid(step_one.buf_id) then 851 | H.exchange_stop() 852 | return false 853 | end 854 | return true 855 | end 856 | 857 | H.exchange_set_region_extmark = function(mode, add_highlight) 858 | local ns_id = H.ns_id.exchange 859 | 860 | -- Compute regular marks for target region 861 | local region_data = H.get_region_data(mode) 862 | if region_data == nil then return end 863 | local submode = region_data.submode 864 | local markcoords_from, markcoords_to = H.get_mark(region_data.mark_from), H.get_mark(region_data.mark_to) 865 | 866 | -- Compute extmark's range for target region 867 | local extmark_from = { markcoords_from[1] - 1, markcoords_from[2] } 868 | local extmark_to = { markcoords_to[1] - 1, H.get_next_char_bytecol(markcoords_to) } 869 | 870 | -- Adjust for visual selection in case of 'selection=exclusive' 871 | if region_data.mark_to == '>' and vim.o.selection == 'exclusive' then extmark_to[2] = extmark_to[2] - 1 end 872 | 873 | -- - Tweak columns for linewise marks 874 | if submode == 'V' then 875 | extmark_from[2] = 0 876 | extmark_to[2] = vim.fn.col({ extmark_to[1] + 1, '$' }) - 1 877 | end 878 | 879 | -- Set extmark to represent region. Add highlighting inside of it only if 880 | -- needed and not in blockwise submode (can't highlight that way). 881 | local buf_id = vim.api.nvim_get_current_buf() 882 | 883 | local extmark_hl_group 884 | if add_highlight and submode ~= H.submode_keys.block then extmark_hl_group = 'MiniOperatorsExchangeFrom' end 885 | 886 | local extmark_opts = { 887 | end_row = extmark_to[1], 888 | end_col = extmark_to[2], 889 | hl_group = extmark_hl_group, 890 | -- Using this gravity is better for handling empty lines in linewise mode 891 | end_right_gravity = mode == 'line', 892 | } 893 | local region_extmark_id = vim.api.nvim_buf_set_extmark(buf_id, ns_id, extmark_from[1], extmark_from[2], extmark_opts) 894 | 895 | -- - Possibly add highlighting for blockwise mode 896 | if add_highlight and extmark_hl_group == nil then 897 | -- Highlighting blockwise region needs full register type with width 898 | local opts = { regtype = H.exchange_get_blockwise_regtype(markcoords_from, markcoords_to) } 899 | H.highlight_range(buf_id, ns_id, 'MiniOperatorsExchangeFrom', extmark_from, extmark_to, opts) 900 | end 901 | 902 | -- Return data to cache 903 | return { buf_id = buf_id, mode = mode, submode = submode, extmark_id = region_extmark_id } 904 | end 905 | 906 | H.exchange_get_region_extmark = function(step) 907 | return vim.api.nvim_buf_get_extmark_by_id(step.buf_id, H.ns_id.exchange, step.extmark_id, { details = true }) 908 | end 909 | 910 | H.exchange_set_step_marks = function(step, mark_names) 911 | local extmark_details = H.exchange_get_region_extmark(step) 912 | 913 | H.set_mark(mark_names[1], { extmark_details[1] + 1, extmark_details[2] }) 914 | 915 | -- Unadjust for visual selection in case of 'selection=exclusive' 916 | local should_unadjust = step.mode == 'visual' and vim.o.selection == 'exclusive' 917 | local col_offset = should_unadjust and 1 or 0 918 | 919 | H.set_mark(mark_names[2], { extmark_details[3].end_row + 1, extmark_details[3].end_col - 1 + col_offset }) 920 | end 921 | 922 | H.exchange_get_blockwise_regtype = function(markcoords_from, markcoords_to) 923 | local f = function() 924 | -- Yank into "z" register and return its blockwise type 925 | H.set_mark('x', markcoords_from) 926 | H.set_mark('y', markcoords_to) 927 | local yank_data = { mark_from = 'x', mark_to = 'y', submode = H.submode_keys.block, mode = 'block', register = 't' } 928 | H.do_between_marks('y', yank_data) 929 | 930 | return vim.fn.getregtype('t') 931 | end 932 | 933 | return H.with_temp_context({ buf_id = 0, marks = { 'x', 'y' }, registers = { 't' } }, f) 934 | end 935 | 936 | H.exchange_stop = function() 937 | H.exchange_del_stop_mapping() 938 | 939 | local cur, ns_id = H.cache.exchange, H.ns_id.exchange 940 | if cur.step_one ~= nil then pcall(vim.api.nvim_buf_clear_namespace, cur.step_one.buf_id, ns_id, 0, -1) end 941 | if cur.step_two ~= nil then pcall(vim.api.nvim_buf_clear_namespace, cur.step_two.buf_id, ns_id, 0, -1) end 942 | H.cache.exchange = {} 943 | end 944 | 945 | H.exchange_set_stop_mapping = function() 946 | local lhs = '' 947 | H.cache.exchange.stop_restore_map_data = vim.fn.maparg(lhs, 'n', false, true) 948 | vim.keymap.set('n', lhs, H.exchange_stop, { desc = 'Stop exchange' }) 949 | end 950 | 951 | H.exchange_del_stop_mapping = function() 952 | local map_data = H.cache.exchange.stop_restore_map_data 953 | if map_data == nil then return end 954 | 955 | -- Try restore previous mapping if it was set 956 | if vim.tbl_count(map_data) > 0 then 957 | vim.fn.mapset('n', false, map_data) 958 | else 959 | vim.keymap.del('n', map_data.lhs or '') 960 | end 961 | end 962 | 963 | H.exchange_is_same_steps = function(step_one, step_two) 964 | if step_one.buf_id ~= step_two.buf_id or step_one.submode ~= step_two.submode then return false end 965 | -- Region's start and end should be the same 966 | local one, two = H.exchange_get_region_extmark(step_one), H.exchange_get_region_extmark(step_two) 967 | return one[1] == two[1] and one[2] == two[2] and one[3].end_row == two[3].end_row and one[3].end_col == two[3].end_col 968 | end 969 | 970 | -- Multiply ------------------------------------------------------------------- 971 | H.multiply_get_ref_coords = function(mark_from, mark_to, submode) 972 | local markcoords_from, markcoords_to = H.get_mark(mark_from), H.get_mark(mark_to) 973 | if mark_to == '>' and vim.o.selection == 'exclusive' then markcoords_to[2] = markcoords_to[2] - 1 end 974 | 975 | if submode ~= H.submode_keys.block then return markcoords_to end 976 | 977 | -- In blockwise selection go to top right corner (allowing for presence of 978 | -- multibyte characters) 979 | local row = math.min(markcoords_from[1], markcoords_to[1]) 980 | 981 | -- - "from"/"to" may not only be "top-left"/"bottom-right" but also 982 | -- "top-right" and "bottom-left" 983 | local virtcol_from = vim.fn.virtcol({ markcoords_from[1], markcoords_from[2] + 1 }) 984 | local virtcol_to = vim.fn.virtcol({ markcoords_to[1], markcoords_to[2] + 1 }) 985 | local virtcol = math.max(virtcol_from, virtcol_to) 986 | 987 | local col = vim.fn.virtcol2col(0, row, virtcol) 988 | 989 | return { row, col - 1 } 990 | end 991 | 992 | -- Replace -------------------------------------------------------------------- 993 | --- Delete region between two marks and paste from register 994 | --- 995 | ---@param data table Fields: 996 | --- - (optional) - Number of times to paste. 997 | --- - - Name of "from" mark. 998 | --- - - Name of "to" mark. 999 | --- - - Operator mode. One of 'visual', 'char', 'line', 'block'. 1000 | --- - - Name of register from which to paste. 1001 | --- - - Region submode. One of 'v', 'V', '\22'. 1002 | ---@private 1003 | H.replace_do = function(data) 1004 | -- NOTE: Ideally, implementation would leverage "Visually select - press `P`" 1005 | -- approach, but it has issues with dot-repeat. The `cancel_redo()` approach 1006 | -- doesn't work probably because `P` implementation uses more than one 1007 | -- dot-repeat overwrite. 1008 | local register, submode = data.register, data.submode 1009 | local mark_from, mark_to = data.mark_from, data.mark_to 1010 | 1011 | -- Do nothing with invalid register (don't allow A-Z because they are used to 1012 | -- append to lowercase register and have no use here) 1013 | local reg_is_invalid = string.find(register, '^[0-9a-z"%-:.%%#=*+_/]$') == nil 1014 | if reg_is_invalid then H.error('Register ' .. vim.inspect(register) .. ' is invalid.') end 1015 | 1016 | -- Get reginfo and infer missing data (can be empty for special registers) 1017 | local reg_info = vim.fn.getreginfo(register) 1018 | if reg_info.regcontents == nil then H.error('Register ' .. vim.inspect(register) .. ' is empty.') end 1019 | reg_info.regtype = reg_info.regtype or 'v' 1020 | 1021 | -- Determine if region is at edge which is needed for the correct paste key 1022 | local from_line, from_col = unpack(H.get_mark(mark_from)) 1023 | local to_line, to_col = unpack(H.get_mark(mark_to)) 1024 | local edge_to_col = vim.fn.col({ to_line, '$' }) - 1 - (vim.o.selection == 'exclusive' and 0 or 1) 1025 | 1026 | local is_edge_line = submode == 'V' and to_line == vim.fn.line('$') 1027 | local is_edge_col = submode ~= 'V' and to_col == edge_to_col and vim.o.virtualedit ~= 'all' 1028 | local is_edge = is_edge_line or is_edge_col 1029 | 1030 | local covers_linewise_all_buffer = is_edge_line and from_line == 1 1031 | 1032 | -- Compute current indent if needed 1033 | local init_indent 1034 | local should_reindent = data.reindent_linewise and data.submode == 'V' and vim.o.equalprg == '' 1035 | if should_reindent then init_indent = H.get_region_indent(mark_from, mark_to) end 1036 | 1037 | -- Delete region to black whole register 1038 | -- - Delete single character in blockwise submode with inclusive motion. 1039 | -- See https://github.com/neovim/neovim/issues/24613 1040 | local is_blockwise_single_cell = submode == H.submode_keys.block and from_line == to_line and from_col == to_col 1041 | local forced_motion = is_blockwise_single_cell and 'v' or submode 1042 | 1043 | local delete_data = 1044 | { mark_from = mark_from, mark_to = mark_to, submode = forced_motion, mode = data.mode, register = '_' } 1045 | H.do_between_marks('d', delete_data) 1046 | 1047 | -- Set temporary register data to have proper submode and indent 1048 | -- NOTE: use dedicated temporary register to workaround not being able to 1049 | -- write register data into readonly registers ('%', '#', '.'). 1050 | local tmp_register, tmp_reg_info = register == '=' and '=' or 'x', vim.deepcopy(reg_info) 1051 | if tmp_reg_info.regtype:sub(1, 1) ~= submode then tmp_reg_info.regtype = submode end 1052 | if should_reindent then tmp_reg_info.regcontents = H.update_indent(tmp_reg_info.regcontents, init_indent) end 1053 | 1054 | local cache_reg_info = vim.fn.getreginfo(tmp_register) 1055 | vim.fn.setreg(tmp_register, tmp_reg_info) 1056 | 1057 | -- Paste 1058 | local expr_reg_keys = tmp_register == '=' and (reg_info.regcontents[1] .. '\r') or '' 1059 | local paste_keys = (data.count or 1) .. '"' .. tmp_register .. expr_reg_keys .. (is_edge and 'p' or 'P') 1060 | H.cmd_normal(paste_keys) 1061 | 1062 | -- Restore temporary register data 1063 | vim.fn.setreg(tmp_register, cache_reg_info) 1064 | 1065 | -- Adjust cursor to be at start mark 1066 | vim.api.nvim_win_set_cursor(0, { from_line, from_col }) 1067 | 1068 | -- Adjust for extra empty line after pasting inside empty buffer 1069 | if covers_linewise_all_buffer then vim.api.nvim_buf_set_lines(0, 0, 1, true, {}) end 1070 | end 1071 | 1072 | -- Sort ----------------------------------------------------------------------- 1073 | H.sort_charwise_split = function(lines, split_patterns) 1074 | local lines_str = table.concat(lines, '\n') 1075 | 1076 | local pat 1077 | for _, pattern in ipairs(split_patterns) do 1078 | if lines_str:find(pattern) ~= nil then 1079 | pat = pattern 1080 | break 1081 | end 1082 | end 1083 | 1084 | if pat == nil then return lines, {} end 1085 | 1086 | -- Allow pattern to be an empty string to get every character 1087 | if pat == '' then 1088 | local parts = vim.split(lines_str, '') 1089 | local seps = vim.fn['repeat']({ '' }, #parts - 1) 1090 | return parts, seps 1091 | end 1092 | 1093 | -- Split into parts and separators 1094 | local parts, seps = {}, {} 1095 | local init, n = 1, lines_str:len() 1096 | while init < n do 1097 | local sep_from, sep_to = string.find(lines_str, pat, init) 1098 | if sep_from == nil then break end 1099 | table.insert(parts, lines_str:sub(init, sep_from - 1)) 1100 | table.insert(seps, lines_str:sub(sep_from, sep_to)) 1101 | init = sep_to + 1 1102 | end 1103 | table.insert(parts, lines_str:sub(init, n)) 1104 | 1105 | return parts, seps 1106 | end 1107 | 1108 | H.sort_charwise_unsplit = function(parts, seps) 1109 | local all = {} 1110 | for i = 1, #parts do 1111 | table.insert(all, parts[i]) 1112 | table.insert(all, seps[i] or '') 1113 | end 1114 | 1115 | return vim.split(table.concat(all, ''), '\n') 1116 | end 1117 | 1118 | -- General -------------------------------------------------------------------- 1119 | H.apply_content_func = function(content_func, data) 1120 | if data == nil then return end 1121 | local mark_from, mark_to, submode = data.mark_from, data.mark_to, data.submode 1122 | local reindent_linewise = data.reindent_linewise 1123 | 1124 | H.with_temp_context({ marks = { '>' }, registers = { 'x', '"' } }, function() 1125 | -- Yank effective region content into "x" register. 1126 | data.register = 'x' 1127 | H.do_between_marks('y', data) 1128 | 1129 | -- Apply content function to register content 1130 | local reg_info = vim.fn.getreginfo('x') 1131 | local content_init = { lines = reg_info.regcontents, submode = submode } 1132 | reg_info.regcontents = content_func(content_init) 1133 | vim.fn.setreg('x', reg_info) 1134 | 1135 | -- Replace region with new register content 1136 | local replace_data = { 1137 | count = 1, 1138 | mark_from = mark_from, 1139 | mark_to = mark_to, 1140 | mode = data.mode, 1141 | register = 'x', 1142 | reindent_linewise = reindent_linewise, 1143 | submode = submode, 1144 | } 1145 | H.replace_do(replace_data) 1146 | end) 1147 | end 1148 | 1149 | H.do_between_marks = function(operator, data) 1150 | -- Force 'inclusive' selection as `` submode does not force it (while 1151 | -- `v` does). This means that in case of 'selection=exclusive' marks should 1152 | -- be adjusted prior to this. 1153 | local cache_selection = vim.o.selection 1154 | if data.mode == 'block' and vim.o.selection == 'exclusive' then vim.o.selection = 'inclusive' end 1155 | 1156 | -- Don't trigger `TextYankPost` event as these yanks are not user-facing 1157 | local is_yank = operator == 'y' 1158 | local cache_eventignore = vim.o.eventignore 1159 | if is_yank then vim.o.eventignore = 'TextYankPost' end 1160 | 1161 | -- Make sure that marks `[` and `]` don't change after `y` 1162 | local context_marks = { '<', '>' } 1163 | if is_yank then context_marks = vim.list_extend(context_marks, { '[', ']' }) end 1164 | H.with_temp_context({ marks = context_marks }, function() 1165 | local mark_from, mark_to, submode, register = data.mark_from, data.mark_to, data.submode, data.register 1166 | local keys 1167 | if data.mode == 'visual' and vim.o.selection == 'exclusive' then 1168 | keys = ('`' .. mark_from) .. submode .. ('`' .. mark_to) .. ('"' .. register .. operator) 1169 | else 1170 | keys = ('`' .. mark_from) .. ('"' .. register .. operator .. submode) .. ('`' .. mark_to) 1171 | end 1172 | 1173 | -- Make sure that outer action is dot-repeatable by cancelling effect of 1174 | -- `d` or dot-repeatable `y` 1175 | local cancel_redo = operator == 'd' or (operator == 'y' and vim.o.cpoptions:find('y') ~= nil) 1176 | H.cmd_normal(keys, { cancel_redo = cancel_redo }) 1177 | end) 1178 | 1179 | vim.o.selection = cache_selection 1180 | if is_yank then vim.o.eventignore = cache_eventignore end 1181 | end 1182 | 1183 | H.is_content = function(x) return type(x) == 'table' and H.islist(x.lines) and type(x.submode) == 'string' end 1184 | 1185 | -- Marks ---------------------------------------------------------------------- 1186 | H.get_region_data = function(mode) 1187 | local submode = H.get_submode(mode) 1188 | local selection_is_visual = mode == 'visual' 1189 | 1190 | -- Make sure that visual selection marks are relevant 1191 | if selection_is_visual and H.is_visual_mode() then vim.cmd('normal! \27') end 1192 | 1193 | local mark_from = selection_is_visual and '<' or '[' 1194 | local mark_to = selection_is_visual and '>' or ']' 1195 | 1196 | -- Detect empty region. NOTE: This doesn't work when cursor is on first line 1197 | -- and first column, but there doesn't seem to be a better way to do that. 1198 | local pos_from, pos_to = H.get_mark(mark_from), H.get_mark(mark_to) 1199 | if pos_to[1] < pos_from[1] or (pos_to[1] == pos_from[1] and pos_to[2] < pos_from[2]) then return end 1200 | 1201 | return { mode = mode, submode = submode, mark_from = mark_from, mark_to = mark_to } 1202 | end 1203 | 1204 | H.get_region_indent = function(mark_from, mark_to) 1205 | local l_from, l_to = H.get_mark(mark_from)[1], H.get_mark(mark_to)[1] 1206 | local lines = vim.api.nvim_buf_get_lines(0, l_from - 1, l_to, true) 1207 | return H.compute_indent(lines) 1208 | end 1209 | 1210 | H.get_mark = function(mark_name) return vim.api.nvim_buf_get_mark(0, mark_name) end 1211 | 1212 | H.set_mark = function(mark_name, mark_data) vim.api.nvim_buf_set_mark(0, mark_name, mark_data[1], mark_data[2], {}) end 1213 | 1214 | H.get_next_char_bytecol = function(markcoords) 1215 | local line = vim.fn.getline(markcoords[1]) 1216 | local utf_index = vim.str_utfindex(line, math.min(line:len(), markcoords[2] + 1)) 1217 | return vim.str_byteindex(line, utf_index) 1218 | end 1219 | 1220 | -- Indent --------------------------------------------------------------------- 1221 | H.compute_indent = function(lines) 1222 | local res_indent, res_indent_width = nil, math.huge 1223 | local blank_indent, blank_indent_width = nil, math.huge 1224 | for _, l in ipairs(lines) do 1225 | local cur_indent = l:match('^%s*') 1226 | local cur_indent_width = cur_indent:len() 1227 | local is_blank = cur_indent_width == l:len() 1228 | if not is_blank and cur_indent_width < res_indent_width then 1229 | res_indent, res_indent_width = cur_indent, cur_indent_width 1230 | elseif is_blank and cur_indent_width < blank_indent_width then 1231 | blank_indent, blank_indent_width = cur_indent, cur_indent_width 1232 | end 1233 | end 1234 | 1235 | return res_indent or blank_indent or '' 1236 | end 1237 | 1238 | H.update_indent = function(lines, new_indent) 1239 | -- Replace current indent with new indent without affecting blank lines 1240 | local n_cur_indent = H.compute_indent(lines):len() 1241 | return vim.tbl_map(function(l) 1242 | if l:find('^%s*$') ~= nil then return l end 1243 | return new_indent .. l:sub(n_cur_indent + 1) 1244 | end, lines) 1245 | end 1246 | 1247 | -- Utilities ------------------------------------------------------------------ 1248 | H.error = function(msg) error('(mini.operators) ' .. msg, 0) end 1249 | 1250 | H.check_type = function(name, val, ref, allow_nil) 1251 | if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end 1252 | H.error(string.format('`%s` should be %s, not %s', name, ref, type(val))) 1253 | end 1254 | 1255 | H.map = function(mode, lhs, rhs, opts) 1256 | if lhs == '' then return end 1257 | opts = vim.tbl_deep_extend('force', { silent = true }, opts or {}) 1258 | vim.keymap.set(mode, lhs, rhs, opts) 1259 | end 1260 | 1261 | H.get_submode = function(mode) 1262 | if mode == 'visual' then return H.is_visual_mode() and vim.fn.mode() or vim.fn.visualmode() end 1263 | return H.submode_keys[mode] 1264 | end 1265 | 1266 | H.is_visual_mode = function() 1267 | local cur_mode = vim.fn.mode() 1268 | return cur_mode == 'v' or cur_mode == 'V' or cur_mode == H.submode_keys.block 1269 | end 1270 | 1271 | H.with_temp_context = function(context, f) 1272 | local res 1273 | vim.api.nvim_buf_call(context.buf_id or 0, function() 1274 | -- Cache temporary data 1275 | local marks_data = {} 1276 | for _, mark_name in ipairs(context.marks or {}) do 1277 | marks_data[mark_name] = H.get_mark(mark_name) 1278 | end 1279 | 1280 | local reg_data = {} 1281 | for _, reg_name in ipairs(context.registers or {}) do 1282 | reg_data[reg_name] = vim.fn.getreginfo(reg_name) 1283 | end 1284 | 1285 | -- Perform action 1286 | res = f() 1287 | 1288 | -- Restore data 1289 | for mark_name, data in pairs(marks_data) do 1290 | pcall(H.set_mark, mark_name, data) 1291 | end 1292 | for reg_name, data in pairs(reg_data) do 1293 | pcall(vim.fn.setreg, reg_name, data) 1294 | end 1295 | end) 1296 | 1297 | return res 1298 | end 1299 | 1300 | -- A hack to restore previous dot-repeat action 1301 | H.cancel_redo = function() end 1302 | (function() 1303 | local has_ffi, ffi = pcall(require, 'ffi') 1304 | if not has_ffi then return end 1305 | local has_cancel_redo = pcall(ffi.cdef, 'void CancelRedo(void)') 1306 | if not has_cancel_redo then return end 1307 | H.cancel_redo = function() pcall(ffi.C.CancelRedo) end 1308 | end)() 1309 | 1310 | H.cmd_normal = function(command, opts) 1311 | opts = opts or {} 1312 | local cancel_redo = opts.cancel_redo 1313 | if cancel_redo == nil then cancel_redo = true end 1314 | 1315 | vim.cmd('silent keepjumps normal! ' .. command) 1316 | 1317 | if cancel_redo then H.cancel_redo() end 1318 | end 1319 | 1320 | -- TODO: Remove after compatibility with Neovim=0.9 is dropped 1321 | H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist 1322 | 1323 | -- TODO: Remove after compatibility with Neovim=0.10 is dropped 1324 | H.highlight_range = function(...) vim.hl.range(...) end 1325 | if vim.fn.has('nvim-0.11') == 0 then H.highlight_range = function(...) vim.highlight.range(...) end end 1326 | 1327 | return MiniOperators 1328 | --------------------------------------------------------------------------------