Showing preview only (340K chars total). Download the full file or copy to clipboard to get everything.
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 <<EOF
-- Set up nvim-cmp.
local cmp = require'cmp'
cmp.setup({
snippet = {
-- REQUIRED - you must specify a snippet engine
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
-- vim.snippet.expand(args.body) -- For native neovim snippets (Neovim v0.10+)
-- For `mini.snippets` users:
-- local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
-- insert({ body = args.body }) -- Insert at cursor
-- cmp.resubscribe({ "TextChangedI", "TextChangedP" })
-- require("cmp.config").set_onetime({ sources = {} })
end,
},
window = {
-- completion = cmp.config.window.bordered(),
-- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = 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 <YOUR_LSP_SERVER> with each lsp server you've enabled.
vim.lsp.config('<YOUR_LSP_SERVER>', {
capabilities = capabilities
})
vim.lsp.enable('<YOUR_LSP_SERVER>')
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 <<EOF
local cmp = require'cmp'
-- Global setup.
cmp.setup({
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require'snippy'.expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
-- vim.snippet.expand(args.body) -- For native neovim snippets (Neovim v0.10+)
-- For `mini.snippets` users:
-- local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
-- insert({ body = args.body }) -- Insert at cursor
-- cmp.resubscribe({ "TextChangedI", "TextChangedP" })
-- require("cmp.config").set_onetime({ sources = {} })
end,
},
window = {
-- completion = cmp.config.window.bordered(),
-- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<CR>'] = 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: `<Cmd>lua require('cmp').complete()<CR>` 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 = {
["<C-j>"] = 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 = {
["<C-k>"] = 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 = {
['<C-s>'] = cmp.mapping.complete({
config = {
sources = {
{ name = 'vsnip' }
}
}
})
}
}
< >vim
inoremap <C-S> <Cmd>lua require('cmp').complete({ config = { sources = { { name = 'vsnip' } } } })<CR>
<
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 = {
['<C-l>'] = 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 = {
["<CR>"] = 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 `<CR>` 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 = {
['<CR>'] = 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 = {
['<Tab>'] = 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 = {
['<CR>'] = cmp.mapping(your_mapping_function, { 'i', 'c' })
}
}
<
You can also specify different mappings for different modes by passing a table:
>lua
cmp.setup {
mapping = {
['<CR>'] = 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~
`table<string, fun(fallback: function)`
See |cmp-mapping| section.
*cmp-config.snippet.expand*
snippet.expand~
`fun(option: cmp.SnippetExpansionParams)`
The snippet expansion function. That's how nvim-cmp interacts with a
particular snippet engine.
*cmp-config.completion.keyword_length*
completion.keyword_length~
`number`
The number of characters needed to trigger auto-completion.
*cmp-config.completion.keyword_pattern*
completion.keyword_pattern~
`string`
The default keyword pattern.
*cmp-config.completion.autocomplete*
completion.autocomplete~
`cmp.TriggerEvent[] | false`
The event to trigger autocompletion. If set to `false`, then completion is
only invoked manually (e.g. by calling `cmp.complete`).
*cmp-config.completion.completeopt*
completion.completeopt~
`string`
Like vim's completeopt setting. See 'completeopt'.
In general, you don't need to change this.
*cmp-config.confirmation.get_commit_characters*
confirmation.get_commit_characters~
`fun(commit_characters:string[]):string[]`
You can append or exclude commitCharacters via this configuration option
function. The commitCharacters are defined by the LSP spec.
*cmp-config.formatting.expandable_indicator*
formatting.expandable_indicator~
`cmp.expandable_indicator`
Boolean to show the `~` expandable indicator in cmp's floating window.
*cmp-config.formatting.fields*
formatting.fields~
`cmp.ItemField[]`
An array of completion fields to specify their order.
*cmp-config.formatting.format*
formatting.format~
`fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem`
The function used to customize the appearance of the completion menu. See
|complete-items|. This value can also be used to modify the `dup` property.
NOTE: The `vim.CompletedItem` can contain the special properties
`abbr_hl_group`, `icon_hl_group`, `kind_hl_group` and `menu_hl_group`.
*cmp-config.matching.disallow_fuzzy_matching*
matching.disallow_fuzzy_matching~
`boolean`
Whether to allow fuzzy matching.
*cmp-config.matching.disallow_fullfuzzy_matching*
matching.disallow_fullfuzzy_matching~
`boolean`
Whether to allow full-fuzzy matching.
*cmp-config.matching.disallow_partial_fuzzy_matching*
matching.disallow_partial_fuzzy_matching~
`boolean`
Whether to allow fuzzy matching without prefix matching.
*cmp-config.matching.disallow_partial_matching*
matching.disallow_partial_matching~
`boolean`
Whether to allow partial matching.
*cmp-config.matching.disallow_prefix_unmatching*
matching.disallow_prefix_unmatching~
`boolean`
Whether to allow prefix unmatching.
cmp-config.matching.disallow_symbol_nonprefix_matching
matching.disallow_symbol_nonprefix_matching
`boolean`
Whether to allow symbols in matches if the match is not a prefix match.
*cmp-config.sorting.priority_weight*
sorting.priority_weight~
`number`
Each item's original priority (given by its corresponding source) will be
increased by `#sources - (source_index - 1)` and multiplied by `priority_weight`.
That is, the final priority is calculated by the following formula:
>lua
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 = {
['<C-g>'] = 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 <C-x><C-o> <Cmd>lua require('cmp').complete()<CR>
<
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 <expr> <Plug>(vimrc:copilot-dummy-map) copilot#Accept("\<Tab>")
<
You can manage copilot.vim's accept feature inside nvim-cmp's key-mapping function:
>lua
cmp.setup {
mapping = {
['<C-g>'] = cmp.mapping(function(fallback)
vim.api.nvim_feedkeys(vim.fn['copilot#Accept'](vim.api.nvim_replace_termcodes('<Tab>', 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 {}, {
['<Down>'] = {
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<Up>'] = {
i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<C-n>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-p>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-y>'] = {
i = mapping.confirm({ select = false }),
},
['<C-e>'] = {
i = mapping.abort(),
},
})
end
---Mapping preset cmdline-mode configuration.
mapping.preset.cmdline = function(override)
return merge_keymaps(override or {}, {
['<C-z>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<S-Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
cmp.complete()
end
end,
},
['<C-n>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end,
},
['<C-p>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
fallback()
end
end,
},
['<C-e>'] = {
c = mapping.abort(),
},
['<C-y>'] = {
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<integer, cmp.ConfigSchema>
config.buffers = {}
---@type table<string, cmp.ConfigSchema>
config.filetypes = {}
---@type table<string, cmp.ConfigSchema>
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 `<C-y>` 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***<Left><Left><Left><Left><Left>'), '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***<Left><Left><Left><Left><Left>'), '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<Left><Left><Left>'):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***<Left><Left><Left><Left><Left>'), '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 <Tab> 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<string, boolean>
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 = ' </div',
},
})
assert.are.equal(e.offset, 5)
assert.are.equal(e:get_vim_item(e.offset).word, '</div')
end)
it('[clangd] 1', function()
--NOTE: clangd does not return `.foo` as filterText but we should care about it.
--nvim-cmp does care it by special handling in entry.lua.
local state = spec.state('foo', 1, 4)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
insertText = '->foo',
label = ' foo',
textEdit = {
newText = '->foo',
range = {
start = {
character = 3,
line = 1,
},
['end'] = {
character = 4,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(4).word, '->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('<C-n>', option.count)), 'in')
else
feedkeys.call(keymap.t(string.rep('<Down>', 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('<C-p>', option.count)), 'in')
else
feedkeys.call(keymap.t(string.rep('<Up>', 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 `:<C-u>...<CR>` 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 <CR> if the trigger characters contain the <Space>.
-- 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<string, cmp.Mapping>
---@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.e
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
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (344K chars).
[
{
"path": ".githooks/pre-commit",
"chars": 201,
"preview": "#!/bin/sh\n\nDIR=\"$(dirname $(dirname $( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )))\"\n\ncd $DIR\nmake p"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1955,
"preview": "name: Bug Report\ndescription: Report a problem in nvim-cmp\nlabels: [bug]\nbody:\n - type: checkboxes\n id: faq-prerequi"
},
{
"path": ".github/workflows/format.yaml",
"chars": 481,
"preview": "name: format\n\non:\n push:\n branches:\n - main\n paths:\n - '**.lua'\n\njobs:\n postprocessing:\n runs-on: u"
},
{
"path": ".github/workflows/integration.yaml",
"chars": 708,
"preview": "name: integration\n\non:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n\njobs:\n integratio"
},
{
"path": ".github/workflows/release-please.yaml",
"chars": 310,
"preview": "---\npermissions:\n contents: write\n pull-requests: write\n\nname: Release Please\n\non:\n workflow_dispatch:\n push:\n br"
},
{
"path": ".gitignore",
"chars": 33,
"preview": "doc/tags\nutils/stylua\n.DS_Store\n\n"
},
{
"path": ".luacheckrc",
"chars": 110,
"preview": "globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' }\nmax_line_length = false\n"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2021 hrsh7th\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "Makefile",
"chars": 202,
"preview": ".PHONY: lint\nlint:\n\tluacheck ./lua\n\n.PHONY: test\ntest:\n\tvusted --output=gtest ./lua\n\n.PHONY: pre-commit\npre-commit:\n\tlua"
},
{
"path": "README.md",
"chars": 5029,
"preview": "# nvim-cmp\n\nA completion engine plugin for neovim written in Lua.\nCompletion sources are installed from external reposit"
},
{
"path": "autoload/cmp.vim",
"chars": 2117,
"preview": "let s:bridge_id = 0\nlet s:sources = {}\n\n\"\n\" cmp#register_source\n\"\nfunction! cmp#register_source(name, source) abort\n le"
},
{
"path": "doc/cmp.txt",
"chars": 37170,
"preview": "*nvim-cmp* *cmp*\n\nA completion plugin for neovim coded in Lua.\n\n========================================================"
},
{
"path": "init.sh",
"chars": 161,
"preview": "DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\n\nrm $DIR/.git/hooks/*\ncp $DIR/.githooks/* $DIR/."
},
{
"path": "lua/cmp/config/compare.lua",
"chars": 9029,
"preview": "local types = require('cmp.types')\nlocal cache = require('cmp.utils.cache')\n\n---@type cmp.Comparator[]\nlocal compare = {"
},
{
"path": "lua/cmp/config/context.lua",
"chars": 1708,
"preview": "local api = require('cmp.utils.api')\n\nlocal context = {}\n\n---Check if cursor is in syntax group\n---@param group string |"
},
{
"path": "lua/cmp/config/default.lua",
"chars": 3306,
"preview": "local compare = require('cmp.config.compare')\nlocal types = require('cmp.types')\nlocal window = require('cmp.config.wind"
},
{
"path": "lua/cmp/config/mapping.lua",
"chars": 5093,
"preview": "local types = require('cmp.types')\nlocal misc = require('cmp.utils.misc')\nlocal keymap = require('cmp.utils.keymap')\n\nlo"
},
{
"path": "lua/cmp/config/sources.lua",
"chars": 217,
"preview": "return function(...)\n local sources = {}\n for i, group in ipairs({ ... }) do\n for _, source in ipairs(group) do\n "
},
{
"path": "lua/cmp/config/window.lua",
"chars": 833,
"preview": "local window = {}\n\nwindow.bordered = function(opts)\n opts = opts or {}\n return {\n border = opts.border or window.ge"
},
{
"path": "lua/cmp/config.lua",
"chars": 6634,
"preview": "local mapping = require('cmp.config.mapping')\nlocal cache = require('cmp.utils.cache')\nlocal keymap = require('cmp.utils"
},
{
"path": "lua/cmp/context.lua",
"chars": 3125,
"preview": "local misc = require('cmp.utils.misc')\nlocal pattern = require('cmp.utils.pattern')\nlocal types = require('cmp.types')\nl"
},
{
"path": "lua/cmp/context_spec.lua",
"chars": 926,
"preview": "local spec = require('cmp.utils.spec')\n\nlocal context = require('cmp.context')\n\ndescribe('context', function()\n before_"
},
{
"path": "lua/cmp/core.lua",
"chars": 16344,
"preview": "local debug = require('cmp.utils.debug')\nlocal str = require('cmp.utils.str')\nlocal char = require('cmp.utils.char')\nloc"
},
{
"path": "lua/cmp/core_spec.lua",
"chars": 7172,
"preview": "local spec = require('cmp.utils.spec')\nlocal feedkeys = require('cmp.utils.feedkeys')\nlocal types = require('cmp.types')"
},
{
"path": "lua/cmp/entry.lua",
"chars": 22252,
"preview": "local cache = require('cmp.utils.cache')\nlocal char = require('cmp.utils.char')\nlocal misc = require('cmp.utils.misc')\nl"
},
{
"path": "lua/cmp/entry_spec.lua",
"chars": 9582,
"preview": "local spec = require('cmp.utils.spec')\n\nlocal entry = require('cmp.entry')\n\ndescribe('entry', function()\n before_each(s"
},
{
"path": "lua/cmp/init.lua",
"chars": 10843,
"preview": "local core = require('cmp.core')\nlocal source = require('cmp.source')\nlocal config = require('cmp.config')\nlocal feedkey"
},
{
"path": "lua/cmp/matcher.lua",
"chars": 10613,
"preview": "local char = require('cmp.utils.char')\n\nlocal matcher = {}\n\nmatcher.WORD_BOUNDALY_ORDER_FACTOR = 10\n\nmatcher.PREFIX_FACT"
},
{
"path": "lua/cmp/matcher_spec.lua",
"chars": 5585,
"preview": "local spec = require('cmp.utils.spec')\nlocal default_config = require('cmp.config.default')\n\nlocal matcher = require('cm"
},
{
"path": "lua/cmp/source.lua",
"chars": 12882,
"preview": "local context = require('cmp.context')\nlocal config = require('cmp.config')\nlocal entry = require('cmp.entry')\nlocal deb"
},
{
"path": "lua/cmp/source_spec.lua",
"chars": 3286,
"preview": "local config = require('cmp.config')\nlocal spec = require('cmp.utils.spec')\n\nlocal source = require('cmp.source')\n\ndescr"
},
{
"path": "lua/cmp/types/cmp.lua",
"chars": 6730,
"preview": "local cmp = {}\n\n---@alias cmp.ConfirmBehavior 'insert' | 'replace'\ncmp.ConfirmBehavior = {\n Insert = 'insert',\n Replac"
},
{
"path": "lua/cmp/types/init.lua",
"chars": 143,
"preview": "local types = {}\n\ntypes.cmp = require('cmp.types.cmp')\ntypes.lsp = require('cmp.types.lsp')\ntypes.vim = require('cmp.typ"
},
{
"path": "lua/cmp/types/lsp.lua",
"chars": 8304,
"preview": "local misc = require('cmp.utils.misc')\n\n---@see https://microsoft.github.io/language-server-protocol/specifications/spec"
},
{
"path": "lua/cmp/types/lsp_spec.lua",
"chars": 1696,
"preview": "local spec = require('cmp.utils.spec')\nlocal lsp = require('cmp.types.lsp')\n\ndescribe('types.lsp', function()\n before_e"
},
{
"path": "lua/cmp/types/vim.lua",
"chars": 677,
"preview": "---@class vim.CompletedItem\n---@field public word string\n---@field public abbr string|nil\n---@field public icon string|n"
},
{
"path": "lua/cmp/utils/api.lua",
"chars": 2375,
"preview": "local api = {}\n\nlocal CTRL_V = vim.api.nvim_replace_termcodes('<C-v>', true, true, true)\nlocal CTRL_S = vim.api.nvim_rep"
},
{
"path": "lua/cmp/utils/api_spec.lua",
"chars": 2233,
"preview": "local spec = require('cmp.utils.spec')\nlocal keymap = require('cmp.utils.keymap')\nlocal feedkeys = require('cmp.utils.fe"
},
{
"path": "lua/cmp/utils/async.lua",
"chars": 6719,
"preview": "local feedkeys = require('cmp.utils.feedkeys')\nlocal config = require('cmp.config')\n\nlocal async = {}\n\n---@class cmp.Asy"
},
{
"path": "lua/cmp/utils/async_spec.lua",
"chars": 1579,
"preview": "local async = require('cmp.utils.async')\n\ndescribe('utils.async', function()\n it('throttle', function()\n local count"
},
{
"path": "lua/cmp/utils/autocmd.lua",
"chars": 1801,
"preview": "local debug = require('cmp.utils.debug')\n\nlocal autocmd = {}\n\nautocmd.group = vim.api.nvim_create_augroup('___cmp___', {"
},
{
"path": "lua/cmp/utils/binary.lua",
"chars": 715,
"preview": "local binary = {}\n\n---Insert item to list to ordered index\n---@param list any[]\n---@param item any\n---@param func fun(a:"
},
{
"path": "lua/cmp/utils/binary_spec.lua",
"chars": 1048,
"preview": "local binary = require('cmp.utils.binary')\n\ndescribe('utils.binary', function()\n it('insort', function()\n local func"
},
{
"path": "lua/cmp/utils/buffer.lua",
"chars": 531,
"preview": "local buffer = {}\n\nbuffer.cache = {}\n\n---@return integer buf\nbuffer.get = function(name)\n local buf = buffer.cache[name"
},
{
"path": "lua/cmp/utils/cache.lua",
"chars": 1142,
"preview": "---@class cmp.Cache\n---@field public entries any\nlocal cache = {}\n\ncache.new = function()\n local self = setmetatable({}"
},
{
"path": "lua/cmp/utils/char.lua",
"chars": 2456,
"preview": "local _\n\nlocal alpha = {}\n_ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)\n alpha[string.byte(char)] ="
},
{
"path": "lua/cmp/utils/debug.lua",
"chars": 380,
"preview": "local debug = {}\n\ndebug.flag = false\n\n---Print log\n---@vararg any\ndebug.log = function(...)\n if debug.flag then\n loc"
},
{
"path": "lua/cmp/utils/event.lua",
"chars": 1101,
"preview": "---@class cmp.Event\n---@field private events table<string, function[]>\nlocal event = {}\n\n---Create vents\nevent.new = fun"
},
{
"path": "lua/cmp/utils/feedkeys.lua",
"chars": 1867,
"preview": "local keymap = require('cmp.utils.keymap')\nlocal misc = require('cmp.utils.misc')\n\nlocal feedkeys = {}\n\nfeedkeys.call = "
},
{
"path": "lua/cmp/utils/feedkeys_spec.lua",
"chars": 1617,
"preview": "local spec = require('cmp.utils.spec')\nlocal keymap = require('cmp.utils.keymap')\n\nlocal feedkeys = require('cmp.utils.f"
},
{
"path": "lua/cmp/utils/highlight.lua",
"chars": 623,
"preview": "local highlight = {}\n\nhighlight.keys = {\n 'fg',\n 'bg',\n 'bold',\n 'italic',\n 'reverse',\n 'standout',\n 'underline',"
},
{
"path": "lua/cmp/utils/keymap.lua",
"chars": 7300,
"preview": "local misc = require('cmp.utils.misc')\nlocal buffer = require('cmp.utils.buffer')\nlocal api = require('cmp.utils.api')\n\n"
},
{
"path": "lua/cmp/utils/keymap_spec.lua",
"chars": 6347,
"preview": "local spec = require('cmp.utils.spec')\nlocal api = require('cmp.utils.api')\nlocal feedkeys = require('cmp.utils.feedkeys"
},
{
"path": "lua/cmp/utils/misc.lua",
"chars": 5598,
"preview": "local misc = {}\n\nlocal islist = vim.islist or vim.tbl_islist\n\n---Create once callback\n---@param callback function\n---@re"
},
{
"path": "lua/cmp/utils/misc_spec.lua",
"chars": 1533,
"preview": "local spec = require('cmp.utils.spec')\n\nlocal misc = require('cmp.utils.misc')\n\ndescribe('misc', function()\n before_eac"
},
{
"path": "lua/cmp/utils/options.lua",
"chars": 737,
"preview": "local M = {}\n\n-- Set window option without triggering the OptionSet event\n---@param window number\n---@param name string\n"
},
{
"path": "lua/cmp/utils/pattern.lua",
"chars": 493,
"preview": "local pattern = {}\n\npattern._regexes = {}\n\npattern.regex = function(p)\n if not pattern._regexes[p] then\n pattern._re"
},
{
"path": "lua/cmp/utils/snippet.lua",
"chars": 10413,
"preview": "local misc = require('cmp.utils.misc')\n\nlocal P = {}\n\n---Take characters until the target characters (The escape sequenc"
},
{
"path": "lua/cmp/utils/spec.lua",
"chars": 2360,
"preview": "local context = require('cmp.context')\nlocal source = require('cmp.source')\nlocal types = require('cmp.types')\nlocal con"
},
{
"path": "lua/cmp/utils/str.lua",
"chars": 4596,
"preview": "local char = require('cmp.utils.char')\n\nlocal str = {}\n\nlocal INVALIDS = {}\nINVALIDS[string.byte(\"'\")] = true\nINVALIDS[s"
},
{
"path": "lua/cmp/utils/str_spec.lua",
"chars": 2830,
"preview": "local str = require('cmp.utils.str')\n\ndescribe('utils.str', function()\n it('get_word', function()\n assert.are.equal("
},
{
"path": "lua/cmp/utils/window.lua",
"chars": 9868,
"preview": "local misc = require('cmp.utils.misc')\nlocal opt = require('cmp.utils.options')\nlocal buffer = require('cmp.utils.buffer"
},
{
"path": "lua/cmp/view/custom_entries_view.lua",
"chars": 16849,
"preview": "local event = require('cmp.utils.event')\nlocal autocmd = require('cmp.utils.autocmd')\nlocal feedkeys = require('cmp.util"
},
{
"path": "lua/cmp/view/docs_view.lua",
"chars": 4409,
"preview": "local window = require('cmp.utils.window')\nlocal config = require('cmp.config')\n\n---@class cmp.DocsView\n---@field public"
},
{
"path": "lua/cmp/view/ghost_text_view.lua",
"chars": 3971,
"preview": "local config = require('cmp.config')\nlocal misc = require('cmp.utils.misc')\nlocal snippet = require('cmp.utils.snippet')"
},
{
"path": "lua/cmp/view/native_entries_view.lua",
"chars": 5107,
"preview": "local event = require('cmp.utils.event')\nlocal autocmd = require('cmp.utils.autocmd')\nlocal keymap = require('cmp.utils."
},
{
"path": "lua/cmp/view/wildmenu_entries_view.lua",
"chars": 8104,
"preview": "local event = require('cmp.utils.event')\nlocal autocmd = require('cmp.utils.autocmd')\nlocal feedkeys = require('cmp.util"
},
{
"path": "lua/cmp/view.lua",
"chars": 9039,
"preview": "local config = require('cmp.config')\nlocal async = require('cmp.utils.async')\nlocal event = require('cmp.utils.event')\nl"
},
{
"path": "lua/cmp/vim_source.lua",
"chars": 1188,
"preview": "local misc = require('cmp.utils.misc')\n\nlocal vim_source = {}\n\n---@param id integer\n---@param args any[]\nvim_source.on_c"
},
{
"path": "nvim-cmp-scm-1.rockspec",
"chars": 628,
"preview": "local MODREV, SPECREV = 'scm', '-1'\nrockspec_format = '3.0'\npackage = 'nvim-cmp'\nversion = MODREV .. SPECREV\n\ndescriptio"
},
{
"path": "plugin/cmp.lua",
"chars": 3023,
"preview": "if vim.g.loaded_cmp then\n return\nend\nvim.g.loaded_cmp = true\n\nif not vim.api.nvim_create_autocmd then\n return print('["
},
{
"path": "stylua.toml",
"chars": 93,
"preview": "indent_type = \"Spaces\"\nindent_width = 2\ncolumn_width = 1200\nquote_style = \"AutoPreferSingle\"\n"
},
{
"path": "utils/vimrc.vim",
"chars": 1143,
"preview": "if has('vim_starting')\n set encoding=utf-8\nendif\nscriptencoding utf-8\n\nif &compatible\n set nocompatible\nendif\n\nlet s:p"
}
]
About this extraction
This page contains the full source code of the hrsh7th/nvim-cmp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (320.6 KB), approximately 85.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.