retrovim 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.conda/retrovim.yaml +259 -0
- package/.conda/terax.icns +0 -0
- package/.conda/terax.ico +0 -0
- package/.conda/terax.png +0 -0
- package/.github/workflows/release.yml +95 -0
- package/.gitmodules +3 -0
- package/.npm/kanata_touchcursor.js +28 -0
- package/.npm/nvim.js +26 -0
- package/.npm/yazi.js +26 -0
- package/.npm/zsh.js +17 -0
- package/.pip/kanata_touchcursor/__init__.py +28 -0
- package/.pip/nvim/__init__.py +31 -0
- package/.pip/yazi/__init__.py +31 -0
- package/.pip/zsh/__init__.py +22 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +674 -0
- package/README.md +336 -0
- package/kanata/touchcursor.kbd +164 -0
- package/nvim/init.lua +813 -0
- package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
- package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
- package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
- package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
- package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
- package/nvim/plugins/mini.nvim/.styluaignore +1 -0
- package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
- package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
- package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
- package/nvim/plugins/mini.nvim/LICENSE +21 -0
- package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
- package/nvim/plugins/mini.nvim/Makefile +51 -0
- package/nvim/plugins/mini.nvim/README.md +261 -0
- package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
- package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
- package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
- package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
- package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
- package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
- package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
- package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
- package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
- package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
- package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
- package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
- package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
- package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
- package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
- package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
- package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
- package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
- package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
- package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
- package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
- package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
- package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
- package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
- package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
- package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
- package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
- package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
- package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
- package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
- package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
- package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
- package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
- package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
- package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
- package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
- package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
- package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
- package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
- package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
- package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
- package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
- package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
- package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
- package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
- package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
- package/nvim/plugins/mini.nvim/logo.png +0 -0
- package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
- package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
- package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
- package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
- package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
- package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
- package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
- package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
- package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
- package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
- package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
- package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
- package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
- package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
- package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
- package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
- package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
- package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
- package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
- package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
- package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
- package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
- package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
- package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
- package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
- package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
- package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
- package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
- package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
- package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
- package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
- package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
- package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
- package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
- package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
- package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
- package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
- package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
- package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
- package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
- package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
- package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
- package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
- package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
- package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
- package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
- package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
- package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
- package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
- package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
- package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
- package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
- package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
- package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
- package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
- package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
- package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
- package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
- package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
- package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
- package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
- package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
- package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
- package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
- package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
- package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
- package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
- package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
- package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
- package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
- package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
- package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
- package/package.json +16 -0
- package/pyproject.toml +43 -0
- package/yazi/init.lua +12 -0
- package/yazi/keymap.toml +114 -0
- package/yazi/lesskey +5 -0
- package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
- package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
- package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
- package/yazi/theme.toml +26 -0
- package/yazi/yazi.toml +15 -0
- package/zsh/.zshrc +145 -0
- package/zsh/starship.toml +20 -0
- package/zsh/zsh-patina.toml +2 -0
|
@@ -0,0 +1,2244 @@
|
|
|
1
|
+
--- *mini.ai* Extend and create a/i textobjects
|
|
2
|
+
---
|
|
3
|
+
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
|
|
4
|
+
|
|
5
|
+
--- Enhance some builtin |text-objects| (like |a(|, |a)|, |a'|, and more),
|
|
6
|
+
--- create new ones (like `a*`, `a<Space>`, `af`, `a?`, and more), and allow
|
|
7
|
+
--- user to create their own.
|
|
8
|
+
---
|
|
9
|
+
--- Features:
|
|
10
|
+
--- - Customizable creation of `a`/`i` textobjects using Lua patterns and functions.
|
|
11
|
+
--- Supports:
|
|
12
|
+
--- - Dot-repeat.
|
|
13
|
+
--- - |v:count|.
|
|
14
|
+
--- - Different search methods (see |MiniAi.config|).
|
|
15
|
+
--- - Consecutive application (update selection without leaving Visual mode).
|
|
16
|
+
--- - Aliases for multiple textobjects.
|
|
17
|
+
---
|
|
18
|
+
--- - Comprehensive builtin textobjects (see more in |MiniAi-builtin-textobjects|):
|
|
19
|
+
--- - Balanced brackets (with and without whitespace) plus alias.
|
|
20
|
+
--- - Balanced quotes plus alias.
|
|
21
|
+
--- - Function call.
|
|
22
|
+
--- - Argument.
|
|
23
|
+
--- - Tag.
|
|
24
|
+
--- - Derived from user prompt.
|
|
25
|
+
--- - Default for anything but Latin letters (to fall back to |text-objects|).
|
|
26
|
+
---
|
|
27
|
+
--- For more textobjects see |MiniExtra.gen_ai_spec|.
|
|
28
|
+
---
|
|
29
|
+
--- - Motions for jumping to left/right edge of textobject.
|
|
30
|
+
---
|
|
31
|
+
--- - Set of specification generators to tweak some builtin textobjects (see
|
|
32
|
+
--- |MiniAi.gen_spec|).
|
|
33
|
+
---
|
|
34
|
+
--- - Treesitter textobjects (through |MiniAi.gen_spec.treesitter()| helper).
|
|
35
|
+
---
|
|
36
|
+
--- This module works by defining mappings for both `a` and `i` in Visual and
|
|
37
|
+
--- Operator-pending mode. After typing, they wait for single character user input
|
|
38
|
+
--- treated as textobject identifier and apply resolved textobject specification
|
|
39
|
+
--- (fall back to other mappings if can't find proper textobject id). For more
|
|
40
|
+
--- information see |MiniAi-textobject-specification| and |MiniAi-algorithm|.
|
|
41
|
+
---
|
|
42
|
+
--- Known issues which won't be resolved:
|
|
43
|
+
--- - Search for builtin textobjects is done mostly using Lua patterns
|
|
44
|
+
--- (regex-like approach). Certain amount of false positives is to be expected.
|
|
45
|
+
---
|
|
46
|
+
--- - During search for builtin textobjects there is no distinction if it is
|
|
47
|
+
--- inside string or comment. For example, in the following case there will
|
|
48
|
+
--- be wrong match for a function call: `f(a = ")", b = 1)`.
|
|
49
|
+
---
|
|
50
|
+
--- General rule of thumb: any instrument using available parser for document
|
|
51
|
+
--- structure (like treesitter) will usually provide more precise results. This
|
|
52
|
+
--- module has builtins mostly for plain text textobjects which are useful
|
|
53
|
+
--- most of the times (like "inside brackets", "around quotes/underscore", etc.).
|
|
54
|
+
--- For advanced use cases define function specification for custom textobjects.
|
|
55
|
+
---
|
|
56
|
+
--- What it doesn't (and probably won't) do:
|
|
57
|
+
--- - Have special operators to specially handle whitespace (like `I` and `A`
|
|
58
|
+
--- in `wellle/targets.vim`). Whitespace handling is assumed to be done inside
|
|
59
|
+
--- textobject specification (like `i(` and `i)` handle whitespace differently).
|
|
60
|
+
---
|
|
61
|
+
--- # Setup ~
|
|
62
|
+
---
|
|
63
|
+
--- This module needs a setup with `require('mini.ai').setup({})` (replace
|
|
64
|
+
--- `{}` with your `config` table). It will create global Lua table `MiniAi`
|
|
65
|
+
--- which you can use for scripting or manually (with `:lua MiniAi.*`).
|
|
66
|
+
---
|
|
67
|
+
--- See |MiniAi.config| for available config settings.
|
|
68
|
+
---
|
|
69
|
+
--- You can override runtime config settings (like `config.custom_textobjects`)
|
|
70
|
+
--- locally to buffer inside `vim.b.miniai_config` which should have same structure
|
|
71
|
+
--- as `MiniAi.config`. See |mini.nvim-buffer-local-config| for more details.
|
|
72
|
+
---
|
|
73
|
+
--- To stop module from showing non-error feedback, set `config.silent = true`.
|
|
74
|
+
---
|
|
75
|
+
--- # Comparisons ~
|
|
76
|
+
---
|
|
77
|
+
--- - [wellle/targets.vim](https://github.com/wellle/targets.vim):
|
|
78
|
+
--- - Has limited support for creating own textobjects: it is constrained
|
|
79
|
+
--- to pre-defined detection rules. |mini.ai| allows creating own rules
|
|
80
|
+
--- via Lua patterns and functions (see |MiniAi-textobject-specification|).
|
|
81
|
+
--- - Doesn't provide any programmatical API for getting information about
|
|
82
|
+
--- textobjects. |mini.ai| does it via |MiniAi.find_textobject()|.
|
|
83
|
+
--- - Has no implementation of "moving to edge of textobject". |mini.ai|
|
|
84
|
+
--- does it via |MiniAi.move_cursor()| and `g[` and `g]` default mappings.
|
|
85
|
+
--- - Both implement the notion of manual "next"/"last" search directions.
|
|
86
|
+
--- - Implements `A`, `I` operators. |mini.ai| does not by design: it is
|
|
87
|
+
--- assumed to be a property of textobject, not operator.
|
|
88
|
+
--- - Doesn't implement "function call" and "user prompt" textobjects.
|
|
89
|
+
--- |mini.ai| does (with `f` and `?` identifiers).
|
|
90
|
+
--- - Has limited support for "argument" textobject. Although it works in
|
|
91
|
+
--- most situations, it often misdetects commas as argument separator
|
|
92
|
+
--- (like if it is inside quotes or `{}`). |mini.ai| deals with these cases.
|
|
93
|
+
--- - [nvim-treesitter/nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects):
|
|
94
|
+
--- - Along with textobject functionality provides a curated and maintained
|
|
95
|
+
--- set of popular textobject queries for many languages (which can power
|
|
96
|
+
--- |MiniAi.gen_spec.treesitter()| functionality).
|
|
97
|
+
--- - Both support working with |treesitter-directives| allowing more
|
|
98
|
+
--- fine-tuned textobjects.
|
|
99
|
+
--- - Implements only textobjects based on treesitter.
|
|
100
|
+
--- - Doesn't support |v:count|.
|
|
101
|
+
--- - Doesn't support multiple search method (basically, only `'cover'`).
|
|
102
|
+
--- - Doesn't support consecutive application of target textobject.
|
|
103
|
+
---
|
|
104
|
+
--- # Disabling ~
|
|
105
|
+
---
|
|
106
|
+
--- To disable, set `vim.g.miniai_disable` (globally) or `vim.b.miniai_disable`
|
|
107
|
+
--- (for a buffer) to `true`. Considering high number of different scenarios
|
|
108
|
+
--- and customization intentions, writing exact rules for disabling module's
|
|
109
|
+
--- functionality is left to user. See |mini.nvim-disabling-recipes| for common
|
|
110
|
+
--- recipes.
|
|
111
|
+
---@tag MiniAi
|
|
112
|
+
|
|
113
|
+
--- This table describes all builtin textobjects along with what they
|
|
114
|
+
--- represent. Explanation:
|
|
115
|
+
--- - `Key` represents the textobject identifier: single character which should
|
|
116
|
+
--- be typed after `a`/`i`.
|
|
117
|
+
--- - `Name` is a description of textobject.
|
|
118
|
+
--- - `Example line` contains a string for which examples are constructed. The
|
|
119
|
+
--- `*` denotes the cursor position.
|
|
120
|
+
--- - `a`/`i` describe inclusive region representing `a` and `i` textobjects.
|
|
121
|
+
--- Use numbers in separators for easier navigation.
|
|
122
|
+
--- - `2a`/`2i` describe either `2a`/`2i` (support for |v:count|) textobjects
|
|
123
|
+
--- or `a`/`i` textobject followed by another `a`/`i` textobject (consecutive
|
|
124
|
+
--- application leads to incremental selection).
|
|
125
|
+
---
|
|
126
|
+
--- Example: typing `va)` with cursor on `*` leads to selection from column 2
|
|
127
|
+
--- to column 12. Another typing `a)` changes selection to [1; 13]. Also, besides
|
|
128
|
+
--- visual selection, any |operator| can be used or `g[`/`g]` motions to move
|
|
129
|
+
--- to left/right edge of `a` textobject.
|
|
130
|
+
--- >
|
|
131
|
+
--- ┌───┬───────────────┬──────────────────┬────────┬────────┬────────┬────────┐
|
|
132
|
+
--- │Key│ Name │ Example line │ a │ i │ 2a │ 2i │
|
|
133
|
+
--- ├───┴───────────────┴──────────────────┴────────┴────────┴────────┴────────┤
|
|
134
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
135
|
+
--- │ ( │ Balanced () │ (( *a (bb) )) │ │ │ │ │
|
|
136
|
+
--- │ [ │ Balanced [] │ [[ *a [bb] ]] │ [2;12] │ [4;10] │ [1;13] │ [2;12] │
|
|
137
|
+
--- │ { │ Balanced {} │ {{ *a {bb} }} │ │ │ │ │
|
|
138
|
+
--- │ < │ Balanced <> │ << *a <bb> >> │ │ │ │ │
|
|
139
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
140
|
+
--- │ ) │ Balanced () │ (( *a (bb) )) │ │ │ │ │
|
|
141
|
+
--- │ ] │ Balanced [] │ [[ *a [bb] ]] │ │ │ │ │
|
|
142
|
+
--- │ } │ Balanced {} │ {{ *a {bb} }} │ [2;12] │ [3;11] │ [1;13] │ [2;12] │
|
|
143
|
+
--- │ > │ Balanced <> │ << *a <bb> >> │ │ │ │ │
|
|
144
|
+
--- │ b │ Alias for │ [( *a {bb} )] │ │ │ │ │
|
|
145
|
+
--- │ │ ), ], or } │ │ │ │ │ │
|
|
146
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
147
|
+
--- │ " │ Balanced " │ "*a" " bb " │ │ │ │ │
|
|
148
|
+
--- │ ' │ Balanced ' │ '*a' ' bb ' │ │ │ │ │
|
|
149
|
+
--- │ ` │ Balanced ` │ `*a` ` bb ` │ [1;4] │ [2;3] │ [6;11] │ [7;10] │
|
|
150
|
+
--- │ q │ Alias for │ '*a' " bb " │ │ │ │ │
|
|
151
|
+
--- │ │ ", ', or ` │ │ │ │ │ │
|
|
152
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
153
|
+
--- │ ? │ User prompt │ e*e o e o o │ [3;5] │ [4;4] │ [7;9] │ [8;8] │
|
|
154
|
+
--- │ │(typed e and o)│ │ │ │ │ │
|
|
155
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
156
|
+
--- │ t │ Tag │ <x><y>*a</y></x> │ [4;12] │ [7;8] │ [1;16] │ [4;12] │
|
|
157
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
158
|
+
--- │ f │ Function call │ f(a, g(*b, c) ) │ [6;13] │ [8;12] │ [1;15] │ [3;14] │
|
|
159
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
160
|
+
--- │ a │ Argument │ f(*a, g(b, c) ) │ [3;5] │ [3;4] │ [5;14] │ [7;13] │
|
|
161
|
+
--- ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈1234567890123456┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
|
|
162
|
+
--- │ │ Default │ aa_*b__cc___ │ [4;7] │ [4;5] │ [8;12] │ [8;9] │
|
|
163
|
+
--- │ │ (typed _) │ │ │ │ │ │
|
|
164
|
+
--- └───┴───────────────┴──────────────────┴────────┴────────┴────────┴────────┘
|
|
165
|
+
--- <
|
|
166
|
+
--- Notes:
|
|
167
|
+
--- - All examples assume default `config.search_method`.
|
|
168
|
+
--- - Open brackets differ from close brackets by how they treat inner edge
|
|
169
|
+
--- whitespace for `i` textobject: open ignores it, close - includes.
|
|
170
|
+
--- - Default textobject is activated for identifiers which are not Latin letters.
|
|
171
|
+
--- They are designed to be treated as separators, so include only right edge
|
|
172
|
+
--- in `a` textobject. To include both edges, use custom textobjects
|
|
173
|
+
--- (see |MiniAi-textobject-specification| and |MiniAi.config|). Note:
|
|
174
|
+
--- - When cursor is exactly on the identifier character while there are
|
|
175
|
+
--- two matching candidates on both left and right, the resulting region
|
|
176
|
+
--- with smaller width is preferred.
|
|
177
|
+
---@tag MiniAi-builtin-textobjects
|
|
178
|
+
|
|
179
|
+
--- Note: this is similar to |MiniSurround-glossary|.
|
|
180
|
+
---
|
|
181
|
+
--- REGION ~
|
|
182
|
+
--- Table representing region in a buffer. Fields:
|
|
183
|
+
--- - <from> and <to> for inclusive start and end positions (<to> might be
|
|
184
|
+
--- `nil` to describe empty region). Each position is also a table with
|
|
185
|
+
--- line <line> and column <col> (both start at 1).
|
|
186
|
+
--- - <vis_mode> for which Visual mode will be used to select textobject.
|
|
187
|
+
--- See `opts` argument of |MiniAi.select_textobject()|.
|
|
188
|
+
--- One of `'v'`, `'V'`, `'\22'` (escaped `'<C-v>'`).
|
|
189
|
+
---
|
|
190
|
+
--- Examples: >lua
|
|
191
|
+
---
|
|
192
|
+
--- { from = { line = 1, col = 1 }, to = { line = 2, col = 1 } }
|
|
193
|
+
---
|
|
194
|
+
--- -- Forced linewise mode
|
|
195
|
+
--- {
|
|
196
|
+
--- from = { line = 1, col = 1 }, to = { line = 2, col = 1 },
|
|
197
|
+
--- vis_mode = 'V',
|
|
198
|
+
--- }
|
|
199
|
+
---
|
|
200
|
+
--- -- Empty region
|
|
201
|
+
--- { from = { line = 10, col = 10 } }
|
|
202
|
+
--- <
|
|
203
|
+
--- PATTERN ~
|
|
204
|
+
--- String describing Lua pattern.
|
|
205
|
+
---
|
|
206
|
+
--- SPAN ~
|
|
207
|
+
--- Interval inside a string (end-exclusive). Like [1, 5). Equal `from` and `to` edges
|
|
208
|
+
--- describe empty span at that point.
|
|
209
|
+
---
|
|
210
|
+
--- Span `A = [a1, a2)` covers `B = [b1, b2)` if every element of `B` is within
|
|
211
|
+
--- `A` (`a1 <= b < a2`). It also is described as "B is nested inside A".
|
|
212
|
+
---
|
|
213
|
+
--- NESTED PATTERN ~
|
|
214
|
+
--- Array of patterns aimed to describe nested spans.
|
|
215
|
+
---
|
|
216
|
+
--- SPAN MATCHES NESTED PATTERN ~
|
|
217
|
+
--- If there is a sequence of consecutively nested spans each matching
|
|
218
|
+
--- corresponding pattern within substring of previous span (or input string
|
|
219
|
+
--- for first span). Example: >lua
|
|
220
|
+
---
|
|
221
|
+
--- -- Nested patterns for balanced `()` with inner space
|
|
222
|
+
--- { '%b()', '^. .* .$' }
|
|
223
|
+
---
|
|
224
|
+
--- -- Example input string (with columns underneath for easier reading):
|
|
225
|
+
--- "( ( () ( ) ) )"
|
|
226
|
+
--- -- 12345678901234
|
|
227
|
+
--- <
|
|
228
|
+
--- Here are all matching spans [1, 15) and [3, 13). Both [5, 7) and [8, 10)
|
|
229
|
+
--- match first pattern but not second. All other combinations of `(` and `)`
|
|
230
|
+
--- don't match first pattern (not balanced).
|
|
231
|
+
---
|
|
232
|
+
--- COMPOSED PATTERN ~
|
|
233
|
+
--- Array with each element describing possible pattern (or array of them) at
|
|
234
|
+
--- that place. Composed pattern basically defines all possible combinations of
|
|
235
|
+
--- nested pattern (their cartesian product). Examples:
|
|
236
|
+
---
|
|
237
|
+
--- 1. Either balanced `()` or balanced `[]` but both with inner edge space: >lua
|
|
238
|
+
---
|
|
239
|
+
--- -- Composed pattern
|
|
240
|
+
--- { { '%b()', '%b[]' }, '^. .* .$' }
|
|
241
|
+
---
|
|
242
|
+
--- -- Composed pattern expanded into equivalent array of nested patterns
|
|
243
|
+
--- { '%b()', '^. .* .$' } -- and
|
|
244
|
+
--- { '%b[]', '^. .* .$' }
|
|
245
|
+
--- <
|
|
246
|
+
--- 2. Either "balanced `()` with inner edge space" or "balanced `[]` with no
|
|
247
|
+
--- inner edge space", both with 5 or more characters: >lua
|
|
248
|
+
---
|
|
249
|
+
--- -- Composed pattern
|
|
250
|
+
--- { { { '%b()', '^. .* .$' }, { '%b[]', '^.[^ ].*[^ ].$' } }, '.....' }
|
|
251
|
+
---
|
|
252
|
+
--- -- Composed pattern expanded into equivalent array of nested patterns
|
|
253
|
+
--- { '%b()', '^. .* .$', '.....' } -- and
|
|
254
|
+
--- { '%b[]', '^.[^ ].*[^ ].$', '.....' }
|
|
255
|
+
--- <
|
|
256
|
+
--- SPAN MATCHES COMPOSED PATTERN ~
|
|
257
|
+
--- If it matches at least one nested pattern from expanded composed pattern.
|
|
258
|
+
---@tag MiniAi-glossary
|
|
259
|
+
|
|
260
|
+
--- Textobject specification has a structure of composed pattern (see
|
|
261
|
+
--- |MiniAi-glossary|) with two differences:
|
|
262
|
+
--- - Last pattern(s) should have even number of empty capture groups denoting
|
|
263
|
+
--- how the last string should be processed to extract `a` or `i` textobject:
|
|
264
|
+
--- - Zero captures mean that whole string represents both `a` and `i`.
|
|
265
|
+
--- Example: `xxx` will define textobject matching string `xxx` literally.
|
|
266
|
+
--- - Two captures represent `i` textobject inside of them. `a` - whole string.
|
|
267
|
+
--- Example: `x()x()x` defines `a` textobject to be `xxx`, `i` - middle `x`.
|
|
268
|
+
--- - Four captures define `a` textobject inside captures 1 and 4, `i` -
|
|
269
|
+
--- inside captures 2 and 3. Example: `x()()x()x()` defines `a`
|
|
270
|
+
--- textobject to be last `xx`, `i` - middle `x`.
|
|
271
|
+
--- - Allows callable objects (see |vim.is_callable()|) in certain places
|
|
272
|
+
--- (enables more complex textobjects in exchange of increase in configuration
|
|
273
|
+
--- complexity and computations):
|
|
274
|
+
--- - If specification itself is a callable, it will be called with the same
|
|
275
|
+
--- arguments as |MiniAi.find_textobject()| and should return one of:
|
|
276
|
+
--- - Composed pattern. Useful for implementing user input. Example of
|
|
277
|
+
--- simplified variant of textobject for function call with name taken
|
|
278
|
+
--- from user prompt: >lua
|
|
279
|
+
---
|
|
280
|
+
--- function()
|
|
281
|
+
--- local left_edge = vim.pesc(vim.fn.input('Function name: '))
|
|
282
|
+
--- return { left_edge .. '%b()', '^.-%(().*()%)$' }
|
|
283
|
+
--- end
|
|
284
|
+
--- <
|
|
285
|
+
--- - Single output region. Useful to allow full control over
|
|
286
|
+
--- textobject. Will be taken as is. Example of returning whole buffer: >lua
|
|
287
|
+
---
|
|
288
|
+
--- function()
|
|
289
|
+
--- local from = { line = 1, col = 1 }
|
|
290
|
+
--- local to = {
|
|
291
|
+
--- line = vim.fn.line('$'),
|
|
292
|
+
--- col = math.max(vim.fn.getline('$'):len(), 1)
|
|
293
|
+
--- }
|
|
294
|
+
--- return { from = from, to = to, vis_mode = 'V' }
|
|
295
|
+
--- end
|
|
296
|
+
--- <
|
|
297
|
+
--- - Array of output region(s). Useful for incorporating other
|
|
298
|
+
--- instruments, like treesitter (see |MiniAi.gen_spec.treesitter()|).
|
|
299
|
+
--- The best region will be picked in the same manner as with composed
|
|
300
|
+
--- pattern (respecting options `n_lines`, `search_method`, etc.).
|
|
301
|
+
--- Example of selecting "best" line with display width more than 80: >lua
|
|
302
|
+
---
|
|
303
|
+
--- function(_, _, _)
|
|
304
|
+
--- local res = {}
|
|
305
|
+
--- for i = 1, vim.api.nvim_buf_line_count(0) do
|
|
306
|
+
--- local cur_line = vim.fn.getline(i)
|
|
307
|
+
--- if vim.fn.strdisplaywidth(cur_line) > 80 then
|
|
308
|
+
--- local region = {
|
|
309
|
+
--- from = { line = i, col = 1 },
|
|
310
|
+
--- to = { line = i, col = cur_line:len() },
|
|
311
|
+
--- }
|
|
312
|
+
--- table.insert(res, region)
|
|
313
|
+
--- end
|
|
314
|
+
--- end
|
|
315
|
+
--- return res
|
|
316
|
+
--- end
|
|
317
|
+
--- <
|
|
318
|
+
--- - If there is a callable instead of assumed string pattern, it is expected
|
|
319
|
+
--- to have signature `(line, init)` and behave like `pattern:find()`.
|
|
320
|
+
--- It should return two numbers representing span in `line` next after
|
|
321
|
+
--- or at `init` (`nil` if there is no such span).
|
|
322
|
+
--- !IMPORTANT NOTE!: it means that output's `from` shouldn't be strictly
|
|
323
|
+
--- to the left of `init` (it will lead to infinite loop). Not allowed as
|
|
324
|
+
--- last item (as it should be pattern with captures).
|
|
325
|
+
--- Example of matching only balanced parenthesis with big enough width: >lua
|
|
326
|
+
---
|
|
327
|
+
--- {
|
|
328
|
+
--- '%b()',
|
|
329
|
+
--- function(s, init)
|
|
330
|
+
--- if init > 1 or s:len() < 5 then return end
|
|
331
|
+
--- return 1, s:len()
|
|
332
|
+
--- end,
|
|
333
|
+
--- '^.().*().$'
|
|
334
|
+
--- }
|
|
335
|
+
--- <
|
|
336
|
+
--- More examples: >lua
|
|
337
|
+
---
|
|
338
|
+
--- -- Pair of balanced brackets from set (used for builtin `b` identifier):
|
|
339
|
+
--- { { '%b()', '%b[]', '%b{}' }, '^.().*().$' }
|
|
340
|
+
---
|
|
341
|
+
--- -- Imitate word ignoring digits and punctuation (only for Latin alphabet):
|
|
342
|
+
--- { '()()%f[%w]%w+()[ \t]*()' }
|
|
343
|
+
---
|
|
344
|
+
--- -- Word with camel case support (also supports only Latin alphabet):
|
|
345
|
+
--- {
|
|
346
|
+
--- {
|
|
347
|
+
--- '%u[%l%d]+%f[^%l%d]',
|
|
348
|
+
--- '%f[%S][%l%d]+%f[^%l%d]',
|
|
349
|
+
--- '%f[%P][%l%d]+%f[^%l%d]',
|
|
350
|
+
--- '^[%l%d]+%f[^%l%d]',
|
|
351
|
+
--- },
|
|
352
|
+
--- '^().*()$'
|
|
353
|
+
--- }
|
|
354
|
+
---
|
|
355
|
+
--- -- Number:
|
|
356
|
+
--- { '%f[%d]%d+' }
|
|
357
|
+
---
|
|
358
|
+
--- -- Date in 'YYYY-MM-DD' format:
|
|
359
|
+
--- { '()%d%d%d%d%-%d%d%-%d%d()' }
|
|
360
|
+
---
|
|
361
|
+
--- -- Lua block string:
|
|
362
|
+
--- { '%[%[().-()%]%]' }
|
|
363
|
+
--- <
|
|
364
|
+
--- See |MiniAi.gen_spec| for function wrappers to create commonly used
|
|
365
|
+
--- textobject specifications.
|
|
366
|
+
---@tag MiniAi-textobject-specification
|
|
367
|
+
|
|
368
|
+
--- Search for the textobjects relies on these principles:
|
|
369
|
+
--- - It uses same input data as described in |MiniAi.find_textobject()|,
|
|
370
|
+
--- i.e. whether it is `a` or `i` textobject, its identifier, reference region, etc.
|
|
371
|
+
--- - Textobject specification is constructed based on textobject identifier
|
|
372
|
+
--- (see |MiniAi-textobject-specification|).
|
|
373
|
+
--- - General search is done by converting some 2d buffer region (neighborhood
|
|
374
|
+
--- of reference region) into 1d string (each line is appended with `\n`).
|
|
375
|
+
--- Then search for a best span matching textobject specification is done
|
|
376
|
+
--- inside string (see |MiniAi-glossary|). After that, span is converted back
|
|
377
|
+
--- into 2d region. Note: first search is done inside reference region lines,
|
|
378
|
+
--- and only after that - inside its neighborhood within `config.n_lines`
|
|
379
|
+
--- (see |MiniAi.config|).
|
|
380
|
+
--- - The best matching span is chosen by iterating over all spans matching
|
|
381
|
+
--- textobject specification and comparing them with "current best".
|
|
382
|
+
--- Comparison also depends on reference region (tighter covering is better,
|
|
383
|
+
--- otherwise closer is better) and search method (if span is even considered).
|
|
384
|
+
--- - Extract span based on extraction pattern (last item in nested pattern).
|
|
385
|
+
--- - If task is to perform a consecutive search (`opts.n_times` is greater than 1),
|
|
386
|
+
--- steps are repeated with current best match becoming reference region.
|
|
387
|
+
--- One such additional step is also done if final region is equal to
|
|
388
|
+
--- reference region (this enables consecutive application).
|
|
389
|
+
---
|
|
390
|
+
--- Notes:
|
|
391
|
+
--- - Iteration over all matched spans is done in depth-first fashion with
|
|
392
|
+
--- respect to nested pattern.
|
|
393
|
+
--- - It is guaranteed that span is compared only once.
|
|
394
|
+
--- - For the sake of increasing functionality, during iteration over all
|
|
395
|
+
--- matching spans, some Lua patterns in composed pattern are handled
|
|
396
|
+
--- specially.
|
|
397
|
+
--- - `%bxx` (`xx` is two identical characters). It denotes balanced pair
|
|
398
|
+
--- of identical characters and results into "paired" matches. For
|
|
399
|
+
--- example, `%b""` for `"aa" "bb"` would match `"aa"` and `"bb"`, but
|
|
400
|
+
--- not middle `" "`.
|
|
401
|
+
--- - `x.-y` (`x` and `y` are different strings). It results only in matches with
|
|
402
|
+
--- smallest width. For example, `e.-o` for `e e o o` will result only in
|
|
403
|
+
--- middle `e o`. Note: it has some implications for when parts have
|
|
404
|
+
--- quantifiers (like `+`, etc.), which usually can be resolved with
|
|
405
|
+
--- frontier pattern `%f[]` (see examples in |MiniAi-textobject-specification|).
|
|
406
|
+
---@tag MiniAi-algorithm
|
|
407
|
+
|
|
408
|
+
-- Module definition ==========================================================
|
|
409
|
+
local MiniAi = {}
|
|
410
|
+
local H = {}
|
|
411
|
+
|
|
412
|
+
--- Module setup
|
|
413
|
+
---
|
|
414
|
+
---@param config table|nil Module config table. See |MiniAi.config|.
|
|
415
|
+
---
|
|
416
|
+
---@usage >lua
|
|
417
|
+
--- require('mini.ai').setup() -- use default config
|
|
418
|
+
--- -- OR
|
|
419
|
+
--- require('mini.ai').setup({}) -- replace {} with your config table
|
|
420
|
+
--- <
|
|
421
|
+
MiniAi.setup = function(config)
|
|
422
|
+
-- TODO: Remove after Neovim=0.9 support is dropped
|
|
423
|
+
if vim.fn.has('nvim-0.10') == 0 then
|
|
424
|
+
vim.notify(
|
|
425
|
+
'(mini.ai) Neovim<0.10 is soft deprecated (module works but is not supported).'
|
|
426
|
+
.. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
|
|
427
|
+
.. ' Please update your Neovim version.'
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
-- Export module
|
|
432
|
+
_G.MiniAi = MiniAi
|
|
433
|
+
|
|
434
|
+
-- Setup config
|
|
435
|
+
config = H.setup_config(config)
|
|
436
|
+
|
|
437
|
+
-- Apply config
|
|
438
|
+
H.apply_config(config)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
--- Defaults ~
|
|
442
|
+
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
|
|
443
|
+
---@text # Custom textobjects ~
|
|
444
|
+
---
|
|
445
|
+
--- User can define own textobjects by supplying `config.custom_textobjects`.
|
|
446
|
+
--- It should be a table with keys being single character textobject identifier
|
|
447
|
+
--- (supported by |getcharstr()|, except <Esc> and <C-c> which are used to cancel)
|
|
448
|
+
--- and values - textobject specification (see |MiniAi-textobject-specification|).
|
|
449
|
+
---
|
|
450
|
+
--- General recommendations:
|
|
451
|
+
--- - This can be used to override builtin ones (|MiniAi-builtin-textobjects|).
|
|
452
|
+
--- Supply non-valid input (not in specification format) to disable module's
|
|
453
|
+
--- builtin textobject in favor of external or Neovim's builtin mapping.
|
|
454
|
+
--- - Keys should use character representation which can be |getcharstr()| output.
|
|
455
|
+
--- For example, `'\r'` and not `'<CR>'`.
|
|
456
|
+
---
|
|
457
|
+
--- Examples:
|
|
458
|
+
--- >lua
|
|
459
|
+
--- require('mini.ai').setup({
|
|
460
|
+
--- custom_textobjects = {
|
|
461
|
+
--- -- Tweak argument textobject
|
|
462
|
+
--- a = require('mini.ai').gen_spec.argument({ brackets = { '%b()' } }),
|
|
463
|
+
---
|
|
464
|
+
--- -- Disable brackets alias in favor of builtin block textobject
|
|
465
|
+
--- b = false,
|
|
466
|
+
---
|
|
467
|
+
--- -- Now `vax` should select `xxx` and `vix` - middle `x`
|
|
468
|
+
--- x = { 'x()x()x' },
|
|
469
|
+
---
|
|
470
|
+
--- -- Whole buffer
|
|
471
|
+
--- g = function()
|
|
472
|
+
--- local from = { line = 1, col = 1 }
|
|
473
|
+
--- local to = {
|
|
474
|
+
--- line = vim.fn.line('$'),
|
|
475
|
+
--- col = math.max(vim.fn.getline('$'):len(), 1)
|
|
476
|
+
--- }
|
|
477
|
+
--- return { from = from, to = to }
|
|
478
|
+
--- end
|
|
479
|
+
--- }
|
|
480
|
+
--- })
|
|
481
|
+
---
|
|
482
|
+
--- -- Use `vim.b.miniai_config` to customize per buffer
|
|
483
|
+
--- -- Example of specification useful for Markdown files:
|
|
484
|
+
--- local spec_pair = require('mini.ai').gen_spec.pair
|
|
485
|
+
--- vim.b.miniai_config = {
|
|
486
|
+
--- custom_textobjects = {
|
|
487
|
+
--- ['*'] = spec_pair('*', '*', { type = 'greedy' }),
|
|
488
|
+
--- ['_'] = spec_pair('_', '_', { type = 'greedy' }),
|
|
489
|
+
--- },
|
|
490
|
+
--- }
|
|
491
|
+
--- <
|
|
492
|
+
--- There are more example specifications in |MiniAi-textobject-specification|.
|
|
493
|
+
---
|
|
494
|
+
--- # Search method ~
|
|
495
|
+
---
|
|
496
|
+
--- Value of `config.search_method` defines how best match search is done.
|
|
497
|
+
--- Based on its value, one of the following matches will be selected:
|
|
498
|
+
--- - Covering match. Left/right edge is before/after left/right edge of
|
|
499
|
+
--- reference region.
|
|
500
|
+
--- - Previous match. Left/right edge is before left/right edge of reference
|
|
501
|
+
--- region.
|
|
502
|
+
--- - Next match. Left/right edge is after left/right edge of reference region.
|
|
503
|
+
--- - Nearest match. Whichever is closest among previous and next matches.
|
|
504
|
+
---
|
|
505
|
+
--- Possible values are:
|
|
506
|
+
--- - `'cover'` - use only covering match. Don't use either previous or
|
|
507
|
+
--- next; report that there is no textobject found.
|
|
508
|
+
--- - `'cover_or_next'` (default) - use covering match. If not found, use next.
|
|
509
|
+
--- - `'cover_or_prev'` - use covering match. If not found, use previous.
|
|
510
|
+
--- - `'cover_or_nearest'` - use covering match. If not found, use nearest.
|
|
511
|
+
--- - `'next'` - use next match.
|
|
512
|
+
--- - `'prev'` - use previous match.
|
|
513
|
+
--- - `'nearest'` - use nearest match.
|
|
514
|
+
---
|
|
515
|
+
--- Note: search is first performed on the reference region lines and only
|
|
516
|
+
--- after failure - on the whole neighborhood defined by `config.n_lines`. This
|
|
517
|
+
--- means that with `config.search_method` not equal to `'cover'`, "prev" or
|
|
518
|
+
--- "next" textobject will end up as search result if they are found on first
|
|
519
|
+
--- stage although covering match might be found in bigger, whole neighborhood.
|
|
520
|
+
--- This design is based on observation that most of the time operation is done
|
|
521
|
+
--- within reference region lines (usually cursor line).
|
|
522
|
+
---
|
|
523
|
+
--- Here is an example of what `a)` textobject is based on a value of
|
|
524
|
+
--- `'config.search_method'` when cursor is inside `bbb` word:
|
|
525
|
+
--- - `'cover'`: `(a) bbb (c)` -> none
|
|
526
|
+
--- - `'cover_or_next'`: `(a) bbb (c)` -> `(c)`
|
|
527
|
+
--- - `'cover_or_prev'`: `(a) bbb (c)` -> `(a)`
|
|
528
|
+
--- - `'cover_or_nearest'`: depends on cursor position.
|
|
529
|
+
--- For first and second `b` - as in `cover_or_prev` (as previous match is
|
|
530
|
+
--- nearer), for third - as in `cover_or_next` (as next match is nearer).
|
|
531
|
+
--- - `'next'`: `(a) bbb (c)` -> `(c)`. Same outcome for `(bbb)`.
|
|
532
|
+
--- - `'prev'`: `(a) bbb (c)` -> `(a)`. Same outcome for `(bbb)`.
|
|
533
|
+
--- - `'nearest'`: depends on cursor position (same as in `'cover_or_nearest'`).
|
|
534
|
+
---
|
|
535
|
+
--- # Mappings ~
|
|
536
|
+
---
|
|
537
|
+
--- Mappings `around_next` / `inside_next` and `around_last` / `inside_last` are
|
|
538
|
+
--- essentially `around` / `inside` but using search method `'next'` and `'prev'`.
|
|
539
|
+
---
|
|
540
|
+
--- ## Overriding default `an` and `in` ~
|
|
541
|
+
--- *MiniAi-default-an-in*
|
|
542
|
+
---
|
|
543
|
+
--- Default `around_next` / `inside_next` mappings override newly added in Neovim=0.12
|
|
544
|
+
--- built-in mappings |v_an| and |v_in|. This is intentional for better usability
|
|
545
|
+
--- and backwards compatibility. There are several ways to work around this:
|
|
546
|
+
---
|
|
547
|
+
--- - Remap Neovim's `an` / `in` to something else before calling |MiniAi.setup()|: >lua
|
|
548
|
+
---
|
|
549
|
+
--- -- Use `<Leader>ls` and `<Leader>lS` for incremental selection
|
|
550
|
+
--- local copy_keymap = function(mode, from_lhs, to_lhs)
|
|
551
|
+
--- local keymap = vim.fn.maparg(from_lhs, mode, false, true)
|
|
552
|
+
--- local rhs = keymap.callback or keymap.rhs
|
|
553
|
+
--- vim.keymap.set(mode, to_lhs, rhs, { desc = keymap.desc })
|
|
554
|
+
--- end
|
|
555
|
+
--- copy_keymap('x', 'an', '<Leader>ls')
|
|
556
|
+
--- copy_keymap('x', 'in', '<Leader>lS')
|
|
557
|
+
---
|
|
558
|
+
--- -- ...
|
|
559
|
+
--- require('mini.ai').setup({...})
|
|
560
|
+
--- <
|
|
561
|
+
--- - Use |MiniAi.gen_spec.treesitter()| with one or many captures for versatile
|
|
562
|
+
--- tree-sitter based textobjects. It also allows consecutive application in
|
|
563
|
+
--- Visual mode: similar to incremental selection, with `next` / `last` variants,
|
|
564
|
+
--- but without "decrease selection".
|
|
565
|
+
---
|
|
566
|
+
--- - Use other values for "next" / "last" variants. For example: >lua
|
|
567
|
+
---
|
|
568
|
+
--- require('mini.ai').setup({
|
|
569
|
+
--- mappings = {
|
|
570
|
+
--- around_next = 'aN',
|
|
571
|
+
--- inside_next = 'iN',
|
|
572
|
+
--- around_last = 'aL',
|
|
573
|
+
--- inside_last = 'iL',
|
|
574
|
+
--- }
|
|
575
|
+
--- })
|
|
576
|
+
--- <
|
|
577
|
+
--- ## Overriding default `al` and `il` ~
|
|
578
|
+
--- *MiniAi-default-al-il*
|
|
579
|
+
---
|
|
580
|
+
--- Default `around_last` / `inside_last` mappings override newly added in Neovim=0.13
|
|
581
|
+
--- built-in mappings |al| and |il|. This is intentional for better usability
|
|
582
|
+
--- and backwards compatibility. There are several ways to work around this:
|
|
583
|
+
---
|
|
584
|
+
--- - Use |MiniExtra.gen_ai_spec.buffer()| and |MiniExtra.gen_ai_spec.line()| to
|
|
585
|
+
--- create custom textobjects for buffer and line: >lua
|
|
586
|
+
---
|
|
587
|
+
--- require('mini.extra').setup()
|
|
588
|
+
--- require('mini.ai').setup({
|
|
589
|
+
--- custom_textobjects = {
|
|
590
|
+
--- -- Makes `aB` equivalent to built-in `al`
|
|
591
|
+
--- B = MiniExtra.gen_ai_spec.buffer(),
|
|
592
|
+
--- -- Makes `iL` equivalent to built-in `il`
|
|
593
|
+
--- L = MiniExtra.gen_ai_spec.line(),
|
|
594
|
+
--- },
|
|
595
|
+
--- })
|
|
596
|
+
--- <
|
|
597
|
+
--- - Use other values for "next" / "last" variants. See |MiniAi-default-an-in|.
|
|
598
|
+
MiniAi.config = {
|
|
599
|
+
-- Table with textobject id as fields, textobject specification as values.
|
|
600
|
+
-- Also use this to disable builtin textobjects. See |MiniAi.config|.
|
|
601
|
+
custom_textobjects = nil,
|
|
602
|
+
|
|
603
|
+
-- Module mappings. Use `''` (empty string) to disable one.
|
|
604
|
+
mappings = {
|
|
605
|
+
-- Main textobject prefixes
|
|
606
|
+
around = 'a',
|
|
607
|
+
inside = 'i',
|
|
608
|
+
|
|
609
|
+
-- Next/last textobjects
|
|
610
|
+
-- NOTE: This (deliberately) overrides Neovim>=0.12 built-in incremental
|
|
611
|
+
-- selection mappings. See `:h MiniAi-default-an-in` for more details.
|
|
612
|
+
around_next = 'an',
|
|
613
|
+
inside_next = 'in',
|
|
614
|
+
around_last = 'al',
|
|
615
|
+
inside_last = 'il',
|
|
616
|
+
|
|
617
|
+
-- Move cursor to corresponding edge of `a` textobject
|
|
618
|
+
goto_left = 'g[',
|
|
619
|
+
goto_right = 'g]',
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
-- Number of lines within which textobject is searched
|
|
623
|
+
n_lines = 50,
|
|
624
|
+
|
|
625
|
+
-- How to search for object (first inside current line, then inside
|
|
626
|
+
-- neighborhood). One of 'cover', 'cover_or_next', 'cover_or_prev',
|
|
627
|
+
-- 'cover_or_nearest', 'next', 'prev', 'nearest'.
|
|
628
|
+
search_method = 'cover_or_next',
|
|
629
|
+
|
|
630
|
+
-- Whether to disable showing non-error feedback
|
|
631
|
+
-- This also affects (purely informational) helper messages shown after
|
|
632
|
+
-- idle time if user input is required.
|
|
633
|
+
silent = false,
|
|
634
|
+
}
|
|
635
|
+
--minidoc_afterlines_end
|
|
636
|
+
|
|
637
|
+
-- Module functionality =======================================================
|
|
638
|
+
--- Find textobject region
|
|
639
|
+
---
|
|
640
|
+
---@param ai_type string One of `'a'` or `'i'`.
|
|
641
|
+
---@param id string Single character string representing textobject id. It is
|
|
642
|
+
--- used to get specification which is later used to compute textobject region.
|
|
643
|
+
--- Note: if specification is a function, it is called with all present
|
|
644
|
+
--- arguments (`opts` is populated with default arguments).
|
|
645
|
+
---@param opts table|nil Options. Possible fields:
|
|
646
|
+
--- - <n_lines> - Number of lines within which textobject is searched.
|
|
647
|
+
--- Default: `config.n_lines` (see |MiniAi.config|).
|
|
648
|
+
--- - <n_times> - Number of times to perform a consecutive search. Each one
|
|
649
|
+
--- is done with reference region being previous found textobject region.
|
|
650
|
+
--- Default: 1.
|
|
651
|
+
--- - <reference_region> - region to try to cover (see |MiniAi-glossary|). It
|
|
652
|
+
--- is guaranteed that output region will not be inside or equal to this one.
|
|
653
|
+
--- Default: empty region at cursor position.
|
|
654
|
+
--- - <search_method> - Search method. Default: `config.search_method`.
|
|
655
|
+
---
|
|
656
|
+
---@return table|nil Region of textobject or `nil` if no textobject different
|
|
657
|
+
--- from `opts.reference_region` was consecutively found `opts.n_times` times.
|
|
658
|
+
MiniAi.find_textobject = function(ai_type, id, opts)
|
|
659
|
+
if not (ai_type == 'a' or ai_type == 'i') then H.error([[`ai_type` should be one of 'a' or 'i'.]]) end
|
|
660
|
+
opts = vim.tbl_deep_extend('force', H.get_default_opts(), opts or {})
|
|
661
|
+
H.validate_search_method(opts.search_method)
|
|
662
|
+
|
|
663
|
+
-- Get textobject specification
|
|
664
|
+
local tobj_spec = H.get_textobject_spec(id, { ai_type, id, opts })
|
|
665
|
+
if tobj_spec == nil then return end
|
|
666
|
+
if H.is_region(tobj_spec) then return tobj_spec end
|
|
667
|
+
|
|
668
|
+
-- Find region
|
|
669
|
+
local res = H.find_textobject_region(tobj_spec, ai_type, opts)
|
|
670
|
+
|
|
671
|
+
if res == nil then
|
|
672
|
+
local msg = string.format(
|
|
673
|
+
[[No textobject %s found covering region%s within %d line%s and `search_method = '%s'`.]],
|
|
674
|
+
vim.inspect(ai_type .. id),
|
|
675
|
+
opts.n_times == 1 and '' or (' %s times'):format(opts.n_times),
|
|
676
|
+
opts.n_lines,
|
|
677
|
+
opts.n_lines == 1 and '' or 's',
|
|
678
|
+
opts.search_method
|
|
679
|
+
)
|
|
680
|
+
H.message(msg)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
return res
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
--- Move cursor to edge of textobject
|
|
687
|
+
---
|
|
688
|
+
---@param side string One of `'left'` or `'right'`.
|
|
689
|
+
---@param ai_type string One of `'a'` or `'i'`.
|
|
690
|
+
---@param id string Single character string representing textobject id.
|
|
691
|
+
---@param opts table|nil Same as in |MiniAi.find_textobject()|.
|
|
692
|
+
--- `opts.n_times` means number of actual jumps (important when cursor
|
|
693
|
+
--- already on the potential jump spot).
|
|
694
|
+
MiniAi.move_cursor = function(side, ai_type, id, opts)
|
|
695
|
+
if not (side == 'left' or side == 'right') then H.error([[`side` should be one of 'left' or 'right'.]]) end
|
|
696
|
+
opts = opts or {}
|
|
697
|
+
local init_pos = vim.api.nvim_win_get_cursor(0)
|
|
698
|
+
|
|
699
|
+
-- Compute single textobject first to find out if it would move the cursor.
|
|
700
|
+
-- If not, then eventual `n_times` should be bigger by 1 to imitate `n_times`
|
|
701
|
+
-- *actual* jumps. This implements consecutive jumps and has logic of "If
|
|
702
|
+
-- cursor is strictly inside region, move to its side first".
|
|
703
|
+
local new_opts = vim.tbl_deep_extend('force', opts, { n_times = 1 })
|
|
704
|
+
local tobj_single = MiniAi.find_textobject(ai_type, id, new_opts)
|
|
705
|
+
if tobj_single == nil then return end
|
|
706
|
+
local tobj_side = side == 'left' and 'from' or 'to'
|
|
707
|
+
|
|
708
|
+
-- Allow empty region
|
|
709
|
+
tobj_single.to = tobj_single.to or tobj_single.from
|
|
710
|
+
|
|
711
|
+
new_opts.n_times = opts.n_times or 1
|
|
712
|
+
if (init_pos[1] == tobj_single[tobj_side].line) and (init_pos[2] == tobj_single[tobj_side].col - 1) then
|
|
713
|
+
new_opts.n_times = new_opts.n_times + 1
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
-- Compute actually needed textobject while avoiding unnecessary computation
|
|
717
|
+
-- in a most common usage (`v:count1 == 1`)
|
|
718
|
+
local pos = tobj_single[tobj_side]
|
|
719
|
+
if new_opts.n_times > 1 then
|
|
720
|
+
local tobj = MiniAi.find_textobject(ai_type, id, new_opts)
|
|
721
|
+
if tobj == nil then return end
|
|
722
|
+
tobj.to = tobj.to or tobj.from
|
|
723
|
+
pos = tobj[tobj_side]
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
-- Move cursor and open enough folds
|
|
727
|
+
vim.cmd("normal! m'")
|
|
728
|
+
vim.api.nvim_win_set_cursor(0, { pos.line, pos.col - 1 })
|
|
729
|
+
vim.cmd('normal! zv')
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
--- Generate common textobject specifications
|
|
733
|
+
---
|
|
734
|
+
--- This is a table with function elements. Call to actually get specification.
|
|
735
|
+
---
|
|
736
|
+
--- Example: >lua
|
|
737
|
+
---
|
|
738
|
+
--- local gen_spec = require('mini.ai').gen_spec
|
|
739
|
+
--- require('mini.ai').setup({
|
|
740
|
+
--- custom_textobjects = {
|
|
741
|
+
--- -- Tweak argument to be recognized only inside `()` between `;`
|
|
742
|
+
--- a = gen_spec.argument({ brackets = { '%b()' }, separator = ';' }),
|
|
743
|
+
---
|
|
744
|
+
--- -- Tweak function call to not detect dot in function name
|
|
745
|
+
--- f = gen_spec.function_call({ name_pattern = '[%w_]' }),
|
|
746
|
+
---
|
|
747
|
+
--- -- Function definition (needs treesitter queries with these captures)
|
|
748
|
+
--- F = gen_spec.treesitter({ a = '@function.outer', i = '@function.inner' }),
|
|
749
|
+
---
|
|
750
|
+
--- -- Make `|` select both edges in non-balanced way
|
|
751
|
+
--- ['|'] = gen_spec.pair('|', '|', { type = 'non-balanced' }),
|
|
752
|
+
--- }
|
|
753
|
+
--- })
|
|
754
|
+
--- <
|
|
755
|
+
MiniAi.gen_spec = {}
|
|
756
|
+
|
|
757
|
+
--- Argument specification
|
|
758
|
+
---
|
|
759
|
+
--- Argument textobject (has default `a` identifier) is a region inside
|
|
760
|
+
--- balanced bracket between allowed not excluded separators. Use this function
|
|
761
|
+
--- to tweak how it works.
|
|
762
|
+
---
|
|
763
|
+
--- Examples:
|
|
764
|
+
--- - `argument({ brackets = { '%b()' } })` will search for an argument only
|
|
765
|
+
--- inside balanced `()`.
|
|
766
|
+
--- - `argument({ separator = '[,;]' })` will treat both `,` and `;` as separators.
|
|
767
|
+
--- - `argument({ exclude_regions = { '%b()' } })` will exclude separators
|
|
768
|
+
--- which are inside balanced `()` (inside outer brackets).
|
|
769
|
+
---
|
|
770
|
+
---@param opts table|nil Options. Allowed fields:
|
|
771
|
+
--- - <brackets> - array of patterns for outer balanced brackets.
|
|
772
|
+
--- Default: `{ '%b()', '%b[]', '%b{}' }` (any `()`, `[]`, or `{}` can
|
|
773
|
+
--- enclose arguments).
|
|
774
|
+
--- - <separator> - separator pattern. Default: `','`.
|
|
775
|
+
--- One of the practical usages of this option is to include whitespace
|
|
776
|
+
--- around character to be a part of separator. For example, `'%s*,%s*'`
|
|
777
|
+
--- will treat as separator not only `,`, but its possible surrounding
|
|
778
|
+
--- whitespace. This has both positive and negative effects. On one hand,
|
|
779
|
+
--- `daa` executed over the first argument will delete whitespace after
|
|
780
|
+
--- first comma, leading to a more expected outcome. On the other hand it
|
|
781
|
+
--- is ambiguous which argument is picked when cursor is over whitespace
|
|
782
|
+
--- near the character separator.
|
|
783
|
+
--- - <exclude_regions> - array with patterns for regions inside which
|
|
784
|
+
--- separators will be ignored.
|
|
785
|
+
--- Default: `{ '%b""', "%b''", '%b()', '%b[]', '%b{}' }` (separators
|
|
786
|
+
--- inside balanced quotes or brackets are ignored).
|
|
787
|
+
MiniAi.gen_spec.argument = function(opts)
|
|
788
|
+
opts = vim.tbl_extend('force', {
|
|
789
|
+
brackets = { '%b()', '%b[]', '%b{}' },
|
|
790
|
+
separator = ',',
|
|
791
|
+
exclude_regions = { '%b""', "%b''", '%b()', '%b[]', '%b{}' },
|
|
792
|
+
}, opts or {})
|
|
793
|
+
|
|
794
|
+
local brackets, separator, exclude_regions = opts.brackets, opts.separator, opts.exclude_regions
|
|
795
|
+
|
|
796
|
+
local res = {}
|
|
797
|
+
-- Match brackets
|
|
798
|
+
res[1] = brackets
|
|
799
|
+
|
|
800
|
+
-- Match argument with both left and right separators/brackets
|
|
801
|
+
res[2] = function(s, init)
|
|
802
|
+
-- Cache string separators per spec as they are used multiple times.
|
|
803
|
+
-- Storing per spec allows coexistence of several argument specifications.
|
|
804
|
+
H.cache.argument_sep_spans = H.cache.argument_sep_spans or {}
|
|
805
|
+
H.cache.argument_sep_spans[res] = H.cache.argument_sep_spans[res] or {}
|
|
806
|
+
local sep_spans = H.cache.argument_sep_spans[res][s] or H.arg_get_separator_spans(s, separator, exclude_regions)
|
|
807
|
+
H.cache.argument_sep_spans[res][s] = sep_spans
|
|
808
|
+
|
|
809
|
+
-- Return span fully on right of `init`, `nil` otherwise
|
|
810
|
+
-- For first argument returns left bracket; for last - right one.
|
|
811
|
+
for i = 1, #sep_spans - 1 do
|
|
812
|
+
if init <= sep_spans[i][1] then return sep_spans[i][1], sep_spans[i + 1][2] end
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
return nil
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
-- Make extraction part
|
|
819
|
+
--
|
|
820
|
+
-- Extraction of `a` type depends on argument number, `i` - as `a` but
|
|
821
|
+
-- without separators and inner whitespace. The reason for this complex
|
|
822
|
+
-- solution are the following requirements:
|
|
823
|
+
-- - Don't match argument region when cursor is on the outer bracket.
|
|
824
|
+
-- Example: `f(xxx)` should select argument only when cursor is on 'x'.
|
|
825
|
+
-- - Don't select edge whitespace for first and last argument BUT MATCH WHEN
|
|
826
|
+
-- CURSOR IS ON THEM which needs to match edge whitespace right until the
|
|
827
|
+
-- extraction part. This is useful when working with padded brackets.
|
|
828
|
+
-- Example for `f( xx , yy )`:
|
|
829
|
+
-- - `a` object should select 'xx ,' when cursor is on all ' xx ';
|
|
830
|
+
-- should select ', yy' when cursor is on all ' yy '.
|
|
831
|
+
-- - `i` object should select 'xx' when cursor is on all ' xx ';
|
|
832
|
+
-- should select 'yy' when cursor is on all ' yy '.
|
|
833
|
+
--
|
|
834
|
+
-- At this stage whether argument is first, middle, last, or single is
|
|
835
|
+
-- determined by presence of matching separator at either left or right edge.
|
|
836
|
+
-- If edge matches separator pattern - it has separator. If not - a bracket.
|
|
837
|
+
local left_edge_separator = '^' .. separator
|
|
838
|
+
local find_after_left_separator = function(s)
|
|
839
|
+
local _, sep_end = s:find(left_edge_separator)
|
|
840
|
+
if sep_end == nil then return nil end
|
|
841
|
+
return sep_end + 1
|
|
842
|
+
end
|
|
843
|
+
local find_after_left_bracket = function(s)
|
|
844
|
+
local left_sep = find_after_left_separator(s)
|
|
845
|
+
if left_sep ~= nil then return nil end
|
|
846
|
+
return 2
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
local right_edge_sep = separator .. '$'
|
|
850
|
+
local find_before_right_separator = function(s)
|
|
851
|
+
local sep_start, _ = s:find(right_edge_sep)
|
|
852
|
+
if sep_start == nil then return nil end
|
|
853
|
+
return sep_start - 1
|
|
854
|
+
end
|
|
855
|
+
local find_before_right_bracket = function(s)
|
|
856
|
+
local right_sep = find_before_right_separator(s)
|
|
857
|
+
if right_sep ~= nil then return nil end
|
|
858
|
+
return s:len() - 1
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
local match_and_include = function(left_type, left_include, right_type, right_include)
|
|
862
|
+
local find_after_left = left_type == 'bracket' and find_after_left_bracket or find_after_left_separator
|
|
863
|
+
local find_before_right = right_type == 'bracket' and find_before_right_bracket or find_before_right_separator
|
|
864
|
+
|
|
865
|
+
return function(s, init)
|
|
866
|
+
-- Match only once
|
|
867
|
+
if init > 1 then return nil end
|
|
868
|
+
|
|
869
|
+
-- Make sure that string matches left and right targets
|
|
870
|
+
local left_after, right_before = find_after_left(s), find_before_right(s)
|
|
871
|
+
if left_after == nil or right_before == nil then return nil end
|
|
872
|
+
|
|
873
|
+
-- Possibly include matched edge targets
|
|
874
|
+
local left = left_include and 1 or left_after
|
|
875
|
+
local right = right_include and s:len() or right_before
|
|
876
|
+
|
|
877
|
+
return left, right
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
local extract_first_arg = '^%s*()().-()%s*' .. separator .. '()$'
|
|
882
|
+
local extract_nonfirst_arg = '^()' .. separator .. '%s*().-()()%s*$'
|
|
883
|
+
local extract_single_arg = '^%s*().-()%s*$'
|
|
884
|
+
|
|
885
|
+
res[3] = {
|
|
886
|
+
-- First argument. Include right separator, exclude left whitespace.
|
|
887
|
+
{ match_and_include('bracket', false, 'separator', true), extract_first_arg },
|
|
888
|
+
|
|
889
|
+
-- Middle argument. Include only left separator.
|
|
890
|
+
{ match_and_include('separator', true, 'separator', false), extract_nonfirst_arg },
|
|
891
|
+
|
|
892
|
+
-- Last argument. Include left separator, exclude right whitespace.
|
|
893
|
+
-- NOTE: it misbehaves for whitespace argument. It's OK because it's rare.
|
|
894
|
+
{ match_and_include('separator', true, 'bracket', false), extract_nonfirst_arg },
|
|
895
|
+
|
|
896
|
+
-- Single argument. Include both whitespace (makes `aa` and `ia` differ).
|
|
897
|
+
{ match_and_include('bracket', false, 'bracket', false), extract_single_arg },
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return res
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
--- Function call specification
|
|
904
|
+
---
|
|
905
|
+
--- Function call textobject (has default `f` identifier) is a region with some
|
|
906
|
+
--- characters followed by balanced `()`. Use this function to tweak how it works.
|
|
907
|
+
---
|
|
908
|
+
--- Example:
|
|
909
|
+
--- - `function_call({ name_pattern = '[%w_]' })` will recognize function name with
|
|
910
|
+
--- only alphanumeric or underscore (not dot).
|
|
911
|
+
---
|
|
912
|
+
---@param opts table|nil Options. Allowed fields:
|
|
913
|
+
--- - <name_pattern> - string pattern of character set allowed in function name.
|
|
914
|
+
--- Default: `'[%w_%.]'` (alphanumeric, underscore, or dot).
|
|
915
|
+
--- Note: should be enclosed in `[]`.
|
|
916
|
+
MiniAi.gen_spec.function_call = function(opts)
|
|
917
|
+
opts = vim.tbl_deep_extend('force', { name_pattern = '[%w_%.]' }, opts or {})
|
|
918
|
+
-- Use frontier pattern to select widest possible name
|
|
919
|
+
return { '%f' .. opts.name_pattern .. opts.name_pattern .. '+%b()', '^.-%(().*()%)$' }
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
--- Pair specification
|
|
923
|
+
---
|
|
924
|
+
--- Use it to define textobject for region surrounded with `left` from left and
|
|
925
|
+
--- `right` from right. The `a` textobject includes both edges, `i` - excludes them.
|
|
926
|
+
---
|
|
927
|
+
--- Region can be one of several types (controlled with `opts.type`). All
|
|
928
|
+
--- examples are for default search method, `a` textobject, and use `'_'` as
|
|
929
|
+
--- both `left` and `right`:
|
|
930
|
+
--- - Non-balanced (`{ type = 'non-balanced' }`), default. Equivalent to using
|
|
931
|
+
--- `x.-y` as first pattern. Example: on line `_a_b_c_` it consecutively
|
|
932
|
+
--- matches `_a_`, `_b_`, `_c_`.
|
|
933
|
+
--- - Balanced (`{ type = 'balanced' }`). Equivalent to using `%bxy` as first
|
|
934
|
+
--- pattern. Example: on line `_a_b_c_` it consecutively matches `_a_`, `_c_`.
|
|
935
|
+
--- Note: both `left` and `right` should be single character.
|
|
936
|
+
--- - Greedy (`{ type = 'greedy' }`). Like non-balanced but will select maximum
|
|
937
|
+
--- consecutive `left` and `right` edges. Example: on line `__a__b_` it
|
|
938
|
+
--- consecutively selects `__a__` and `__b_`. Note: both `left` and `right`
|
|
939
|
+
--- should be single character.
|
|
940
|
+
---
|
|
941
|
+
---@param left string Left edge.
|
|
942
|
+
---@param right string Right edge.
|
|
943
|
+
---@param opts table|nil Options. Possible fields:
|
|
944
|
+
--- - <type> - Type of a pair. One of `'non-balanced'` (default), `'balanced'`,
|
|
945
|
+
--- `'greedy'`.
|
|
946
|
+
MiniAi.gen_spec.pair = function(left, right, opts)
|
|
947
|
+
if not (type(left) == 'string' and type(right) == 'string') then
|
|
948
|
+
H.error('Both `left` and `right` should be strings.')
|
|
949
|
+
end
|
|
950
|
+
opts = vim.tbl_deep_extend('force', { type = 'non-balanced' }, opts or {})
|
|
951
|
+
|
|
952
|
+
if (opts.type == 'balanced' or opts.type == 'greedy') and not (left:len() == 1 and right:len() == 1) then
|
|
953
|
+
local msg =
|
|
954
|
+
string.format([[Both `left` and `right` should be single character for `opts.type == '%s'`.]], opts.type)
|
|
955
|
+
H.error(msg)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
local left_esc = vim.pesc(left)
|
|
959
|
+
local right_esc = vim.pesc(right)
|
|
960
|
+
|
|
961
|
+
if opts.type == 'balanced' then return { string.format('%%b%s%s', left, right), '^.().*().$' } end
|
|
962
|
+
if opts.type == 'non-balanced' then return { string.format('%s().-()%s', left_esc, right_esc) } end
|
|
963
|
+
if opts.type == 'greedy' then
|
|
964
|
+
return { string.format('%%f[%s]%s+()[^%s]-()%s+%%f[^%s]', left_esc, left_esc, left_esc, right_esc, right_esc) }
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
H.error([[`opts.type` should be one of 'balanced', 'non-balanced', 'greedy'.]])
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
--- Treesitter specification
|
|
971
|
+
---
|
|
972
|
+
--- This is a specification in function form. When called with a pair of
|
|
973
|
+
--- treesitter captures, it returns a specification function outputting an
|
|
974
|
+
--- array of regions that match corresponding (`a` or `i`) capture.
|
|
975
|
+
---
|
|
976
|
+
--- In order for this to work, apart from working treesitter parser for desired
|
|
977
|
+
--- language, user should have a reachable language-specific `'textobjects'`
|
|
978
|
+
--- query (see |vim.treesitter.query.get()|).
|
|
979
|
+
--- The most straightforward way for this is to have `textobjects.scm` query
|
|
980
|
+
--- file with treesitter captures stored in some recognized path. This is
|
|
981
|
+
--- primarily designed to be compatible with plugin
|
|
982
|
+
--- `nvim-treesitter/nvim-treesitter-textobjects`, but can be used without it.
|
|
983
|
+
---
|
|
984
|
+
--- Two most common approaches for having a query file:
|
|
985
|
+
--- - Install `nvim-treesitter/nvim-treesitter-textobjects`. It has curated and
|
|
986
|
+
--- well maintained builtin query files for many languages with a standardized
|
|
987
|
+
--- capture names, like `function.outer`, `function.inner`, etc.
|
|
988
|
+
--- - Manually create file `after/queries/<language-name>/textobjects.scm` in
|
|
989
|
+
--- your |$XDG_CONFIG_HOME| directory. It should contain queries with
|
|
990
|
+
--- captures (later used to define textobjects). See |lua-treesitter-query|.
|
|
991
|
+
--- To verify that query file is reachable, run (example for "lua" language,
|
|
992
|
+
--- output should have at least an intended file): >vim
|
|
993
|
+
---
|
|
994
|
+
--- :lua print(vim.inspect(vim.treesitter.query.get_files('lua','textobjects')))
|
|
995
|
+
--- <
|
|
996
|
+
--- Example configuration for function definition textobject with
|
|
997
|
+
--- `nvim-treesitter/nvim-treesitter-textobjects` captures:
|
|
998
|
+
--- >lua
|
|
999
|
+
--- local spec_treesitter = require('mini.ai').gen_spec.treesitter
|
|
1000
|
+
--- require('mini.ai').setup({
|
|
1001
|
+
--- custom_textobjects = {
|
|
1002
|
+
--- F = spec_treesitter({ a = '@function.outer', i = '@function.inner' }),
|
|
1003
|
+
--- o = spec_treesitter({
|
|
1004
|
+
--- a = { '@conditional.outer', '@loop.outer' },
|
|
1005
|
+
--- i = { '@conditional.inner', '@loop.inner' },
|
|
1006
|
+
--- })
|
|
1007
|
+
--- }
|
|
1008
|
+
--- })
|
|
1009
|
+
--- <
|
|
1010
|
+
--- Notes:
|
|
1011
|
+
--- - Be sure that query files don't contain unknown |treesitter-directives|
|
|
1012
|
+
--- (like `#make-range!`, for example). Otherwise textobject for such capture
|
|
1013
|
+
--- might not be found as |lua-treesitter-core| won't treat them as captures.
|
|
1014
|
+
--- Verify with `:=vim.treesitter.query.get('lang', 'textobjects')` and see
|
|
1015
|
+
--- if the target capture is recognized as one.
|
|
1016
|
+
--- - It uses buffer's |filetype| to determine query language.
|
|
1017
|
+
--- - It first searches the language under cursor for matches. If no matches are
|
|
1018
|
+
--- found, it falls back to searching parent languages (up to the buffer's root
|
|
1019
|
+
--- language). If no matches are found again, it falls back to recursively
|
|
1020
|
+
--- searching all children languages (from the language under cursor). If no
|
|
1021
|
+
--- matches again - report no matches.
|
|
1022
|
+
--- - On large files it is slower than pattern-based textobjects. Still very
|
|
1023
|
+
--- fast though (one search should be magnitude of milliseconds or tens of
|
|
1024
|
+
--- milliseconds on really large file).
|
|
1025
|
+
---
|
|
1026
|
+
---@param ai_captures table Captures for `a` and `i` textobjects: table with
|
|
1027
|
+
--- <a> and <i> fields with captures for `a` and `i` textobjects respectively.
|
|
1028
|
+
--- Each value can be either a string capture (should start with `'@'`) or an
|
|
1029
|
+
--- array of such captures (best among all matches will be chosen).
|
|
1030
|
+
---@param opts table|nil Options. Possible values:
|
|
1031
|
+
--- - <use_nvim_treesitter> - whether to try to use `nvim-treesitter` plugin
|
|
1032
|
+
--- (if present) to do the query. It used to implement more advanced behavior
|
|
1033
|
+
--- and more coherent experience if `nvim-treesitter-textobjects` queries are
|
|
1034
|
+
--- used. However, as |lua-treesitter-core| methods are more capable now,
|
|
1035
|
+
--- the option will soon be removed. Only present for backward compatibility.
|
|
1036
|
+
--- Default: `false`.
|
|
1037
|
+
---
|
|
1038
|
+
---@return function Function with |MiniAi.find_textobject()| signature which
|
|
1039
|
+
--- returns array of current buffer regions representing matches for
|
|
1040
|
+
--- corresponding (`a` or `i`) treesitter capture.
|
|
1041
|
+
---
|
|
1042
|
+
---@seealso - |MiniAi-textobject-specification| for how this type of textobject
|
|
1043
|
+
--- specification is processed.
|
|
1044
|
+
--- - |vim.treesitter.query.get()| for how query is fetched.
|
|
1045
|
+
--- - |Query:iter_captures()| for how all query captures are iterated in case of
|
|
1046
|
+
--- no `nvim-treesitter`.
|
|
1047
|
+
MiniAi.gen_spec.treesitter = function(ai_captures, opts)
|
|
1048
|
+
-- TODO: Remove after releasing 'mini.nvim' 0.17.0
|
|
1049
|
+
opts = vim.tbl_deep_extend('force', { use_nvim_treesitter = false }, opts or {})
|
|
1050
|
+
ai_captures = H.prepare_ai_captures(ai_captures)
|
|
1051
|
+
|
|
1052
|
+
-- Tree-sitter ranges are 0-based, end-exclusive, and usually
|
|
1053
|
+
-- `row1-col1-byte1-row2-col2-byte2` (i.e. "range six") format.
|
|
1054
|
+
local ts_range_to_region = function(r)
|
|
1055
|
+
-- The `master` branch of 'nvim-treesitter' can return "range four" format
|
|
1056
|
+
-- if it uses custom directives, like `#make-range!`. Due to the fact that
|
|
1057
|
+
-- it doesn't fully mock the `TSNode:range()` method to return "range six".
|
|
1058
|
+
-- TODO: Remove after 'nvim-treesitter' `master` branch support is dropped.
|
|
1059
|
+
local offset = #r == 4 and -1 or 0
|
|
1060
|
+
local res = { from = { line = r[1] + 1, col = r[2] + 1 }, to = { line = r[4 + offset] + 1, col = r[5 + offset] } }
|
|
1061
|
+
|
|
1062
|
+
-- NOTE: Adjust "row-exclusive, col-0" range that means "all previous row
|
|
1063
|
+
-- including the newline character"
|
|
1064
|
+
if res.to.col == 0 then
|
|
1065
|
+
res.to.line = res.to.line - 1
|
|
1066
|
+
res.to.col = vim.fn.col({ res.to.line, '$' })
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
return res
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
return function(ai_type, _, _)
|
|
1073
|
+
-- Get array of matched treesitter nodes
|
|
1074
|
+
local target_captures = ai_captures[ai_type]
|
|
1075
|
+
local has_nvim_treesitter = pcall(require, 'nvim-treesitter') and pcall(require, 'nvim-treesitter.query')
|
|
1076
|
+
local range_querier = (has_nvim_treesitter and opts.use_nvim_treesitter) and H.get_matched_ranges_plugin
|
|
1077
|
+
or H.get_matched_ranges_builtin
|
|
1078
|
+
local matched_ranges = range_querier(target_captures)
|
|
1079
|
+
return vim.tbl_map(ts_range_to_region, matched_ranges)
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
--- Specification from user prompt
|
|
1084
|
+
---
|
|
1085
|
+
--- - Ask user for left and right textobject edges as raw strings (no pattern).
|
|
1086
|
+
--- It uses |MiniInput.get()| (if enabled) or |input().
|
|
1087
|
+
--- - Construct specification for a textobject that matches from left edge string
|
|
1088
|
+
--- to right edge string: `a` includes both strings, `i` only insides.
|
|
1089
|
+
---
|
|
1090
|
+
--- Used for |MiniAi-builtin-textobjects| with identifier `?`.
|
|
1091
|
+
---
|
|
1092
|
+
---@return function Textobject specification as function.
|
|
1093
|
+
MiniAi.gen_spec.user_prompt = function()
|
|
1094
|
+
return function()
|
|
1095
|
+
-- Using cache allows for a dot-repeat without another user input
|
|
1096
|
+
if H.cache.prompted_textobject ~= nil then return H.cache.prompted_textobject end
|
|
1097
|
+
|
|
1098
|
+
local left = H.user_input('Left edge')
|
|
1099
|
+
if left == nil or left == '' then return end
|
|
1100
|
+
local right = H.user_input('Right edge')
|
|
1101
|
+
if right == nil or right == '' then return end
|
|
1102
|
+
|
|
1103
|
+
-- Clean command line from prompt messages (does not work in Visual mode)
|
|
1104
|
+
vim.cmd([[echo '' | redraw]])
|
|
1105
|
+
|
|
1106
|
+
local left_esc, right_esc = vim.pesc(left), vim.pesc(right)
|
|
1107
|
+
local res = { string.format('%s().-()%s', left_esc, right_esc) }
|
|
1108
|
+
H.cache.prompted_textobject = res
|
|
1109
|
+
return res
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
--- Visually select textobject region
|
|
1114
|
+
---
|
|
1115
|
+
--- Does nothing if no region is found.
|
|
1116
|
+
---
|
|
1117
|
+
---@param ai_type string One of `'a'` or `'i'`.
|
|
1118
|
+
---@param id string Single character string representing textobject id.
|
|
1119
|
+
---@param opts table|nil Same as in |MiniAi.find_textobject()|. Extra fields:
|
|
1120
|
+
--- - <vis_mode> - One of `'v'`, `'V'`, or `'\22'` (escaped version of `'<C-v>'`).
|
|
1121
|
+
--- Default: Latest visual mode.
|
|
1122
|
+
--- - <operator_pending> - Whether selection is for Operator-pending mode.
|
|
1123
|
+
--- Used in that mode's mappings, shouldn't be used directly. Default: `false`.
|
|
1124
|
+
MiniAi.select_textobject = function(ai_type, id, opts)
|
|
1125
|
+
if H.is_disabled() then return end
|
|
1126
|
+
|
|
1127
|
+
opts = opts or {}
|
|
1128
|
+
local operator_pending = opts.operator_pending
|
|
1129
|
+
|
|
1130
|
+
-- Exit to Normal before getting textobject id. This way invalid id doesn't
|
|
1131
|
+
-- result into staying in current mode (which seems to be more convenient).
|
|
1132
|
+
H.exit_to_normal_mode()
|
|
1133
|
+
|
|
1134
|
+
local tobj = MiniAi.find_textobject(ai_type, id, opts)
|
|
1135
|
+
if tobj == nil then return end
|
|
1136
|
+
|
|
1137
|
+
local set_cursor = function(position) vim.api.nvim_win_set_cursor(0, { position.line, position.col - 1 }) end
|
|
1138
|
+
|
|
1139
|
+
-- Allow empty regions
|
|
1140
|
+
local tobj_is_empty = tobj.to == nil
|
|
1141
|
+
tobj.to = tobj.to or tobj.from
|
|
1142
|
+
|
|
1143
|
+
-- Compute selection type preferring the one coming from textobject
|
|
1144
|
+
local vis_mode = tobj.vis_mode
|
|
1145
|
+
if vis_mode == nil or not H.is_visual_mode(vis_mode) then
|
|
1146
|
+
local prev_vis_mode = vim.fn.visualmode()
|
|
1147
|
+
prev_vis_mode = prev_vis_mode == '' and 'v' or prev_vis_mode
|
|
1148
|
+
vis_mode = opts.vis_mode and vim.api.nvim_replace_termcodes(opts.vis_mode, true, true, true) or prev_vis_mode
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
local cache_eventignore = vim.o.eventignore
|
|
1152
|
+
-- Allow going past end of line in order to collapse multiline regions
|
|
1153
|
+
local cache_virtualedit, cache_whichwrap = vim.o.virtualedit, vim.o.whichwrap
|
|
1154
|
+
|
|
1155
|
+
-- Cache window horizontal view data to possibly counter unwanted side scroll
|
|
1156
|
+
local leftcol = vim.fn.winsaveview().leftcol
|
|
1157
|
+
|
|
1158
|
+
pcall(function()
|
|
1159
|
+
-- Do nothing in Operator-pending mode for empty region (except `c`, `d`,
|
|
1160
|
+
-- or selected "replace" operators). These are hand picked because they
|
|
1161
|
+
-- completely remove selected text, which is necessary for currently only
|
|
1162
|
+
-- possible empty region selection implementation.
|
|
1163
|
+
local is_empty_opending = tobj_is_empty and opts.operator_pending
|
|
1164
|
+
if is_empty_opending then
|
|
1165
|
+
local is_allowed_empty_opending = vim.v.operator == 'c'
|
|
1166
|
+
or vim.v.operator == 'd'
|
|
1167
|
+
or (vim.v.operator == 'g@' and vim.o.operatorfunc:find('MiniOperators%.replace') ~= nil)
|
|
1168
|
+
or (vim.v.operator == 'g@' and vim.o.operatorfunc:find('substitute') ~= nil)
|
|
1169
|
+
if not is_allowed_empty_opending then return H.message('Textobject region is empty. Nothing is done.') end
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
-- Allow setting cursor past line end (allows collapsing multiline region)
|
|
1173
|
+
-- NOTE: This doesn't work for 'virtualedit=all' and 'selection=inclusive'
|
|
1174
|
+
-- (default). The reason is that later option restoring is done immediately
|
|
1175
|
+
-- leading to a selection obey 'virtualedit=all' rules and thus won't treat
|
|
1176
|
+
-- end-of-line as '\n' and collapse multiline region. The solution is to
|
|
1177
|
+
-- `vim.schedule()` option restore, but it feels too much for a niche case.
|
|
1178
|
+
vim.o.virtualedit = 'onemore'
|
|
1179
|
+
|
|
1180
|
+
-- Select region:
|
|
1181
|
+
-- - Go from start to end stay at range end in Visual mode (as done in
|
|
1182
|
+
-- built-in visual selection).
|
|
1183
|
+
-- - Open just enough folds to have both ends visible.
|
|
1184
|
+
-- - Respect exclusive selection (including when selecting end of line)
|
|
1185
|
+
set_cursor(tobj.from)
|
|
1186
|
+
vim.cmd('normal! zv')
|
|
1187
|
+
vim.cmd('normal! ' .. vis_mode)
|
|
1188
|
+
set_cursor(tobj.to)
|
|
1189
|
+
if vim.o.selection == 'exclusive' and not tobj_is_empty then vim.cmd('set whichwrap=l | normal! l') end
|
|
1190
|
+
vim.cmd('normal! zv')
|
|
1191
|
+
|
|
1192
|
+
-- Restore horizontal view which was possibly affected by moving cursor
|
|
1193
|
+
-- NOTE: It seems to not affect cursor if it is outside of restored view
|
|
1194
|
+
vim.fn.winrestview({ leftcol = leftcol })
|
|
1195
|
+
|
|
1196
|
+
if is_empty_opending then
|
|
1197
|
+
-- Add single space (without triggering events) and visually select it.
|
|
1198
|
+
-- Seems like the only way to make `ci)` and `di)` move inside empty
|
|
1199
|
+
-- brackets. Original idea is from 'wellle/targets.vim'.
|
|
1200
|
+
vim.o.eventignore = 'all'
|
|
1201
|
+
|
|
1202
|
+
-- First escape from previously started Visual mode
|
|
1203
|
+
vim.cmd([[silent! execute "normal! \<Esc>i \<Esc>v"]])
|
|
1204
|
+
end
|
|
1205
|
+
end)
|
|
1206
|
+
|
|
1207
|
+
-- Restore options
|
|
1208
|
+
vim.o.eventignore = cache_eventignore
|
|
1209
|
+
vim.o.virtualedit, vim.o.whichwrap = cache_virtualedit, cache_whichwrap
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
-- Helper data ================================================================
|
|
1213
|
+
-- Module default config
|
|
1214
|
+
H.default_config = vim.deepcopy(MiniAi.config)
|
|
1215
|
+
|
|
1216
|
+
-- Cache for various operations
|
|
1217
|
+
H.cache = {}
|
|
1218
|
+
|
|
1219
|
+
-- Builtin textobjects
|
|
1220
|
+
H.builtin_textobjects = {
|
|
1221
|
+
-- Use balanced pair for brackets. Use opening ones to possibly remove edge
|
|
1222
|
+
-- whitespace from `i` textobject.
|
|
1223
|
+
['('] = { '%b()', '^.%s*().-()%s*.$' },
|
|
1224
|
+
[')'] = { '%b()', '^.().*().$' },
|
|
1225
|
+
['['] = { '%b[]', '^.%s*().-()%s*.$' },
|
|
1226
|
+
[']'] = { '%b[]', '^.().*().$' },
|
|
1227
|
+
['{'] = { '%b{}', '^.%s*().-()%s*.$' },
|
|
1228
|
+
['}'] = { '%b{}', '^.().*().$' },
|
|
1229
|
+
['<'] = { '%b<>', '^.%s*().-()%s*.$' },
|
|
1230
|
+
['>'] = { '%b<>', '^.().*().$' },
|
|
1231
|
+
-- Use special "same balanced" pattern to select quotes in pairs
|
|
1232
|
+
["'"] = { "%b''", '^.().*().$' },
|
|
1233
|
+
['"'] = { '%b""', '^.().*().$' },
|
|
1234
|
+
['`'] = { '%b``', '^.().*().$' },
|
|
1235
|
+
-- Derived from user prompt
|
|
1236
|
+
['?'] = MiniAi.gen_spec.user_prompt(),
|
|
1237
|
+
-- Argument
|
|
1238
|
+
['a'] = MiniAi.gen_spec.argument(),
|
|
1239
|
+
-- Brackets
|
|
1240
|
+
['b'] = { { '%b()', '%b[]', '%b{}' }, '^.().*().$' },
|
|
1241
|
+
-- Function call
|
|
1242
|
+
['f'] = MiniAi.gen_spec.function_call(),
|
|
1243
|
+
-- Tag
|
|
1244
|
+
['t'] = { '<(%w-)%f[^<%w][^<>]->.-</%1>', '^<.->().*()</[^/]->$' },
|
|
1245
|
+
-- Quotes
|
|
1246
|
+
['q'] = { { "%b''", '%b""', '%b``' }, '^.().*().$' },
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
-- Module's namespaces
|
|
1250
|
+
H.ns_id = {
|
|
1251
|
+
-- Track user input
|
|
1252
|
+
input = vim.api.nvim_create_namespace('MiniAiInput'),
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
-- Helper functionality =======================================================
|
|
1256
|
+
-- Settings -------------------------------------------------------------------
|
|
1257
|
+
H.setup_config = function(config)
|
|
1258
|
+
H.check_type('config', config, 'table', true)
|
|
1259
|
+
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
|
|
1260
|
+
|
|
1261
|
+
H.check_type('custom_textobjects', config.custom_textobjects, 'table', true)
|
|
1262
|
+
|
|
1263
|
+
H.check_type('mappings', config.mappings, 'table')
|
|
1264
|
+
H.check_type('mappings.around', config.mappings.around, 'string')
|
|
1265
|
+
H.check_type('mappings.inside', config.mappings.inside, 'string')
|
|
1266
|
+
H.check_type('mappings.around_next', config.mappings.around_next, 'string')
|
|
1267
|
+
H.check_type('mappings.inside_next', config.mappings.inside_next, 'string')
|
|
1268
|
+
H.check_type('mappings.around_last', config.mappings.around_last, 'string')
|
|
1269
|
+
H.check_type('mappings.inside_last', config.mappings.inside_last, 'string')
|
|
1270
|
+
H.check_type('mappings.goto_left', config.mappings.goto_left, 'string')
|
|
1271
|
+
H.check_type('mappings.goto_right', config.mappings.goto_right, 'string')
|
|
1272
|
+
|
|
1273
|
+
H.check_type('n_lines', config.n_lines, 'number')
|
|
1274
|
+
H.validate_search_method(config.search_method, 'search_method')
|
|
1275
|
+
H.check_type('silent', config.silent, 'boolean')
|
|
1276
|
+
|
|
1277
|
+
return config
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
--stylua: ignore
|
|
1281
|
+
H.apply_config = function(config)
|
|
1282
|
+
MiniAi.config = config
|
|
1283
|
+
|
|
1284
|
+
-- Make mappings
|
|
1285
|
+
local maps = config.mappings
|
|
1286
|
+
local m = function(mode, lhs, rhs, opts)
|
|
1287
|
+
opts.expr = true
|
|
1288
|
+
-- Allow recursive mapping to support falling back on user defined mapping
|
|
1289
|
+
opts.remap = true
|
|
1290
|
+
H.map(mode, lhs, rhs, opts)
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
m({ 'n', 'x', 'o' }, maps.goto_left, function() return H.expr_motion('left') end, { desc = 'Move to left "around"' })
|
|
1294
|
+
m({ 'n', 'x', 'o' }, maps.goto_right, function() return H.expr_motion('right') end, { desc = 'Move to right "around"' })
|
|
1295
|
+
|
|
1296
|
+
local make_tobj = function(mode, ai_type, search_method)
|
|
1297
|
+
return function() return H.expr_textobject(mode, ai_type, { search_method = search_method }) end
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
m('x', maps.around, make_tobj('x', 'a'), { desc = 'Around textobject' })
|
|
1301
|
+
m('x', maps.inside, make_tobj('x', 'i'), { desc = 'Inside textobject' })
|
|
1302
|
+
m('o', maps.around, make_tobj('o', 'a'), { desc = 'Around textobject' })
|
|
1303
|
+
m('o', maps.inside, make_tobj('o', 'i'), { desc = 'Inside textobject' })
|
|
1304
|
+
|
|
1305
|
+
m('x', maps.around_next, make_tobj('x', 'a', 'next'), { desc = 'Around next textobject' })
|
|
1306
|
+
m('x', maps.around_last, make_tobj('x', 'a', 'prev'), { desc = 'Around last textobject' })
|
|
1307
|
+
m('x', maps.inside_next, make_tobj('x', 'i', 'next'), { desc = 'Inside next textobject' })
|
|
1308
|
+
m('x', maps.inside_last, make_tobj('x', 'i', 'prev'), { desc = 'Inside last textobject' })
|
|
1309
|
+
m('o', maps.around_next, make_tobj('o', 'a', 'next'), { desc = 'Around next textobject' })
|
|
1310
|
+
m('o', maps.around_last, make_tobj('o', 'a', 'prev'), { desc = 'Around last textobject' })
|
|
1311
|
+
m('o', maps.inside_next, make_tobj('o', 'i', 'next'), { desc = 'Inside next textobject' })
|
|
1312
|
+
m('o', maps.inside_last, make_tobj('o', 'i', 'prev'), { desc = 'Inside last textobject' })
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
H.is_disabled = function() return vim.g.miniai_disable == true or vim.b.miniai_disable == true end
|
|
1316
|
+
|
|
1317
|
+
H.get_config = function(config)
|
|
1318
|
+
return vim.tbl_deep_extend('force', MiniAi.config, vim.b.miniai_config or {}, config or {})
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
H.is_search_method = function(x, x_name)
|
|
1322
|
+
x = x or H.get_config().search_method
|
|
1323
|
+
x_name = x_name or '`config.search_method`'
|
|
1324
|
+
|
|
1325
|
+
local allowed_methods = vim.tbl_keys(H.span_compare_methods)
|
|
1326
|
+
if vim.tbl_contains(allowed_methods, x) then return true end
|
|
1327
|
+
|
|
1328
|
+
table.sort(allowed_methods)
|
|
1329
|
+
local allowed_methods_string = table.concat(vim.tbl_map(vim.inspect, allowed_methods), ', ')
|
|
1330
|
+
local msg = ([[%s should be one of %s.]]):format(x_name, allowed_methods_string)
|
|
1331
|
+
return false, msg
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
H.validate_search_method = function(x, x_name)
|
|
1335
|
+
local is_valid, msg = H.is_search_method(x, x_name)
|
|
1336
|
+
if not is_valid then H.error(msg) end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
-- Mappings -------------------------------------------------------------------
|
|
1340
|
+
H.expr_textobject = function(mode, ai_type, opts)
|
|
1341
|
+
local tobj_id = H.user_textobject_id(ai_type)
|
|
1342
|
+
|
|
1343
|
+
if tobj_id == nil then return mode == 'o' and '<Esc>' or '' end
|
|
1344
|
+
|
|
1345
|
+
-- Possibly fall back to builtin `a`/`i` textobjects
|
|
1346
|
+
if H.is_disabled() or not H.is_valid_textobject_id(tobj_id) then
|
|
1347
|
+
local mappings = H.get_config().mappings
|
|
1348
|
+
local main_key = mappings[ai_type == 'a' and 'around' or 'inside']
|
|
1349
|
+
local res = main_key .. tobj_id
|
|
1350
|
+
-- If fallback is an existing user mapping, prepend it with '<Ignore>'.
|
|
1351
|
+
-- This deals with `:h recursive_mapping`. Shouldn't prepend if it is a
|
|
1352
|
+
-- builtin textobject. Also see https://github.com/vim/vim/issues/10907 .
|
|
1353
|
+
if vim.fn.maparg(res, mode) ~= '' then res = '<Ignore>' .. res end
|
|
1354
|
+
return res
|
|
1355
|
+
end
|
|
1356
|
+
opts = vim.tbl_deep_extend('force', H.get_default_opts(), opts or {})
|
|
1357
|
+
|
|
1358
|
+
-- Clear cache
|
|
1359
|
+
H.cache = {}
|
|
1360
|
+
|
|
1361
|
+
-- Construct call options based on mode
|
|
1362
|
+
local reference_region_field, operator_pending_field, vis_mode_field = 'nil', 'nil', 'nil'
|
|
1363
|
+
|
|
1364
|
+
if mode == 'x' then
|
|
1365
|
+
-- Use Visual selection as reference region for Visual mode mappings
|
|
1366
|
+
reference_region_field = vim.inspect(H.get_visual_region(), { newline = '', indent = '' })
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
if mode == 'o' then
|
|
1370
|
+
-- Supply `operator_pending` flag in Operator-pending mode
|
|
1371
|
+
operator_pending_field = 'true'
|
|
1372
|
+
|
|
1373
|
+
-- Take into account forced Operator-pending modes ('nov', 'noV', 'no<C-V>')
|
|
1374
|
+
vis_mode_field = vim.fn.mode(1):gsub('^no', '')
|
|
1375
|
+
vis_mode_field = vim.inspect(vis_mode_field == '' and 'v' or vis_mode_field)
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
-- Make expression
|
|
1379
|
+
return '<Cmd>lua '
|
|
1380
|
+
.. string.format(
|
|
1381
|
+
[[MiniAi.select_textobject('%s', %s, { search_method = %s, n_times = %d, reference_region = %s, operator_pending = %s, vis_mode = %s })]],
|
|
1382
|
+
ai_type,
|
|
1383
|
+
vim.inspect(tobj_id),
|
|
1384
|
+
vim.inspect(opts.search_method),
|
|
1385
|
+
vim.v.count1,
|
|
1386
|
+
reference_region_field,
|
|
1387
|
+
operator_pending_field,
|
|
1388
|
+
vis_mode_field
|
|
1389
|
+
)
|
|
1390
|
+
.. '<CR>'
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
H.expr_motion = function(side)
|
|
1394
|
+
if H.is_disabled() then return '' end
|
|
1395
|
+
|
|
1396
|
+
if not (side == 'left' or side == 'right') then H.error([[`side` should be one of 'left' or 'right'.]]) end
|
|
1397
|
+
|
|
1398
|
+
-- Get user input
|
|
1399
|
+
local tobj_id = H.user_textobject_id('a')
|
|
1400
|
+
if tobj_id == nil then return end
|
|
1401
|
+
|
|
1402
|
+
-- Clear cache
|
|
1403
|
+
H.cache = {}
|
|
1404
|
+
|
|
1405
|
+
-- Make expression for moving cursor
|
|
1406
|
+
return '<Cmd>lua '
|
|
1407
|
+
.. string.format([[MiniAi.move_cursor('%s', 'a', %s, { n_times = %d })]], side, vim.inspect(tobj_id), vim.v.count1)
|
|
1408
|
+
.. '<CR>'
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
-- Work with textobject info --------------------------------------------------
|
|
1412
|
+
H.make_textobject_table = function()
|
|
1413
|
+
-- Extend builtins with data from `config`. Don't use `tbl_deep_extend()`
|
|
1414
|
+
-- because only top level keys should be merged.
|
|
1415
|
+
local textobjects = vim.tbl_extend('force', H.builtin_textobjects, H.get_config().custom_textobjects or {})
|
|
1416
|
+
|
|
1417
|
+
-- Use default textobject pattern for anything excluding Latin characters, as
|
|
1418
|
+
-- they are needed to fall back to Neovim's built-in textobjects (like `aw`)
|
|
1419
|
+
return setmetatable(textobjects, {
|
|
1420
|
+
__index = function(_, key)
|
|
1421
|
+
if type(key) == 'string' and string.find(key, '^%a$') ~= nil then return end
|
|
1422
|
+
local key_esc = vim.pesc(key)
|
|
1423
|
+
-- Use `%f[]` to ensure maximum stretch in both directions. Include only
|
|
1424
|
+
-- right edge in `a` textobject.
|
|
1425
|
+
-- Example output: '_()()[^_]-()_+%f[^_]()'
|
|
1426
|
+
return { string.format('%s()()[^%s]-()%s+%%f[^%s]()', key_esc, key_esc, key_esc, key_esc) }
|
|
1427
|
+
end,
|
|
1428
|
+
})
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
H.get_textobject_spec = function(id, args)
|
|
1432
|
+
local textobject_tbl = H.make_textobject_table()
|
|
1433
|
+
local spec = textobject_tbl[id]
|
|
1434
|
+
|
|
1435
|
+
-- Allow function returning spec or region(s)
|
|
1436
|
+
if vim.is_callable(spec) then spec = spec(unpack(args)) end
|
|
1437
|
+
|
|
1438
|
+
-- Wrap callable tables to be an actual functions. Otherwise they might be
|
|
1439
|
+
-- confused with list of patterns.
|
|
1440
|
+
if H.is_composed_pattern(spec) then return vim.tbl_map(H.wrap_callable_table, spec) end
|
|
1441
|
+
|
|
1442
|
+
if not (H.is_region(spec) or H.is_region_array(spec)) then return nil end
|
|
1443
|
+
return spec
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
H.is_valid_textobject_id = function(id)
|
|
1447
|
+
local spec = H.make_textobject_table()[id]
|
|
1448
|
+
return type(spec) == 'table' or vim.is_callable(spec)
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
H.is_region = function(x)
|
|
1452
|
+
if type(x) ~= 'table' then return false end
|
|
1453
|
+
local from_is_valid = type(x.from) == 'table' and type(x.from.line) == 'number' and type(x.from.col) == 'number'
|
|
1454
|
+
-- Allow `to` to be `nil` to describe empty regions
|
|
1455
|
+
local to_is_valid = true
|
|
1456
|
+
if x.to ~= nil then
|
|
1457
|
+
to_is_valid = type(x.to) == 'table' and type(x.to.line) == 'number' and type(x.to.col) == 'number'
|
|
1458
|
+
end
|
|
1459
|
+
return from_is_valid and to_is_valid
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
H.is_region_array = function(x)
|
|
1463
|
+
if not H.islist(x) then return false end
|
|
1464
|
+
for _, v in ipairs(x) do
|
|
1465
|
+
if not H.is_region(v) then return false end
|
|
1466
|
+
end
|
|
1467
|
+
return true
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
H.is_composed_pattern = function(x)
|
|
1471
|
+
if not (H.islist(x) and #x > 0) then return false end
|
|
1472
|
+
for _, val in ipairs(x) do
|
|
1473
|
+
local val_type = type(val)
|
|
1474
|
+
if not (val_type == 'table' or val_type == 'string' or vim.is_callable(val)) then return false end
|
|
1475
|
+
end
|
|
1476
|
+
return true
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
-- Work with finding textobjects ----------------------------------------------
|
|
1480
|
+
---@param tobj_spec table Composed pattern. Last item(s) - extraction template.
|
|
1481
|
+
---@param ai_type string One of `'a'` or `'i'`.
|
|
1482
|
+
---@param opts table Textobject options with all fields present.
|
|
1483
|
+
---@private
|
|
1484
|
+
H.find_textobject_region = function(tobj_spec, ai_type, opts)
|
|
1485
|
+
local reference_region, n_times, n_lines = opts.reference_region, opts.n_times, opts.n_lines
|
|
1486
|
+
|
|
1487
|
+
if n_times == 0 then return end
|
|
1488
|
+
|
|
1489
|
+
-- Find `n_times` matching spans evolving from reference region span
|
|
1490
|
+
-- First try to find inside 0-neighborhood
|
|
1491
|
+
local neigh = H.get_neighborhood(reference_region, 0)
|
|
1492
|
+
local reference_span = neigh.region_to_span(reference_region)
|
|
1493
|
+
|
|
1494
|
+
local find_next = function(cur_reference_span)
|
|
1495
|
+
local res = H.find_best_match(neigh, tobj_spec, cur_reference_span, opts)
|
|
1496
|
+
|
|
1497
|
+
-- If didn't find in 0-neighborhood, possibly try extend one
|
|
1498
|
+
if res.span == nil then
|
|
1499
|
+
-- Stop if no need to extend neighborhood
|
|
1500
|
+
if n_lines == 0 or neigh.n_neighbors > 0 then return {} end
|
|
1501
|
+
|
|
1502
|
+
-- Update data with respect to new neighborhood
|
|
1503
|
+
local cur_reference_region = neigh.span_to_region(cur_reference_span)
|
|
1504
|
+
neigh = H.get_neighborhood(reference_region, n_lines)
|
|
1505
|
+
reference_span = neigh.region_to_span(reference_region)
|
|
1506
|
+
cur_reference_span = neigh.region_to_span(cur_reference_region)
|
|
1507
|
+
|
|
1508
|
+
-- Recompute based on new neighborhood
|
|
1509
|
+
res = H.find_best_match(neigh, tobj_spec, cur_reference_span, opts)
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
return res
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
local find_res = { span = reference_span }
|
|
1516
|
+
for _ = 1, n_times do
|
|
1517
|
+
find_res = find_next(find_res.span)
|
|
1518
|
+
if find_res.span == nil then return end
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
-- Extract final span
|
|
1522
|
+
local extract = function(span, extract_pattern)
|
|
1523
|
+
-- Use `nil` extract pattern to allow array of regions as textobject spec
|
|
1524
|
+
if extract_pattern == nil then return span end
|
|
1525
|
+
|
|
1526
|
+
-- First extract local (with respect to best matched span) span
|
|
1527
|
+
local s = neigh['1d']:sub(span.from, span.to - 1)
|
|
1528
|
+
local local_span = H.extract_span(s, extract_pattern, ai_type)
|
|
1529
|
+
|
|
1530
|
+
-- Convert local span to global
|
|
1531
|
+
local offset = span.from - 1
|
|
1532
|
+
return { from = local_span.from + offset, to = local_span.to + offset }
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
local final_span = extract(find_res.span, find_res.extract_pattern)
|
|
1536
|
+
|
|
1537
|
+
-- Ensure that output region is different from reference. This is needed if
|
|
1538
|
+
-- final span was shrunk during extraction and resulted into equal to input
|
|
1539
|
+
-- reference. This powers consecutive application of most `i` textobjects.
|
|
1540
|
+
if H.is_span_covering(reference_span, final_span) then
|
|
1541
|
+
find_res = find_next(find_res.span)
|
|
1542
|
+
if find_res.span == nil then return end
|
|
1543
|
+
final_span = extract(find_res.span, find_res.extract_pattern)
|
|
1544
|
+
if H.is_span_covering(reference_span, final_span) then return end
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
-- Convert to region
|
|
1548
|
+
return neigh.span_to_region(final_span, find_res.vis_mode)
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
H.get_default_opts = function()
|
|
1552
|
+
local config = H.get_config()
|
|
1553
|
+
local cur_pos = vim.api.nvim_win_get_cursor(0)
|
|
1554
|
+
return {
|
|
1555
|
+
n_lines = config.n_lines,
|
|
1556
|
+
n_times = vim.v.count1,
|
|
1557
|
+
-- Empty region at cursor position
|
|
1558
|
+
reference_region = { from = { line = cur_pos[1], col = cur_pos[2] + 1 } },
|
|
1559
|
+
search_method = config.search_method,
|
|
1560
|
+
}
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
-- Work with argument textobject ----------------------------------------------
|
|
1564
|
+
H.arg_get_separator_spans = function(s, sep_pattern, exclude_regions)
|
|
1565
|
+
if s:len() <= 2 then return {} end
|
|
1566
|
+
|
|
1567
|
+
-- Pre-compute edge separator spans (assumes edge characters are brackets)
|
|
1568
|
+
local left_bracket_span = { 1, 1 }
|
|
1569
|
+
local right_bracket_span = { s:len(), s:len() }
|
|
1570
|
+
|
|
1571
|
+
-- Get all separator spans (meaning separator is allowed to match more than
|
|
1572
|
+
-- a single character)
|
|
1573
|
+
local sep_spans = {}
|
|
1574
|
+
s:gsub('()' .. sep_pattern .. '()', function(l, r) table.insert(sep_spans, { l, r - 1 }) end)
|
|
1575
|
+
if #sep_spans == 0 then return { left_bracket_span, right_bracket_span } end
|
|
1576
|
+
|
|
1577
|
+
-- Remove separators that are in "excluded regions": by default, inside
|
|
1578
|
+
-- brackets or quotes
|
|
1579
|
+
local inner_s, forbidden = s:sub(2, -2), {}
|
|
1580
|
+
local add_to_forbidden = function(l, r) table.insert(forbidden, { l + 1, r }) end
|
|
1581
|
+
|
|
1582
|
+
for _, pat in ipairs(exclude_regions) do
|
|
1583
|
+
local capture_pat = string.format('()%s()', pat)
|
|
1584
|
+
inner_s:gsub(capture_pat, add_to_forbidden)
|
|
1585
|
+
end
|
|
1586
|
+
|
|
1587
|
+
local res = vim.tbl_filter(function(x) return not H.is_span_inside_spans(x, forbidden) end, sep_spans)
|
|
1588
|
+
|
|
1589
|
+
-- Append edge separators (assumes first and last characters are from
|
|
1590
|
+
-- brackets). This allows single argument and ensures at least 2 elements.
|
|
1591
|
+
table.insert(res, 1, left_bracket_span)
|
|
1592
|
+
table.insert(res, right_bracket_span)
|
|
1593
|
+
return res
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
-- Work with treesitter textobject --------------------------------------------
|
|
1597
|
+
H.prepare_ai_captures = function(ai_captures)
|
|
1598
|
+
local is_capture = function(x)
|
|
1599
|
+
if type(x) == 'string' then x = { x } end
|
|
1600
|
+
if not H.islist(x) then return false end
|
|
1601
|
+
|
|
1602
|
+
for _, v in ipairs(x) do
|
|
1603
|
+
if not (type(v) == 'string' and v:sub(1, 1) == '@') then return false end
|
|
1604
|
+
end
|
|
1605
|
+
return true
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1608
|
+
if not (type(ai_captures) == 'table' and is_capture(ai_captures.a) and is_capture(ai_captures.i)) then
|
|
1609
|
+
H.error('Wrong format for `ai_captures`. See `MiniAi.gen_spec.treesitter()` for details.')
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
local prepare = function(x)
|
|
1613
|
+
if type(x) == 'string' then return { x } end
|
|
1614
|
+
return x
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
return { a = prepare(ai_captures.a), i = prepare(ai_captures.i) }
|
|
1618
|
+
end
|
|
1619
|
+
|
|
1620
|
+
H.get_matched_ranges_plugin = function(captures)
|
|
1621
|
+
local ts_queries = require('nvim-treesitter.query')
|
|
1622
|
+
local buf_id = vim.api.nvim_get_current_buf()
|
|
1623
|
+
local matches = ts_queries.get_capture_matches_recursively(buf_id, captures, 'textobjects')
|
|
1624
|
+
local res = vim.tbl_map(function(m) return vim.treesitter.get_range(m.node, buf_id, m.metadata) end, matches)
|
|
1625
|
+
return res
|
|
1626
|
+
end
|
|
1627
|
+
|
|
1628
|
+
H.get_matched_ranges_builtin = function(captures)
|
|
1629
|
+
-- Get buffer's parser (LanguageTree)
|
|
1630
|
+
local buf_id = vim.api.nvim_get_current_buf()
|
|
1631
|
+
-- TODO: Remove `opts.error` after compatibility with Neovim=0.11 is dropped
|
|
1632
|
+
local has_parser, parser = pcall(vim.treesitter.get_parser, buf_id, nil, { error = false })
|
|
1633
|
+
if not has_parser or parser == nil then H.error_treesitter('parser') end
|
|
1634
|
+
|
|
1635
|
+
-- Get parser (LanguageTree) at cursor (important for injected languages)
|
|
1636
|
+
local pos = vim.api.nvim_win_get_cursor(0)
|
|
1637
|
+
local lang_tree = parser:language_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] })
|
|
1638
|
+
local init_lang_tree = lang_tree
|
|
1639
|
+
|
|
1640
|
+
local missing_query_langs = {}
|
|
1641
|
+
local res = {}
|
|
1642
|
+
-- Go up parent trees to work with injected languages
|
|
1643
|
+
while vim.tbl_isempty(res) and lang_tree ~= nil do
|
|
1644
|
+
H.append_lang_ranges(res, missing_query_langs, buf_id, captures, lang_tree)
|
|
1645
|
+
|
|
1646
|
+
-- `LanguageTree:parent()` was added in Neovim=0.10
|
|
1647
|
+
-- TODO: Drop extra check after compatibility with Neovim=0.9 is dropped
|
|
1648
|
+
lang_tree = lang_tree.parent and lang_tree:parent() or nil
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
-- Fall back to children trees for injected languages
|
|
1652
|
+
if vim.tbl_isempty(res) then
|
|
1653
|
+
local check_children
|
|
1654
|
+
check_children = function(l_tree)
|
|
1655
|
+
for _, child in pairs(l_tree:children()) do
|
|
1656
|
+
H.append_lang_ranges(res, missing_query_langs, buf_id, captures, child)
|
|
1657
|
+
check_children(child)
|
|
1658
|
+
end
|
|
1659
|
+
end
|
|
1660
|
+
check_children(init_lang_tree)
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
if vim.tbl_isempty(res) and not vim.tbl_isempty(missing_query_langs) then
|
|
1664
|
+
H.error_treesitter('query', vim.tbl_keys(missing_query_langs))
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
return res
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
H.append_lang_ranges = function(res, missing_query_langs, buf_id, captures, lang_tree)
|
|
1671
|
+
local lang = lang_tree:lang()
|
|
1672
|
+
local query = vim.treesitter.query.get(lang, 'textobjects')
|
|
1673
|
+
|
|
1674
|
+
if query ~= nil then H.append_ranges(res, buf_id, query, captures, lang_tree) end
|
|
1675
|
+
if query == nil then missing_query_langs[lang] = true end
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
H.append_ranges = function(res, buf_id, query, captures, lang_tree)
|
|
1679
|
+
-- Compute ranges of matched captures
|
|
1680
|
+
local capture_is_requested = vim.tbl_map(function(c) return vim.tbl_contains(captures, '@' .. c) end, query.captures)
|
|
1681
|
+
|
|
1682
|
+
for _, tree in ipairs(lang_tree:trees()) do
|
|
1683
|
+
-- TODO: Remove `opts.all`after compatibility with Neovim=0.10 is dropped
|
|
1684
|
+
for _, match, metadata in query:iter_matches(tree:root(), buf_id, nil, nil, { all = true }) do
|
|
1685
|
+
for capture_id, nodes in pairs(match) do
|
|
1686
|
+
local mt = metadata[capture_id]
|
|
1687
|
+
if capture_is_requested[capture_id] then table.insert(res, H.get_nodes_range_builtin(nodes, buf_id, mt)) end
|
|
1688
|
+
end
|
|
1689
|
+
end
|
|
1690
|
+
end
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1693
|
+
H.get_nodes_range_builtin = function(nodes, buf_id, metadata)
|
|
1694
|
+
-- In Neovim<0.10 `Query:iter_matches()` has `match` map to single node.
|
|
1695
|
+
-- TODO: Remove `opts.all`after compatibility with Neovim=0.9 is dropped
|
|
1696
|
+
nodes = type(nodes) == 'table' and nodes or { nodes }
|
|
1697
|
+
|
|
1698
|
+
-- Get matched range as spanning from left most node start to right most node
|
|
1699
|
+
-- end. This accounts for several matched nodes that are intentionally there
|
|
1700
|
+
-- to cover complex cases. Approach is named "quantified captures".
|
|
1701
|
+
local left, right
|
|
1702
|
+
for _, node in ipairs(nodes) do
|
|
1703
|
+
local range = vim.treesitter.get_range(node, buf_id, metadata)
|
|
1704
|
+
if left == nil or range[3] < left[3] then left = range end
|
|
1705
|
+
if right == nil or range[6] > right[6] then right = range end
|
|
1706
|
+
end
|
|
1707
|
+
return { left[1], left[2], left[3], right[4], right[5], right[6] }
|
|
1708
|
+
end
|
|
1709
|
+
|
|
1710
|
+
H.error_treesitter = function(failed_get, langs)
|
|
1711
|
+
local buf_id, ft = vim.api.nvim_get_current_buf(), vim.bo.filetype
|
|
1712
|
+
if langs == nil then
|
|
1713
|
+
local has_lang, ft_lang = pcall(vim.treesitter.language.get_lang, ft)
|
|
1714
|
+
-- `vim.treesitter.language.get_lang()` defaults to `ft` on Neovim>0.11
|
|
1715
|
+
-- TODO: Drop check after compatibility with Neovim=0.10 is dropped
|
|
1716
|
+
langs = (has_lang and ft_lang ~= nil) and { ft_lang } or { ft }
|
|
1717
|
+
end
|
|
1718
|
+
table.sort(langs)
|
|
1719
|
+
local langs_str = table.concat(vim.tbl_map(vim.inspect, langs), ', ')
|
|
1720
|
+
local langs_noun = #langs == 1 and 'language' or 'languages'
|
|
1721
|
+
local msg = string.format('Can not get %s for buffer %d and %s %s.', failed_get, buf_id, langs_noun, langs_str)
|
|
1722
|
+
H.error(msg)
|
|
1723
|
+
end
|
|
1724
|
+
|
|
1725
|
+
-- Work with matching spans ---------------------------------------------------
|
|
1726
|
+
---@param neighborhood table Output of `get_neighborhood()`.
|
|
1727
|
+
---@param tobj_spec table Textobject specification.
|
|
1728
|
+
---@param reference_span table Span to cover.
|
|
1729
|
+
---@param opts table Fields: <search_method>.
|
|
1730
|
+
---@private
|
|
1731
|
+
H.find_best_match = function(neighborhood, tobj_spec, reference_span, opts)
|
|
1732
|
+
local best_span, best_nested_pattern, best_vis_mode, current_nested_pattern
|
|
1733
|
+
local f = function(span, vis_mode)
|
|
1734
|
+
if H.is_better_span(span, best_span, reference_span, opts) then
|
|
1735
|
+
best_span, best_nested_pattern, best_vis_mode = span, current_nested_pattern, vis_mode
|
|
1736
|
+
end
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
if H.is_region_array(tobj_spec) then
|
|
1740
|
+
-- Iterate over all spans representing regions in array
|
|
1741
|
+
for _, region in ipairs(tobj_spec) do
|
|
1742
|
+
-- Consider region only if it is completely within neighborhood
|
|
1743
|
+
if neighborhood.is_region_inside(region) then f(neighborhood.region_to_span(region), region.vis_mode) end
|
|
1744
|
+
end
|
|
1745
|
+
else
|
|
1746
|
+
-- Iterate over all matched spans
|
|
1747
|
+
for _, nested_pattern in ipairs(H.cartesian_product(tobj_spec)) do
|
|
1748
|
+
current_nested_pattern = nested_pattern
|
|
1749
|
+
H.iterate_matched_spans(neighborhood['1d'], nested_pattern, f)
|
|
1750
|
+
end
|
|
1751
|
+
end
|
|
1752
|
+
|
|
1753
|
+
local extract_pattern
|
|
1754
|
+
if best_nested_pattern ~= nil then extract_pattern = best_nested_pattern[#best_nested_pattern] end
|
|
1755
|
+
return { span = best_span, vis_mode = best_vis_mode, extract_pattern = extract_pattern }
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
H.iterate_matched_spans = function(line, nested_pattern, f)
|
|
1759
|
+
local max_level = #nested_pattern
|
|
1760
|
+
-- Keep track of visited spans to ensure only one call of `f`.
|
|
1761
|
+
-- Example: `((a) (b))`, `{'%b()', '%b()'}`
|
|
1762
|
+
local visited = {}
|
|
1763
|
+
|
|
1764
|
+
local process
|
|
1765
|
+
process = function(level, level_line, level_offset)
|
|
1766
|
+
local pattern = nested_pattern[level]
|
|
1767
|
+
local next_span = function(s, init) return H.string_find(s, pattern, init) end
|
|
1768
|
+
if vim.is_callable(pattern) then next_span = pattern end
|
|
1769
|
+
|
|
1770
|
+
local is_same_balanced = type(pattern) == 'string' and pattern:match('^%%b(.)%1$') ~= nil
|
|
1771
|
+
local init = 1
|
|
1772
|
+
while init <= level_line:len() do
|
|
1773
|
+
local from, to = next_span(level_line, init)
|
|
1774
|
+
if from == nil then break end
|
|
1775
|
+
|
|
1776
|
+
if level == max_level then
|
|
1777
|
+
local found_match = H.new_span(from + level_offset, to + level_offset)
|
|
1778
|
+
local found_match_id = string.format('%s_%s', found_match.from, found_match.to)
|
|
1779
|
+
if not visited[found_match_id] then
|
|
1780
|
+
f(found_match)
|
|
1781
|
+
visited[found_match_id] = true
|
|
1782
|
+
end
|
|
1783
|
+
else
|
|
1784
|
+
local next_level_line = level_line:sub(from, to)
|
|
1785
|
+
local next_level_offset = level_offset + from - 1
|
|
1786
|
+
process(level + 1, next_level_line, next_level_offset)
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1789
|
+
-- Start searching from right end to implement "balanced" pair.
|
|
1790
|
+
-- This doesn't work with regular balanced pattern because it doesn't
|
|
1791
|
+
-- capture nested brackets.
|
|
1792
|
+
init = (is_same_balanced and to or from) + 1
|
|
1793
|
+
end
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
process(1, line, 0)
|
|
1797
|
+
end
|
|
1798
|
+
|
|
1799
|
+
-- NOTE: spans are end-exclusive to allow empty spans via `from == to`
|
|
1800
|
+
H.new_span = function(from, to) return { from = from, to = to == nil and from or (to + 1) } end
|
|
1801
|
+
|
|
1802
|
+
---@param candidate table Candidate span to test against `current`.
|
|
1803
|
+
---@param current table|nil Current best span.
|
|
1804
|
+
---@param reference table Reference span to cover.
|
|
1805
|
+
---@param opts table Fields: <search_method>.
|
|
1806
|
+
---@private
|
|
1807
|
+
H.is_better_span = function(candidate, current, reference, opts)
|
|
1808
|
+
-- Candidate should be never equal or nested inside reference
|
|
1809
|
+
if H.is_span_covering(reference, candidate) or H.is_span_equal(candidate, reference) then return false end
|
|
1810
|
+
|
|
1811
|
+
return H.span_compare_methods[opts.search_method](candidate, current, reference)
|
|
1812
|
+
end
|
|
1813
|
+
|
|
1814
|
+
H.span_compare_methods = {
|
|
1815
|
+
cover = function(candidate, current, reference)
|
|
1816
|
+
local res = H.is_better_covering_span(candidate, current, reference)
|
|
1817
|
+
if res ~= nil then return res end
|
|
1818
|
+
-- If both are not covering, `candidate` is not better (as it must cover)
|
|
1819
|
+
return false
|
|
1820
|
+
end,
|
|
1821
|
+
|
|
1822
|
+
cover_or_next = function(candidate, current, reference)
|
|
1823
|
+
local res = H.is_better_covering_span(candidate, current, reference)
|
|
1824
|
+
if res ~= nil then return res end
|
|
1825
|
+
|
|
1826
|
+
-- If not covering, `candidate` must be "next" and closer to reference
|
|
1827
|
+
if not H.is_span_on_left(reference, candidate) then return false end
|
|
1828
|
+
if current == nil then return true end
|
|
1829
|
+
|
|
1830
|
+
local dist = H.span_distance.next
|
|
1831
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1832
|
+
end,
|
|
1833
|
+
|
|
1834
|
+
cover_or_prev = function(candidate, current, reference)
|
|
1835
|
+
local res = H.is_better_covering_span(candidate, current, reference)
|
|
1836
|
+
if res ~= nil then return res end
|
|
1837
|
+
|
|
1838
|
+
-- If not covering, `candidate` must be "previous" and closer to reference
|
|
1839
|
+
if not H.is_span_on_left(candidate, reference) then return false end
|
|
1840
|
+
if current == nil then return true end
|
|
1841
|
+
|
|
1842
|
+
local dist = H.span_distance.prev
|
|
1843
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1844
|
+
end,
|
|
1845
|
+
|
|
1846
|
+
cover_or_nearest = function(candidate, current, reference)
|
|
1847
|
+
local res = H.is_better_covering_span(candidate, current, reference)
|
|
1848
|
+
if res ~= nil then return res end
|
|
1849
|
+
|
|
1850
|
+
-- If not covering, `candidate` must be closer to reference
|
|
1851
|
+
if current == nil then return true end
|
|
1852
|
+
|
|
1853
|
+
local dist = H.span_distance.near
|
|
1854
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1855
|
+
end,
|
|
1856
|
+
|
|
1857
|
+
next = function(candidate, current, reference)
|
|
1858
|
+
if H.is_span_covering(candidate, reference) then return false end
|
|
1859
|
+
|
|
1860
|
+
-- `candidate` must be "next" and closer to reference
|
|
1861
|
+
if not H.is_span_on_left(reference, candidate) then return false end
|
|
1862
|
+
if current == nil then return true end
|
|
1863
|
+
|
|
1864
|
+
local dist = H.span_distance.next
|
|
1865
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1866
|
+
end,
|
|
1867
|
+
|
|
1868
|
+
prev = function(candidate, current, reference)
|
|
1869
|
+
if H.is_span_covering(candidate, reference) then return false end
|
|
1870
|
+
|
|
1871
|
+
-- `candidate` must be "previous" and closer to reference
|
|
1872
|
+
if not H.is_span_on_left(candidate, reference) then return false end
|
|
1873
|
+
if current == nil then return true end
|
|
1874
|
+
|
|
1875
|
+
local dist = H.span_distance.prev
|
|
1876
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1877
|
+
end,
|
|
1878
|
+
|
|
1879
|
+
nearest = function(candidate, current, reference)
|
|
1880
|
+
if H.is_span_covering(candidate, reference) then return false end
|
|
1881
|
+
|
|
1882
|
+
-- `candidate` must be closer to reference
|
|
1883
|
+
if current == nil then return true end
|
|
1884
|
+
|
|
1885
|
+
local dist = H.span_distance.near
|
|
1886
|
+
return dist(candidate, reference) < dist(current, reference)
|
|
1887
|
+
end,
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
H.span_distance = {
|
|
1891
|
+
-- Other possible choices of distance between [a1, a2] and [b1, b2]:
|
|
1892
|
+
-- - Hausdorff distance: max(|a1 - b1|, |a2 - b2|).
|
|
1893
|
+
-- Source:
|
|
1894
|
+
-- https://math.stackexchange.com/questions/41269/distance-between-two-ranges
|
|
1895
|
+
-- - Minimum distance: min(|a1 - b1|, |a2 - b2|).
|
|
1896
|
+
|
|
1897
|
+
-- Distance is chosen so that "next span" in certain direction is the closest
|
|
1898
|
+
next = function(span_1, span_2) return math.abs(span_1.from - span_2.from) end,
|
|
1899
|
+
prev = function(span_1, span_2) return math.abs(span_1.to - span_2.to) end,
|
|
1900
|
+
near = function(span_1, span_2) return math.min(math.abs(span_1.from - span_2.from), math.abs(span_1.to - span_2.to)) end,
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
H.is_better_covering_span = function(candidate, current, reference)
|
|
1904
|
+
local candidate_is_covering = H.is_span_covering(candidate, reference)
|
|
1905
|
+
local current_is_covering = H.is_span_covering(current, reference)
|
|
1906
|
+
|
|
1907
|
+
if candidate_is_covering and current_is_covering then
|
|
1908
|
+
-- Covering candidate is better than covering current if it is narrower
|
|
1909
|
+
return (candidate.to - candidate.from) < (current.to - current.from)
|
|
1910
|
+
end
|
|
1911
|
+
if candidate_is_covering and not current_is_covering then return true end
|
|
1912
|
+
if not candidate_is_covering and current_is_covering then return false end
|
|
1913
|
+
|
|
1914
|
+
-- Return `nil` if neither span is covering
|
|
1915
|
+
return nil
|
|
1916
|
+
end
|
|
1917
|
+
|
|
1918
|
+
--stylua: ignore
|
|
1919
|
+
H.is_span_covering = function(span, span_to_cover)
|
|
1920
|
+
if span == nil or span_to_cover == nil then return false end
|
|
1921
|
+
if span.from == span.to then
|
|
1922
|
+
return (span.from == span_to_cover.from) and (span_to_cover.to == span.to)
|
|
1923
|
+
end
|
|
1924
|
+
if span_to_cover.from == span_to_cover.to then
|
|
1925
|
+
return (span.from <= span_to_cover.from) and (span_to_cover.to < span.to)
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1928
|
+
return (span.from <= span_to_cover.from) and (span_to_cover.to <= span.to)
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1931
|
+
H.is_span_equal = function(span_1, span_2)
|
|
1932
|
+
if span_1 == nil or span_2 == nil then return false end
|
|
1933
|
+
return (span_1.from == span_2.from) and (span_1.to == span_2.to)
|
|
1934
|
+
end
|
|
1935
|
+
|
|
1936
|
+
H.is_span_on_left = function(span_1, span_2)
|
|
1937
|
+
if span_1 == nil or span_2 == nil then return false end
|
|
1938
|
+
return (span_1.from <= span_2.from) and (span_1.to <= span_2.to)
|
|
1939
|
+
end
|
|
1940
|
+
|
|
1941
|
+
H.is_span_inside_spans = function(ref_span, spans)
|
|
1942
|
+
for _, span in ipairs(spans) do
|
|
1943
|
+
if span[1] <= ref_span[1] and ref_span[2] <= span[2] then return true end
|
|
1944
|
+
end
|
|
1945
|
+
return false
|
|
1946
|
+
end
|
|
1947
|
+
|
|
1948
|
+
-- Work with Lua patterns -----------------------------------------------------
|
|
1949
|
+
H.extract_span = function(s, extract_pattern, ai_type)
|
|
1950
|
+
local positions = { s:match(extract_pattern) }
|
|
1951
|
+
|
|
1952
|
+
if #positions == 1 and type(positions[1]) == 'string' then
|
|
1953
|
+
if s:len() == 0 then return H.new_span(0, 0) end
|
|
1954
|
+
return H.new_span(1, s:len())
|
|
1955
|
+
end
|
|
1956
|
+
|
|
1957
|
+
local is_all_numbers = true
|
|
1958
|
+
for _, pos in ipairs(positions) do
|
|
1959
|
+
if type(pos) ~= 'number' then is_all_numbers = false end
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
local is_valid_positions = is_all_numbers and (#positions == 2 or #positions == 4)
|
|
1963
|
+
if not is_valid_positions then
|
|
1964
|
+
local msg = 'Could not extract proper positions (two or four empty captures) from '
|
|
1965
|
+
.. string.format([[string '%s' with extraction pattern '%s'.]], s, extract_pattern)
|
|
1966
|
+
H.error(msg)
|
|
1967
|
+
end
|
|
1968
|
+
|
|
1969
|
+
local ai_spans
|
|
1970
|
+
if #positions == 2 then
|
|
1971
|
+
ai_spans = { a = H.new_span(1, s:len()), i = H.new_span(positions[1], positions[2] - 1) }
|
|
1972
|
+
else
|
|
1973
|
+
ai_spans = { a = H.new_span(positions[1], positions[4] - 1), i = H.new_span(positions[2], positions[3] - 1) }
|
|
1974
|
+
end
|
|
1975
|
+
|
|
1976
|
+
return ai_spans[ai_type]
|
|
1977
|
+
end
|
|
1978
|
+
|
|
1979
|
+
-- Work with cursor neighborhood ----------------------------------------------
|
|
1980
|
+
---@param reference_region table Reference region.
|
|
1981
|
+
---@param n_neighbors number Maximum number of neighbors to include before
|
|
1982
|
+
--- start line and after end line.
|
|
1983
|
+
---@private
|
|
1984
|
+
H.get_neighborhood = function(reference_region, n_neighbors)
|
|
1985
|
+
-- Compute '2d neighborhood' of (possibly empty) region
|
|
1986
|
+
local from_line, to_line = reference_region.from.line, (reference_region.to or reference_region.from).line
|
|
1987
|
+
local line_start = math.max(1, from_line - n_neighbors)
|
|
1988
|
+
local line_end = math.min(vim.api.nvim_buf_line_count(0), to_line + n_neighbors)
|
|
1989
|
+
local neigh2d = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false)
|
|
1990
|
+
-- Append 'newline' character to distinguish between lines in 1d case
|
|
1991
|
+
for k, v in pairs(neigh2d) do
|
|
1992
|
+
neigh2d[k] = v .. '\n'
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1995
|
+
-- '1d neighborhood': position is determined by offset from start
|
|
1996
|
+
local neigh1d = table.concat(neigh2d, '')
|
|
1997
|
+
|
|
1998
|
+
-- Convert 2d buffer position to 1d offset
|
|
1999
|
+
local pos_to_offset = function(pos)
|
|
2000
|
+
if pos == nil then return nil end
|
|
2001
|
+
local line_num = line_start
|
|
2002
|
+
local offset = 0
|
|
2003
|
+
while line_num < pos.line do
|
|
2004
|
+
offset = offset + neigh2d[line_num - line_start + 1]:len()
|
|
2005
|
+
line_num = line_num + 1
|
|
2006
|
+
end
|
|
2007
|
+
|
|
2008
|
+
return offset + pos.col
|
|
2009
|
+
end
|
|
2010
|
+
|
|
2011
|
+
-- Convert 1d offset to 2d buffer position
|
|
2012
|
+
local offset_to_pos = function(offset)
|
|
2013
|
+
if offset == nil then return nil end
|
|
2014
|
+
local line_num = 1
|
|
2015
|
+
local line_offset = 0
|
|
2016
|
+
while line_num <= #neigh2d and line_offset + neigh2d[line_num]:len() < offset do
|
|
2017
|
+
line_offset = line_offset + neigh2d[line_num]:len()
|
|
2018
|
+
line_num = line_num + 1
|
|
2019
|
+
end
|
|
2020
|
+
|
|
2021
|
+
return { line = line_start + line_num - 1, col = offset - line_offset }
|
|
2022
|
+
end
|
|
2023
|
+
|
|
2024
|
+
-- Convert 2d region to 1d span
|
|
2025
|
+
local region_to_span = function(region)
|
|
2026
|
+
if region == nil then return nil end
|
|
2027
|
+
local is_empty = region.to == nil
|
|
2028
|
+
local to = region.to or region.from
|
|
2029
|
+
return { from = pos_to_offset(region.from), to = pos_to_offset(to) + (is_empty and 0 or 1) }
|
|
2030
|
+
end
|
|
2031
|
+
|
|
2032
|
+
-- Convert 1d span to 2d region
|
|
2033
|
+
local span_to_region = function(span, vis_mode)
|
|
2034
|
+
if span == nil then return nil end
|
|
2035
|
+
-- NOTE: this might lead to outside of line positions due to added `\n` at
|
|
2036
|
+
-- the end of lines in 1d-neighborhood. However, this is crucial for
|
|
2037
|
+
-- allowing `i` textobjects to collapse multiline selections.
|
|
2038
|
+
local res = { from = offset_to_pos(span.from), vis_mode = vis_mode }
|
|
2039
|
+
|
|
2040
|
+
-- Convert empty span to empty region
|
|
2041
|
+
if span.from < span.to then res.to = offset_to_pos(span.to - 1) end
|
|
2042
|
+
return res
|
|
2043
|
+
end
|
|
2044
|
+
|
|
2045
|
+
local is_region_inside = function(region)
|
|
2046
|
+
local res = line_start <= region.from.line
|
|
2047
|
+
if region.to ~= nil then res = res and (region.to.line <= line_end) end
|
|
2048
|
+
return res
|
|
2049
|
+
end
|
|
2050
|
+
|
|
2051
|
+
return {
|
|
2052
|
+
n_neighbors = n_neighbors,
|
|
2053
|
+
region = reference_region,
|
|
2054
|
+
['1d'] = neigh1d,
|
|
2055
|
+
['2d'] = neigh2d,
|
|
2056
|
+
pos_to_offset = pos_to_offset,
|
|
2057
|
+
offset_to_pos = offset_to_pos,
|
|
2058
|
+
region_to_span = region_to_span,
|
|
2059
|
+
span_to_region = span_to_region,
|
|
2060
|
+
is_region_inside = is_region_inside,
|
|
2061
|
+
}
|
|
2062
|
+
end
|
|
2063
|
+
|
|
2064
|
+
-- Work with user input -------------------------------------------------------
|
|
2065
|
+
H.user_textobject_id = function(ai_type)
|
|
2066
|
+
-- Get from user single character textobject identifier
|
|
2067
|
+
local needs_reminder = true
|
|
2068
|
+
vim.defer_fn(function()
|
|
2069
|
+
if not needs_reminder then return end
|
|
2070
|
+
|
|
2071
|
+
local msg = string.format('Reminder to press `%s` textobject id ', ai_type)
|
|
2072
|
+
H.echo(msg)
|
|
2073
|
+
H.cache.msg_shown = true
|
|
2074
|
+
end, 1000)
|
|
2075
|
+
local ok, char = pcall(vim.fn.getcharstr)
|
|
2076
|
+
needs_reminder = false
|
|
2077
|
+
H.unecho()
|
|
2078
|
+
|
|
2079
|
+
-- Terminate if couldn't get input (like with <C-c>) or on `<Esc>`
|
|
2080
|
+
if not ok or char == '' or char == '\3' or char == '\27' then return nil end
|
|
2081
|
+
return char
|
|
2082
|
+
end
|
|
2083
|
+
|
|
2084
|
+
H.user_input = function(prompt, text)
|
|
2085
|
+
prompt = '(mini.ai) ' .. prompt
|
|
2086
|
+
if _G.MiniInput ~= nil then return MiniInput.get({ prompt = prompt, scope = 'cursor', init_keys = { text } }) end
|
|
2087
|
+
|
|
2088
|
+
-- Use `on_key` to distinguish cancel with `<Esc>` and immediate `<CR>`
|
|
2089
|
+
local was_cancelled = false
|
|
2090
|
+
vim.on_key(function(key) was_cancelled = was_cancelled or key == '\27' end, H.ns_id.input)
|
|
2091
|
+
|
|
2092
|
+
-- Ask for input. Use `pcall` to allow `<C-c>` to cancel user input
|
|
2093
|
+
vim.cmd('echohl Question')
|
|
2094
|
+
local ok, res = pcall(vim.fn.input, { prompt = prompt .. ': ', default = text or '' })
|
|
2095
|
+
vim.cmd('echohl None | echo "" | redraw')
|
|
2096
|
+
|
|
2097
|
+
vim.on_key(nil, H.ns_id.input)
|
|
2098
|
+
return (ok and not was_cancelled) and res or nil
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
-- Work with Visual mode ------------------------------------------------------
|
|
2102
|
+
H.is_visual_mode = function(mode)
|
|
2103
|
+
mode = mode or vim.fn.mode()
|
|
2104
|
+
-- '\22' is an escaped `<C-v>`
|
|
2105
|
+
return mode == 'v' or mode == 'V' or mode == '\22', mode
|
|
2106
|
+
end
|
|
2107
|
+
|
|
2108
|
+
H.exit_to_normal_mode = function()
|
|
2109
|
+
-- Don't use `<C-\><C-n>` in command-line window as they close it
|
|
2110
|
+
if vim.fn.getcmdwintype() ~= '' then
|
|
2111
|
+
local is_vis, cur_mode = H.is_visual_mode()
|
|
2112
|
+
if is_vis then vim.cmd('normal! ' .. cur_mode) end
|
|
2113
|
+
else
|
|
2114
|
+
-- '\28\14' is an escaped version of `<C-\><C-n>`
|
|
2115
|
+
vim.cmd('normal! \28\14')
|
|
2116
|
+
end
|
|
2117
|
+
end
|
|
2118
|
+
|
|
2119
|
+
H.get_visual_region = function()
|
|
2120
|
+
local is_vis, _ = H.is_visual_mode()
|
|
2121
|
+
if not is_vis then return end
|
|
2122
|
+
local res = {
|
|
2123
|
+
from = { line = vim.fn.line('v'), col = vim.fn.col('v') },
|
|
2124
|
+
to = { line = vim.fn.line('.'), col = vim.fn.col('.') },
|
|
2125
|
+
}
|
|
2126
|
+
if res.from.line > res.to.line or (res.from.line == res.to.line and res.from.col > res.to.col) then
|
|
2127
|
+
res = { from = res.to, to = res.from }
|
|
2128
|
+
end
|
|
2129
|
+
return res
|
|
2130
|
+
end
|
|
2131
|
+
|
|
2132
|
+
-- Utilities ------------------------------------------------------------------
|
|
2133
|
+
H.error = function(msg) error('(mini.ai) ' .. msg, 0) end
|
|
2134
|
+
|
|
2135
|
+
H.check_type = function(name, val, ref, allow_nil)
|
|
2136
|
+
if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
|
|
2137
|
+
H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
|
|
2138
|
+
end
|
|
2139
|
+
|
|
2140
|
+
H.echo = function(msg, is_important)
|
|
2141
|
+
if H.get_config().silent then return end
|
|
2142
|
+
|
|
2143
|
+
-- Construct message chunks
|
|
2144
|
+
msg = type(msg) == 'string' and { { msg } } or msg
|
|
2145
|
+
table.insert(msg, 1, { '(mini.ai) ', 'WarningMsg' })
|
|
2146
|
+
|
|
2147
|
+
-- Avoid hit-enter-prompt
|
|
2148
|
+
local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace
|
|
2149
|
+
local chunks, tot_width = {}, 0
|
|
2150
|
+
for _, ch in ipairs(msg) do
|
|
2151
|
+
local new_ch = { vim.fn.strcharpart(ch[1], 0, max_width - tot_width), ch[2] }
|
|
2152
|
+
table.insert(chunks, new_ch)
|
|
2153
|
+
tot_width = tot_width + vim.fn.strdisplaywidth(new_ch[1])
|
|
2154
|
+
if tot_width >= max_width then break end
|
|
2155
|
+
end
|
|
2156
|
+
|
|
2157
|
+
-- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
|
|
2158
|
+
vim.cmd([[echo '' | redraw]])
|
|
2159
|
+
vim.api.nvim_echo(chunks, is_important, {})
|
|
2160
|
+
end
|
|
2161
|
+
|
|
2162
|
+
H.unecho = function()
|
|
2163
|
+
if H.cache.msg_shown then vim.cmd([[echo '' | redraw]]) end
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
H.message = function(msg) H.echo(msg, true) end
|
|
2167
|
+
|
|
2168
|
+
H.map = function(mode, lhs, rhs, opts)
|
|
2169
|
+
if lhs == '' then return end
|
|
2170
|
+
opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
|
|
2171
|
+
vim.keymap.set(mode, lhs, rhs, opts)
|
|
2172
|
+
end
|
|
2173
|
+
|
|
2174
|
+
H.string_find = function(s, pattern, init)
|
|
2175
|
+
init = init or 1
|
|
2176
|
+
|
|
2177
|
+
-- Match only start of full string if pattern says so.
|
|
2178
|
+
-- This is needed because `string.find()` doesn't do this.
|
|
2179
|
+
-- Example: `string.find('(aaa)', '^.*$', 4)` returns `4, 5`
|
|
2180
|
+
if pattern:sub(1, 1) == '^' then
|
|
2181
|
+
if init > 1 then return nil end
|
|
2182
|
+
return string.find(s, pattern)
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
-- Handle patterns `x.-y` differently: make match as small as possible. This
|
|
2186
|
+
-- doesn't allow `x` be present inside `.-` match, just as with `yyy`. Which
|
|
2187
|
+
-- also leads to a behavior similar to punctuation id (like with `va_`): no
|
|
2188
|
+
-- covering is possible, only next, previous, or nearest.
|
|
2189
|
+
local check_left, _, prev = string.find(pattern, '(.)%.%-')
|
|
2190
|
+
local is_pattern_special = check_left ~= nil and prev ~= '%'
|
|
2191
|
+
if not is_pattern_special then return string.find(s, pattern, init) end
|
|
2192
|
+
|
|
2193
|
+
-- Make match as small as possible
|
|
2194
|
+
local from, to = string.find(s, pattern, init)
|
|
2195
|
+
if from == nil then return end
|
|
2196
|
+
|
|
2197
|
+
local cur_from, cur_to = from, to
|
|
2198
|
+
while cur_to == to do
|
|
2199
|
+
from, to = cur_from, cur_to
|
|
2200
|
+
cur_from, cur_to = string.find(s, pattern, cur_from + 1)
|
|
2201
|
+
end
|
|
2202
|
+
|
|
2203
|
+
return from, to
|
|
2204
|
+
end
|
|
2205
|
+
|
|
2206
|
+
---@param arr table List of items. If item is list, consider as set for
|
|
2207
|
+
--- product. Else - make it single item list.
|
|
2208
|
+
---@private
|
|
2209
|
+
H.cartesian_product = function(arr)
|
|
2210
|
+
if not (type(arr) == 'table' and #arr > 0) then return {} end
|
|
2211
|
+
arr = vim.tbl_map(function(x) return H.islist(x) and x or { x } end, arr)
|
|
2212
|
+
|
|
2213
|
+
local res, cur_item = {}, {}
|
|
2214
|
+
local process
|
|
2215
|
+
process = function(level)
|
|
2216
|
+
for i = 1, #arr[level] do
|
|
2217
|
+
table.insert(cur_item, arr[level][i])
|
|
2218
|
+
if level == #arr then
|
|
2219
|
+
-- Flatten array to allow tables as elements of step tables
|
|
2220
|
+
table.insert(res, H.tbl_flatten(cur_item))
|
|
2221
|
+
else
|
|
2222
|
+
process(level + 1)
|
|
2223
|
+
end
|
|
2224
|
+
table.remove(cur_item, #cur_item)
|
|
2225
|
+
end
|
|
2226
|
+
end
|
|
2227
|
+
|
|
2228
|
+
process(1)
|
|
2229
|
+
return res
|
|
2230
|
+
end
|
|
2231
|
+
|
|
2232
|
+
H.wrap_callable_table = function(x)
|
|
2233
|
+
if vim.is_callable(x) and type(x) == 'table' then
|
|
2234
|
+
return function(...) return x(...) end
|
|
2235
|
+
end
|
|
2236
|
+
return x
|
|
2237
|
+
end
|
|
2238
|
+
|
|
2239
|
+
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
|
|
2240
|
+
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
|
|
2241
|
+
H.tbl_flatten = vim.fn.has('nvim-0.10') == 1 and function(x) return vim.iter(x):flatten(math.huge):totable() end
|
|
2242
|
+
or vim.tbl_flatten
|
|
2243
|
+
|
|
2244
|
+
return MiniAi
|