├── .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 | ![Demo](https://gist.githubusercontent.com/girishji/40e35cd669626212a9691140de4bd6e7/raw/6041405e45072a7fbc4e352cbd461e450a7af90e/vimcomplete-demo.gif) 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 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