├── .github
└── workflows
│ └── stale_issue.yml
├── .gitignore
├── LICENSE
├── README.md
├── autoload
└── vimcomplete
│ ├── abbrev.vim
│ ├── buffer.vim
│ ├── completor.vim
│ ├── dictionary.vim
│ ├── lsp.vim
│ ├── omnifunc.vim
│ ├── options.vim
│ ├── path.vim
│ ├── recent.vim
│ ├── tag.vim
│ ├── tmux.vim
│ ├── util.vim
│ ├── vimscript.vim
│ └── vsnip.vim
├── data
├── demo.tape
├── vim9.dict
└── vimrc
├── doc
└── vimcomplete.txt
├── plugin
├── addons.vim
└── vimcomplete.vim
└── tools
└── vimdictgen.vim
/.github/workflows/stale_issue.yml:
--------------------------------------------------------------------------------
1 | name: Close inactive issues
2 | on:
3 | schedule:
4 | - cron: "30 1 * * *"
5 |
6 | jobs:
7 | close-issues:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 | steps:
13 | - uses: actions/stale@v5
14 | with:
15 | days-before-issue-stale: 30
16 | days-before-issue-close: 14
17 | stale-issue-label: "stale"
18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
20 | days-before-pr-stale: -1
21 | days-before-pr-close: -1
22 | repo-token: ${{ secrets.GITHUB_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | doc/tags
3 | *.swp
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 giri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
VimComplete
3 |
4 | A lightweight autocompletion plugin for Vim, written in vim9script.
5 |
6 |
7 | Key Features •
8 | Requirements •
9 | Installation •
10 | Configuration •
11 | Commands •
12 | Add-on
13 |
14 |
15 | 
16 |
17 |
18 | # Key Features
19 |
20 | Words are sourced ***asynchronously*** from various sources:
21 |
22 | - **[Buffers](#buffer-Completion)**
23 | - **[Dictionary](#dictionary-Completion)** files
24 | - **[Code](#lsp-Completion)** completion sourced from [LSP](https://github.com/yegappan/lsp) client
25 | - **[Snippets](#snippets-Completion)** from [vim-vsnip](https://github.com/hrsh7th/vim-vsnip) client
26 | - **[Words and bigrams](#ngrams-Completion)** from [Ngrams](https://norvig.com/ngrams/) database
27 | - Vim's **[omnifunc](#omnifunc-Completion)**
28 | - **[Path](#path-Completion)** search
29 | - Vim's **[abbreviations](#abbreviations-Completion)**
30 | - **[Vim9script](#vim9script-language-Completion)** language (similar to LSP)
31 | - **[Tmux](#tmux-Completion)** panes
32 | - **[Tag](#tag-Completion)** names
33 |
34 | All crucial source modules are integrated, eliminating the need to manage
35 | multiple plugins. Users have the flexibility to enable or disable each
36 | completion source and customize settings on a per-file-type basis (`:h filetype`).
37 |
38 |
39 | Completion items are _sorted_ according to the following criteria:
40 |
41 | - Recency (using a LRU cache)
42 | - Length of item
43 | - Priority
44 | - Proximity of item (for buffer completion)
45 | - Case match
46 |
47 | > [!NOTE]
48 | > For cmdline-mode completion (`/`, `?`, and `:` commands), refer to **[VimSuggest](https://github.com/girishji/vimsuggest)** plugin.
49 |
50 |
51 | # Requirements
52 |
53 | - Vim version 9.1 or higher
54 |
55 | # Installation
56 |
57 | Install it via [vim-plug](https://github.com/junegunn/vim-plug).
58 |
59 | Show instructions
60 |
61 |
62 | Using vim9 script:
63 |
64 | ```vim
65 | vim9script
66 | plug#begin()
67 | Plug 'girishji/vimcomplete'
68 | plug#end()
69 | ```
70 |
71 | Using legacy script:
72 |
73 | ```vim
74 | call plug#begin()
75 | Plug 'girishji/vimcomplete'
76 | call plug#end()
77 | ```
78 |
79 |
80 |
81 | Install using Vim's built-in package manager.
82 |
83 | Show instructions
84 |
85 |
86 | ```bash
87 | $ mkdir -p $HOME/.vim/pack/downloads/opt
88 | $ cd $HOME/.vim/pack/downloads/opt
89 | $ git clone https://github.com/girishji/vimcomplete.git
90 | ```
91 |
92 | Add the following line to your $HOME/.vimrc file.
93 |
94 | ```vim
95 | packadd vimcomplete
96 | ```
97 |
98 |
99 |
100 | # Configuration
101 |
102 | The completion sources mentioned above, aside from [buffer](#Buffer-Completion), [path](#path-completion), and [lsp](#lsp-completion) completion, are not enabled by default. This section provides instructions on configuring both the completion sources and the completion engine itself.
103 |
104 | ## Completion Engine
105 |
106 | This entity retrieves completion items from the enabled completion sources and then displays the popup menu.
107 |
108 | Option|Type|Description
109 | ------|----|-----------
110 | `alwaysOn`|`Boolean`| If set to `true`, the completion menu is automatically triggered by any change in the buffer. If set to `false`, use `` (control-space) to manually trigger auto-completion. If you choose to map some other key instead, map your favorite key to `(vimcomplete-do-complete)`. If set to `false`, `completeopt` Vim option determins popup behavior. Default: true.
111 | `completionKinds`|`Dictionary`|Custom text to use when `customCompletionKinds` is set (explained below). Default: `{}`.
112 | `customCompletionKinds`|`Boolean`|Set this option to customize the 'kind' attribute (explained below). Default: `false`.
113 | `kindDisplayType`|`String`|The 'kind' field of completion item can be displayed in a number of ways: as a single letter symbol (`symbol`), a single letter with descriptive text (`symboltext`), only text (`text`), an icon (`icon`), or icon with text (`icontext`). For showing VSCode like icons you need [a patched font](https://www.nerdfonts.com/). Default: `symbol`.
114 | `matchCase`|`Boolean`|Prioritize the items that match the case of the prefix being completed. Default: `true`.
115 | `noNewlineInCompletion` | `Boolean` | When `true`, pressing `` (``) in insert mode will insert a newline only if an item in the popup menu is selected. If an item is not selected, the popup is dismissed without inserting a newline. Default: `false`.
116 | `noNewlineInCompletionEver` | `Boolean` | When `true`, pressing `` (``) will never insert a newline, regardless of whether an item in the popup menu is selected. This option overrides `noNewlineInCompletion`. If both options are `false`, `` behaves as per the default Vim behavior, inserting a newline whether an item is selected or not. Default: `false`.
117 | `postfixClobber` | `Boolean` | When completing 'foo\bar' and the candidate is 'foosome', enabling this option (`true`) will complete 'foosome' instead of 'foosomebar'. Default: `false`.
118 | `postfixHighlight` | `Boolean` | This option functions similarly to `postfixClobber`, but instead of deleting adjoining text to the right of the completed text, it highlights it using the 'VimCompletePostfix' highlight group. Use `` to retain the adjoining text and `` to delete. Default: `false`.
119 | `recency`|`Boolean`|Display recently chosen items from the LRU cache. Items are shown at the top of the list. Default: `true`.
120 | `recentItemCount`|`Number`|Count of recent items to show from LRU cache. Default: `5`.
121 | `showKind`|`Boolean`|Show the type ('kind') of completion item returned by LSP server. Default: `true`.
122 | `showCmpSource`|`Boolean`|Show the source of the completion item in the menu. Default: `true`.
123 | `cmpSourceWidth`|`Number`|Number of characters displayed for completion source. Default: `4`.
124 | `shuffleEqualPriority`|`Boolean`|Arrange items from sources with equal priority such that the first item of all sources appear before the second item of any source. Default: `false`.
125 | `sortByLength`|`Boolean`|Sort completion items by length. Default: `false`.
126 | `triggerWordLen`|`Number`|Minimum number of characters needed to trigger completion menu. Not applicable to completion triggered by LSP trigger characters (this exemption applies only to Vim version 9.1.650 or higher). Default: `1`.
127 | `infoPopup`|`Boolean`|Show an info popup (`:h completepopup`) for extra information. If you prefer the preview window, set this option to `false` and `set completeopt+=preview`. If you prefer to not have any info window, set this option to `false` and `set completeopt-=preview`. Default: `true`.
128 | `setCompleteOpt` | `Boolean` | If `false`, `completeopt` is not set automatically. To have the first item selected when `alwaysOn` is enabled, you can manually add `set completeopt=menuone,noinsert` to your `vimrc` and type `` to insert the selected item. If `true`, the first item is not automatically selected. Default: `true`.
129 |
130 | ## Buffer Completion
131 |
132 | The current buffer, as well as other open buffers, are searched for completion candidates using an asynchronous mechanism with a timeout. This approach ensures that the completion engine is not slowed down by large buffers.
133 |
134 | Option|Type|Description
135 | ------|----|-----------
136 | `completionMatcher`| `String` | Enable fuzzy or case insensitive completion. Accepts one of the following values: `case` for case sensitive matching, `icase` for ignoring case while matching, and `fuzzy` for fuzzy match. Default: `icase`.
137 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`.
138 | `enable`|`Boolean`|Set this to `false` to disable buffer completion. Default: `true`.
139 | `envComplete` | `Boolean` | Complete environment variables after typing the `$` character. Default: `false`.
140 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']` (all file types).
141 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
142 | `otherBuffersCount`| `Number` | Maximum number of other listed buffers to search. Set it to `0` to only search current buffer. Default: `3`.
143 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`.
144 | `timeout` | `Number` | Maximum time allocated for searching completion candidates in the current buffer. Default: `100` milliseconds. If searching in multiple buffers, an additional 100 milliseconds is allocated. The search is aborted if any key is pressed.
145 | `urlComplete` | `Boolean` | Enable completion of http links in entirety. This is useful when typing the same URL multiple times. Default: `false`.
146 |
147 | ## Dictionary Completion
148 |
149 | The dictionary provider is capable of searching an arbitrary list of words placed one per line in a text file. These words can encompass any non-space characters, and the file doesn't necessarily need to be sorted. This feature presents various opportunities. For instance, you can create a dictionary akin to [Pydiction](https://github.com/vim-scripts/Pydiction), enabling the completion of keywords, functions, and method names for any programming language. Moreover, it can efficiently search a sorted dictionary using binary search.
150 |
151 | Unsorted dictionaries are searched in linear time `O(n)`, but they tend to perform acceptably well for file sizes below 3MB (performance might vary depending on your system). Only one unsorted dictionary is used for completion, while any number of sorted dictionaries can be used simultaneously.
152 |
153 | Dictionary can also include comments. Any line starting with `---` is considered as a comment and ignored.
154 |
155 | Additional information can be included for each item which can be displayed in a preview or popup window. Any line starting with 4 spaces is displayed in the 'info' popup (or preview) window as part of the item above. Here is an example of dictionary file:
156 |
157 | ```
158 | --- This is a comment
159 | item_1
160 | Additional information for 'info' window related to item_1. Can be multi-line.
161 | item_2
162 | Additional information for 'info' window related to item_2.
163 | ```
164 |
165 | Option|Type|Description
166 | ------|----|-----------
167 | `commentStr` | `String` | Any lines beginning with this string is ignored. Default: `---`.
168 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `false`.
169 | `enable`|`Boolean`|Set this to `true` to enable dictionary completion. Default: `false`.
170 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['text', 'markdown']`.
171 | `matcher`| `String` | This option is active only when `onlyWords` is `true` (text files). It makes sense only when `sortedDict` is set to `false` since binary search is done case sensitive (assuming that sorting of the dictionary file is done case sensitive). Accepted values are `case` (case sensitive) and `ignorecase`. Default: `case`.
172 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
173 | `onlyWords`| `Boolean` | Set this to `true` if both the prefix you are trying to complete and the dictionary contain alphanumeric characters only (text files). For programming language dictionaries it should be set to `false`, since they can contain characters like `@`, `.`, `(`, etc. Default: `false`.
174 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`.
175 | `sortedDict` | `Boolean` | `true` if the dictionary file is sorted, `false` otherwise. This option affects both performance and correctness. Take care to set it correctly. Searching is case sensitive. Default: `true`.
176 | `triggerWordLen`|`Number`|Minimum number of characters needed to trigger completion menu. Default: `1`.
177 |
178 | ### Sample Configuration
179 |
180 | Further information about setting up configurations will be available later. Nonetheless, here is a sample configuration specifically targeting the dictionary source.
181 | Dictionary files can be configured individually for each 'filetype' (`:h filetype`).
182 |
183 | ```
184 | vim9script
185 | var dictproperties = {
186 | python: { sortedDict: false },
187 | text: { sortedDict: true }
188 | }
189 | var vcoptions = {
190 | dictionary: { enable: true, priority: 11, filetypes: ['python', 'text'], properties: dictproperties },
191 | }
192 | autocmd VimEnter * g:VimCompleteOptionsSet(vcoptions)
193 | autocmd FileType text set dictionary+=/usr/share/dict/words
194 | ```
195 |
196 | By adding the following snippet to your `vimrc`, you can add custom completions for each filetype, say `python`, by adding a text file `dicts/python` into the `vimrc` folder:
197 |
198 | ```vim
199 | vim9script
200 | g:vimfiles_dir = &runtimepath->split(',')[0]
201 | autocmd FileType * {
202 | var dict = $'{g:vimfiles_dir}/dicts/{expand("")}'->resolve()->fnamemodify(':p')
203 | if dict->filereadable()
204 | exe $'setlocal dictionary={dict}'
205 | endif
206 | }
207 | ```
208 |
209 | > [!TIP]
210 | > For completing English words, you can utilize [ngram](https://en.wikipedia.org/wiki/N-gram) completion as outlined below, or opt for a custom dictionary containing frequently used words. Unfortunately, the default dictionary that comes pre-installed with Linux or MacOS contains numerous infrequently used words that spam the menu.
211 |
212 | > [!NOTE]
213 | > For legacy script, the syntax for 'autocmd' is:
214 | > ```
215 | > autocmd VimEnter * call g:VimCompleteOptionsSet(vcoptions)
216 | > ```
217 |
218 | ## LSP Completion
219 |
220 | This source obtains autocompletion items from the
221 | [LSP client](https://github.com/yegappan/lsp).
222 |
223 | > [!IMPORTANT]
224 | > Please install the [LSP client](https://github.com/yegappan/lsp) separately.
225 |
226 | Option|Type|Description
227 | ------|----|-----------
228 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`.
229 | `enable`|`Boolean`|Set this to `false` to disable LSP completion. Default: `true`.
230 | `filetypes`|`List`|This option need not be specified. If this option is not specified or is empty, completion items are sourced for any file type for which LSP is configured. Otherwise, items are sourced only for listed file types. Default: Not specified.
231 | `keywordOnly`|`Boolean`|If `true` completion will be triggered after any keyword character as defined by the file type (`:h 'iskeyword'`). `false` will trigger completion after non-keywords like `.` (for instance). Default: `false`.
232 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
233 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`.
234 |
235 | > [!NOTE]
236 | > For fuzzy and case insensitive completion, set the `completionMatcher` option in the [LSP client](https://github.com/yegappan/lsp). See `:h lsp-opt-completionMatcher`.
237 |
238 | ## Snippets Completion
239 |
240 | This source provides snippet completion from [vim-vsnip](https://github.com/hrsh7th/vim-vsnip).
241 |
242 | > [!IMPORTANT]
243 | > Please install the following separately.
244 | > - [vim-vsnip](https://github.com/hrsh7th/vim-vsnip)
245 | > - [vim-vsnip-integ](https://github.com/hrsh7th/vim-vsnip-integ)
246 | >
247 | > Optional:
248 | > - [friendly-snippets](https://github.com/rafamadriz/friendly-snippets)
249 |
250 |
251 | Option|Type|Description
252 | ------|----|-----------
253 | `adaptNonKeyword`|`Boolean`|(experimental) When completing snippets starting with non-keywords, say '#i' for instance, adjust completion such that they are compatible with items starting with keywords like 'i' (returned by LSP, for instance). Default is `false`.
254 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`.
255 | `enable`|`Boolean`|Set this to `true` to enable this source. Default: `false`.
256 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']` (all file types).
257 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
258 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`.
259 |
260 | > [!NOTE]
261 | > The `` key facilitates movement within a snippet. When a snippet is active, the popup completion menu won't open. However, the popup window will activate upon reaching the final stop within the snippet. If you wish to navigate backward within the snippet using ``, you can dismiss the popup by using `CTRL-E`.
262 |
263 |
264 | ## Ngrams Completion
265 |
266 | This source is kept as a separate plugin since it includes large database
267 | files. Please see
268 | **[ngram-complete](https://github.com/girishji/ngram-complete.vim)** for
269 | installation and usage instructions.
270 |
271 | ## Omnifunc Completion
272 |
273 | This source completes items emitted by the function set in `omnifunc` (`:h 'omnifunc'`).
274 |
275 | Vim provides language based autocompletion through Omni completion for many
276 | languages (see `$VIMRUNTIME/autoload`). This is a lightweight alternative to using LSP.
277 |
278 | | __Vim File__ | __Language__ |
279 | |---|---|
280 | |ccomplete.vim|C|
281 | |csscomplete.vim|HTML / CSS|
282 | |htmlcomplete.vim|HTML|
283 | |javascriptcomplete.vim|Javascript|
284 | |phpcomplete.vim|PHP|
285 | |pythoncomplete.vim|Python|
286 | |rubycomplete.vim|Ruby|
287 | |syntaxcomplete.vim|from syntax highlighting|
288 | |xmlcomplete.vim|XML (uses files in the xml directory)|
289 |
290 | Vim sets the `omnifunc` option automatically when file type is detected.
291 |
292 | Also, any user defined `omnifunc` can also be used for autocompletion.
293 |
294 | > [!CAUTION]
295 | > Disable the LSP Completion when using omnifunc.
296 |
297 | Option|Type|Description
298 | ------|----|-----------
299 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`.
300 | `enable`|`Boolean`|Set this to `true` to enable omnifunc completion. Default: `false`.
301 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']` (all file types).
302 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
303 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`.
304 |
305 | ## Path Completion
306 |
307 | Both relative and absolute path names are completed.
308 |
309 | | Option | Type | Description |
310 | |---------------------|-----------|---------------|
311 | | `bufferRelativePath`| `Boolean` | Interpret relative paths relative to the directory of the current buffer. Otherwise paths are interpreted relative to the directory from which Vim is started. Default: `true`. |
312 | | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`. |
313 | | `enable`|`Boolean`|Set this to `false` to disable path completion. Default: `true`. |
314 | | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']` (all file types). |
315 | | `groupDirectoriesFirst`| `Boolean` | Group directories before files (like linux's 'ls --group-directories-first'). Default: `false`. |
316 | | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`. |
317 | | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `12`. |
318 | | `showPathSeparatorAtEnd`| `Boolean` | Show path separator (`/` in unix) at the end of directory entry. Default: `false`. |
319 |
320 | > [!NOTE]
321 | > Path completion activates when there is a `/` (`\` for Windows when Vim option `shellslash` is not set) or `.` in the word before the cursor. To autocomplete deeper in a directory type `/` at the end.
322 |
323 | ## Abbreviations Completion
324 |
325 | Abbreviations (`:h abbreviations`) are completed based on the `id`.
326 |
327 | | Option | Type | Description |
328 | |---------------------|-----------|---------------|
329 | | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`. |
330 | | `enable`|`Boolean`|Set this to `true` to enable abbreviation completion. Default: `false`. |
331 | | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']` (all file types). |
332 | | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`. |
333 | | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`. |
334 |
335 | ## Vim9script Language Completion
336 |
337 | This source completes Vim9 script function names, arguments, variables, reserved words and
338 | the like. Enable this if you are developing a Vim plugin or configuring a non-trivial _.vimrc_.
339 |
340 |
341 | | Option | Type | Description |
342 | |---------------------|-----------|---------------|
343 | | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `true`. |
344 | | `enable`|`Boolean`|Set this to `false` to disable this source. Default: `true`. |
345 | | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['vim']`. |
346 | | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`. |
347 | | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `10`. |
348 |
349 | ## Tmux Completion
350 |
351 | Words are sourced asynchronously from adjacent tmux panes, ensuring Vim won't hang even with a lot of output in the tmux windows.
352 |
353 | Option|Type|Description
354 | ------|----|-----------
355 | `completionMatcher`| `String` | Enable fuzzy or case insensitive completion. Accepts one of the following values: `case` for case sensitive matching, `icase` for ignoring case while matching, and `fuzzy` for fuzzy match. Default: `icase`.
356 | `dup`|`Boolean`|If true, include items from this source that are duplicates of items from other sources. Default: `false`.
357 | `enable`|`Boolean`|Set this to `true` to enable tmux completion. Default: `false`.
358 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']`.
359 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
360 | `name`|`String`|Name of the executable. You can specify the full path if the *tmux* executable is not found in $PATH. Default: `tmux`.
361 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `8`.
362 | `scrollCount`| `Number` | Number of lines above visible lines to search for words. Excludes visible lines if Vim is running in a pane. Default: 200.
363 |
364 | ## Tag Completion
365 |
366 | Tag names are autocompleted if tags file is available.
367 |
368 | Option|Type|Description
369 | ------|----|-----------
370 | `enable`|`Boolean`|Set this to `true` to enable tag name completion. Default: `false`.
371 | `filetypes`|`List`|List of file types for which this source is enabled. Default: `['*']`.
372 | `maxCount`|`Number`|Total number of completion candidates emitted by this source. Default: `10`.
373 | `priority`|`Number`|Priority of this source relative to others. Items from higher priority sources are displayed at the top. Default: `8`.
374 |
375 | ## Configure Options
376 |
377 | Options can be configured using the global function `g:VimCompleteOptionsSet()`. The example below illustrates how to enable and configure completion sources. Please note that not all options are demonstrated here; for a comprehensive list of all available options, refer to the tables provided above.
378 |
379 | ```vim
380 | vim9script
381 | var options = {
382 | completor: { shuffleEqualPriority: true, postfixHighlight: true },
383 | buffer: { enable: true, priority: 10, urlComplete: true, envComplete: true },
384 | abbrev: { enable: true, priority: 10 },
385 | lsp: { enable: true, priority: 10, maxCount: 5 },
386 | omnifunc: { enable: false, priority: 8, filetypes: ['python', 'javascript'] },
387 | vsnip: { enable: true, priority: 11 },
388 | vimscript: { enable: true, priority: 11 },
389 | ngram: {
390 | enable: true,
391 | priority: 10,
392 | bigram: false,
393 | filetypes: ['text', 'help', 'markdown'],
394 | filetypesComments: ['c', 'cpp', 'python', 'java'],
395 | },
396 | }
397 | autocmd VimEnter * g:VimCompleteOptionsSet(options)
398 | ```
399 |
400 | > [!NOTE]
401 | > For legacy script, the syntax for 'autocmd' is:
402 | > ```
403 | > autocmd VimEnter * call g:VimCompleteOptionsSet(options)
404 | > ```
405 |
406 | ## Tab Completion
407 |
408 | You can map the `` and `` keys to navigate autocomplete items in insert mode. By default, Vim uses `CTRL-N` and `CTRL-P` to cycle through completion menu options. `` and `` also jump between snippet placeholders where appropriate.
409 |
410 | To enable `` and `` for this purpose, add the following to your configuration:
411 |
412 | ```vim
413 | vim9script
414 | g:vimcomplete_tab_enable = 1
415 | ```
416 |
417 | > **Note**: Enabling this option will remove any existing mappings for `` and ``.
418 |
419 | If you'd like to retain `` and `` mappings from other plugins, unset the above variable and instead use these custom mappings, substituting `{rhs}` as needed (using `"\"` as `"{rhs}"` will behave the same as setting the variable):
420 |
421 | ```vim
422 | inoremap g:VimCompleteTab() ?? "{rhs}"
423 | inoremap g:VimCompleteSTab() ?? "{rhs}"
424 | ```
425 |
426 | This configuration allows `` and `` to integrate with other plugin mappings.
427 |
428 | Alternatively, you can use the keys `(vimcomplete-tab)` and `(vimcomplete-s-tab)` directly in your custom mappings.
429 |
430 | ## Enter Key Handling
431 |
432 | `` is mapped by default to insert the currently selected item and/or insert a literal ``, depending on configuration (see `noNewlineInCompletion` and `noNewlineInCompletionEver` options).
433 |
434 | In case of conflicts with other plugins, this mapping can be disabled entirely:
435 |
436 | ```vim
437 | vim9script
438 | g:vimcomplete_cr_enable = 0
439 | ```
440 |
441 | In this case, the user must define an appropriate `` mapping to resolve conflicts between plugins. When creating your mapping, if `alwaysOn` is enabled, consider emitting `(vimcomplete-skip)` to prevent the next keystroke from automatically reactivating the completion popup.
442 |
443 | > [!NOTE]
444 | > For help with other keybindings see `:h popupmenu-keys`. This help section includes keybindings for ``, `CTRL-H`, `CTRL-L`, `CTRL-Y`, `CTRL-E`, ``, ``, ``, and `` keys when popup menu is open.
445 |
446 | ## Disable mappings
447 |
448 | Disable all mappings set by vimcomplete.
449 | Please set your preferred mappings.
450 | Both `g:vimcomplete_tab_enable` and `g:vimcomplete_cr_enable` are ignored.
451 |
452 | ```vim
453 | vim9script
454 | g:vimcomplete_do_mapping = 0
455 | ```
456 |
457 | ## Highlight Groups
458 |
459 | You can use `Pmenu`, `PmenuThumb`, `PmenuSbar`, `PmenuSel`, `PmenuKind`,
460 | `PmenuKindSel`, `PmenuExtra` and `PmenuExtraSel` Vim highlight groups to alter the
461 | appearance of the popup menu.
462 |
463 | You can also customize the appearance of the column containing the 'kind' attribute in the menu. For example, to modify the appearance of the 'Keyword' kind, configure the `PmenuKindKeyword` highlight group. Refer to the [list](#Custom-Completion-Kinds) for all available 'kind' items.
464 |
465 | If `postfixHighlight` option is enabled, you can utilize the `VimCompletePostfix` highlight group to adjust the appearance of text adjacent to the completion. By default, it is linked to `DiffChange`.
466 |
467 | ## Info Popup Window
468 |
469 | Vim's completion system can display an additional popup window (referred to as the "info" window) next to the selected menu item when supplementary information is available.
470 |
471 | To apply basic customizations, you can use the `completepopup` option. For example:
472 |
473 | ```vim
474 | autocmd VimEnter * set completepopup+=border:off,highlight:Normal
475 | ```
476 |
477 | This provides limited customization. For advanced control over the appearance and behavior of the "info" window, use the `g:VimCompleteInfoWindowOptionsSet()` function. This function accepts a dictionary of popup window options. Refer to `:h popup_create-arguments` for detailed descriptions. Here’s an example:
478 |
479 | ```vim
480 | g:VimCompleteInfoWindowOptionsSet({
481 | drag: false, # Disable dragging
482 | close: 'none', # Disable close button
483 | resize: false, # Disable resizing
484 | })
485 | ```
486 |
487 | Info window can be scrolled using the following keys:
488 | `(vimcomplete-info-window-pageup)`,
489 | `(vimcomplete-info-window-pagedown)`,
490 | `(vimcomplete-info-window-home)`, and
491 | `(vimcomplete-info-window-end)`. Here's an example mapping:
492 |
493 | ```vim
494 | inoremap (vimcomplete-info-window-pageup)
495 | inoremap (vimcomplete-info-window-pagedown)
496 | ```
497 |
498 | # Commands
499 |
500 | Commands are available to list completion sources and to enable or disable the plugin.
501 |
502 | ## Listing Completion Sources
503 |
504 | The following command displays a list of completion sources enabled for the current buffer.
505 |
506 | ```vim
507 | :VimCompleteCompletors
508 | ```
509 |
510 | ## Enable and Disable
511 |
512 | Autocompletion is enabled by default. At any time, you can enable or disable the plugin using the following commands:
513 |
514 | ```vim
515 | :VimCompleteEnable
516 | :VimCompleteDisable
517 | ```
518 |
519 | You can selectively enable autocompletion for specific _file types_. For instance, enable autocompletion for `c`, `cpp`, `python`, `vim`, `text`, and `markdown` files.
520 |
521 | ```vim
522 | :VimCompleteEnable c cpp python vim text markdown
523 | ```
524 |
525 | To start Vim with autocompletion disabled, set the following variable.
526 |
527 | ```vim
528 | g:vimcomplete_enable_by_default = false
529 | ```
530 |
531 | `VimCompleteEnable` takes a space-separated list of _file types_ as an argument. If no argument is specified, autocompletion is enabled for _all file types_.
532 |
533 | When Vim is started without any arguments or a new buffer is created with
534 | `:bufnew`, it opens an unnamed buffer. This buffer is not associated with any
535 | _file type_. To enable/disable autocompletion on this buffer use the following
536 | variable. It is set by default.
537 |
538 | ```vim
539 | g:vimcomplete_noname_buf_enable = true
540 | ```
541 |
542 | # Custom Completion Kinds
543 |
544 | Each item returned by the LSP server has a type associated with it, which can
545 | be displayed on the popup menu. To customize , you need to use the option
546 | `customCompletionKinds` and set all custom kinds in the `completionKinds`.
547 | The following table has all default LSP kinds:
548 |
549 | Kind|Description
550 | ----|-----------
551 | t | Text
552 | m | Method
553 | f | Function
554 | C | Constructor
555 | F | Field
556 | v | Variable
557 | c | Class
558 | i | Interface
559 | M | Module
560 | p | Property
561 | u | Unit
562 | V | Value
563 | e | Enum
564 | k | Keyword
565 | S | Snippet
566 | C | Color
567 | f | File
568 | r | Reference
569 | F | Folder
570 | E | EnumMember
571 | d | Constant
572 | s | Struct
573 | E | Event
574 | o | Operator
575 | T | TypeParameter
576 | B | Buffer[^2]
577 | D | Dictionary[^2]
578 | w | Word[^2]
579 | O | Option[^2]
580 | a | Abbreviation[^2]
581 | e | EnvVariable[^2]
582 | U | URL[^2]
583 | c | Command[^2]
584 | X | Tmux[^2]
585 | G | Tag[^2]
586 |
587 | [^2]: This is not returned by LSP.
588 |
589 | For example, if you want to change the "Method" kind to the kind "method()":
590 |
591 | ```vim
592 | vim9script
593 | g:VimCompleteOptionsSet({ Completor: {
594 | customCompletionKinds: true,
595 | completionKinds: {
596 | "Method": "method()"
597 | }
598 | })
599 |
600 | In the completion popup, will show something like this: >
601 |
602 | var file = new File()
603 |
604 | file.cre
605 | | create method() |
606 | | createIfNotExists method() |
607 | | ... |
608 | ```
609 |
610 | # Writing Your Own Extension
611 |
612 | Start by examining the implementation of an external plugin like [ngrams-viewer](https://github.com/girishji/ngramview-complete.vim) (which spawns a new process to handle http requests) or [ngram-complete](https://github.com/girishji/ngram-complete.vim).
613 |
614 | The completion engine employs an interface similar to Vim's [complete-functions](https://vimhelp.org/insert.txt.html#complete-functions). However, the function is invoked in three ways instead of two:
615 |
616 | - First, to find the start of the text to be completed.
617 | - Next, to check if completion candidates are available.
618 | - Lastly, to find the actual matches.
619 |
620 | The first and last invocation are identical to Vim's [complete-functions](https://vimhelp.org/insert.txt.html#complete-functions). During the second invocation, the arguments are:
621 |
622 | - `findstart: 2`
623 | - `base: empty`
624 |
625 | The function must return `true` or `false` to indicate whether completion candidates are ready. Only when this return value is `true` will the function be invoked for the third time to get the actual matches. This step is essential for asynchronous completion.
626 |
627 | The name of the completion function does not matter, but it should take two arguments: `findstart: Number` and `base: String`, and return ``. Register this function with the completion engine by calling `vimcompletor.Register()`. Use the `User` event of type `VimCompleteLoaded` to time the registration.
628 |
629 | When users set options through the configuration file, a `User` event with type `VimCompleteOptionsChanged` is issued. The plugin should register for this event and update its internal state accordingly.
630 |
631 | # Other Plugins to Enhance Your Workflow
632 |
633 | 1. [**Devdocs.vim**](https://github.com/girishji/devdocs.vim) - browse documentation from [devdocs.io](https://devdocs.io).
634 |
635 | 2. [**Scope.vim**](https://github.com/girishji/scope.vim) - fuzzy find anything.
636 |
637 | 3. [**VimBits**](https://github.com/girishji/vimbits) - curated suite of lightweight Vim plugins.
638 |
639 | 4. [**VimSuggest**](https://github.com/girishji/vimsuggest) - autocompletion for Vim's command-line.
640 |
641 | # Contributing
642 |
643 | Pull requests are welcomed.
644 |
645 | # Similar Vim Plugins
646 |
647 | - [asyncomplete](https://github.com/prabirshrestha/asyncomplete.vim)
648 | - [nvim-cmp](https://github.com/hrsh7th/nvim-cmp)
649 |
650 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/abbrev.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | export var options: dict = {
6 | enable: false,
7 | dup: true,
8 | maxCount: 10,
9 | }
10 |
11 | def GetAbbrevs(prefix: string): list
12 | var lines = execute('ia', 'silent!')
13 | if lines =~? gettext('No abbreviation found')
14 | return []
15 | endif
16 | var abb = []
17 | for line in lines->split("\n")
18 | var matches = line->matchlist('\v^i\s+\zs(\S+)\s+(.*)$')
19 | if matches->len() > 2 && matches[1]->stridx(prefix) == 0
20 | abb->add({ prefix: matches[1], expn: matches[2] })
21 | endif
22 | endfor
23 | return abb
24 | enddef
25 |
26 | export def Completor(findstart: number, base: string): any
27 | if findstart == 2
28 | return 1
29 | elseif findstart == 1
30 | var line = getline('.')->strpart(0, col('.') - 1)
31 | var prefix = line->matchstr('\S\+$')
32 | if prefix->empty()
33 | return -2
34 | endif
35 | if GetAbbrevs(prefix)->empty()
36 | prefix = line->matchstr('\k\+$')
37 | if prefix->empty()
38 | return -2
39 | endif
40 | endif
41 | return col('.') - prefix->len()
42 | endif
43 |
44 | var prefix = base
45 | var abbrevs = GetAbbrevs(prefix)
46 | if abbrevs == []
47 | return []
48 | endif
49 | var citems = []
50 | var kind = util.GetItemKindValue('Abbrev')
51 | var kindhl = util.GetKindHighlightGroup('Abbrev')
52 | for abbrev in abbrevs
53 | citems->add({
54 | word: abbrev.prefix,
55 | info: abbrev.expn,
56 | kind: kind,
57 | kind_hlgroup: kindhl,
58 | dup: options.dup ? 1 : 0,
59 | })
60 | endfor
61 | return citems->empty() ? [] : citems->sort((v1, v2) => {
62 | return v1.word < v2.word ? -1 : v1.word ==# v2.word ? 0 : 1
63 | })->slice(0, options.maxCount)
64 | enddef
65 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/buffer.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | # Completion from current and loaded buffers
6 |
7 | # Completion candidates are sorted according to locality (how close they are to
8 | # cursor). Case sensitive matches are preferred to case insensitive and partial
9 | # matches. If user type 'fo', then 'foo' appears before 'Foo' and 'barfoo'.
10 |
11 | export var options: dict = {
12 | timeout: 90, # Match this with throttle timeout in completor
13 | maxCount: 10,
14 | otherBuffersCount: 3, # Max count of other listed buffers to search
15 | completionMatcher: 'icase', # 'case', 'fuzzy', 'icase'
16 | urlComplete: false,
17 | envComplete: false,
18 | maxWordLen: 100, # Words beyond this length are ignored
19 | dup: true,
20 | }
21 |
22 | var Elapsed = (t) => float2nr(t->reltime()->reltimefloat() * 1000)
23 |
24 | # Return a list of keywords from a buffer
25 | def BufWords(bufnr: number, prefix: string, timelimit: number, curbuf = false): list
26 | var found = {}
27 | var start = reltime()
28 | var timeout = timelimit
29 | var linenr = 1
30 | var items = []
31 | var kind = util.GetItemKindValue('Buffer')
32 | var kindhl = util.GetKindHighlightGroup('Buffer')
33 | var bufname = ''
34 | def GetLines(): list
35 | if curbuf
36 | return getline(1, '$')
37 | else
38 | bufname = $'#{bufnr}'->expand()->fnamemodify(':t')
39 | return getbufline(bufnr, 1, '$')
40 | endif
41 | enddef
42 | for line in GetLines()
43 | for word in line->split('\W\+')
44 | if !found->has_key(word) && word->len() > 1
45 | found[word] = 1
46 | if curbuf
47 | items->add({word: word, kind: kind, kind_hlgroup: kindhl})
48 | else
49 | items->add({word: word, kind: kind, kind_hlgroup: kindhl, menu: bufname})
50 | endif
51 | endif
52 | endfor
53 | # Check every 100 lines if timeout is exceeded
54 | if (timeout > 0 && linenr % 100 == 0 && start->Elapsed() > timeout)
55 | break
56 | endif
57 | linenr += 1
58 | endfor
59 | if options.completionMatcher == 'fuzzy'
60 | return items->matchfuzzy(prefix, {limit: 100, key: 'word'})
61 | else
62 | var pattern = (options.completionMatcher == 'icase') ? $'\c^{prefix}' : $'\C^{prefix}'
63 | var citems = []
64 | for item in items
65 | try
66 | if item.word =~ pattern
67 | citems->add(item)
68 | endif
69 | catch # E33 is caught if prefix has a "~"
70 | endtry
71 | endfor
72 | return citems
73 | endif
74 | enddef
75 |
76 | def ExtendUnique(dest: list>, src: list>): list>
77 | var found = {}
78 | for item in dest
79 | found[item.word] = 1
80 | endfor
81 | var items = dest->copy()
82 | for item in src
83 | if !found->has_key(item.word)
84 | items->add(item)
85 | endif
86 | endfor
87 | return items
88 | enddef
89 |
90 | def GetLength(items: list>, prefix: string): number
91 | try
92 | return items->reduce((sum, val) => sum + (val.word =~# $'^{prefix}' ? 1 : 0), 0)
93 | catch
94 | return 0
95 | endtry
96 | enddef
97 |
98 | def OtherBufMatches(items: list>, prefix: string, timelimit: number): list>
99 | if GetLength(items, prefix) > options.maxCount || options.otherBuffersCount < 1
100 | return items
101 | endif
102 | var buffers = getbufinfo({ bufloaded: 1 })
103 | var curbufnr = bufnr('%')
104 | var Buflisted = (bufnr) => getbufinfo(bufnr)->get(0, {listed: false}).listed
105 | buffers = buffers->filter((_, v) => v.bufnr != curbufnr && Buflisted(v.bufnr))
106 | buffers->sort((v1, v2) => v1.lastused > v2.lastused ? -1 : 1)
107 | buffers = buffers->slice(0, options.otherBuffersCount)
108 | var citems = items->copy()
109 | var remaining = timelimit
110 | for b in buffers
111 | if remaining > 0
112 | var start = reltime()
113 | citems = ExtendUnique(citems, BufWords(b.bufnr, prefix, remaining))
114 | remaining -= start->Elapsed()
115 | endif
116 | if GetLength(citems, prefix) > options.maxCount
117 | break
118 | endif
119 | endfor
120 | return citems
121 | enddef
122 |
123 | # Search for http links in current buffer
124 | def UrlMatches(base: string): list>
125 | var start = reltime()
126 | var timeout = options.timeout
127 | var linenr = 1
128 | var items = []
129 | var baselen = base->len()
130 | for line in getline(1, '$')
131 | var url = line->matchstr('\chttp\S\+')
132 | # url can have non-word characters like ~)( etc., (RFC3986) that need to be
133 | # escaped in a regex. Error prone. More robust way is to compare strings.
134 | if !url->empty() && url->strpart(0, baselen) ==? base
135 | items->add(url)
136 | endif
137 | # Check every 200 lines if timeout is exceeded
138 | if (timeout > 0 && linenr % 200 == 0 && start->Elapsed() > timeout)
139 | break
140 | endif
141 | linenr += 1
142 | endfor
143 | items->sort()->uniq()
144 | var kind = util.GetItemKindValue('URL')
145 | var kind_hl = util.GetKindHighlightGroup('URL')
146 | return items->map((_, v) => ({ word: v, abbr: v, kind: kind, kind_hlgroup: kind_hl }))
147 | enddef
148 |
149 | # Using searchpos() is ~15% faster than gathering words by splitting lines and
150 | # comparing each word for pattern.
151 | def CurBufMatches(prefix: string): list>
152 | var icasepat = $'\c\<{prefix}\k*'
153 | var pattern = $'\<{prefix}'
154 | var searchStartTime = reltime()
155 | var timeout: number = options.timeout / 2
156 |
157 | def SearchWords(forward: bool): list
158 | var [startl, startc] = [line('.'), col('.')]
159 | var [lnum, cnum] = [1, 1]
160 | var flags = forward ? 'W' : 'Wb'
161 | var words = []
162 | var found = {}
163 | var count = 0
164 | try
165 | [lnum, cnum] = icasepat->searchpos(flags, 0, timeout)
166 | catch # a `~` in icasepat keyword (&isk) in txt file throws E33
167 | echom v:exception
168 | return []
169 | endtry
170 | while [lnum, cnum] != [0, 0]
171 | var [endl, endc] = icasepat->searchpos('ceW') # end of matching string
172 | const line = getline(lnum)
173 | const beginidx = line->charidx(cnum - 1)
174 | var mstr = line->strcharpart(beginidx, line->charidx(endc - 1) - beginidx + 1)
175 | if mstr != prefix && !found->has_key(mstr)
176 | found[mstr] = 1
177 | words->add([mstr, abs(lnum - startl)])
178 | try
179 | if mstr =~# pattern
180 | count += 1
181 | endif
182 | catch
183 | endtry
184 | endif
185 | if (count >= options.maxCount) || searchStartTime->Elapsed() > timeout
186 | timeout = 0
187 | cursor([startl, startc])
188 | break
189 | endif
190 | if !forward
191 | cursor(lnum, cnum) # restore cursor, otherwise backward search loops
192 | endif
193 | [lnum, cnum] = icasepat->searchpos(flags, 0, timeout)
194 | endwhile
195 | timeout = max([0, timeout - searchStartTime->Elapsed()])
196 | cursor([startl, startc])
197 | return words
198 | enddef
199 |
200 | # Search backwards and forward
201 | var bwd = SearchWords(false)
202 | timeout += options.timeout / 2
203 | var fwd = SearchWords(true)
204 | var dist = {} # {word: distance}
205 | for word in bwd
206 | dist[word[0]] = word[1]
207 | endfor
208 | for word in fwd
209 | dist[word[0]] = dist->has_key(word[0]) ? min([dist[word[0]], word[1]]) : word[1]
210 | endfor
211 | fwd->filter((_, v) => v[1] == dist[v[0]])
212 | bwd->filter((_, v) => v[1] == dist[v[0]])
213 | var found = {}
214 | for word in fwd
215 | found[word[0]] = 1
216 | endfor
217 | bwd->filter((_, v) => !found->has_key(v[0])) # exclude word in both fwd and bwd with same dist
218 |
219 | # Merge the two lists
220 | var fwdlen = fwd->len()
221 | var bwdlen = bwd->len()
222 | var fwdidx = 0
223 | var bwdidx = 0
224 | var citems = []
225 | var kind = util.GetItemKindValue('Buffer')
226 | var kind_hl = util.GetKindHighlightGroup('Buffer')
227 | while fwdidx < fwdlen && bwdidx < bwdlen
228 | var wordf = fwd[fwdidx]
229 | var wordb = bwd[bwdidx]
230 | if wordf[1] < wordb[1]
231 | citems->add({ word: wordf[0], kind: kind, kind_hlgroup: kind_hl })
232 | fwdidx += 1
233 | else
234 | citems->add({ word: wordb[0], kind: kind, kind_hlgroup: kind_hl })
235 | bwdidx += 1
236 | endif
237 | endwhile
238 | while fwdidx < fwdlen
239 | var wordf = fwd[fwdidx]
240 | citems->add({ word: wordf[0], kind: kind, kind_hlgroup: kind_hl })
241 | fwdidx += 1
242 | endwhile
243 | while bwdidx < bwdlen
244 | var wordb = bwd[bwdidx]
245 | citems->add({ word: wordb[0], kind: kind, kind_hlgroup: kind_hl })
246 | bwdidx += 1
247 | endwhile
248 |
249 | var candidates: list = []
250 | if !citems->empty()
251 | try
252 | candidates = citems->copy()->filter((_, v) => v.word =~# pattern)
253 | catch
254 | endtry
255 | if candidates->len() >= options.maxCount
256 | return candidates->slice(0, options.maxCount)
257 | endif
258 | if options.completionMatcher == 'icase'
259 | try
260 | candidates += citems->copy()->filter((_, v) => v.word !~# pattern)
261 | catch
262 | endtry
263 | if candidates->len() >= options.maxCount
264 | return candidates->slice(0, options.maxCount)
265 | endif
266 | endif
267 | endif
268 | return candidates
269 | enddef
270 |
271 | var previous = {prefix: '', completed: true}
272 |
273 | export def Completor(findstart: number, base: string): any
274 | if findstart == 2
275 | return 1
276 | elseif findstart == 1
277 | var line = getline('.')->strpart(0, col('.') - 1)
278 | var prefix: string
279 | if options.urlComplete
280 | prefix = line->matchstr('\c\vhttp(s)?(:)?(/){0,2}\S+$')
281 | endif
282 | if prefix == '' && options.envComplete
283 | prefix = line->matchstr('$\zs\k\+$')
284 | endif
285 | if prefix == ''
286 | prefix = line->matchstr('\k\+$')
287 | if prefix == ''
288 | return -2
289 | endif
290 | endif
291 | if previous.prefix != '' && !previous.completed
292 | var plen = (previous.prefix)->len()
293 | if prefix->strpart(0, plen) == previous.prefix
294 | # if previous attempt was unsuccessful for the same prefix, do not try again
295 | previous.prefix = prefix
296 | return -2
297 | endif
298 | endif
299 | previous.prefix = prefix
300 | return line->len() - prefix->len() + 1
301 | endif
302 |
303 | if options->has_key('icase') # legacy option
304 | options.completionMatcher = options.icase ? 'icase' : 'case'
305 | endif
306 | var candidates: list> = []
307 | if options.urlComplete && base =~? '^http'
308 | candidates += UrlMatches(base)
309 | endif
310 | if options.envComplete
311 | var line = getline('.')->strpart(0, col('.') - 1)
312 | if line =~ '$\k\+$'
313 | var kind = util.GetItemKindValue('EnvVariable')
314 | var kind_hl = util.GetKindHighlightGroup('EnvVariable')
315 | var envs = base->getcompletion('environment')->map((_, v) => ({ word: v, abbr: v, kind: kind, kind_hlgroup: kind_hl }))
316 | candidates += envs
317 | endif
318 | endif
319 | if base =~ '^\k\+$' # not url or env complete
320 | var start = reltime()
321 | if options.completionMatcher == 'fuzzy'
322 | candidates += BufWords(0, base, options.timeout, true)
323 | else
324 | candidates += CurBufMatches(base)
325 | endif
326 | if options.timeout > 0
327 | var remaining = options.timeout - start->Elapsed()
328 | if remaining > 0 && candidates->len() < options.maxCount
329 | candidates = OtherBufMatches(candidates, base, remaining)
330 | endif
331 | endif
332 | # remove long words
333 | candidates->filter((_, v) => v.word->len() <= options.maxWordLen)
334 | # remove items identical to what is already typed
335 | candidates->filter((_, v) => v.word !=# base)
336 | # remove item xxxyyy when it appears in the form of xxx|yyy (where '|' is the cursor)
337 | var postfix = getline('.')->matchstr('^\w\+', col('.') - 1)
338 | if !postfix->empty()
339 | var excluded = $'{base}{postfix}'
340 | candidates->filter((_, v) => v.word !=# excluded)
341 | endif
342 | # in 'txt' and 'help' files show camelcase as appropriate
343 | if &filetype =~ 'text\|help'
344 | if base =~ '^\l\+$'
345 | candidates->mapnew((_, v) => {
346 | if v.word =~ '^\u\l\+$'
347 | v.word = v.word->tolower()
348 | endif
349 | return v
350 | })
351 | elseif base =~ '^\u\l\+$'
352 | candidates->mapnew((_, v) => {
353 | if v.word =~ '^\l\+$'
354 | v.word = v.word->slice(0, 1)->toupper() .. v.word->slice(1)
355 | endif
356 | return v
357 | })
358 | endif
359 | endif
360 | endif
361 | if options.dup
362 | candidates->map((_, v) => v->extend({ dup: 1 }))
363 | endif
364 |
365 | previous.completed = !candidates->empty()
366 | return candidates->slice(0, options.maxCount)
367 | enddef
368 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/completor.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | # Main autocompletion engine
4 |
5 | import autoload './util.vim'
6 | import autoload './lsp.vim'
7 | import autoload './options.vim' as copts
8 |
9 | export var options: dict
10 | options = copts.options
11 | var saved_options: dict = {}
12 |
13 | export def GetOptions(provider: string): dict
14 | return saved_options->get(provider, {})
15 | enddef
16 |
17 | export def SetOptions(opts: dict)
18 | saved_options = opts
19 | enddef
20 |
21 | var registered: dict = { any: [] }
22 | var completors: list
23 |
24 | def SetupCompletors()
25 | if &ft == '' || !registered->has_key(&ft)
26 | completors = registered.any
27 | else
28 | completors = registered[&ft] + registered.any
29 | endif
30 | completors->sort((v1, v2) => v2.priority - v1.priority)
31 | enddef
32 |
33 | export def ShowCompletors()
34 | for completor in completors
35 | echom completor
36 | endfor
37 | enddef
38 |
39 | export def IsCompletor(source: string): bool
40 | return completors->indexof((_, v) => v.name == source) != -1
41 | enddef
42 |
43 | export def ClearRegistered()
44 | registered = { any: [] }
45 | enddef
46 |
47 | export def Register(name: string, Completor: func, ftype: list, priority: number)
48 | def AddCompletor(ft: string)
49 | if !registered->has_key(ft)
50 | registered[ft] = []
51 | endif
52 | if registered[ft]->indexof((_, v) => v.name == name) == -1
53 | registered[ft]->add({name: name, completor: Completor, priority: priority})
54 | endif
55 | enddef
56 | if ftype->empty()
57 | return
58 | endif
59 | # clear prior registrations
60 | for ft in ftype
61 | if registered->has_key(ft)
62 | registered[ft]->filter((_, v) => v.name != name)
63 | endif
64 | endfor
65 | if ftype[0] == '*'
66 | AddCompletor('any')
67 | else
68 | for ft in ftype
69 | AddCompletor(ft)
70 | endfor
71 | endif
72 | SetupCompletors()
73 | enddef
74 |
75 | export def Unregister(name: string)
76 | for providers in registered->values()
77 | providers->filter((_, v) => v.name != name)
78 | endfor
79 | SetupCompletors()
80 | enddef
81 |
82 | import autoload './recent.vim'
83 |
84 | def DisplayPopup(citems: list, line: string)
85 | if citems->empty()
86 | return
87 | endif
88 | # Only one value for startcol is allowed. Pick the longest completion.
89 | var startcol = citems->mapnew((_, v) => {
90 | return v.startcol
91 | })->min()
92 | citems->filter((_, v) => v.startcol == startcol)
93 | citems->sort((v1, v2) => v1.priority > v2.priority ? -1 : 1)
94 |
95 | var items: list> = []
96 | var prefix = line->strpart(startcol - 1)
97 | var prefixlen = prefix->len()
98 | if options.shuffleEqualPriority
99 | for priority in citems->copy()->map((_, v) => v.priority)->uniq()
100 | var eqitems = citems->copy()->filter((_, v) => v.priority == priority)
101 | var maxlen = eqitems->copy()->map((_, v) => v.items->len())->max()
102 | def PopulateItems(exactMatch: bool)
103 | for idx in maxlen->range()
104 | for it in eqitems
105 | if !it.items->get(idx)
106 | continue
107 | endif
108 | var repl = it.items[idx]->get('abbr', '')
109 | if repl->empty()
110 | repl = it.items[idx].word
111 | endif
112 | if exactMatch
113 | if repl->strpart(0, prefixlen) ==# prefix
114 | items->add(it.items[idx])
115 | endif
116 | else
117 | if repl->strpart(0, prefixlen) !=# prefix
118 | items->add(it.items[idx])
119 | endif
120 | endif
121 | endfor
122 | endfor
123 | enddef
124 | PopulateItems(true)
125 | PopulateItems(false)
126 | endfor
127 | else
128 | for it in citems
129 | items->extend(it.items)
130 | endfor
131 | endif
132 |
133 | if options.sortByLength
134 | items->sort((v1, v2) => v1.word->len() <= v2.word->len() ? -1 : 1)
135 | endif
136 |
137 | if options.matchCase
138 | items = items->copy()->filter((_, v) => v.word->strpart(0, prefixlen) ==# prefix) +
139 | items->copy()->filter((_, v) => v.word->strpart(0, prefixlen) !=# prefix)
140 | # Note: Comparing strings (above) is more robust than regex match, since
141 | # items can include non-keyword characters like ')' which otherwise
142 | # needs escaping.
143 | endif
144 |
145 | if options.recency
146 | items = recent.Recent(items, prefix, options.recentItemCount)
147 | endif
148 | if options.debug
149 | echom items
150 | endif
151 | items->complete(startcol)
152 | enddef
153 |
154 | def GetCurLine(): string
155 | var m = mode()
156 | if m != 'i' && m != 'R' && m != 'Rv' # not in insert or replace mode
157 | return ''
158 | endif
159 | var curcol = col('.')
160 | var curline = getline('.')
161 | if curcol == 0 || curline->empty()
162 | return ''
163 | endif
164 | return curline->strpart(0, curcol - 1)
165 | enddef
166 |
167 | def GetItems(cmp: dict, line: string): list
168 | # Non ascii chars like ’ occupy >1 columns since they have composing
169 | # characters. strpart(), col('.'), len() use byte index, while
170 | # strcharpart(), slice(), strcharlen() use char index.
171 | var base = line->strpart(cmp.startcol - 1)
172 | # Note: when triggerCharacter is used in LSP (like '.') base is empty.
173 | var items = cmp.completor(0, base)
174 | if options.showCmpSource
175 | var cmp_name = options.cmpSourceWidth > 0 ?
176 | cmp.name->slice(0, options.cmpSourceWidth) : cmp.name
177 | items->map((_, v) => {
178 | if v->has_key('menu')
179 | if v.menu !~? $'^\[{cmp.name}]'
180 | v.menu = $'[{cmp_name}] {v.menu}'
181 | endif
182 | else
183 | v.menu = $'[{cmp_name}]'
184 | endif
185 | return v
186 | })
187 | endif
188 | if !options.showKind
189 | items->map((_, v) => {
190 | if v->has_key('kind')
191 | v->remove('kind')
192 | endif
193 | return v
194 | })
195 | endif
196 | return items
197 | enddef
198 |
199 | def AsyncGetItems(curline: string, pendingcompletors: list, partialitems: list, count: number, timer: number)
200 | var line = GetCurLine()
201 | # If user already tabbed on an item from popup menu or typed something,
202 | # then current line will change and this completion prefix is no longer valid
203 | if curline !=# line
204 | return
205 | endif
206 | if count < 0
207 | DisplayPopup(partialitems, line)
208 | return
209 | endif
210 |
211 | var citems = partialitems->copy()
212 | var asyncompletors: list = []
213 | var partial_items_returned = false
214 | for cmp in pendingcompletors
215 | if cmp.completor(2, '') > 0
216 | var items = GetItems(cmp, line)
217 | if !items->empty()
218 | citems->add({ priority: cmp.priority, startcol: cmp.startcol,
219 | items: items })
220 | endif
221 | if cmp.completor(2, '') == 2 # more items expected
222 | asyncompletors->add(cmp)
223 | partial_items_returned = true
224 | endif
225 | else
226 | asyncompletors->add(cmp)
227 | endif
228 | endfor
229 |
230 | if asyncompletors->empty() || partial_items_returned
231 | DisplayPopup(citems, line)
232 | else
233 | timer_start(0, function(AsyncGetItems, [line, asyncompletors, citems, count - 1]))
234 | endif
235 | enddef
236 |
237 | # Text does not change after or but TextChanged will get
238 | # called anyway. To avoid and from closing popup and reopening
239 | # again, set a flag.
240 | # https://github.com/girishji/vimcomplete/issues/37
241 | var skip_complete: bool = false
242 |
243 | export def SkipCompleteSet(): string
244 | if options.alwaysOn && pumvisible()
245 | skip_complete = true
246 | endif
247 | return ''
248 | enddef
249 |
250 | def SkipComplete(): bool
251 | if skip_complete
252 | skip_complete = false
253 | return true
254 | endif
255 | if exists('*vsnip#jumpable') && vsnip#jumpable(1)
256 | return true
257 | endif
258 | return false
259 | enddef
260 |
261 | def VimComplete(saved_curline = null_string, timer = 0)
262 | if SkipComplete()
263 | return
264 | endif
265 | var line = GetCurLine()
266 | if line->empty()
267 | return
268 | endif
269 | # Throttle auto-completion if user types too fast (make typing responsive)
270 | # Vim bug: If pum is visible, and when characters are typed really fast, Vim
271 | # freezes. After a while it sends all the typed characters in one
272 | # TextChangedI (Not TextChangedP) event.
273 | if saved_curline == null_string
274 | timer_start(options.throttleTimeout, function(VimComplete, [line]))
275 | return
276 | elseif saved_curline != line
277 | return
278 | endif
279 |
280 | if options.triggerWordLen > 0
281 | var keyword = line->matchstr('\k\+$')
282 | if keyword->len() < options.triggerWordLen &&
283 | (!IsCompletor('lsp') || lsp.GetTriggerKind() != 2)
284 | return
285 | endif
286 | endif
287 | var syncompletors: list = []
288 | for cmp in completors
289 | var scol: number = cmp.completor(1, '')
290 | if scol < 0
291 | continue
292 | endif
293 | syncompletors->add(cmp->extendnew({ startcol: scol }))
294 | endfor
295 |
296 | # Collect items that are immediately available
297 | var citems = []
298 | var asyncompletors: list = []
299 | for cmp in syncompletors
300 | if cmp.completor(2, '') > 0
301 | var items = GetItems(cmp, line)
302 | if !items->empty()
303 | citems->add({ priority: cmp.priority, startcol: cmp.startcol,
304 | items: items })
305 | endif
306 | else
307 | asyncompletors->add(cmp)
308 | endif
309 | endfor
310 | DisplayPopup(citems, line)
311 | if !asyncompletors->empty()
312 | # wait a maximum 2 sec, checking every 2ms to receive items from completors
313 | timer_start(5, function(AsyncGetItems, [line, asyncompletors, citems, 400]))
314 | endif
315 | enddef
316 |
317 | def VimCompletePopupVisible()
318 | var compl = complete_info(['selected', 'pum_visible'])
319 | if !compl.pum_visible # should not happen
320 | return
321 | endif
322 | if compl.selected == -1 # no item is selected in the menu
323 | VimComplete()
324 | endif
325 | enddef
326 |
327 | export def DoComplete(): string
328 | pumvisible() ? VimCompletePopupVisible() : VimComplete()
329 | return ''
330 | enddef
331 |
332 | def LRU_Cache()
333 | if v:completed_item->empty()
334 | # CompleteDone is triggered very frequently with empty dict
335 | return
336 | endif
337 | if options.recency
338 | recent.CacheAdd(v:completed_item)
339 | endif
340 | enddef
341 |
342 | export def Enable()
343 | var bnr = bufnr()
344 | var cotval = null_string
345 | if options.alwaysOn && options.setCompleteOpt
346 | cotval = 'menuone,noselect,noinsert'
347 | endif
348 | if options.infoPopup
349 | # Hide the popup -- for customizing "info" popup window
350 | cotval ..= (cotval != null_string ? ',popuphidden' : 'popuphidden')
351 | endif
352 | if cotval != null_string
353 | cotval = (&completeopt != '' ? $'{&completeopt},' : '') .. cotval
354 | setbufvar(bnr, '&completeopt', cotval)
355 | endif
356 |
357 | augroup VimCompBufAutocmds | autocmd! *
358 | if options.alwaysOn
359 | autocmd TextChangedI VimComplete()
360 | autocmd TextChangedP VimCompletePopupVisible()
361 | endif
362 | # Note: When &ft is set in modeline, BufEnter will not have &ft set and
363 | # thus SetupCompletors() will not set completors appropriately. However,
364 | # if 'FileType' event is used to trigger SetupCompletors() and if :make
365 | # is used, it causes a FileType event for 'qf' (quickfix) file even when
366 | # :make does not have errors. This causes completors to be reset. There
367 | # will be no BufEnter to undo the damage. Thus, do not use FileType
368 | # event below.
369 | autocmd BufEnter,BufReadPost SetupCompletors()
370 | autocmd CompleteDone LRU_Cache()
371 | if options.postfixClobber
372 | autocmd CompleteDone util.TextAction(true)
373 | autocmd CompleteChanged util.TextActionPre(true)
374 | autocmd InsertLeave util.UndoTextAction(true)
375 | elseif options.postfixHighlight
376 | autocmd CompleteChanged util.TextActionPre()
377 | autocmd CompleteDone,InsertLeave util.UndoTextAction()
378 | endif
379 | if options.infoPopup
380 | autocmd CompleteChanged util.InfoPopupWindowSetOptions()
381 | endif
382 | augroup END
383 |
384 | if options.postfixHighlight
385 | highlight default link VimCompletePostfix DiffChange
386 | endif
387 |
388 | if !get(g:, 'vimcomplete_do_mapping', 1)
389 | return
390 | endif
391 |
392 | util.CREnable()
393 |
394 | if options.alwaysOn
395 | inoremap (vimcomplete-skip)
396 | inoremap (vimcomplete-skip)
397 | else
398 | silent! iunmap
399 | inoremap (vimcomplete-do-complete)
400 | imap
401 | endif
402 |
403 | if options.postfixClobber
404 | inoremap (vimcomplete-undo-text-action) util.UndoTextAction(true)
405 | inoremap (vimcomplete-undo-text-action)
406 | elseif options.postfixHighlight
407 | inoremap (vimcomplete-undo-text-action) util.UndoTextAction()
408 | inoremap (vimcomplete-undo-text-action)
409 | inoremap util.TextActionWrapper()
410 | endif
411 |
412 | util.TabEnable()
413 | enddef
414 |
415 | export def Disable()
416 | augroup VimCompBufAutocmds | autocmd! *
417 | augroup END
418 | enddef
419 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/dictionary.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | # /usr/share/dict/words is ~2M. Reading in the file took 33ms, and
6 | # for string 'za' took 62ms. Parsing each line into a list took 1.2 sec. Three
7 | # options: 1) read in the file into a buffer and search it using searchpos(),
8 | # 2) read file into a list and do binary search, 3) use external command
9 | # 'look' which does binary search. 'look' maybe faster since Vim does not have
10 | # to read the file. Other options are :vimgrep and :grep commands.
11 |
12 | # Note: Sorted dictionaries cannot have empty lines
13 |
14 | export var options: dict = {
15 | enable: false,
16 | matcher: 'case', # 'case', 'ignorecase'. active for sortedDict or onlyWords is true,
17 | maxCount: 10,
18 | sortedDict: true,
19 | onlyWords: true, # [0-9z-zA-Z] if true, else any non-space char is allowed (sorted=false assumed)
20 | commentStr: '---',
21 | triggerWordLen: 0,
22 | timeout: 0, # not implemented yet
23 | dup: false, # suppress duplicates
24 | matchStr: '\k\+$',
25 | matchAny: false,
26 | info: false, # Whether 'info' popup needs to be populated
27 | }
28 |
29 | def GetProperty(s: string): any
30 | if options->has_key('properties') && options.properties->has_key(&filetype)
31 | && options.properties->get(&filetype)->has_key(s)
32 | return options.properties->get(&filetype)->get(s)
33 | endif
34 | return options->get(s)
35 | enddef
36 |
37 | def MatchStr(): string
38 | return GetProperty('matchStr')
39 | enddef
40 |
41 | def CommentStr(): string
42 | return GetProperty('commentStr')
43 | enddef
44 |
45 | def OnlyWords(): bool
46 | return GetProperty('onlyWords')
47 | enddef
48 |
49 | def SortedDict(): bool
50 | return GetProperty('sortedDict')
51 | enddef
52 |
53 | def TriggerWordLen(): number
54 | return GetProperty('triggerWordLen')
55 | enddef
56 |
57 | def Info(): bool
58 | return GetProperty('info')
59 | enddef
60 |
61 | var dictbufs = {}
62 |
63 | # Create a readonly, unlisted buffer for each dictionary file so we don't have
64 | # to read from disk repeatedly. This is a one-time thing, took 45ms for a 2M
65 | # dictionary file on Macbook Air M1.
66 | #
67 | # Return a list of buffer numbers of dictionary files.
68 | def GetDict(): list
69 | var ftype = &filetype # filetype of active buffer
70 | if dictbufs->has_key(ftype)
71 | return dictbufs[ftype]
72 | endif
73 | if &dictionary == ''
74 | return []
75 | endif
76 | var dictbuf = []
77 | for d in &dictionary->split(',')
78 | var bnr = bufadd(d)
79 | # Note: Use 'noautocmd' below. Otherwise BufEnter gets called with empty
80 | # &ft, which triggers SetCompletors(), which removes dict completion.
81 | noautocmd bnr->bufload()
82 | setbufvar(bnr, "&buftype", 'nowrite')
83 | setbufvar(bnr, "&swapfile", 0)
84 | setbufvar(bnr, "&buflisted", 0)
85 | dictbuf->add(bnr)
86 | endfor
87 | if !dictbuf->empty()
88 | dictbufs[ftype] = dictbuf
89 | return dictbufs[ftype]
90 | endif
91 | return []
92 | enddef
93 |
94 | var dictwords: dict = {} # dictionary file -> words
95 |
96 | def GetWords(prefix: string, bufnr: number): dict
97 | var startcol: number
98 | if options.timeout <= 0
99 | # read the whole dict buffer at once and cache it
100 | if !dictwords->has_key(bufnr)
101 | dictwords[bufnr] = []
102 | var word = null_string
103 | var info = []
104 | var has_info = Info()
105 | for line in bufnr->getbufline(1, '$')
106 | if line !~ $'^\s*{CommentStr()}' # ignore comments (https://github.com/vim-scripts/Pydiction)
107 | if has_info
108 | if line =~ '^\%(\s\+\|$\)' # Info document
109 | # Strip only 4 spaces at the start of line to keep the formatting.
110 | info->add(line->substitute('^ ', '', ''))
111 | else
112 | if word != null_string
113 | var idx = dictwords[bufnr]->indexof((_, v) => v.word == word)
114 | if idx != -1
115 | dictwords[bufnr]->remove(idx)
116 | endif
117 | dictwords[bufnr]->add({word: word}->extend(info != [] ? {info: info->join("\n")} : {}))
118 | endif
119 | word = line
120 | info = []
121 | endif
122 | elseif !line->empty()
123 | dictwords[bufnr]->add({word: line})
124 | endif
125 | endif
126 | endfor
127 | if has_info && word != null_string
128 | dictwords[bufnr]->add({word: word}->extend(info != [] ? {info: info->join("\n")} : {}))
129 | endif
130 | endif
131 |
132 | var items = []
133 | if OnlyWords()
134 | var pat = (options.matcher == 'case') ? $'\C^{prefix}' : $'\c^{prefix}'
135 | items = dictwords[bufnr]->copy()->filter((_, v) => v.word =~ pat)
136 | startcol = col('.') - prefix->strlen()
137 | else
138 | var prefix_kw = prefix->matchstr(MatchStr())
139 | if prefix_kw->len() < prefix->len()
140 | # Do not pattern match, but compare (equality) instead.
141 | var prefixlen = prefix->len()
142 | items = dictwords[bufnr]->copy()->filter((_, v) => v.word->strpart(0, prefixlen) == prefix)
143 | # We should return xxx from yyy.xxx.
144 | var first_part_len = prefix->len() - prefix_kw->len()
145 | items->map((_, v) => v->strpart(first_part_len))
146 | startcol = col('.') - prefix_kw->strlen()
147 | elseif !prefix_kw->empty()
148 | try
149 | # XXX in C++, typing 'ranges:' will cause the word to jump
150 | # through indentation to the first column. This is caused by
151 | # indentation program, not Vimcomplete.
152 | items = dictwords[bufnr]->copy()->filter((_, v) => v.word =~# $'^{prefix_kw}')
153 | # Match 'foo' in 'barfoobaz'.
154 | # items += dictwords[bufnr]->copy()->filter((_, v) => v.word =~# $'^.\{{-}}{prefix}')
155 | startcol = col('.') - prefix_kw->strlen()
156 | catch
157 | endtry
158 | endif
159 | endif
160 | return { startcol: startcol, items: items }
161 | endif
162 | return { startcol: 0, items: [] } # not implemented
163 | enddef
164 |
165 | # Binary search dictionary buffer. Use getbufline() instead of creating a
166 | # list (for efficiency).
167 | def GetWordsBinarySearchCase(prefix: string, bufnr: number, icase = true): dict
168 | var lidx = 1
169 | var binfo = getbufinfo(bufnr)
170 | if binfo == []
171 | return { startcol: 0, items: [] }
172 | endif
173 | var prefixlen = prefix->strlen()
174 | var ridx = binfo[0].linecount
175 | while lidx + 1 < ridx
176 | var mid: number = (ridx + lidx) / 2
177 | var words: list
178 | var line = bufnr->getbufoneline(mid)
179 | words = line->split() # in case line has >1 word, split
180 | if words->empty()
181 | echoerr '(vimcomplete) error: Dictionary has empty line'
182 | return { startcol: 0, items: [] } # error in dictionary file
183 | endif
184 | var partial = words[0]->strpart(0, prefixlen)
185 | if (icase && prefix ==? partial) || (!icase && prefix ==# partial)
186 | lidx = mid
187 | ridx = mid
188 | break
189 | endif
190 | if (icase && prefix partial) || (!icase && prefix <# partial)
191 | ridx = mid
192 | else
193 | lidx = mid
194 | endif
195 | endwhile
196 | lidx = max([1, lidx - options.maxCount])
197 | ridx = min([binfo[0].linecount, ridx + options.maxCount])
198 | var items = []
199 | for line in bufnr->getbufline(lidx, ridx)
200 | if prefix == line->strpart(0, prefixlen)
201 | items->add(line)
202 | endif
203 | endfor
204 | var startcol = col('.') - prefixlen
205 | return { startcol: startcol, items: items }
206 | enddef
207 |
208 | # NOTE:
209 | # - /usr/share/dict/words has uppercase letters. It is not sorted w.r.t. case,
210 | # but sorted when case is ignored. Make this function work when sorted with or
211 | # without case sensitiveness.
212 | # - Only one word per line
213 | def GetWordsBinarySearch(prefix: string, bufnr: number): dict
214 | var words = GetWordsBinarySearchCase(prefix, bufnr, true)
215 | if words.items->empty()
216 | words = GetWordsBinarySearchCase(prefix, bufnr, false)
217 | endif
218 | words.items->map((_, v) => {
219 | return {word: v}
220 | })
221 | return words
222 | enddef
223 |
224 | def GetCompletionItems(prefix: string): dict
225 | var startcol: number = -1
226 | var dwords = {}
227 | if SortedDict()
228 | for bufnr in GetDict()
229 | if dwords->empty()
230 | dwords = GetWordsBinarySearch(prefix, bufnr)
231 | else
232 | dwords.items->extend(GetWordsBinarySearch(prefix, bufnr).items)
233 | endif
234 | endfor
235 | else # only one dictionary supported, since startcol can differ among dicts
236 | var dicts = GetDict()
237 | if !dicts->empty()
238 | dwords = GetWords(prefix, dicts[0])
239 | endif
240 | endif
241 | var items = dwords->get('items', {})
242 | if items->empty()
243 | return { startcol: 0, items: [] }
244 | endif
245 | startcol = dwords.startcol
246 | var candidates = []
247 | # remove duplicates
248 | var found = {}
249 | for item in items
250 | if !found->has_key(item.word)
251 | found[item.word] = 1
252 | candidates->add(item)
253 | endif
254 | endfor
255 | candidates = candidates->slice(0, options.maxCount)
256 | var citems = []
257 | for cand in candidates
258 | citems->add({
259 | word: cand.word,
260 | kind: util.GetItemKindValue('Dictionary'),
261 | kind_hlgroup: util.GetKindHighlightGroup('Dictionary'),
262 | dup: options.dup ? 1 : 0,
263 | }->extend(cand->has_key('info') ? {info: cand.info} : {}))
264 | endfor
265 | return { startcol: startcol, items: citems }
266 | enddef
267 |
268 | var completionItems: dict = {}
269 |
270 | export def Completor(findstart: number, base: string): any
271 | if findstart == 2
272 | return 1
273 | elseif findstart == 1
274 | var line = getline('.')->strpart(0, col('.') - 1)
275 | var prefix: string
276 | if OnlyWords()
277 | prefix = line->matchstr('\w\+$')
278 | else
279 | prefix = line->matchstr(MatchStr())
280 | if prefix == null_string && options.matchAny
281 | prefix = line->matchstr('\S\+$')
282 | endif
283 | completionItems = GetCompletionItems(prefix)
284 | if completionItems.items->empty()
285 | prefix = line->matchstr('\k\+$')
286 | endif
287 | endif
288 | if prefix == '' ||
289 | (TriggerWordLen() > 0 && prefix->len() < TriggerWordLen())
290 | return -2
291 | endif
292 | completionItems = GetCompletionItems(prefix)
293 | return completionItems.items->empty() ? -2 : completionItems.startcol
294 | endif
295 | return completionItems.items
296 | enddef
297 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/lsp.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | # Interface to https://github.com/yegappan/lsp through omnifunc
4 |
5 | import autoload './util.vim'
6 |
7 | export var options: dict = {
8 | enable: true,
9 | maxCount: 10,
10 | keywordOnly: false, # 'false' will complete after '.' in 'builtins.'
11 | dup: true,
12 | }
13 |
14 | export def Setup()
15 | # Turn off LSP client even if this module is disabled. Otherwise LSP will
16 | # open a separate popup window with completions.
17 | if exists('*g:LspOptionsSet')
18 | var lspOpts = {
19 | useBufferCompletion: false,
20 | snippetSupport: true, # snippets from lsp server
21 | vsnipSupport: false,
22 | autoComplete: false,
23 | omniComplete: true,
24 | }
25 | if options->has_key('completionMatcher')
26 | lspOpts->extend({completionMatcher: options.completionMatcher})
27 | endif
28 | g:LspOptionsSet(lspOpts)
29 | endif
30 | enddef
31 |
32 | export def Completor(findstart: number, base: string): any
33 | if !exists('*g:LspOmniFunc')
34 | return -2 # cancel but stay in completion mode
35 | endif
36 | var line = getline('.')->strpart(0, col('.') - 1)
37 | if line =~ '\s$'
38 | return -2
39 | endif
40 | if options.keywordOnly
41 | var prefix = line->matchstr('\k\+$')
42 | if prefix->empty()
43 | return -2
44 | endif
45 | endif
46 | if findstart == 1
47 | var startcol = g:LspOmniFunc(findstart, base)
48 | return startcol < 0 ? startcol : startcol + 1
49 | elseif findstart == 2
50 | return g:LspOmniCompletePending() ? 0 : 1
51 | endif
52 | var items = g:LspOmniFunc(findstart, base)
53 | # items->filter((_, v) => v.word != base)
54 | items = items->slice(0, options.maxCount)
55 | if !options.dup
56 | items->map((_, v) => v->extend({ dup: 0 }))
57 | endif
58 | items = items->mapnew((_, v) => {
59 | var ud = v.user_data
60 | if ud->type() == v:t_dict
61 | if !v->has_key('kind_hlgroup')
62 | v.kind_hlgroup = util.GetKindHighlightGroup(ud->get('kind', ''))
63 | endif
64 | v.kind = ud->has_key('kind') ? util.GetItemKindValue(ud.kind) : ''
65 | endif
66 | return v
67 | })
68 | return items
69 | enddef
70 |
71 | if v:versionlong < 9010650
72 | export def GetTriggerKind(): number
73 | return -1
74 | enddef
75 | finish
76 | endif
77 |
78 | if get(g:, 'loaded_lsp', false)
79 | import autoload 'lsp/buffer.vim' as buf
80 | # Return trigger kind and trigger char. If completion trigger is not a keyword
81 | # and not one of the triggerCharacters, return -1.
82 | # Trigger kind is 1 for keyword and 2 for trigger char initiated completion.
83 | export def GetTriggerKind(): number
84 | var lspserver: dict = buf.CurbufGetServerChecked('completion')
85 | var triggerKind: number = 1
86 | var line: string = getline('.')
87 | var cur_col = charcol('.')
88 | if line[cur_col - 2] !~ '\k'
89 | var trigChars = lspserver.completionTriggerChars
90 | var trigidx = trigChars->index(line[cur_col - 2])
91 | if trigidx == -1
92 | triggerKind = -1
93 | else
94 | triggerKind = 2
95 | endif
96 | endif
97 | return triggerKind
98 | enddef
99 | else
100 | export def GetTriggerKind(): number
101 | return -1
102 | enddef
103 | endif
104 |
105 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/omnifunc.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 | import autoload './completor.vim'
5 |
6 | export var options: dict = {
7 | enable: false,
8 | maxCount: 10,
9 | dup: true,
10 | partialWord: ['python3complete#Complete'], # returns 'ow()' instead of 'pow()' when completing builtins.p
11 | }
12 |
13 | export def Completor(findstart: number, base: string): any
14 | if &omnifunc == null_string || completor.IsCompletor('lsp')
15 | return -2 # cancel but stay in completion mode
16 | endif
17 | var Omnifunc = function(&omnifunc)
18 | var line = getline('.')->strpart(0, col('.') - 1)
19 | if line =~ '\s$'
20 | return -2
21 | endif
22 | var partial = options.partialWord->index(&omnifunc) != -1
23 | var prefix = partial ? line->matchstr('\k\+$') : ''
24 | if findstart == 1
25 | var startcol = Omnifunc(findstart, base)
26 | return startcol < 0 ? startcol : startcol + 1 - prefix->len()
27 | elseif findstart == 2
28 | return 1
29 | endif
30 | var items: list> =
31 | Omnifunc(findstart, partial ? base->strpart(prefix->len()) : base)
32 | if items->empty()
33 | return []
34 | endif
35 | items = items->slice(0, options.maxCount)
36 | if partial
37 | var kind = util.GetItemKindValue('Keyword')
38 | items->map((_, v) => {
39 | v.word = v.abbr
40 | v.kind = (v->has_key('kind')) ? v.kind : kind
41 | return v
42 | })
43 | endif
44 | if options.dup
45 | # Cannot use extend() because of https://github.com/vim/vim/issues/16607
46 | # items->map((_, v) => v->extend({ dup: 1 })) #
47 | for item in items
48 | item['dup'] = 1
49 | endfor
50 | endif
51 | return items
52 | enddef
53 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/options.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | # Completor options
4 |
5 | export var options: dict = {
6 | noNewlineInCompletion: false,
7 | noNewlineInCompletionEver: false,
8 | matchCase: true,
9 | sortByLength: false,
10 | recency: true,
11 | recentItemCount: 5,
12 | shuffleEqualPriority: false,
13 | alwaysOn: true,
14 | setCompleteOpt: true,
15 | showCmpSource: true,
16 | cmpSourceWidth: 4,
17 | showKind: true,
18 | customCompletionKinds: false,
19 | completionKinds: {},
20 | kindDisplayType: 'symbol', # 'icon', 'icontext', 'text', 'symboltext', 'symbol', 'text'
21 | postfixClobber: false, # remove yyy in xxxyyy
22 | postfixHighlight: false, # highlight yyy in xxxyyy
23 | triggerWordLen: 0,
24 | infoPopup: true,
25 | throttleTimeout: 1,
26 | debug: false,
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/path.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | # Autocomplete file path
4 |
5 | import autoload './util.vim'
6 |
7 | export var options: dict = {
8 | enable: true,
9 | bufferRelativePath: true,
10 | groupDirectoriesFirst: false,
11 | showPathSeparatorAtEnd: false,
12 | }
13 |
14 | export def Completor(findstart: number, base: string): any
15 | if findstart == 2
16 | return 1
17 | elseif findstart == 1
18 | var line = getline('.')->strpart(0, col('.') - 1)
19 | var prefix = line->matchstr('\f\+$')
20 | if !prefix->empty() &&
21 | !(line->matchstr('\c\vhttp(s)?(:)?(/){0,2}\S+$')->empty())
22 | return -2
23 | endif
24 | if prefix->empty() || prefix =~ '?$' || prefix !~ (has('unix') || &shellslash ? '/' : '\')
25 | return -2
26 | endif
27 | return col('.') - prefix->strlen()
28 | endif
29 |
30 | var citems = []
31 | var cwd: string = ''
32 | var sep: string = has('win32') && !&shellslash ? '\' : '/'
33 | try
34 | if options.bufferRelativePath && expand('%:h') !=# '.' # not already in buffer dir
35 | # change directory to get completions for paths relative to current buffer dir
36 | cwd = getcwd()
37 | execute 'cd ' .. expand('%:p:h')
38 | endif
39 | var completions = getcompletion(base, 'file', 1)
40 | def IsDir(v: string): bool
41 | return isdirectory(fnamemodify(v, ':p'))
42 | enddef
43 | if options.groupDirectoriesFirst
44 | completions = completions->copy()->filter((_, v) => IsDir(v)) +
45 | completions->copy()->filter((_, v) => !IsDir(v))
46 | endif
47 | for item in completions
48 | var citem = item
49 | var itemlen = item->len()
50 | var isdir = IsDir(item)
51 | if isdir && item[itemlen - 1] == sep
52 | citem = item->slice(0, itemlen - 1)
53 | endif
54 | citems->add({
55 | word: citem,
56 | abbr: options.showPathSeparatorAtEnd ? item : citem,
57 | kind: isdir ? util.GetItemKindValue('Folder') : util.GetItemKindValue('File'),
58 | kind_hlgroup: isdir ? util.GetKindHighlightGroup('Folder') : util.GetKindHighlightGroup('File'),
59 | })
60 | endfor
61 | catch # on MacOS it does not complete /tmp/* (throws E344, looks for /private/tmp/...)
62 | echom v:exception
63 | finally
64 | if !cwd->empty()
65 | execute $'cd {cwd}'
66 | endif
67 | endtry
68 | return citems
69 | enddef
70 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/recent.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | export var options: dict = {}
4 |
5 | var cache: dict = {} # LRU cache
6 |
7 | const CacheSize = 10000
8 |
9 | def LRU_Keys(): list
10 | var leastrecent = cache->items()->copy() # [key, value] list
11 | leastrecent->sort((v1, v2) => v1[1].reltime < v2[1].reltime ? -1 : 1)
12 | return leastrecent->slice(0, max([ 1, CacheSize / 100 ]))
13 | enddef
14 |
15 | def KeyWord(item: dict): string
16 | return item->has_key('abbr') && !item.abbr->empty() ? item.abbr : item.word
17 | enddef
18 |
19 | def Key(item: dict): string
20 | if !item->has_key('kind')
21 | return ''
22 | endif
23 | return $'{item.kind}{KeyWord(item)}'
24 | enddef
25 |
26 | export def CacheAdd(item: dict)
27 | if cache->len() > CacheSize
28 | for it in LRU_Keys()
29 | cache->remove(it[0])
30 | endfor
31 | endif
32 | var key = Key(item)
33 | if !key->empty()
34 | cache[key] = { item: item, reltime: reltime()->reltimefloat() }
35 | endif
36 | enddef
37 |
38 | export def Recent(items: list>, prefix: string, maxcount: number = 10): list>
39 | var candidates = []
40 | for item in items
41 | var key = Key(item)
42 | if !key
43 | continue
44 | endif
45 | if cache->has_key(key) && KeyWord(item)->strpart(0, prefix->len()) ==# prefix
46 | candidates->add({ key: key, item: item, reltime: cache[key].reltime })
47 | endif
48 | endfor
49 | if candidates->empty()
50 | return items
51 | endif
52 | candidates->sort((v1, v2) => v1.reltime < v2.reltime ? 1 : -1)
53 | candidates = candidates->slice(0, maxcount + 1)
54 |
55 | var citems: list> = []
56 | var iscandidate = {}
57 | for item in candidates
58 | citems->add(item.item)
59 | iscandidate[item.key] = 1
60 | endfor
61 | for item in items
62 | var key = Key(item)
63 | if !key->empty() && iscandidate->has_key(key)
64 | continue
65 | endif
66 | citems->add(item)
67 | endfor
68 | return citems
69 | enddef
70 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/tag.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | export var options: dict = {
6 | enable: false,
7 | maxCount: 10,
8 | }
9 |
10 | export def Completor(findstart: number, base: string): any
11 | if findstart == 2
12 | return 1
13 | elseif findstart == 1
14 | var line = getline('.')->strpart(0, col('.') - 1)
15 | var prefix = line->matchstr('\k\+$')
16 | if prefix->empty()
17 | return -2
18 | endif
19 | return col('.') - prefix->len()
20 | endif
21 |
22 | var prefix = base
23 | var taglist = taglist($'^{base}', '%:h'->expand())
24 | if taglist == []
25 | return []
26 | endif
27 | var citems = []
28 | var found = {}
29 | for tag in taglist
30 | if !found->has_key(tag.name)
31 | found[tag.name] = true
32 | citems->add({
33 | word: tag.name,
34 | menu: tag.kind,
35 | info: tag.filename,
36 | kind: util.GetItemKindValue('Tag'),
37 | kind_hlgroup: util.GetKindHighlightGroup('Tag'),
38 | dup: 0,
39 | })
40 | endif
41 | endfor
42 | return citems->slice(0, options.maxCount)
43 | enddef
44 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/tmux.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | export var options: dict = {
6 | enable: false,
7 | dup: false,
8 | maxCount: 10,
9 | scrollCount: 200,
10 | completionMatcher: 'icase', # 'case', 'fuzzy', 'icase'
11 | name: 'tmux',
12 | }
13 |
14 | def ActivePane(): string
15 | var cmd = options.name .. ' list-panes -a -F "#{pane_id}" -f "#{==:#{window_active}#{pane_active},11}" 2>/dev/null'
16 | return system(cmd)->trim()
17 | enddef
18 |
19 | def Panes(exclude_current: bool = false): string
20 | var cmd = $'{options.name} list-panes -a -F "#{{pane_id}}"'
21 | if exclude_current
22 | cmd ..= ' -f "#{!=:#{window_active}#{pane_active},11}"'
23 | endif
24 | cmd ..= ' 2>/dev/null'
25 | var lst = systemlist(cmd)
26 | return lst->join(' ')
27 | enddef
28 |
29 | def CaptureCmd(): string
30 | var cmd = $'{options.name} capture-pane -J -p -S -{options.scrollCount}' # visible lines plus scroll scrollCount lines above
31 | if $TMUX_PANE != null_string # running inside tmux
32 | var cmd_active = $'{cmd} -E -1 -t {ActivePane()}' # scroll scrollCount lines above first line (for active pane)
33 | var panes = Panes(true)
34 | if panes != null_string
35 | return $'sh -c "{cmd_active}; for p in {panes}; do {cmd} -t $p; done 2>/dev/null"'
36 | endif
37 | else
38 | var panes = Panes()
39 | if panes != null_string
40 | return $'sh -c "for p in {panes}; do {cmd} -t $p; done 2>/dev/null"'
41 | endif
42 | endif
43 | return null_string
44 | enddef
45 |
46 | var items = []
47 | var start = reltime()
48 | var status = 0 # 0 not ready, 1 finished, 2 has some intermediate completions
49 | var job: job
50 |
51 | def JobStart()
52 | # ch_logfile('/tmp/channellog', 'w')
53 | # ch_log('BuildItemsList call')
54 | var cmd = CaptureCmd()
55 | if cmd != null_string
56 | job = job_start(cmd, {
57 | out_cb: (ch, str) => {
58 | # out_cb is invoked when channel reads a line; if you don't care
59 | # about intermediate output use close_cb
60 | items->extend(str->split())
61 | if start->reltime()->reltimefloat() * 1000 > 100 # update every 100ms
62 | status = 2
63 | start = reltime()
64 | endif
65 | },
66 | close_cb: (ch) => {
67 | status = 1
68 | },
69 | err_cb: (chan: channel, msg: string) => {
70 | status = 1
71 | echohl ErrorMsg | echoerr $'error: {msg} from {cmd}' | echohl None
72 | },
73 | })
74 | endif
75 | enddef
76 |
77 | export def Completor(findstart: number, base: string): any
78 | if findstart == 1
79 | var line = getline('.')->strpart(0, col('.') - 1)
80 | var prefix = line->matchstr('\k\+$')
81 | if line =~ '\s$' || prefix->empty() || 'tmux'->exepath() == null_string
82 | return -2
83 | endif
84 | items = []
85 | start = reltime()
86 | if job->job_status() ==# 'run'
87 | job->job_stop()
88 | endif
89 | status = 0
90 | JobStart()
91 | return col('.') - prefix->len()
92 | elseif findstart == 2
93 | if status == 2
94 | status = 0
95 | return 2
96 | endif
97 | return status
98 | else
99 | var candidates = []
100 | if options.completionMatcher == 'fuzzy'
101 | candidates = items->matchfuzzy(base)
102 | else
103 | var icase = options.completionMatcher == 'icase'
104 | candidates = items->copy()->filter((_, v) => v !=# base
105 | && ((icase && v->tolower()->stridx(base) == 0) || (!icase && v->stridx(base) == 0)))
106 | endif
107 | candidates = candidates->slice(0, options.maxCount)
108 | var kind = util.GetItemKindValue('Tmux')
109 | var hlkind = util.GetKindHighlightGroup('Tmux')
110 | return candidates->mapnew((_, v) => {
111 | return { word: v, kind: kind, kind_hlgroup: hlkind, dup: (options.dup ? 1 : 0) }
112 | })
113 | endif
114 | enddef
115 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/util.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './options.vim' as opts
4 |
5 | var copts = opts.options
6 |
7 | export def TabEnable()
8 | if !get(g:, 'vimcomplete_tab_enable')
9 | return
10 | endif
11 | silent! iunmap
12 | silent! iunmap
13 | inoremap g:VimCompleteTab() ?? "\"
14 | inoremap g:VimCompleteSTab() ?? "\"
15 | enddef
16 |
17 | export def CREnable()
18 | if !get(g:, 'vimcomplete_cr_enable', 1) || !maparg('', 'i')->empty()
19 | return
20 | endif
21 | # By default, Vim's behavior (using `` or ``) is as follows:
22 | # - If an item is selected, pressing `` accepts the item and inserts
23 | # a newline.
24 | # - If no item is selected, pressing `` dismisses the popup and
25 | # inserts a newline.
26 | # This default behavior occurs when both `noNewlineInCompletion` and
27 | # `noNewlineInCompletionEver` are set to `false` (the default settings).
28 | # - If `noNewlineInCompletion` is `true`, pressing `` accepts the
29 | # completion choice and inserts a newline if an item is selected. If no
30 | # item is selected, it dismisses the popup and does not insert a newline.
31 | # - If `noNewlineInCompletionEver` is `true`, pressing `` will not
32 | # insert a newline, even if an item is selected.
33 | if copts.noNewlineInCompletionEver
34 | inoremap complete_info().selected > -1 ?
35 | \ "\(vimcomplete-skip)\" : "\(vimcomplete-skip)\"
36 | elseif copts.noNewlineInCompletion
37 | inoremap (vimcomplete-skip)
38 | else
39 | inoremap pumvisible() ? "\\" : "\"
40 | endif
41 | enddef
42 |
43 | # when completing word where cursor is in the middle, like xxx|yyy, yyy should
44 | # be hidden while tabbing through menu.
45 | var text_action_save = {
46 | id: -1,
47 | conceallevel: 0,
48 | concealcursor: '',
49 | }
50 |
51 | export def UndoTextAction(concealed: bool = false): string
52 | if text_action_save.id > 0
53 | text_action_save.id->matchdelete()
54 | text_action_save.id = 0
55 | if concealed
56 | &conceallevel = text_action_save.conceallevel
57 | &concealcursor = text_action_save.concealcursor
58 | endif
59 | endif
60 | return ''
61 | enddef
62 |
63 | def TextActionSave(id: number)
64 | text_action_save.id = id
65 | text_action_save.conceallevel = &conceallevel
66 | text_action_save.concealcursor = &concealcursor
67 | enddef
68 |
69 | export def TextAction(conceal: bool = false)
70 | UndoTextAction(conceal)
71 | if v:completed_item->empty()
72 | # CompleteDone is triggered very frequently with empty dict
73 | return
74 | endif
75 | # when cursor is in the middle, say xx|yy (| is cursor) pmenu leaves yy at
76 | # the end after insertion. it looks like xxfooyy. in many cases it is best
77 | # to remove yy.
78 | var line = getline('.')
79 | var curpos = col('.')
80 | var postfix = line->matchstr('^\k\+', curpos - 1)
81 | if postfix != null_string
82 | var newline = line->strpart(0, curpos - 1) .. line->strpart(curpos + postfix->len() - 1)
83 | setline('.', newline)
84 | endif
85 | enddef
86 |
87 | export def TextActionWrapper(): string
88 | if pumvisible() && !v:completed_item->empty()
89 | autocmd CompleteDone ++once TextAction()
90 | feedkeys("\")
91 | endif
92 | return ''
93 | enddef
94 |
95 | export def TextActionPre(conceal: bool = false)
96 | # hide text that is going to be removed by TextAction()
97 | var line = getline('.')
98 | var curpos = col('.')
99 | var postfix = line->matchstr('^\k\+', curpos - 1)
100 | if postfix != null_string && v:event.completed_item->has_key('word')
101 | UndoTextAction(conceal)
102 | if conceal
103 | var id = matchaddpos('Conceal', [[line('.'), curpos, postfix->len()]], 100, -1, {conceal: ''})
104 | if id > 0
105 | TextActionSave(id)
106 | set conceallevel=3
107 | set concealcursor=i
108 | endif
109 | else
110 | var id = matchaddpos('VimCompletePostfix', [[line('.'), curpos, postfix->len()]], 100, -1)
111 | if id > 0
112 | TextActionSave(id)
113 | endif
114 | endif
115 | endif
116 | enddef
117 |
118 | export var info_popup_options = {}
119 |
120 | export def InfoPopupWindowSetOptions()
121 | # the only way to change the look of info window is to set popuphidden,
122 | # subscribe to CompleteChanged, and set the text.
123 | var id = popup_findinfo()
124 | if id > 0
125 | # it is possible to set options only once since info popup window is
126 | # persistent for a buffer, but it'd require caching a buffer local
127 | # variable (setbufvar()). not worth it.
128 | id->popup_setoptions(info_popup_options)
129 | var item = v:event.completed_item
130 | if item->has_key('info') && item.info != ''
131 | # remove null chars (^@) (:h NL-used-for-Nul) by splitting into new lines
132 | id->popup_settext(item.info->split('[[:cntrl:]]'))
133 | id->popup_show()
134 | endif
135 | # setting completeopt back to 'menuone' causes a flicker, so comment out.
136 | # setbufvar(bufnr(), '&completeopt', 'menuone,popup,noinsert,noselect')
137 | # autocmd! VimCompBufAutocmds CompleteChanged
138 | endif
139 | enddef
140 |
141 | export def InfoWindowSendKey(key: string): string
142 | var id = popup_findinfo()
143 | if id > 0
144 | win_execute(id, $'normal! ' .. key)
145 | endif
146 | return ''
147 | enddef
148 |
149 | export def InfoWindowPageUp(): string
150 | return InfoWindowSendKey("\")
151 | enddef
152 |
153 | export def InfoWindowPageDown(): string
154 | return InfoWindowSendKey("\")
155 | enddef
156 |
157 | export def InfoWindowHome(): string
158 | return InfoWindowSendKey("gg")
159 | enddef
160 |
161 | export def InfoWindowEnd(): string
162 | return InfoWindowSendKey("G")
163 | enddef
164 |
165 | export var defaultKindItems = [
166 | [],
167 | ['Text', 't', ""],
168 | ['Method', 'm', ""],
169 | ['Function', 'f', ""],
170 | ['Constructor', 'C', ""],
171 | ['Field', 'F', ""],
172 | ['Variable', 'v', ""],
173 | ['Class', 'c', ""],
174 | ['Interface', 'i', ""],
175 | ['Module', 'M', ""],
176 | ['Property', 'p', ""],
177 | ['Unit', 'u', ""],
178 | ['Value', 'V', ""],
179 | ['Enum', 'e', ""],
180 | ['Keyword', 'k', ""],
181 | ['Snippet', 'S', ""],
182 | ['Color', 'C', ""],
183 | ['File', 'f', ""],
184 | ['Reference', 'r', ""],
185 | ['Folder', 'F', ""],
186 | ['EnumMember', 'E', ""],
187 | ['Constant', 'd', ""],
188 | ['Struct', 's', ""],
189 | ['Event', 'E', ""],
190 | ['Operator', 'o', ""],
191 | ['TypeParameter', 'T', ""],
192 | ['Buffer', 'B', ""],
193 | ['Dictionary', 'D', ""],
194 | ['Word', 'w', ""],
195 | ['Option', 'O', ""],
196 | ['Abbrev', 'a', ""],
197 | ['EnvVariable', 'e', ""],
198 | ['URL', 'U', ""],
199 | ['Command', 'c', ""],
200 | ['Tmux', 'X', ""],
201 | ['Tag', 'G', ""],
202 | ]
203 |
204 | def CreateKindsDict(): dict>
205 | var d = {}
206 | for it in defaultKindItems
207 | if !it->empty()
208 | d[it[0]] = [it[1], it[2]]
209 | endif
210 | endfor
211 | return d
212 | enddef
213 |
214 | export var defaultKinds: dict> = CreateKindsDict()
215 |
216 | # Map LSP (and other) complete item kind to a character/symbol
217 | export def GetItemKindValue(kind: any): string
218 | var kindValue: string
219 | if kind->type() == v:t_number # From LSP
220 | if kind > 26
221 | return ''
222 | endif
223 | kindValue = defaultKindItems[kind][0]
224 | else
225 | kindValue = kind
226 | endif
227 | if copts.customCompletionKinds &&
228 | copts.completionKinds->has_key(kind)
229 | kindValue = copts.completionKinds[kind]
230 | else
231 | if !defaultKinds->has_key(kindValue)
232 | echohl ErrorMsg | echo $"vimcomplete: {kindValue} not found in dict" | echohl None
233 | return ''
234 | endif
235 | if copts.kindDisplayType ==? 'symboltext'
236 | kindValue = $'{defaultKinds[kindValue][0]} {kindValue}'
237 | elseif copts.kindDisplayType ==? 'icon'
238 | kindValue = defaultKinds[kindValue][1]
239 | elseif copts.kindDisplayType ==? 'icontext'
240 | kindValue = $'{defaultKinds[kindValue][1]} {kindValue}'
241 | elseif copts.kindDisplayType !=? 'text'
242 | kindValue = defaultKinds[kindValue][0]
243 | endif
244 | endif
245 | return kindValue
246 | enddef
247 |
248 | export def GetKindHighlightGroup(kind: any): string
249 | var kindValue: string
250 | if kind->type() == v:t_number # From LSP
251 | if kind > 26
252 | return 'PmenuKind'
253 | endif
254 | kindValue = defaultKindItems[kind][0]
255 | else
256 | kindValue = kind
257 | endif
258 | return 'PmenuKind' .. kindValue
259 | enddef
260 |
261 | export def InitKindHighlightGroups()
262 | for k in defaultKinds->keys()
263 | var grp = GetKindHighlightGroup(k)
264 | var tgt = hlget(k)->empty() ? 'PmenuKind' : k
265 | if hlget(grp)->empty()
266 | exec $'highlight! default link {grp} {k}'
267 | endif
268 | endfor
269 | enddef
270 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/vimscript.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | import autoload './util.vim'
4 |
5 | export var options: dict = {
6 | enable: false,
7 | filetypes: ['vim'],
8 | maxCount: 10,
9 | }
10 |
11 | def Prefix(): list
12 | var type = ''
13 | var prefix = ''
14 | var startcol = -1
15 | var line = getline('.')->strpart(0, col('.') - 1)
16 | var MatchStr = (pat) => {
17 | prefix = line->matchstr(pat)
18 | startcol = col('.') - prefix->len()
19 | return prefix != ''
20 | }
21 | var kind = ''
22 | var kindhl = ''
23 | if MatchStr('\v-\>\zs\k+$')
24 | type = 'function'
25 | kind = util.GetItemKindValue('Function')
26 | kindhl = util.GetKindHighlightGroup('Function')
27 | elseif MatchStr('\v(\A+:|^:)\zs\k+$')
28 | type = 'command'
29 | kind = util.GetItemKindValue('Command')
30 | kindhl = util.GetKindHighlightGroup('Command')
31 | elseif MatchStr('\v(\A+\&|^\&)\zs\k+$')
32 | type = 'option'
33 | kind = util.GetItemKindValue('Option')
34 | kindhl = util.GetKindHighlightGroup('Option')
35 | elseif MatchStr('\v(\A+\$|^\$)\zs\k+$')
36 | type = 'environment'
37 | kind = util.GetItemKindValue('EnvVariable')
38 | kindhl = util.GetKindHighlightGroup('EnvVariable')
39 | elseif MatchStr('\v(\A+\zs\a:|^\a:)\k+$')
40 | type = 'var'
41 | kind = util.GetItemKindValue('Variable')
42 | kindhl = util.GetKindHighlightGroup('Variable')
43 | else
44 | # XXX: Following makes vim hang when typing ':cs find g'
45 | # var matches = line->matchlist('\v<(\a+)!{0,1}\s+(\k+)$')
46 | # # autocmd, augroup, highlight, map, etc.
47 | # if matches != [] && matches[1] != '' && matches[2] != ''
48 | # type = 'cmdline'
49 | # prefix = $'{matches[1]} {matches[2]}'
50 | # kind = 'V'
51 | # startcol = col('.') - matches[2]->len()
52 | # var items = prefix->getcompletion(type)
53 | # if items == []
54 | # [prefix, type, kind] = ['', '', '']
55 | # endif
56 | # endif
57 | endif
58 | if type == ''
59 | # last resort, search vimscript reserved words dictionary
60 | if MatchStr('\v\k+$')
61 | type = 'vimdict'
62 | kind = util.GetItemKindValue('Keyword')
63 | kindhl = util.GetItemKindValue('Keyword')
64 | endif
65 | endif
66 | return [prefix, type, kind, kindhl, startcol]
67 | enddef
68 |
69 | var dictwords = []
70 |
71 | def GetDictCompletion(prefix: string): list
72 | if dictwords->empty()
73 | # xxx: Fragile way of getting dictionary file path
74 | var scripts = getscriptinfo({ name: 'vimscript.vim' })
75 | var vpath = scripts->filter((_, v) => v.name =~ 'vimcomplete')
76 | if vpath->empty()
77 | return []
78 | endif
79 | var path = fnamemodify(vpath[0].name, ':p:h:h:h')
80 | var fname = $'{path}/data/vim9.dict'
81 | dictwords = fname->readfile()
82 | endif
83 | return dictwords->copy()->filter((_, v) => v =~? $'\v^{prefix}')
84 | enddef
85 |
86 | export def Completor(findstart: number, base: string): any
87 | if findstart == 2
88 | return 1
89 | endif
90 | var [prefix, type, kind, kindhl, startcol] = Prefix()
91 | if findstart == 1
92 | if type == ''
93 | return -2
94 | endif
95 | return startcol
96 | endif
97 |
98 | var items = type == 'vimdict' ? GetDictCompletion(base) : prefix->getcompletion(type)
99 | items->sort((v1, v2) => v1->len() < v2->len() ? -1 : 1)
100 | items = items->copy()->filter((_, v) => v =~# $'\v^{prefix}') +
101 | items->copy()->filter((_, v) => v !~# $'\v^{prefix}')
102 | var citems = []
103 | for item in items
104 | citems->add({
105 | word: item,
106 | kind: kind,
107 | kind_hl: kindhl,
108 | })
109 | endfor
110 | return citems->slice(0, options.maxCount)
111 | enddef
112 |
--------------------------------------------------------------------------------
/autoload/vimcomplete/vsnip.vim:
--------------------------------------------------------------------------------
1 | vim9script
2 |
3 | # Interface to github.com/hrsh7th/vim-vsnip
4 |
5 | import autoload './util.vim'
6 |
7 | export var options: dict = {
8 | enable: false,
9 | maxCount: 10,
10 | adaptNonKeyword: false,
11 | dup: true,
12 | }
13 |
14 | def Pattern(abbr: string): string
15 | var chars = escape(abbr, '\/?')->split('\zs')
16 | var chars_pattern = '\%(\V' .. chars->join('\m\|\V') .. '\m\)'
17 | var separator = chars[0] =~ '\a' ? '\<' : ''
18 | return $'{separator}\V{chars[0]}\m{chars_pattern}*$'
19 | enddef
20 |
21 | def GetCandidates(line: string): list>
22 | var citems = []
23 | for item in vsnip#get_complete_items(bufnr('%'))
24 | if line->matchstr(Pattern(item.abbr)) == ''
25 | continue
26 | endif
27 | item.kind = util.GetItemKindValue('Snippet')
28 | item.kind_hlgroup = util.GetKindHighlightGroup('Snippet')
29 | citems->add(item)
30 | endfor
31 | return citems
32 | enddef
33 |
34 | def GetItems(): dict
35 | var line = getline('.')->strpart(0, col('.') - 1)
36 | var items = GetCandidates(line)
37 | var prefix = line->matchstr('\S\+$')
38 | if prefix->empty() || items->empty()
39 | return { startcol: -2, items: [] }
40 | endif
41 | var prefixlen = prefix->len()
42 | var filtered = items->copy()->filter((_, v) => v.abbr->strpart(0, prefixlen) ==? prefix)
43 | var startcol = col('.') - prefixlen
44 | var kwprefix = line->matchstr('\k\+$')
45 | var lendiff = prefixlen - kwprefix->len()
46 | if !filtered->empty()
47 | if options.adaptNonKeyword && !kwprefix->empty() && lendiff > 0
48 | # When completing '#if', LSP supplies appropriate completions
49 | # items but without '#'. To mix vsnip and LSP items '#' needs to
50 | # be removed from snippet (in 'user_data') and 'word'
51 | for item in filtered
52 | item.word = item.word->strpart(lendiff)
53 | var user_data = item.user_data->json_decode()
54 | var snippet = user_data.vsnip.snippet
55 | if !snippet->empty()
56 | snippet[0] = snippet[0]->strpart(lendiff)
57 | endif
58 | item.user_data = user_data->json_encode()
59 | endfor
60 | startcol += lendiff
61 | endif
62 | endif
63 | return { startcol: startcol, items: filtered }
64 | enddef
65 |
66 | export def Completor(findstart: number, base: string): any
67 | if findstart == 2
68 | return 1
69 | endif
70 | if !exists('*vsnip#get_complete_items')
71 | return -2
72 | endif
73 | var citems = GetItems()
74 | if findstart == 1
75 | return citems.items->empty() ? -2 : citems.startcol
76 | endif
77 |
78 | citems.items->sort((v1, v2) => {
79 | var w1 = v1.abbr
80 | var w2 = v2.abbr
81 | if w1->len() < w2->len()
82 | return -1
83 | elseif w1->len() == w2->len()
84 | return w1 < w2 ? 1 : -1
85 | else
86 | return 1
87 | endif
88 | })
89 | if options.dup
90 | citems.items->map((_, v) => v->extend({ dup: 1 }))
91 | endif
92 | return citems.items->slice(0, options.maxCount)
93 | enddef
94 |
--------------------------------------------------------------------------------
/data/demo.tape:
--------------------------------------------------------------------------------
1 | # VHS documentation
2 | #
3 | # Output:
4 | # Output .gif Create a GIF output at the given
5 | # Output .mp4 Create an MP4 output at the given
6 | # Output .webm Create a WebM output at the given
7 | #
8 | # Require:
9 | # Require Ensure a program is on the $PATH to proceed
10 | #
11 | # Settings:
12 | # Set FontSize Set the font size of the terminal
13 | # Set FontFamily Set the font family of the terminal
14 | # Set Height Set the height of the terminal
15 | # Set Width Set the width of the terminal
16 | # Set LetterSpacing Set the font letter spacing (tracking)
17 | # Set LineHeight Set the font line height
18 | # Set LoopOffset % Set the starting frame offset for the GIF loop
19 | # Set Theme Set the theme of the terminal
20 | # Set Padding Set the padding of the terminal
21 | # Set Framerate Set the framerate of the recording
22 | # Set PlaybackSpeed Set the playback speed of the recording
23 | # Set MarginFill Set the file or color the margin will be filled with.
24 | # Set Margin Set the size of the margin. Has no effect if MarginFill isn't set.
25 | # Set BorderRadius Set terminal border radius, in pixels.
26 | # Set WindowBar Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
27 | # Set WindowBarSize Set window bar size, in pixels. Default is 40.
28 | # Set TypingSpeed Set the typing speed of the terminal. Default is 50ms.
29 | #
30 | # Sleep:
31 | # Sleep Sleep for a set amount of in seconds
32 | #
33 | # Type:
34 | # Type[@] "" Type into the terminal with a
35 | # delay between each character
36 | #
37 | # Keys:
38 | # Escape[@] [number] Press the Escape key
39 | # Backspace[@] [number] Press the Backspace key
40 | # Delete[@] [number] Press the Delete key
41 | # Insert[@] [number] Press the Insert key
42 | # Down[@] [number] Press the Down key
43 | # Enter[@] [number] Press the Enter key
44 | # Space[@] [number] Press the Space key
45 | # Tab[@] [number] Press the Tab key
46 | # Left[@