Repository: hrsh7th/nvim-cmp Branch: main Commit: da88697d7f45 Files: 73 Total size: 320.6 KB Directory structure: gitextract_18jfbkey/ ├── .githooks/ │ └── pre-commit ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.yml │ └── workflows/ │ ├── format.yaml │ ├── integration.yaml │ └── release-please.yaml ├── .gitignore ├── .luacheckrc ├── LICENSE ├── Makefile ├── README.md ├── autoload/ │ └── cmp.vim ├── doc/ │ └── cmp.txt ├── init.sh ├── lua/ │ └── cmp/ │ ├── config/ │ │ ├── compare.lua │ │ ├── context.lua │ │ ├── default.lua │ │ ├── mapping.lua │ │ ├── sources.lua │ │ └── window.lua │ ├── config.lua │ ├── context.lua │ ├── context_spec.lua │ ├── core.lua │ ├── core_spec.lua │ ├── entry.lua │ ├── entry_spec.lua │ ├── init.lua │ ├── matcher.lua │ ├── matcher_spec.lua │ ├── source.lua │ ├── source_spec.lua │ ├── types/ │ │ ├── cmp.lua │ │ ├── init.lua │ │ ├── lsp.lua │ │ ├── lsp_spec.lua │ │ └── vim.lua │ ├── utils/ │ │ ├── api.lua │ │ ├── api_spec.lua │ │ ├── async.lua │ │ ├── async_spec.lua │ │ ├── autocmd.lua │ │ ├── binary.lua │ │ ├── binary_spec.lua │ │ ├── buffer.lua │ │ ├── cache.lua │ │ ├── char.lua │ │ ├── debug.lua │ │ ├── event.lua │ │ ├── feedkeys.lua │ │ ├── feedkeys_spec.lua │ │ ├── highlight.lua │ │ ├── keymap.lua │ │ ├── keymap_spec.lua │ │ ├── misc.lua │ │ ├── misc_spec.lua │ │ ├── options.lua │ │ ├── pattern.lua │ │ ├── snippet.lua │ │ ├── spec.lua │ │ ├── str.lua │ │ ├── str_spec.lua │ │ └── window.lua │ ├── view/ │ │ ├── custom_entries_view.lua │ │ ├── docs_view.lua │ │ ├── ghost_text_view.lua │ │ ├── native_entries_view.lua │ │ └── wildmenu_entries_view.lua │ ├── view.lua │ └── vim_source.lua ├── nvim-cmp-scm-1.rockspec ├── plugin/ │ └── cmp.lua ├── stylua.toml └── utils/ └── vimrc.vim ================================================ FILE CONTENTS ================================================ ================================================ FILE: .githooks/pre-commit ================================================ #!/bin/sh DIR="$(dirname $(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )))" cd $DIR make pre-commit for FILE in `git diff --staged --name-only`; do git add $FILE done ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report a problem in nvim-cmp labels: [bug] body: - type: checkboxes id: faq-prerequisite attributes: label: FAQ options: - label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/main/doc/cmp.txt) and it didn't resolve my problem. required: true - type: checkboxes id: announcement-prerequisite attributes: label: Announcement options: - label: I have checked [Breaking change announcement](https://github.com/hrsh7th/nvim-cmp/issues/231). required: true - type: textarea attributes: label: "Minimal reproducible full config" description: | You must provide a working config based on [this](https://github.com/hrsh7th/nvim-cmp/blob/main/utils/vimrc.vim). Not part of config. 1. Copy the base minimal config to the `~/cmp-repro.vim` 2. Edit `~/cmp-repro.vim` for reproducing the issue 3. Open `nvim -u ~/cmp-repro.vim` 4. Check reproduction step value: | ```vim ``` validations: required: true - type: textarea attributes: label: "Description" description: "Describe in detail what happens" validations: required: true - type: textarea attributes: label: "Steps to reproduce" description: "Full reproduction steps. Include a sample file if your issue relates to a specific filetype." validations: required: true - type: textarea attributes: label: "Expected behavior" description: "A description of the behavior you expected." validations: required: true - type: textarea attributes: label: "Actual behavior" description: "A description of the actual behavior." validations: required: true - type: textarea attributes: label: "Additional context" description: "Any other relevant information" ================================================ FILE: .github/workflows/format.yaml ================================================ name: format on: push: branches: - main paths: - '**.lua' jobs: postprocessing: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Format with Stylua uses: JohnnyMorganz/stylua-action@v2 with: token: ${{ secrets.GITHUB_TOKEN }} version: v0.16.1 args: ./lua - uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: "Format with stylua" ================================================ FILE: .github/workflows/integration.yaml ================================================ name: integration on: push: branches: - main pull_request: branches: - main jobs: integration: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Setup neovim uses: rhysd/action-setup-vim@v1 with: version: nightly neovim: true - name: Setup lua uses: leafo/gh-actions-lua@v10 with: luaVersion: "luajit-openresty" - name: Setup luarocks uses: leafo/gh-actions-luarocks@v6 - name: Setup tools shell: bash run: | luarocks install luacheck luarocks install vusted - name: Run tests shell: bash run: make integration ================================================ FILE: .github/workflows/release-please.yaml ================================================ --- permissions: contents: write pull-requests: write name: Release Please on: workflow_dispatch: push: branches: - main jobs: release: name: release runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 with: release-type: simple ================================================ FILE: .gitignore ================================================ doc/tags utils/stylua .DS_Store ================================================ FILE: .luacheckrc ================================================ globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' } max_line_length = false ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 hrsh7th Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: lint lint: luacheck ./lua .PHONY: test test: vusted --output=gtest ./lua .PHONY: pre-commit pre-commit: luacheck lua vusted lua .PHONY: integration integration: luacheck lua vusted lua ================================================ FILE: README.md ================================================ # nvim-cmp A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced". https://github.com/hrsh7th/nvim-cmp/assets/22756295/afa70011-9121-4e42-aedd-0153b630eeab Readme! ==================== 1. There is a GitHub issue that documents [breaking changes](https://github.com/hrsh7th/nvim-cmp/issues/231) for nvim-cmp. Subscribe to the issue to be notified of upcoming breaking changes. 2. This is my hobby project. You can support me via GitHub sponsors. 3. Bug reports are welcome, but don't expect a fix unless you provide minimal configuration and steps to reproduce your issue. 4. The `cmp.mapping.preset.*` is pre-defined configuration that aims to mimic neovim's native like behavior. It can be changed without announcement. Please manage key-mapping by yourself. Concept ==================== - Full support for LSP completion related capabilities - Powerful customizability via Lua functions - Smart handling of key mappings - No flicker Setup ==================== ### Recommended Configuration This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as a snippet plugin. ```vim call plug#begin(s:plug_dir) Plug 'neovim/nvim-lspconfig' Plug 'hrsh7th/cmp-nvim-lsp' Plug 'hrsh7th/cmp-buffer' Plug 'hrsh7th/cmp-path' Plug 'hrsh7th/cmp-cmdline' Plug 'hrsh7th/nvim-cmp' " For vsnip users. Plug 'hrsh7th/cmp-vsnip' Plug 'hrsh7th/vim-vsnip' " For luasnip users. " Plug 'L3MON4D3/LuaSnip' " Plug 'saadparwaiz1/cmp_luasnip' " For mini.snippets users. " Plug 'echasnovski/mini.snippets' " Plug 'abeldekat/cmp-mini-snippets' " For ultisnips users. " Plug 'SirVer/ultisnips' " Plug 'quangnguyen30192/cmp-nvim-ultisnips' " For snippy users. " Plug 'dcampos/nvim-snippy' " Plug 'dcampos/cmp-snippy' call plug#end() lua <'] = cmp.mapping.scroll_docs(-4), [''] = cmp.mapping.scroll_docs(4), [''] = cmp.mapping.complete(), [''] = cmp.mapping.abort(), [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'vsnip' }, -- For vsnip users. -- { name = 'luasnip' }, -- For luasnip users. -- { name = 'ultisnips' }, -- For ultisnips users. -- { name = 'snippy' }, -- For snippy users. }, { { name = 'buffer' }, }) }) -- To use git you need to install the plugin petertriho/cmp-git and uncomment lines below -- Set configuration for specific filetype. --[[ cmp.setup.filetype('gitcommit', { sources = cmp.config.sources({ { name = 'git' }, }, { { name = 'buffer' }, }) }) require("cmp_git").setup() ]]-- -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore). cmp.setup.cmdline({ '/', '?' }, { mapping = cmp.mapping.preset.cmdline(), sources = { { name = 'buffer' } } }) -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). cmp.setup.cmdline(':', { mapping = cmp.mapping.preset.cmdline(), sources = cmp.config.sources({ { name = 'path' } }, { { name = 'cmdline' } }), matching = { disallow_symbol_nonprefix_matching = false } }) -- Set up lspconfig. local capabilities = require('cmp_nvim_lsp').default_capabilities() -- Replace with each lsp server you've enabled. vim.lsp.config('', { capabilities = capabilities }) vim.lsp.enable('') EOF ``` ### Where can I find more completion sources? Have a look at the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) and the `nvim-cmp` [GitHub topic](https://github.com/topics/nvim-cmp). ### Where can I find advanced configuration examples? See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki). ================================================ FILE: autoload/cmp.vim ================================================ let s:bridge_id = 0 let s:sources = {} " " cmp#register_source " function! cmp#register_source(name, source) abort let l:methods = [] for l:method in [ \ 'is_available', \ 'get_debug_name', \ 'get_position_encoding_kind', \ 'get_trigger_characters', \ 'get_keyword_pattern', \ 'complete', \ 'execute', \ 'resolve' \ ] if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func call add(l:methods, l:method) endif endfor let s:bridge_id += 1 let a:source.bridge_id = s:bridge_id let a:source.id = luaeval('require("cmp").register_source(_A[1], require("cmp.vim_source").new(_A[2], _A[3]))', [a:name, s:bridge_id, l:methods]) let s:sources[s:bridge_id] = a:source return a:source.id endfunction " " cmp#unregister_source " function! cmp#unregister_source(id) abort if has_key(s:sources, a:id) unlet s:sources[a:id] endif call luaeval('require("cmp").unregister_source(_A)', a:id) endfunction " " cmp#_method " function! cmp#_method(bridge_id, method, args) abort try let l:source = s:sources[a:bridge_id] if a:method ==# 'is_available' return l:source[a:method]() elseif a:method ==# 'get_debug_name' return l:source[a:method]() elseif a:method ==# 'get_position_encoding_kind' return l:source[a:method](a:args[0]) elseif a:method ==# 'get_keyword_pattern' return l:source[a:method](a:args[0]) elseif a:method ==# 'get_trigger_characters' return l:source[a:method](a:args[0]) elseif a:method ==# 'complete' return l:source[a:method](a:args[0], s:callback(a:args[1])) elseif a:method ==# 'resolve' return l:source[a:method](a:args[0], s:callback(a:args[1])) elseif a:method ==# 'execute' return l:source[a:method](a:args[0], s:callback(a:args[1])) endif catch /.*/ echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) endtry return v:null endfunction " " s:callback " function! s:callback(id) abort return { ... -> luaeval('require("cmp.vim_source").on_callback(_A[1], _A[2])', [a:id, a:000]) } endfunction ================================================ FILE: doc/cmp.txt ================================================ *nvim-cmp* *cmp* A completion plugin for neovim coded in Lua. ============================================================================== CONTENTS *cmp-contents* Abstract |cmp-abstract| Concept |cmp-concept| Usage |cmp-usage| Function |cmp-function| Mapping |cmp-mapping| Command |cmp-command| Highlight |cmp-highlight| FileType |cmp-filetype| Autocmd |cmp-autocmd| Config |cmp-config| Config Helper |cmp-config-helper| Develop |cmp-develop| FAQ |cmp-faq| ============================================================================== Abstract *cmp-abstract* This is nvim-cmp's document. 1. This help file uses the type definition notation like `{lsp,cmp,vim}.*` - You can find it in `../lua/cmp/types/init.lua`. 2. Advanced configuration is described in the wiki. - https://github.com/hrsh7th/nvim-cmp/wiki ============================================================================== Concept *cmp-concept* - Full support for LSP completion related capabilities - Powerful customization abilities via Lua functions - Smart handling of key mappings - No flicker ============================================================================== Usage *cmp-usage* A recommended configuration can be found below. NOTE: 1. You must provide a `snippet.expand` function. 2. `cmp.setup.cmdline` won't work if you use the `native` completion menu. 3. You can disable the `default` options by specifying `cmp.config.disable` value. >vim call plug#begin(s:plug_dir) Plug 'neovim/nvim-lspconfig' Plug 'hrsh7th/cmp-nvim-lsp' Plug 'hrsh7th/cmp-buffer' Plug 'hrsh7th/cmp-path' Plug 'hrsh7th/cmp-cmdline' Plug 'hrsh7th/nvim-cmp' " For vsnip users. Plug 'hrsh7th/cmp-vsnip' Plug 'hrsh7th/vim-vsnip' " For luasnip users. " Plug 'L3MON4D3/LuaSnip' " Plug 'saadparwaiz1/cmp_luasnip' " For mini.snippets users. " Plug 'echasnovski/mini.snippets' " Plug 'abeldekat/cmp-mini-snippets' " For snippy users. " Plug 'dcampos/nvim-snippy' " Plug 'dcampos/cmp-snippy' " For ultisnips users. " Plug 'SirVer/ultisnips' " Plug 'quangnguyen30192/cmp-nvim-ultisnips' call plug#end() set completeopt=menu,menuone,noselect lua <'] = cmp.mapping.scroll_docs(-4), [''] = cmp.mapping.scroll_docs(4), [''] = cmp.mapping.complete(), [''] = cmp.mapping.confirm({ select = true }), }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'vsnip' }, -- For vsnip users. -- { name = 'luasnip' }, -- For luasnip users. -- { name = 'snippy' }, -- For snippy users. -- { name = 'ultisnips' }, -- For ultisnips users. }, { { name = 'buffer' }, }) }) -- `/` cmdline setup. cmp.setup.cmdline('/', { mapping = cmp.mapping.preset.cmdline(), sources = { { name = 'buffer' } } }) -- `:` cmdline setup. cmp.setup.cmdline(':', { mapping = cmp.mapping.preset.cmdline(), sources = cmp.config.sources({ { name = 'path' } }, { { name = 'cmdline' } }), matching = { disallow_symbol_nonprefix_matching = false } }) -- Setup lspconfig. local capabilities = require('cmp_nvim_lsp').default_capabilities() vim.lsp.config(%YOUR_LSP_SERVER%, { capabilities = capabilities }) vim.lsp.enable(%YOUR_LSP_SERVER%) EOF < ============================================================================== Function *cmp-function* NOTE: `lua require('cmp').complete()` can be used to call these functions in a mapping. *cmp.setup* (config: cmp.ConfigSchema) Setup global configuration. See configuration options. *cmp.setup.filetype* (filetype: string, config: cmp.ConfigSchema) Setup filetype-specific configuration. *cmp.setup.buffer* (config: cmp.ConfigSchema) Setup configuration for the current buffer. *cmp.setup.cmdline* (cmdtype: string, config: cmp.ConfigSchema) Setup cmdline configuration for the specific type of command. See |getcmdtype()|. NOTE: nvim-cmp does not support the `=` command type. *cmp.get_registered_sources* () Get all registered sources. *cmp.visible* () Return a boolean showing whether the completion menu is visible or not. *cmp.visible_docs* () Return a boolean showing whether the docs window is visible or not. *cmp.get_entries* () Return all current entries. *cmp.get_selected_entry* () Return currently selected entry (including preselected). *cmp.get_active_entry* () Return currently selected entry (excluding preselected). *cmp.close* () Close the completion menu. *cmp.abort* () Closes the completion menu and restore the current line to the state before the current completion was started. *cmp.select_next_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) Select the next item. Set count with large number to select pagedown. `behavior` can be one of: - `cmp.SelectBehavior.Insert`: Inserts the text at cursor. - `cmp.SelectBehavior.Select`: Only selects the text, potentially adds ghost_text at cursor. >lua cmp.setup { mapping = { [""] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }), } } < *cmp.select_prev_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) Select the previous item. Set count with large number to select pageup. `behavior` can be one of: - `cmp.SelectBehavior.Insert`: Inserts the text at cursor. - `cmp.SelectBehavior.Select`: Only selects the text, potentially adds ghost_text at cursor. >lua cmp.setup { mapping = { [""] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }), } } < *cmp.open_docs* () Open docs view. *cmp.close_docs* () Close docs view. *cmp.scroll_docs* (delta: number) Scroll the documentation window if visible. *cmp.complete* (option: { reason = cmp.ContextReason, config = cmp.ConfigSchema }) Invoke completion. The following configuration defines a key mapping to show completion only for vsnip snippets. >lua cmp.setup { mapping = { [''] = cmp.mapping.complete({ config = { sources = { { name = 'vsnip' } } } }) } } < >vim inoremap lua require('cmp').complete({ config = { sources = { { name = 'vsnip' } } } }) < NOTE: `config` in that case means a temporary setting, but `config.mapping` remains permanent. *cmp.complete_common_string* () Complete common string (similar to shell completion behavior). >lua cmp.setup { mapping = { [''] = cmp.mapping(function(fallback) if cmp.visible() then return cmp.complete_common_string() end fallback() end, { 'i', 'c' }), } } < *cmp.confirm* (option: cmp.ConfirmOption, callback: function) Accepts the currently selected completion item. If you didn't select any item and the option table contains `select = true`, nvim-cmp will automatically select the first item. You can control how the completion item is injected into the file through the `behavior` option: `behavior=cmp.ConfirmBehavior.Insert`: inserts the selected item and moves adjacent text to the right (default). `behavior=cmp.ConfirmBehavior.Replace`: replaces adjacent text with the selected item. >lua cmp.setup { mapping = { [""] = cmp.mapping.confirm({ select = true, behavior = cmp.ConfirmBehavior.Replace }), } } < *cmp.event:on* (%EVENT_NAME%, callback) Subscribe to nvim-cmp's event. Events are listed below. - `complete_done`: emit after current completion is done. - `confirm_done`: emit after confirmation is done. - `menu_opened`: emit after opening a new completion menu. Called with a table holding a key named `window`, pointing to the completion menu implementation. - `menu_closed`: emit after completion menu is closed. Called with a table holding a key named `window`, pointing to the completion menu implementation. ============================================================================== Mapping *cmp-mapping* Nvim-cmp's mapping mechanism is complex but flexible and user-friendly. You can specify a mapping function that receives a `fallback` function as an argument. The `fallback` function can be used to call an existing mapping. For example, typical pair-wise plugins automatically define mappings for `` and `(`. Nvim-cmp will overwrite it if you provide a mapping. To call the existing mapping, you would need to invoke the `fallback` function. >lua cmp.setup { mapping = { [''] = function(fallback) if cmp.visible() then cmp.confirm() else fallback() -- If you use vim-endwise, this fallback will behave the same as vim-endwise. end end } } < >lua cmp.setup { mapping = { [''] = function(fallback) if cmp.visible() then cmp.select_next_item() else fallback() end end } } < It is possible to specify the modes the mapping should be active in (`i` = insert mode, `c` = command mode, `s` = select mode): >lua cmp.setup { mapping = { [''] = cmp.mapping(your_mapping_function, { 'i', 'c' }) } } < You can also specify different mappings for different modes by passing a table: >lua cmp.setup { mapping = { [''] = cmp.mapping({ i = your_mapping_function_a, c = your_mapping_function_b, }) } } < There are also builtin mapping helper functions you can use: *cmp.mapping.close* () Same as |cmp.close|. *cmp.mapping.abort* () Same as |cmp.abort|. *cmp.mapping.select_next_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) Same as |cmp.select_next_item|. *cmp.mapping.select_prev_item* (option: { behavior = cmp.SelectBehavior, count = 1 }) Same as |cmp.select_prev_item|. *cmp.mapping.open_docs* () Same as |cmp.open_docs|. *cmp.mapping.close_docs* () Same as |cmp.close_docs|. *cmp.mapping.scroll_docs* (delta: number) Same as |cmp.scroll_docs|. *cmp.mapping.complete* (option: cmp.CompleteParams) Same as |cmp.complete|. *cmp.mapping.complete_common_string* () Same as |cmp.complete_common_string|. *cmp.mapping.confirm* (option: cmp.ConfirmOption) Same as |cmp.confirm|. Built-in mapping helpers are only available as a configuration option. If you want to call nvim-cmp features directly, please use |cmp-function| instead. ============================================================================== Command *cmp-command* *CmpStatus* Describes statuses and states of sources. Sometimes `unknown` will be printed - this is expected. For example, `cmp-nvim-lsp` registers itself on InsertEnter autocommand so the status will be shown as `unknown` when running the command. ============================================================================== Highlight *cmp-highlight* *CmpItemAbbr* Highlight group for unmatched characters of each completion field. *CmpItemAbbrDeprecated* Highlight group for unmatched characters of each deprecated completion field. *CmpItemAbbrMatch* Highlight group for matched characters of each completion field. Matched characters must form a substring of a field which share a starting position. *CmpItemAbbrMatchFuzzy* Highlight group for fuzzy-matched characters of each completion field. *CmpItemKind* Highlight group for the kind of the field. NOTE: `kind` is a symbol after each completion option. *CmpItemKindIcon Highlight group for the icons used for each `lsp.CompletionItemKind`. *CmpItemKind%KIND_NAME%* Highlight group for the kind of the field for a specific `lsp.CompletionItemKind`. If you only want to overwrite the `method` kind's highlight group, you can do this: >vim highlight CmpItemKindMethod guibg=NONE guifg=Orange *CmpItemKind%KIND_NAME%Icon Highlight group for the icon shown for a specific `lsp.CompletionItemKind`. Can be overriden the same way as shown above OR with a custom function in your format table (e.g.): local function set_colors(str) local color = vim.api.nvim_get_hl(args...) vim.api.nvim_set_hl(args...) end < *CmpItemMenu* The menu field's highlight group. ============================================================================== FileType *cmp-filetype* *cmp_menu* The completion menu buffer's filetype. *cmp_docs* The documentation window buffer's filetype. ============================================================================== Autocmd *cmp-autocmd* You can create custom autocommands for certain nvim-cmp events by defining autocommands for the User event with the following patterns: *CmpReady* Invoked when nvim-cmp gets sourced from `plugin/cmp.lua`. *CmpRegisterSource* Invoke when source was registered. *CmpUnregisterSource* Invoke when source was un-registered. ============================================================================== Config *cmp-config* You can use the following options via `cmp.setup { ... }` . *cmp-config.enabled* enabled~ `boolean | fun(): boolean` Toggles the plugin on and off. *cmp-config.performance.debounce* performance.debounce~ `number` Sets debounce time This is the interval used to group up completions from different sources for filtering and displaying. *cmp-config.performance.throttle* performance.throttle~ `number` Sets throttle time This is used to delay filtering and displaying completions. *cmp-config.performance.fetching_timeout* performance.fetching_timeout~ `number` Sets the timeout of candidate fetching process. The nvim-cmp will wait to display the most prioritized source. *cmp-config.performance.filtering_context_budget* performance.filtering_context_budget~ `number` Sets the filtering context budget in ms. If filtering takes longer than this, it will be deferred. *cmp-config.performance.confirm_resolve_timeout* performance.confirm_resolve_timeout~ `number` Sets the timeout for resolving item before confirmation. *cmp-config.performance.async_budget* performance.async_budget~ `number` Maximum time (in ms) an async function is allowed to run during one step of the event loop. *cmp-config.performance.max_view_entries* performance.max_view_entries~ `number` Maximum number of items to show in the entries list. *cmp-config.preselect* preselect~ `cmp.PreselectMode` 1. `cmp.PreselectMode.Item` nvim-cmp will preselect the item that the source specified. 2. `cmp.PreselectMode.None` nvim-cmp will not preselect any items. *cmp-config.mapping* mapping~ `tablelua final_score = orig_score + ((#sources - (source_index - 1)) * sorting.priority_weight) < *cmp-config.sorting.comparators* sorting.comparators~ `(fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil)[]` The function to customize the sorting behavior. You can use built-in comparators via `cmp.config.compare.*`. *cmp-config.sources* sources~ `cmp.SourceConfig[]` List of the sources and their configurations to use. The order of the sources determines their order in the completion results. *cmp-config.sources[n].name* sources[n].name~ `string` The name of the source. *cmp-config.sources[n].option* sources[n].option~ `table` Any specific options defined by the source itself. *cmp-config.sources[n].keyword_length* sources[n].keyword_length~ `number` The source-specific keyword length to trigger auto completion. *cmp-config.sources[n].keyword_pattern* sources[n].keyword_pattern~ `string` The source-specific keyword pattern. *cmp-config.sources[n].trigger_characters* sources[n].trigger_characters~ `string[]` A source-specific keyword pattern. *cmp-config.sources[n].priority* sources[n].priority~ `number` The source-specific priority value. *cmp-config.sources[n].max_item_count* sources[n].max_item_count~ `number` The source-specific maximum item count option Note: This is applied before sorting, so items that aren't well-matched may be selected. *cmp-config.sources[n].group_index* sources[n].group_index~ `number` The source group index. For instance, you can set the `buffer`'s source `group_index` to a larger number if you don't want to see `buffer` source items while `nvim-lsp` source is available: >lua cmp.setup { sources = { { name = 'nvim_lsp', group_index = 1 }, { name = 'buffer', group_index = 2 }, } } < You can also achieve this by using the built-in configuration helper like this: >lua cmp.setup { sources = cmp.config.sources({ { name = 'nvim_lsp' }, }, { { name = 'buffer' }, }) } < *cmp-config.sources[n].entry_filter* sources[n].entry_filter~ `function` A source-specific entry filter, with the following function signature: > function(entry: cmp.Entry, ctx: cmp.Context): boolean < Returning `true` will keep the entry, while returning `false` will remove it. This can be used to hide certain entries from a given source. For instance, you could hide all entries with kind `Text` from the `nvim_lsp` filter using the following source definition: >lua { name = 'nvim_lsp', entry_filter = function(entry, ctx) return require('cmp.types').lsp.CompletionItemKind[entry:get_kind()] ~= 'Text' end } < Using the `ctx` parameter, you can further customize the behaviour of the source. *cmp-config.view* view~ `{ docs: cmp.DocsViewConfig }` `{ entries: cmp.EntriesViewConfig|string }` The view class used to customize nvim-cmp's appearance. Currently available configuration options are: *cmp-config.view.docs.auto_open* view.docs.auto_open~ `boolean` Specify whether to show the docs_view when selecting an item. *cmp-config.view.entries.selection_order* view.entries.selection_order~ `string` Specify whether to select the option in the pmenu that is at the top (`top_down`) or nearest to the cursor (`near_cursor`). Useful if pmenu is above cursor and you want to change default selection direction. Custom view only. `top_down` by default. *cmp-config.view.entries.follow_cursor* view.entries.follow_cursor~ `boolean` Specify whether the pmenu should follow the current position of the cursor as the user types. Custom view only. `false` by default. *cmp-config.window.{completion,documentation}.border* window.{completion,documentation}.border~ `string | string[] | nil` Border characters used for the completion popup menu when |experimental.native_menu| is disabled. See |nvim_open_win|. *cmp-config.window.{completion,documentation}.winhighlight* window.{completion,documentation}.winhighlight~ `string | cmp.WinhighlightConfig` Specify the window's winhighlight option. See |nvim_open_win|. *cmp-config.window.{completion,documentation}.winblend* window.{completion,documentation}.winblend~ `string | cmp.WinhighlightConfig` Specify the window's winblend option. See |nvim_open_win|. *cmp-config.window.{completion,documentation}.zindex* window.{completion,documentation}.zindex~ `number` The completion window's zindex. See |nvim_open_win|. *cmp-config.window.{completion,documentation}.scrolloff* window.completion.scrolloff~ `number` Specify the window's scrolloff option. See |'scrolloff'|. *cmp-config.window.{completion,documentation}.col_offset* window.completion.col_offset~ `number` Offsets the completion window relative to the cursor. Offsets the documentation window relative to the completion window. *cmp-config.window.completion.side_padding* window.completion.side_padding~ `number` The amount of padding to add on the completion window's sides *cmp-config.window.completion.scrollbar* window.completion.scrollbar~ `boolean` Whether the scrollbar should be enabled if there are more items that fit *cmp-config.window.completion.max_height* window.completion.max_height~ `number | nil` The completion window's max height, can be set to 0 to use all available space. To use `vim.o.pumheight` set this to `nil`. See |'pumheight'|. *cmp-config.window.documentation.max_width* window.documentation.max_width~ `number` The documentation window's max width, can be set to 0 to use all available space. *cmp-config.window.documentation.max_height* window.documentation.max_height~ `number` The documentation window's max height, can be set to 0 to use all available space. *cmp-config.experimental.ghost_text* experimental.ghost_text~ `boolean | { hl_group = string }` Whether to enable the ghost_text feature. ============================================================================== Config Helper *cmp-config-helper* You can use the following configuration helpers: cmp.config.compare~ TBD cmp.config.context~ The `cmp.config.context` can be used for context-aware completion toggling. >lua cmp.setup { enabled = function() -- disable completion if the cursor is `Comment` syntax group. return not cmp.config.context.in_syntax_group('Comment') end } < *cmp.config.context.in_syntax_group* (group) You can specify the vim's built-in syntax group. If you use tree-sitter, you should use `cmp.config.context.in_treesitter_capture` instead. *cmp.config.context.in_treesitter_capture* (capture) You can specify the treesitter capture name. If you don't use the `nvim-treesitter` plugin, this helper will not work correctly. cmp.config.mapping~ See |cmp-mapping|. cmp.config.sources~ *cmp.config.sources* (...sources) You can specify multiple source arrays. The sources are grouped in the order you specify, and the groups are displayed as a fallback, like chain completion. >lua cmp.setup { sources = cmp.config.sources({ { name = 'nvim_lsp' }, }, { { name = 'buffer' }, }) } < cmp.config.window~ *cmp.config.window.bordered* (option) Make the completion window `bordered`. The option is described in `cmp.ConfigSchema`. >lua cmp.setup { window = { completion = cmp.config.window.bordered(), documentation = cmp.config.window.bordered(), } } < ============================================================================== Develop *cmp-develop* Creating a custom source~ NOTE: 1. The `complete` method is required. Others can be omitted. 2. The `callback` function must always be called. 3. You can use only `require('cmp')` in custom source. 4. If the LSP spec was changed, nvim-cmp may implement it without any announcement (potentially introducing breaking changes). 5. You should read ./lua/cmp/types and https://microsoft.github.io/language-server-protocol/specifications/specification-current. 6. Please add your source to the list of sources in the Wiki (https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) and if you publish it on GitHub, add the `nvim-cmp` topic so users can find it more easily. Here is an example on how to create a custom source: >lua local source = {} ---Return whether this source is available in the current context or not (optional). ---@return boolean function source:is_available() return true end ---Return the debug name of this source (optional). ---@return string function source:get_debug_name() return 'debug name' end ---Return LSP's PositionEncodingKind. ---@NOTE: If this method is omitted, the default value will be `utf-16`. ---@return lsp.PositionEncodingKind function source:get_position_encoding_kind() return 'utf-16' end ---Return the keyword pattern for triggering completion (optional). ---If this is omitted, nvim-cmp will use a default keyword pattern. See |cmp-config.completion.keyword_pattern|. ---@return string function source:get_keyword_pattern() return [[\k\+]] end ---Return trigger characters for triggering completion (optional). function source:get_trigger_characters() return { '.' } end ---Invoke completion (required). ---@param params cmp.SourceCompletionApiParams ---@param callback fun(response: lsp.CompletionResponse|nil) function source:complete(params, callback) callback({ { label = 'January' }, { label = 'February' }, { label = 'March' }, { label = 'April' }, { label = 'May' }, { label = 'June' }, { label = 'July' }, { label = 'August' }, { label = 'September' }, { label = 'October' }, { label = 'November' }, { label = 'December' }, }) end ---Resolve completion item (optional). This is called right before the completion is about to be displayed. ---Useful for setting the text shown in the documentation window (`completion_item.documentation`). ---@param completion_item lsp.CompletionItem ---@param callback fun(completion_item: lsp.CompletionItem|nil) function source:resolve(completion_item, callback) callback(completion_item) end ---Executed after the item was selected. ---@param completion_item lsp.CompletionItem ---@param callback fun(completion_item: lsp.CompletionItem|nil) function source:execute(completion_item, callback) callback(completion_item) end ---Register your source to nvim-cmp. require('cmp').register_source('month', source) < ============================================================================== FAQ *cmp-faq* Why does cmp automatically select a particular item? ~ How to disable the preselect feature? ~ Nvim-cmp respects the LSP (Language Server Protocol) specification. The LSP spec defines the `preselect` feature for completion. You can disable the `preselect` feature like this: >lua cmp.setup { preselect = cmp.PreselectMode.None } < How to disable only specific language-server's completion?~ You can disable `completionProvider` in lspconfig configuration. >lua lspconfig[%SERVER_NAME%].setup { on_attach = function(client) client.server_capabilities.completionProvider = false end } < How to disable commitCharacters?~ You can disable the commitCharacters feature (which is defined in LSP spec): >lua cmp.setup { confirmation = { get_commit_characters = function(commit_characters) return {} end } } < How to disable automatic display of docs view?~ You can add the `view.docs.auto_open = false` for configuration. >lua cmp.setup { ... view = { docs = { auto_open = false } } ... } < additionally, if you want to open/close docs view via your key mapping, you can define keymapping as the following. >lua cmp.setup { ... mapping = { [''] = function() if cmp.visible_docs() then cmp.close_docs() else cmp.open_docs() end end } ... } < How to disable auto-completion?~ How to use nvim-cmp as omnifunc?~ You can disable auto-completion like this: >lua cmp.setup { ... completion = { autocomplete = false } ... } < Then you will need to invoke completion manually. >vim inoremap lua require('cmp').complete() < How to disable nvim-cmp for a specific buffer?~ How to setup nvim-cmp for a specific buffer?~ You can setup buffer-specific configuration like this: >lua cmp.setup.filetype({ 'markdown', 'help' }, { sources = { { name = 'path' }, { name = 'buffer' }, } }) < How to disable the documentation window?~ Simply use the following config: >lua cmp.setup.filetype({ 'markdown', 'help' }, { window = { documentation = cmp.config.disable } }) < I'm using clangd. The menu items are mis-indented.~ It's caused by clangd. You can specify `--header-insertion-decorators` for clangd's command-line arguments. See #999. How to integrate with copilot.vim?~ Copilot.vim and nvim-cmp both have a `key-mapping fallback` mechanism. Therefore, you should manage those plugins by yourself. Fortunately, the copilot.vim has a feature that disables the fallback mechanism. >vim let g:copilot_no_tab_map = v:true imap (vimrc:copilot-dummy-map) copilot#Accept("\") < You can manage copilot.vim's accept feature inside nvim-cmp's key-mapping function: >lua cmp.setup { mapping = { [''] = cmp.mapping(function(fallback) vim.api.nvim_feedkeys(vim.fn['copilot#Accept'](vim.api.nvim_replace_termcodes('', true, true, true)), 'n', true) end) }, experimental = { ghost_text = false -- this feature conflict with copilot.vim's preview. } } < nvim-cmp does not work as expected.~ There are some known issues. Please check the following. - nvim-cmp does not work with `set paste` option. - Command line mode key mapping is unified regardless of `:`, `/`, `?`. Therefore, it is impossible to apply the mapping only to `:`. How to customize the menu appearance?~ Have a look at the wiki (https://github.com/hrsh7th/nvim-cmp/wiki). ============================================================================== vim:tw=78:ts=2:et:ft=help:norl: ================================================ FILE: init.sh ================================================ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" rm $DIR/.git/hooks/* cp $DIR/.githooks/* $DIR/.git/hooks/ chmod 755 $DIR/.git/hooks/* ================================================ FILE: lua/cmp/config/compare.lua ================================================ local types = require('cmp.types') local cache = require('cmp.utils.cache') ---@type cmp.Comparator[] local compare = {} --- Comparators (:help cmp-config.sorting.comparators) should return --- true when the first entry should come EARLIER (i.e., higher ranking) than the second entry, --- or nil if no pairwise ordering preference from the comparator. --- See also :help table.sort() and cmp.view.open() to see how comparators are used. ---@class cmp.ComparatorFunctor ---@overload fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil ---@alias cmp.ComparatorFunction fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil ---@alias cmp.Comparator cmp.ComparatorFunction | cmp.ComparatorFunctor ---offset: Entries with smaller offset will be ranked higher. ---@type cmp.ComparatorFunction compare.offset = function(entry1, entry2) local diff = entry1.offset - entry2.offset if diff < 0 then return true elseif diff > 0 then return false end return nil end ---exact: Entries with exact == true will be ranked higher. ---@type cmp.ComparatorFunction compare.exact = function(entry1, entry2) if entry1.exact ~= entry2.exact then return entry1.exact end return nil end ---score: Entries with higher score will be ranked higher. ---@type cmp.ComparatorFunction compare.score = function(entry1, entry2) local diff = entry2.score - entry1.score if diff < 0 then return true elseif diff > 0 then return false end return nil end ---recently_used: Entries that are used recently will be ranked higher. ---@type cmp.ComparatorFunctor compare.recently_used = setmetatable({ records = {}, add_entry = function(self, e) self.records[e.completion_item.label] = vim.loop.now() end, }, { ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil __call = function(self, entry1, entry2) local t1 = self.records[entry1.completion_item.label] or -1 local t2 = self.records[entry2.completion_item.label] or -1 if t1 ~= t2 then return t1 > t2 end return nil end, }) ---kind: Entries with smaller ordinal value of 'kind' will be ranked higher. ---(see lsp.CompletionItemKind enum). ---Exceptions are that Text(1) will be ranked the lowest, and snippets be the highest. ---@type cmp.ComparatorFunction compare.kind = function(entry1, entry2) local kind1 = entry1:get_kind() --- @type lsp.CompletionItemKind | number local kind2 = entry2:get_kind() --- @type lsp.CompletionItemKind | number kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1 kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2 if kind1 ~= kind2 then if kind1 == types.lsp.CompletionItemKind.Snippet then return true end if kind2 == types.lsp.CompletionItemKind.Snippet then return false end local diff = kind1 - kind2 if diff < 0 then return true elseif diff > 0 then return false end end return nil end ---sort_text: Entries will be ranked according to the lexicographical order of sortText. ---@type cmp.ComparatorFunction compare.sort_text = function(entry1, entry2) if entry1.completion_item.sortText and entry2.completion_item.sortText then local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText) if diff < 0 then return true elseif diff > 0 then return false end end return nil end ---length: Entries with shorter label length will be ranked higher. ---@type cmp.ComparatorFunction compare.length = function(entry1, entry2) local diff = #entry1.completion_item.label - #entry2.completion_item.label if diff < 0 then return true elseif diff > 0 then return false end return nil end ----order: Entries with smaller id will be ranked higher. ---@type fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil compare.order = function(entry1, entry2) local diff = entry1.id - entry2.id if diff < 0 then return true elseif diff > 0 then return false end return nil end ---locality: Entries with higher locality (i.e., words that are closer to the cursor) ---will be ranked higher. See GH-183 for more details. ---@type cmp.ComparatorFunctor compare.locality = setmetatable({ lines_count = 10, lines_cache = cache.new(), locality_map = {}, update = function(self) local config = require('cmp').get_config() if not vim.tbl_contains(config.sorting.comparators, compare.locality) then return end local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 local max = vim.api.nvim_buf_line_count(buf) if self.lines_cache:get('buf') ~= buf then self.lines_cache:clear() self.lines_cache:set('buf', buf) end self.locality_map = {} for i = math.max(0, cursor_row - self.lines_count), math.min(max, cursor_row + self.lines_count) do local is_above = i < cursor_row local buffer = vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or '' local locality_map = self.lines_cache:ensure({ 'line', buffer }, function() local locality_map = {} local regexp = vim.regex(config.completion.keyword_pattern) -- the buffer length check is to avoid performance issues on very long lines, #1841 while buffer ~= '' and #buffer < 5000 do local s, e = regexp:match_str(buffer) if s and e then local w = string.sub(buffer, s + 1, e) local d = math.abs(i - cursor_row) - (is_above and 1 or 0) locality_map[w] = math.min(locality_map[w] or math.huge, d) buffer = string.sub(buffer, e + 1) else break end end return locality_map end) for w, d in pairs(locality_map) do self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row)) end end end, }, { ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil __call = function(self, entry1, entry2) local local1 = self.locality_map[entry1.word] local local2 = self.locality_map[entry2.word] if local1 ~= local2 then if local1 == nil then return false end if local2 == nil then return true end return local1 < local2 end return nil end, }) ---scopes: Entries defined in a closer scope will be ranked higher (e.g., prefer local variables to globals). ---@type cmp.ComparatorFunctor compare.scopes = setmetatable({ scopes_map = {}, update = function(self) local config = require('cmp').get_config() if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then return end local ok, locals = pcall(require, 'nvim-treesitter.locals') if ok then local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 -- Cursor scope. local cursor_scope = nil -- Prioritize the older get_scopes method from nvim-treesitter `master` over get from `main` local scopes = locals.get_scopes and locals.get_scopes(buf) or select(3, locals.get(buf)) for _, scope in ipairs(scopes) do if scope:start() <= cursor_row and cursor_row <= scope:end_() then if not cursor_scope then cursor_scope = scope else if cursor_scope:start() <= scope:start() and scope:end_() <= cursor_scope:end_() then cursor_scope = scope end end elseif cursor_scope and cursor_scope:end_() <= scope:start() then break end end -- Definitions. local definitions = locals.get_definitions_lookup_table(buf) -- Narrow definitions. local depth = 0 for scope in locals.iter_scope_tree(cursor_scope, buf) do local s, e = scope:start(), scope:end_() -- Check scope's direct child. for _, definition in pairs(definitions) do if s <= definition.node:start() and definition.node:end_() <= e then if scope:id() == locals.containing_scope(definition.node, buf):id() then local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text local text = get_node_text(definition.node, buf) or '' if not self.scopes_map[text] then self.scopes_map[text] = depth end end end end depth = depth + 1 end end end, }, { ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil __call = function(self, entry1, entry2) local local1 = self.scopes_map[entry1.word] local local2 = self.scopes_map[entry2.word] if local1 ~= local2 then if local1 == nil then return false end if local2 == nil then return true end return local1 < local2 end end, }) return compare ================================================ FILE: lua/cmp/config/context.lua ================================================ local api = require('cmp.utils.api') local context = {} ---Check if cursor is in syntax group ---@param group string | []string ---@return boolean context.in_syntax_group = function(group) local row, col = unpack(vim.api.nvim_win_get_cursor(0)) if not api.is_insert_mode() then col = col + 1 end for _, syn_id in ipairs(vim.fn.synstack(row, col)) do syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links local g = vim.fn.synIDattr(syn_id, 'name') if type(group) == 'string' and g == group then return true elseif type(group) == 'table' and vim.tbl_contains(group, g) then return true end end return false end ---Check if cursor is in treesitter capture ---@param capture string | []string ---@return boolean context.in_treesitter_capture = function(capture) local buf = vim.api.nvim_get_current_buf() local row, col = unpack(vim.api.nvim_win_get_cursor(0)) row = row - 1 if vim.api.nvim_get_mode().mode == 'i' then col = col - 1 end local get_captures_at_pos = -- See neovim/neovim#20331 require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8 local captures_at_cursor = vim.tbl_map(function(x) return x.capture end, get_captures_at_pos(buf, row, col)) if vim.tbl_isempty(captures_at_cursor) then return false elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then return true elseif type(capture) == 'table' then for _, v in ipairs(capture) do if vim.tbl_contains(captures_at_cursor, v) then return true end end end return false end return context ================================================ FILE: lua/cmp/config/default.lua ================================================ local compare = require('cmp.config.compare') local types = require('cmp.types') local window = require('cmp.config.window') local WIDE_HEIGHT = 40 ---@return cmp.ConfigSchema return function() ---@type cmp.ConfigSchema local config = { enabled = function() local disabled = false disabled = disabled or (vim.api.nvim_get_option_value('buftype', { buf = 0 }) == 'prompt') disabled = disabled or (vim.fn.reg_recording() ~= '') disabled = disabled or (vim.fn.reg_executing() ~= '') return not disabled end, performance = { debounce = 60, throttle = 30, fetching_timeout = 500, filtering_context_budget = 3, confirm_resolve_timeout = 80, async_budget = 1, max_view_entries = 200, }, preselect = types.cmp.PreselectMode.Item, mapping = {}, snippet = { expand = vim.fn.has('nvim-0.10') == 1 and function(args) vim.snippet.expand(args.body) end or function(_) error('snippet engine is not configured.') end, }, completion = { autocomplete = { types.cmp.TriggerEvent.TextChanged, }, completeopt = 'menu,menuone,noselect', keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], keyword_length = 1, }, formatting = { expandable_indicator = true, fields = { 'abbr', 'icon', 'kind', 'menu' }, format = function(_, vim_item) return vim_item end, }, matching = { disallow_fuzzy_matching = false, disallow_fullfuzzy_matching = false, disallow_partial_fuzzy_matching = true, disallow_partial_matching = false, disallow_prefix_unmatching = false, disallow_symbol_nonprefix_matching = true, }, sorting = { priority_weight = 2, comparators = { compare.offset, compare.exact, -- compare.scopes, compare.score, compare.recently_used, compare.locality, compare.kind, compare.sort_text, compare.length, compare.order, }, }, sources = {}, confirmation = { default_behavior = types.cmp.ConfirmBehavior.Insert, get_commit_characters = function(commit_characters) return commit_characters end, }, event = {}, experimental = { ghost_text = false, }, view = { entries = { name = 'custom', selection_order = 'top_down', vertical_positioning = 'below', follow_cursor = false, }, docs = { auto_open = true, }, }, window = { completion = { border = window.get_border(), winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None', winblend = vim.o.pumblend, scrolloff = 0, col_offset = 0, side_padding = 1, scrollbar = true, }, documentation = { max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), border = window.get_border(), winhighlight = 'FloatBorder:NormalFloat', winblend = vim.o.pumblend, col_offset = 0, }, }, } return config end ================================================ FILE: lua/cmp/config/mapping.lua ================================================ local types = require('cmp.types') local misc = require('cmp.utils.misc') local keymap = require('cmp.utils.keymap') local function merge_keymaps(base, override) local normalized_base = {} for k, v in pairs(base) do normalized_base[keymap.normalize(k)] = v end local normalized_override = {} for k, v in pairs(override) do normalized_override[keymap.normalize(k)] = v end return misc.merge(normalized_base, normalized_override) end local mapping = setmetatable({}, { __call = function(_, invoke, modes) if type(invoke) == 'function' then local map = {} for _, mode in ipairs(modes or { 'i' }) do map[mode] = invoke end return map end return invoke end, }) ---Mapping preset configuration. mapping.preset = {} ---Mapping preset insert-mode configuration. mapping.preset.insert = function(override) return merge_keymaps(override or {}, { [''] = { i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), }, [''] = { i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }), }, [''] = { i = function() local cmp = require('cmp') if cmp.visible() then cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }) else cmp.complete() end end, }, [''] = { i = function() local cmp = require('cmp') if cmp.visible() then cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }) else cmp.complete() end end, }, [''] = { i = mapping.confirm({ select = false }), }, [''] = { i = mapping.abort(), }, }) end ---Mapping preset cmdline-mode configuration. mapping.preset.cmdline = function(override) return merge_keymaps(override or {}, { [''] = { c = function() local cmp = require('cmp') if cmp.visible() then cmp.select_next_item() else cmp.complete() end end, }, [''] = { c = function() local cmp = require('cmp') if cmp.visible() then cmp.select_next_item() else cmp.complete() end end, }, [''] = { c = function() local cmp = require('cmp') if cmp.visible() then cmp.select_prev_item() else cmp.complete() end end, }, [''] = { c = function(fallback) local cmp = require('cmp') if cmp.visible() then cmp.select_next_item() else fallback() end end, }, [''] = { c = function(fallback) local cmp = require('cmp') if cmp.visible() then cmp.select_prev_item() else fallback() end end, }, [''] = { c = mapping.abort(), }, [''] = { c = mapping.confirm({ select = false }), }, }) end ---Invoke completion ---@param option? cmp.CompleteParams mapping.complete = function(option) return function(fallback) if not require('cmp').complete(option) then fallback() end end end ---Complete common string. mapping.complete_common_string = function() return function(fallback) if not require('cmp').complete_common_string() then fallback() end end end ---Close current completion menu if it displayed. mapping.close = function() return function(fallback) if not require('cmp').close() then fallback() end end end ---Abort current completion menu if it displayed. mapping.abort = function() return function(fallback) if not require('cmp').abort() then fallback() end end end ---Scroll documentation window. mapping.scroll_docs = function(delta) return function(fallback) if not require('cmp').scroll_docs(delta) then fallback() end end end --- Opens the documentation window. mapping.open_docs = function() return function(fallback) if not require('cmp').open_docs() then fallback() end end end --- Close the documentation window. mapping.close_docs = function() return function(fallback) if not require('cmp').close_docs() then fallback() end end end ---Select next completion item. mapping.select_next_item = function(option) return function(fallback) if not require('cmp').select_next_item(option) then local release = require('cmp').core:suspend() fallback() vim.schedule(release) end end end ---Select prev completion item. mapping.select_prev_item = function(option) return function(fallback) if not require('cmp').select_prev_item(option) then local release = require('cmp').core:suspend() fallback() vim.schedule(release) end end end ---Confirm selection mapping.confirm = function(option) return function(fallback) if not require('cmp').confirm(option) then fallback() end end end return mapping ================================================ FILE: lua/cmp/config/sources.lua ================================================ return function(...) local sources = {} for i, group in ipairs({ ... }) do for _, source in ipairs(group) do source.group_index = i table.insert(sources, source) end end return sources end ================================================ FILE: lua/cmp/config/window.lua ================================================ local window = {} window.bordered = function(opts) opts = opts or {} return { border = opts.border or window.get_border(), winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:FloatBorder,CursorLine:Visual,Search:None', zindex = opts.zindex or 1001, scrolloff = opts.scrolloff or 0, col_offset = opts.col_offset or 0, side_padding = opts.side_padding or 1, scrollbar = opts.scrollbar == nil or opts.scrollbar, max_height = opts.max_height or nil, } end window.get_border = function() -- On neovim 0.11+, use the vim.o.winborder option by default local has_winborder, winborder = pcall(function() return vim.o.winborder end) if has_winborder and winborder ~= '' then return winborder end -- On lower versions return the default return 'none' end return window ================================================ FILE: lua/cmp/config.lua ================================================ local mapping = require('cmp.config.mapping') local cache = require('cmp.utils.cache') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local api = require('cmp.utils.api') ---@class cmp.Config ---@field public g cmp.ConfigSchema local config = {} ---@type cmp.Cache config.cache = cache.new() ---@type cmp.ConfigSchema config.global = require('cmp.config.default')() ---@type table config.buffers = {} ---@type table config.filetypes = {} ---@type table config.cmdline = {} ---@type cmp.ConfigSchema config.onetime = {} ---Set configuration for global. ---@param c cmp.ConfigSchema config.set_global = function(c) config.global = config.normalize(misc.merge(c, config.global)) config.global.revision = config.global.revision or 1 config.global.revision = config.global.revision + 1 end ---Set configuration for buffer ---@param c cmp.ConfigSchema ---@param bufnr integer config.set_buffer = function(c, bufnr) local revision = (config.buffers[bufnr] or {}).revision or 1 config.buffers[bufnr] = c or {} config.buffers[bufnr].revision = revision + 1 end ---Set configuration for filetype ---@param c cmp.ConfigSchema ---@param filetypes string[]|string config.set_filetype = function(c, filetypes) for _, filetype in ipairs(type(filetypes) == 'table' and filetypes or { filetypes }) do local revision = (config.filetypes[filetype] or {}).revision or 1 config.filetypes[filetype] = c or {} config.filetypes[filetype].revision = revision + 1 end end ---Set configuration for cmdline ---@param c cmp.ConfigSchema ---@param cmdtypes string|string[] config.set_cmdline = function(c, cmdtypes) for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do local revision = (config.cmdline[cmdtype] or {}).revision or 1 config.cmdline[cmdtype] = c or {} config.cmdline[cmdtype].revision = revision + 1 end end ---Set configuration as oneshot completion. ---@param c cmp.ConfigSchema config.set_onetime = function(c) local revision = (config.onetime or {}).revision or 1 config.onetime = c or {} config.onetime.revision = revision + 1 end ---@return cmp.ConfigSchema config.get = function() local global_config = config.global -- The config object already has `revision` key. if #vim.tbl_keys(config.onetime) > 1 then local onetime_config = config.onetime return config.cache:ensure({ 'get', 'onetime', global_config.revision or 0, onetime_config.revision or 0, }, function() local c = {} c = misc.merge(c, config.normalize(onetime_config)) c = misc.merge(c, config.normalize(global_config)) return c end) elseif api.is_cmdline_mode() then local cmdtype = vim.fn.getcmdtype() local cmdline_config = config.cmdline[cmdtype] or { revision = 1, sources = {} } return config.cache:ensure({ 'get', 'cmdline', global_config.revision or 0, cmdtype, cmdline_config.revision or 0, }, function() local c = {} c = misc.merge(c, config.normalize(cmdline_config)) c = misc.merge(c, config.normalize(global_config)) return c end) else local bufnr = vim.api.nvim_get_current_buf() local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) local buffer_config = config.buffers[bufnr] or { revision = 1 } local filetype_config = config.filetypes[filetype] or { revision = 1 } return config.cache:ensure({ 'get', 'default', global_config.revision or 0, filetype, filetype_config.revision or 0, bufnr, buffer_config.revision or 0, }, function() local c = {} c = misc.merge(config.normalize(c), config.normalize(buffer_config)) c = misc.merge(config.normalize(c), config.normalize(filetype_config)) c = misc.merge(config.normalize(c), config.normalize(global_config)) return c end) end end ---Return cmp is enabled or not. config.enabled = function() local enabled = config.get().enabled if type(enabled) == 'function' then enabled = enabled() end return enabled and api.is_suitable_mode() end ---Return source config ---@param name string ---@return cmp.SourceConfig config.get_source_config = function(name) local c = config.get() for _, s in ipairs(c.sources) do if s.name == name then return s end end return nil end ---Return the current menu is native or not. config.is_native_menu = function() local c = config.get() if c.view and c.view.entries then return c.view.entries == 'native' or c.view.entries.name == 'native' end return false end ---Normalize mapping key ---@param c any ---@return cmp.ConfigSchema config.normalize = function(c) -- make sure c is not 'nil' ---@type any c = c == nil and {} or c -- Normalize mapping. if c.mapping then local normalized = {} for k, v in pairs(c.mapping) do normalized[keymap.normalize(k)] = mapping(v, { 'i' }) end c.mapping = normalized end -- Notice experimental.native_menu. if c.experimental and c.experimental.native_menu then vim.api.nvim_echo({ { '[nvim-cmp] ', 'Normal' }, { 'experimental.native_menu', 'WarningMsg' }, { ' is deprecated.\n', 'Normal' }, { '[nvim-cmp] Please use ', 'Normal' }, { 'view.entries = "native"', 'WarningMsg' }, { ' instead.', 'Normal' }, }, true, {}) c.view = c.view or {} c.view.entries = c.view.entries or 'native' end -- Notice documentation. if c.documentation ~= nil then vim.api.nvim_echo({ { '[nvim-cmp] ', 'Normal' }, { 'documentation', 'WarningMsg' }, { ' is deprecated.\n', 'Normal' }, { '[nvim-cmp] Please use ', 'Normal' }, { 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' }, { ' instead.', 'Normal' }, }, true, {}) c.window = c.window or {} c.window.documentation = c.documentation end -- Notice sources.[n].opts if c.sources then for _, s in ipairs(c.sources) do if s.opts and not s.option then s.option = s.opts s.opts = nil vim.api.nvim_echo({ { '[nvim-cmp] ', 'Normal' }, { 'sources[number].opts', 'WarningMsg' }, { ' is deprecated.\n', 'Normal' }, { '[nvim-cmp] Please use ', 'Normal' }, { 'sources[number].option', 'WarningMsg' }, { ' instead.', 'Normal' }, }, true, {}) end s.option = s.option or {} end end return c end return config ================================================ FILE: lua/cmp/context.lua ================================================ local misc = require('cmp.utils.misc') local pattern = require('cmp.utils.pattern') local types = require('cmp.types') local cache = require('cmp.utils.cache') local api = require('cmp.utils.api') ---@class cmp.Context ---@field public id string ---@field public cache cmp.Cache ---@field public prev_context cmp.Context ---@field public option cmp.ContextOption ---@field public filetype string ---@field public time integer ---@field public bufnr integer ---@field public cursor vim.Position|lsp.Position ---@field public cursor_line string ---@field public cursor_after_line string ---@field public cursor_before_line string ---@field public aborted boolean local context = {} ---Create new empty context ---@return cmp.Context context.empty = function() local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`. ctx.bufnr = -1 ctx.input = '' ctx.cursor = {} ctx.cursor.row = -1 ctx.cursor.col = -1 return ctx end ---Create new context ---@param prev_context? cmp.Context ---@param option? cmp.ContextOption ---@return cmp.Context context.new = function(prev_context, option) option = option or {} local self = setmetatable({}, { __index = context }) self.id = misc.id('cmp.context.new') self.cache = cache.new() self.prev_context = prev_context or context.empty() self.option = option or { reason = types.cmp.ContextReason.None } self.filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) self.time = vim.loop.now() self.bufnr = vim.api.nvim_get_current_buf() local cursor = api.get_cursor() self.cursor_line = api.get_current_line() self.cursor = {} self.cursor.row = cursor[1] self.cursor.col = cursor[2] + 1 self.cursor.line = self.cursor.row - 1 self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col) self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1) self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col) self.aborted = false return self end context.abort = function(self) self.aborted = true end ---Return context creation reason. ---@return cmp.ContextReason context.get_reason = function(self) return self.option.reason end ---Get keyword pattern offset ---@return integer context.get_offset = function(self, keyword_pattern) return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function() return pattern.offset([[\%(]] .. keyword_pattern .. [[\)\m$]], self.cursor_before_line) or self.cursor.col end) end ---Return if this context is changed from previous context or not. ---@return boolean context.changed = function(self, ctx) local curr = self if curr.bufnr ~= ctx.bufnr then return true end if curr.cursor.row ~= ctx.cursor.row then return true end if curr.cursor.col ~= ctx.cursor.col then return true end if curr:get_reason() == types.cmp.ContextReason.Manual then return true end return false end ---Shallow clone context.clone = function(self) local cloned = {} for k, v in pairs(self) do cloned[k] = v end return cloned end return context ================================================ FILE: lua/cmp/context_spec.lua ================================================ local spec = require('cmp.utils.spec') local context = require('cmp.context') describe('context', function() before_each(spec.before) describe('new', function() it('middle of text', function() vim.fn.setline('1', 'function! s:name() abort') vim.bo.filetype = 'vim' vim.fn.execute('normal! fm') local ctx = context.new() assert.are.equal(ctx.filetype, 'vim') assert.are.equal(ctx.cursor.row, 1) assert.are.equal(ctx.cursor.col, 15) assert.are.equal(ctx.cursor_line, 'function! s:name() abort') end) it('tab indent', function() vim.fn.setline('1', '\t\tab') vim.bo.filetype = 'vim' vim.fn.execute('normal! fb') local ctx = context.new() assert.are.equal(ctx.filetype, 'vim') assert.are.equal(ctx.cursor.row, 1) assert.are.equal(ctx.cursor.col, 4) assert.are.equal(ctx.cursor_line, '\t\tab') end) end) end) ================================================ FILE: lua/cmp/core.lua ================================================ local debug = require('cmp.utils.debug') local str = require('cmp.utils.str') local char = require('cmp.utils.char') local feedkeys = require('cmp.utils.feedkeys') local async = require('cmp.utils.async') local keymap = require('cmp.utils.keymap') local context = require('cmp.context') local source = require('cmp.source') local view = require('cmp.view') local misc = require('cmp.utils.misc') local config = require('cmp.config') local types = require('cmp.types') local api = require('cmp.utils.api') local event = require('cmp.utils.event') ---@class cmp.Core ---@field public suspending boolean ---@field public view cmp.View ---@field public sources cmp.Source[] ---@field public context cmp.Context ---@field public event cmp.Event local core = {} core.new = function() local self = setmetatable({}, { __index = core }) self.suspending = false self.sources = {} self.context = context.new() self.event = event.new() self.view = view.new() self.view.event:on('keymap', function(...) self:on_keymap(...) end) for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do self.view.event:on(event_name, function(evt) self.event:emit(event_name, evt) end) end return self end ---Register source ---@param s cmp.Source core.register_source = function(self, s) self.sources[s.id] = s end ---Unregister source ---@param source_id integer ---@return cmp.Source? core.unregister_source = function(self, source_id) local s = self.sources[source_id] self.sources[source_id] = nil return s end ---Get new context ---@param option? cmp.ContextOption ---@return cmp.Context core.get_context = function(self, option) self.context:abort() local prev = self.context:clone() prev.prev_context = nil prev.cache = nil local ctx = context.new(prev, option) self:set_context(ctx) return self.context end ---Set new context ---@param ctx cmp.Context core.set_context = function(self, ctx) self.context = ctx end ---Suspend completion core.suspend = function(self) self.suspending = true -- It's needed to avoid conflicting with autocmd debouncing. return vim.schedule_wrap(function() self.suspending = false end) end ---Get sources that sorted by priority ---@param filter? cmp.SourceStatus[]|fun(s: cmp.Source): boolean ---@return cmp.Source[] core.get_sources = function(self, filter) local f = function(s) if type(filter) == 'table' then return vim.tbl_contains(filter, s.status) elseif type(filter) == 'function' then return filter(s) end return true end local sources = {} for _, c in pairs(config.get().sources) do for _, s in pairs(self.sources) do if c.name == s.name then if s:is_available() and f(s) then table.insert(sources, s) end end end end return sources end ---Return registered sources. ---@return cmp.Source[] core.get_registered_sources = function(self) return self.sources end ---Keypress handler core.on_keymap = function(self, keys, fallback) local mode = api.get_mode() for key, mapping in pairs(config.get().mapping) do if keymap.equals(key, keys) and mapping[mode] then return mapping[mode](fallback) end end --Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly. local chars = keymap.t(keys) local e = self.view:get_active_entry() if e and vim.tbl_contains(config.get().confirmation.get_commit_characters(e:get_commit_characters()), chars) then local is_printable = char.is_printable(string.byte(chars, 1)) self:confirm(e, { behavior = is_printable and 'insert' or 'replace', commit_character = chars, }, function() local ctx = self:get_context() local word = e.word if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then fallback() else self:reset() end end) return end fallback() end ---Prepare completion core.prepare = function(self) for keys, mapping in pairs(config.get().mapping) do for mode in pairs(mapping) do keymap.listen(mode, keys, function(...) self:on_keymap(...) end) end end end ---Check auto-completion core.on_change = function(self, trigger_event) local ignore = false ignore = ignore or self.suspending ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word) ignore = ignore or not self.view:ready() if ignore then self:get_context({ reason = types.cmp.ContextReason.Auto }) return end self:autoindent(trigger_event, function() local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto }) debug.log(('ctx: `%s`'):format(ctx.cursor_before_line)) if ctx:changed(ctx.prev_context) then self.view:on_change() debug.log('changed') if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then self:complete(ctx) else self.filter.timeout = self.view:visible() and config.get().performance.throttle or 0 self:filter() end else debug.log('unchanged') end end) end ---Cursor moved. core.on_moved = function(self) local ignore = false ignore = ignore or self.suspending ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word) ignore = ignore or not self.view:visible() if ignore then return end self:filter() end ---Returns the suffix of the specified `line`. --- ---Contains `%s`: returns everything after the last `%s` in `line` ---Else: returns `line` unmodified ---@param line string ---@return string suffix local function find_line_suffix(line) return line:match('%S*$') --[[@as string]] end ---Check autoindent ---@param trigger_event cmp.TriggerEvent ---@param callback function core.autoindent = function(self, trigger_event, callback) if trigger_event ~= types.cmp.TriggerEvent.TextChanged then return callback() end if not api.is_insert_mode() then return callback() end -- Check prefix local cursor_before_line = api.get_cursor_before_line() local prefix = find_line_suffix(cursor_before_line) or '' if #prefix == 0 then return callback() end -- Reset current completion if indentkeys matched. for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then self:reset() self:set_context(context.empty()) break end end callback() end ---Complete common string for current completed entries. core.complete_common_string = function(self) if not self.view:visible() or self.view:get_selected_entry() then return false end config.set_onetime({ sources = config.get().sources, matching = { disallow_prefix_unmatching = true, disallow_partial_matching = true, disallow_fuzzy_matching = true, }, }) self:filter() self.filter:sync(1000) config.set_onetime({}) local cursor = api.get_cursor() local offset = self.view:get_offset() or cursor[2] local common_string for _, e in ipairs(self.view:get_entries()) do local vim_item = e:get_vim_item(offset) if not common_string then common_string = vim_item.word else common_string = str.get_common_string(common_string, vim_item.word) end end local cursor_before_line = api.get_cursor_before_line() local pretext = cursor_before_line:sub(offset) if common_string and #common_string > #pretext then feedkeys.call(keymap.backspace(pretext) .. common_string, 'n') return true end return false end ---Invoke completion ---@param ctx cmp.Context core.complete = function(self, ctx) if not api.is_suitable_mode() then return end self:set_context(ctx) -- Invoke completion sources. local sources = self:get_sources() for _, s in ipairs(sources) do local callback callback = (function(s_) return function() local new = context.new(ctx) if s_.incomplete and new:changed(s_.context) then s_:complete(new, callback) else if not self.view:get_active_entry() then self.filter.stop() self.filter.timeout = config.get().performance.debounce self:filter() end end end end)(s) s:complete(ctx, callback) end if not self.view:get_active_entry() then self.filter.timeout = self.view:visible() and config.get().performance.throttle or 1 self:filter() end end ---Update completion menu local async_filter = async.wrap(function(self) self.filter.timeout = config.get().performance.throttle -- Check invalid condition. local ignore = false ignore = ignore or not api.is_suitable_mode() if ignore then return end -- Check fetching sources. local sources = {} for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do -- Reserve filter call for timeout. if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time() self:filter() if #sources == 0 then return end end table.insert(sources, s) end local ctx = self:get_context() -- Display completion results. local did_open = self.view:open(ctx, sources) local fetching = #self:get_sources(function(s) return s.status == source.SourceStatus.FETCHING end) -- Check onetime config. if not did_open and fetching == 0 then config.set_onetime({}) end end) core.filter = async.throttle(async_filter, config.get().performance.throttle) ---Confirm completion. ---@param e cmp.Entry ---@param option cmp.ConfirmOption ---@param callback function core.confirm = function(self, e, option, callback) if not (e and not e.confirmed) then if callback then callback() end return end e.confirmed = true debug.log('entry.confirm', e.completion_item) async.sync(function(done) e:resolve(done) end, config.get().performance.confirm_resolve_timeout) local release = self:suspend() -- Close menus. self.view:close() feedkeys.call(keymap.indentkeys(), 'n') feedkeys.call('', 'n', function() -- Emulate `` behavior to save `.` register. local ctx = context.new() local keys = {} table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e.offset))) table.insert(keys, e.word) table.insert(keys, keymap.undobreak()) feedkeys.call(table.concat(keys, ''), 'in') end) feedkeys.call('', 'n', function() -- Restore the line at the time of request. local ctx = context.new() if api.is_cmdline_mode() then local keys = {} table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e.offset))) table.insert(keys, string.sub(e.context.cursor_before_line, e.offset)) feedkeys.call(table.concat(keys, ''), 'in') else vim.cmd([[silent! undojoin]]) -- This logic must be used nvim_buf_set_text. -- If not used, the snippet engine's placeholder will be broken. vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e.offset - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { e.context.cursor_before_line:sub(e.offset), }) vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 }) end end) feedkeys.call('', 'n', function() -- Apply additionalTextEdits. local ctx = context.new() if #(e.completion_item.additionalTextEdits or {}) == 0 then e:resolve(function() local new = context.new() local text_edits = e.completion_item.additionalTextEdits or {} if #text_edits == 0 then return end local has_cursor_line_text_edit = (function() local minrow = math.min(ctx.cursor.row, new.cursor.row) local maxrow = math.max(ctx.cursor.row, new.cursor.row) for _, te in ipairs(text_edits) do local srow = te.range.start.line + 1 local erow = te.range['end'].line + 1 if srow <= minrow and maxrow <= erow then return true end end return false end)() if has_cursor_line_text_edit then return end vim.cmd([[silent! undojoin]]) api.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind()) end) else vim.cmd([[silent! undojoin]]) api.apply_text_edits(e.completion_item.additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind()) end end) feedkeys.call('', 'n', function() local ctx = context.new() local completion_item = misc.copy(e.completion_item) if not completion_item.textEdit then completion_item.textEdit = {} local insertText = completion_item.insertText if misc.empty(insertText) then insertText = nil end completion_item.textEdit.newText = insertText or completion_item.word or completion_item.label end local behavior = option.behavior or config.get().confirmation.default_behavior if behavior == types.cmp.ConfirmBehavior.Replace then completion_item.textEdit.range = e.replace_range else completion_item.textEdit.range = e.insert_range end local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1)) local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col) local new_text = completion_item.textEdit.newText completion_item.textEdit.range.start.line = ctx.cursor.line completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before completion_item.textEdit.range['end'].line = ctx.cursor.line completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after if api.is_insert_mode() then if false then --To use complex expansion debug. vim.print({ -- luacheck: ignore item = e.completion_item, diff_before = diff_before, diff_after = diff_after, new_text = new_text, text_edit_new_text = completion_item.textEdit.newText, range_start = completion_item.textEdit.range.start.character, range_end = completion_item.textEdit.range['end'].character, original_range_start = e.completion_item.textEdit.range.start.character, original_range_end = e.completion_item.textEdit.range['end'].character, cursor_line = ctx.cursor_line, cursor_col0 = ctx.cursor.col - 1, }) end local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet if is_snippet then completion_item.textEdit.newText = '' end api.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8') local texts = vim.split(completion_item.textEdit.newText, '\n') vim.api.nvim_win_set_cursor(0, { completion_item.textEdit.range.start.line + #texts, (#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]), }) if is_snippet then config.get().snippet.expand({ body = new_text, insert_text_mode = completion_item.insertTextMode, }) end else local keys = {} table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1))) table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character))) table.insert(keys, new_text) feedkeys.call(table.concat(keys, ''), 'in') end end) feedkeys.call(keymap.indentkeys(vim.bo.indentkeys), 'n') feedkeys.call('', 'n', function() e:execute(vim.schedule_wrap(function() release() self.event:emit('confirm_done', { entry = e, commit_character = option.commit_character, }) if callback then callback() end end)) end) end ---Reset current completion state core.reset = function(self) for _, s in pairs(self.sources) do s:reset() end self.context = context.empty() end return core ================================================ FILE: lua/cmp/core_spec.lua ================================================ local spec = require('cmp.utils.spec') local feedkeys = require('cmp.utils.feedkeys') local types = require('cmp.types') local core = require('cmp.core') local source = require('cmp.source') local keymap = require('cmp.utils.keymap') local api = require('cmp.utils.api') describe('cmp.core', function() describe('confirm', function() ---@param request string ---@param filter string ---@param completion_item lsp.CompletionItem ---@param option? { position_encoding_kind: lsp.PositionEncodingKind } ---@return table local confirm = function(request, filter, completion_item, option) option = option or {} local c = core.new() local s = source.new('spec', { get_position_encoding_kind = function() return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16 end, complete = function(_, _, callback) callback({ completion_item }) end, }) c:register_source(s) feedkeys.call(request, 'n', function() c:complete(c:get_context({ reason = types.cmp.ContextReason.Manual })) vim.wait(5000, function() return #c.sources[s.id].entries > 0 end) end) feedkeys.call(filter, 'n', function() c:confirm(c.sources[s.id].entries[1], {}, function() end) end) local state = {} feedkeys.call('', 'x', function() feedkeys.call('', 'n', function() if api.is_cmdline_mode() then state.buffer = { api.get_current_line() } else state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) end state.cursor = api.get_cursor() end) end) return state end describe('insert-mode', function() before_each(spec.before) it('label', function() local state = confirm('iA', 'IU', { label = 'AIUEO', }) assert.are.same(state.buffer, { 'AIUEO' }) assert.are.same(state.cursor, { 1, 5 }) end) it('insertText', function() local state = confirm('iA', 'IU', { label = 'AIUEO', insertText = '_AIUEO_', }) assert.are.same(state.buffer, { '_AIUEO_' }) assert.are.same(state.cursor, { 1, 7 }) end) it('textEdit', function() local state = confirm(keymap.t('i***AEO***'), 'IU', { label = 'AIUEO', textEdit = { range = { start = { line = 0, character = 3, }, ['end'] = { line = 0, character = 6, }, }, newText = 'foo\nbar\nbaz', }, }) assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) assert.are.same(state.cursor, { 3, 3 }) end) it('#1552', function() local state = confirm(keymap.t('ios.'), '', { filterText = 'IsPermission', insertTextFormat = 2, label = 'IsPermission', textEdit = { newText = 'IsPermission($0)', range = { ['end'] = { character = 3, line = 0, }, start = { character = 3, line = 0, }, }, }, }) assert.are.same(state.buffer, { 'os.IsPermission()' }) assert.are.same(state.cursor, { 1, 16 }) end) it('insertText & snippet', function() local state = confirm('iA', 'IU', { label = 'AIUEO', insertText = 'AIUEO($0)', insertTextFormat = types.lsp.InsertTextFormat.Snippet, }) assert.are.same(state.buffer, { 'AIUEO()' }) assert.are.same(state.cursor, { 1, 6 }) end) it('textEdit & snippet', function() local state = confirm(keymap.t('i***AEO***'), 'IU', { label = 'AIUEO', insertTextFormat = types.lsp.InsertTextFormat.Snippet, textEdit = { range = { start = { line = 0, character = 3, }, ['end'] = { line = 0, character = 6, }, }, newText = 'foo\nba$0r\nbaz', }, }) assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) assert.are.same(state.cursor, { 2, 2 }) end) local char = '🗿' for _, case in ipairs({ { encoding = types.lsp.PositionEncodingKind.UTF8, char_size = #char, }, { encoding = types.lsp.PositionEncodingKind.UTF16, char_size = select(2, vim.str_utfindex(char)), }, { encoding = types.lsp.PositionEncodingKind.UTF32, char_size = select(1, vim.str_utfindex(char)), }, }) do it('textEdit & multibyte: ' .. case.encoding, function() local state = confirm(keymap.t('i%s:%s%s:%s'):format(char, char, char, char), char, { label = char .. char .. char, textEdit = { range = { start = { line = 0, character = case.char_size + #':', }, ['end'] = { line = 0, character = case.char_size + #':' + case.char_size + case.char_size, }, }, newText = char .. char .. char .. char .. char, }, }, { position_encoding_kind = case.encoding, }) vim.print({ state = state, case = case }) assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) }) assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) }) end) end end) describe('cmdline-mode', function() before_each(spec.before) it('label', function() local state = confirm(':A', 'IU', { label = 'AIUEO', }) assert.are.same(state.buffer, { 'AIUEO' }) assert.are.same(state.cursor[2], 5) end) it('insertText', function() local state = confirm(':A', 'IU', { label = 'AIUEO', insertText = '_AIUEO_', }) assert.are.same(state.buffer, { '_AIUEO_' }) assert.are.same(state.cursor[2], 7) end) it('textEdit', function() local state = confirm(keymap.t(':***AEO***'), 'IU', { label = 'AIUEO', textEdit = { range = { start = { line = 0, character = 3, }, ['end'] = { line = 0, character = 6, }, }, newText = 'AIUEO', }, }) assert.are.same(state.buffer, { '***AIUEO***' }) assert.are.same(state.cursor[2], 6) end) end) end) end) ================================================ FILE: lua/cmp/entry.lua ================================================ local cache = require('cmp.utils.cache') local char = require('cmp.utils.char') local misc = require('cmp.utils.misc') local str = require('cmp.utils.str') local snippet = require('cmp.utils.snippet') local config = require('cmp.config') local types = require('cmp.types') local matcher = require('cmp.matcher') local ok, lspkind = pcall(require, 'lspkind') local function get_icon(kind) if ok then local icon = lspkind.symbol_map[kind] return icon end return '' end ---@class cmp.Entry ---@field public id integer ---@field public cache cmp.Cache ---@field public match_cache cmp.Cache ---@field public score integer ---@field public exact boolean ---@field public matches table ---@field public context cmp.Context ---@field public source cmp.Source ---@field public source_offset integer ---@field public source_insert_range lsp.Range ---@field public source_replace_range lsp.Range ---@field public completion_item lsp.CompletionItem ---@field public item_defaults? lsp.internal.CompletionItemDefaults ---@field public resolved_completion_item lsp.CompletionItem|nil ---@field public resolved_callbacks fun()[] ---@field public resolving boolean ---@field public confirmed boolean ---@field public insert_range lsp.Range ---@field public replace_range lsp.Range ---@field public offset integer ---@field public word string ---@field public filter_text string ---@field private match_view_args_ret {input:string, word:string, option:cmp.MatchingConfig, matches:table[]} local entry = {} entry.__index = entry ---Create new entry ---@param ctx cmp.Context ---@param source cmp.Source ---@param completion_item lsp.CompletionItem ---@param item_defaults? lsp.internal.CompletionItemDefaults ---@return cmp.Entry entry.new = function(ctx, source, completion_item, item_defaults) local self = setmetatable({}, entry) self.id = misc.id('entry.new') self.cache = cache.new() self.match_cache = cache.new() self.score = 0 self.exact = false self.matches = {} self.context = ctx self.source = source self.offset = source.request_offset self.source_offset = source.request_offset self.source_insert_range = source.default_insert_range self.source_replace_range = source.default_replace_range self.item_defaults = item_defaults self.resolved_completion_item = nil self.resolved_callbacks = {} self.resolving = false self.confirmed = false self:_set_completion_item(completion_item) return self end ---@package entry._set_completion_item = function(self, completion_item) if not self.completion_item then self.completion_item = self:fill_defaults(completion_item, self.item_defaults) else -- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts#L588 -- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/base/common/objects.ts#L89 -- @see https://github.com/microsoft/vscode/blob/a00f2e64f4fa9a1f774875562e1e9697d7138ed3/src/vs/editor/contrib/suggest/browser/suggest.ts#L147 for k, v in pairs(completion_item) do self.completion_item[k] = v or self.completion_item[k] end end local item = self.completion_item ---Create filter text self.filter_text = item.filterText or str.trim(item.label) -- TODO: the order below is important if item.textEdit then self.insert_range = self:convert_range_encoding(item.textEdit.insert or item.textEdit.range) self.replace_range = self:convert_range_encoding(item.textEdit.replace or item.textEdit.range) end self.word = self:_get_word() self.offset = self:_get_offset() if not self.insert_range then self.insert_range = { start = { line = self.context.cursor.row - 1, character = self.offset - 1, }, ['end'] = self.source_insert_range['end'], } end if not self.replace_range or ((self.context.cursor.col - 1) == self.replace_range['end'].character) then self.replace_range = { start = { line = self.source_replace_range.start.line, character = self.offset - 1, }, ['end'] = self.source_replace_range['end'], } end end ---@deprecated use entry.offset instead entry.get_offset = function(self) return self.offset end ---Make offset value ---@package ---@return integer entry._get_offset = function(self) local offset = self.source_offset if self.completion_item.textEdit then local range = self.insert_range if range then local start = math.min(range.start.character + 1, offset) for idx = start, self.source_offset do local byte = string.byte(self.context.cursor_line, idx) if byte == nil or not char.is_white(byte) then return idx end end return offset end else -- NOTE -- The VSCode does not implement this but it's useful if the server does not care about word patterns. -- We should care about this performance. local word = self.word for idx = self.source_offset - 1, self.source_offset - #word, -1 do if char.is_semantic_index(self.context.cursor_line, idx) then local c = string.byte(self.context.cursor_line, idx) if char.is_white(c) then break end local match = true for i = 1, self.source_offset - idx do local c1 = string.byte(word, i) local c2 = string.byte(self.context.cursor_line, idx + i - 1) if not c1 or not c2 or c1 ~= c2 then match = false break end end if match then offset = math.min(offset, idx) end end end end return offset end ---@deprecated use entry.word instead entry.get_word = function(self) return self.word end ---Create word for vim.CompletedItem ---NOTE: This method doesn't clear the cache after completionItem/resolve. ---@package ---@return string entry._get_word = function(self) --NOTE: This is nvim-cmp specific implementation. local completion_item = self.completion_item if completion_item.word then return completion_item.word end local word if completion_item.textEdit and not misc.empty(completion_item.textEdit.newText) then word = str.trim(completion_item.textEdit.newText) if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = tostring(snippet.parse(word)) end local overwrite = self:get_overwrite() if 0 < overwrite[2] or completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0) end elseif not misc.empty(completion_item.insertText) then word = str.trim(completion_item.insertText) if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.get_word(tostring(snippet.parse(word))) end else word = str.trim(completion_item.label) end return str.oneline(word) end ---Get overwrite information ---@return integer[] entry.get_overwrite = function(self) return self.cache:ensure('get_overwrite', entry._get_overwrite, self) end ---@package entry._get_overwrite = function(self) if self.completion_item.textEdit then local range = self.insert_range if range then local vim_start = range.start.character + 1 local vim_end = range['end'].character + 1 local before = self.context.cursor.col - vim_start local after = vim_end - self.context.cursor.col return { before, after } end end return { 0, 0 } end ---@package entry.get_filter_text = function(self) return self.filter_text end ---Get LSP's insert text ---@return string entry.get_insert_text = function(self) return self.cache:ensure('get_insert_text', entry._get_insert_text, self) end ---@package entry._get_insert_text = function(self) local completion_item = self.completion_item local word if completion_item.textEdit then word = str.trim(completion_item.textEdit.newText) if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') end elseif completion_item.insertText then word = str.trim(completion_item.insertText) if completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') end else word = str.trim(completion_item.label) end return word end ---Return the item is deprecated or not. ---@return boolean entry.is_deprecated = function(self) return self.completion_item.deprecated or vim.tbl_contains(self.completion_item.tags or {}, types.lsp.CompletionItemTag.Deprecated) end ---Return view information. ---@param suggest_offset integer ---@param entries_buf integer The buffer this entry will be rendered into. ---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string|table }, icon: { text: string, bytes: integer, width: integer, hl_group: string|table }, kind: { text: string, bytes: integer, width: integer, hl_group: string|table }, menu: { text: string, bytes: integer, width: integer, hl_group: string|table } } entry.get_view = function(self, suggest_offset, entries_buf) local item = self:get_vim_item(suggest_offset) return self.cache:ensure('get_view:' .. tostring(entries_buf), entry._get_view, self, item, entries_buf) end ---@package entry._get_view = function(self, item, entries_buf) local view = {} -- The result of vim.fn.strdisplaywidth depends on which buffer it was -- called in because it reads the values of the option 'tabstop' when -- rendering characters. vim.api.nvim_buf_call(entries_buf, function() view.abbr = {} view.abbr.text = item.abbr or '' view.abbr.bytes = #view.abbr.text view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text) view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr') view.icon = {} view.icon.text = item.icon or get_icon(types.lsp.CompletionItemKind[self:get_kind()]) view.icon.bytes = #view.icon.text view.icon.width = vim.fn.strdisplaywidth(view.icon.text) view.icon.hl_group = item.icon_hl_group or (('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '') .. 'Icon') or 'CmpItemKind') view.kind = {} view.kind.text = item.kind or '' view.kind.bytes = #view.kind.text view.kind.width = vim.fn.strdisplaywidth(view.kind.text) view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '')) view.menu = {} view.menu.text = item.menu or '' view.menu.bytes = #view.menu.text view.menu.width = vim.fn.strdisplaywidth(view.menu.text) view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu' view.dup = item.dup end) return view end ---Make vim.CompletedItem ---@param suggest_offset integer ---@return vim.CompletedItem entry.get_vim_item = function(self, suggest_offset) return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), entry._get_vim_item, self, suggest_offset) end ---@package entry._get_vim_item = function(self, suggest_offset) local completion_item = self.completion_item local word = self.word local abbr = str.oneline(completion_item.label) -- ~ indicator local is_expandable = false local expandable_indicator = config.get().formatting.expandable_indicator if #(completion_item.additionalTextEdits or {}) > 0 then is_expandable = true elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then is_expandable = self:get_insert_text() ~= word elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then is_expandable = true end if expandable_indicator and is_expandable then abbr = abbr .. '~' end -- append delta text if suggest_offset < self.offset then word = string.sub(self.context.cursor_before_line, suggest_offset, self.offset - 1) .. word end -- labelDetails. local menu = nil if completion_item.labelDetails then menu = '' if completion_item.labelDetails.detail then menu = menu .. completion_item.labelDetails.detail end if completion_item.labelDetails.description then menu = menu .. completion_item.labelDetails.description end end -- remove duplicated string. if self.offset ~= self.context.cursor.col then for i = 1, #word do if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then word = string.sub(word, 1, i - 1) break end end end local cmp_opts = completion_item.cmp or {} local vim_item = { word = word, abbr = abbr, icon = cmp_opts.icon or get_icon(types.lsp.CompletionItemKind[self:get_kind()]), icon_hl_group = cmp_opts.icon_hl_group, kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], kind_hl_group = cmp_opts.kind_hl_group, menu = menu, dup = completion_item.dup or 1, } if config.get().formatting.format then vim_item = config.get().formatting.format(self, vim_item) end vim_item.word = str.oneline(vim_item.word or '') vim_item.abbr = str.oneline(vim_item.abbr or '') vim_item.icon = str.oneline(vim_item.icon or '') vim_item.kind = str.oneline(vim_item.kind or '') vim_item.menu = str.oneline(vim_item.menu or '') vim_item.equal = 1 vim_item.empty = 1 return vim_item end ---Get commit characters ---@return string[] entry.get_commit_characters = function(self) return self.completion_item.commitCharacters or {} end ---@deprecated use entry.insert_range instead entry.get_insert_range = function(self) return self.insert_range end ---@deprecated use entry.replace_range instead entry.get_replace_range = function(self) return self.replace_range end ---Match line. ---@param input string ---@param matching_config cmp.MatchingConfig ---@return { score: integer, matches: table[] } entry.match = function(self, input, matching_config) -- https://www.lua.org/pil/11.6.html -- do not use '..' to allocate multiple strings local cache_key = string.format('%s:%d:%d:%d:%d:%d:%d', input, self.resolved_completion_item and 1 or 0, matching_config.disallow_fuzzy_matching and 1 or 0, matching_config.disallow_partial_matching and 1 or 0, matching_config.disallow_prefix_unmatching and 1 or 0, matching_config.disallow_partial_fuzzy_matching and 1 or 0, matching_config.disallow_symbol_nonprefix_matching and 1 or 0) local matched = self.match_cache:get(cache_key) if matched then if self.match_view_args_ret and self.match_view_args_ret.input ~= input then self.match_view_args_ret.input = input self.match_view_args_ret.word = matched._word self.match_view_args_ret.matches = matched.matches end return matched end matched = self:_match(input, matching_config) self.match_cache:set(cache_key, matched) return matched end ---@package entry._match = function(self, input, matching_config) local completion_item = self.completion_item local option = { disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching, disallow_partial_matching = matching_config.disallow_partial_matching, disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching, disallow_symbol_nonprefix_matching = matching_config.disallow_symbol_nonprefix_matching, synonyms = { self.word, self.completion_item.label, }, } local score, matches, filter_text local checked = {} ---@type table filter_text = self.filter_text checked[filter_text] = true score, matches = matcher.match(input, filter_text, option) -- Support the language server that doesn't respect VSCode's behaviors. if score == 0 then if completion_item.textEdit and not misc.empty(completion_item.textEdit.newText) then local diff = self.source_offset - self.offset if diff > 0 then local prefix = string.sub(self.context.cursor_line, self.offset, self.offset + diff) local accept = nil accept = accept or string.match(prefix, '^[^%a]+$') accept = accept or string.find(completion_item.textEdit.newText, prefix, 1, true) if accept then filter_text = prefix .. filter_text if not checked[filter_text] then checked[filter_text] = true score, matches = matcher.match(input, filter_text, option) end end end end end -- Fix highlight if filterText is not the same to vim_item.abbr. if score > 0 then self.match_view_args_ret = { input = input, word = filter_text, option = option, matches = matches, } end return { score = score, matches = matches, _word = filter_text } end ---@param view string entry.get_view_matches = function(self, view) if self.match_view_args_ret then if self.match_view_args_ret.word == view then return self.match_view_args_ret.matches end self.match_view_args_ret.word = view local input = self.match_view_args_ret.input local diff = self.source_offset - self.offset if diff > 0 then input = input:sub(1 + diff) end local _, matches = matcher.match(input, view, self.match_view_args_ret.option) self.match_view_args_ret.matches = matches return matches end end ---@deprecated use entry.completion_item instead entry.get_completion_item = function(self) return self.completion_item end ---Create documentation ---@return string[] entry.get_documentation = function(self) local item = self.completion_item local documents = {} -- detail if item.detail and item.detail ~= '' then local ft = self.context.filetype local dot_index = string.find(ft, '%.') if dot_index ~= nil then ft = string.sub(ft, 0, dot_index - 1) end table.insert(documents, { kind = types.lsp.MarkupKind.Markdown, value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)), }) end local documentation = item.documentation if type(documentation) == 'string' and documentation ~= '' then local value = str.trim(documentation) if value ~= '' then table.insert(documents, { kind = types.lsp.MarkupKind.PlainText, value = value, }) end elseif type(documentation) == 'table' and not misc.empty(documentation.value) then local value = str.trim(documentation.value) if value ~= '' then table.insert(documents, { kind = documentation.kind, value = value, }) end end return vim.lsp.util.convert_input_to_markdown_lines(documents) end ---Get completion item kind ---@return lsp.CompletionItemKind entry.get_kind = function(self) return self.completion_item.kind or types.lsp.CompletionItemKind.Text end ---Execute completion item's command. ---@param callback fun() entry.execute = function(self, callback) self.source:execute(self.completion_item, callback) end ---Resolve completion item. ---@param callback fun() entry.resolve = function(self, callback) if self.resolved_completion_item then return callback() end table.insert(self.resolved_callbacks, callback) if not self.resolving then self.resolving = true self.source:resolve(self.completion_item, function(completion_item) self.resolving = false if not completion_item then return end self:_set_completion_item(completion_item) self.resolved_completion_item = self.completion_item self.cache:clear() for _, c in ipairs(self.resolved_callbacks) do c() end end) end end ---@param completion_item lsp.CompletionItem ---@param defaults? lsp.internal.CompletionItemDefaults ---@return lsp.CompletionItem entry.fill_defaults = function(_, completion_item, defaults) defaults = defaults or {} if defaults.data then completion_item.data = completion_item.data or defaults.data end if defaults.commitCharacters then completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters end if defaults.insertTextFormat then completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat end if defaults.insertTextMode then completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode end if defaults.editRange then if not completion_item.textEdit then if defaults.editRange.insert then completion_item.textEdit = { insert = defaults.editRange.insert, replace = defaults.editRange.replace, newText = completion_item.textEditText or completion_item.label, } else completion_item.textEdit = { range = defaults.editRange, --[[@as lsp.Range]] newText = completion_item.textEditText or completion_item.label, } end end end return completion_item end ---Convert the oneline range encoding. entry.convert_range_encoding = function(self, range) local from_encoding = self.source.position_encoding local cache_key = string.format('entry.convert_range_encoding:%d:%d:%s', range.start.character, range['end'].character, from_encoding) local res = self.context.cache:get(cache_key) if res then return res end res = { start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding), ['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding), } self.context.cache:set(cache_key, res) return res end ---Return true if the entry is invalid. entry.is_invalid = function(self) local is_invalid = false is_invalid = is_invalid or misc.empty(self.completion_item.label) if self.completion_item.textEdit then local range = self.completion_item.textEdit.range or self.completion_item.textEdit.insert is_invalid = is_invalid or range.start.line ~= range['end'].line or range.start.line ~= self.context.cursor.line end return is_invalid end return entry ================================================ FILE: lua/cmp/entry_spec.lua ================================================ local spec = require('cmp.utils.spec') local entry = require('cmp.entry') describe('entry', function() before_each(spec.before) it('one char', function() local state = spec.state('@.', 1, 3) state.input('@') local e = entry.new(state.manual(), state.source(), { label = '@', }) assert.are.equal(e.offset, 3) assert.are.equal(e:get_vim_item(e.offset).word, '@') end) it('word length (no fix)', function() local state = spec.state('a.b', 1, 4) state.input('.') local e = entry.new(state.manual(), state.source(), { label = 'b', }) assert.are.equal(e.offset, 5) assert.are.equal(e:get_vim_item(e.offset).word, 'b') end) it('word length (fix)', function() local state = spec.state('a.b', 1, 4) state.input('.') local e = entry.new(state.manual(), state.source(), { label = 'b.', }) assert.are.equal(e.offset, 3) assert.are.equal(e:get_vim_item(e.offset).word, 'b.') end) it('semantic index (no fix)', function() local state = spec.state('a.bc', 1, 5) state.input('.') local e = entry.new(state.manual(), state.source(), { label = 'c.', }) assert.are.equal(e.offset, 6) assert.are.equal(e:get_vim_item(e.offset).word, 'c.') end) it('semantic index (fix)', function() local state = spec.state('a.bc', 1, 5) state.input('.') local e = entry.new(state.manual(), state.source(), { label = 'bc.', }) assert.are.equal(e.offset, 3) assert.are.equal(e:get_vim_item(e.offset).word, 'bc.') end) it('[vscode-html-language-server] 1', function() local state = spec.state(' ', 1, 7) state.input('.') local e = entry.new(state.manual(), state.source(), { label = '/div', textEdit = { range = { start = { line = 0, character = 0, }, ['end'] = { line = 0, character = 6, }, }, newText = ' foo') assert.are.equal(e.filter_text, 'foo') end) it('[typescript-language-server] 1', function() local state = spec.state('Promise.resolve()', 1, 18) state.input('.') local e = entry.new(state.manual(), state.source(), { label = 'catch', }) -- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate. assert.are.equal(e:get_vim_item(18).word, '.catch') assert.are.equal(e.filter_text, 'catch') end) it('[typescript-language-server] 2', function() local state = spec.state('Promise.resolve()', 1, 18) state.input('.') local e = entry.new(state.manual(), state.source(), { filterText = '.Symbol', label = 'Symbol', textEdit = { newText = '[Symbol]', range = { ['end'] = { character = 18, line = 0, }, start = { character = 17, line = 0, }, }, }, }) assert.are.equal(e:get_vim_item(18).word, '[Symbol]') assert.are.equal(e.filter_text, '.Symbol') end) it('[lua-language-server] 1', function() local state = spec.state("local m = require'cmp.confi", 1, 28) local e -- press g state.input('g') e = entry.new(state.manual(), state.source(), { insertTextFormat = 2, label = 'cmp.config', textEdit = { newText = 'cmp.config', range = { ['end'] = { character = 27, line = 1, }, start = { character = 18, line = 1, }, }, }, }) assert.are.equal(e:get_vim_item(19).word, 'cmp.config') assert.are.equal(e.filter_text, 'cmp.config') -- press ' state.input("'") e = entry.new(state.manual(), state.source(), { insertTextFormat = 2, label = 'cmp.config', textEdit = { newText = 'cmp.config', range = { ['end'] = { character = 27, line = 1, }, start = { character = 18, line = 1, }, }, }, }) assert.are.equal(e:get_vim_item(19).word, 'cmp.config') assert.are.equal(e.filter_text, 'cmp.config') end) it('[lua-language-server] 2', function() local state = spec.state("local m = require'cmp.confi", 1, 28) local e -- press g state.input('g') e = entry.new(state.manual(), state.source(), { insertTextFormat = 2, label = 'lua.cmp.config', textEdit = { newText = 'lua.cmp.config', range = { ['end'] = { character = 27, line = 1, }, start = { character = 18, line = 1, }, }, }, }) assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') assert.are.equal(e.filter_text, 'lua.cmp.config') -- press ' state.input("'") e = entry.new(state.manual(), state.source(), { insertTextFormat = 2, label = 'lua.cmp.config', textEdit = { newText = 'lua.cmp.config', range = { ['end'] = { character = 27, line = 1, }, start = { character = 18, line = 1, }, }, }, }) assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') assert.are.equal(e.filter_text, 'lua.cmp.config') end) it('[intelephense] 1', function() local state = spec.state('\t\t', 1, 4) -- press g state.input('$') local e = entry.new(state.manual(), state.source(), { kind = 6, label = '$this', sortText = '$this', textEdit = { newText = '$this', range = { ['end'] = { character = 3, line = 1, }, start = { character = 2, line = 1, }, }, }, }) assert.are.equal(e:get_vim_item(e.offset).word, '$this') assert.are.equal(e.filter_text, '$this') end) it('[odin-language-server] 1', function() local state = spec.state('\t\t', 1, 4) -- press g state.input('s') local e = entry.new(state.manual(), state.source(), { additionalTextEdits = {}, command = { arguments = {}, command = '', title = '', }, deprecated = false, detail = 'string', documentation = '', insertText = '', insertTextFormat = 1, kind = 14, label = 'string', tags = {}, }) assert.are.equal(e:get_vim_item(e.offset).word, 'string') end) it('[#47] word should not contain \\n character', function() local state = spec.state('', 1, 1) -- press g state.input('_') local e = entry.new(state.manual(), state.source(), { kind = 6, label = '__init__', insertTextFormat = 1, insertText = '__init__(self) -> None:\n pass', }) assert.are.equal(e:get_vim_item(e.offset).word, '__init__(self) -> None:') assert.are.equal(e.filter_text, '__init__') end) -- I can't understand this test case... -- it('[#1533] keyword pattern that include whitespace', function() -- local state = spec.state(' ', 1, 2) -- local state_source = state.source() -- state_source.get_keyword_pattern = function(_) -- return '.' -- end -- state.input(' ') -- local e = entry.new(state.manual(), state_source, { -- filterText = "constructor() {\n ... st = 'test';\n ", -- kind = 1, -- label = "constructor() {\n ... st = 'test';\n }", -- textEdit = { -- newText = "constructor() {\n this.test = 'test';\n }", -- range = { -- ['end'] = { -- character = 2, -- line = 2, -- }, -- start = { -- character = 0, -- line = 2, -- }, -- }, -- }, -- }) -- assert.are.equal(e:get_offset(), 2) -- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {') -- end) it('[#1533] clang regression test', function() local state = spec.state('jsonReader', 3, 11) local state_source = state.source() state.input('.') local e = entry.new(state.manual(), state_source, { filterText = 'getPath()', kind = 1, label = 'getPath()', textEdit = { newText = 'getPath()', range = { ['end'] = { character = 11, col = 12, line = 2, row = 3, }, start = { character = 11, line = 2, }, }, }, }) assert.are.equal(e.offset, 12) assert.are.equal(e:get_vim_item(e.offset).word, 'getPath()') end) end) ================================================ FILE: lua/cmp/init.lua ================================================ local core = require('cmp.core') local source = require('cmp.source') local config = require('cmp.config') local feedkeys = require('cmp.utils.feedkeys') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local async = require('cmp.utils.async') local cmp = {} cmp.core = core.new() ---Expose types for k, v in pairs(require('cmp.types.cmp')) do cmp[k] = v end cmp.lsp = require('cmp.types.lsp') cmp.vim = require('cmp.types.vim') ---Expose event cmp.event = cmp.core.event ---Export mapping for special case cmp.mapping = require('cmp.config.mapping') ---Export default config presets cmp.config = {} cmp.config.disable = misc.none cmp.config.compare = require('cmp.config.compare') cmp.config.sources = require('cmp.config.sources') cmp.config.mapping = require('cmp.config.mapping') cmp.config.window = require('cmp.config.window') ---Sync asynchronous process. cmp.sync = function(callback) return function(...) cmp.core.filter:sync(1000) if callback then return callback(...) end end end ---Suspend completion. cmp.suspend = function() return cmp.core:suspend() end ---Register completion sources ---@param name string ---@param s cmp.Source ---@return integer cmp.register_source = function(name, s) local src = source.new(name, s) cmp.core:register_source(src) vim.api.nvim_exec_autocmds('User', { pattern = 'CmpRegisterSource', data = { source_id = src.id, }, }) return src.id end ---Unregister completion source ---@param id integer cmp.unregister_source = function(id) local s = cmp.core:unregister_source(id) if s then vim.api.nvim_exec_autocmds('User', { pattern = 'CmpUnregisterSource', data = { source_id = id, }, }) end end ---Get registered sources. ---@return cmp.Source[] cmp.get_registered_sources = function() return cmp.core:get_registered_sources() end ---Get current configuration. ---@return cmp.ConfigSchema cmp.get_config = function() return require('cmp.config').get() end ---Invoke completion manually ---@param option cmp.CompleteParams cmp.complete = cmp.sync(function(option) option = option or {} config.set_onetime(option.config) cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual })) return true end) ---Complete common string in current entries. cmp.complete_common_string = cmp.sync(function() return cmp.core:complete_common_string() end) ---Return view is visible or not. cmp.visible = cmp.sync(function() return cmp.core.view:visible() or vim.fn.pumvisible() == 1 end) ---Get what number candidates are currently selected. ---If not selected, nil is returned. cmp.get_selected_index = cmp.sync(function() return cmp.core.view:get_selected_index() end) ---Get current selected entry or nil cmp.get_selected_entry = cmp.sync(function() return cmp.core.view:get_selected_entry() end) ---Get current active entry or nil cmp.get_active_entry = cmp.sync(function() return cmp.core.view:get_active_entry() end) ---Get current all entries cmp.get_entries = cmp.sync(function() return cmp.core.view:get_entries() end) ---Close current completion cmp.close = cmp.sync(function() if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:close() cmp.core:reset() vim.schedule(release) return true else return false end end) ---Abort current completion cmp.abort = cmp.sync(function() if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:abort() cmp.core:reset() vim.schedule(release) return true else return false end end) ---Select next item if possible cmp.select_next_item = cmp.sync(function(option) option = option or {} option.behavior = option.behavior or cmp.SelectBehavior.Insert option.count = option.count or 1 if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:select_next_item(option) vim.schedule(release) return true elseif vim.fn.pumvisible() == 1 then if option.behavior == cmp.SelectBehavior.Insert then feedkeys.call(keymap.t(string.rep('', option.count)), 'in') else feedkeys.call(keymap.t(string.rep('', option.count)), 'in') end return true end return false end) ---Select prev item if possible cmp.select_prev_item = cmp.sync(function(option) option = option or {} option.behavior = option.behavior or cmp.SelectBehavior.Insert option.count = option.count or 1 if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:select_prev_item(option) vim.schedule(release) return true elseif vim.fn.pumvisible() == 1 then if option.behavior == cmp.SelectBehavior.Insert then feedkeys.call(keymap.t(string.rep('', option.count)), 'in') else feedkeys.call(keymap.t(string.rep('', option.count)), 'in') end return true end return false end) ---Scrolling documentation window if possible cmp.scroll_docs = cmp.sync(function(delta) if cmp.core.view.docs_view:visible() then cmp.core.view:scroll_docs(delta) return true else return false end end) ---Whether the documentation window is visible or not. cmp.visible_docs = cmp.sync(function() return cmp.core.view.docs_view:visible() end) ---Opens the documentation window. cmp.open_docs = cmp.sync(function() if not cmp.visible_docs() then cmp.core.view:open_docs() return true else return false end end) ---Closes the documentation window. cmp.close_docs = cmp.sync(function() if cmp.visible_docs() then cmp.core.view:close_docs() return true else return false end end) ---Confirm completion cmp.confirm = cmp.sync(function(option, callback) option = option or {} option.select = option.select or false option.behavior = option.behavior or cmp.get_config().confirmation.default_behavior or cmp.ConfirmBehavior.Insert callback = callback or function() end if cmp.core.view:visible() then local e = cmp.core.view:get_selected_entry() if not e and option.select then e = cmp.core.view:get_first_entry() end if e then cmp.core:confirm(e, { behavior = option.behavior, }, function() callback() cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly })) end) return true end elseif vim.fn.pumvisible() == 1 then local index = vim.fn.complete_info({ 'selected' }).selected if index == -1 and option.select then index = 0 end if index ~= -1 then vim.api.nvim_select_popupmenu_item(index, true, true, {}) return true end end return false end) ---Show status cmp.status = function() local kinds = {} kinds.available = {} kinds.unavailable = {} kinds.installed = {} kinds.invalid = {} local names = {} for _, s in pairs(cmp.core.sources) do names[s.name] = true if config.get_source_config(s.name) then if s:is_available() then table.insert(kinds.available, s:get_debug_name()) else table.insert(kinds.unavailable, s:get_debug_name()) end else table.insert(kinds.installed, s:get_debug_name()) end end for _, s in ipairs(config.get().sources) do if not names[s.name] then table.insert(kinds.invalid, s.name) end end if #kinds.available > 0 then vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {}) for _, name in ipairs(kinds.available) do vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) end end if #kinds.unavailable > 0 then vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {}) for _, name in ipairs(kinds.unavailable) do vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) end end if #kinds.installed > 0 then vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {}) for _, name in ipairs(kinds.installed) do vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) end end if #kinds.invalid > 0 then vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {}) for _, name in ipairs(kinds.invalid) do vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) end end end ---Ensures that cmp is the last receiver of the events specified. ---@param events string[] cmp.resubscribe = function(events) autocmd.resubscribe(events) end ---@type cmp.Setup cmp.setup = setmetatable({ global = function(c) config.set_global(c) end, filetype = function(filetype, c) config.set_filetype(c, filetype) end, buffer = function(c) config.set_buffer(c, vim.api.nvim_get_current_buf()) end, cmdline = function(type, c) config.set_cmdline(c, type) end, }, { __call = function(self, c) self.global(c) end, }) -- In InsertEnter autocmd, vim will detects mode=normal unexpectedly. local on_insert_enter = function() if config.enabled() then cmp.config.compare.scopes:update() cmp.config.compare.locality:update() cmp.core:prepare() cmp.core:on_change('InsertEnter') end end autocmd.subscribe({ 'CmdlineEnter' }, async.debounce_next_tick(on_insert_enter)) autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter)) -- async.throttle is needed for performance. The mapping `:...` will fire `CmdlineChanged` for each character. local on_text_changed = function() if config.enabled() then cmp.core:on_change('TextChanged') end end autocmd.subscribe({ 'TextChangedI', 'TextChangedP' }, on_text_changed) autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed)) autocmd.subscribe('CursorMovedI', function() if config.enabled() then cmp.core:on_moved() else cmp.core:reset() cmp.core.view:close() end end) -- If make this asynchronous, the completion menu will not close when the command output is displayed. autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave', 'CmdwinEnter' }, function() cmp.core:reset() cmp.core.view:close() end) cmp.event:on('complete_done', function(evt) if evt.entry then cmp.config.compare.recently_used:add_entry(evt.entry) end cmp.config.compare.scopes:update() cmp.config.compare.locality:update() end) cmp.event:on('confirm_done', function(evt) if evt.entry then cmp.config.compare.recently_used:add_entry(evt.entry) end end) return cmp ================================================ FILE: lua/cmp/matcher.lua ================================================ local char = require('cmp.utils.char') local matcher = {} matcher.WORD_BOUNDALY_ORDER_FACTOR = 10 matcher.PREFIX_FACTOR = 8 matcher.NOT_FUZZY_FACTOR = 6 ---@type function matcher.debug = function(...) return ... end --- score -- -- ### The score -- -- The `score` is `matched char count` generally. -- -- But cmp will fix the score with some of the below points so the actual score is not `matched char count`. -- -- 1. Word boundary order -- -- cmp prefers the match that near by word-beggining. -- -- 2. Strict case -- -- cmp prefers strict match than ignorecase match. -- -- -- ### Matching specs. -- -- 1. Prefix matching per word boundary -- -- `bora` -> `border-radius` # imaginary score: 4 -- ^^~~ ^^ ~~ -- -- 2. Try sequential match first -- -- `woroff` -> `word_offset` # imaginary score: 6 -- ^^^~~~ ^^^ ~~~ -- -- * The `woroff`'s second `o` should not match `word_offset`'s first `o` -- -- 3. Prefer early word boundary -- -- `call` -> `call` # imaginary score: 4.1 -- ^^^^ ^^^^ -- `call` -> `condition_all` # imaginary score: 4 -- ^~~~ ^ ~~~ -- -- 4. Prefer strict match -- -- `Buffer` -> `Buffer` # imaginary score: 6.1 -- ^^^^^^ ^^^^^^ -- `buffer` -> `Buffer` # imaginary score: 6 -- ^^^^^^ ^^^^^^ -- -- 5. Use remaining characters for substring match -- -- `fmodify` -> `fnamemodify` # imaginary score: 1 -- ^~~~~~~ ^ ~~~~~~ -- -- 6. Avoid unexpected match detection -- -- `candlesingle` -> candle#accept#single -- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~ -- * The `accept`'s `a` should not match to `candle`'s `a` -- -- 7. Avoid false positive matching -- -- `,` -> print, -- ~ -- * Typically, the middle match with symbol characters only is false positive. should be ignored. -- This doesn't work for command line completions like ":b foo_" which we like to match -- "lib/foo_bar.txt". The option disallow_symbol_nonprefix_matching controls this and defaults -- to preventing matches like these. The documentation recommends it for command line completion. -- -- ---Match entry ---@param input string ---@param word string ---@param option { synonyms: string[], disallow_fullfuzzy_matching: boolean, disallow_fuzzy_matching: boolean, disallow_partial_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean, disallow_symbol_nonprefix_matching: boolean } ---@return integer, table matcher.match = function(input, word, option) option = option or {} -- Empty input if #input == 0 then return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {} end -- Ignore if input is long than word if #input > #word then return 0, {} end -- Check prefix matching. if option.disallow_prefix_unmatching then if not char.match(string.byte(input, 1), string.byte(word, 1)) then return 0, {} end end -- Gather matched regions local matches = {} local input_start_index = 1 local input_end_index = 1 local word_index = 1 local word_bound_index = 1 local no_symbol_match = false while input_end_index <= #input and word_index <= #word do local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index) if m and input_end_index <= m.input_match_end then m.index = word_bound_index input_start_index = m.input_match_start + 1 input_end_index = m.input_match_end + 1 no_symbol_match = no_symbol_match or m.no_symbol_match word_index = char.get_next_semantic_index(word, m.word_match_end) table.insert(matches, m) else word_index = char.get_next_semantic_index(word, word_index) end word_bound_index = word_bound_index + 1 end -- Check partial matching. if option.disallow_partial_matching and #matches > 1 then return 0, {} end if #matches == 0 then if not option.disallow_fuzzy_matching and not option.disallow_prefix_unmatching and not option.disallow_partial_fuzzy_matching then if matcher.fuzzy(input, word, matches, option) then return 1, matches end end return 0, {} end matcher.debug(word, matches) -- Add prefix bonus local prefix = false if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then prefix = true else for _, synonym in ipairs(option.synonyms or {}) do prefix = true local o = 1 for i = matches[1].input_match_start, matches[1].input_match_end do if not char.match(string.byte(synonym, o), string.byte(input, i)) then prefix = false break end o = o + 1 end if prefix then break end end end if no_symbol_match and not prefix then if option.disallow_symbol_nonprefix_matching then return 0, {} end end -- Compute prefix match score local score = prefix and matcher.PREFIX_FACTOR or 0 local offset = prefix and matches[1].index - 1 or 0 local idx = 1 for _, m in ipairs(matches) do local s = 0 for i = math.max(idx, m.input_match_start), m.input_match_end do s = s + 1 idx = i end idx = idx + 1 if s > 0 then s = s * (1 + m.strict_ratio) s = s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - offset)) / matcher.WORD_BOUNDALY_ORDER_FACTOR) score = score + s end end -- Check remaining input as fuzzy if matches[#matches].input_match_end < #input then if not option.disallow_fuzzy_matching then if not option.disallow_partial_fuzzy_matching or prefix then if matcher.fuzzy(input, word, matches, option) then return score, matches end end end return 0, {} end return score + matcher.NOT_FUZZY_FACTOR, matches end --- fuzzy matcher.fuzzy = function(input, word, matches, option) local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1 -- Lately specified middle of text. for i = 1, #matches - 1 do local curr_match = matches[i] local next_match = matches[i + 1] local word_offset = 0 local word_index = char.get_next_semantic_index(word, curr_match.word_match_end) while word_offset + word_index < next_match.word_match_start and input_index <= #input do if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then input_index = input_index + 1 word_offset = word_offset + 1 else word_index = char.get_next_semantic_index(word, word_index + word_offset) word_offset = 0 end end end -- Remaining text fuzzy match. local matched = false local word_offset = 0 local word_index = matches[#matches] and (matches[#matches].word_match_end + 1) or 1 local input_match_start = -1 local input_match_end = -1 local word_match_start = -1 local strict_count = 0 local match_count = 0 while word_offset + word_index <= #word and input_index <= #input do local c1, c2 = string.byte(word, word_index + word_offset), string.byte(input, input_index) if char.match(c1, c2) then if not matched then input_match_start = input_index word_match_start = word_index + word_offset end matched = true input_index = input_index + 1 strict_count = strict_count + (c1 == c2 and 1 or 0) match_count = match_count + 1 else if option.disallow_fullfuzzy_matching then break else if matched then table.insert(matches, { input_match_start = input_match_start, input_match_end = input_index - 1, word_match_start = word_match_start, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, fuzzy = true, }) end end matched = false end word_offset = word_offset + 1 end if matched and input_index > #input then table.insert(matches, { input_match_start = input_match_start, input_match_end = input_match_end, word_match_start = word_match_start, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, fuzzy = true, }) return true end return false end --- find_match_region matcher.find_match_region = function(input, input_start_index, input_end_index, word, word_index) -- determine input position ( woroff -> word_offset ) while input_start_index < input_end_index do if char.match(string.byte(input, input_end_index), string.byte(word, word_index)) then break end input_end_index = input_end_index - 1 end -- Can't determine input position if input_end_index < input_start_index then return nil end local input_match_start = -1 local input_index = input_end_index local word_offset = 0 local strict_count = 0 local match_count = 0 local no_symbol_match = false while input_index <= #input and word_index + word_offset <= #word do local c1 = string.byte(input, input_index) local c2 = string.byte(word, word_index + word_offset) if char.match(c1, c2) then -- Match start. if input_match_start == -1 then input_match_start = input_index end strict_count = strict_count + (c1 == c2 and 1 or 0) match_count = match_count + 1 word_offset = word_offset + 1 no_symbol_match = no_symbol_match or char.is_symbol(c1) else -- Match end (partial region) if input_match_start ~= -1 then return { input_match_start = input_match_start, input_match_end = input_index - 1, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, no_symbol_match = no_symbol_match, fuzzy = false, } else return nil end end input_index = input_index + 1 end -- Match end (whole region) if input_match_start ~= -1 then return { input_match_start = input_match_start, input_match_end = input_index - 1, word_match_start = word_index, word_match_end = word_index + word_offset - 1, strict_ratio = strict_count / match_count, no_symbol_match = no_symbol_match, fuzzy = false, } end return nil end return matcher ================================================ FILE: lua/cmp/matcher_spec.lua ================================================ local spec = require('cmp.utils.spec') local default_config = require('cmp.config.default') local matcher = require('cmp.matcher') describe('matcher', function() before_each(spec.before) it('match', function() local config = default_config() assert.is.truthy(matcher.match('', 'a', config.matching) >= 1) assert.is.truthy(matcher.match('a', 'a', config.matching) >= 1) assert.is.truthy(matcher.match('ab', 'a', config.matching) == 0) assert.is.truthy(matcher.match('ab', 'ab', config.matching) > matcher.match('ab', 'a_b', config.matching)) assert.is.truthy(matcher.match('ab', 'a_b_c', config.matching) > matcher.match('ac', 'a_b_c', config.matching)) assert.is.truthy(matcher.match('bora', 'border-radius', config.matching) >= 1) assert.is.truthy(matcher.match('woroff', 'word_offset', config.matching) >= 1) assert.is.truthy(matcher.match('call', 'call', config.matching) > matcher.match('call', 'condition_all', config.matching)) assert.is.truthy(matcher.match('Buffer', 'Buffer', config.matching) > matcher.match('Buffer', 'buffer', config.matching)) assert.is.truthy(matcher.match('luacon', 'lua_context', config.matching) > matcher.match('luacon', 'LuaContext', config.matching)) assert.is.truthy(matcher.match('fmodify', 'fnamemodify', config.matching) >= 1) assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single', config.matching) >= 1) assert.is.truthy(matcher.match('vi', 'void#', config.matching) >= 1) assert.is.truthy(matcher.match('vo', 'void#', config.matching) >= 1) assert.is.truthy(matcher.match('var_', 'var_dump', config.matching) >= 1) assert.is.truthy(matcher.match('conso', 'console', config.matching) > matcher.match('conso', 'ConstantSourceNode', config.matching)) assert.is.truthy(matcher.match('usela', 'useLayoutEffect', config.matching) > matcher.match('usela', 'useDataLayer', config.matching)) assert.is.truthy(matcher.match('my_', 'my_awesome_variable', config.matching) > matcher.match('my_', 'completion_matching_strategy_list', config.matching)) assert.is.truthy(matcher.match('2', '[[2021', config.matching) >= 1) assert.is.truthy(matcher.match(',', 'pri,', config.matching) == 0) assert.is.truthy(matcher.match('/', '/**', config.matching) >= 1) assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }, config.matching) == matcher.match('true', 'true', config.matching)) assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }, config.matching) > matcher.match('g', 'dein#get', { 'dein#get' }, config.matching)) assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }, config.matching) == 0) assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }, config.matching) >= 1) assert.is.truthy(matcher.match('emg', 'error_msg', config.matching) >= 1) assert.is.truthy(matcher.match('sasr', 'saved_splitright', config.matching) >= 1) -- TODO: #1420 test-case -- assert.is.truthy(matcher.match('asset_', '????') >= 0) local score, matches score, matches = matcher.match('tail', 'HCDetails', { disallow_fuzzy_matching = false, disallow_partial_matching = false, disallow_prefix_unmatching = false, disallow_partial_fuzzy_matching = false, disallow_symbol_nonprefix_matching = true, }) assert.is.truthy(score >= 1) assert.equals(matches[1].word_match_start, 5) score = matcher.match('tail', 'HCDetails', { disallow_fuzzy_matching = false, disallow_partial_matching = false, disallow_prefix_unmatching = false, disallow_partial_fuzzy_matching = true, disallow_symbol_nonprefix_matching = true, }) assert.is.truthy(score == 0) end) it('disallow_fuzzy_matching', function() assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0) assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) end) it('disallow_fullfuzzy_matching', function() assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0) assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = false }) >= 1) end) it('disallow_partial_matching', function() assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0) assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1) assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1) end) it('disallow_prefix_unmatching', function() assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0) assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1) end) it('disallow_symbol_nonprefix_matching', function() assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = true }) == 0) assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = false }) >= 1) end) it('debug', function() matcher.debug = function(...) print(vim.inspect({ ... })) end -- print(vim.inspect({ -- a = matcher.match('true', 'v:true', { 'true' }), -- b = matcher.match('true', 'true'), -- })) end) end) ================================================ FILE: lua/cmp/source.lua ================================================ local context = require('cmp.context') local config = require('cmp.config') local entry = require('cmp.entry') local debug = require('cmp.utils.debug') local misc = require('cmp.utils.misc') local cache = require('cmp.utils.cache') local types = require('cmp.types') local async = require('cmp.utils.async') local pattern = require('cmp.utils.pattern') local char = require('cmp.utils.char') ---@class cmp.Source ---@field public id integer ---@field public name string ---@field public source any ---@field public cache cmp.Cache ---@field public revision integer ---@field public response? lsp.CompletionResponse|nil ---@field public incomplete boolean ---@field public is_triggered_by_symbol boolean ---@field public entries cmp.Entry[] ---@field public offset integer ---@field public request_offset integer ---@field public context cmp.Context ---@field public completion_context lsp.CompletionContext|nil ---@field public status cmp.SourceStatus ---@field public complete_dedup function ---@field public default_replace_range lsp.Range ---@field public default_insert_range lsp.Range ---@field public position_encoding lsp.PositionEncodingKind local source = {} ---@alias cmp.SourceStatus 1 | 2 | 3 source.SourceStatus = {} source.SourceStatus.WAITING = 1 source.SourceStatus.FETCHING = 2 source.SourceStatus.COMPLETED = 3 ---@return cmp.Source source.new = function(name, s) local self = setmetatable({}, { __index = source }) self.id = misc.id('cmp.source.new') self.name = name self.source = s self.cache = cache.new() self.complete_dedup = async.dedup() self.revision = 0 self.position_encoding = self:get_position_encoding_kind() self:reset() return self end ---Reset current completion state source.reset = function(self) self.cache:clear() self.revision = self.revision + 1 self.context = context.empty() self.is_triggered_by_symbol = false self.incomplete = false self.entries = {} self.offset = -1 self.request_offset = -1 self.completion_context = nil self.status = source.SourceStatus.WAITING self.complete_dedup(function() end) end ---Return source config ---@return cmp.SourceConfig source.get_source_config = function(self) return config.get_source_config(self.name) or {} end ---Return matching config ---@return cmp.MatchingConfig source.get_matching_config = function() return config.get().matching end ---Get fetching time source.get_fetching_time = function(self) if self.status == source.SourceStatus.FETCHING then return vim.loop.now() - self.context.time end return 100 * 1000 -- return pseudo time if source isn't fetching. end ---Return filtered entries ---@param ctx cmp.Context ---@return cmp.Entry[] source.get_entries = function(self, ctx) if self.offset == -1 then return {} end local target_entries = self.entries if not self.incomplete then local prev = self.cache:get({ 'get_entries', tostring(self.revision) }) if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then -- only use prev entries when cursor is moved forward. -- and the pattern offset is the same. if prev.ctx.cursor.col <= ctx.cursor.col then target_entries = prev.entries end end end local entry_filter = self:get_entry_filter() local inputs = {} ---@type cmp.Entry[] local entries = {} local matching_config = self:get_matching_config() local filtering_context_budget = config.get().performance.filtering_context_budget / 1000 local stime = (vim.uv or vim.loop).hrtime() / 1000000 for _, e in ipairs(target_entries) do local o = e.offset if not inputs[o] then inputs[o] = string.sub(ctx.cursor_before_line, o) end local match = e:match(inputs[o], matching_config) e.score = match.score e.exact = false if e.score >= 1 then e.matches = match.matches e.exact = e.filter_text == inputs[o] or e.word == inputs[o] if entry_filter(e, ctx) then entries[#entries + 1] = e end end local etime = (vim.uv or vim.loop).hrtime() / 1000000 if etime - stime > filtering_context_budget then async.yield() if ctx.aborted then async.abort() end stime = etime end end if not self.incomplete then self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset }) end return entries end ---Get default insert range (UTF8 byte index). ---@package ---@return lsp.Range source._get_default_insert_range = function(self) return { start = { line = self.context.cursor.row - 1, character = self.offset - 1, }, ['end'] = { line = self.context.cursor.row - 1, character = self.context.cursor.col - 1, }, } end ---Get default replace range (UTF8 byte index). ---@package ---@return lsp.Range source._get_default_replace_range = function(self) local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset)) return { start = { line = self.context.cursor.row - 1, character = self.offset, }, ['end'] = { line = self.context.cursor.row - 1, character = (e and self.offset + e - 2 or self.context.cursor.col - 1), }, } end ---@deprecated use source.default_insert_range instead source.get_default_insert_range = function(self) return self.default_insert_range end ---@deprecated use source.default_replace_range instead source.get_default_replae_range = function(self) return self.default_replace_range end ---Return source name. source.get_debug_name = function(self) local name = self.name if self.source.get_debug_name then name = self.source:get_debug_name() end return name end ---Return the source is available or not. source.is_available = function(self) if self.source.is_available then return self.source:is_available() end return true end ---Get trigger_characters ---@return string[] source.get_trigger_characters = function(self) local c = self:get_source_config() if c.trigger_characters then return c.trigger_characters end local trigger_characters = {} if self.source.get_trigger_characters then trigger_characters = self.source:get_trigger_characters(misc.copy(c)) or {} end if config.get().completion.get_trigger_characters then return config.get().completion.get_trigger_characters(trigger_characters) end return trigger_characters end ---Get keyword_pattern ---@return string source.get_keyword_pattern = function(self) local c = self:get_source_config() if c.keyword_pattern then return c.keyword_pattern end if self.source.get_keyword_pattern then local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c)) if keyword_pattern then return keyword_pattern end end return config.get().completion.keyword_pattern end ---Get keyword_length ---@return integer source.get_keyword_length = function(self) local c = self:get_source_config() if c.keyword_length then return c.keyword_length end return config.get().completion.keyword_length or 1 end ---Get filter --@return fun(entry: cmp.Entry, context: cmp.Context): boolean source.get_entry_filter = function(self) local c = self:get_source_config() if c.entry_filter then return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]] end return function(_, _) return true end end ---Get lsp.PositionEncodingKind ---@return lsp.PositionEncodingKind source.get_position_encoding_kind = function(self) if self.source.get_position_encoding_kind then return self.source:get_position_encoding_kind() end return types.lsp.PositionEncodingKind.UTF16 end ---Invoke completion ---@param ctx cmp.Context ---@param callback function ---@return boolean? Return true if not trigger completion. source.complete = function(self, ctx, callback) local offset = ctx:get_offset(self:get_keyword_pattern()) -- NOTE: This implementation is nvim-cmp specific. -- We trigger new completion after core.confirm but we check only the symbol trigger_character in this case. local before_char = string.sub(ctx.cursor_before_line, -1) if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then before_char = string.match(ctx.cursor_before_line, '(.)%s*$') if not before_char or not char.is_symbol(string.byte(before_char)) then before_char = '' end end local completion_context if ctx:get_reason() == types.cmp.ContextReason.Manual then completion_context = { triggerKind = types.lsp.CompletionTriggerKind.Invoked, } elseif vim.tbl_contains(self:get_trigger_characters(), before_char) then completion_context = { triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, triggerCharacter = before_char, } elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then if offset < ctx.cursor.col and self:get_keyword_length() <= (ctx.cursor.col - offset) then if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then completion_context = { triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions, } elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then completion_context = { triggerKind = types.lsp.CompletionTriggerKind.Invoked, } end else self:reset() -- Should clear current completion if the TriggerKind isn't TriggerCharacter or Manual and keyword length does not enough. end else self:reset() -- Should clear current completion if ContextReason is TriggerOnly and the triggerCharacter isn't matched end -- Does not perform completions. if not completion_context then return end if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then self.is_triggered_by_symbol = char.is_symbol(string.byte(completion_context.triggerCharacter)) end debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context)) local prev_status = self.status self.status = source.SourceStatus.FETCHING self.offset = offset self.request_offset = offset self.context = ctx self.default_replace_range = self:_get_default_replace_range() self.default_insert_range = self:_get_default_insert_range() self.position_encoding = self:get_position_encoding_kind() self.completion_context = completion_context self.source:complete( vim.tbl_extend('keep', misc.copy(self:get_source_config()), { offset = self.offset, context = ctx, completion_context = completion_context, }), self.complete_dedup(vim.schedule_wrap(function(response) if self.context ~= ctx then return end ---@type lsp.CompletionResponse response = response or {} self.response = response self.incomplete = response.isIncomplete or false if #(response.items or response) > 0 then debug.log(self:get_debug_name(), 'retrieve', #(response.items or response)) local old_offset = self.offset local old_entries = self.entries self.status = source.SourceStatus.COMPLETED self.entries = {} for _, item in ipairs(response.items or response) do if item.label then local e = entry.new(ctx, self, item, response.itemDefaults) if not e:is_invalid() then table.insert(self.entries, e) self.offset = math.min(self.offset, e.offset) end end end self.revision = self.revision + 1 if #self.entries == 0 then self.offset = old_offset self.entries = old_entries self.revision = self.revision + 1 end else -- The completion will be invoked when pressing if the trigger characters contain the . -- If the server returns an empty response in such a case, should invoke the keyword completion on the next keypress. if offset == ctx.cursor.col then self:reset() end self.status = prev_status end callback() end)) ) return true end ---Resolve CompletionItem ---@param item lsp.CompletionItem ---@param callback fun(item: lsp.CompletionItem) source.resolve = function(self, item, callback) if not self.source.resolve then return callback(item) end self.source:resolve(item, function(resolved_item) callback(resolved_item or item) end) end ---Execute command ---@param item lsp.CompletionItem ---@param callback fun() source.execute = function(self, item, callback) if not self.source.execute then return callback() end self.source:execute(item, function() callback() end) end return source ================================================ FILE: lua/cmp/source_spec.lua ================================================ local config = require('cmp.config') local spec = require('cmp.utils.spec') local source = require('cmp.source') describe('source', function() before_each(spec.before) describe('keyword length', function() it('not enough', function() config.set_buffer({ completion = { keyword_length = 3, }, }, vim.api.nvim_get_current_buf()) local state = spec.state('', 1, 1) local s = source.new('spec', { complete = function(_, _, callback) callback({ { label = 'spec' } }) end, }) assert.is.truthy(not s:complete(state.input('a'), function() end)) end) it('enough', function() config.set_buffer({ completion = { keyword_length = 3, }, }, vim.api.nvim_get_current_buf()) local state = spec.state('', 1, 1) local s = source.new('spec', { complete = function(_, _, callback) callback({ { label = 'spec' } }) end, }) assert.is.truthy(s:complete(state.input('aiu'), function() end)) end) it('enough -> not enough', function() config.set_buffer({ completion = { keyword_length = 3, }, }, vim.api.nvim_get_current_buf()) local state = spec.state('', 1, 1) local s = source.new('spec', { complete = function(_, _, callback) callback({ { label = 'spec' } }) end, }) assert.is.truthy(s:complete(state.input('aiu'), function() end)) assert.is.truthy(not s:complete(state.backspace(), function() end)) end) it('continue', function() config.set_buffer({ completion = { keyword_length = 3, }, }, vim.api.nvim_get_current_buf()) local state = spec.state('', 1, 1) local s = source.new('spec', { complete = function(_, _, callback) callback({ { label = 'spec' } }) end, }) assert.is.truthy(s:complete(state.input('aiu'), function() end)) assert.is.truthy(not s:complete(state.input('eo'), function() end)) end) end) describe('isIncomplete', function() it('isIncomplete=true', function() local state = spec.state('', 1, 1) local s = source.new('spec', { complete = function(_, _, callback) callback({ items = { { label = 'spec' } }, isIncomplete = true, }) end, }) vim.wait(100, function() return s.status == source.SourceStatus.COMPLETED end, 100, false) assert.is.truthy(s:complete(state.input('s'), function() end)) vim.wait(100, function() return s.status == source.SourceStatus.COMPLETED end, 100, false) assert.is.truthy(s:complete(state.input('p'), function() end)) vim.wait(100, function() return s.status == source.SourceStatus.COMPLETED end, 100, false) assert.is.truthy(s:complete(state.input('e'), function() end)) vim.wait(100, function() return s.status == source.SourceStatus.COMPLETED end, 100, false) assert.is.truthy(s:complete(state.input('c'), function() end)) vim.wait(100, function() return s.status == source.SourceStatus.COMPLETED end, 100, false) end) end) end) ================================================ FILE: lua/cmp/types/cmp.lua ================================================ local cmp = {} ---@alias cmp.ConfirmBehavior 'insert' | 'replace' cmp.ConfirmBehavior = { Insert = 'insert', Replace = 'replace', } ---@alias cmp.SelectBehavior 'insert' | 'select' cmp.SelectBehavior = { Insert = 'insert', Select = 'select', } ---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none' cmp.ContextReason = { Auto = 'auto', Manual = 'manual', TriggerOnly = 'triggerOnly', None = 'none', } ---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged' cmp.TriggerEvent = { InsertEnter = 'InsertEnter', TextChanged = 'TextChanged', } ---@alias cmp.PreselectMode 'item' | 'None' cmp.PreselectMode = { Item = 'item', None = 'none', } ---@alias cmp.ItemField 'abbr' | 'icon' | 'kind' | 'menu' cmp.ItemField = { Abbr = 'abbr', Icon = 'icon', Kind = 'kind', Menu = 'menu', } ---@class cmp.ContextOption ---@field public reason cmp.ContextReason|nil ---@class cmp.ConfirmOption ---@field public behavior cmp.ConfirmBehavior ---@field public commit_character? string ---@class cmp.SelectOption ---@field public behavior cmp.SelectBehavior ---@class cmp.SnippetExpansionParams ---@field public body string ---@field public insert_text_mode integer ---@class cmp.CompleteParams ---@field public reason? cmp.ContextReason ---@field public config? cmp.ConfigSchema ---@class cmp.SetupProperty ---@field public buffer fun(c: cmp.ConfigSchema) ---@field public global fun(c: cmp.ConfigSchema) ---@field public cmdline fun(type: string|string[], c: cmp.ConfigSchema) ---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema) ---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema) ---@class cmp.SourceApiParams: cmp.SourceConfig ---@class cmp.SourceCompletionApiParams : cmp.SourceConfig ---@field public offset integer ---@field public context cmp.Context ---@field public completion_context lsp.CompletionContext ---@alias cmp.MappingFunction fun(fallback: function): nil ---@class cmp.MappingClass ---@field public i nil|cmp.MappingFunction ---@field public c nil|cmp.MappingFunction ---@field public x nil|cmp.MappingFunction ---@field public s nil|cmp.MappingFunction ---@alias cmp.Mapping cmp.MappingFunction | cmp.MappingClass ---@class cmp.ConfigSchema ---@field private revision? integer ---@field public enabled? boolean | fun(): boolean ---@field public performance? cmp.PerformanceConfig ---@field public preselect? cmp.PreselectMode ---@field public completion? cmp.CompletionConfig ---@field public window? cmp.WindowConfig|nil ---@field public confirmation? cmp.ConfirmationConfig ---@field public matching? cmp.MatchingConfig ---@field public sorting? cmp.SortingConfig ---@field public formatting? cmp.FormattingConfig ---@field public snippet? cmp.SnippetConfig ---@field public mapping? table ---@field public sources? cmp.SourceConfig[] ---@field public view? cmp.ViewConfig ---@field public experimental? cmp.ExperimentalConfig ---@class cmp.PerformanceConfig ---@field public debounce integer ---@field public throttle integer ---@field public fetching_timeout integer ---@field public filtering_context_budget integer ---@field public confirm_resolve_timeout integer ---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop. ---@field public max_view_entries integer ---@class cmp.CompletionConfig ---@field public autocomplete? cmp.TriggerEvent[]|false ---@field public completeopt? string ---@field public get_trigger_characters? fun(trigger_characters: string[]): string[] ---@field public keyword_length? integer ---@field public keyword_pattern? string ---@class cmp.WindowConfig ---@field public completion? cmp.CompletionWindowOptions ---@field public documentation? cmp.DocumentationWindowOptions|vim.NIL ---@class cmp.WindowOptions ---@field public border? string|string[] ---@field public winhighlight? string ---@field public winblend? number ---@field public zindex? integer|nil ---@class cmp.CompletionWindowOptions: cmp.WindowOptions ---@field public scrolloff? integer|nil ---@field public col_offset? integer|nil ---@field public side_padding? integer|nil ---@field public scrollbar? boolean|true ---@field public max_height? integer|nil ---@class cmp.DocumentationWindowOptions: cmp.WindowOptions ---@field public max_height? integer|nil ---@field public max_width? integer|nil ---@field public scrolloff integer|nil ---@field public scrollbar boolean|true ---@field public col_offset integer|nil ---@class cmp.ConfirmationConfig ---@field public default_behavior cmp.ConfirmBehavior ---@field public get_commit_characters fun(commit_characters: string[]): string[] ---@class cmp.MatchingConfig ---@field public disallow_fuzzy_matching boolean ---@field public disallow_fullfuzzy_matching boolean ---@field public disallow_partial_fuzzy_matching boolean ---@field public disallow_partial_matching boolean ---@field public disallow_prefix_unmatching boolean ---@field public disallow_symbol_nonprefix_matching boolean ---@class cmp.SortingConfig ---@field public priority_weight integer ---@field public comparators cmp.Comparator[] ---@class cmp.FormattingConfig ---@field public fields? cmp.ItemField[] ---@field public expandable_indicator? boolean ---@field public format? fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem ---@class cmp.SnippetConfig ---@field public expand fun(args: cmp.SnippetExpansionParams) ---@class cmp.ExperimentalConfig ---@field public ghost_text cmp.GhostTextConfig|boolean ---@class cmp.GhostTextConfig ---@field hl_group string ---@class cmp.SourceConfig ---@field public name string ---@field public option table|nil ---@field public priority integer|nil ---@field public trigger_characters string[]|nil ---@field public keyword_pattern string|nil ---@field public keyword_length integer|nil ---@field public max_item_count integer|nil ---@field public group_index integer|nil ---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean ---@class cmp.ViewConfig ---@field public entries? cmp.EntriesViewConfig ---@field public docs? cmp.DocsViewConfig ---@alias cmp.EntriesViewConfig cmp.CustomEntriesViewConfig|cmp.NativeEntriesViewConfig|cmp.WildmenuEntriesViewConfig|string ---@class cmp.CustomEntriesViewConfig ---@field name 'custom' ---@field selection_order 'top_down'|'near_cursor' ---@field vertical_positioning 'auto'|'above'|'below' ---@field follow_cursor boolean ---@class cmp.NativeEntriesViewConfig ---@field name 'native' ---@class cmp.WildmenuEntriesViewConfig ---@field name 'wildmenu' ---@field separator string|nil ---@class cmp.DocsViewConfig ---@field public auto_open boolean return cmp ================================================ FILE: lua/cmp/types/init.lua ================================================ local types = {} types.cmp = require('cmp.types.cmp') types.lsp = require('cmp.types.lsp') types.vim = require('cmp.types.vim') return types ================================================ FILE: lua/cmp/types/lsp.lua ================================================ local misc = require('cmp.utils.misc') ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/ ---@class lsp local lsp = {} ---@enum lsp.PositionEncodingKind lsp.PositionEncodingKind = { UTF8 = 'utf-8', UTF16 = 'utf-16', UTF32 = 'utf-32', } lsp.Position = { ---Convert lsp.Position to vim.Position ---@param buf integer ---@param position lsp.Position -- ---@return vim.Position to_vim = function(buf, position) if not vim.api.nvim_buf_is_loaded(buf) then vim.fn.bufload(buf) end local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) if #lines > 0 then return { row = position.line + 1, col = misc.to_vimindex(lines[1], position.character), } end return { row = position.line + 1, col = position.character + 1, } end, ---Convert vim.Position to lsp.Position ---@param buf integer ---@param position vim.Position ---@return lsp.Position to_lsp = function(buf, position) if not vim.api.nvim_buf_is_loaded(buf) then vim.fn.bufload(buf) end local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) if #lines > 0 then return { line = position.row - 1, character = misc.to_utfindex(lines[1], position.col), } end return { line = position.row - 1, character = position.col - 1, } end, ---Convert position to utf8 from specified encoding. ---@param text string ---@param position lsp.Position ---@param from_encoding? lsp.PositionEncodingKind ---@return lsp.Position to_utf8 = function(text, position, from_encoding) from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 if from_encoding == lsp.PositionEncodingKind.UTF8 then return position end local ok, byteindex = pcall(vim.str_byteindex, text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16) if not ok then return position end return { line = position.line, character = byteindex } end, ---Convert position to utf16 from specified encoding. ---@param text string ---@param position lsp.Position ---@param from_encoding? lsp.PositionEncodingKind ---@return lsp.Position to_utf16 = function(text, position, from_encoding) from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 if from_encoding == lsp.PositionEncodingKind.UTF16 then return position end local utf8 = lsp.Position.to_utf8(text, position, from_encoding) for index = utf8.character, 0, -1 do local ok, utf16index = pcall(function() return select(2, vim.str_utfindex(text, index)) end) if ok then return { line = utf8.line, character = utf16index } end end return position end, ---Convert position to utf32 from specified encoding. ---@param text string ---@param position lsp.Position ---@param from_encoding? lsp.PositionEncodingKind ---@return lsp.Position to_utf32 = function(text, position, from_encoding) from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 if from_encoding == lsp.PositionEncodingKind.UTF32 then return position end local utf8 = lsp.Position.to_utf8(text, position, from_encoding) for index = utf8.character, 0, -1 do local ok, utf32index = pcall(function() return select(1, vim.str_utfindex(text, index)) end) if ok then return { line = utf8.line, character = utf32index } end end return position end, } lsp.Range = { ---Convert lsp.Range to vim.Range ---@param buf integer ---@param range lsp.Range ---@return vim.Range to_vim = function(buf, range) return { start = lsp.Position.to_vim(buf, range.start), ['end'] = lsp.Position.to_vim(buf, range['end']), } end, ---Convert vim.Range to lsp.Range ---@param buf integer ---@param range vim.Range ---@return lsp.Range to_lsp = function(buf, range) return { start = lsp.Position.to_lsp(buf, range.start), ['end'] = lsp.Position.to_lsp(buf, range['end']), } end, } ---@alias lsp.CompletionTriggerKind 1 | 2 | 3 lsp.CompletionTriggerKind = { Invoked = 1, TriggerCharacter = 2, TriggerForIncompleteCompletions = 3, } ---@alias lsp.InsertTextFormat 1 | 2 lsp.InsertTextFormat = {} lsp.InsertTextFormat.PlainText = 1 lsp.InsertTextFormat.Snippet = 2 ---@alias lsp.InsertTextMode 1 | 2 lsp.InsertTextMode = { AsIs = 1, AdjustIndentation = 2, } ---@alias lsp.MarkupKind 'plaintext' | 'markdown' lsp.MarkupKind = { PlainText = 'plaintext', Markdown = 'markdown', } ---@alias lsp.CompletionItemTag 1 lsp.CompletionItemTag = { Deprecated = 1, } ---@alias lsp.CompletionItemKind 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 lsp.CompletionItemKind = { Text = 1, Method = 2, Function = 3, Constructor = 4, Field = 5, Variable = 6, Class = 7, Interface = 8, Module = 9, Property = 10, Unit = 11, Value = 12, Enum = 13, Keyword = 14, Snippet = 15, Color = 16, File = 17, Reference = 18, Folder = 19, EnumMember = 20, Constant = 21, Struct = 22, Event = 23, Operator = 24, TypeParameter = 25, } for _, k in ipairs(vim.tbl_keys(lsp.CompletionItemKind)) do local v = lsp.CompletionItemKind[k] lsp.CompletionItemKind[v] = k end ---@class lsp.internal.CompletionItemDefaults ---@field public commitCharacters? string[] ---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range } ---@field public insertTextFormat? lsp.InsertTextFormat ---@field public insertTextMode? lsp.InsertTextMode ---@field public data? any ---@class lsp.CompletionContext ---@field public triggerKind lsp.CompletionTriggerKind ---@field public triggerCharacter string|nil ---@class lsp.CompletionList ---@field public isIncomplete boolean ---@field public itemDefaults? lsp.internal.CompletionItemDefaults ---@field public items lsp.CompletionItem[] ---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[] ---@class lsp.MarkupContent ---@field public kind lsp.MarkupKind ---@field public value string ---@class lsp.Position ---@field public line integer ---@field public character integer ---@class lsp.Range ---@field public start lsp.Position ---@field public end lsp.Position ---@class lsp.Command ---@field public title string ---@field public command string ---@field public arguments any[]|nil ---@class lsp.TextEdit ---@field public range lsp.Range|nil ---@field public newText string ---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit ---@class lsp.internal.InsertTextEdit ---@field public insert lsp.Range ---@field public newText string ---@class lsp.internal.ReplaceTextEdit ---@field public replace lsp.Range ---@field public newText string ---@class lsp.CompletionItemLabelDetails ---@field public detail? string ---@field public description? string ---@class lsp.internal.CmpCompletionExtension ---@field public icon string ---@field public icon_hl_group string ---@field public kind_text string ---@field public kind_hl_group string ---@class lsp.CompletionItem ---@field public label string ---@field public labelDetails? lsp.CompletionItemLabelDetails ---@field public kind? lsp.CompletionItemKind ---@field public tags? lsp.CompletionItemTag[] ---@field public detail? string ---@field public documentation? lsp.MarkupContent|string ---@field public deprecated? boolean ---@field public preselect? boolean ---@field public sortText? string ---@field public filterText? string ---@field public insertText? string ---@field public insertTextFormat? lsp.InsertTextFormat ---@field public insertTextMode? lsp.InsertTextMode ---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit ---@field public textEditText? string ---@field public additionalTextEdits? lsp.TextEdit[] ---@field public commitCharacters? string[] ---@field public command? lsp.Command ---@field public data? any ---@field public cmp? lsp.internal.CmpCompletionExtension --- ---TODO: Should send the issue for upstream? ---@field public word string|nil ---@field public dup boolean|nil return lsp ================================================ FILE: lua/cmp/types/lsp_spec.lua ================================================ local spec = require('cmp.utils.spec') local lsp = require('cmp.types.lsp') describe('types.lsp', function() before_each(spec.before) describe('Position', function() vim.fn.setline('1', { 'あいうえお', 'かきくけこ', 'さしすせそ', }) local vim_position, lsp_position local bufnr = vim.api.nvim_get_current_buf() vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 3 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 10) lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 3) vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 0 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 1) lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 0) vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 5 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 16) lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 5) -- overflow (lsp -> vim) vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 6 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 16) -- overflow(vim -> lsp) vim_position.col = vim_position.col + 1 lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 5) end) end) ================================================ FILE: lua/cmp/types/vim.lua ================================================ ---@class vim.CompletedItem ---@field public word string ---@field public abbr string|nil ---@field public icon string|nil ---@field public kind string|nil ---@field public menu string|nil ---@field public equal 1|nil ---@field public empty 1|nil ---@field public dup 1|nil ---@field public id any ---@field public abbr_hl_group string|table|nil ---@field public icon_hl_group string|table|nil ---@field public kind_hl_group string|table|nil ---@field public menu_hl_group string|table|nil ---@class vim.Position 1-based index ---@field public row integer ---@field public col integer ---@class vim.Range ---@field public start vim.Position ---@field public end vim.Position ================================================ FILE: lua/cmp/utils/api.lua ================================================ local api = {} local CTRL_V = vim.api.nvim_replace_termcodes('', true, true, true) local CTRL_S = vim.api.nvim_replace_termcodes('', true, true, true) api.get_mode = function() local mode = vim.api.nvim_get_mode().mode:sub(1, 1) if mode == 'i' then return 'i' -- insert elseif mode == 'v' or mode == 'V' or mode == CTRL_V then return 'x' -- visual elseif mode == 's' or mode == 'S' or mode == CTRL_S then return 's' -- select elseif mode == 'c' and vim.fn.getcmdtype() ~= '=' then return 'c' -- cmdline end end api.is_insert_mode = function() return api.get_mode() == 'i' end api.is_cmdline_mode = function() return api.get_mode() == 'c' end api.is_select_mode = function() return api.get_mode() == 's' end api.is_visual_mode = function() return api.get_mode() == 'x' end api.is_suitable_mode = function() local mode = api.get_mode() return mode == 'i' or mode == 'c' end api.get_current_line = function() if api.is_cmdline_mode() then return vim.fn.getcmdline() end return vim.api.nvim_get_current_line() end ---@return { [1]: integer, [2]: integer } api.get_cursor = function() if api.is_cmdline_mode() then return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option_value('cmdheight', {}) - 1)), vim.fn.getcmdpos() - 1 } end return vim.api.nvim_win_get_cursor(0) end api.get_screen_cursor = function() if api.is_cmdline_mode() then local cursor = api.get_cursor() return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) } end local cursor = api.get_cursor() local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) return { pos.row, pos.col - 1 } end api.get_cursor_before_line = function() local cursor = api.get_cursor() return string.sub(api.get_current_line(), 1, cursor[2]) end --- Applies a list of text edits to a buffer. Preserves 'buflisted' state. ---@param text_edits lsp.TextEdit[] ---@param bufnr integer Buffer id ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' api.apply_text_edits = function(text_edits, bufnr, position_encoding) -- preserve 'buflisted' state because vim.lsp.util.apply_text_edits forces it to true local prev_buflisted = vim.bo[bufnr].buflisted vim.lsp.util.apply_text_edits(text_edits, bufnr, position_encoding) vim.bo[bufnr].buflisted = prev_buflisted end return api ================================================ FILE: lua/cmp/utils/api_spec.lua ================================================ local spec = require('cmp.utils.spec') local keymap = require('cmp.utils.keymap') local feedkeys = require('cmp.utils.feedkeys') local api = require('cmp.utils.api') describe('api', function() before_each(spec.before) describe('get_cursor', function() it('insert-mode', function() local cursor feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() cursor = api.get_cursor() end) assert.are.equal(cursor[2], 11) end) it('cmdline-mode', function() local cursor keymap.set_map(0, 'c', '(cmp-spec-spy)', function() cursor = api.get_cursor() end, { expr = true, noremap = true }) feedkeys.call(keymap.t(':\t1234567890'), 'n') feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') assert.are.equal(cursor[2], 11) end) end) describe('get_screen_cursor', function() it('insert-mode', function() local screen_cursor feedkeys.call(keymap.t('iあいうえお'), 'nx', function() screen_cursor = api.get_screen_cursor() end) assert.are.equal(10, screen_cursor[2]) end) it('cmdline-mode', function() local screen_cursor keymap.set_map(0, 'c', '(cmp-spec-spy)', function() screen_cursor = api.get_screen_cursor() end, { expr = true, noremap = true }) feedkeys.call(keymap.t(':あいうえお'), 'n') feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') assert.are.equal(10, screen_cursor[2]) end) end) describe('get_cursor_before_line', function() it('insert-mode', function() local cursor_before_line feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() cursor_before_line = api.get_cursor_before_line() end) assert.are.same(cursor_before_line, '\t12345678') end) it('cmdline-mode', function() local cursor_before_line keymap.set_map(0, 'c', '(cmp-spec-spy)', function() cursor_before_line = api.get_cursor_before_line() end, { expr = true, noremap = true }) feedkeys.call(keymap.t(':\t1234567890'), 'n') feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') assert.are.same(cursor_before_line, '\t12345678') end) end) end) ================================================ FILE: lua/cmp/utils/async.lua ================================================ local feedkeys = require('cmp.utils.feedkeys') local config = require('cmp.config') local async = {} ---@class cmp.AsyncThrottle ---@field public running boolean ---@field public timeout integer ---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil) ---@field public stop function ---@field public __call function ---@type uv_timer_t[] local timers = {} vim.api.nvim_create_autocmd('VimLeavePre', { callback = function() for _, timer in pairs(timers) do if timer and not timer:is_closing() then timer:stop() timer:close() end end end, }) ---@param fn function ---@param timeout integer ---@return cmp.AsyncThrottle async.throttle = function(fn, timeout) local time = nil local timer = assert(vim.loop.new_timer()) local _async = nil ---@type Async? timers[#timers + 1] = timer local throttle throttle = setmetatable({ running = false, timeout = timeout, sync = function(self, timeout_) if not self.running then return end vim.wait(timeout_ or 1000, function() return not self.running end, 10) end, stop = function(reset_time) if reset_time ~= false then time = nil end -- can't use self here unfortunately throttle.running = false timer:stop() if _async then _async:cancel() _async = nil end end, }, { __call = function(self, ...) local args = { ... } if time == nil then time = vim.loop.now() end self.stop(false) self.running = true timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() vim.schedule(function() time = nil local ret = fn(unpack(args)) if async.is_async(ret) then ---@cast ret Async _async = ret _async:await(function(_, error) _async = nil self.running = false if error and error ~= 'abort' then vim.notify(error, vim.log.levels.ERROR) end end) else self.running = false end end) end) end, }) return throttle end ---Control async tasks. async.step = function(...) local tasks = { ... } local next next = function(...) if #tasks > 0 then table.remove(tasks, 1)(next, ...) end end table.remove(tasks, 1)(next) end ---Timeout callback function ---@param fn function ---@param timeout integer ---@return function async.timeout = function(fn, timeout) local timer local done = false local callback = function(...) if not done then done = true timer:stop() timer:close() fn(...) end end timer = vim.loop.new_timer() timer:start(timeout, 0, function() callback() end) return callback end ---@alias cmp.AsyncDedup fun(callback: function): function ---Create deduplicated callback ---@return function async.dedup = function() local id = 0 return function(callback) id = id + 1 local current = id return function(...) if current == id then callback(...) end end end end ---Convert async process as sync async.sync = function(runner, timeout) local done = false runner(function() done = true end) vim.wait(timeout, function() return done end, 10, false) end ---Wait and callback for next safe state. async.debounce_next_tick = function(callback) local running = false return function() if running then return end running = true vim.schedule(function() running = false callback() end) end end ---Wait and callback for consuming next keymap. async.debounce_next_tick_by_keymap = function(callback) return function() feedkeys.call('', '', callback) end end local Scheduler = {} Scheduler._queue = {} Scheduler._executor = assert(vim.loop.new_check()) function Scheduler.step() local budget = config.get().performance.async_budget * 1e6 local start = vim.loop.hrtime() while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do local a = table.remove(Scheduler._queue, 1) a:_step() if a.running then table.insert(Scheduler._queue, a) end end if #Scheduler._queue == 0 then return Scheduler._executor:stop() end end ---@param a Async function Scheduler.add(a) table.insert(Scheduler._queue, a) if not Scheduler._executor:is_active() then Scheduler._executor:start(vim.schedule_wrap(Scheduler.step)) end end --- @alias AsyncCallback fun(result?:any, error?:string) --- @class Async --- @field running boolean --- @field result? any --- @field error? string --- @field callbacks AsyncCallback[] --- @field thread thread local Async = {} Async.__index = Async function Async.new(fn) local self = setmetatable({}, Async) self.callbacks = {} self.running = true self.thread = coroutine.create(fn) Scheduler.add(self) return self end ---@param result? any ---@param error? string function Async:_done(result, error) if self.running then self.running = false self.result = result self.error = error end for _, callback in ipairs(self.callbacks) do callback(result, error) end -- only run each callback once. -- _done can possibly be called multiple times. -- so we need to clear callbacks after executing them. self.callbacks = {} end function Async:_step() local ok, res = coroutine.resume(self.thread) if not ok then return self:_done(nil, res) elseif res == 'abort' then return self:_done(nil, 'abort') elseif coroutine.status(self.thread) == 'dead' then return self:_done(res) end end function Async:cancel() self:_done(nil, 'abort') end ---@param cb AsyncCallback function Async:await(cb) if not cb then error('callback is required') end if self.running then table.insert(self.callbacks, cb) else cb(self.result, self.error) end end function Async:sync() while self.running do vim.wait(10) end return self.error and error(self.error) or self.result end --- @return boolean function async.is_async(obj) return obj and type(obj) == 'table' and getmetatable(obj) == Async end --- @return fun(...): Async function async.wrap(fn) return function(...) local args = { ... } return Async.new(function() return fn(unpack(args)) end) end end -- This will yield when called from a coroutine function async.yield(...) if coroutine.running() == nil then error('Trying to yield from a non-yieldable context') return ... end return coroutine.yield(...) end function async.abort() return async.yield('abort') end return async ================================================ FILE: lua/cmp/utils/async_spec.lua ================================================ local async = require('cmp.utils.async') describe('utils.async', function() it('throttle', function() local count = 0 local now local f = async.throttle(function() count = count + 1 end, 100) -- 1. delay for 100ms now = vim.loop.now() f.timeout = 100 f() vim.wait(1000, function() return count == 1 end) assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) -- 2. delay for 500ms now = vim.loop.now() f.timeout = 500 f() vim.wait(1000, function() return count == 2 end) assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) -- 4. delay for 500ms and wait 100ms (remain 400ms) f.timeout = 500 f() vim.wait(100) -- remain 400ms -- 5. call immediately (100ms already elapsed from No.4) now = vim.loop.now() f.timeout = 100 f() vim.wait(1000, function() return count == 3 end) assert.is.truthy(math.abs(vim.loop.now() - now) < 10) end) it('step', function() local done = false local step = {} async.step(function(next) vim.defer_fn(function() table.insert(step, 1) next() end, 10) end, function(next) vim.defer_fn(function() table.insert(step, 2) next() end, 10) end, function(next) vim.defer_fn(function() table.insert(step, 3) next() end, 10) end, function() done = true end) vim.wait(1000, function() return done end) assert.are.same(step, { 1, 2, 3 }) end) end) ================================================ FILE: lua/cmp/utils/autocmd.lua ================================================ local debug = require('cmp.utils.debug') local autocmd = {} autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true }) autocmd.events = {} local function create_autocmd(event) vim.api.nvim_create_autocmd(event, { desc = ('nvim-cmp: autocmd: %s'):format(event), group = autocmd.group, callback = function() autocmd.emit(event) end, }) end ---Subscribe autocmd ---@param events string|string[] ---@param callback function ---@return function autocmd.subscribe = function(events, callback) events = type(events) == 'string' and { events } or events for _, event in ipairs(events) do if not autocmd.events[event] then autocmd.events[event] = {} create_autocmd(event) end table.insert(autocmd.events[event], callback) end return function() for _, event in ipairs(events) do for i, callback_ in ipairs(autocmd.events[event]) do if callback_ == callback then table.remove(autocmd.events[event], i) break end end end end end ---Emit autocmd ---@param event string autocmd.emit = function(event) debug.log(' ') debug.log(string.format('>>> %s', event)) autocmd.events[event] = autocmd.events[event] or {} for _, callback in ipairs(autocmd.events[event]) do callback() end end ---Resubscribe to events ---@param events string[] autocmd.resubscribe = function(events) -- Delete the autocommands if present local found = vim.api.nvim_get_autocmds({ group = autocmd.group, event = events, }) for _, to_delete in ipairs(found) do vim.api.nvim_del_autocmd(to_delete.id) end -- Recreate if event is known for _, event in ipairs(events) do if autocmd.events[event] then create_autocmd(event) end end end return autocmd ================================================ FILE: lua/cmp/utils/binary.lua ================================================ local binary = {} ---Insert item to list to ordered index ---@param list any[] ---@param item any ---@param func fun(a: any, b: any): 1|-1|0 binary.insort = function(list, item, func) table.insert(list, binary.search(list, item, func), item) end ---Search suitable index from list ---@param list any[] ---@param item any ---@param func fun(a: any, b: any): 1|-1|0 ---@return integer binary.search = function(list, item, func) local s = 1 local e = #list while s <= e do local idx = math.floor((e + s) / 2) local diff = func(item, list[idx]) if diff > 0 then s = idx + 1 elseif diff < 0 then e = idx - 1 else return idx + 1 end end return s end return binary ================================================ FILE: lua/cmp/utils/binary_spec.lua ================================================ local binary = require('cmp.utils.binary') describe('utils.binary', function() it('insort', function() local func = function(a, b) return a.score - b.score end local list = {} binary.insort(list, { id = 'a', score = 1 }, func) binary.insort(list, { id = 'b', score = 5 }, func) binary.insort(list, { id = 'c', score = 2.5 }, func) binary.insort(list, { id = 'd', score = 2 }, func) binary.insort(list, { id = 'e', score = 8 }, func) binary.insort(list, { id = 'g', score = 8 }, func) binary.insort(list, { id = 'h', score = 7 }, func) binary.insort(list, { id = 'i', score = 6 }, func) binary.insort(list, { id = 'j', score = 4 }, func) assert.are.equal(list[1].id, 'a') assert.are.equal(list[2].id, 'd') assert.are.equal(list[3].id, 'c') assert.are.equal(list[4].id, 'j') assert.are.equal(list[5].id, 'b') assert.are.equal(list[6].id, 'i') assert.are.equal(list[7].id, 'h') assert.are.equal(list[8].id, 'e') assert.are.equal(list[9].id, 'g') end) end) ================================================ FILE: lua/cmp/utils/buffer.lua ================================================ local buffer = {} buffer.cache = {} ---@return integer buf buffer.get = function(name) local buf = buffer.cache[name] if buf and vim.api.nvim_buf_is_valid(buf) then return buf else return nil end end ---@return integer buf ---@return boolean created_new buffer.ensure = function(name) local created_new = false local buf = buffer.get(name) if not buf then created_new = true buf = vim.api.nvim_create_buf(false, true) buffer.cache[name] = buf end return buf, created_new end return buffer ================================================ FILE: lua/cmp/utils/cache.lua ================================================ ---@class cmp.Cache ---@field public entries any local cache = {} cache.new = function() local self = setmetatable({}, { __index = cache }) self.entries = {} return self end ---Get cache value ---@param key string|string[] ---@return any|nil cache.get = function(self, key) key = self:key(key) if self.entries[key] ~= nil then return self.entries[key] end return nil end ---Set cache value explicitly ---@param key string|string[] ---@vararg any cache.set = function(self, key, value) key = self:key(key) self.entries[key] = value end ---Ensure value by callback ---@generic T ---@param key string|string[] ---@param callback fun(...): T ---@return T cache.ensure = function(self, key, callback, ...) local value = self:get(key) if value == nil then local v = callback(...) self:set(key, v) return v end return value end ---Clear all cache entries cache.clear = function(self) self.entries = {} end ---Create key ---@param key string|string[] ---@return string cache.key = function(_, key) if type(key) == 'table' then return table.concat(key, ':') end return key end return cache ================================================ FILE: lua/cmp/utils/char.lua ================================================ local _ local alpha = {} _ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) alpha[string.byte(char)] = true end) local ALPHA = {} _ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) ALPHA[string.byte(char)] = true end) local digit = {} _ = string.gsub('1234567890', '.', function(char) digit[string.byte(char)] = true end) local white = {} _ = string.gsub(' \t\n', '.', function(char) white[string.byte(char)] = true end) local char = {} ---@param byte integer ---@return boolean char.is_upper = function(byte) return ALPHA[byte] end ---@param byte integer ---@return boolean char.is_alpha = function(byte) return alpha[byte] or ALPHA[byte] end ---@param byte integer ---@return boolean char.is_digit = function(byte) return digit[byte] end ---@param byte integer ---@return boolean char.is_white = function(byte) return white[byte] end ---@param byte integer ---@return boolean char.is_symbol = function(byte) return not (char.is_alnum(byte) or char.is_white(byte)) end ---@param byte integer ---@return boolean char.is_printable = function(byte) return string.match(string.char(byte), '^%c$') == nil end ---@param byte integer ---@return boolean char.is_alnum = function(byte) return char.is_alpha(byte) or char.is_digit(byte) end ---@param text string ---@param index integer ---@return boolean char.is_semantic_index = function(text, index) if index <= 1 then return true end local prev = string.byte(text, index - 1) local curr = string.byte(text, index) if not char.is_upper(prev) and char.is_upper(curr) then return true end if char.is_symbol(curr) or char.is_white(curr) then return true end if not char.is_alpha(prev) and char.is_alpha(curr) then return true end if not char.is_digit(prev) and char.is_digit(curr) then return true end return false end ---@param text string ---@param current_index integer ---@return integer char.get_next_semantic_index = function(text, current_index) for i = current_index + 1, #text do if char.is_semantic_index(text, i) then return i end end return #text + 1 end ---Ignore case match ---@param byte1 integer ---@param byte2 integer ---@return boolean char.match = function(byte1, byte2) if not char.is_alpha(byte1) or not char.is_alpha(byte2) then return byte1 == byte2 end local diff = byte1 - byte2 return diff == 0 or diff == 32 or diff == -32 end return char ================================================ FILE: lua/cmp/utils/debug.lua ================================================ local debug = {} debug.flag = false ---Print log ---@vararg any debug.log = function(...) if debug.flag then local data = {} for _, v in ipairs({ ... }) do if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then v = vim.inspect(v) end table.insert(data, v) end print(table.concat(data, '\t')) end end return debug ================================================ FILE: lua/cmp/utils/event.lua ================================================ ---@class cmp.Event ---@field private events table local event = {} ---Create vents event.new = function() local self = setmetatable({}, { __index = event }) self.events = {} return self end ---Add event listener ---@param name string ---@param callback function ---@return function event.on = function(self, name, callback) if not self.events[name] then self.events[name] = {} end table.insert(self.events[name], callback) return function() self:off(name, callback) end end ---Remove event listener ---@param name string ---@param callback function event.off = function(self, name, callback) for i, callback_ in ipairs(self.events[name] or {}) do if callback_ == callback then table.remove(self.events[name], i) break end end end ---Remove all events event.clear = function(self) self.events = {} end ---Emit event ---@param name string event.emit = function(self, name, ...) for _, callback in ipairs(self.events[name] or {}) do if type(callback) == 'function' then callback(...) end end end return event ================================================ FILE: lua/cmp/utils/feedkeys.lua ================================================ local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local feedkeys = {} feedkeys.call = setmetatable({ callbacks = {}, }, { __call = function(self, keys, mode, callback) local is_insert = string.match(mode, 'i') ~= nil local is_immediate = string.match(mode, 'x') ~= nil local queue = {} if #keys > 0 then table.insert(queue, { keymap.t('setlocal lazyredraw'), 'n' }) table.insert(queue, { keymap.t('setlocal textwidth=0'), 'n' }) table.insert(queue, { keymap.t('setlocal backspace=nostop'), 'n' }) table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true }) table.insert(queue, { keymap.t('setlocal %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) table.insert(queue, { keymap.t('setlocal textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) table.insert(queue, { keymap.t('setlocal backspace=%s'):format(vim.go.backspace or 2), 'n' }) end if callback then local id = misc.id('cmp.utils.feedkeys.call') self.callbacks[id] = callback table.insert(queue, { keymap.t('lua require"cmp.utils.feedkeys".run(%s)'):format(id), 'n', true }) end if is_insert then for i = #queue, 1, -1 do vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3]) end else for i = 1, #queue do vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3]) end end if is_immediate then vim.api.nvim_feedkeys('', 'x', true) end end, }) feedkeys.run = function(id) if feedkeys.call.callbacks[id] then local ok, err = pcall(feedkeys.call.callbacks[id]) if not ok then vim.notify(err, vim.log.levels.ERROR) end feedkeys.call.callbacks[id] = nil end return '' end return feedkeys ================================================ FILE: lua/cmp/utils/feedkeys_spec.lua ================================================ local spec = require('cmp.utils.spec') local keymap = require('cmp.utils.keymap') local feedkeys = require('cmp.utils.feedkeys') describe('feedkeys', function() before_each(spec.before) it('dot-repeat', function() local reg feedkeys.call(keymap.t('iaiueo'), 'nx', function() reg = vim.fn.getreg('.') end) assert.are.equal(reg, keymap.t('aiueo')) end) it('textwidth', function() vim.cmd([[setlocal textwidth=6]]) feedkeys.call(keymap.t('iaiueo '), 'nx') feedkeys.call(keymap.t('aaiueoaiueo'), 'nx') assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { 'aiueo aiueoaiueo', }) end) it('backspace', function() vim.cmd([[setlocal backspace=""]]) feedkeys.call(keymap.t('iaiueo'), 'nx') feedkeys.call(keymap.t('a'), 'nx') assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { 'aiu', }) end) it('testability', function() feedkeys.call('i', 'n', function() feedkeys.call('', 'n', function() feedkeys.call('aiueo', 'in') end) feedkeys.call('', 'n', function() feedkeys.call(keymap.t(''), 'in') end) feedkeys.call('', 'n', function() feedkeys.call(keymap.t('abcde'), 'in') end) feedkeys.call('', 'n', function() feedkeys.call(keymap.t(''), 'in') end) feedkeys.call('', 'n', function() feedkeys.call(keymap.t('12345'), 'in') end) end) feedkeys.call('', 'x') assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { '12345' }) end) end) ================================================ FILE: lua/cmp/utils/highlight.lua ================================================ local highlight = {} highlight.keys = { 'fg', 'bg', 'bold', 'italic', 'reverse', 'standout', 'underline', 'undercurl', 'strikethrough', } highlight.inherit = function(name, source, settings) for _, key in ipairs(highlight.keys) do if not settings[key] then local v = vim.fn.synIDattr(vim.fn.hlID(source), key) if key == 'fg' or key == 'bg' then local n = tonumber(v, 10) v = type(n) == 'number' and n or v else v = v == 1 end settings[key] = v == '' and 'NONE' or v end end vim.api.nvim_set_hl(0, name, settings) end return highlight ================================================ FILE: lua/cmp/utils/keymap.lua ================================================ local misc = require('cmp.utils.misc') local buffer = require('cmp.utils.buffer') local api = require('cmp.utils.api') local keymap = {} ---Shortcut for nvim_replace_termcodes ---@param keys string ---@return string keymap.t = function(keys) return (string.gsub(keys, "(<[A-Za-z0-9\\%-%[%]%^@;,:_'`%./]->)", function(match) return vim.api.nvim_eval(string.format([["\%s"]], match)) end)) end ---Normalize key sequence. ---@param keys string ---@return string keymap.normalize = vim.fn.has('nvim-0.8') == 1 and function(keys) local t = string.gsub(keys, "<([A-Za-z0-9\\%-%[%]%^@;,:_'`%./]-)>", function(match) -- Use the \<* notation, which distinguishes from , etc. return vim.api.nvim_eval(string.format([["\<*%s>"]], match)) end) return vim.fn.keytrans(t) end or function(keys) local normalize_buf = buffer.ensure('cmp.util.keymap.normalize') vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '(cmp.utils.keymap.normalize)', {}) for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do if keymap.t(map.rhs) == keymap.t('(cmp.utils.keymap.normalize)') then vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) return map.lhs end end vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) vim.api.nvim_buf_delete(normalize_buf, {}) return keys end ---Return vim notation keymapping (simple conversion). ---@param s string ---@return string keymap.to_keymap = setmetatable({ [''] = { '\n', '\r', '\r\n' }, [''] = { '\t' }, [''] = { '\\' }, [''] = { '|' }, [''] = { ' ' }, }, { __call = function(self, s) return string.gsub(s, '.', function(c) for key, chars in pairs(self) do if vim.tbl_contains(chars, c) then return key end end return c end) end, }) ---Mode safe break undo keymap.undobreak = function() if not api.is_insert_mode() then return '' end return keymap.t('u') end ---Mode safe join undo keymap.undojoin = function() if not api.is_insert_mode() then return '' end return keymap.t('U') end ---Create backspace keys. ---@param count string|integer ---@return string keymap.backspace = function(count) if type(count) == 'string' then count = vim.fn.strchars(count, true) end if count <= 0 then return '' end local keys = {} table.insert(keys, keymap.t(string.rep('', count))) return table.concat(keys, '') end ---Create delete keys. ---@param count string|integer ---@return string keymap.delete = function(count) if type(count) == 'string' then count = vim.fn.strchars(count, true) end if count <= 0 then return '' end local keys = {} table.insert(keys, keymap.t(string.rep('', count))) return table.concat(keys, '') end ---Update indentkeys. ---@param expr? string ---@return string keymap.indentkeys = function(expr) return string.format(keymap.t('set indentkeys=%s'), expr and vim.fn.escape(expr, '| \t\\') or '') end ---Return two key sequence are equal or not. ---@param a string ---@param b string ---@return boolean keymap.equals = function(a, b) return keymap.normalize(a) == keymap.normalize(b) end ---Register keypress handler. keymap.listen = function(mode, lhs, callback) lhs = keymap.normalize(keymap.to_keymap(lhs)) local existing = keymap.get_map(mode, lhs) if existing.desc == 'cmp.utils.keymap.set_map' then return end local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1 local fallback = keymap.fallback(bufnr, mode, existing) keymap.set_map(bufnr, mode, lhs, function() local ignore = false ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=') if ignore then fallback() else callback(lhs, misc.once(fallback)) end end, { expr = false, noremap = true, silent = true, }) end ---Fallback keymap.fallback = function(bufnr, mode, map) return function() if map.expr then local fallback_lhs = string.format('(cmp.u.k.fallback_expr:%s)', map.lhs) keymap.set_map(bufnr, mode, fallback_lhs, function() return keymap.solve(bufnr, mode, map).keys end, { expr = true, noremap = map.noremap, script = map.script, nowait = map.nowait, silent = map.silent and mode ~= 'c', replace_keycodes = map.replace_keycodes, }) vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true) elseif map.callback then map.callback() else local solved = keymap.solve(bufnr, mode, map) vim.api.nvim_feedkeys(solved.keys, solved.mode, true) end end end ---Solve keymap.solve = function(bufnr, mode, map) local lhs = keymap.t(map.lhs) local rhs = keymap.t(map.rhs) if map.expr then if map.callback then rhs = map.callback() else rhs = vim.api.nvim_eval(keymap.t(map.rhs)) end end if map.noremap then return { keys = rhs, mode = 'in' } end if string.find(rhs, lhs, 1, true) == 1 then local recursive = string.format('0_(cmp.u.k.recursive:%s)', lhs) keymap.set_map(bufnr, mode, recursive, lhs, { noremap = true, script = true, nowait = map.nowait, silent = map.silent and mode ~= 'c', replace_keycodes = map.replace_keycodes, }) return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' } end return { keys = rhs, mode = 'im' } end ---Get map ---@param mode string ---@param lhs string ---@return table keymap.get_map = function(mode, lhs) lhs = keymap.normalize(lhs) for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do if keymap.equals(map.lhs, lhs) then return { lhs = map.lhs, rhs = map.rhs or '', expr = map.expr == 1, callback = map.callback, desc = map.desc, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, nowait = map.nowait == 1, buffer = true, replace_keycodes = map.replace_keycodes == 1, } end end for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do if keymap.equals(map.lhs, lhs) then return { lhs = map.lhs, rhs = map.rhs or '', expr = map.expr == 1, callback = map.callback, desc = map.desc, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, nowait = map.nowait == 1, buffer = false, replace_keycodes = map.replace_keycodes == 1, } end end return { lhs = lhs, rhs = lhs, expr = false, callback = nil, noremap = true, script = false, silent = true, nowait = false, buffer = false, replace_keycodes = true, } end ---Set keymapping keymap.set_map = function(bufnr, mode, lhs, rhs, opts) if type(rhs) == 'function' then opts.callback = rhs rhs = '' end opts.desc = 'cmp.utils.keymap.set_map' if vim.fn.has('nvim-0.8') == 0 then opts.replace_keycodes = nil end if bufnr == -1 then vim.api.nvim_set_keymap(mode, lhs, rhs, opts) else vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) end end return keymap ================================================ FILE: lua/cmp/utils/keymap_spec.lua ================================================ local spec = require('cmp.utils.spec') local api = require('cmp.utils.api') local feedkeys = require('cmp.utils.feedkeys') local keymap = require('cmp.utils.keymap') describe('keymap', function() before_each(spec.before) it('t', function() for _, key in ipairs({ '', '', '', '', '', '', '', '', '', '', '', '', '', "", '', '', '', '(example)', '="abc"', 'normal! ==', }) do assert.are.equal(keymap.t(key), vim.api.nvim_replace_termcodes(key, true, true, true)) assert.are.equal(keymap.t(key .. key), vim.api.nvim_replace_termcodes(key .. key, true, true, true)) assert.are.equal(keymap.t(key .. key .. key), vim.api.nvim_replace_termcodes(key .. key .. key, true, true, true)) end end) it('to_keymap', function() assert.are.equal(keymap.to_keymap('\n'), '') assert.are.equal(keymap.to_keymap(''), '') assert.are.equal(keymap.to_keymap('|'), '') end) describe('fallback', function() before_each(spec.before) local run_fallback = function(keys, fallback) local state = {} feedkeys.call(keys, '', function() fallback() end) feedkeys.call('', '', function() if api.is_cmdline_mode() then state.buffer = { api.get_current_line() } else state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) end state.cursor = api.get_cursor() end) feedkeys.call('', 'x') return state end describe('basic', function() it('', function() vim.api.nvim_buf_set_keymap(0, 'i', '(pairs)', '()', { noremap = true }) vim.api.nvim_buf_set_keymap(0, 'i', '(', '(pairs)', { noremap = false }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) it('=', function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '="()"', {}) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) it('callback', function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { callback = function() vim.api.nvim_feedkeys('()' .. keymap.t(''), 'int', true) end, }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) it('expr-callback', function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { expr = true, noremap = false, silent = true, callback = function() return '()' .. keymap.t('') end, }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) -- it('cmdline default ', function() -- local fallback = keymap.fallback(0, 'c', keymap.get_map('c', '')) -- local state = run_fallback(':', fallback) -- assert.are.same({ '' }, state.buffer) -- assert.are.same({ 1, 0 }, state.cursor) -- end) end) describe('recursive', function() it('non-expr', function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '()', { expr = false, noremap = false, silent = true, }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) it('expr', function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '"()"', { expr = true, noremap = false, silent = true, }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) it('expr-callback', function() pcall(function() vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { expr = true, noremap = false, silent = true, callback = function() return keymap.t('()') end, }) local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) local state = run_fallback('i', fallback) assert.are.same({ '()' }, state.buffer) assert.are.same({ 1, 1 }, state.cursor) end) end) end) end) describe('realworld', function() before_each(spec.before) it('#226', function() keymap.listen('i', '', function(_, fallback) fallback() end) vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) end) it('#414', function() keymap.listen('i', '', function() vim.api.nvim_feedkeys(keymap.t(''), 'int', true) end) vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) end) it('#744', function() vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { noremap = true, }) vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { noremap = false, }) keymap.listen('i', '', function(_, fallback) fallback() end) feedkeys.call(keymap.t('i'), 'tx') assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) end) end) end) ================================================ FILE: lua/cmp/utils/misc.lua ================================================ local misc = {} local islist = vim.islist or vim.tbl_islist ---Create once callback ---@param callback function ---@return function misc.once = function(callback) local done = false return function(...) if done then return end done = true callback(...) end end ---Return concatenated list ---@param list1 any[] ---@param list2 any[] ---@return any[] misc.concat = function(list1, list2) local new_list = {} for _, v in ipairs(list1) do table.insert(new_list, v) end for _, v in ipairs(list2) do table.insert(new_list, v) end return new_list end ---Repeat values ---@generic T ---@param str_or_tbl T ---@param count integer ---@return T misc.rep = function(str_or_tbl, count) if type(str_or_tbl) == 'string' then return string.rep(str_or_tbl, count) end local rep = {} for _ = 1, count do for _, v in ipairs(str_or_tbl) do table.insert(rep, v) end end return rep end ---Return whether the value is empty or not. ---@param v any ---@return boolean misc.empty = function(v) if not v then return true end if v == vim.NIL then return true end if type(v) == 'string' and v == '' then return true end if type(v) == 'table' and vim.tbl_isempty(v) then return true end if type(v) == 'number' and v == 0 then return true end return false end ---Search value in table misc.contains = function(tbl, v) for _, value in ipairs(tbl) do if value == v then return true end end return false end ---The symbol to remove key in misc.merge. misc.none = vim.NIL ---Merge two tables recursively ---@generic T ---@param tbl1 T ---@param tbl2 T ---@return T misc.merge = function(tbl1, tbl2) local is_dict1 = type(tbl1) == 'table' and (not islist(tbl1) or vim.tbl_isempty(tbl1)) local is_dict2 = type(tbl2) == 'table' and (not islist(tbl2) or vim.tbl_isempty(tbl2)) if is_dict1 and is_dict2 then local new_tbl = {} for k, v in pairs(tbl2) do if tbl1[k] ~= misc.none then new_tbl[k] = misc.merge(tbl1[k], v) end end for k, v in pairs(tbl1) do if tbl2[k] == nil then if v ~= misc.none then new_tbl[k] = misc.merge(v, {}) else new_tbl[k] = nil end end end return new_tbl end if tbl1 == misc.none then return nil elseif tbl1 == nil then return misc.merge(tbl2, {}) else return tbl1 end end ---Generate id for group name misc.id = setmetatable({ group = {}, }, { __call = function(_, group) misc.id.group[group] = misc.id.group[group] or 0 misc.id.group[group] = misc.id.group[group] + 1 return misc.id.group[group] end, }) ---Treat 1/0 as bool value ---@param v boolean|1|0 ---@param def boolean ---@return boolean misc.bool = function(v, def) if v == nil then return def end return v == true or v == 1 end ---Set value to deep object ---@param t table ---@param keys string[] ---@param v any misc.set = function(t, keys, v) local c = t for i = 1, #keys - 1 do local key = keys[i] c[key] = c[key] or {} c = c[key] end c[keys[#keys]] = v end do local function do_copy(tbl, seen) if type(tbl) ~= 'table' then return tbl end if seen[tbl] then return seen[tbl] end if islist(tbl) then local copy = {} seen[tbl] = copy for i, value in ipairs(tbl) do copy[i] = do_copy(value, seen) end return copy end local copy = {} seen[tbl] = copy for key, value in pairs(tbl) do copy[key] = do_copy(value, seen) end return copy end ---Copy table ---@generic T ---@param tbl T ---@return T misc.copy = function(tbl) return do_copy(tbl, {}) end end ---Safe version of vim.str_utfindex ---@param text string ---@param vimindex integer|nil ---@return integer misc.to_utfindex = function(text, vimindex) vimindex = vimindex or #text + 1 if vim.fn.has('nvim-0.11') == 1 then return vim.str_utfindex(text, 'utf-16', math.max(0, math.min(vimindex - 1, #text))) end return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) end ---Safe version of vim.str_byteindex ---@param text string ---@param utfindex integer ---@return integer misc.to_vimindex = function(text, utfindex) utfindex = utfindex or #text for i = utfindex, 1, -1 do local s, v = pcall(function() return vim.str_byteindex(text, 'utf-16', i) + 1 end) if s then return v end end return utfindex + 1 end ---Mark the function as deprecated misc.deprecated = function(fn, msg) local printed = false return function(...) if not printed then print(msg) printed = true end return fn(...) end end --Redraw misc.redraw = setmetatable({ doing = false, force = false, -- We use `` to redraw the screen. (Previously, We use . it will remove the unmatches search history.) incsearch_redraw_keys = ' ', }, { __call = function(self, force) local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true) if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then if vim.o.incsearch then return vim.api.nvim_feedkeys(termcode, 'ni', true) end end if self.doing then return end self.doing = true self.force = not not force vim.schedule(function() if self.force then vim.cmd([[redraw!]]) else vim.cmd([[redraw]]) end self.doing = false self.force = false end) end, }) return misc ================================================ FILE: lua/cmp/utils/misc_spec.lua ================================================ local spec = require('cmp.utils.spec') local misc = require('cmp.utils.misc') describe('misc', function() before_each(spec.before) it('copy', function() -- basic. local tbl, copy tbl = { a = { b = 1, }, } copy = misc.copy(tbl) assert.are_not.equal(tbl, copy) assert.are_not.equal(tbl.a, copy.a) assert.are.same(tbl, copy) -- self reference. tbl = { a = { b = 1, }, } tbl.a.c = tbl.a copy = misc.copy(tbl) assert.are_not.equal(tbl, copy) assert.are_not.equal(tbl.a, copy.a) assert.are_not.equal(tbl.a.c, copy.a.c) assert.are.same(tbl, copy) end) it('merge', function() local merged merged = misc.merge({ a = {}, }, { a = { b = 1, }, }) assert.are.equal(merged.a.b, 1) merged = misc.merge({ a = { i = 1, }, }, { a = { c = 2, }, }) assert.are.equal(merged.a.i, 1) assert.are.equal(merged.a.c, 2) merged = misc.merge({ a = false, }, { a = { b = 1, }, }) assert.are.equal(merged.a, false) merged = misc.merge({ a = misc.none, }, { a = { b = 1, }, }) assert.are.equal(merged.a, nil) merged = misc.merge({ a = misc.none, }, { a = nil, }) assert.are.equal(merged.a, nil) merged = misc.merge({ a = nil, }, { a = misc.none, }) assert.are.equal(merged.a, nil) end) end) ================================================ FILE: lua/cmp/utils/options.lua ================================================ local M = {} -- Set window option without triggering the OptionSet event ---@param window number ---@param name string ---@param value any M.win_set_option = function(window, name, value) local eventignore = vim.opt.eventignore:get() vim.opt.eventignore:append('OptionSet') vim.api.nvim_win_set_option(window, name, value) vim.opt.eventignore = eventignore end -- Set buffer option without triggering the OptionSet event ---@param buffer number ---@param name string ---@param value any M.buf_set_option = function(buffer, name, value) local eventignore = vim.opt.eventignore:get() vim.opt.eventignore:append('OptionSet') vim.api.nvim_buf_set_option(buffer, name, value) vim.opt.eventignore = eventignore end return M ================================================ FILE: lua/cmp/utils/pattern.lua ================================================ local pattern = {} pattern._regexes = {} pattern.regex = function(p) if not pattern._regexes[p] then pattern._regexes[p] = vim.regex(p) end return pattern._regexes[p] end pattern.offset = function(p, text) local s, e = pattern.regex(p):match_str(text) if s then return s + 1, e + 1 end return nil, nil end pattern.matchstr = function(p, text) local s, e = pattern.offset(p, text) if s then return string.sub(text, s, e) end return nil end return pattern ================================================ FILE: lua/cmp/utils/snippet.lua ================================================ local misc = require('cmp.utils.misc') local P = {} ---Take characters until the target characters (The escape sequence is '\' + char) ---@param targets string[] The character list for stop consuming text. ---@param specials string[] If the character isn't contained in targets/specials, '\' will be left. P.take_until = function(targets, specials) targets = targets or {} specials = specials or {} return function(input, pos) local new_pos = pos local raw = {} local esc = {} while new_pos <= #input do local c = string.sub(input, new_pos, new_pos) if c == '\\' then table.insert(raw, '\\') new_pos = new_pos + 1 c = string.sub(input, new_pos, new_pos) if not misc.contains(targets, c) and not misc.contains(specials, c) then table.insert(esc, '\\') end table.insert(raw, c) table.insert(esc, c) new_pos = new_pos + 1 else if misc.contains(targets, c) then break end table.insert(raw, c) table.insert(esc, c) new_pos = new_pos + 1 end end if new_pos == pos then return P.unmatch(pos) end return { parsed = true, value = { raw = table.concat(raw, ''), esc = table.concat(esc, ''), }, pos = new_pos, } end end P.unmatch = function(pos) return { parsed = false, value = nil, pos = pos, } end P.map = function(parser, map) return function(input, pos) local result = parser(input, pos) if result.parsed then return { parsed = true, value = map(result.value), pos = result.pos, } end return P.unmatch(pos) end end P.lazy = function(factory) return function(input, pos) return factory()(input, pos) end end P.token = function(token) return function(input, pos) local maybe_token = string.sub(input, pos, pos + #token - 1) if token == maybe_token then return { parsed = true, value = maybe_token, pos = pos + #token, } end return P.unmatch(pos) end end P.pattern = function(p) return function(input, pos) local maybe_match = string.match(string.sub(input, pos), '^' .. p) if maybe_match then return { parsed = true, value = maybe_match, pos = pos + #maybe_match, } end return P.unmatch(pos) end end P.many = function(parser) return function(input, pos) local values = {} local new_pos = pos while new_pos <= #input do local result = parser(input, new_pos) if not result.parsed then break end table.insert(values, result.value) new_pos = result.pos end if #values > 0 then return { parsed = true, value = values, pos = new_pos, } end return P.unmatch(pos) end end P.any = function(...) local parsers = { ... } return function(input, pos) for _, parser in ipairs(parsers) do local result = parser(input, pos) if result.parsed then return result end end return P.unmatch(pos) end end P.opt = function(parser) return function(input, pos) local result = parser(input, pos) return { parsed = true, value = result.value, pos = result.pos, } end end P.seq = function(...) local parsers = { ... } return function(input, pos) local values = {} local new_pos = pos for i, parser in ipairs(parsers) do local result = parser(input, new_pos) if result.parsed then values[i] = result.value new_pos = result.pos else return P.unmatch(pos) end end return { parsed = true, value = values, pos = new_pos, } end end local Node = {} Node.Type = { SNIPPET = 0, TABSTOP = 1, PLACEHOLDER = 2, VARIABLE = 3, CHOICE = 4, TRANSFORM = 5, FORMAT = 6, TEXT = 7, } function Node:__tostring() local insert_text = {} if self.type == Node.Type.SNIPPET then for _, c in ipairs(self.children) do table.insert(insert_text, tostring(c)) end elseif self.type == Node.Type.CHOICE then table.insert(insert_text, self.items[1]) elseif self.type == Node.Type.PLACEHOLDER then for _, c in ipairs(self.children or {}) do table.insert(insert_text, tostring(c)) end elseif self.type == Node.Type.TEXT then table.insert(insert_text, self.esc) end return table.concat(insert_text, '') end --@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar local S = {} S.dollar = P.token('$') S.open = P.token('{') S.close = P.token('}') S.colon = P.token(':') S.slash = P.token('/') S.comma = P.token(',') S.pipe = P.token('|') S.plus = P.token('+') S.minus = P.token('-') S.question = P.token('?') S.int = P.map(P.pattern('[0-9]+'), function(value) return tonumber(value, 10) end) S.var = P.pattern('[%a_][%w_]+') S.text = function(targets, specials) return P.map(P.take_until(targets, specials), function(value) return setmetatable({ type = Node.Type.TEXT, raw = value.raw, esc = value.esc, }, Node) end) end S.toplevel = P.lazy(function() return P.any(S.placeholder, S.tabstop, S.variable, S.choice) end) S.format = P.any( P.map(P.seq(S.dollar, S.int), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[2], }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(P.token('upcase'), P.token('downcase'), P.token('capitalize'), P.token('camelcase'), P.token('pascalcase')), S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], modifier = values[6], }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.question, P.opt(P.take_until({ ':' }, { '\\' })), S.colon, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], if_text = values[5][2] and values[5][2].esc or '', else_text = values[5][4] and values[5][4].esc or '', }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], if_text = values[5][2] and values[5][2].esc or '', else_text = '', }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.minus, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], if_text = '', else_text = values[6] and values[6].esc or '', }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values) return setmetatable({ type = Node.Type.FORMAT, capture_index = values[3], if_text = '', else_text = values[5] and values[5].esc or '', }, Node) end) ) S.transform = P.map(P.seq(S.slash, P.take_until({ '/' }, { '\\' }), S.slash, P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))), S.slash, P.opt(P.pattern('[ig]+'))), function(values) return setmetatable({ type = Node.Type.TRANSFORM, pattern = values[2].raw, format = values[4], option = values[6], }, Node) end) S.tabstop = P.any( P.map(P.seq(S.dollar, S.int), function(values) return setmetatable({ type = Node.Type.TABSTOP, tabstop = values[2], }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values) return setmetatable({ type = Node.Type.TABSTOP, tabstop = values[3], }, Node) end), P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values) return setmetatable({ type = Node.Type.TABSTOP, tabstop = values[3], transform = values[4], }, Node) end) ) S.placeholder = P.any(P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))), S.close), function(values) return setmetatable({ type = Node.Type.PLACEHOLDER, tabstop = values[3], -- insert empty text if opt did not match. children = values[5] or { setmetatable({ type = Node.Type.TEXT, raw = '', esc = '', }, Node), }, }, Node) end)) S.choice = P.map( P.seq( S.dollar, S.open, S.int, S.pipe, P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values) return values[1].esc end)), S.pipe, S.close ), function(values) return setmetatable({ type = Node.Type.CHOICE, tabstop = values[3], items = values[5], }, Node) end ) S.variable = P.any( P.map(P.seq(S.dollar, S.var), function(values) return setmetatable({ type = Node.Type.VARIABLE, name = values[2], }, Node) end), P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values) return setmetatable({ type = Node.Type.VARIABLE, name = values[3], }, Node) end), P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values) return setmetatable({ type = Node.Type.VARIABLE, name = values[3], transform = values[4], }, Node) end), P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values) return setmetatable({ type = Node.Type.VARIABLE, name = values[3], children = values[5], }, Node) end) ) S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values) return setmetatable({ type = Node.Type.SNIPPET, children = values, }, Node) end) local M = {} ---The snippet node type enum ---@types table M.NodeType = Node.Type ---Parse snippet string and returns the AST ---@param input string ---@return table function M.parse(input) local result = S.snippet(input, 1) if not result.parsed then error('snippet parsing failed.') end return result.value end return M ================================================ FILE: lua/cmp/utils/spec.lua ================================================ local context = require('cmp.context') local source = require('cmp.source') local types = require('cmp.types') local config = require('cmp.config') local spec = {} spec.before = function() vim.cmd([[ bdelete! enew! imapclear imapclear cmapclear cmapclear smapclear smapclear xmapclear xmapclear tmapclear tmapclear setlocal noswapfile setlocal virtualedit=all setlocal completeopt=menu,menuone,noselect ]]) config.set_global({ sources = { { name = 'spec' }, }, snippet = { expand = function(args) local ctx = context.new() vim.api.nvim_buf_set_text(ctx.bufnr, ctx.cursor.row - 1, ctx.cursor.col - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, vim.split(string.gsub(args.body, '%$0', ''), '\n')) for i, t in ipairs(vim.split(args.body, '\n')) do local s = string.find(t, '$0', 1, true) if s then if i == 1 then vim.api.nvim_win_set_cursor(0, { ctx.cursor.row, ctx.cursor.col + s - 2 }) else vim.api.nvim_win_set_cursor(0, { ctx.cursor.row + i - 1, s - 1 }) end break end end end, }, }) config.set_cmdline({ sources = { { name = 'spec' }, }, }, ':') end spec.state = function(text, row, col) vim.fn.setline(1, text) vim.fn.cursor(row, col) local ctx = context.empty() local s = source.new('spec', { complete = function() end, }) return { context = function() return ctx end, source = function() return s end, backspace = function() vim.fn.feedkeys('x', 'nx') vim.fn.feedkeys('h', 'nx') ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) s:complete(ctx, function() end) return ctx end, input = function(char) vim.fn.feedkeys(('i%s'):format(char), 'nx') vim.fn.feedkeys(string.rep('l', #char), 'nx') ctx.prev_context = nil ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) s:complete(ctx, function() end) return ctx end, manual = function() ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual }) s:complete(ctx, function() end) return ctx end, } end return spec ================================================ FILE: lua/cmp/utils/str.lua ================================================ local char = require('cmp.utils.char') local str = {} local INVALIDS = {} INVALIDS[string.byte("'")] = true INVALIDS[string.byte('"')] = true INVALIDS[string.byte('=')] = true INVALIDS[string.byte('$')] = true INVALIDS[string.byte('(')] = true INVALIDS[string.byte('[')] = true INVALIDS[string.byte('<')] = true INVALIDS[string.byte('{')] = true INVALIDS[string.byte(' ')] = true INVALIDS[string.byte('\t')] = true INVALIDS[string.byte('\n')] = true INVALIDS[string.byte('\r')] = true local NR_BYTE = string.byte('\n') local PAIRS = {} PAIRS[string.byte('<')] = string.byte('>') PAIRS[string.byte('[')] = string.byte(']') PAIRS[string.byte('(')] = string.byte(')') PAIRS[string.byte('{')] = string.byte('}') PAIRS[string.byte('"')] = string.byte('"') PAIRS[string.byte("'")] = string.byte("'") ---Return if specified text has prefix or not ---@param text string ---@param prefix string ---@return boolean str.has_prefix = function(text, prefix) if #text < #prefix then return false end for i = 1, #prefix do if not char.match(string.byte(text, i), string.byte(prefix, i)) then return false end end return true end ---get_common_string str.get_common_string = function(text1, text2) local min = math.min(#text1, #text2) for i = 1, min do if not char.match(string.byte(text1, i), string.byte(text2, i)) then if string.byte(text1, i) > 127 then -- Differing byte is non-ASCII, use Unicode-safe path local char_min_len = math.min(vim.fn.strchars(text1), vim.fn.strchars(text2)) for j = 0, char_min_len - 1 do local char1 = vim.fn.strcharpart(text1, j, 1) local char2 = vim.fn.strcharpart(text2, j, 1) -- Use case-insensitive comparison for Unicode like char.match does for ASCII if vim.fn.tolower(char1) ~= vim.fn.tolower(char2) then return vim.fn.strcharpart(text1, 0, j) end end return vim.fn.strcharpart(text1, 0, char_min_len) end return string.sub(text1, 1, i - 1) end end return string.sub(text1, 1, min) end ---Remove suffix ---@param text string ---@param suffix string ---@return string str.remove_suffix = function(text, suffix) if #text < #suffix then return text end local i = 0 while i < #suffix do if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then return text end i = i + 1 end return string.sub(text, 1, -#suffix - 1) end ---trim ---@param text string ---@return string str.trim = function(text) local s = 1 for i = 1, #text do if not char.is_white(string.byte(text, i)) then s = i break end end local e = #text for i = #text, 1, -1 do if not char.is_white(string.byte(text, i)) then e = i break end end if s == 1 and e == #text then return text end return string.sub(text, s, e) end ---get_word ---@param text string ---@param stop_char? integer ---@param min_length? integer ---@return string str.get_word = function(text, stop_char, min_length) min_length = min_length or 0 local has_alnum = false local stack = {} local word = {} local add = function(c) table.insert(word, string.char(c)) if stack[#stack] == c then table.remove(stack, #stack) else if PAIRS[c] then table.insert(stack, c) end end end for i = 1, #text do local c = string.byte(text, i, i) if #word < min_length then table.insert(word, string.char(c)) elseif not INVALIDS[c] then add(c) has_alnum = has_alnum or char.is_alnum(c) elseif not has_alnum then add(c) elseif #stack ~= 0 then add(c) if has_alnum and #stack == 0 then break end else break end end if stop_char and word[#word] == string.char(stop_char) then table.remove(word, #word) end return table.concat(word, '') end ---Oneline ---@param text string ---@return string str.oneline = function(text) for i = 1, #text do if string.byte(text, i) == NR_BYTE then return string.sub(text, 1, i - 1) end end return text end ---Escape special chars ---@param text string ---@param chars string[] ---@return string str.escape = function(text, chars) table.insert(chars, '\\') local escaped = {} local i = 1 while i <= #text do local c = string.sub(text, i, i) if vim.tbl_contains(chars, c) then table.insert(escaped, '\\') table.insert(escaped, c) else table.insert(escaped, c) end i = i + 1 end return table.concat(escaped, '') end return str ================================================ FILE: lua/cmp/utils/str_spec.lua ================================================ local str = require('cmp.utils.str') describe('utils.str', function() it('get_word', function() assert.are.equal(str.get_word('print'), 'print') assert.are.equal(str.get_word('$variable'), '$variable') assert.are.equal(str.get_word('print()'), 'print') assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]') assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies') assert.are.equal(str.get_word('"devDependencies": ${1},', string.byte('"')), '"devDependencies') assert.are.equal(str.get_word('#[cfg(test)]'), '#[cfg(test)]') assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps') end) it('remove_suffix', function() assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()') assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}') end) it('escape', function() assert.are.equal(str.escape('plain', {}), 'plain') assert.are.equal(str.escape('plain\\', {}), 'plain\\\\') assert.are.equal(str.escape('plain\\"', {}), 'plain\\\\"') assert.are.equal(str.escape('pla"in', { '"' }), 'pla\\"in') assert.are.equal(str.escape('call("")', { '"' }), 'call(\\"\\")') end) it('get_common_string', function() -- ASCII tests assert.are.equal(str.get_common_string('hello', 'help'), 'hel') assert.are.equal(str.get_common_string('abc', 'xyz'), '') assert.are.equal(str.get_common_string('test', 'Testing'), 'test') -- Unicode tests assert.are.equal(str.get_common_string('получаем', 'получив'), 'получ') assert.are.equal(str.get_common_string('тест', 'тестинг'), 'тест') assert.are.equal(str.get_common_string('тест', 'Тестинг'), 'тест') assert.are.equal(str.get_common_string('Тест', 'тестинг'), 'Тест') assert.are.equal(str.get_common_string('тЕст', 'тестинг'), 'тЕст') assert.are.equal(str.get_common_string('тест', 'тЕстинг'), 'тест') assert.are.equal(str.get_common_string('тесТ', 'тЕстинг'), 'тесТ') assert.are.equal(str.get_common_string('тест', 'тесТинг'), 'тест') assert.are.equal(str.get_common_string('а', 'я'), '') -- 0xD0 0xB0 - 0xD1 0x8F assert.are.equal(str.get_common_string('а', 'б'), '') -- 0xD0 0xB0 - 0xD0 0xB1 assert.are.equal(str.get_common_string('Я', 'я'), 'Я') -- 0xD0 0xAF - 0xD1 0x8F assert.are.equal(str.get_common_string('А', 'а'), 'А') -- 0xD0 0x90 - 0xD0 0xB0 -- Normalization is not supported yet assert.are.equal(str.get_common_string('й', 'и'), '') -- 0xD0 0xB9 - 0xD0 0xB8 assert.are.equal(str.get_common_string('й', 'и'), 'и') -- 0xD0 0xB8 0xD1 0x8E - 0xD0 0xB8 end) end) ================================================ FILE: lua/cmp/utils/window.lua ================================================ local misc = require('cmp.utils.misc') local opt = require('cmp.utils.options') local buffer = require('cmp.utils.buffer') local api = require('cmp.utils.api') local config = require('cmp.config') ---@class cmp.WindowStyle ---@field public relative string ---@field public row integer ---@field public col integer ---@field public width integer|float ---@field public height integer|float ---@field public border string|string[]|nil ---@field public zindex integer|nil ---@class cmp.Window ---@field public name string ---@field public win integer|nil ---@field public thumb_win integer|nil ---@field public sbar_win integer|nil ---@field public style cmp.WindowStyle ---@field public opt table ---@field public buffer_opt table local window = {} ---new ---@return cmp.Window window.new = function() local self = setmetatable({}, { __index = window }) self.name = misc.id('cmp.utils.window.new') self.win = nil self.sbar_win = nil self.thumb_win = nil self.style = {} self.opt = {} self.buffer_opt = {} return self end ---Set window option. ---NOTE: If the window already visible, immediately applied to it. ---@param key string ---@param value any window.option = function(self, key, value) if vim.fn.exists('+' .. key) == 0 then return end if value == nil then return self.opt[key] end self.opt[key] = value if self:visible() then opt.win_set_option(self.win, key, value) end end ---Set buffer option. ---NOTE: If the buffer already visible, immediately applied to it. ---@param key string ---@param value any window.buffer_option = function(self, key, value) if vim.fn.exists('+' .. key) == 0 then return end if value == nil then return self.buffer_opt[key] end self.buffer_opt[key] = value local existing_buf = buffer.get(self.name) if existing_buf then opt.buf_set_option(existing_buf, key, value) end end ---Set style. ---@param style cmp.WindowStyle window.set_style = function(self, style) self.style = style local info = self:info() if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then self.style.height = vim.o.lines - info.row - info.border_info.vert - 1 end self.style.zindex = self.style.zindex or 1 --- GUI clients are allowed to return fractional bounds, but we need integer --- bounds to open the window self.style.width = math.ceil(self.style.width) self.style.height = math.ceil(self.style.height) end ---Return buffer id. ---@return integer window.get_buffer = function(self) local buf, created_new = buffer.ensure(self.name) if created_new then for k, v in pairs(self.buffer_opt) do opt.buf_set_option(buf, k, v) end end return buf end ---Open window ---@param style cmp.WindowStyle window.open = function(self, style) if style then self:set_style(style) end if self.style.width < 1 or self.style.height < 1 then return end if self.win and vim.api.nvim_win_is_valid(self.win) then vim.api.nvim_win_set_config(self.win, self.style) else local s = misc.copy(self.style) s.noautocmd = true self.win = vim.api.nvim_open_win(self:get_buffer(), false, s) for k, v in pairs(self.opt) do opt.win_set_option(self.win, k, v) end end self:update() end ---Update window.update = function(self) local info = self:info() if info.scrollable and self.style.height > 0 then -- Draw the background of the scrollbar if not info.border_info.visible then local style = { relative = 'editor', style = 'minimal', width = 1, height = self.style.height, row = info.row, col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset. zindex = (self.style.zindex and (self.style.zindex + 1) or 1), border = 'none', } if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then vim.api.nvim_win_set_config(self.sbar_win, style) else style.noautocmd = true self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style) opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar') end end -- Draw the scrollbar thumb local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height())) thumb_height = math.max(1, thumb_height) local topline = vim.fn.getwininfo(self.win)[1].topline local scroll_ratio = topline / (self:get_content_height() - info.inner_height + 1) -- row grid start from 0 on nvim-0.10 local thumb_offset_raw = (info.inner_height - thumb_height) * scroll_ratio -- round half if topline > 1 local thumb_offset = math.floor(thumb_offset_raw) if topline > 1 and thumb_offset_raw + 0.5 >= thumb_offset + 1 then thumb_offset = thumb_offset + 1 end local style = { relative = 'editor', style = 'minimal', width = 1, height = thumb_height, row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0), col = info.col + info.width - 1, -- info.col was already added scrollbar offset. zindex = (self.style.zindex and (self.style.zindex + 2) or 2), border = 'none', } if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then vim.api.nvim_win_set_config(self.thumb_win, style) else style.noautocmd = true self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style) opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb') end else if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then vim.api.nvim_win_hide(self.sbar_win) self.sbar_win = nil end if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then vim.api.nvim_win_hide(self.thumb_win) self.thumb_win = nil end end -- In cmdline, vim does not redraw automatically. if api.is_cmdline_mode() then vim.api.nvim_win_call(self.win, function() misc.redraw() end) end end ---Close window window.close = function(self) if self.win and vim.api.nvim_win_is_valid(self.win) then if self.win and vim.api.nvim_win_is_valid(self.win) then vim.api.nvim_win_hide(self.win) self.win = nil end if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then vim.api.nvim_win_hide(self.sbar_win) self.sbar_win = nil end if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then vim.api.nvim_win_hide(self.thumb_win) self.thumb_win = nil end end end ---Return the window is visible or not. window.visible = function(self) return self.win and vim.api.nvim_win_is_valid(self.win) end ---Return win info. window.info = function(self) local border_info = self:get_border_info() local scrollbar = config.get().window.completion.scrollbar local info = { row = self.style.row, col = self.style.col, width = self.style.width + border_info.left + border_info.right, height = self.style.height + border_info.top + border_info.bottom, inner_width = self.style.width, inner_height = self.style.height, border_info = border_info, scrollable = false, scrollbar_offset = 0, } if self:get_content_height() > info.inner_height and scrollbar then info.scrollable = true if not border_info.visible then info.scrollbar_offset = 1 info.width = info.width + 1 end end return info end ---Return border information. ---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean } window.get_border_info = function(self) local border = self.style.border if not border or border == 'none' then return { top = 0, left = 0, right = 0, bottom = 0, vert = 0, horiz = 0, visible = false, } end if type(border) == 'string' then if border == 'shadow' then return { top = 0, left = 0, right = 1, bottom = 1, vert = 1, horiz = 1, visible = false, } end return { top = 1, left = 1, right = 1, bottom = 1, vert = 2, horiz = 2, visible = true, } end local new_border = {} while #new_border <= 8 do for _, b in ipairs(border) do table.insert(new_border, type(b) == 'string' and b or b[1]) end end local info = {} info.top = new_border[2] == '' and 0 or 1 info.right = new_border[4] == '' and 0 or 1 info.bottom = new_border[6] == '' and 0 or 1 info.left = new_border[8] == '' and 0 or 1 info.vert = info.top + info.bottom info.horiz = info.left + info.right info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8])) return info end ---Get scroll height. ---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view). ---@return integer window.get_content_height = function(self) if not self:option('wrap') then return vim.api.nvim_buf_line_count(self:get_buffer()) end local height = 0 vim.api.nvim_buf_call(self:get_buffer(), function() for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do -- nvim_buf_get_lines sometimes returns a blob. see #2050 if vim.fn.type(text) == vim.v.t_blob then text = vim.fn.string(text) end height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width)) end end) return height end return window ================================================ FILE: lua/cmp/view/custom_entries_view.lua ================================================ local event = require('cmp.utils.event') local autocmd = require('cmp.utils.autocmd') local feedkeys = require('cmp.utils.feedkeys') local window = require('cmp.utils.window') local config = require('cmp.config') local types = require('cmp.types') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local api = require('cmp.utils.api') local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45 ---@class cmp.CustomEntriesView ---@field private entries_win cmp.Window ---@field private offset integer ---@field private active boolean ---@field private entries cmp.Entry[] ---@field private column_width any ---@field private bottom_up boolean ---@field public event cmp.Event local custom_entries_view = {} custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view') custom_entries_view.new = function() local self = setmetatable({}, { __index = custom_entries_view }) self.entries_win = window.new() self.entries_win:option('conceallevel', 2) self.entries_win:option('concealcursor', 'n') self.entries_win:option('cursorlineopt', 'line') self.entries_win:option('foldenable', false) self.entries_win:option('wrap', false) -- This is done so that strdisplaywidth calculations for lines in the -- custom_entries_view window exactly match with what is really displayed, -- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be -- always rendered one column wide, which removes the unpredictability coming -- from variable width of the tab character. self.entries_win:buffer_option('tabstop', 1) self.entries_win:buffer_option('filetype', 'cmp_menu') self.entries_win:buffer_option('buftype', 'nofile') self.event = event.new() self.offset = -1 self.active = false self.entries = {} self.bottom_up = false autocmd.subscribe( 'CompleteChanged', vim.schedule_wrap(function() if self:visible() and vim.fn.pumvisible() == 1 then self:close() end end) ) vim.api.nvim_set_decoration_provider(custom_entries_view.ns, { on_win = function(_, win, buf, top, bot) if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then return end local fields = config.get().formatting.fields for i = top, bot do local e = self.entries[i + 1] if e then local v = e:get_view(self.offset, buf) local o = config.get().window.completion.side_padding local a = 0 for _, field in ipairs(fields) do if field == types.cmp.ItemField.Abbr then a = o end if type(v[field].hl_group) == 'table' then for _, extmark in ipairs(v[field].hl_group) do local hl_start, hl_end = unpack(extmark.range) vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o + hl_start, { end_line = i, end_col = o + hl_end, hl_group = extmark[1], hl_eol = false, ephemeral = true, }) end else vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, { end_line = i, end_col = o + v[field].bytes, hl_group = v[field].hl_group, hl_mode = 'combine', ephemeral = true, }) end o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1 end for _, m in ipairs(e:get_view_matches(v.abbr.text) or {}) do vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, { end_line = i, end_col = a + m.word_match_end, hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch', hl_mode = 'combine', ephemeral = true, }) end end end end, }) return self end custom_entries_view.ready = function() return vim.fn.pumvisible() == 0 end custom_entries_view.on_change = function(self) self.active = false end custom_entries_view.is_direction_top_down = function(self) local c = config.get() if (c.view and c.view.entries and c.view.entries.selection_order) == 'top_down' then return true elseif c.view.entries == nil or c.view.entries.selection_order == nil then return true else return not self.bottom_up end end custom_entries_view.open = function(self, offset, entries) local c = config.get() local completion = c.window.completion assert(completion, 'config.get() must resolve window.completion with defaults') self.offset = offset self.entries = {} self.column_width = { abbr = 0, icon = 0, kind = 0, menu = 0 } local entries_buf = self.entries_win:get_buffer() local fields = config.get().formatting.fields local lines = {} local dedup = {} local preselect_index = 0 for _, e in ipairs(entries) do local view = e:get_view(offset, entries_buf) if view.dup == 1 or not dedup[e.completion_item.label] then dedup[e.completion_item.label] = true for _, field in ipairs(fields) do self.column_width[field] = math.max(self.column_width[field], view[field].width) end table.insert(self.entries, e) table.insert(lines, ' ') if preselect_index == 0 and e.completion_item.preselect then preselect_index = #self.entries end end end if vim.bo[entries_buf].modifiable == false then vim.bo[entries_buf].modifiable = true vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines) vim.bo[entries_buf].modifiable = false else vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines) end vim.api.nvim_buf_set_option(entries_buf, 'modified', false) local width = 0 width = width + 1 width = width + self.column_width.abbr + (self.column_width.icon > 0 and 1 or 0) width = width + self.column_width.icon + (self.column_width.kind > 0 and 1 or 0) width = width + self.column_width.kind + (self.column_width.menu > 0 and 1 or 0) width = width + self.column_width.menu + 1 local height = completion.max_height or vim.api.nvim_get_option_value('pumheight', {}) height = height ~= 0 and height or #self.entries height = math.min(height, #self.entries) local delta = 0 if not config.get().view.entries.follow_cursor then local cursor_before_line = api.get_cursor_before_line() delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset)) end local pos = api.get_screen_cursor() local row, col = pos[1], pos[2] - delta - 1 local border_info = window.get_border_info({ style = completion }) local border_offset_row = border_info.top + border_info.bottom local border_offset_col = border_info.left + border_info.right local prefers_above = c.view.entries.vertical_positioning == 'above' local prefers_auto = c.view.entries.vertical_positioning == 'auto' local cant_fit_at_bottom = vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) local cant_fit_at_top = row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) local is_in_top_half = math.floor(vim.o.lines * 0.5) > row + border_offset_row local should_position_above = cant_fit_at_bottom or (prefers_above and not cant_fit_at_top) or (prefers_auto and is_in_top_half) if should_position_above then self.bottom_up = true height = math.min(height, row - 1) row = row - height - border_offset_row - 1 if row < 0 then height = height + row end else self.bottom_up = false end if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then width = math.min(width, vim.o.columns - 1) col = vim.o.columns - width - border_offset_col - 1 if col < 0 then width = width + col end end if not self:is_direction_top_down() then local n = #self.entries for i = 1, math.floor(n / 2) do self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i] end if preselect_index ~= 0 then preselect_index = #self.entries - preselect_index + 1 end end -- Apply window options (that might be changed) on the custom completion menu. self.entries_win:option('winblend', completion.winblend) self.entries_win:option('winhighlight', completion.winhighlight) self.entries_win:option('scrolloff', completion.scrolloff) self.entries_win:open({ relative = 'editor', style = 'minimal', row = math.max(0, row), col = math.max(0, col + completion.col_offset), width = width, height = height, border = completion.border, zindex = completion.zindex or 1001, }) -- Don't set the cursor if the entries_win:open function fails -- due to the window's width or height being less than 1 if self.entries_win.win == nil then return end -- Always set cursor when starting. It will be adjusted on the call to _select vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 }) if preselect_index > 0 and c.preselect == types.cmp.PreselectMode.Item then self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false }) elseif not string.match(c.completion.completeopt, 'noselect') then if self:is_direction_top_down() then self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false }) else self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false }) end else if self:is_direction_top_down() then self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false }) else self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false }) end end end custom_entries_view.close = function(self) self.prefix = nil self.offset = -1 self.active = false self.entries = {} self.entries_win:close() self.bottom_up = false end custom_entries_view.abort = function(self) if self.prefix then self:_insert(self.prefix) end feedkeys.call('', 'n', function() self:close() end) end custom_entries_view.draw = function(self) local info = vim.fn.getwininfo(self.entries_win.win)[1] local topline = info.topline - 1 local botline = info.topline + info.height - 1 local texts = {} local fields = config.get().formatting.fields local entries_buf = self.entries_win:get_buffer() for i = topline, botline - 1 do local e = self.entries[i + 1] if e then local view = e:get_view(self.offset, entries_buf) local text = {} table.insert(text, string.rep(' ', config.get().window.completion.side_padding)) for _, field in ipairs(fields) do table.insert(text, view[field].text) table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width)) end table.insert(text, string.rep(' ', config.get().window.completion.side_padding)) table.insert(texts, table.concat(text, '')) end end if vim.bo[entries_buf].modifiable == false then vim.bo[entries_buf].modifiable = true vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts) vim.bo[entries_buf].modifiable = false else vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts) end vim.api.nvim_buf_set_option(entries_buf, 'modified', false) if api.is_cmdline_mode() then vim.api.nvim_win_call(self.entries_win.win, function() misc.redraw() end) end end custom_entries_view.visible = function(self) return self.entries_win:visible() end custom_entries_view.info = function(self) return self.entries_win:info() end custom_entries_view.get_selected_index = function(self) if self:visible() and self.entries_win:option('cursorline') then return vim.api.nvim_win_get_cursor(self.entries_win.win)[1] end end custom_entries_view.select_next_item = function(self, option) if self:visible() then local cursor = self:get_selected_index() local is_top_down = self:is_direction_top_down() local last = #self.entries if not self.entries_win:option('cursorline') then cursor = (is_top_down and 1) or last else if is_top_down then if cursor == last then cursor = 0 else cursor = cursor + option.count if last < cursor then cursor = last end end else if cursor == 0 then cursor = last else cursor = cursor - option.count if cursor < 0 then cursor = 0 end end end end self:_select(cursor, { behavior = option.behavior or types.cmp.SelectBehavior.Insert, active = true, }) end end custom_entries_view.select_prev_item = function(self, option) if self:visible() then local cursor = self:get_selected_index() local is_top_down = self:is_direction_top_down() local last = #self.entries if not self.entries_win:option('cursorline') then cursor = (is_top_down and last) or 1 else if is_top_down then if cursor == 1 then cursor = 0 else cursor = cursor - option.count if cursor < 0 then cursor = 1 end end else if cursor == last then cursor = 0 else cursor = cursor + option.count if last < cursor then cursor = last end end end end self:_select(cursor, { behavior = option.behavior or types.cmp.SelectBehavior.Insert, active = true, }) end end custom_entries_view.get_offset = function(self) if self:visible() then return self.offset end return nil end custom_entries_view.get_entries = function(self) if self:visible() then return self.entries end return {} end custom_entries_view.get_first_entry = function(self) if self:visible() then return (self:is_direction_top_down() and self.entries[1]) or self.entries[#self.entries] end end custom_entries_view.get_selected_entry = function(self) if self:visible() and self.entries_win:option('cursorline') then return self.entries[self:get_selected_index()] end end custom_entries_view.get_active_entry = function(self) if self:visible() and self.active then return self:get_selected_entry() end end custom_entries_view._select = function(self, cursor, option) local is_insert = (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert if is_insert and not self.active then self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or '' end self.active = (0 < cursor and cursor <= #self.entries and option.active == true) self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries) vim.api.nvim_win_set_cursor(self.entries_win.win, { math.max(math.min(cursor, #self.entries), 1), 0, }) if is_insert then self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix) end self.entries_win:update() self:draw() self.event:emit('change') end custom_entries_view._insert = setmetatable({ pending = false, }, { __call = function(this, self, word) word = word or '' if api.is_cmdline_mode() then local cursor = api.get_cursor() -- setcmdline() added in v0.8.0 if vim.fn.has('nvim-0.8') == 1 then local current_line = api.get_current_line() local before_line = current_line:sub(1, self.offset - 1) local after_line = current_line:sub(cursor[2] + 1) local pos = #before_line + #word + 1 vim.fn.setcmdline(before_line .. word .. after_line, pos) vim.api.nvim_feedkeys(keymap.t('redraw'), 'ni', false) else vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) end else if this.pending then return end this.pending = true local release = require('cmp').suspend() feedkeys.call('', '', function() local cursor = api.get_cursor() local keys = {} table.insert(keys, keymap.indentkeys()) table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2]))) table.insert(keys, word) table.insert(keys, keymap.indentkeys(vim.bo.indentkeys)) feedkeys.call( table.concat(keys, ''), 'int', vim.schedule_wrap(function() this.pending = false release() end) ) end) end end, }) return custom_entries_view ================================================ FILE: lua/cmp/view/docs_view.lua ================================================ local window = require('cmp.utils.window') local config = require('cmp.config') ---@class cmp.DocsView ---@field public window cmp.Window local docs_view = {} ---Create new floating window module docs_view.new = function() local self = setmetatable({}, { __index = docs_view }) self.entry = nil self.window = window.new() self.window:option('conceallevel', 2) self.window:option('concealcursor', 'n') self.window:option('foldenable', false) self.window:option('linebreak', true) self.window:option('scrolloff', 0) self.window:option('showbreak', 'NONE') self.window:option('wrap', true) self.window:buffer_option('filetype', 'cmp_docs') self.window:buffer_option('buftype', 'nofile') return self end ---Open documentation window ---@param e cmp.Entry ---@param view cmp.WindowStyle ---@param bottom_up boolean|nil docs_view.open = function(self, e, view, bottom_up) local documentation = config.get().window.documentation if not documentation then return end if not e or not view then return self:close() end local border_info = window.get_border_info({ style = documentation }) local right_space = vim.o.columns - (view.col + view.width) - 1 local left_space = view.col - 1 local max_width = math.max(left_space, right_space) if documentation.max_width > 0 then max_width = math.min(documentation.max_width, max_width) end -- Update buffer content if needed. if not self.entry or e.id ~= self.entry.id then local documents = e:get_documentation() if #documents == 0 then return self:close() end self.entry = e vim.api.nvim_buf_call(self.window:get_buffer(), function() vim.cmd([[syntax clear]]) vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {}) end) local opts = { max_width = max_width - border_info.horiz, } if documentation.max_height > 0 then opts.max_height = documentation.max_height end vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, opts) end -- Set buffer as not modified, so it can be removed without errors vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false) -- Calculate window size. local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), { max_width = max_width - border_info.horiz, max_height = documentation.max_height - border_info.vert, }) if width <= 0 or height <= 0 then return self:close() end -- Calculate window position. local right_col = view.col + view.width local left_col = view.col - width - border_info.horiz local col, left if right_space >= width and left_space >= width then if right_space < left_space then col = left_col left = true else col = right_col end elseif right_space >= width then col = right_col elseif left_space >= width then col = left_col left = true else return self:close() end local row = bottom_up and math.max(view.row - (height + border_info.vert - view.height), 1) or view.row -- Render window. self.window:option('winblend', documentation.winblend) self.window:option('winhighlight', documentation.winhighlight) local style = { relative = 'editor', style = 'minimal', width = width, height = height, row = view.row, col = col + documentation.col_offset, border = documentation.border, zindex = documentation.zindex or 50, } self.window:open(style) -- Correct left-col for scrollbar existence. if left then style.col = col - self.window:info().scrollbar_offset - documentation.col_offset self.window:open(style) end end ---Close floating window docs_view.close = function(self) self.window:close() self.entry = nil end docs_view.scroll = function(self, delta) if self:visible() then local info = vim.fn.getwininfo(self.window.win)[1] or {} local top = info.topline or 1 top = top + delta top = math.max(top, 1) top = math.min(top, self.window:get_content_height() - info.height + 1) vim.defer_fn(function() vim.api.nvim_buf_call(self.window:get_buffer(), function() vim.api.nvim_command('normal! ' .. top .. 'zt') self.window:update() end) end, 0) end end docs_view.visible = function(self) return self.window:visible() end return docs_view ================================================ FILE: lua/cmp/view/ghost_text_view.lua ================================================ local config = require('cmp.config') local misc = require('cmp.utils.misc') local snippet = require('cmp.utils.snippet') -- local str = require('cmp.utils.str') local api = require('cmp.utils.api') local types = require('cmp.types') ---@class cmp.GhostTextView ---@field win number|nil ---@field entry cmp.Entry|nil local ghost_text_view = {} ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT') local has_inline = (function() return (pcall(function() local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, { virt_text = { { ' ', 'Comment' } }, virt_text_pos = 'inline', hl_mode = 'combine', ephemeral = false, }) vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id) end)) end)() ghost_text_view.new = function() local self = setmetatable({}, { __index = ghost_text_view }) self.win = nil self.entry = nil self.extmark_id = nil vim.api.nvim_set_decoration_provider(ghost_text_view.ns, { on_win = function(_, win) if self.extmark_id then if vim.api.nvim_buf_is_loaded(self.extmark_buf) then vim.api.nvim_buf_del_extmark(self.extmark_buf, ghost_text_view.ns, self.extmark_id) end self.extmark_id = nil end if win ~= self.win then return false end local c = config.get().experimental.ghost_text if not c then return end if not self.entry then return end local row, col = unpack(vim.api.nvim_win_get_cursor(0)) local line = vim.api.nvim_get_current_line() if not has_inline then if string.sub(line, col + 1) ~= '' then return end end local text = self.text_gen(self, line, col) if #text > 0 then local virt_lines = {} for _, l in ipairs(vim.fn.split(text, '\n')) do table.insert(virt_lines, { { l, type(c) == 'table' and c.hl_group or 'Comment' } }) end local first_line = table.remove(virt_lines, 1) self.extmark_buf = vim.api.nvim_get_current_buf() self.extmark_id = vim.api.nvim_buf_set_extmark(self.extmark_buf, ghost_text_view.ns, row - 1, col, { right_gravity = true, virt_text = first_line, virt_text_pos = has_inline and 'inline' or 'overlay', virt_lines = virt_lines, hl_mode = 'combine', ephemeral = false, }) end end, }) return self end ---Generate the ghost text --- This function calculates the bytes of the entry to display calculating the number --- of character differences instead of just byte difference. ghost_text_view.text_gen = function(self, line, cursor_col) local word = self.entry:get_insert_text() if self.entry:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = tostring(snippet.parse(word)) end local word_clen = vim.fn.strchars(word, true) local cword = string.sub(line, self.entry.offset, cursor_col) local cword_clen = vim.fn.strchars(cword, true) -- Number of characters from entry text (word) to be displayed as ghost thext local nchars = word_clen - cword_clen -- Missing characters to complete the entry text local text if nchars > 0 then text = string.sub(word, misc.to_vimindex(word, word_clen - nchars)) else text = '' end return text end ---Show ghost text ---@param e cmp.Entry ghost_text_view.show = function(self, e) if not api.is_insert_mode() then return end local c = config.get().experimental.ghost_text if not c then return end local changed = e ~= self.entry self.win = vim.api.nvim_get_current_win() self.entry = e if changed then misc.redraw(true) -- force invoke decoration provider. end end ghost_text_view.hide = function(self) if self.win and self.entry then self.win = nil self.entry = nil misc.redraw(true) -- force invoke decoration provider. end end return ghost_text_view ================================================ FILE: lua/cmp/view/native_entries_view.lua ================================================ local event = require('cmp.utils.event') local autocmd = require('cmp.utils.autocmd') local keymap = require('cmp.utils.keymap') local feedkeys = require('cmp.utils.feedkeys') local types = require('cmp.types') local config = require('cmp.config') local api = require('cmp.utils.api') ---@class cmp.NativeEntriesView ---@field private offset integer ---@field private items vim.CompletedItem ---@field private entries cmp.Entry[] ---@field private preselect_index integer ---@field public event cmp.Event local native_entries_view = {} native_entries_view.new = function() local self = setmetatable({}, { __index = native_entries_view }) self.event = event.new() self.offset = -1 self.items = {} self.entries = {} self.preselect_index = 0 autocmd.subscribe('CompleteChanged', function() self.event:emit('change') end) return self end native_entries_view.ready = function(_) if vim.fn.pumvisible() == 0 then return true end return vim.fn.complete_info({ 'mode' }).mode == 'eval' end native_entries_view.on_change = function(self) if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] + 1 then local preselect_enabled = config.get().preselect == types.cmp.PreselectMode.Item local completeopt = vim.o.completeopt if self.preselect_index == 1 and preselect_enabled then vim.o.completeopt = 'menu,menuone,noinsert' else vim.o.completeopt = config.get().completion.completeopt end vim.fn.complete(self.offset, self.items) vim.o.completeopt = completeopt if self.preselect_index > 1 and preselect_enabled then self:preselect(self.preselect_index) end end end native_entries_view.open = function(self, offset, entries) local dedup = {} local items = {} local dedup_entries = {} local preselect_index = 0 for _, e in ipairs(entries) do local item = e:get_vim_item(offset) if item.dup == 1 or not dedup[item.abbr] then dedup[item.abbr] = true table.insert(items, item) table.insert(dedup_entries, e) if preselect_index == 0 and e.completion_item.preselect then preselect_index = #dedup_entries end end end self.offset = offset self.items = items self.entries = dedup_entries self.preselect_index = preselect_index self:on_change() end native_entries_view.close = function(self) if api.is_insert_mode() and self:visible() then vim.api.nvim_select_popupmenu_item(-1, false, true, {}) end self.offset = -1 self.entries = {} self.items = {} self.preselect_index = 0 end native_entries_view.abort = function(_) if api.is_suitable_mode() then vim.api.nvim_select_popupmenu_item(-1, true, true, {}) end end native_entries_view.visible = function(_) return vim.fn.pumvisible() == 1 end native_entries_view.info = function(self) if self:visible() then local info = vim.fn.pum_getpos() return { width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1), height = info.height, row = info.row, col = info.col == 0 and 0 or info.col - 1, } end end native_entries_view.preselect = function(self, index) if self:visible() then if index <= #self.entries then vim.api.nvim_select_popupmenu_item(index - 1, false, false, {}) end end end native_entries_view.get_selected_index = function(self) if self:visible() then local idx = vim.fn.complete_info({ 'selected' }).selected if idx > -1 then return math.max(0, idx) + 1 end end end native_entries_view.select_next_item = function(self, option) local callback = function() self.event:emit('change') end if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) else feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) end end end native_entries_view.select_prev_item = function(self, option) local callback = function() self.event:emit('change') end if self:visible() then if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) else feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) end end end native_entries_view.get_offset = function(self) if self:visible() then return self.offset end return nil end native_entries_view.get_entries = function(self) if self:visible() then return self.entries end return {} end native_entries_view.get_first_entry = function(self) if self:visible() then return self.entries[1] end end native_entries_view.get_selected_entry = function(self) local idx = self:get_selected_index() if idx then return self.entries[idx] end end native_entries_view.get_active_entry = function(self) if self:visible() and (vim.v.completed_item or {}).word then return self:get_selected_entry() end end return native_entries_view ================================================ FILE: lua/cmp/view/wildmenu_entries_view.lua ================================================ local event = require('cmp.utils.event') local autocmd = require('cmp.utils.autocmd') local feedkeys = require('cmp.utils.feedkeys') local config = require('cmp.config') local window = require('cmp.utils.window') local types = require('cmp.types') local keymap = require('cmp.utils.keymap') local misc = require('cmp.utils.misc') local api = require('cmp.utils.api') ---@class cmp.CustomEntriesView ---@field private offset integer ---@field private entries_win cmp.Window ---@field private active boolean ---@field private entries cmp.Entry[] ---@field public event cmp.Event local wildmenu_entries_view = {} wildmenu_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view') wildmenu_entries_view.new = function() local self = setmetatable({}, { __index = wildmenu_entries_view }) self.event = event.new() self.offset = -1 self.active = false self.entries = {} self.offsets = {} self.selected_index = 0 self.entries_win = window.new() self.entries_win:option('conceallevel', 2) self.entries_win:option('concealcursor', 'n') self.entries_win:option('cursorlineopt', 'line') self.entries_win:option('foldenable', false) self.entries_win:option('wrap', false) self.entries_win:option('scrolloff', 0) self.entries_win:option('sidescrolloff', 0) self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None') self.entries_win:buffer_option('tabstop', 1) autocmd.subscribe( 'CompleteChanged', vim.schedule_wrap(function() if self:visible() and vim.fn.pumvisible() == 1 then self:close() end end) ) vim.api.nvim_set_decoration_provider(wildmenu_entries_view.ns, { on_win = function(_, win, buf, _, _) if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then return end for i, e in ipairs(self.entries) do if e then local view = e:get_view(self.offset, buf) vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], { end_line = 0, end_col = self.offsets[i] + view.abbr.bytes, hl_group = view.abbr.hl_group, hl_mode = 'combine', ephemeral = true, }) if i == self.selected_index then vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], { end_line = 0, end_col = self.offsets[i] + view.abbr.bytes, hl_group = 'PmenuSel', hl_mode = 'combine', ephemeral = true, }) end for _, m in ipairs(e:get_view_matches(view.abbr.text) or {}) do vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, { end_line = 0, end_col = self.offsets[i] + m.word_match_end, hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch', hl_mode = 'combine', ephemeral = true, }) end end end end, }) return self end wildmenu_entries_view.close = function(self) self.entries_win:close() end wildmenu_entries_view.ready = function() return vim.fn.pumvisible() == 0 end wildmenu_entries_view.on_change = function(self) self.active = false end wildmenu_entries_view.open = function(self, offset, entries) self.offset = offset self.entries = {} -- Apply window options (that might be changed) on the custom completion menu. self.entries_win:option('winblend', vim.o.pumblend) local dedup = {} local preselect = 0 local i = 1 for _, e in ipairs(entries) do local view = e:get_view(offset, 0) if view.dup == 1 or not dedup[e.completion_item.label] then dedup[e.completion_item.label] = true table.insert(self.entries, e) if preselect == 0 and e.completion_item.preselect then preselect = i end i = i + 1 end end self.entries_win:open({ relative = 'editor', style = 'minimal', row = vim.o.lines - 2, col = 0, width = vim.o.columns, height = 1, zindex = 1001, }) self:draw() if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select }) elseif not string.match(config.get().completion.completeopt, 'noselect') then self:_select(1, { behavior = types.cmp.SelectBehavior.Select }) else self:_select(0, { behavior = types.cmp.SelectBehavior.Select }) end end wildmenu_entries_view.abort = function(self) feedkeys.call('', 'n', function() self:close() end) end wildmenu_entries_view.draw = function(self) self.offsets = {} local entries_buf = self.entries_win:get_buffer() local texts = {} local offset = 0 for _, e in ipairs(self.entries) do local view = e:get_view(self.offset, entries_buf) table.insert(self.offsets, offset) table.insert(texts, view.abbr.text) offset = offset + view.abbr.bytes + #self:_get_separator() end vim.api.nvim_buf_set_lines(entries_buf, 0, 1, false, { table.concat(texts, self:_get_separator()) }) vim.api.nvim_buf_set_option(entries_buf, 'modified', false) vim.api.nvim_win_call(0, function() misc.redraw() end) end wildmenu_entries_view.visible = function(self) return self.entries_win:visible() end wildmenu_entries_view.info = function(self) return self.entries_win:info() end wildmenu_entries_view.get_selected_index = function(self) if self:visible() and self.active then return self.selected_index end end wildmenu_entries_view.select_next_item = function(self, option) if self:visible() then local cursor if self.selected_index == 0 or self.selected_index == #self.entries then cursor = option.count else cursor = self.selected_index + option.count end cursor = math.max(math.min(cursor, #self.entries), 0) self:_select(cursor, option) end end wildmenu_entries_view.select_prev_item = function(self, option) if self:visible() then if self.selected_index == 0 or self.selected_index <= 1 then self:_select(#self.entries, option) else self:_select(math.max(self.selected_index - option.count, 0), option) end end end wildmenu_entries_view.get_offset = function(self) if self:visible() then return self.offset end return nil end wildmenu_entries_view.get_entries = function(self) if self:visible() then return self.entries end return {} end wildmenu_entries_view.get_first_entry = function(self) if self:visible() then return self.entries[1] end end wildmenu_entries_view.get_selected_entry = function(self) local idx = self:get_selected_index() if idx then return self.entries[idx] end end wildmenu_entries_view.get_active_entry = function(self) if self:visible() and self.active then return self:get_selected_entry() end end wildmenu_entries_view._select = function(self, selected_index, option) local is_next = self.selected_index < selected_index self.selected_index = selected_index self.active = (selected_index ~= 0) if self.active then local e = self:get_active_entry() if option.behavior == types.cmp.SelectBehavior.Insert then local cursor = api.get_cursor() local word = e:get_vim_item(self.offset).word vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) end vim.api.nvim_win_call(self.entries_win.win, function() local view = e:get_view(self.offset, self.entries_win:get_buffer()) vim.api.nvim_win_set_cursor(0, { 1, self.offsets[selected_index] + (is_next and view.abbr.bytes or 0) }) vim.cmd([[redraw!]]) -- Force refresh for vim.api.nvim_set_decoration_provider end) end self.event:emit('change') end wildmenu_entries_view._get_separator = function() local c = config.get() return (c and c.view and c.view.entries and c.view.entries.separator) or ' ' end return wildmenu_entries_view ================================================ FILE: lua/cmp/view.lua ================================================ local config = require('cmp.config') local async = require('cmp.utils.async') local event = require('cmp.utils.event') local keymap = require('cmp.utils.keymap') local docs_view = require('cmp.view.docs_view') local custom_entries_view = require('cmp.view.custom_entries_view') local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view') local native_entries_view = require('cmp.view.native_entries_view') local ghost_text_view = require('cmp.view.ghost_text_view') ---@class cmp.View ---@field public event cmp.Event ---@field private is_docs_view_pinned boolean ---@field private resolve_dedup cmp.AsyncDedup ---@field private native_entries_view cmp.NativeEntriesView ---@field private custom_entries_view cmp.CustomEntriesView ---@field private wildmenu_entries_view cmp.CustomEntriesView ---@field private change_dedup cmp.AsyncDedup ---@field private docs_view cmp.DocsView ---@field private ghost_text_view cmp.GhostTextView local view = {} ---Create menu view.new = function() local self = setmetatable({}, { __index = view }) self.resolve_dedup = async.dedup() self.is_docs_view_pinned = false self.custom_entries_view = custom_entries_view.new() self.native_entries_view = native_entries_view.new() self.wildmenu_entries_view = wildmenu_entries_view.new() self.docs_view = docs_view.new() self.ghost_text_view = ghost_text_view.new() self.event = event.new() return self end ---Return the view components are available or not. ---@return boolean view.ready = function(self) return self:_get_entries_view():ready() end ---OnChange handler. view.on_change = function(self) self:_get_entries_view():on_change() end ---Open menu ---@param ctx cmp.Context ---@param sources cmp.Source[] ---@return boolean did_open view.open = function(self, ctx, sources) local source_group_map = {} for _, s in ipairs(sources) do local group_index = s:get_source_config().group_index or 0 if not source_group_map[group_index] then source_group_map[group_index] = {} end table.insert(source_group_map[group_index], s) end local group_indexes = vim.tbl_keys(source_group_map) table.sort(group_indexes, function(a, b) return a ~= b and (a < b) or nil end) local entries = {} for _, group_index in ipairs(group_indexes) do local source_group = source_group_map[group_index] or {} -- check the source triggered by character local has_triggered_by_symbol_source = false for _, s in ipairs(source_group) do if #s:get_entries(ctx) > 0 then if s.is_triggered_by_symbol then has_triggered_by_symbol_source = true break end end end -- create filtered entries. local offset = ctx.cursor.col local group_entries = {} local max_item_counts = {} for i, s in ipairs(source_group) do if s.offset <= ctx.cursor.col then if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then -- prepare max_item_counts map for filtering after sort. local max_item_count = s:get_source_config().max_item_count if max_item_count ~= nil then max_item_counts[s.name] = max_item_count end -- source order priority bonus. local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) for _, e in ipairs(s:get_entries(ctx)) do e.score = e.score + priority table.insert(group_entries, e) offset = math.min(offset, e.offset) end end end end -- sort. local comparators = config.get().sorting.comparators table.sort(group_entries, function(e1, e2) for _, fn in ipairs(comparators) do local diff = fn(e1, e2) if diff ~= nil then return diff end end end) -- filter by max_item_count. for _, e in ipairs(group_entries) do if max_item_counts[e.source.name] ~= nil then if max_item_counts[e.source.name] > 0 then max_item_counts[e.source.name] = max_item_counts[e.source.name] - 1 table.insert(entries, e) end else table.insert(entries, e) end end local max_view_entries = config.get().performance.max_view_entries or 200 entries = vim.list_slice(entries, 1, max_view_entries) -- open if #entries > 0 then self:_get_entries_view():open(offset, entries) self.event:emit('menu_opened', { window = self:_get_entries_view(), }) break end end -- complete_done. if #entries == 0 then self:close() end return #entries > 0 end ---Close menu view.close = function(self) if self:visible() then self.is_docs_view_pinned = false self.event:emit('complete_done', { entry = self:_get_entries_view():get_selected_entry(), }) end self:_get_entries_view():close() self.docs_view:close() self.ghost_text_view:hide() self.event:emit('menu_closed', { window = self:_get_entries_view(), }) end ---Abort menu view.abort = function(self) if self:visible() then self.is_docs_view_pinned = false end self:_get_entries_view():abort() self.docs_view:close() self.ghost_text_view:hide() self.event:emit('menu_closed', { window = self:_get_entries_view(), }) end ---Return the view is visible or not. ---@return boolean view.visible = function(self) return self:_get_entries_view():visible() end ---Opens the documentation window. view.open_docs = function(self) self.is_docs_view_pinned = true local e = self:get_selected_entry() if e then e:resolve(vim.schedule_wrap(self.resolve_dedup(function() if not self:visible() then return end local bottom_up = self.custom_entries_view.bottom_up self.docs_view:open(e, self:_get_entries_view():info(), bottom_up) end))) end end ---Closes the documentation window. view.close_docs = function(self) self.is_docs_view_pinned = false if self:get_selected_entry() then self.docs_view:close() end end ---Scroll documentation window if possible. ---@param delta integer view.scroll_docs = function(self, delta) self.docs_view:scroll(delta) end ---Get what number candidates are currently selected. ---If not selected, nil is returned. ---@return integer|nil view.get_selected_index = function(self) return self:_get_entries_view():get_selected_index() end ---Select prev menu item. ---@param option cmp.SelectOption view.select_next_item = function(self, option) self:_get_entries_view():select_next_item(option) end ---Select prev menu item. ---@param option cmp.SelectOption view.select_prev_item = function(self, option) self:_get_entries_view():select_prev_item(option) end ---Get offset. view.get_offset = function(self) return self:_get_entries_view():get_offset() end ---Get entries. ---@return cmp.Entry[] view.get_entries = function(self) return self:_get_entries_view():get_entries() end ---Get first entry ---@param self cmp.Entry|nil view.get_first_entry = function(self) return self:_get_entries_view():get_first_entry() end ---Get current selected entry ---@return cmp.Entry|nil view.get_selected_entry = function(self) return self:_get_entries_view():get_selected_entry() end ---Get current active entry ---@return cmp.Entry|nil view.get_active_entry = function(self) return self:_get_entries_view():get_active_entry() end ---Return current configured entries_view ---@return cmp.CustomEntriesView|cmp.NativeEntriesView view._get_entries_view = function(self) self.native_entries_view.event:clear() self.custom_entries_view.event:clear() self.wildmenu_entries_view.event:clear() local c = config.get() local v = self.custom_entries_view if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then v = self.wildmenu_entries_view elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then v = self.native_entries_view end v.event:on('change', function() self:on_entry_change() end) return v end ---On entry change view.on_entry_change = async.throttle(function(self) if not self:visible() then return end local e = self:get_selected_entry() if e then for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do keymap.listen('i', c, function(...) self.event:emit('keymap', ...) end) end e:resolve(vim.schedule_wrap(self.resolve_dedup(function() if not self:visible() then return end if self.is_docs_view_pinned or config.get().view.docs.auto_open then local bottom_up = self.custom_entries_view.bottom_up self.docs_view:open(e, self:_get_entries_view():info(), bottom_up) end end))) else self.docs_view:close() end e = e or self:get_first_entry() if e then self.ghost_text_view:show(e) else self.ghost_text_view:hide() end end, 20) return view ================================================ FILE: lua/cmp/vim_source.lua ================================================ local misc = require('cmp.utils.misc') local vim_source = {} ---@param id integer ---@param args any[] vim_source.on_callback = function(id, args) if vim_source.to_callback.callbacks[id] then vim_source.to_callback.callbacks[id](unpack(args)) end end ---@param callback function ---@return integer vim_source.to_callback = setmetatable({ callbacks = {}, }, { __call = function(self, callback) local id = misc.id('cmp.vim_source.to_callback') self.callbacks[id] = function(...) callback(...) self.callbacks[id] = nil end return id end, }) ---Convert to serializable args. ---@param args any[] vim_source.to_args = function(args) for i, arg in ipairs(args) do if type(arg) == 'function' then args[i] = vim_source.to_callback(arg) end end return args end ---@param bridge_id integer ---@param methods string[] vim_source.new = function(bridge_id, methods) local self = {} for _, method in ipairs(methods) do self[method] = (function(m) return function(_, ...) return vim.fn['cmp#_method'](bridge_id, m, vim_source.to_args({ ... })) end end)(method) end return self end return vim_source ================================================ FILE: nvim-cmp-scm-1.rockspec ================================================ local MODREV, SPECREV = 'scm', '-1' rockspec_format = '3.0' package = 'nvim-cmp' version = MODREV .. SPECREV description = { summary = 'A completion plugin for neovim', labels = { 'neovim' }, detailed = [[ A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced". ]], homepage = 'https://github.com/hrsh7th/nvim-cmp', license = 'MIT', } dependencies = { 'lua >= 5.1, < 5.4', } source = { url = 'git://github.com/hrsh7th/nvim-cmp', } build = { type = 'builtin', copy_directories = { 'autoload', 'plugin', 'doc' } } ================================================ FILE: plugin/cmp.lua ================================================ if vim.g.loaded_cmp then return end vim.g.loaded_cmp = true if not vim.api.nvim_create_autocmd then return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.') end local api = require('cmp.utils.api') local types = require('cmp.types') local highlight = require('cmp.utils.highlight') local autocmd = require('cmp.utils.autocmd') vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemKindIcon', { link = 'CmpItemKindIconDefault', default = true }) vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true }) for kind in pairs(types.lsp.CompletionItemKind) do if type(kind) == 'string' then local name = ('CmpItemKind%s'):format(kind) local icon_hl = name .. "Icon" vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true }) vim.api.nvim_set_hl(0, icon_hl, { link = ('%sDefault'):format(icon_hl), default = true }) end end autocmd.subscribe({ 'ColorScheme', 'UIEnter' }, function() highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false }) highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false }) highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false }) highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false }) highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false }) highlight.inherit('CmpItemKindIconDefault', 'Special', {bg = 'NONE', default = false }) highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false }) for name in pairs(types.lsp.CompletionItemKind) do if type(name) == 'string' then vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false }) vim.api.nvim_set_hl(0, ('CmpItemKind%sIconDefault'):format(name), {link = 'CmpItemKindIcon', default = false }) end end end) autocmd.emit('ColorScheme') if vim.on_key then local control_c_termcode = vim.api.nvim_replace_termcodes('', true, true, true) vim.on_key(function(keys) if keys == control_c_termcode then vim.schedule(function() if not api.is_suitable_mode() then autocmd.emit('InsertLeave') end end) end end, vim.api.nvim_create_namespace('cmp.plugin')) end vim.api.nvim_create_user_command('CmpStatus', function() require('cmp').status() end, { desc = 'Check status of cmp sources' }) vim.cmd([[doautocmd User CmpReady]]) ================================================ FILE: stylua.toml ================================================ indent_type = "Spaces" indent_width = 2 column_width = 1200 quote_style = "AutoPreferSingle" ================================================ FILE: utils/vimrc.vim ================================================ if has('vim_starting') set encoding=utf-8 endif scriptencoding utf-8 if &compatible set nocompatible endif let s:plug_dir = expand('/tmp/plugged/vim-plug') if !filereadable(s:plug_dir .. '/plug.vim') execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir) end execute 'set runtimepath+=' . s:plug_dir call plug#begin(s:plug_dir) Plug 'hrsh7th/nvim-cmp' Plug 'hrsh7th/cmp-buffer' Plug 'hrsh7th/cmp-nvim-lsp' Plug 'hrsh7th/vim-vsnip' Plug 'neovim/nvim-lspconfig' call plug#end() PlugInstall | quit " Setup global configuration. More on configuration below. lua << EOF local cmp = require "cmp" cmp.setup { snippet = { expand = function(args) vim.fn["vsnip#anonymous"](args.body) end, }, mapping = { [''] = cmp.mapping.confirm({ select = true }) }, sources = cmp.config.sources({ { name = "nvim_lsp" }, { name = "buffer" }, }), } EOF lua << EOF local capabilities = require('cmp_nvim_lsp').default_capabilities() vim.lsp.config('cssls', { capabilities = capabilities, }) vim.lsp.enable('cssls') EOF