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