├── .githooks └── pre-commit ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ ├── format.yaml │ ├── integration.yaml │ └── release.yaml ├── .gitignore ├── .luacheckrc ├── LICENSE ├── Makefile ├── README.md ├── autoload └── cmp.vim ├── doc └── cmp.txt ├── init.sh ├── lua └── cmp │ ├── config.lua │ ├── config │ ├── compare.lua │ ├── context.lua │ ├── default.lua │ ├── mapping.lua │ ├── sources.lua │ └── window.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.lua │ ├── view │ ├── custom_entries_view.lua │ ├── docs_view.lua │ ├── ghost_text_view.lua │ ├── native_entries_view.lua │ └── wildmenu_entries_view.lua │ └── vim_source.lua ├── nvim-cmp-scm-1.rockspec ├── plugin └── cmp.lua ├── stylua.toml └── utils └── vimrc.vim /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR="$(dirname $(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )))" 4 | 5 | cd $DIR 6 | make pre-commit 7 | for FILE in `git diff --staged --name-only`; do 8 | git add $FILE 9 | done 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a problem in nvim-cmp 3 | labels: [bug] 4 | body: 5 | - type: checkboxes 6 | id: faq-prerequisite 7 | attributes: 8 | label: FAQ 9 | options: 10 | - label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/main/doc/cmp.txt) and it didn't resolve my problem. 11 | required: true 12 | 13 | - type: checkboxes 14 | id: announcement-prerequisite 15 | attributes: 16 | label: Announcement 17 | options: 18 | - label: I have checked [Breaking change announcement](https://github.com/hrsh7th/nvim-cmp/issues/231). 19 | required: true 20 | 21 | - type: textarea 22 | attributes: 23 | label: "Minimal reproducible full config" 24 | description: | 25 | You must provide a working config based on [this](https://github.com/hrsh7th/nvim-cmp/blob/main/utils/vimrc.vim). Not part of config. 26 | 1. Copy the base minimal config to the `~/cmp-repro.vim` 27 | 2. Edit `~/cmp-repro.vim` for reproducing the issue 28 | 3. Open `nvim -u ~/cmp-repro.vim` 29 | 4. Check reproduction step 30 | value: | 31 | ```vim 32 | 33 | ``` 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | attributes: 39 | label: "Description" 40 | description: "Describe in detail what happens" 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | attributes: 46 | label: "Steps to reproduce" 47 | description: "Full reproduction steps. Include a sample file if your issue relates to a specific filetype." 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | attributes: 53 | label: "Expected behavior" 54 | description: "A description of the behavior you expected." 55 | validations: 56 | required: true 57 | 58 | - type: textarea 59 | attributes: 60 | label: "Actual behavior" 61 | description: "A description of the actual behavior." 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | attributes: 67 | label: "Additional context" 68 | description: "Any other relevant information" 69 | -------------------------------------------------------------------------------- /.github/workflows/format.yaml: -------------------------------------------------------------------------------- 1 | name: format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.lua' 9 | 10 | jobs: 11 | postprocessing: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Format with Stylua 17 | uses: JohnnyMorganz/stylua-action@v2 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | version: v0.16.1 21 | args: ./lua 22 | 23 | - uses: stefanzweifel/git-auto-commit-action@v4 24 | with: 25 | commit_message: "Format with stylua" 26 | -------------------------------------------------------------------------------- /.github/workflows/integration.yaml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | integration: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup neovim 20 | uses: rhysd/action-setup-vim@v1 21 | with: 22 | version: nightly 23 | neovim: true 24 | 25 | - name: Setup lua 26 | uses: leafo/gh-actions-lua@v10 27 | with: 28 | luaVersion: "luajit-openresty" 29 | 30 | - name: Setup luarocks 31 | uses: leafo/gh-actions-luarocks@v4 32 | 33 | - name: Setup tools 34 | shell: bash 35 | run: | 36 | luarocks install luacheck 37 | luarocks install vusted 38 | 39 | - name: Run tests 40 | shell: bash 41 | run: make integration 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "release" 2 | on: 3 | push: 4 | tags: # Will upload to luarocks.org when a tag is pushed 5 | - "*" 6 | pull_request: # Will test a local install without uploading to luarocks.org 7 | 8 | jobs: 9 | luarocks-release: 10 | name: LuaRocks upload 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: LuaRocks Upload 16 | uses: nvim-neorocks/luarocks-tag-release@v5 17 | env: 18 | LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} 19 | with: 20 | labels: | 21 | neovim 22 | nvim-cmp 23 | detailed_description: | 24 | A completion engine plugin for neovim written in Lua. 25 | Completion sources are installed from external repositories and "sourced". 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | utils/stylua 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' } 2 | max_line_length = false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 hrsh7th 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | luacheck ./lua 4 | 5 | .PHONY: test 6 | test: 7 | vusted --output=gtest ./lua 8 | 9 | .PHONY: pre-commit 10 | pre-commit: 11 | luacheck lua 12 | vusted lua 13 | 14 | .PHONY: integration 15 | integration: 16 | luacheck lua 17 | vusted lua 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-cmp 2 | 3 | A completion engine plugin for neovim written in Lua. 4 | Completion sources are installed from external repositories and "sourced". 5 | 6 | https://github.com/hrsh7th/nvim-cmp/assets/22756295/afa70011-9121-4e42-aedd-0153b630eeab 7 | 8 | Readme! 9 | ==================== 10 | 11 | 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. 12 | 2. This is my hobby project. You can support me via GitHub sponsors. 13 | 3. Bug reports are welcome, but don't expect a fix unless you provide minimal configuration and steps to reproduce your issue. 14 | 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. 15 | 16 | Concept 17 | ==================== 18 | 19 | - Full support for LSP completion related capabilities 20 | - Powerful customizability via Lua functions 21 | - Smart handling of key mappings 22 | - No flicker 23 | 24 | 25 | Setup 26 | ==================== 27 | 28 | ### Recommended Configuration 29 | 30 | This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as a snippet plugin. 31 | 32 | ```vim 33 | call plug#begin(s:plug_dir) 34 | Plug 'neovim/nvim-lspconfig' 35 | Plug 'hrsh7th/cmp-nvim-lsp' 36 | Plug 'hrsh7th/cmp-buffer' 37 | Plug 'hrsh7th/cmp-path' 38 | Plug 'hrsh7th/cmp-cmdline' 39 | Plug 'hrsh7th/nvim-cmp' 40 | 41 | " For vsnip users. 42 | Plug 'hrsh7th/cmp-vsnip' 43 | Plug 'hrsh7th/vim-vsnip' 44 | 45 | " For luasnip users. 46 | " Plug 'L3MON4D3/LuaSnip' 47 | " Plug 'saadparwaiz1/cmp_luasnip' 48 | 49 | " For mini.snippets users. 50 | " Plug 'echasnovski/mini.snippets' 51 | " Plug 'abeldekat/cmp-mini-snippets' 52 | 53 | " For ultisnips users. 54 | " Plug 'SirVer/ultisnips' 55 | " Plug 'quangnguyen30192/cmp-nvim-ultisnips' 56 | 57 | " For snippy users. 58 | " Plug 'dcampos/nvim-snippy' 59 | " Plug 'dcampos/cmp-snippy' 60 | 61 | call plug#end() 62 | 63 | lua <'] = cmp.mapping.scroll_docs(-4), 90 | [''] = cmp.mapping.scroll_docs(4), 91 | [''] = cmp.mapping.complete(), 92 | [''] = cmp.mapping.abort(), 93 | [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. 94 | }), 95 | sources = cmp.config.sources({ 96 | { name = 'nvim_lsp' }, 97 | { name = 'vsnip' }, -- For vsnip users. 98 | -- { name = 'luasnip' }, -- For luasnip users. 99 | -- { name = 'ultisnips' }, -- For ultisnips users. 100 | -- { name = 'snippy' }, -- For snippy users. 101 | }, { 102 | { name = 'buffer' }, 103 | }) 104 | }) 105 | 106 | -- To use git you need to install the plugin petertriho/cmp-git and uncomment lines below 107 | -- Set configuration for specific filetype. 108 | --[[ cmp.setup.filetype('gitcommit', { 109 | sources = cmp.config.sources({ 110 | { name = 'git' }, 111 | }, { 112 | { name = 'buffer' }, 113 | }) 114 | }) 115 | require("cmp_git").setup() ]]-- 116 | 117 | -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore). 118 | cmp.setup.cmdline({ '/', '?' }, { 119 | mapping = cmp.mapping.preset.cmdline(), 120 | sources = { 121 | { name = 'buffer' } 122 | } 123 | }) 124 | 125 | -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). 126 | cmp.setup.cmdline(':', { 127 | mapping = cmp.mapping.preset.cmdline(), 128 | sources = cmp.config.sources({ 129 | { name = 'path' } 130 | }, { 131 | { name = 'cmdline' } 132 | }), 133 | matching = { disallow_symbol_nonprefix_matching = false } 134 | }) 135 | 136 | -- Set up lspconfig. 137 | local capabilities = require('cmp_nvim_lsp').default_capabilities() 138 | -- Replace with each lsp server you've enabled. 139 | require('lspconfig')[''].setup { 140 | capabilities = capabilities 141 | } 142 | EOF 143 | ``` 144 | 145 | 146 | ### Where can I find more completion sources? 147 | 148 | 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). 149 | 150 | 151 | ### Where can I find advanced configuration examples? 152 | 153 | See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki). 154 | -------------------------------------------------------------------------------- /autoload/cmp.vim: -------------------------------------------------------------------------------- 1 | let s:bridge_id = 0 2 | let s:sources = {} 3 | 4 | " 5 | " cmp#register_source 6 | " 7 | function! cmp#register_source(name, source) abort 8 | let l:methods = [] 9 | for l:method in [ 10 | \ 'is_available', 11 | \ 'get_debug_name', 12 | \ 'get_position_encoding_kind', 13 | \ 'get_trigger_characters', 14 | \ 'get_keyword_pattern', 15 | \ 'complete', 16 | \ 'execute', 17 | \ 'resolve' 18 | \ ] 19 | if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func 20 | call add(l:methods, l:method) 21 | endif 22 | endfor 23 | 24 | let s:bridge_id += 1 25 | let a:source.bridge_id = s:bridge_id 26 | 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]) 27 | let s:sources[s:bridge_id] = a:source 28 | return a:source.id 29 | endfunction 30 | 31 | " 32 | " cmp#unregister_source 33 | " 34 | function! cmp#unregister_source(id) abort 35 | if has_key(s:sources, a:id) 36 | unlet s:sources[a:id] 37 | endif 38 | call luaeval('require("cmp").unregister_source(_A)', a:id) 39 | endfunction 40 | 41 | " 42 | " cmp#_method 43 | " 44 | function! cmp#_method(bridge_id, method, args) abort 45 | try 46 | let l:source = s:sources[a:bridge_id] 47 | if a:method ==# 'is_available' 48 | return l:source[a:method]() 49 | elseif a:method ==# 'get_debug_name' 50 | return l:source[a:method]() 51 | elseif a:method ==# 'get_position_encoding_kind' 52 | return l:source[a:method](a:args[0]) 53 | elseif a:method ==# 'get_keyword_pattern' 54 | return l:source[a:method](a:args[0]) 55 | elseif a:method ==# 'get_trigger_characters' 56 | return l:source[a:method](a:args[0]) 57 | elseif a:method ==# 'complete' 58 | return l:source[a:method](a:args[0], s:callback(a:args[1])) 59 | elseif a:method ==# 'resolve' 60 | return l:source[a:method](a:args[0], s:callback(a:args[1])) 61 | elseif a:method ==# 'execute' 62 | return l:source[a:method](a:args[0], s:callback(a:args[1])) 63 | endif 64 | catch /.*/ 65 | echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) 66 | endtry 67 | return v:null 68 | endfunction 69 | 70 | " 71 | " s:callback 72 | " 73 | function! s:callback(id) abort 74 | return { ... -> luaeval('require("cmp.vim_source").on_callback(_A[1], _A[2])', [a:id, a:000]) } 75 | endfunction 76 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 2 | 3 | rm $DIR/.git/hooks/* 4 | cp $DIR/.githooks/* $DIR/.git/hooks/ 5 | chmod 755 $DIR/.git/hooks/* 6 | 7 | 8 | -------------------------------------------------------------------------------- /lua/cmp/config.lua: -------------------------------------------------------------------------------- 1 | local mapping = require('cmp.config.mapping') 2 | local cache = require('cmp.utils.cache') 3 | local keymap = require('cmp.utils.keymap') 4 | local misc = require('cmp.utils.misc') 5 | local api = require('cmp.utils.api') 6 | 7 | ---@class cmp.Config 8 | ---@field public g cmp.ConfigSchema 9 | local config = {} 10 | 11 | ---@type cmp.Cache 12 | config.cache = cache.new() 13 | 14 | ---@type cmp.ConfigSchema 15 | config.global = require('cmp.config.default')() 16 | 17 | ---@type table 18 | config.buffers = {} 19 | 20 | ---@type table 21 | config.filetypes = {} 22 | 23 | ---@type table 24 | config.cmdline = {} 25 | 26 | ---@type cmp.ConfigSchema 27 | config.onetime = {} 28 | 29 | ---Set configuration for global. 30 | ---@param c cmp.ConfigSchema 31 | config.set_global = function(c) 32 | config.global = config.normalize(misc.merge(c, config.global)) 33 | config.global.revision = config.global.revision or 1 34 | config.global.revision = config.global.revision + 1 35 | end 36 | 37 | ---Set configuration for buffer 38 | ---@param c cmp.ConfigSchema 39 | ---@param bufnr integer 40 | config.set_buffer = function(c, bufnr) 41 | local revision = (config.buffers[bufnr] or {}).revision or 1 42 | config.buffers[bufnr] = c or {} 43 | config.buffers[bufnr].revision = revision + 1 44 | end 45 | 46 | ---Set configuration for filetype 47 | ---@param c cmp.ConfigSchema 48 | ---@param filetypes string[]|string 49 | config.set_filetype = function(c, filetypes) 50 | for _, filetype in ipairs(type(filetypes) == 'table' and filetypes or { filetypes }) do 51 | local revision = (config.filetypes[filetype] or {}).revision or 1 52 | config.filetypes[filetype] = c or {} 53 | config.filetypes[filetype].revision = revision + 1 54 | end 55 | end 56 | 57 | ---Set configuration for cmdline 58 | ---@param c cmp.ConfigSchema 59 | ---@param cmdtypes string|string[] 60 | config.set_cmdline = function(c, cmdtypes) 61 | for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do 62 | local revision = (config.cmdline[cmdtype] or {}).revision or 1 63 | config.cmdline[cmdtype] = c or {} 64 | config.cmdline[cmdtype].revision = revision + 1 65 | end 66 | end 67 | 68 | ---Set configuration as oneshot completion. 69 | ---@param c cmp.ConfigSchema 70 | config.set_onetime = function(c) 71 | local revision = (config.onetime or {}).revision or 1 72 | config.onetime = c or {} 73 | config.onetime.revision = revision + 1 74 | end 75 | 76 | ---@return cmp.ConfigSchema 77 | config.get = function() 78 | local global_config = config.global 79 | 80 | -- The config object already has `revision` key. 81 | if #vim.tbl_keys(config.onetime) > 1 then 82 | local onetime_config = config.onetime 83 | return config.cache:ensure({ 84 | 'get', 85 | 'onetime', 86 | global_config.revision or 0, 87 | onetime_config.revision or 0, 88 | }, function() 89 | local c = {} 90 | c = misc.merge(c, config.normalize(onetime_config)) 91 | c = misc.merge(c, config.normalize(global_config)) 92 | return c 93 | end) 94 | elseif api.is_cmdline_mode() then 95 | local cmdtype = vim.fn.getcmdtype() 96 | local cmdline_config = config.cmdline[cmdtype] or { revision = 1, sources = {} } 97 | return config.cache:ensure({ 98 | 'get', 99 | 'cmdline', 100 | global_config.revision or 0, 101 | cmdtype, 102 | cmdline_config.revision or 0, 103 | }, function() 104 | local c = {} 105 | c = misc.merge(c, config.normalize(cmdline_config)) 106 | c = misc.merge(c, config.normalize(global_config)) 107 | return c 108 | end) 109 | else 110 | local bufnr = vim.api.nvim_get_current_buf() 111 | local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) 112 | local buffer_config = config.buffers[bufnr] or { revision = 1 } 113 | local filetype_config = config.filetypes[filetype] or { revision = 1 } 114 | return config.cache:ensure({ 115 | 'get', 116 | 'default', 117 | global_config.revision or 0, 118 | filetype, 119 | filetype_config.revision or 0, 120 | bufnr, 121 | buffer_config.revision or 0, 122 | }, function() 123 | local c = {} 124 | c = misc.merge(config.normalize(c), config.normalize(buffer_config)) 125 | c = misc.merge(config.normalize(c), config.normalize(filetype_config)) 126 | c = misc.merge(config.normalize(c), config.normalize(global_config)) 127 | return c 128 | end) 129 | end 130 | end 131 | 132 | ---Return cmp is enabled or not. 133 | config.enabled = function() 134 | local enabled = config.get().enabled 135 | if type(enabled) == 'function' then 136 | enabled = enabled() 137 | end 138 | return enabled and api.is_suitable_mode() 139 | end 140 | 141 | ---Return source config 142 | ---@param name string 143 | ---@return cmp.SourceConfig 144 | config.get_source_config = function(name) 145 | local c = config.get() 146 | for _, s in ipairs(c.sources) do 147 | if s.name == name then 148 | return s 149 | end 150 | end 151 | return nil 152 | end 153 | 154 | ---Return the current menu is native or not. 155 | config.is_native_menu = function() 156 | local c = config.get() 157 | if c.view and c.view.entries then 158 | return c.view.entries == 'native' or c.view.entries.name == 'native' 159 | end 160 | return false 161 | end 162 | 163 | ---Normalize mapping key 164 | ---@param c any 165 | ---@return cmp.ConfigSchema 166 | config.normalize = function(c) 167 | -- make sure c is not 'nil' 168 | ---@type any 169 | c = c == nil and {} or c 170 | 171 | -- Normalize mapping. 172 | if c.mapping then 173 | local normalized = {} 174 | for k, v in pairs(c.mapping) do 175 | normalized[keymap.normalize(k)] = mapping(v, { 'i' }) 176 | end 177 | c.mapping = normalized 178 | end 179 | 180 | -- Notice experimental.native_menu. 181 | if c.experimental and c.experimental.native_menu then 182 | vim.api.nvim_echo({ 183 | { '[nvim-cmp] ', 'Normal' }, 184 | { 'experimental.native_menu', 'WarningMsg' }, 185 | { ' is deprecated.\n', 'Normal' }, 186 | { '[nvim-cmp] Please use ', 'Normal' }, 187 | { 'view.entries = "native"', 'WarningMsg' }, 188 | { ' instead.', 'Normal' }, 189 | }, true, {}) 190 | 191 | c.view = c.view or {} 192 | c.view.entries = c.view.entries or 'native' 193 | end 194 | 195 | -- Notice documentation. 196 | if c.documentation ~= nil then 197 | vim.api.nvim_echo({ 198 | { '[nvim-cmp] ', 'Normal' }, 199 | { 'documentation', 'WarningMsg' }, 200 | { ' is deprecated.\n', 'Normal' }, 201 | { '[nvim-cmp] Please use ', 'Normal' }, 202 | { 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' }, 203 | { ' instead.', 'Normal' }, 204 | }, true, {}) 205 | c.window = c.window or {} 206 | c.window.documentation = c.documentation 207 | end 208 | 209 | -- Notice sources.[n].opts 210 | if c.sources then 211 | for _, s in ipairs(c.sources) do 212 | if s.opts and not s.option then 213 | s.option = s.opts 214 | s.opts = nil 215 | vim.api.nvim_echo({ 216 | { '[nvim-cmp] ', 'Normal' }, 217 | { 'sources[number].opts', 'WarningMsg' }, 218 | { ' is deprecated.\n', 'Normal' }, 219 | { '[nvim-cmp] Please use ', 'Normal' }, 220 | { 'sources[number].option', 'WarningMsg' }, 221 | { ' instead.', 'Normal' }, 222 | }, true, {}) 223 | end 224 | s.option = s.option or {} 225 | end 226 | end 227 | 228 | return c 229 | end 230 | 231 | return config 232 | -------------------------------------------------------------------------------- /lua/cmp/config/compare.lua: -------------------------------------------------------------------------------- 1 | local types = require('cmp.types') 2 | local cache = require('cmp.utils.cache') 3 | 4 | ---@type cmp.Comparator[] 5 | local compare = {} 6 | 7 | --- Comparators (:help cmp-config.sorting.comparators) should return 8 | --- true when the first entry should come EARLIER (i.e., higher ranking) than the second entry, 9 | --- or nil if no pairwise ordering preference from the comparator. 10 | --- See also :help table.sort() and cmp.view.open() to see how comparators are used. 11 | 12 | ---@class cmp.ComparatorFunctor 13 | ---@overload fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil 14 | ---@alias cmp.ComparatorFunction fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil 15 | ---@alias cmp.Comparator cmp.ComparatorFunction | cmp.ComparatorFunctor 16 | 17 | ---offset: Entries with smaller offset will be ranked higher. 18 | ---@type cmp.ComparatorFunction 19 | compare.offset = function(entry1, entry2) 20 | local diff = entry1.offset - entry2.offset 21 | if diff < 0 then 22 | return true 23 | elseif diff > 0 then 24 | return false 25 | end 26 | return nil 27 | end 28 | 29 | ---exact: Entries with exact == true will be ranked higher. 30 | ---@type cmp.ComparatorFunction 31 | compare.exact = function(entry1, entry2) 32 | if entry1.exact ~= entry2.exact then 33 | return entry1.exact 34 | end 35 | return nil 36 | end 37 | 38 | ---score: Entries with higher score will be ranked higher. 39 | ---@type cmp.ComparatorFunction 40 | compare.score = function(entry1, entry2) 41 | local diff = entry2.score - entry1.score 42 | if diff < 0 then 43 | return true 44 | elseif diff > 0 then 45 | return false 46 | end 47 | return nil 48 | end 49 | 50 | ---recently_used: Entries that are used recently will be ranked higher. 51 | ---@type cmp.ComparatorFunctor 52 | compare.recently_used = setmetatable({ 53 | records = {}, 54 | add_entry = function(self, e) 55 | self.records[e.completion_item.label] = vim.loop.now() 56 | end, 57 | }, { 58 | ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil 59 | __call = function(self, entry1, entry2) 60 | local t1 = self.records[entry1.completion_item.label] or -1 61 | local t2 = self.records[entry2.completion_item.label] or -1 62 | if t1 ~= t2 then 63 | return t1 > t2 64 | end 65 | return nil 66 | end, 67 | }) 68 | 69 | ---kind: Entries with smaller ordinal value of 'kind' will be ranked higher. 70 | ---(see lsp.CompletionItemKind enum). 71 | ---Exceptions are that Text(1) will be ranked the lowest, and snippets be the highest. 72 | ---@type cmp.ComparatorFunction 73 | compare.kind = function(entry1, entry2) 74 | local kind1 = entry1:get_kind() --- @type lsp.CompletionItemKind | number 75 | local kind2 = entry2:get_kind() --- @type lsp.CompletionItemKind | number 76 | kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1 77 | kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2 78 | if kind1 ~= kind2 then 79 | if kind1 == types.lsp.CompletionItemKind.Snippet then 80 | return true 81 | end 82 | if kind2 == types.lsp.CompletionItemKind.Snippet then 83 | return false 84 | end 85 | local diff = kind1 - kind2 86 | if diff < 0 then 87 | return true 88 | elseif diff > 0 then 89 | return false 90 | end 91 | end 92 | return nil 93 | end 94 | 95 | ---sort_text: Entries will be ranked according to the lexicographical order of sortText. 96 | ---@type cmp.ComparatorFunction 97 | compare.sort_text = function(entry1, entry2) 98 | if entry1.completion_item.sortText and entry2.completion_item.sortText then 99 | local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText) 100 | if diff < 0 then 101 | return true 102 | elseif diff > 0 then 103 | return false 104 | end 105 | end 106 | return nil 107 | end 108 | 109 | ---length: Entries with shorter label length will be ranked higher. 110 | ---@type cmp.ComparatorFunction 111 | compare.length = function(entry1, entry2) 112 | local diff = #entry1.completion_item.label - #entry2.completion_item.label 113 | if diff < 0 then 114 | return true 115 | elseif diff > 0 then 116 | return false 117 | end 118 | return nil 119 | end 120 | 121 | ----order: Entries with smaller id will be ranked higher. 122 | ---@type fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil 123 | compare.order = function(entry1, entry2) 124 | local diff = entry1.id - entry2.id 125 | if diff < 0 then 126 | return true 127 | elseif diff > 0 then 128 | return false 129 | end 130 | return nil 131 | end 132 | 133 | ---locality: Entries with higher locality (i.e., words that are closer to the cursor) 134 | ---will be ranked higher. See GH-183 for more details. 135 | ---@type cmp.ComparatorFunctor 136 | compare.locality = setmetatable({ 137 | lines_count = 10, 138 | lines_cache = cache.new(), 139 | locality_map = {}, 140 | update = function(self) 141 | local config = require('cmp').get_config() 142 | if not vim.tbl_contains(config.sorting.comparators, compare.locality) then 143 | return 144 | end 145 | 146 | local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() 147 | local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 148 | local max = vim.api.nvim_buf_line_count(buf) 149 | 150 | if self.lines_cache:get('buf') ~= buf then 151 | self.lines_cache:clear() 152 | self.lines_cache:set('buf', buf) 153 | end 154 | 155 | self.locality_map = {} 156 | for i = math.max(0, cursor_row - self.lines_count), math.min(max, cursor_row + self.lines_count) do 157 | local is_above = i < cursor_row 158 | local buffer = vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or '' 159 | local locality_map = self.lines_cache:ensure({ 'line', buffer }, function() 160 | local locality_map = {} 161 | local regexp = vim.regex(config.completion.keyword_pattern) 162 | while buffer ~= '' do 163 | local s, e = regexp:match_str(buffer) 164 | if s and e then 165 | local w = string.sub(buffer, s + 1, e) 166 | local d = math.abs(i - cursor_row) - (is_above and 1 or 0) 167 | locality_map[w] = math.min(locality_map[w] or math.huge, d) 168 | buffer = string.sub(buffer, e + 1) 169 | else 170 | break 171 | end 172 | end 173 | return locality_map 174 | end) 175 | for w, d in pairs(locality_map) do 176 | self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row)) 177 | end 178 | end 179 | end, 180 | }, { 181 | ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil 182 | __call = function(self, entry1, entry2) 183 | local local1 = self.locality_map[entry1.word] 184 | local local2 = self.locality_map[entry2.word] 185 | if local1 ~= local2 then 186 | if local1 == nil then 187 | return false 188 | end 189 | if local2 == nil then 190 | return true 191 | end 192 | return local1 < local2 193 | end 194 | return nil 195 | end, 196 | }) 197 | 198 | ---scopes: Entries defined in a closer scope will be ranked higher (e.g., prefer local variables to globals). 199 | ---@type cmp.ComparatorFunctor 200 | compare.scopes = setmetatable({ 201 | scopes_map = {}, 202 | update = function(self) 203 | local config = require('cmp').get_config() 204 | if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then 205 | return 206 | end 207 | 208 | local ok, locals = pcall(require, 'nvim-treesitter.locals') 209 | if ok then 210 | local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() 211 | local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 212 | 213 | -- Cursor scope. 214 | local cursor_scope = nil 215 | -- Prioritize the older get_scopes method from nvim-treesitter `master` over get from `main` 216 | local scopes = locals.get_scopes and locals.get_scopes(buf) or select(3, locals.get(buf)) 217 | for _, scope in ipairs(scopes) do 218 | if scope:start() <= cursor_row and cursor_row <= scope:end_() then 219 | if not cursor_scope then 220 | cursor_scope = scope 221 | else 222 | if cursor_scope:start() <= scope:start() and scope:end_() <= cursor_scope:end_() then 223 | cursor_scope = scope 224 | end 225 | end 226 | elseif cursor_scope and cursor_scope:end_() <= scope:start() then 227 | break 228 | end 229 | end 230 | 231 | -- Definitions. 232 | local definitions = locals.get_definitions_lookup_table(buf) 233 | 234 | -- Narrow definitions. 235 | local depth = 0 236 | for scope in locals.iter_scope_tree(cursor_scope, buf) do 237 | local s, e = scope:start(), scope:end_() 238 | 239 | -- Check scope's direct child. 240 | for _, definition in pairs(definitions) do 241 | if s <= definition.node:start() and definition.node:end_() <= e then 242 | if scope:id() == locals.containing_scope(definition.node, buf):id() then 243 | local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text 244 | local text = get_node_text(definition.node, buf) or '' 245 | if not self.scopes_map[text] then 246 | self.scopes_map[text] = depth 247 | end 248 | end 249 | end 250 | end 251 | depth = depth + 1 252 | end 253 | end 254 | end, 255 | }, { 256 | ---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil 257 | __call = function(self, entry1, entry2) 258 | local local1 = self.scopes_map[entry1.word] 259 | local local2 = self.scopes_map[entry2.word] 260 | if local1 ~= local2 then 261 | if local1 == nil then 262 | return false 263 | end 264 | if local2 == nil then 265 | return true 266 | end 267 | return local1 < local2 268 | end 269 | end, 270 | }) 271 | 272 | return compare 273 | -------------------------------------------------------------------------------- /lua/cmp/config/context.lua: -------------------------------------------------------------------------------- 1 | local api = require('cmp.utils.api') 2 | 3 | local context = {} 4 | 5 | ---Check if cursor is in syntax group 6 | ---@param group string | []string 7 | ---@return boolean 8 | context.in_syntax_group = function(group) 9 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 10 | if not api.is_insert_mode() then 11 | col = col + 1 12 | end 13 | 14 | for _, syn_id in ipairs(vim.fn.synstack(row, col)) do 15 | syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links 16 | local g = vim.fn.synIDattr(syn_id, 'name') 17 | if type(group) == 'string' and g == group then 18 | return true 19 | elseif type(group) == 'table' and vim.tbl_contains(group, g) then 20 | return true 21 | end 22 | end 23 | 24 | return false 25 | end 26 | 27 | ---Check if cursor is in treesitter capture 28 | ---@param capture string | []string 29 | ---@return boolean 30 | context.in_treesitter_capture = function(capture) 31 | local buf = vim.api.nvim_get_current_buf() 32 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 33 | row = row - 1 34 | if vim.api.nvim_get_mode().mode == 'i' then 35 | col = col - 1 36 | end 37 | 38 | local get_captures_at_pos = -- See neovim/neovim#20331 39 | require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8 40 | 41 | local captures_at_cursor = vim.tbl_map(function(x) 42 | return x.capture 43 | end, get_captures_at_pos(buf, row, col)) 44 | 45 | if vim.tbl_isempty(captures_at_cursor) then 46 | return false 47 | elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then 48 | return true 49 | elseif type(capture) == 'table' then 50 | for _, v in ipairs(capture) do 51 | if vim.tbl_contains(captures_at_cursor, v) then 52 | return true 53 | end 54 | end 55 | end 56 | 57 | return false 58 | end 59 | 60 | return context 61 | -------------------------------------------------------------------------------- /lua/cmp/config/default.lua: -------------------------------------------------------------------------------- 1 | local compare = require('cmp.config.compare') 2 | local types = require('cmp.types') 3 | 4 | local WIDE_HEIGHT = 40 5 | 6 | ---@return cmp.ConfigSchema 7 | return function() 8 | ---@type cmp.ConfigSchema 9 | local config = { 10 | enabled = function() 11 | local disabled = false 12 | disabled = disabled or (vim.api.nvim_get_option_value('buftype', { buf = 0 }) == 'prompt') 13 | disabled = disabled or (vim.fn.reg_recording() ~= '') 14 | disabled = disabled or (vim.fn.reg_executing() ~= '') 15 | return not disabled 16 | end, 17 | 18 | performance = { 19 | debounce = 60, 20 | throttle = 30, 21 | fetching_timeout = 500, 22 | filtering_context_budget = 3, 23 | confirm_resolve_timeout = 80, 24 | async_budget = 1, 25 | max_view_entries = 200, 26 | }, 27 | 28 | preselect = types.cmp.PreselectMode.Item, 29 | 30 | mapping = {}, 31 | 32 | snippet = { 33 | expand = vim.fn.has('nvim-0.10') == 1 and function(args) 34 | vim.snippet.expand(args.body) 35 | end or function(_) 36 | error('snippet engine is not configured.') 37 | end, 38 | }, 39 | 40 | completion = { 41 | autocomplete = { 42 | types.cmp.TriggerEvent.TextChanged, 43 | }, 44 | completeopt = 'menu,menuone,noselect', 45 | keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], 46 | keyword_length = 1, 47 | }, 48 | 49 | formatting = { 50 | expandable_indicator = true, 51 | fields = { 'abbr', 'kind', 'menu' }, 52 | format = function(_, vim_item) 53 | return vim_item 54 | end, 55 | }, 56 | 57 | matching = { 58 | disallow_fuzzy_matching = false, 59 | disallow_fullfuzzy_matching = false, 60 | disallow_partial_fuzzy_matching = true, 61 | disallow_partial_matching = false, 62 | disallow_prefix_unmatching = false, 63 | disallow_symbol_nonprefix_matching = true, 64 | }, 65 | 66 | sorting = { 67 | priority_weight = 2, 68 | comparators = { 69 | compare.offset, 70 | compare.exact, 71 | -- compare.scopes, 72 | compare.score, 73 | compare.recently_used, 74 | compare.locality, 75 | compare.kind, 76 | compare.sort_text, 77 | compare.length, 78 | compare.order, 79 | }, 80 | }, 81 | 82 | sources = {}, 83 | 84 | confirmation = { 85 | default_behavior = types.cmp.ConfirmBehavior.Insert, 86 | get_commit_characters = function(commit_characters) 87 | return commit_characters 88 | end, 89 | }, 90 | 91 | event = {}, 92 | 93 | experimental = { 94 | ghost_text = false, 95 | }, 96 | 97 | view = { 98 | entries = { 99 | name = 'custom', 100 | selection_order = 'top_down', 101 | follow_cursor = false, 102 | }, 103 | docs = { 104 | auto_open = true, 105 | }, 106 | }, 107 | 108 | window = { 109 | completion = { 110 | border = { '', '', '', '', '', '', '', '' }, 111 | winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None', 112 | winblend = vim.o.pumblend, 113 | scrolloff = 0, 114 | col_offset = 0, 115 | side_padding = 1, 116 | scrollbar = true, 117 | }, 118 | documentation = { 119 | max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), 120 | max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), 121 | border = { '', '', '', ' ', '', '', '', ' ' }, 122 | winhighlight = 'FloatBorder:NormalFloat', 123 | winblend = vim.o.pumblend, 124 | }, 125 | }, 126 | } 127 | return config 128 | end 129 | -------------------------------------------------------------------------------- /lua/cmp/config/mapping.lua: -------------------------------------------------------------------------------- 1 | local types = require('cmp.types') 2 | local misc = require('cmp.utils.misc') 3 | local keymap = require('cmp.utils.keymap') 4 | 5 | local function merge_keymaps(base, override) 6 | local normalized_base = {} 7 | for k, v in pairs(base) do 8 | normalized_base[keymap.normalize(k)] = v 9 | end 10 | 11 | local normalized_override = {} 12 | for k, v in pairs(override) do 13 | normalized_override[keymap.normalize(k)] = v 14 | end 15 | 16 | return misc.merge(normalized_base, normalized_override) 17 | end 18 | 19 | local mapping = setmetatable({}, { 20 | __call = function(_, invoke, modes) 21 | if type(invoke) == 'function' then 22 | local map = {} 23 | for _, mode in ipairs(modes or { 'i' }) do 24 | map[mode] = invoke 25 | end 26 | return map 27 | end 28 | return invoke 29 | end, 30 | }) 31 | 32 | ---Mapping preset configuration. 33 | mapping.preset = {} 34 | 35 | ---Mapping preset insert-mode configuration. 36 | mapping.preset.insert = function(override) 37 | return merge_keymaps(override or {}, { 38 | [''] = { 39 | i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), 40 | }, 41 | [''] = { 42 | i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }), 43 | }, 44 | [''] = { 45 | i = function() 46 | local cmp = require('cmp') 47 | if cmp.visible() then 48 | cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }) 49 | else 50 | cmp.complete() 51 | end 52 | end, 53 | }, 54 | [''] = { 55 | i = function() 56 | local cmp = require('cmp') 57 | if cmp.visible() then 58 | cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }) 59 | else 60 | cmp.complete() 61 | end 62 | end, 63 | }, 64 | [''] = { 65 | i = mapping.confirm({ select = false }), 66 | }, 67 | [''] = { 68 | i = mapping.abort(), 69 | }, 70 | }) 71 | end 72 | 73 | ---Mapping preset cmdline-mode configuration. 74 | mapping.preset.cmdline = function(override) 75 | return merge_keymaps(override or {}, { 76 | [''] = { 77 | c = function() 78 | local cmp = require('cmp') 79 | if cmp.visible() then 80 | cmp.select_next_item() 81 | else 82 | cmp.complete() 83 | end 84 | end, 85 | }, 86 | [''] = { 87 | c = function() 88 | local cmp = require('cmp') 89 | if cmp.visible() then 90 | cmp.select_next_item() 91 | else 92 | cmp.complete() 93 | end 94 | end, 95 | }, 96 | [''] = { 97 | c = function() 98 | local cmp = require('cmp') 99 | if cmp.visible() then 100 | cmp.select_prev_item() 101 | else 102 | cmp.complete() 103 | end 104 | end, 105 | }, 106 | [''] = { 107 | c = function(fallback) 108 | local cmp = require('cmp') 109 | if cmp.visible() then 110 | cmp.select_next_item() 111 | else 112 | fallback() 113 | end 114 | end, 115 | }, 116 | [''] = { 117 | c = function(fallback) 118 | local cmp = require('cmp') 119 | if cmp.visible() then 120 | cmp.select_prev_item() 121 | else 122 | fallback() 123 | end 124 | end, 125 | }, 126 | [''] = { 127 | c = mapping.abort(), 128 | }, 129 | [''] = { 130 | c = mapping.confirm({ select = false }), 131 | }, 132 | }) 133 | end 134 | 135 | ---Invoke completion 136 | ---@param option? cmp.CompleteParams 137 | mapping.complete = function(option) 138 | return function(fallback) 139 | if not require('cmp').complete(option) then 140 | fallback() 141 | end 142 | end 143 | end 144 | 145 | ---Complete common string. 146 | mapping.complete_common_string = function() 147 | return function(fallback) 148 | if not require('cmp').complete_common_string() then 149 | fallback() 150 | end 151 | end 152 | end 153 | 154 | ---Close current completion menu if it displayed. 155 | mapping.close = function() 156 | return function(fallback) 157 | if not require('cmp').close() then 158 | fallback() 159 | end 160 | end 161 | end 162 | 163 | ---Abort current completion menu if it displayed. 164 | mapping.abort = function() 165 | return function(fallback) 166 | if not require('cmp').abort() then 167 | fallback() 168 | end 169 | end 170 | end 171 | 172 | ---Scroll documentation window. 173 | mapping.scroll_docs = function(delta) 174 | return function(fallback) 175 | if not require('cmp').scroll_docs(delta) then 176 | fallback() 177 | end 178 | end 179 | end 180 | 181 | --- Opens the documentation window. 182 | mapping.open_docs = function() 183 | return function(fallback) 184 | if not require('cmp').open_docs() then 185 | fallback() 186 | end 187 | end 188 | end 189 | 190 | --- Close the documentation window. 191 | mapping.close_docs = function() 192 | return function(fallback) 193 | if not require('cmp').close_docs() then 194 | fallback() 195 | end 196 | end 197 | end 198 | 199 | ---Select next completion item. 200 | mapping.select_next_item = function(option) 201 | return function(fallback) 202 | if not require('cmp').select_next_item(option) then 203 | local release = require('cmp').core:suspend() 204 | fallback() 205 | vim.schedule(release) 206 | end 207 | end 208 | end 209 | 210 | ---Select prev completion item. 211 | mapping.select_prev_item = function(option) 212 | return function(fallback) 213 | if not require('cmp').select_prev_item(option) then 214 | local release = require('cmp').core:suspend() 215 | fallback() 216 | vim.schedule(release) 217 | end 218 | end 219 | end 220 | 221 | ---Confirm selection 222 | mapping.confirm = function(option) 223 | return function(fallback) 224 | if not require('cmp').confirm(option) then 225 | fallback() 226 | end 227 | end 228 | end 229 | 230 | return mapping 231 | -------------------------------------------------------------------------------- /lua/cmp/config/sources.lua: -------------------------------------------------------------------------------- 1 | return function(...) 2 | local sources = {} 3 | for i, group in ipairs({ ... }) do 4 | for _, source in ipairs(group) do 5 | source.group_index = i 6 | table.insert(sources, source) 7 | end 8 | end 9 | return sources 10 | end 11 | -------------------------------------------------------------------------------- /lua/cmp/config/window.lua: -------------------------------------------------------------------------------- 1 | local window = {} 2 | 3 | window.bordered = function(opts) 4 | opts = opts or {} 5 | return { 6 | border = opts.border or 'rounded', 7 | winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:FloatBorder,CursorLine:Visual,Search:None', 8 | zindex = opts.zindex or 1001, 9 | scrolloff = opts.scrolloff or 0, 10 | col_offset = opts.col_offset or 0, 11 | side_padding = opts.side_padding or 1, 12 | scrollbar = opts.scrollbar == nil and true or opts.scrollbar, 13 | } 14 | end 15 | 16 | return window 17 | -------------------------------------------------------------------------------- /lua/cmp/context.lua: -------------------------------------------------------------------------------- 1 | local misc = require('cmp.utils.misc') 2 | local pattern = require('cmp.utils.pattern') 3 | local types = require('cmp.types') 4 | local cache = require('cmp.utils.cache') 5 | local api = require('cmp.utils.api') 6 | 7 | ---@class cmp.Context 8 | ---@field public id string 9 | ---@field public cache cmp.Cache 10 | ---@field public prev_context cmp.Context 11 | ---@field public option cmp.ContextOption 12 | ---@field public filetype string 13 | ---@field public time integer 14 | ---@field public bufnr integer 15 | ---@field public cursor vim.Position|lsp.Position 16 | ---@field public cursor_line string 17 | ---@field public cursor_after_line string 18 | ---@field public cursor_before_line string 19 | ---@field public aborted boolean 20 | local context = {} 21 | 22 | ---Create new empty context 23 | ---@return cmp.Context 24 | context.empty = function() 25 | local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`. 26 | ctx.bufnr = -1 27 | ctx.input = '' 28 | ctx.cursor = {} 29 | ctx.cursor.row = -1 30 | ctx.cursor.col = -1 31 | return ctx 32 | end 33 | 34 | ---Create new context 35 | ---@param prev_context? cmp.Context 36 | ---@param option? cmp.ContextOption 37 | ---@return cmp.Context 38 | context.new = function(prev_context, option) 39 | option = option or {} 40 | 41 | local self = setmetatable({}, { __index = context }) 42 | self.id = misc.id('cmp.context.new') 43 | self.cache = cache.new() 44 | self.prev_context = prev_context or context.empty() 45 | self.option = option or { reason = types.cmp.ContextReason.None } 46 | self.filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 }) 47 | self.time = vim.loop.now() 48 | self.bufnr = vim.api.nvim_get_current_buf() 49 | 50 | local cursor = api.get_cursor() 51 | self.cursor_line = api.get_current_line() 52 | self.cursor = {} 53 | self.cursor.row = cursor[1] 54 | self.cursor.col = cursor[2] + 1 55 | self.cursor.line = self.cursor.row - 1 56 | self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col) 57 | self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1) 58 | self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col) 59 | self.aborted = false 60 | return self 61 | end 62 | 63 | context.abort = function(self) 64 | self.aborted = true 65 | end 66 | 67 | ---Return context creation reason. 68 | ---@return cmp.ContextReason 69 | context.get_reason = function(self) 70 | return self.option.reason 71 | end 72 | 73 | ---Get keyword pattern offset 74 | ---@return integer 75 | context.get_offset = function(self, keyword_pattern) 76 | return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function() 77 | return pattern.offset([[\%(]] .. keyword_pattern .. [[\)\m$]], self.cursor_before_line) or self.cursor.col 78 | end) 79 | end 80 | 81 | ---Return if this context is changed from previous context or not. 82 | ---@return boolean 83 | context.changed = function(self, ctx) 84 | local curr = self 85 | 86 | if curr.bufnr ~= ctx.bufnr then 87 | return true 88 | end 89 | if curr.cursor.row ~= ctx.cursor.row then 90 | return true 91 | end 92 | if curr.cursor.col ~= ctx.cursor.col then 93 | return true 94 | end 95 | if curr:get_reason() == types.cmp.ContextReason.Manual then 96 | return true 97 | end 98 | 99 | return false 100 | end 101 | 102 | ---Shallow clone 103 | context.clone = function(self) 104 | local cloned = {} 105 | for k, v in pairs(self) do 106 | cloned[k] = v 107 | end 108 | return cloned 109 | end 110 | 111 | return context 112 | -------------------------------------------------------------------------------- /lua/cmp/context_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | 3 | local context = require('cmp.context') 4 | 5 | describe('context', function() 6 | before_each(spec.before) 7 | 8 | describe('new', function() 9 | it('middle of text', function() 10 | vim.fn.setline('1', 'function! s:name() abort') 11 | vim.bo.filetype = 'vim' 12 | vim.fn.execute('normal! fm') 13 | local ctx = context.new() 14 | assert.are.equal(ctx.filetype, 'vim') 15 | assert.are.equal(ctx.cursor.row, 1) 16 | assert.are.equal(ctx.cursor.col, 15) 17 | assert.are.equal(ctx.cursor_line, 'function! s:name() abort') 18 | end) 19 | 20 | it('tab indent', function() 21 | vim.fn.setline('1', '\t\tab') 22 | vim.bo.filetype = 'vim' 23 | vim.fn.execute('normal! fb') 24 | local ctx = context.new() 25 | assert.are.equal(ctx.filetype, 'vim') 26 | assert.are.equal(ctx.cursor.row, 1) 27 | assert.are.equal(ctx.cursor.col, 4) 28 | assert.are.equal(ctx.cursor_line, '\t\tab') 29 | end) 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /lua/cmp/core_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local feedkeys = require('cmp.utils.feedkeys') 3 | local types = require('cmp.types') 4 | local core = require('cmp.core') 5 | local source = require('cmp.source') 6 | local keymap = require('cmp.utils.keymap') 7 | local api = require('cmp.utils.api') 8 | 9 | describe('cmp.core', function() 10 | describe('confirm', function() 11 | ---@param request string 12 | ---@param filter string 13 | ---@param completion_item lsp.CompletionItem 14 | ---@param option? { position_encoding_kind: lsp.PositionEncodingKind } 15 | ---@return table 16 | local confirm = function(request, filter, completion_item, option) 17 | option = option or {} 18 | 19 | local c = core.new() 20 | local s = source.new('spec', { 21 | get_position_encoding_kind = function() 22 | return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16 23 | end, 24 | complete = function(_, _, callback) 25 | callback({ completion_item }) 26 | end, 27 | }) 28 | c:register_source(s) 29 | feedkeys.call(request, 'n', function() 30 | c:complete(c:get_context({ reason = types.cmp.ContextReason.Manual })) 31 | vim.wait(5000, function() 32 | return #c.sources[s.id].entries > 0 33 | end) 34 | end) 35 | feedkeys.call(filter, 'n', function() 36 | c:confirm(c.sources[s.id].entries[1], {}, function() end) 37 | end) 38 | local state = {} 39 | feedkeys.call('', 'x', function() 40 | feedkeys.call('', 'n', function() 41 | if api.is_cmdline_mode() then 42 | state.buffer = { api.get_current_line() } 43 | else 44 | state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) 45 | end 46 | state.cursor = api.get_cursor() 47 | end) 48 | end) 49 | return state 50 | end 51 | 52 | describe('insert-mode', function() 53 | before_each(spec.before) 54 | 55 | it('label', function() 56 | local state = confirm('iA', 'IU', { 57 | label = 'AIUEO', 58 | }) 59 | assert.are.same(state.buffer, { 'AIUEO' }) 60 | assert.are.same(state.cursor, { 1, 5 }) 61 | end) 62 | 63 | it('insertText', function() 64 | local state = confirm('iA', 'IU', { 65 | label = 'AIUEO', 66 | insertText = '_AIUEO_', 67 | }) 68 | assert.are.same(state.buffer, { '_AIUEO_' }) 69 | assert.are.same(state.cursor, { 1, 7 }) 70 | end) 71 | 72 | it('textEdit', function() 73 | local state = confirm(keymap.t('i***AEO***'), 'IU', { 74 | label = 'AIUEO', 75 | textEdit = { 76 | range = { 77 | start = { 78 | line = 0, 79 | character = 3, 80 | }, 81 | ['end'] = { 82 | line = 0, 83 | character = 6, 84 | }, 85 | }, 86 | newText = 'foo\nbar\nbaz', 87 | }, 88 | }) 89 | assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) 90 | assert.are.same(state.cursor, { 3, 3 }) 91 | end) 92 | 93 | it('#1552', function() 94 | local state = confirm(keymap.t('ios.'), '', { 95 | filterText = 'IsPermission', 96 | insertTextFormat = 2, 97 | label = 'IsPermission', 98 | textEdit = { 99 | newText = 'IsPermission($0)', 100 | range = { 101 | ['end'] = { 102 | character = 3, 103 | line = 0, 104 | }, 105 | start = { 106 | character = 3, 107 | line = 0, 108 | }, 109 | }, 110 | }, 111 | }) 112 | assert.are.same(state.buffer, { 'os.IsPermission()' }) 113 | assert.are.same(state.cursor, { 1, 16 }) 114 | end) 115 | 116 | it('insertText & snippet', function() 117 | local state = confirm('iA', 'IU', { 118 | label = 'AIUEO', 119 | insertText = 'AIUEO($0)', 120 | insertTextFormat = types.lsp.InsertTextFormat.Snippet, 121 | }) 122 | assert.are.same(state.buffer, { 'AIUEO()' }) 123 | assert.are.same(state.cursor, { 1, 6 }) 124 | end) 125 | 126 | it('textEdit & snippet', function() 127 | local state = confirm(keymap.t('i***AEO***'), 'IU', { 128 | label = 'AIUEO', 129 | insertTextFormat = types.lsp.InsertTextFormat.Snippet, 130 | textEdit = { 131 | range = { 132 | start = { 133 | line = 0, 134 | character = 3, 135 | }, 136 | ['end'] = { 137 | line = 0, 138 | character = 6, 139 | }, 140 | }, 141 | newText = 'foo\nba$0r\nbaz', 142 | }, 143 | }) 144 | assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) 145 | assert.are.same(state.cursor, { 2, 2 }) 146 | end) 147 | 148 | local char = '🗿' 149 | for _, case in ipairs({ 150 | { 151 | encoding = types.lsp.PositionEncodingKind.UTF8, 152 | char_size = #char, 153 | }, 154 | { 155 | encoding = types.lsp.PositionEncodingKind.UTF16, 156 | char_size = select(2, vim.str_utfindex(char)), 157 | }, 158 | { 159 | encoding = types.lsp.PositionEncodingKind.UTF32, 160 | char_size = select(1, vim.str_utfindex(char)), 161 | }, 162 | }) do 163 | it('textEdit & multibyte: ' .. case.encoding, function() 164 | local state = confirm(keymap.t('i%s:%s%s:%s'):format(char, char, char, char), char, { 165 | label = char .. char .. char, 166 | textEdit = { 167 | range = { 168 | start = { 169 | line = 0, 170 | character = case.char_size + #':', 171 | }, 172 | ['end'] = { 173 | line = 0, 174 | character = case.char_size + #':' + case.char_size + case.char_size, 175 | }, 176 | }, 177 | newText = char .. char .. char .. char .. char, 178 | }, 179 | }, { 180 | position_encoding_kind = case.encoding, 181 | }) 182 | vim.print({ state = state, case = case }) 183 | assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) }) 184 | assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) }) 185 | end) 186 | end 187 | end) 188 | 189 | describe('cmdline-mode', function() 190 | before_each(spec.before) 191 | 192 | it('label', function() 193 | local state = confirm(':A', 'IU', { 194 | label = 'AIUEO', 195 | }) 196 | assert.are.same(state.buffer, { 'AIUEO' }) 197 | assert.are.same(state.cursor[2], 5) 198 | end) 199 | 200 | it('insertText', function() 201 | local state = confirm(':A', 'IU', { 202 | label = 'AIUEO', 203 | insertText = '_AIUEO_', 204 | }) 205 | assert.are.same(state.buffer, { '_AIUEO_' }) 206 | assert.are.same(state.cursor[2], 7) 207 | end) 208 | 209 | it('textEdit', function() 210 | local state = confirm(keymap.t(':***AEO***'), 'IU', { 211 | label = 'AIUEO', 212 | textEdit = { 213 | range = { 214 | start = { 215 | line = 0, 216 | character = 3, 217 | }, 218 | ['end'] = { 219 | line = 0, 220 | character = 6, 221 | }, 222 | }, 223 | newText = 'AIUEO', 224 | }, 225 | }) 226 | assert.are.same(state.buffer, { '***AIUEO***' }) 227 | assert.are.same(state.cursor[2], 6) 228 | end) 229 | end) 230 | end) 231 | end) 232 | -------------------------------------------------------------------------------- /lua/cmp/entry_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | 3 | local entry = require('cmp.entry') 4 | 5 | describe('entry', function() 6 | before_each(spec.before) 7 | 8 | it('one char', function() 9 | local state = spec.state('@.', 1, 3) 10 | state.input('@') 11 | local e = entry.new(state.manual(), state.source(), { 12 | label = '@', 13 | }) 14 | assert.are.equal(e.offset, 3) 15 | assert.are.equal(e:get_vim_item(e.offset).word, '@') 16 | end) 17 | 18 | it('word length (no fix)', function() 19 | local state = spec.state('a.b', 1, 4) 20 | state.input('.') 21 | local e = entry.new(state.manual(), state.source(), { 22 | label = 'b', 23 | }) 24 | assert.are.equal(e.offset, 5) 25 | assert.are.equal(e:get_vim_item(e.offset).word, 'b') 26 | end) 27 | 28 | it('word length (fix)', function() 29 | local state = spec.state('a.b', 1, 4) 30 | state.input('.') 31 | local e = entry.new(state.manual(), state.source(), { 32 | label = 'b.', 33 | }) 34 | assert.are.equal(e.offset, 3) 35 | assert.are.equal(e:get_vim_item(e.offset).word, 'b.') 36 | end) 37 | 38 | it('semantic index (no fix)', function() 39 | local state = spec.state('a.bc', 1, 5) 40 | state.input('.') 41 | local e = entry.new(state.manual(), state.source(), { 42 | label = 'c.', 43 | }) 44 | assert.are.equal(e.offset, 6) 45 | assert.are.equal(e:get_vim_item(e.offset).word, 'c.') 46 | end) 47 | 48 | it('semantic index (fix)', function() 49 | local state = spec.state('a.bc', 1, 5) 50 | state.input('.') 51 | local e = entry.new(state.manual(), state.source(), { 52 | label = 'bc.', 53 | }) 54 | assert.are.equal(e.offset, 3) 55 | assert.are.equal(e:get_vim_item(e.offset).word, 'bc.') 56 | end) 57 | 58 | it('[vscode-html-language-server] 1', function() 59 | local state = spec.state(' ', 1, 7) 60 | state.input('.') 61 | local e = entry.new(state.manual(), state.source(), { 62 | label = '/div', 63 | textEdit = { 64 | range = { 65 | start = { 66 | line = 0, 67 | character = 0, 68 | }, 69 | ['end'] = { 70 | line = 0, 71 | character = 6, 72 | }, 73 | }, 74 | newText = ' foo') 104 | assert.are.equal(e.filter_text, 'foo') 105 | end) 106 | 107 | it('[typescript-language-server] 1', function() 108 | local state = spec.state('Promise.resolve()', 1, 18) 109 | state.input('.') 110 | local e = entry.new(state.manual(), state.source(), { 111 | label = 'catch', 112 | }) 113 | -- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate. 114 | assert.are.equal(e:get_vim_item(18).word, '.catch') 115 | assert.are.equal(e.filter_text, 'catch') 116 | end) 117 | 118 | it('[typescript-language-server] 2', function() 119 | local state = spec.state('Promise.resolve()', 1, 18) 120 | state.input('.') 121 | local e = entry.new(state.manual(), state.source(), { 122 | filterText = '.Symbol', 123 | label = 'Symbol', 124 | textEdit = { 125 | newText = '[Symbol]', 126 | range = { 127 | ['end'] = { 128 | character = 18, 129 | line = 0, 130 | }, 131 | start = { 132 | character = 17, 133 | line = 0, 134 | }, 135 | }, 136 | }, 137 | }) 138 | assert.are.equal(e:get_vim_item(18).word, '[Symbol]') 139 | assert.are.equal(e.filter_text, '.Symbol') 140 | end) 141 | 142 | it('[lua-language-server] 1', function() 143 | local state = spec.state("local m = require'cmp.confi", 1, 28) 144 | local e 145 | 146 | -- press g 147 | state.input('g') 148 | e = entry.new(state.manual(), state.source(), { 149 | insertTextFormat = 2, 150 | label = 'cmp.config', 151 | textEdit = { 152 | newText = 'cmp.config', 153 | range = { 154 | ['end'] = { 155 | character = 27, 156 | line = 1, 157 | }, 158 | start = { 159 | character = 18, 160 | line = 1, 161 | }, 162 | }, 163 | }, 164 | }) 165 | assert.are.equal(e:get_vim_item(19).word, 'cmp.config') 166 | assert.are.equal(e.filter_text, 'cmp.config') 167 | 168 | -- press ' 169 | state.input("'") 170 | e = entry.new(state.manual(), state.source(), { 171 | insertTextFormat = 2, 172 | label = 'cmp.config', 173 | textEdit = { 174 | newText = 'cmp.config', 175 | range = { 176 | ['end'] = { 177 | character = 27, 178 | line = 1, 179 | }, 180 | start = { 181 | character = 18, 182 | line = 1, 183 | }, 184 | }, 185 | }, 186 | }) 187 | assert.are.equal(e:get_vim_item(19).word, 'cmp.config') 188 | assert.are.equal(e.filter_text, 'cmp.config') 189 | end) 190 | 191 | it('[lua-language-server] 2', function() 192 | local state = spec.state("local m = require'cmp.confi", 1, 28) 193 | local e 194 | 195 | -- press g 196 | state.input('g') 197 | e = entry.new(state.manual(), state.source(), { 198 | insertTextFormat = 2, 199 | label = 'lua.cmp.config', 200 | textEdit = { 201 | newText = 'lua.cmp.config', 202 | range = { 203 | ['end'] = { 204 | character = 27, 205 | line = 1, 206 | }, 207 | start = { 208 | character = 18, 209 | line = 1, 210 | }, 211 | }, 212 | }, 213 | }) 214 | assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') 215 | assert.are.equal(e.filter_text, 'lua.cmp.config') 216 | 217 | -- press ' 218 | state.input("'") 219 | e = entry.new(state.manual(), state.source(), { 220 | insertTextFormat = 2, 221 | label = 'lua.cmp.config', 222 | textEdit = { 223 | newText = 'lua.cmp.config', 224 | range = { 225 | ['end'] = { 226 | character = 27, 227 | line = 1, 228 | }, 229 | start = { 230 | character = 18, 231 | line = 1, 232 | }, 233 | }, 234 | }, 235 | }) 236 | assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') 237 | assert.are.equal(e.filter_text, 'lua.cmp.config') 238 | end) 239 | 240 | it('[intelephense] 1', function() 241 | local state = spec.state('\t\t', 1, 4) 242 | 243 | -- press g 244 | state.input('$') 245 | local e = entry.new(state.manual(), state.source(), { 246 | kind = 6, 247 | label = '$this', 248 | sortText = '$this', 249 | textEdit = { 250 | newText = '$this', 251 | range = { 252 | ['end'] = { 253 | character = 3, 254 | line = 1, 255 | }, 256 | start = { 257 | character = 2, 258 | line = 1, 259 | }, 260 | }, 261 | }, 262 | }) 263 | assert.are.equal(e:get_vim_item(e.offset).word, '$this') 264 | assert.are.equal(e.filter_text, '$this') 265 | end) 266 | 267 | it('[odin-language-server] 1', function() 268 | local state = spec.state('\t\t', 1, 4) 269 | 270 | -- press g 271 | state.input('s') 272 | local e = entry.new(state.manual(), state.source(), { 273 | additionalTextEdits = {}, 274 | command = { 275 | arguments = {}, 276 | command = '', 277 | title = '', 278 | }, 279 | deprecated = false, 280 | detail = 'string', 281 | documentation = '', 282 | insertText = '', 283 | insertTextFormat = 1, 284 | kind = 14, 285 | label = 'string', 286 | tags = {}, 287 | }) 288 | assert.are.equal(e:get_vim_item(e.offset).word, 'string') 289 | end) 290 | 291 | it('[#47] word should not contain \\n character', function() 292 | local state = spec.state('', 1, 1) 293 | 294 | -- press g 295 | state.input('_') 296 | local e = entry.new(state.manual(), state.source(), { 297 | kind = 6, 298 | label = '__init__', 299 | insertTextFormat = 1, 300 | insertText = '__init__(self) -> None:\n pass', 301 | }) 302 | assert.are.equal(e:get_vim_item(e.offset).word, '__init__(self) -> None:') 303 | assert.are.equal(e.filter_text, '__init__') 304 | end) 305 | 306 | -- I can't understand this test case... 307 | -- it('[#1533] keyword pattern that include whitespace', function() 308 | -- local state = spec.state(' ', 1, 2) 309 | -- local state_source = state.source() 310 | 311 | -- state_source.get_keyword_pattern = function(_) 312 | -- return '.' 313 | -- end 314 | 315 | -- state.input(' ') 316 | -- local e = entry.new(state.manual(), state_source, { 317 | -- filterText = "constructor() {\n ... st = 'test';\n ", 318 | -- kind = 1, 319 | -- label = "constructor() {\n ... st = 'test';\n }", 320 | -- textEdit = { 321 | -- newText = "constructor() {\n this.test = 'test';\n }", 322 | -- range = { 323 | -- ['end'] = { 324 | -- character = 2, 325 | -- line = 2, 326 | -- }, 327 | -- start = { 328 | -- character = 0, 329 | -- line = 2, 330 | -- }, 331 | -- }, 332 | -- }, 333 | -- }) 334 | -- assert.are.equal(e:get_offset(), 2) 335 | -- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {') 336 | -- end) 337 | 338 | it('[#1533] clang regression test', function() 339 | local state = spec.state('jsonReader', 3, 11) 340 | local state_source = state.source() 341 | 342 | state.input('.') 343 | local e = entry.new(state.manual(), state_source, { 344 | filterText = 'getPath()', 345 | kind = 1, 346 | label = 'getPath()', 347 | textEdit = { 348 | newText = 'getPath()', 349 | range = { 350 | ['end'] = { 351 | character = 11, 352 | col = 12, 353 | line = 2, 354 | row = 3, 355 | }, 356 | start = { 357 | character = 11, 358 | line = 2, 359 | }, 360 | }, 361 | }, 362 | }) 363 | assert.are.equal(e.offset, 12) 364 | assert.are.equal(e:get_vim_item(e.offset).word, 'getPath()') 365 | end) 366 | end) 367 | -------------------------------------------------------------------------------- /lua/cmp/matcher_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local default_config = require('cmp.config.default') 3 | 4 | local matcher = require('cmp.matcher') 5 | 6 | describe('matcher', function() 7 | before_each(spec.before) 8 | 9 | it('match', function() 10 | local config = default_config() 11 | assert.is.truthy(matcher.match('', 'a', config.matching) >= 1) 12 | assert.is.truthy(matcher.match('a', 'a', config.matching) >= 1) 13 | assert.is.truthy(matcher.match('ab', 'a', config.matching) == 0) 14 | assert.is.truthy(matcher.match('ab', 'ab', config.matching) > matcher.match('ab', 'a_b', config.matching)) 15 | assert.is.truthy(matcher.match('ab', 'a_b_c', config.matching) > matcher.match('ac', 'a_b_c', config.matching)) 16 | 17 | assert.is.truthy(matcher.match('bora', 'border-radius', config.matching) >= 1) 18 | assert.is.truthy(matcher.match('woroff', 'word_offset', config.matching) >= 1) 19 | assert.is.truthy(matcher.match('call', 'call', config.matching) > matcher.match('call', 'condition_all', config.matching)) 20 | assert.is.truthy(matcher.match('Buffer', 'Buffer', config.matching) > matcher.match('Buffer', 'buffer', config.matching)) 21 | assert.is.truthy(matcher.match('luacon', 'lua_context', config.matching) > matcher.match('luacon', 'LuaContext', config.matching)) 22 | assert.is.truthy(matcher.match('fmodify', 'fnamemodify', config.matching) >= 1) 23 | assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single', config.matching) >= 1) 24 | 25 | assert.is.truthy(matcher.match('vi', 'void#', config.matching) >= 1) 26 | assert.is.truthy(matcher.match('vo', 'void#', config.matching) >= 1) 27 | assert.is.truthy(matcher.match('var_', 'var_dump', config.matching) >= 1) 28 | assert.is.truthy(matcher.match('conso', 'console', config.matching) > matcher.match('conso', 'ConstantSourceNode', config.matching)) 29 | assert.is.truthy(matcher.match('usela', 'useLayoutEffect', config.matching) > matcher.match('usela', 'useDataLayer', config.matching)) 30 | assert.is.truthy(matcher.match('my_', 'my_awesome_variable', config.matching) > matcher.match('my_', 'completion_matching_strategy_list', config.matching)) 31 | assert.is.truthy(matcher.match('2', '[[2021', config.matching) >= 1) 32 | 33 | assert.is.truthy(matcher.match(',', 'pri,', config.matching) == 0) 34 | assert.is.truthy(matcher.match('/', '/**', config.matching) >= 1) 35 | 36 | assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }, config.matching) == matcher.match('true', 'true', config.matching)) 37 | assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }, config.matching) > matcher.match('g', 'dein#get', { 'dein#get' }, config.matching)) 38 | 39 | assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }, config.matching) == 0) 40 | assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }, config.matching) >= 1) 41 | 42 | assert.is.truthy(matcher.match('emg', 'error_msg', config.matching) >= 1) 43 | assert.is.truthy(matcher.match('sasr', 'saved_splitright', config.matching) >= 1) 44 | 45 | -- TODO: #1420 test-case 46 | -- assert.is.truthy(matcher.match('asset_', '????') >= 0) 47 | 48 | local score, matches 49 | score, matches = matcher.match('tail', 'HCDetails', { 50 | disallow_fuzzy_matching = false, 51 | disallow_partial_matching = false, 52 | disallow_prefix_unmatching = false, 53 | disallow_partial_fuzzy_matching = false, 54 | disallow_symbol_nonprefix_matching = true, 55 | }) 56 | assert.is.truthy(score >= 1) 57 | assert.equals(matches[1].word_match_start, 5) 58 | 59 | score = matcher.match('tail', 'HCDetails', { 60 | disallow_fuzzy_matching = false, 61 | disallow_partial_matching = false, 62 | disallow_prefix_unmatching = false, 63 | disallow_partial_fuzzy_matching = true, 64 | disallow_symbol_nonprefix_matching = true, 65 | }) 66 | assert.is.truthy(score == 0) 67 | end) 68 | 69 | it('disallow_fuzzy_matching', function() 70 | assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0) 71 | assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) 72 | end) 73 | 74 | it('disallow_fullfuzzy_matching', function() 75 | assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0) 76 | assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = false }) >= 1) 77 | end) 78 | 79 | it('disallow_partial_matching', function() 80 | assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0) 81 | assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) 82 | assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1) 83 | assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1) 84 | end) 85 | 86 | it('disallow_prefix_unmatching', function() 87 | assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0) 88 | assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1) 89 | end) 90 | 91 | it('disallow_symbol_nonprefix_matching', function() 92 | assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = true }) == 0) 93 | assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = false }) >= 1) 94 | end) 95 | 96 | it('debug', function() 97 | matcher.debug = function(...) 98 | print(vim.inspect({ ... })) 99 | end 100 | -- print(vim.inspect({ 101 | -- a = matcher.match('true', 'v:true', { 'true' }), 102 | -- b = matcher.match('true', 'true'), 103 | -- })) 104 | end) 105 | end) 106 | -------------------------------------------------------------------------------- /lua/cmp/source_spec.lua: -------------------------------------------------------------------------------- 1 | local config = require('cmp.config') 2 | local spec = require('cmp.utils.spec') 3 | 4 | local source = require('cmp.source') 5 | 6 | describe('source', function() 7 | before_each(spec.before) 8 | 9 | describe('keyword length', function() 10 | it('not enough', function() 11 | config.set_buffer({ 12 | completion = { 13 | keyword_length = 3, 14 | }, 15 | }, vim.api.nvim_get_current_buf()) 16 | 17 | local state = spec.state('', 1, 1) 18 | local s = source.new('spec', { 19 | complete = function(_, _, callback) 20 | callback({ { label = 'spec' } }) 21 | end, 22 | }) 23 | assert.is.truthy(not s:complete(state.input('a'), function() end)) 24 | end) 25 | 26 | it('enough', function() 27 | config.set_buffer({ 28 | completion = { 29 | keyword_length = 3, 30 | }, 31 | }, vim.api.nvim_get_current_buf()) 32 | 33 | local state = spec.state('', 1, 1) 34 | local s = source.new('spec', { 35 | complete = function(_, _, callback) 36 | callback({ { label = 'spec' } }) 37 | end, 38 | }) 39 | assert.is.truthy(s:complete(state.input('aiu'), function() end)) 40 | end) 41 | 42 | it('enough -> not enough', function() 43 | config.set_buffer({ 44 | completion = { 45 | keyword_length = 3, 46 | }, 47 | }, vim.api.nvim_get_current_buf()) 48 | 49 | local state = spec.state('', 1, 1) 50 | local s = source.new('spec', { 51 | complete = function(_, _, callback) 52 | callback({ { label = 'spec' } }) 53 | end, 54 | }) 55 | assert.is.truthy(s:complete(state.input('aiu'), function() end)) 56 | assert.is.truthy(not s:complete(state.backspace(), function() end)) 57 | end) 58 | 59 | it('continue', function() 60 | config.set_buffer({ 61 | completion = { 62 | keyword_length = 3, 63 | }, 64 | }, vim.api.nvim_get_current_buf()) 65 | 66 | local state = spec.state('', 1, 1) 67 | local s = source.new('spec', { 68 | complete = function(_, _, callback) 69 | callback({ { label = 'spec' } }) 70 | end, 71 | }) 72 | assert.is.truthy(s:complete(state.input('aiu'), function() end)) 73 | assert.is.truthy(not s:complete(state.input('eo'), function() end)) 74 | end) 75 | end) 76 | 77 | describe('isIncomplete', function() 78 | it('isIncomplete=true', function() 79 | local state = spec.state('', 1, 1) 80 | local s = source.new('spec', { 81 | complete = function(_, _, callback) 82 | callback({ 83 | items = { { label = 'spec' } }, 84 | isIncomplete = true, 85 | }) 86 | end, 87 | }) 88 | vim.wait(100, function() 89 | return s.status == source.SourceStatus.COMPLETED 90 | end, 100, false) 91 | assert.is.truthy(s:complete(state.input('s'), function() end)) 92 | vim.wait(100, function() 93 | return s.status == source.SourceStatus.COMPLETED 94 | end, 100, false) 95 | assert.is.truthy(s:complete(state.input('p'), function() end)) 96 | vim.wait(100, function() 97 | return s.status == source.SourceStatus.COMPLETED 98 | end, 100, false) 99 | assert.is.truthy(s:complete(state.input('e'), function() end)) 100 | vim.wait(100, function() 101 | return s.status == source.SourceStatus.COMPLETED 102 | end, 100, false) 103 | assert.is.truthy(s:complete(state.input('c'), function() end)) 104 | vim.wait(100, function() 105 | return s.status == source.SourceStatus.COMPLETED 106 | end, 100, false) 107 | end) 108 | end) 109 | end) 110 | -------------------------------------------------------------------------------- /lua/cmp/types/cmp.lua: -------------------------------------------------------------------------------- 1 | local cmp = {} 2 | 3 | ---@alias cmp.ConfirmBehavior 'insert' | 'replace' 4 | cmp.ConfirmBehavior = { 5 | Insert = 'insert', 6 | Replace = 'replace', 7 | } 8 | 9 | ---@alias cmp.SelectBehavior 'insert' | 'select' 10 | cmp.SelectBehavior = { 11 | Insert = 'insert', 12 | Select = 'select', 13 | } 14 | 15 | ---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none' 16 | cmp.ContextReason = { 17 | Auto = 'auto', 18 | Manual = 'manual', 19 | TriggerOnly = 'triggerOnly', 20 | None = 'none', 21 | } 22 | 23 | ---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged' 24 | cmp.TriggerEvent = { 25 | InsertEnter = 'InsertEnter', 26 | TextChanged = 'TextChanged', 27 | } 28 | 29 | ---@alias cmp.PreselectMode 'item' | 'None' 30 | cmp.PreselectMode = { 31 | Item = 'item', 32 | None = 'none', 33 | } 34 | 35 | ---@alias cmp.ItemField 'abbr' | 'kind' | 'menu' 36 | cmp.ItemField = { 37 | Abbr = 'abbr', 38 | Kind = 'kind', 39 | Menu = 'menu', 40 | } 41 | 42 | ---@class cmp.ContextOption 43 | ---@field public reason cmp.ContextReason|nil 44 | 45 | ---@class cmp.ConfirmOption 46 | ---@field public behavior cmp.ConfirmBehavior 47 | ---@field public commit_character? string 48 | 49 | ---@class cmp.SelectOption 50 | ---@field public behavior cmp.SelectBehavior 51 | 52 | ---@class cmp.SnippetExpansionParams 53 | ---@field public body string 54 | ---@field public insert_text_mode integer 55 | 56 | ---@class cmp.CompleteParams 57 | ---@field public reason? cmp.ContextReason 58 | ---@field public config? cmp.ConfigSchema 59 | 60 | ---@class cmp.SetupProperty 61 | ---@field public buffer fun(c: cmp.ConfigSchema) 62 | ---@field public global fun(c: cmp.ConfigSchema) 63 | ---@field public cmdline fun(type: string|string[], c: cmp.ConfigSchema) 64 | ---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema) 65 | 66 | ---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema) 67 | 68 | ---@class cmp.SourceApiParams: cmp.SourceConfig 69 | 70 | ---@class cmp.SourceCompletionApiParams : cmp.SourceConfig 71 | ---@field public offset integer 72 | ---@field public context cmp.Context 73 | ---@field public completion_context lsp.CompletionContext 74 | 75 | ---@alias cmp.MappingFunction fun(fallback: function): nil 76 | 77 | ---@class cmp.MappingClass 78 | ---@field public i nil|cmp.MappingFunction 79 | ---@field public c nil|cmp.MappingFunction 80 | ---@field public x nil|cmp.MappingFunction 81 | ---@field public s nil|cmp.MappingFunction 82 | 83 | ---@alias cmp.Mapping cmp.MappingFunction | cmp.MappingClass 84 | 85 | ---@class cmp.ConfigSchema 86 | ---@field private revision? integer 87 | ---@field public enabled? boolean | fun(): boolean 88 | ---@field public performance? cmp.PerformanceConfig 89 | ---@field public preselect? cmp.PreselectMode 90 | ---@field public completion? cmp.CompletionConfig 91 | ---@field public window? cmp.WindowConfig|nil 92 | ---@field public confirmation? cmp.ConfirmationConfig 93 | ---@field public matching? cmp.MatchingConfig 94 | ---@field public sorting? cmp.SortingConfig 95 | ---@field public formatting? cmp.FormattingConfig 96 | ---@field public snippet? cmp.SnippetConfig 97 | ---@field public mapping? table 98 | ---@field public sources? cmp.SourceConfig[] 99 | ---@field public view? cmp.ViewConfig 100 | ---@field public experimental? cmp.ExperimentalConfig 101 | 102 | ---@class cmp.PerformanceConfig 103 | ---@field public debounce integer 104 | ---@field public throttle integer 105 | ---@field public fetching_timeout integer 106 | ---@field public filtering_context_budget integer 107 | ---@field public confirm_resolve_timeout integer 108 | ---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop. 109 | ---@field public max_view_entries integer 110 | 111 | ---@class cmp.CompletionConfig 112 | ---@field public autocomplete? cmp.TriggerEvent[]|false 113 | ---@field public completeopt? string 114 | ---@field public get_trigger_characters? fun(trigger_characters: string[]): string[] 115 | ---@field public keyword_length? integer 116 | ---@field public keyword_pattern? string 117 | 118 | ---@class cmp.WindowConfig 119 | ---@field public completion? cmp.CompletionWindowOptions 120 | ---@field public documentation? cmp.DocumentationWindowOptions|nil 121 | 122 | ---@class cmp.WindowOptions 123 | ---@field public border? string|string[] 124 | ---@field public winhighlight? string 125 | ---@field public winblend? number 126 | ---@field public zindex? integer|nil 127 | 128 | ---@class cmp.CompletionWindowOptions: cmp.WindowOptions 129 | ---@field public scrolloff? integer|nil 130 | ---@field public col_offset? integer|nil 131 | ---@field public side_padding? integer|nil 132 | ---@field public scrollbar? boolean|true 133 | 134 | ---@class cmp.DocumentationWindowOptions: cmp.WindowOptions 135 | ---@field public max_height? integer|nil 136 | ---@field public max_width? integer|nil 137 | 138 | ---@class cmp.ConfirmationConfig 139 | ---@field public default_behavior cmp.ConfirmBehavior 140 | ---@field public get_commit_characters fun(commit_characters: string[]): string[] 141 | 142 | ---@class cmp.MatchingConfig 143 | ---@field public disallow_fuzzy_matching boolean 144 | ---@field public disallow_fullfuzzy_matching boolean 145 | ---@field public disallow_partial_fuzzy_matching boolean 146 | ---@field public disallow_partial_matching boolean 147 | ---@field public disallow_prefix_unmatching boolean 148 | ---@field public disallow_symbol_nonprefix_matching boolean 149 | 150 | ---@class cmp.SortingConfig 151 | ---@field public priority_weight integer 152 | ---@field public comparators cmp.Comparator[] 153 | 154 | ---@class cmp.FormattingConfig 155 | ---@field public fields? cmp.ItemField[] 156 | ---@field public expandable_indicator? boolean 157 | ---@field public format? fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem 158 | 159 | ---@class cmp.SnippetConfig 160 | ---@field public expand fun(args: cmp.SnippetExpansionParams) 161 | 162 | ---@class cmp.ExperimentalConfig 163 | ---@field public ghost_text cmp.GhostTextConfig|boolean 164 | 165 | ---@class cmp.GhostTextConfig 166 | ---@field hl_group string 167 | 168 | ---@class cmp.SourceConfig 169 | ---@field public name string 170 | ---@field public option table|nil 171 | ---@field public priority integer|nil 172 | ---@field public trigger_characters string[]|nil 173 | ---@field public keyword_pattern string|nil 174 | ---@field public keyword_length integer|nil 175 | ---@field public max_item_count integer|nil 176 | ---@field public group_index integer|nil 177 | ---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean 178 | 179 | ---@class cmp.ViewConfig 180 | ---@field public entries? cmp.EntriesViewConfig 181 | ---@field public docs? cmp.DocsViewConfig 182 | 183 | ---@alias cmp.EntriesViewConfig cmp.CustomEntriesViewConfig|cmp.NativeEntriesViewConfig|cmp.WildmenuEntriesViewConfig|string 184 | 185 | ---@class cmp.CustomEntriesViewConfig 186 | ---@field name 'custom' 187 | ---@field selection_order 'top_down'|'near_cursor' 188 | ---@field follow_cursor boolean 189 | 190 | ---@class cmp.NativeEntriesViewConfig 191 | ---@field name 'native' 192 | 193 | ---@class cmp.WildmenuEntriesViewConfig 194 | ---@field name 'wildmenu' 195 | ---@field separator string|nil 196 | 197 | ---@class cmp.DocsViewConfig 198 | ---@field public auto_open boolean 199 | 200 | return cmp 201 | -------------------------------------------------------------------------------- /lua/cmp/types/init.lua: -------------------------------------------------------------------------------- 1 | local types = {} 2 | 3 | types.cmp = require('cmp.types.cmp') 4 | types.lsp = require('cmp.types.lsp') 5 | types.vim = require('cmp.types.vim') 6 | 7 | return types 8 | -------------------------------------------------------------------------------- /lua/cmp/types/lsp.lua: -------------------------------------------------------------------------------- 1 | local misc = require('cmp.utils.misc') 2 | 3 | ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/ 4 | ---@class lsp 5 | local lsp = {} 6 | 7 | ---@enum lsp.PositionEncodingKind 8 | lsp.PositionEncodingKind = { 9 | UTF8 = 'utf-8', 10 | UTF16 = 'utf-16', 11 | UTF32 = 'utf-32', 12 | } 13 | 14 | lsp.Position = { 15 | ---Convert lsp.Position to vim.Position 16 | ---@param buf integer 17 | ---@param position lsp.Position 18 | -- 19 | ---@return vim.Position 20 | to_vim = function(buf, position) 21 | if not vim.api.nvim_buf_is_loaded(buf) then 22 | vim.fn.bufload(buf) 23 | end 24 | local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) 25 | if #lines > 0 then 26 | return { 27 | row = position.line + 1, 28 | col = misc.to_vimindex(lines[1], position.character), 29 | } 30 | end 31 | return { 32 | row = position.line + 1, 33 | col = position.character + 1, 34 | } 35 | end, 36 | ---Convert vim.Position to lsp.Position 37 | ---@param buf integer 38 | ---@param position vim.Position 39 | ---@return lsp.Position 40 | to_lsp = function(buf, position) 41 | if not vim.api.nvim_buf_is_loaded(buf) then 42 | vim.fn.bufload(buf) 43 | end 44 | local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) 45 | if #lines > 0 then 46 | return { 47 | line = position.row - 1, 48 | character = misc.to_utfindex(lines[1], position.col), 49 | } 50 | end 51 | return { 52 | line = position.row - 1, 53 | character = position.col - 1, 54 | } 55 | end, 56 | 57 | ---Convert position to utf8 from specified encoding. 58 | ---@param text string 59 | ---@param position lsp.Position 60 | ---@param from_encoding? lsp.PositionEncodingKind 61 | ---@return lsp.Position 62 | to_utf8 = function(text, position, from_encoding) 63 | from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 64 | if from_encoding == lsp.PositionEncodingKind.UTF8 then 65 | return position 66 | end 67 | 68 | local ok, byteindex = pcall(vim.str_byteindex, text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16) 69 | if not ok then 70 | return position 71 | end 72 | return { line = position.line, character = byteindex } 73 | end, 74 | 75 | ---Convert position to utf16 from specified encoding. 76 | ---@param text string 77 | ---@param position lsp.Position 78 | ---@param from_encoding? lsp.PositionEncodingKind 79 | ---@return lsp.Position 80 | to_utf16 = function(text, position, from_encoding) 81 | from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 82 | if from_encoding == lsp.PositionEncodingKind.UTF16 then 83 | return position 84 | end 85 | 86 | local utf8 = lsp.Position.to_utf8(text, position, from_encoding) 87 | for index = utf8.character, 0, -1 do 88 | local ok, utf16index = pcall(function() 89 | return select(2, vim.str_utfindex(text, index)) 90 | end) 91 | if ok then 92 | return { line = utf8.line, character = utf16index } 93 | end 94 | end 95 | return position 96 | end, 97 | 98 | ---Convert position to utf32 from specified encoding. 99 | ---@param text string 100 | ---@param position lsp.Position 101 | ---@param from_encoding? lsp.PositionEncodingKind 102 | ---@return lsp.Position 103 | to_utf32 = function(text, position, from_encoding) 104 | from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16 105 | if from_encoding == lsp.PositionEncodingKind.UTF32 then 106 | return position 107 | end 108 | 109 | local utf8 = lsp.Position.to_utf8(text, position, from_encoding) 110 | for index = utf8.character, 0, -1 do 111 | local ok, utf32index = pcall(function() 112 | return select(1, vim.str_utfindex(text, index)) 113 | end) 114 | if ok then 115 | return { line = utf8.line, character = utf32index } 116 | end 117 | end 118 | return position 119 | end, 120 | } 121 | 122 | lsp.Range = { 123 | ---Convert lsp.Range to vim.Range 124 | ---@param buf integer 125 | ---@param range lsp.Range 126 | ---@return vim.Range 127 | to_vim = function(buf, range) 128 | return { 129 | start = lsp.Position.to_vim(buf, range.start), 130 | ['end'] = lsp.Position.to_vim(buf, range['end']), 131 | } 132 | end, 133 | 134 | ---Convert vim.Range to lsp.Range 135 | ---@param buf integer 136 | ---@param range vim.Range 137 | ---@return lsp.Range 138 | to_lsp = function(buf, range) 139 | return { 140 | start = lsp.Position.to_lsp(buf, range.start), 141 | ['end'] = lsp.Position.to_lsp(buf, range['end']), 142 | } 143 | end, 144 | } 145 | 146 | ---@alias lsp.CompletionTriggerKind 1 | 2 | 3 147 | lsp.CompletionTriggerKind = { 148 | Invoked = 1, 149 | TriggerCharacter = 2, 150 | TriggerForIncompleteCompletions = 3, 151 | } 152 | 153 | ---@alias lsp.InsertTextFormat 1 | 2 154 | lsp.InsertTextFormat = {} 155 | lsp.InsertTextFormat.PlainText = 1 156 | lsp.InsertTextFormat.Snippet = 2 157 | 158 | ---@alias lsp.InsertTextMode 1 | 2 159 | lsp.InsertTextMode = { 160 | AsIs = 1, 161 | AdjustIndentation = 2, 162 | } 163 | 164 | ---@alias lsp.MarkupKind 'plaintext' | 'markdown' 165 | lsp.MarkupKind = { 166 | PlainText = 'plaintext', 167 | Markdown = 'markdown', 168 | } 169 | 170 | ---@alias lsp.CompletionItemTag 1 171 | lsp.CompletionItemTag = { 172 | Deprecated = 1, 173 | } 174 | 175 | ---@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 176 | lsp.CompletionItemKind = { 177 | Text = 1, 178 | Method = 2, 179 | Function = 3, 180 | Constructor = 4, 181 | Field = 5, 182 | Variable = 6, 183 | Class = 7, 184 | Interface = 8, 185 | Module = 9, 186 | Property = 10, 187 | Unit = 11, 188 | Value = 12, 189 | Enum = 13, 190 | Keyword = 14, 191 | Snippet = 15, 192 | Color = 16, 193 | File = 17, 194 | Reference = 18, 195 | Folder = 19, 196 | EnumMember = 20, 197 | Constant = 21, 198 | Struct = 22, 199 | Event = 23, 200 | Operator = 24, 201 | TypeParameter = 25, 202 | } 203 | for _, k in ipairs(vim.tbl_keys(lsp.CompletionItemKind)) do 204 | local v = lsp.CompletionItemKind[k] 205 | lsp.CompletionItemKind[v] = k 206 | end 207 | 208 | ---@class lsp.internal.CompletionItemDefaults 209 | ---@field public commitCharacters? string[] 210 | ---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range } 211 | ---@field public insertTextFormat? lsp.InsertTextFormat 212 | ---@field public insertTextMode? lsp.InsertTextMode 213 | ---@field public data? any 214 | 215 | ---@class lsp.CompletionContext 216 | ---@field public triggerKind lsp.CompletionTriggerKind 217 | ---@field public triggerCharacter string|nil 218 | 219 | ---@class lsp.CompletionList 220 | ---@field public isIncomplete boolean 221 | ---@field public itemDefaults? lsp.internal.CompletionItemDefaults 222 | ---@field public items lsp.CompletionItem[] 223 | 224 | ---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[] 225 | 226 | ---@class lsp.MarkupContent 227 | ---@field public kind lsp.MarkupKind 228 | ---@field public value string 229 | 230 | ---@class lsp.Position 231 | ---@field public line integer 232 | ---@field public character integer 233 | 234 | ---@class lsp.Range 235 | ---@field public start lsp.Position 236 | ---@field public end lsp.Position 237 | 238 | ---@class lsp.Command 239 | ---@field public title string 240 | ---@field public command string 241 | ---@field public arguments any[]|nil 242 | 243 | ---@class lsp.TextEdit 244 | ---@field public range lsp.Range|nil 245 | ---@field public newText string 246 | 247 | ---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit 248 | 249 | ---@class lsp.internal.InsertTextEdit 250 | ---@field public insert lsp.Range 251 | ---@field public newText string 252 | 253 | ---@class lsp.internal.ReplaceTextEdit 254 | ---@field public replace lsp.Range 255 | ---@field public newText string 256 | 257 | ---@class lsp.CompletionItemLabelDetails 258 | ---@field public detail? string 259 | ---@field public description? string 260 | 261 | ---@class lsp.internal.CmpCompletionExtension 262 | ---@field public kind_text string 263 | ---@field public kind_hl_group string 264 | 265 | ---@class lsp.CompletionItem 266 | ---@field public label string 267 | ---@field public labelDetails? lsp.CompletionItemLabelDetails 268 | ---@field public kind? lsp.CompletionItemKind 269 | ---@field public tags? lsp.CompletionItemTag[] 270 | ---@field public detail? string 271 | ---@field public documentation? lsp.MarkupContent|string 272 | ---@field public deprecated? boolean 273 | ---@field public preselect? boolean 274 | ---@field public sortText? string 275 | ---@field public filterText? string 276 | ---@field public insertText? string 277 | ---@field public insertTextFormat? lsp.InsertTextFormat 278 | ---@field public insertTextMode? lsp.InsertTextMode 279 | ---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit 280 | ---@field public textEditText? string 281 | ---@field public additionalTextEdits? lsp.TextEdit[] 282 | ---@field public commitCharacters? string[] 283 | ---@field public command? lsp.Command 284 | ---@field public data? any 285 | ---@field public cmp? lsp.internal.CmpCompletionExtension 286 | --- 287 | ---TODO: Should send the issue for upstream? 288 | ---@field public word string|nil 289 | ---@field public dup boolean|nil 290 | 291 | return lsp 292 | -------------------------------------------------------------------------------- /lua/cmp/types/lsp_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local lsp = require('cmp.types.lsp') 3 | 4 | describe('types.lsp', function() 5 | before_each(spec.before) 6 | describe('Position', function() 7 | vim.fn.setline('1', { 8 | 'あいうえお', 9 | 'かきくけこ', 10 | 'さしすせそ', 11 | }) 12 | local vim_position, lsp_position 13 | 14 | local bufnr = vim.api.nvim_get_current_buf() 15 | vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 3 }) 16 | assert.are.equal(vim_position.row, 2) 17 | assert.are.equal(vim_position.col, 10) 18 | lsp_position = lsp.Position.to_lsp(bufnr, vim_position) 19 | assert.are.equal(lsp_position.line, 1) 20 | assert.are.equal(lsp_position.character, 3) 21 | 22 | vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 0 }) 23 | assert.are.equal(vim_position.row, 2) 24 | assert.are.equal(vim_position.col, 1) 25 | lsp_position = lsp.Position.to_lsp(bufnr, vim_position) 26 | assert.are.equal(lsp_position.line, 1) 27 | assert.are.equal(lsp_position.character, 0) 28 | 29 | vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 5 }) 30 | assert.are.equal(vim_position.row, 2) 31 | assert.are.equal(vim_position.col, 16) 32 | lsp_position = lsp.Position.to_lsp(bufnr, vim_position) 33 | assert.are.equal(lsp_position.line, 1) 34 | assert.are.equal(lsp_position.character, 5) 35 | 36 | -- overflow (lsp -> vim) 37 | vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 6 }) 38 | assert.are.equal(vim_position.row, 2) 39 | assert.are.equal(vim_position.col, 16) 40 | 41 | -- overflow(vim -> lsp) 42 | vim_position.col = vim_position.col + 1 43 | lsp_position = lsp.Position.to_lsp(bufnr, vim_position) 44 | assert.are.equal(lsp_position.line, 1) 45 | assert.are.equal(lsp_position.character, 5) 46 | end) 47 | end) 48 | -------------------------------------------------------------------------------- /lua/cmp/types/vim.lua: -------------------------------------------------------------------------------- 1 | ---@class vim.CompletedItem 2 | ---@field public word string 3 | ---@field public abbr string|nil 4 | ---@field public kind string|nil 5 | ---@field public menu string|nil 6 | ---@field public equal 1|nil 7 | ---@field public empty 1|nil 8 | ---@field public dup 1|nil 9 | ---@field public id any 10 | ---@field public abbr_hl_group string|table|nil 11 | ---@field public kind_hl_group string|table|nil 12 | ---@field public menu_hl_group string|table|nil 13 | 14 | ---@class vim.Position 1-based index 15 | ---@field public row integer 16 | ---@field public col integer 17 | 18 | ---@class vim.Range 19 | ---@field public start vim.Position 20 | ---@field public end vim.Position 21 | -------------------------------------------------------------------------------- /lua/cmp/utils/api.lua: -------------------------------------------------------------------------------- 1 | local api = {} 2 | 3 | local CTRL_V = vim.api.nvim_replace_termcodes('', true, true, true) 4 | local CTRL_S = vim.api.nvim_replace_termcodes('', true, true, true) 5 | 6 | api.get_mode = function() 7 | local mode = vim.api.nvim_get_mode().mode:sub(1, 1) 8 | if mode == 'i' then 9 | return 'i' -- insert 10 | elseif mode == 'v' or mode == 'V' or mode == CTRL_V then 11 | return 'x' -- visual 12 | elseif mode == 's' or mode == 'S' or mode == CTRL_S then 13 | return 's' -- select 14 | elseif mode == 'c' and vim.fn.getcmdtype() ~= '=' then 15 | return 'c' -- cmdline 16 | end 17 | end 18 | 19 | api.is_insert_mode = function() 20 | return api.get_mode() == 'i' 21 | end 22 | 23 | api.is_cmdline_mode = function() 24 | return api.get_mode() == 'c' 25 | end 26 | 27 | api.is_select_mode = function() 28 | return api.get_mode() == 's' 29 | end 30 | 31 | api.is_visual_mode = function() 32 | return api.get_mode() == 'x' 33 | end 34 | 35 | api.is_suitable_mode = function() 36 | local mode = api.get_mode() 37 | return mode == 'i' or mode == 'c' 38 | end 39 | 40 | api.get_current_line = function() 41 | if api.is_cmdline_mode() then 42 | return vim.fn.getcmdline() 43 | end 44 | return vim.api.nvim_get_current_line() 45 | end 46 | 47 | ---@return { [1]: integer, [2]: integer } 48 | api.get_cursor = function() 49 | if api.is_cmdline_mode() then 50 | return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option_value('cmdheight', {}) - 1)), vim.fn.getcmdpos() - 1 } 51 | end 52 | return vim.api.nvim_win_get_cursor(0) 53 | end 54 | 55 | api.get_screen_cursor = function() 56 | if api.is_cmdline_mode() then 57 | local cursor = api.get_cursor() 58 | return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) } 59 | end 60 | local cursor = api.get_cursor() 61 | local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) 62 | return { pos.row, pos.col - 1 } 63 | end 64 | 65 | api.get_cursor_before_line = function() 66 | local cursor = api.get_cursor() 67 | return string.sub(api.get_current_line(), 1, cursor[2]) 68 | end 69 | 70 | --- Applies a list of text edits to a buffer. Preserves 'buflisted' state. 71 | ---@param text_edits lsp.TextEdit[] 72 | ---@param bufnr integer Buffer id 73 | ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' 74 | api.apply_text_edits = function(text_edits, bufnr, position_encoding) 75 | -- preserve 'buflisted' state because vim.lsp.util.apply_text_edits forces it to true 76 | local prev_buflisted = vim.bo[bufnr].buflisted 77 | vim.lsp.util.apply_text_edits(text_edits, bufnr, position_encoding) 78 | vim.bo[bufnr].buflisted = prev_buflisted 79 | end 80 | 81 | return api 82 | -------------------------------------------------------------------------------- /lua/cmp/utils/api_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local keymap = require('cmp.utils.keymap') 3 | local feedkeys = require('cmp.utils.feedkeys') 4 | local api = require('cmp.utils.api') 5 | 6 | describe('api', function() 7 | before_each(spec.before) 8 | describe('get_cursor', function() 9 | it('insert-mode', function() 10 | local cursor 11 | feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() 12 | cursor = api.get_cursor() 13 | end) 14 | assert.are.equal(cursor[2], 11) 15 | end) 16 | it('cmdline-mode', function() 17 | local cursor 18 | keymap.set_map(0, 'c', '(cmp-spec-spy)', function() 19 | cursor = api.get_cursor() 20 | end, { expr = true, noremap = true }) 21 | feedkeys.call(keymap.t(':\t1234567890'), 'n') 22 | feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') 23 | assert.are.equal(cursor[2], 11) 24 | end) 25 | end) 26 | 27 | describe('get_screen_cursor', function() 28 | it('insert-mode', function() 29 | local screen_cursor 30 | feedkeys.call(keymap.t('iあいうえお'), 'nx', function() 31 | screen_cursor = api.get_screen_cursor() 32 | end) 33 | assert.are.equal(10, screen_cursor[2]) 34 | end) 35 | it('cmdline-mode', function() 36 | local screen_cursor 37 | keymap.set_map(0, 'c', '(cmp-spec-spy)', function() 38 | screen_cursor = api.get_screen_cursor() 39 | end, { expr = true, noremap = true }) 40 | feedkeys.call(keymap.t(':あいうえお'), 'n') 41 | feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') 42 | assert.are.equal(10, screen_cursor[2]) 43 | end) 44 | end) 45 | 46 | describe('get_cursor_before_line', function() 47 | it('insert-mode', function() 48 | local cursor_before_line 49 | feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() 50 | cursor_before_line = api.get_cursor_before_line() 51 | end) 52 | assert.are.same(cursor_before_line, '\t12345678') 53 | end) 54 | it('cmdline-mode', function() 55 | local cursor_before_line 56 | keymap.set_map(0, 'c', '(cmp-spec-spy)', function() 57 | cursor_before_line = api.get_cursor_before_line() 58 | end, { expr = true, noremap = true }) 59 | feedkeys.call(keymap.t(':\t1234567890'), 'n') 60 | feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') 61 | assert.are.same(cursor_before_line, '\t12345678') 62 | end) 63 | end) 64 | end) 65 | -------------------------------------------------------------------------------- /lua/cmp/utils/async.lua: -------------------------------------------------------------------------------- 1 | local feedkeys = require('cmp.utils.feedkeys') 2 | local config = require('cmp.config') 3 | 4 | local async = {} 5 | 6 | ---@class cmp.AsyncThrottle 7 | ---@field public running boolean 8 | ---@field public timeout integer 9 | ---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil) 10 | ---@field public stop function 11 | ---@field public __call function 12 | 13 | ---@type uv_timer_t[] 14 | local timers = {} 15 | 16 | vim.api.nvim_create_autocmd('VimLeavePre', { 17 | callback = function() 18 | for _, timer in pairs(timers) do 19 | if timer and not timer:is_closing() then 20 | timer:stop() 21 | timer:close() 22 | end 23 | end 24 | end, 25 | }) 26 | 27 | ---@param fn function 28 | ---@param timeout integer 29 | ---@return cmp.AsyncThrottle 30 | async.throttle = function(fn, timeout) 31 | local time = nil 32 | local timer = assert(vim.loop.new_timer()) 33 | local _async = nil ---@type Async? 34 | timers[#timers + 1] = timer 35 | local throttle 36 | throttle = setmetatable({ 37 | running = false, 38 | timeout = timeout, 39 | sync = function(self, timeout_) 40 | if not self.running then 41 | return 42 | end 43 | vim.wait(timeout_ or 1000, function() 44 | return not self.running 45 | end, 10) 46 | end, 47 | stop = function(reset_time) 48 | if reset_time ~= false then 49 | time = nil 50 | end 51 | -- can't use self here unfortunately 52 | throttle.running = false 53 | timer:stop() 54 | if _async then 55 | _async:cancel() 56 | _async = nil 57 | end 58 | end, 59 | }, { 60 | __call = function(self, ...) 61 | local args = { ... } 62 | 63 | if time == nil then 64 | time = vim.loop.now() 65 | end 66 | self.stop(false) 67 | self.running = true 68 | timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() 69 | vim.schedule(function() 70 | time = nil 71 | local ret = fn(unpack(args)) 72 | if async.is_async(ret) then 73 | ---@cast ret Async 74 | _async = ret 75 | _async:await(function(_, error) 76 | _async = nil 77 | self.running = false 78 | if error and error ~= 'abort' then 79 | vim.notify(error, vim.log.levels.ERROR) 80 | end 81 | end) 82 | else 83 | self.running = false 84 | end 85 | end) 86 | end) 87 | end, 88 | }) 89 | return throttle 90 | end 91 | 92 | ---Control async tasks. 93 | async.step = function(...) 94 | local tasks = { ... } 95 | local next 96 | next = function(...) 97 | if #tasks > 0 then 98 | table.remove(tasks, 1)(next, ...) 99 | end 100 | end 101 | table.remove(tasks, 1)(next) 102 | end 103 | 104 | ---Timeout callback function 105 | ---@param fn function 106 | ---@param timeout integer 107 | ---@return function 108 | async.timeout = function(fn, timeout) 109 | local timer 110 | local done = false 111 | local callback = function(...) 112 | if not done then 113 | done = true 114 | timer:stop() 115 | timer:close() 116 | fn(...) 117 | end 118 | end 119 | timer = vim.loop.new_timer() 120 | timer:start(timeout, 0, function() 121 | callback() 122 | end) 123 | return callback 124 | end 125 | 126 | ---@alias cmp.AsyncDedup fun(callback: function): function 127 | 128 | ---Create deduplicated callback 129 | ---@return function 130 | async.dedup = function() 131 | local id = 0 132 | return function(callback) 133 | id = id + 1 134 | 135 | local current = id 136 | return function(...) 137 | if current == id then 138 | callback(...) 139 | end 140 | end 141 | end 142 | end 143 | 144 | ---Convert async process as sync 145 | async.sync = function(runner, timeout) 146 | local done = false 147 | runner(function() 148 | done = true 149 | end) 150 | vim.wait(timeout, function() 151 | return done 152 | end, 10, false) 153 | end 154 | 155 | ---Wait and callback for next safe state. 156 | async.debounce_next_tick = function(callback) 157 | local running = false 158 | return function() 159 | if running then 160 | return 161 | end 162 | running = true 163 | vim.schedule(function() 164 | running = false 165 | callback() 166 | end) 167 | end 168 | end 169 | 170 | ---Wait and callback for consuming next keymap. 171 | async.debounce_next_tick_by_keymap = function(callback) 172 | return function() 173 | feedkeys.call('', '', callback) 174 | end 175 | end 176 | 177 | local Scheduler = {} 178 | Scheduler._queue = {} 179 | Scheduler._executor = assert(vim.loop.new_check()) 180 | 181 | function Scheduler.step() 182 | local budget = config.get().performance.async_budget * 1e6 183 | local start = vim.loop.hrtime() 184 | while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do 185 | local a = table.remove(Scheduler._queue, 1) 186 | a:_step() 187 | if a.running then 188 | table.insert(Scheduler._queue, a) 189 | end 190 | end 191 | if #Scheduler._queue == 0 then 192 | return Scheduler._executor:stop() 193 | end 194 | end 195 | 196 | ---@param a Async 197 | function Scheduler.add(a) 198 | table.insert(Scheduler._queue, a) 199 | if not Scheduler._executor:is_active() then 200 | Scheduler._executor:start(vim.schedule_wrap(Scheduler.step)) 201 | end 202 | end 203 | 204 | --- @alias AsyncCallback fun(result?:any, error?:string) 205 | 206 | --- @class Async 207 | --- @field running boolean 208 | --- @field result? any 209 | --- @field error? string 210 | --- @field callbacks AsyncCallback[] 211 | --- @field thread thread 212 | local Async = {} 213 | Async.__index = Async 214 | 215 | function Async.new(fn) 216 | local self = setmetatable({}, Async) 217 | self.callbacks = {} 218 | self.running = true 219 | self.thread = coroutine.create(fn) 220 | Scheduler.add(self) 221 | return self 222 | end 223 | 224 | ---@param result? any 225 | ---@param error? string 226 | function Async:_done(result, error) 227 | if self.running then 228 | self.running = false 229 | self.result = result 230 | self.error = error 231 | end 232 | for _, callback in ipairs(self.callbacks) do 233 | callback(result, error) 234 | end 235 | -- only run each callback once. 236 | -- _done can possibly be called multiple times. 237 | -- so we need to clear callbacks after executing them. 238 | self.callbacks = {} 239 | end 240 | 241 | function Async:_step() 242 | local ok, res = coroutine.resume(self.thread) 243 | if not ok then 244 | return self:_done(nil, res) 245 | elseif res == 'abort' then 246 | return self:_done(nil, 'abort') 247 | elseif coroutine.status(self.thread) == 'dead' then 248 | return self:_done(res) 249 | end 250 | end 251 | 252 | function Async:cancel() 253 | self:_done(nil, 'abort') 254 | end 255 | 256 | ---@param cb AsyncCallback 257 | function Async:await(cb) 258 | if not cb then 259 | error('callback is required') 260 | end 261 | if self.running then 262 | table.insert(self.callbacks, cb) 263 | else 264 | cb(self.result, self.error) 265 | end 266 | end 267 | 268 | function Async:sync() 269 | while self.running do 270 | vim.wait(10) 271 | end 272 | return self.error and error(self.error) or self.result 273 | end 274 | 275 | --- @return boolean 276 | function async.is_async(obj) 277 | return obj and type(obj) == 'table' and getmetatable(obj) == Async 278 | end 279 | 280 | --- @return fun(...): Async 281 | function async.wrap(fn) 282 | return function(...) 283 | local args = { ... } 284 | return Async.new(function() 285 | return fn(unpack(args)) 286 | end) 287 | end 288 | end 289 | 290 | -- This will yield when called from a coroutine 291 | function async.yield(...) 292 | if coroutine.running() == nil then 293 | error('Trying to yield from a non-yieldable context') 294 | return ... 295 | end 296 | return coroutine.yield(...) 297 | end 298 | 299 | function async.abort() 300 | return async.yield('abort') 301 | end 302 | 303 | return async 304 | -------------------------------------------------------------------------------- /lua/cmp/utils/async_spec.lua: -------------------------------------------------------------------------------- 1 | local async = require('cmp.utils.async') 2 | 3 | describe('utils.async', function() 4 | it('throttle', function() 5 | local count = 0 6 | local now 7 | local f = async.throttle(function() 8 | count = count + 1 9 | end, 100) 10 | 11 | -- 1. delay for 100ms 12 | now = vim.loop.now() 13 | f.timeout = 100 14 | f() 15 | vim.wait(1000, function() 16 | return count == 1 17 | end) 18 | assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) 19 | 20 | -- 2. delay for 500ms 21 | now = vim.loop.now() 22 | f.timeout = 500 23 | f() 24 | vim.wait(1000, function() 25 | return count == 2 26 | end) 27 | assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) 28 | 29 | -- 4. delay for 500ms and wait 100ms (remain 400ms) 30 | f.timeout = 500 31 | f() 32 | vim.wait(100) -- remain 400ms 33 | 34 | -- 5. call immediately (100ms already elapsed from No.4) 35 | now = vim.loop.now() 36 | f.timeout = 100 37 | f() 38 | vim.wait(1000, function() 39 | return count == 3 40 | end) 41 | assert.is.truthy(math.abs(vim.loop.now() - now) < 10) 42 | end) 43 | it('step', function() 44 | local done = false 45 | local step = {} 46 | async.step(function(next) 47 | vim.defer_fn(function() 48 | table.insert(step, 1) 49 | next() 50 | end, 10) 51 | end, function(next) 52 | vim.defer_fn(function() 53 | table.insert(step, 2) 54 | next() 55 | end, 10) 56 | end, function(next) 57 | vim.defer_fn(function() 58 | table.insert(step, 3) 59 | next() 60 | end, 10) 61 | end, function() 62 | done = true 63 | end) 64 | vim.wait(1000, function() 65 | return done 66 | end) 67 | assert.are.same(step, { 1, 2, 3 }) 68 | end) 69 | end) 70 | -------------------------------------------------------------------------------- /lua/cmp/utils/autocmd.lua: -------------------------------------------------------------------------------- 1 | local debug = require('cmp.utils.debug') 2 | 3 | local autocmd = {} 4 | 5 | autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true }) 6 | 7 | autocmd.events = {} 8 | 9 | local function create_autocmd(event) 10 | vim.api.nvim_create_autocmd(event, { 11 | desc = ('nvim-cmp: autocmd: %s'):format(event), 12 | group = autocmd.group, 13 | callback = function() 14 | autocmd.emit(event) 15 | end, 16 | }) 17 | end 18 | 19 | ---Subscribe autocmd 20 | ---@param events string|string[] 21 | ---@param callback function 22 | ---@return function 23 | autocmd.subscribe = function(events, callback) 24 | events = type(events) == 'string' and { events } or events 25 | 26 | for _, event in ipairs(events) do 27 | if not autocmd.events[event] then 28 | autocmd.events[event] = {} 29 | create_autocmd(event) 30 | end 31 | table.insert(autocmd.events[event], callback) 32 | end 33 | 34 | return function() 35 | for _, event in ipairs(events) do 36 | for i, callback_ in ipairs(autocmd.events[event]) do 37 | if callback_ == callback then 38 | table.remove(autocmd.events[event], i) 39 | break 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | ---Emit autocmd 47 | ---@param event string 48 | autocmd.emit = function(event) 49 | debug.log(' ') 50 | debug.log(string.format('>>> %s', event)) 51 | autocmd.events[event] = autocmd.events[event] or {} 52 | for _, callback in ipairs(autocmd.events[event]) do 53 | callback() 54 | end 55 | end 56 | 57 | ---Resubscribe to events 58 | ---@param events string[] 59 | autocmd.resubscribe = function(events) 60 | -- Delete the autocommands if present 61 | local found = vim.api.nvim_get_autocmds({ 62 | group = autocmd.group, 63 | event = events, 64 | }) 65 | for _, to_delete in ipairs(found) do 66 | vim.api.nvim_del_autocmd(to_delete.id) 67 | end 68 | 69 | -- Recreate if event is known 70 | for _, event in ipairs(events) do 71 | if autocmd.events[event] then 72 | create_autocmd(event) 73 | end 74 | end 75 | end 76 | 77 | return autocmd 78 | -------------------------------------------------------------------------------- /lua/cmp/utils/binary.lua: -------------------------------------------------------------------------------- 1 | local binary = {} 2 | 3 | ---Insert item to list to ordered index 4 | ---@param list any[] 5 | ---@param item any 6 | ---@param func fun(a: any, b: any): 1|-1|0 7 | binary.insort = function(list, item, func) 8 | table.insert(list, binary.search(list, item, func), item) 9 | end 10 | 11 | ---Search suitable index from list 12 | ---@param list any[] 13 | ---@param item any 14 | ---@param func fun(a: any, b: any): 1|-1|0 15 | ---@return integer 16 | binary.search = function(list, item, func) 17 | local s = 1 18 | local e = #list 19 | while s <= e do 20 | local idx = math.floor((e + s) / 2) 21 | local diff = func(item, list[idx]) 22 | if diff > 0 then 23 | s = idx + 1 24 | elseif diff < 0 then 25 | e = idx - 1 26 | else 27 | return idx + 1 28 | end 29 | end 30 | return s 31 | end 32 | 33 | return binary 34 | -------------------------------------------------------------------------------- /lua/cmp/utils/binary_spec.lua: -------------------------------------------------------------------------------- 1 | local binary = require('cmp.utils.binary') 2 | 3 | describe('utils.binary', function() 4 | it('insort', function() 5 | local func = function(a, b) 6 | return a.score - b.score 7 | end 8 | local list = {} 9 | binary.insort(list, { id = 'a', score = 1 }, func) 10 | binary.insort(list, { id = 'b', score = 5 }, func) 11 | binary.insort(list, { id = 'c', score = 2.5 }, func) 12 | binary.insort(list, { id = 'd', score = 2 }, func) 13 | binary.insort(list, { id = 'e', score = 8 }, func) 14 | binary.insort(list, { id = 'g', score = 8 }, func) 15 | binary.insort(list, { id = 'h', score = 7 }, func) 16 | binary.insort(list, { id = 'i', score = 6 }, func) 17 | binary.insort(list, { id = 'j', score = 4 }, func) 18 | assert.are.equal(list[1].id, 'a') 19 | assert.are.equal(list[2].id, 'd') 20 | assert.are.equal(list[3].id, 'c') 21 | assert.are.equal(list[4].id, 'j') 22 | assert.are.equal(list[5].id, 'b') 23 | assert.are.equal(list[6].id, 'i') 24 | assert.are.equal(list[7].id, 'h') 25 | assert.are.equal(list[8].id, 'e') 26 | assert.are.equal(list[9].id, 'g') 27 | end) 28 | end) 29 | -------------------------------------------------------------------------------- /lua/cmp/utils/buffer.lua: -------------------------------------------------------------------------------- 1 | local buffer = {} 2 | 3 | buffer.cache = {} 4 | 5 | ---@return integer buf 6 | buffer.get = function(name) 7 | local buf = buffer.cache[name] 8 | if buf and vim.api.nvim_buf_is_valid(buf) then 9 | return buf 10 | else 11 | return nil 12 | end 13 | end 14 | 15 | ---@return integer buf 16 | ---@return boolean created_new 17 | buffer.ensure = function(name) 18 | local created_new = false 19 | local buf = buffer.get(name) 20 | if not buf then 21 | created_new = true 22 | buf = vim.api.nvim_create_buf(false, true) 23 | buffer.cache[name] = buf 24 | end 25 | return buf, created_new 26 | end 27 | 28 | return buffer 29 | -------------------------------------------------------------------------------- /lua/cmp/utils/cache.lua: -------------------------------------------------------------------------------- 1 | ---@class cmp.Cache 2 | ---@field public entries any 3 | local cache = {} 4 | 5 | cache.new = function() 6 | local self = setmetatable({}, { __index = cache }) 7 | self.entries = {} 8 | return self 9 | end 10 | 11 | ---Get cache value 12 | ---@param key string|string[] 13 | ---@return any|nil 14 | cache.get = function(self, key) 15 | key = self:key(key) 16 | if self.entries[key] ~= nil then 17 | return self.entries[key] 18 | end 19 | return nil 20 | end 21 | 22 | ---Set cache value explicitly 23 | ---@param key string|string[] 24 | ---@vararg any 25 | cache.set = function(self, key, value) 26 | key = self:key(key) 27 | self.entries[key] = value 28 | end 29 | 30 | ---Ensure value by callback 31 | ---@generic T 32 | ---@param key string|string[] 33 | ---@param callback fun(...): T 34 | ---@return T 35 | cache.ensure = function(self, key, callback, ...) 36 | local value = self:get(key) 37 | if value == nil then 38 | local v = callback(...) 39 | self:set(key, v) 40 | return v 41 | end 42 | return value 43 | end 44 | 45 | ---Clear all cache entries 46 | cache.clear = function(self) 47 | self.entries = {} 48 | end 49 | 50 | ---Create key 51 | ---@param key string|string[] 52 | ---@return string 53 | cache.key = function(_, key) 54 | if type(key) == 'table' then 55 | return table.concat(key, ':') 56 | end 57 | return key 58 | end 59 | 60 | return cache 61 | -------------------------------------------------------------------------------- /lua/cmp/utils/char.lua: -------------------------------------------------------------------------------- 1 | local _ 2 | 3 | local alpha = {} 4 | _ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) 5 | alpha[string.byte(char)] = true 6 | end) 7 | 8 | local ALPHA = {} 9 | _ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) 10 | ALPHA[string.byte(char)] = true 11 | end) 12 | 13 | local digit = {} 14 | _ = string.gsub('1234567890', '.', function(char) 15 | digit[string.byte(char)] = true 16 | end) 17 | 18 | local white = {} 19 | _ = string.gsub(' \t\n', '.', function(char) 20 | white[string.byte(char)] = true 21 | end) 22 | 23 | local char = {} 24 | 25 | ---@param byte integer 26 | ---@return boolean 27 | char.is_upper = function(byte) 28 | return ALPHA[byte] 29 | end 30 | 31 | ---@param byte integer 32 | ---@return boolean 33 | char.is_alpha = function(byte) 34 | return alpha[byte] or ALPHA[byte] 35 | end 36 | 37 | ---@param byte integer 38 | ---@return boolean 39 | char.is_digit = function(byte) 40 | return digit[byte] 41 | end 42 | 43 | ---@param byte integer 44 | ---@return boolean 45 | char.is_white = function(byte) 46 | return white[byte] 47 | end 48 | 49 | ---@param byte integer 50 | ---@return boolean 51 | char.is_symbol = function(byte) 52 | return not (char.is_alnum(byte) or char.is_white(byte)) 53 | end 54 | 55 | ---@param byte integer 56 | ---@return boolean 57 | char.is_printable = function(byte) 58 | return string.match(string.char(byte), '^%c$') == nil 59 | end 60 | 61 | ---@param byte integer 62 | ---@return boolean 63 | char.is_alnum = function(byte) 64 | return char.is_alpha(byte) or char.is_digit(byte) 65 | end 66 | 67 | ---@param text string 68 | ---@param index integer 69 | ---@return boolean 70 | char.is_semantic_index = function(text, index) 71 | if index <= 1 then 72 | return true 73 | end 74 | 75 | local prev = string.byte(text, index - 1) 76 | local curr = string.byte(text, index) 77 | 78 | if not char.is_upper(prev) and char.is_upper(curr) then 79 | return true 80 | end 81 | if char.is_symbol(curr) or char.is_white(curr) then 82 | return true 83 | end 84 | if not char.is_alpha(prev) and char.is_alpha(curr) then 85 | return true 86 | end 87 | if not char.is_digit(prev) and char.is_digit(curr) then 88 | return true 89 | end 90 | return false 91 | end 92 | 93 | ---@param text string 94 | ---@param current_index integer 95 | ---@return integer 96 | char.get_next_semantic_index = function(text, current_index) 97 | for i = current_index + 1, #text do 98 | if char.is_semantic_index(text, i) then 99 | return i 100 | end 101 | end 102 | return #text + 1 103 | end 104 | 105 | ---Ignore case match 106 | ---@param byte1 integer 107 | ---@param byte2 integer 108 | ---@return boolean 109 | char.match = function(byte1, byte2) 110 | if not char.is_alpha(byte1) or not char.is_alpha(byte2) then 111 | return byte1 == byte2 112 | end 113 | local diff = byte1 - byte2 114 | return diff == 0 or diff == 32 or diff == -32 115 | end 116 | 117 | return char 118 | -------------------------------------------------------------------------------- /lua/cmp/utils/debug.lua: -------------------------------------------------------------------------------- 1 | local debug = {} 2 | 3 | debug.flag = false 4 | 5 | ---Print log 6 | ---@vararg any 7 | debug.log = function(...) 8 | if debug.flag then 9 | local data = {} 10 | for _, v in ipairs({ ... }) do 11 | if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then 12 | v = vim.inspect(v) 13 | end 14 | table.insert(data, v) 15 | end 16 | print(table.concat(data, '\t')) 17 | end 18 | end 19 | 20 | return debug 21 | -------------------------------------------------------------------------------- /lua/cmp/utils/event.lua: -------------------------------------------------------------------------------- 1 | ---@class cmp.Event 2 | ---@field private events table 3 | local event = {} 4 | 5 | ---Create vents 6 | event.new = function() 7 | local self = setmetatable({}, { __index = event }) 8 | self.events = {} 9 | return self 10 | end 11 | 12 | ---Add event listener 13 | ---@param name string 14 | ---@param callback function 15 | ---@return function 16 | event.on = function(self, name, callback) 17 | if not self.events[name] then 18 | self.events[name] = {} 19 | end 20 | table.insert(self.events[name], callback) 21 | return function() 22 | self:off(name, callback) 23 | end 24 | end 25 | 26 | ---Remove event listener 27 | ---@param name string 28 | ---@param callback function 29 | event.off = function(self, name, callback) 30 | for i, callback_ in ipairs(self.events[name] or {}) do 31 | if callback_ == callback then 32 | table.remove(self.events[name], i) 33 | break 34 | end 35 | end 36 | end 37 | 38 | ---Remove all events 39 | event.clear = function(self) 40 | self.events = {} 41 | end 42 | 43 | ---Emit event 44 | ---@param name string 45 | event.emit = function(self, name, ...) 46 | for _, callback in ipairs(self.events[name] or {}) do 47 | if type(callback) == 'function' then 48 | callback(...) 49 | end 50 | end 51 | end 52 | 53 | return event 54 | -------------------------------------------------------------------------------- /lua/cmp/utils/feedkeys.lua: -------------------------------------------------------------------------------- 1 | local keymap = require('cmp.utils.keymap') 2 | local misc = require('cmp.utils.misc') 3 | 4 | local feedkeys = {} 5 | 6 | feedkeys.call = setmetatable({ 7 | callbacks = {}, 8 | }, { 9 | __call = function(self, keys, mode, callback) 10 | local is_insert = string.match(mode, 'i') ~= nil 11 | local is_immediate = string.match(mode, 'x') ~= nil 12 | 13 | local queue = {} 14 | if #keys > 0 then 15 | table.insert(queue, { keymap.t('setlocal lazyredraw'), 'n' }) 16 | table.insert(queue, { keymap.t('setlocal textwidth=0'), 'n' }) 17 | table.insert(queue, { keymap.t('setlocal backspace=nostop'), 'n' }) 18 | table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true }) 19 | table.insert(queue, { keymap.t('setlocal %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) 20 | table.insert(queue, { keymap.t('setlocal textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) 21 | table.insert(queue, { keymap.t('setlocal backspace=%s'):format(vim.go.backspace or 2), 'n' }) 22 | end 23 | 24 | if callback then 25 | local id = misc.id('cmp.utils.feedkeys.call') 26 | self.callbacks[id] = callback 27 | table.insert(queue, { keymap.t('lua require"cmp.utils.feedkeys".run(%s)'):format(id), 'n', true }) 28 | end 29 | 30 | if is_insert then 31 | for i = #queue, 1, -1 do 32 | vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3]) 33 | end 34 | else 35 | for i = 1, #queue do 36 | vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3]) 37 | end 38 | end 39 | 40 | if is_immediate then 41 | vim.api.nvim_feedkeys('', 'x', true) 42 | end 43 | end, 44 | }) 45 | feedkeys.run = function(id) 46 | if feedkeys.call.callbacks[id] then 47 | local ok, err = pcall(feedkeys.call.callbacks[id]) 48 | if not ok then 49 | vim.notify(err, vim.log.levels.ERROR) 50 | end 51 | feedkeys.call.callbacks[id] = nil 52 | end 53 | return '' 54 | end 55 | 56 | return feedkeys 57 | -------------------------------------------------------------------------------- /lua/cmp/utils/feedkeys_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local keymap = require('cmp.utils.keymap') 3 | 4 | local feedkeys = require('cmp.utils.feedkeys') 5 | 6 | describe('feedkeys', function() 7 | before_each(spec.before) 8 | 9 | it('dot-repeat', function() 10 | local reg 11 | feedkeys.call(keymap.t('iaiueo'), 'nx', function() 12 | reg = vim.fn.getreg('.') 13 | end) 14 | assert.are.equal(reg, keymap.t('aiueo')) 15 | end) 16 | 17 | it('textwidth', function() 18 | vim.cmd([[setlocal textwidth=6]]) 19 | feedkeys.call(keymap.t('iaiueo '), 'nx') 20 | feedkeys.call(keymap.t('aaiueoaiueo'), 'nx') 21 | assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { 22 | 'aiueo aiueoaiueo', 23 | }) 24 | end) 25 | 26 | it('backspace', function() 27 | vim.cmd([[setlocal backspace=""]]) 28 | feedkeys.call(keymap.t('iaiueo'), 'nx') 29 | feedkeys.call(keymap.t('a'), 'nx') 30 | assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { 31 | 'aiu', 32 | }) 33 | end) 34 | 35 | it('testability', function() 36 | feedkeys.call('i', 'n', function() 37 | feedkeys.call('', 'n', function() 38 | feedkeys.call('aiueo', 'in') 39 | end) 40 | feedkeys.call('', 'n', function() 41 | feedkeys.call(keymap.t(''), 'in') 42 | end) 43 | feedkeys.call('', 'n', function() 44 | feedkeys.call(keymap.t('abcde'), 'in') 45 | end) 46 | feedkeys.call('', 'n', function() 47 | feedkeys.call(keymap.t(''), 'in') 48 | end) 49 | feedkeys.call('', 'n', function() 50 | feedkeys.call(keymap.t('12345'), 'in') 51 | end) 52 | end) 53 | feedkeys.call('', 'x') 54 | assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { '12345' }) 55 | end) 56 | end) 57 | -------------------------------------------------------------------------------- /lua/cmp/utils/highlight.lua: -------------------------------------------------------------------------------- 1 | local highlight = {} 2 | 3 | highlight.keys = { 4 | 'fg', 5 | 'bg', 6 | 'bold', 7 | 'italic', 8 | 'reverse', 9 | 'standout', 10 | 'underline', 11 | 'undercurl', 12 | 'strikethrough', 13 | } 14 | 15 | highlight.inherit = function(name, source, settings) 16 | for _, key in ipairs(highlight.keys) do 17 | if not settings[key] then 18 | local v = vim.fn.synIDattr(vim.fn.hlID(source), key) 19 | if key == 'fg' or key == 'bg' then 20 | local n = tonumber(v, 10) 21 | v = type(n) == 'number' and n or v 22 | else 23 | v = v == 1 24 | end 25 | settings[key] = v == '' and 'NONE' or v 26 | end 27 | end 28 | vim.api.nvim_set_hl(0, name, settings) 29 | end 30 | 31 | return highlight 32 | -------------------------------------------------------------------------------- /lua/cmp/utils/keymap.lua: -------------------------------------------------------------------------------- 1 | local misc = require('cmp.utils.misc') 2 | local buffer = require('cmp.utils.buffer') 3 | local api = require('cmp.utils.api') 4 | 5 | local keymap = {} 6 | 7 | ---Shortcut for nvim_replace_termcodes 8 | ---@param keys string 9 | ---@return string 10 | keymap.t = function(keys) 11 | return (string.gsub(keys, "(<[A-Za-z0-9\\%-%[%]%^@;,:_'`%./]->)", function(match) 12 | return vim.api.nvim_eval(string.format([["\%s"]], match)) 13 | end)) 14 | end 15 | 16 | ---Normalize key sequence. 17 | ---@param keys string 18 | ---@return string 19 | keymap.normalize = vim.fn.has('nvim-0.8') == 1 and function(keys) 20 | local t = string.gsub(keys, "<([A-Za-z0-9\\%-%[%]%^@;,:_'`%./]-)>", function(match) 21 | -- Use the \<* notation, which distinguishes from , etc. 22 | return vim.api.nvim_eval(string.format([["\<*%s>"]], match)) 23 | end) 24 | return vim.fn.keytrans(t) 25 | end or function(keys) 26 | local normalize_buf = buffer.ensure('cmp.util.keymap.normalize') 27 | vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '(cmp.utils.keymap.normalize)', {}) 28 | for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do 29 | if keymap.t(map.rhs) == keymap.t('(cmp.utils.keymap.normalize)') then 30 | vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) 31 | return map.lhs 32 | end 33 | end 34 | vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys) 35 | vim.api.nvim_buf_delete(normalize_buf, {}) 36 | return keys 37 | end 38 | 39 | ---Return vim notation keymapping (simple conversion). 40 | ---@param s string 41 | ---@return string 42 | keymap.to_keymap = setmetatable({ 43 | [''] = { '\n', '\r', '\r\n' }, 44 | [''] = { '\t' }, 45 | [''] = { '\\' }, 46 | [''] = { '|' }, 47 | [''] = { ' ' }, 48 | }, { 49 | __call = function(self, s) 50 | return string.gsub(s, '.', function(c) 51 | for key, chars in pairs(self) do 52 | if vim.tbl_contains(chars, c) then 53 | return key 54 | end 55 | end 56 | return c 57 | end) 58 | end, 59 | }) 60 | 61 | ---Mode safe break undo 62 | keymap.undobreak = function() 63 | if not api.is_insert_mode() then 64 | return '' 65 | end 66 | return keymap.t('u') 67 | end 68 | 69 | ---Mode safe join undo 70 | keymap.undojoin = function() 71 | if not api.is_insert_mode() then 72 | return '' 73 | end 74 | return keymap.t('U') 75 | end 76 | 77 | ---Create backspace keys. 78 | ---@param count string|integer 79 | ---@return string 80 | keymap.backspace = function(count) 81 | if type(count) == 'string' then 82 | count = vim.fn.strchars(count, true) 83 | end 84 | if count <= 0 then 85 | return '' 86 | end 87 | local keys = {} 88 | table.insert(keys, keymap.t(string.rep('', count))) 89 | return table.concat(keys, '') 90 | end 91 | 92 | ---Create delete keys. 93 | ---@param count string|integer 94 | ---@return string 95 | keymap.delete = function(count) 96 | if type(count) == 'string' then 97 | count = vim.fn.strchars(count, true) 98 | end 99 | if count <= 0 then 100 | return '' 101 | end 102 | local keys = {} 103 | table.insert(keys, keymap.t(string.rep('', count))) 104 | return table.concat(keys, '') 105 | end 106 | 107 | ---Update indentkeys. 108 | ---@param expr? string 109 | ---@return string 110 | keymap.indentkeys = function(expr) 111 | return string.format(keymap.t('set indentkeys=%s'), expr and vim.fn.escape(expr, '| \t\\') or '') 112 | end 113 | 114 | ---Return two key sequence are equal or not. 115 | ---@param a string 116 | ---@param b string 117 | ---@return boolean 118 | keymap.equals = function(a, b) 119 | return keymap.normalize(a) == keymap.normalize(b) 120 | end 121 | 122 | ---Register keypress handler. 123 | keymap.listen = function(mode, lhs, callback) 124 | lhs = keymap.normalize(keymap.to_keymap(lhs)) 125 | 126 | local existing = keymap.get_map(mode, lhs) 127 | if existing.desc == 'cmp.utils.keymap.set_map' then 128 | return 129 | end 130 | 131 | local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1 132 | local fallback = keymap.fallback(bufnr, mode, existing) 133 | keymap.set_map(bufnr, mode, lhs, function() 134 | local ignore = false 135 | ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=') 136 | if ignore then 137 | fallback() 138 | else 139 | callback(lhs, misc.once(fallback)) 140 | end 141 | end, { 142 | expr = false, 143 | noremap = true, 144 | silent = true, 145 | }) 146 | end 147 | 148 | ---Fallback 149 | keymap.fallback = function(bufnr, mode, map) 150 | return function() 151 | if map.expr then 152 | local fallback_lhs = string.format('(cmp.u.k.fallback_expr:%s)', map.lhs) 153 | keymap.set_map(bufnr, mode, fallback_lhs, function() 154 | return keymap.solve(bufnr, mode, map).keys 155 | end, { 156 | expr = true, 157 | noremap = map.noremap, 158 | script = map.script, 159 | nowait = map.nowait, 160 | silent = map.silent and mode ~= 'c', 161 | replace_keycodes = map.replace_keycodes, 162 | }) 163 | vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true) 164 | elseif map.callback then 165 | map.callback() 166 | else 167 | local solved = keymap.solve(bufnr, mode, map) 168 | vim.api.nvim_feedkeys(solved.keys, solved.mode, true) 169 | end 170 | end 171 | end 172 | 173 | ---Solve 174 | keymap.solve = function(bufnr, mode, map) 175 | local lhs = keymap.t(map.lhs) 176 | local rhs = keymap.t(map.rhs) 177 | if map.expr then 178 | if map.callback then 179 | rhs = map.callback() 180 | else 181 | rhs = vim.api.nvim_eval(keymap.t(map.rhs)) 182 | end 183 | end 184 | 185 | if map.noremap then 186 | return { keys = rhs, mode = 'in' } 187 | end 188 | 189 | if string.find(rhs, lhs, 1, true) == 1 then 190 | local recursive = string.format('0_(cmp.u.k.recursive:%s)', lhs) 191 | keymap.set_map(bufnr, mode, recursive, lhs, { 192 | noremap = true, 193 | script = true, 194 | nowait = map.nowait, 195 | silent = map.silent and mode ~= 'c', 196 | replace_keycodes = map.replace_keycodes, 197 | }) 198 | return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' } 199 | end 200 | return { keys = rhs, mode = 'im' } 201 | end 202 | 203 | ---Get map 204 | ---@param mode string 205 | ---@param lhs string 206 | ---@return table 207 | keymap.get_map = function(mode, lhs) 208 | lhs = keymap.normalize(lhs) 209 | 210 | for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do 211 | if keymap.equals(map.lhs, lhs) then 212 | return { 213 | lhs = map.lhs, 214 | rhs = map.rhs or '', 215 | expr = map.expr == 1, 216 | callback = map.callback, 217 | desc = map.desc, 218 | noremap = map.noremap == 1, 219 | script = map.script == 1, 220 | silent = map.silent == 1, 221 | nowait = map.nowait == 1, 222 | buffer = true, 223 | replace_keycodes = map.replace_keycodes == 1, 224 | } 225 | end 226 | end 227 | 228 | for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do 229 | if keymap.equals(map.lhs, lhs) then 230 | return { 231 | lhs = map.lhs, 232 | rhs = map.rhs or '', 233 | expr = map.expr == 1, 234 | callback = map.callback, 235 | desc = map.desc, 236 | noremap = map.noremap == 1, 237 | script = map.script == 1, 238 | silent = map.silent == 1, 239 | nowait = map.nowait == 1, 240 | buffer = false, 241 | replace_keycodes = map.replace_keycodes == 1, 242 | } 243 | end 244 | end 245 | 246 | return { 247 | lhs = lhs, 248 | rhs = lhs, 249 | expr = false, 250 | callback = nil, 251 | noremap = true, 252 | script = false, 253 | silent = true, 254 | nowait = false, 255 | buffer = false, 256 | replace_keycodes = true, 257 | } 258 | end 259 | 260 | ---Set keymapping 261 | keymap.set_map = function(bufnr, mode, lhs, rhs, opts) 262 | if type(rhs) == 'function' then 263 | opts.callback = rhs 264 | rhs = '' 265 | end 266 | opts.desc = 'cmp.utils.keymap.set_map' 267 | 268 | if vim.fn.has('nvim-0.8') == 0 then 269 | opts.replace_keycodes = nil 270 | end 271 | 272 | if bufnr == -1 then 273 | vim.api.nvim_set_keymap(mode, lhs, rhs, opts) 274 | else 275 | vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) 276 | end 277 | end 278 | 279 | return keymap 280 | -------------------------------------------------------------------------------- /lua/cmp/utils/keymap_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | local api = require('cmp.utils.api') 3 | local feedkeys = require('cmp.utils.feedkeys') 4 | 5 | local keymap = require('cmp.utils.keymap') 6 | 7 | describe('keymap', function() 8 | before_each(spec.before) 9 | 10 | it('t', function() 11 | for _, key in ipairs({ 12 | '', 13 | '', 14 | '', 15 | '', 16 | '', 17 | '', 18 | '', 19 | '', 20 | '', 21 | '', 22 | '', 23 | '', 24 | '', 25 | "", 26 | '', 27 | '', 28 | '', 29 | '(example)', 30 | '="abc"', 31 | 'normal! ==', 32 | }) do 33 | assert.are.equal(keymap.t(key), vim.api.nvim_replace_termcodes(key, true, true, true)) 34 | assert.are.equal(keymap.t(key .. key), vim.api.nvim_replace_termcodes(key .. key, true, true, true)) 35 | assert.are.equal(keymap.t(key .. key .. key), vim.api.nvim_replace_termcodes(key .. key .. key, true, true, true)) 36 | end 37 | end) 38 | 39 | it('to_keymap', function() 40 | assert.are.equal(keymap.to_keymap('\n'), '') 41 | assert.are.equal(keymap.to_keymap(''), '') 42 | assert.are.equal(keymap.to_keymap('|'), '') 43 | end) 44 | 45 | describe('fallback', function() 46 | before_each(spec.before) 47 | 48 | local run_fallback = function(keys, fallback) 49 | local state = {} 50 | feedkeys.call(keys, '', function() 51 | fallback() 52 | end) 53 | feedkeys.call('', '', function() 54 | if api.is_cmdline_mode() then 55 | state.buffer = { api.get_current_line() } 56 | else 57 | state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) 58 | end 59 | state.cursor = api.get_cursor() 60 | end) 61 | feedkeys.call('', 'x') 62 | return state 63 | end 64 | 65 | describe('basic', function() 66 | it('', function() 67 | vim.api.nvim_buf_set_keymap(0, 'i', '(pairs)', '()', { noremap = true }) 68 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '(pairs)', { noremap = false }) 69 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 70 | local state = run_fallback('i', fallback) 71 | assert.are.same({ '()' }, state.buffer) 72 | assert.are.same({ 1, 1 }, state.cursor) 73 | end) 74 | 75 | it('=', function() 76 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '="()"', {}) 77 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 78 | local state = run_fallback('i', fallback) 79 | assert.are.same({ '()' }, state.buffer) 80 | assert.are.same({ 1, 1 }, state.cursor) 81 | end) 82 | 83 | it('callback', function() 84 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { 85 | callback = function() 86 | vim.api.nvim_feedkeys('()' .. keymap.t(''), 'int', true) 87 | end, 88 | }) 89 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 90 | local state = run_fallback('i', fallback) 91 | assert.are.same({ '()' }, state.buffer) 92 | assert.are.same({ 1, 1 }, state.cursor) 93 | end) 94 | 95 | it('expr-callback', function() 96 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { 97 | expr = true, 98 | noremap = false, 99 | silent = true, 100 | callback = function() 101 | return '()' .. keymap.t('') 102 | end, 103 | }) 104 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 105 | local state = run_fallback('i', fallback) 106 | assert.are.same({ '()' }, state.buffer) 107 | assert.are.same({ 1, 1 }, state.cursor) 108 | end) 109 | 110 | -- it('cmdline default ', function() 111 | -- local fallback = keymap.fallback(0, 'c', keymap.get_map('c', '')) 112 | -- local state = run_fallback(':', fallback) 113 | -- assert.are.same({ '' }, state.buffer) 114 | -- assert.are.same({ 1, 0 }, state.cursor) 115 | -- end) 116 | end) 117 | 118 | describe('recursive', function() 119 | it('non-expr', function() 120 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '()', { 121 | expr = false, 122 | noremap = false, 123 | silent = true, 124 | }) 125 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 126 | local state = run_fallback('i', fallback) 127 | assert.are.same({ '()' }, state.buffer) 128 | assert.are.same({ 1, 1 }, state.cursor) 129 | end) 130 | 131 | it('expr', function() 132 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '"()"', { 133 | expr = true, 134 | noremap = false, 135 | silent = true, 136 | }) 137 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 138 | local state = run_fallback('i', fallback) 139 | assert.are.same({ '()' }, state.buffer) 140 | assert.are.same({ 1, 1 }, state.cursor) 141 | end) 142 | 143 | it('expr-callback', function() 144 | pcall(function() 145 | vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { 146 | expr = true, 147 | noremap = false, 148 | silent = true, 149 | callback = function() 150 | return keymap.t('()') 151 | end, 152 | }) 153 | local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) 154 | local state = run_fallback('i', fallback) 155 | assert.are.same({ '()' }, state.buffer) 156 | assert.are.same({ 1, 1 }, state.cursor) 157 | end) 158 | end) 159 | end) 160 | end) 161 | 162 | describe('realworld', function() 163 | before_each(spec.before) 164 | 165 | it('#226', function() 166 | keymap.listen('i', '', function(_, fallback) 167 | fallback() 168 | end) 169 | vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) 170 | assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) 171 | end) 172 | 173 | it('#414', function() 174 | keymap.listen('i', '', function() 175 | vim.api.nvim_feedkeys(keymap.t(''), 'int', true) 176 | end) 177 | vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) 178 | assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) 179 | end) 180 | 181 | it('#744', function() 182 | vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { 183 | noremap = true, 184 | }) 185 | vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { 186 | noremap = false, 187 | }) 188 | keymap.listen('i', '', function(_, fallback) 189 | fallback() 190 | end) 191 | feedkeys.call(keymap.t('i'), 'tx') 192 | assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) 193 | end) 194 | end) 195 | end) 196 | -------------------------------------------------------------------------------- /lua/cmp/utils/misc.lua: -------------------------------------------------------------------------------- 1 | local misc = {} 2 | 3 | local islist = vim.islist or vim.tbl_islist 4 | 5 | ---Create once callback 6 | ---@param callback function 7 | ---@return function 8 | misc.once = function(callback) 9 | local done = false 10 | return function(...) 11 | if done then 12 | return 13 | end 14 | done = true 15 | callback(...) 16 | end 17 | end 18 | 19 | ---Return concatenated list 20 | ---@param list1 any[] 21 | ---@param list2 any[] 22 | ---@return any[] 23 | misc.concat = function(list1, list2) 24 | local new_list = {} 25 | for _, v in ipairs(list1) do 26 | table.insert(new_list, v) 27 | end 28 | for _, v in ipairs(list2) do 29 | table.insert(new_list, v) 30 | end 31 | return new_list 32 | end 33 | 34 | ---Repeat values 35 | ---@generic T 36 | ---@param str_or_tbl T 37 | ---@param count integer 38 | ---@return T 39 | misc.rep = function(str_or_tbl, count) 40 | if type(str_or_tbl) == 'string' then 41 | return string.rep(str_or_tbl, count) 42 | end 43 | local rep = {} 44 | for _ = 1, count do 45 | for _, v in ipairs(str_or_tbl) do 46 | table.insert(rep, v) 47 | end 48 | end 49 | return rep 50 | end 51 | 52 | ---Return whether the value is empty or not. 53 | ---@param v any 54 | ---@return boolean 55 | misc.empty = function(v) 56 | if not v then 57 | return true 58 | end 59 | if v == vim.NIL then 60 | return true 61 | end 62 | if type(v) == 'string' and v == '' then 63 | return true 64 | end 65 | if type(v) == 'table' and vim.tbl_isempty(v) then 66 | return true 67 | end 68 | if type(v) == 'number' and v == 0 then 69 | return true 70 | end 71 | return false 72 | end 73 | 74 | ---Search value in table 75 | misc.contains = function(tbl, v) 76 | for _, value in ipairs(tbl) do 77 | if value == v then 78 | return true 79 | end 80 | end 81 | return false 82 | end 83 | 84 | ---The symbol to remove key in misc.merge. 85 | misc.none = vim.NIL 86 | 87 | ---Merge two tables recursively 88 | ---@generic T 89 | ---@param tbl1 T 90 | ---@param tbl2 T 91 | ---@return T 92 | misc.merge = function(tbl1, tbl2) 93 | local is_dict1 = type(tbl1) == 'table' and (not islist(tbl1) or vim.tbl_isempty(tbl1)) 94 | local is_dict2 = type(tbl2) == 'table' and (not islist(tbl2) or vim.tbl_isempty(tbl2)) 95 | if is_dict1 and is_dict2 then 96 | local new_tbl = {} 97 | for k, v in pairs(tbl2) do 98 | if tbl1[k] ~= misc.none then 99 | new_tbl[k] = misc.merge(tbl1[k], v) 100 | end 101 | end 102 | for k, v in pairs(tbl1) do 103 | if tbl2[k] == nil then 104 | if v ~= misc.none then 105 | new_tbl[k] = misc.merge(v, {}) 106 | else 107 | new_tbl[k] = nil 108 | end 109 | end 110 | end 111 | return new_tbl 112 | end 113 | 114 | if tbl1 == misc.none then 115 | return nil 116 | elseif tbl1 == nil then 117 | return misc.merge(tbl2, {}) 118 | else 119 | return tbl1 120 | end 121 | end 122 | 123 | ---Generate id for group name 124 | misc.id = setmetatable({ 125 | group = {}, 126 | }, { 127 | __call = function(_, group) 128 | misc.id.group[group] = misc.id.group[group] or 0 129 | misc.id.group[group] = misc.id.group[group] + 1 130 | return misc.id.group[group] 131 | end, 132 | }) 133 | 134 | ---Treat 1/0 as bool value 135 | ---@param v boolean|1|0 136 | ---@param def boolean 137 | ---@return boolean 138 | misc.bool = function(v, def) 139 | if v == nil then 140 | return def 141 | end 142 | return v == true or v == 1 143 | end 144 | 145 | ---Set value to deep object 146 | ---@param t table 147 | ---@param keys string[] 148 | ---@param v any 149 | misc.set = function(t, keys, v) 150 | local c = t 151 | for i = 1, #keys - 1 do 152 | local key = keys[i] 153 | c[key] = c[key] or {} 154 | c = c[key] 155 | end 156 | c[keys[#keys]] = v 157 | end 158 | 159 | do 160 | local function do_copy(tbl, seen) 161 | if type(tbl) ~= 'table' then 162 | return tbl 163 | end 164 | if seen[tbl] then 165 | return seen[tbl] 166 | end 167 | 168 | if islist(tbl) then 169 | local copy = {} 170 | seen[tbl] = copy 171 | for i, value in ipairs(tbl) do 172 | copy[i] = do_copy(value, seen) 173 | end 174 | return copy 175 | end 176 | 177 | local copy = {} 178 | seen[tbl] = copy 179 | for key, value in pairs(tbl) do 180 | copy[key] = do_copy(value, seen) 181 | end 182 | return copy 183 | end 184 | 185 | ---Copy table 186 | ---@generic T 187 | ---@param tbl T 188 | ---@return T 189 | misc.copy = function(tbl) 190 | return do_copy(tbl, {}) 191 | end 192 | end 193 | 194 | ---Safe version of vim.str_utfindex 195 | ---@param text string 196 | ---@param vimindex integer|nil 197 | ---@return integer 198 | misc.to_utfindex = function(text, vimindex) 199 | vimindex = vimindex or #text + 1 200 | if vim.fn.has('nvim-0.11') == 1 then 201 | return vim.str_utfindex(text, 'utf-16', math.max(0, math.min(vimindex - 1, #text))) 202 | end 203 | return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) 204 | end 205 | 206 | ---Safe version of vim.str_byteindex 207 | ---@param text string 208 | ---@param utfindex integer 209 | ---@return integer 210 | misc.to_vimindex = function(text, utfindex) 211 | utfindex = utfindex or #text 212 | for i = utfindex, 1, -1 do 213 | local s, v = pcall(function() 214 | return vim.str_byteindex(text, 'utf-16', i) + 1 215 | end) 216 | if s then 217 | return v 218 | end 219 | end 220 | return utfindex + 1 221 | end 222 | 223 | ---Mark the function as deprecated 224 | misc.deprecated = function(fn, msg) 225 | local printed = false 226 | return function(...) 227 | if not printed then 228 | print(msg) 229 | printed = true 230 | end 231 | return fn(...) 232 | end 233 | end 234 | 235 | --Redraw 236 | misc.redraw = setmetatable({ 237 | doing = false, 238 | force = false, 239 | -- We use `` to redraw the screen. (Previously, We use . it will remove the unmatches search history.) 240 | incsearch_redraw_keys = ' ', 241 | }, { 242 | __call = function(self, force) 243 | local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true) 244 | if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then 245 | if vim.o.incsearch then 246 | return vim.api.nvim_feedkeys(termcode, 'ni', true) 247 | end 248 | end 249 | 250 | if self.doing then 251 | return 252 | end 253 | self.doing = true 254 | self.force = not not force 255 | vim.schedule(function() 256 | if self.force then 257 | vim.cmd([[redraw!]]) 258 | else 259 | vim.cmd([[redraw]]) 260 | end 261 | self.doing = false 262 | self.force = false 263 | end) 264 | end, 265 | }) 266 | 267 | return misc 268 | -------------------------------------------------------------------------------- /lua/cmp/utils/misc_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require('cmp.utils.spec') 2 | 3 | local misc = require('cmp.utils.misc') 4 | 5 | describe('misc', function() 6 | before_each(spec.before) 7 | 8 | it('copy', function() 9 | -- basic. 10 | local tbl, copy 11 | tbl = { 12 | a = { 13 | b = 1, 14 | }, 15 | } 16 | copy = misc.copy(tbl) 17 | assert.are_not.equal(tbl, copy) 18 | assert.are_not.equal(tbl.a, copy.a) 19 | assert.are.same(tbl, copy) 20 | 21 | -- self reference. 22 | tbl = { 23 | a = { 24 | b = 1, 25 | }, 26 | } 27 | tbl.a.c = tbl.a 28 | copy = misc.copy(tbl) 29 | assert.are_not.equal(tbl, copy) 30 | assert.are_not.equal(tbl.a, copy.a) 31 | assert.are_not.equal(tbl.a.c, copy.a.c) 32 | assert.are.same(tbl, copy) 33 | end) 34 | 35 | it('merge', function() 36 | local merged 37 | merged = misc.merge({ 38 | a = {}, 39 | }, { 40 | a = { 41 | b = 1, 42 | }, 43 | }) 44 | assert.are.equal(merged.a.b, 1) 45 | 46 | merged = misc.merge({ 47 | a = { 48 | i = 1, 49 | }, 50 | }, { 51 | a = { 52 | c = 2, 53 | }, 54 | }) 55 | assert.are.equal(merged.a.i, 1) 56 | assert.are.equal(merged.a.c, 2) 57 | 58 | merged = misc.merge({ 59 | a = false, 60 | }, { 61 | a = { 62 | b = 1, 63 | }, 64 | }) 65 | assert.are.equal(merged.a, false) 66 | 67 | merged = misc.merge({ 68 | a = misc.none, 69 | }, { 70 | a = { 71 | b = 1, 72 | }, 73 | }) 74 | assert.are.equal(merged.a, nil) 75 | 76 | merged = misc.merge({ 77 | a = misc.none, 78 | }, { 79 | a = nil, 80 | }) 81 | assert.are.equal(merged.a, nil) 82 | 83 | merged = misc.merge({ 84 | a = nil, 85 | }, { 86 | a = misc.none, 87 | }) 88 | assert.are.equal(merged.a, nil) 89 | end) 90 | end) 91 | -------------------------------------------------------------------------------- /lua/cmp/utils/options.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Set window option without triggering the OptionSet event 4 | ---@param window number 5 | ---@param name string 6 | ---@param value any 7 | M.win_set_option = function(window, name, value) 8 | local eventignore = vim.opt.eventignore:get() 9 | vim.opt.eventignore:append('OptionSet') 10 | vim.api.nvim_win_set_option(window, name, value) 11 | vim.opt.eventignore = eventignore 12 | end 13 | 14 | -- Set buffer option without triggering the OptionSet event 15 | ---@param buffer number 16 | ---@param name string 17 | ---@param value any 18 | M.buf_set_option = function(buffer, name, value) 19 | local eventignore = vim.opt.eventignore:get() 20 | vim.opt.eventignore:append('OptionSet') 21 | vim.api.nvim_buf_set_option(buffer, name, value) 22 | vim.opt.eventignore = eventignore 23 | end 24 | 25 | return M 26 | -------------------------------------------------------------------------------- /lua/cmp/utils/pattern.lua: -------------------------------------------------------------------------------- 1 | local pattern = {} 2 | 3 | pattern._regexes = {} 4 | 5 | pattern.regex = function(p) 6 | if not pattern._regexes[p] then 7 | pattern._regexes[p] = vim.regex(p) 8 | end 9 | return pattern._regexes[p] 10 | end 11 | 12 | pattern.offset = function(p, text) 13 | local s, e = pattern.regex(p):match_str(text) 14 | if s then 15 | return s + 1, e + 1 16 | end 17 | return nil, nil 18 | end 19 | 20 | pattern.matchstr = function(p, text) 21 | local s, e = pattern.offset(p, text) 22 | if s then 23 | return string.sub(text, s, e) 24 | end 25 | return nil 26 | end 27 | 28 | return pattern 29 | -------------------------------------------------------------------------------- /lua/cmp/utils/spec.lua: -------------------------------------------------------------------------------- 1 | local context = require('cmp.context') 2 | local source = require('cmp.source') 3 | local types = require('cmp.types') 4 | local config = require('cmp.config') 5 | 6 | local spec = {} 7 | 8 | spec.before = function() 9 | vim.cmd([[ 10 | bdelete! 11 | enew! 12 | imapclear 13 | imapclear 14 | cmapclear 15 | cmapclear 16 | smapclear 17 | smapclear 18 | xmapclear 19 | xmapclear 20 | tmapclear 21 | tmapclear 22 | setlocal noswapfile 23 | setlocal virtualedit=all 24 | setlocal completeopt=menu,menuone,noselect 25 | ]]) 26 | config.set_global({ 27 | sources = { 28 | { name = 'spec' }, 29 | }, 30 | snippet = { 31 | expand = function(args) 32 | local ctx = context.new() 33 | vim.api.nvim_buf_set_text(ctx.bufnr, ctx.cursor.row - 1, ctx.cursor.col - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, vim.split(string.gsub(args.body, '%$0', ''), '\n')) 34 | for i, t in ipairs(vim.split(args.body, '\n')) do 35 | local s = string.find(t, '$0', 1, true) 36 | if s then 37 | if i == 1 then 38 | vim.api.nvim_win_set_cursor(0, { ctx.cursor.row, ctx.cursor.col + s - 2 }) 39 | else 40 | vim.api.nvim_win_set_cursor(0, { ctx.cursor.row + i - 1, s - 1 }) 41 | end 42 | break 43 | end 44 | end 45 | end, 46 | }, 47 | }) 48 | config.set_cmdline({ 49 | sources = { 50 | { name = 'spec' }, 51 | }, 52 | }, ':') 53 | end 54 | 55 | spec.state = function(text, row, col) 56 | vim.fn.setline(1, text) 57 | vim.fn.cursor(row, col) 58 | local ctx = context.empty() 59 | local s = source.new('spec', { 60 | complete = function() end, 61 | }) 62 | return { 63 | context = function() 64 | return ctx 65 | end, 66 | source = function() 67 | return s 68 | end, 69 | backspace = function() 70 | vim.fn.feedkeys('x', 'nx') 71 | vim.fn.feedkeys('h', 'nx') 72 | ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) 73 | s:complete(ctx, function() end) 74 | return ctx 75 | end, 76 | input = function(char) 77 | vim.fn.feedkeys(('i%s'):format(char), 'nx') 78 | vim.fn.feedkeys(string.rep('l', #char), 'nx') 79 | ctx.prev_context = nil 80 | ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) 81 | s:complete(ctx, function() end) 82 | return ctx 83 | end, 84 | manual = function() 85 | ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual }) 86 | s:complete(ctx, function() end) 87 | return ctx 88 | end, 89 | } 90 | end 91 | 92 | return spec 93 | -------------------------------------------------------------------------------- /lua/cmp/utils/str.lua: -------------------------------------------------------------------------------- 1 | local char = require('cmp.utils.char') 2 | 3 | local str = {} 4 | 5 | local INVALIDS = {} 6 | INVALIDS[string.byte("'")] = true 7 | INVALIDS[string.byte('"')] = true 8 | INVALIDS[string.byte('=')] = true 9 | INVALIDS[string.byte('$')] = true 10 | INVALIDS[string.byte('(')] = true 11 | INVALIDS[string.byte('[')] = true 12 | INVALIDS[string.byte('<')] = true 13 | INVALIDS[string.byte('{')] = true 14 | INVALIDS[string.byte(' ')] = true 15 | INVALIDS[string.byte('\t')] = true 16 | INVALIDS[string.byte('\n')] = true 17 | INVALIDS[string.byte('\r')] = true 18 | 19 | local NR_BYTE = string.byte('\n') 20 | 21 | local PAIRS = {} 22 | PAIRS[string.byte('<')] = string.byte('>') 23 | PAIRS[string.byte('[')] = string.byte(']') 24 | PAIRS[string.byte('(')] = string.byte(')') 25 | PAIRS[string.byte('{')] = string.byte('}') 26 | PAIRS[string.byte('"')] = string.byte('"') 27 | PAIRS[string.byte("'")] = string.byte("'") 28 | 29 | ---Return if specified text has prefix or not 30 | ---@param text string 31 | ---@param prefix string 32 | ---@return boolean 33 | str.has_prefix = function(text, prefix) 34 | if #text < #prefix then 35 | return false 36 | end 37 | for i = 1, #prefix do 38 | if not char.match(string.byte(text, i), string.byte(prefix, i)) then 39 | return false 40 | end 41 | end 42 | return true 43 | end 44 | 45 | ---get_common_string 46 | str.get_common_string = function(text1, text2) 47 | local min = math.min(#text1, #text2) 48 | for i = 1, min do 49 | if not char.match(string.byte(text1, i), string.byte(text2, i)) then 50 | return string.sub(text1, 1, i - 1) 51 | end 52 | end 53 | return string.sub(text1, 1, min) 54 | end 55 | 56 | ---Remove suffix 57 | ---@param text string 58 | ---@param suffix string 59 | ---@return string 60 | str.remove_suffix = function(text, suffix) 61 | if #text < #suffix then 62 | return text 63 | end 64 | 65 | local i = 0 66 | while i < #suffix do 67 | if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then 68 | return text 69 | end 70 | i = i + 1 71 | end 72 | return string.sub(text, 1, -#suffix - 1) 73 | end 74 | 75 | ---trim 76 | ---@param text string 77 | ---@return string 78 | str.trim = function(text) 79 | local s = 1 80 | for i = 1, #text do 81 | if not char.is_white(string.byte(text, i)) then 82 | s = i 83 | break 84 | end 85 | end 86 | 87 | local e = #text 88 | for i = #text, 1, -1 do 89 | if not char.is_white(string.byte(text, i)) then 90 | e = i 91 | break 92 | end 93 | end 94 | if s == 1 and e == #text then 95 | return text 96 | end 97 | return string.sub(text, s, e) 98 | end 99 | 100 | ---get_word 101 | ---@param text string 102 | ---@param stop_char? integer 103 | ---@param min_length? integer 104 | ---@return string 105 | str.get_word = function(text, stop_char, min_length) 106 | min_length = min_length or 0 107 | 108 | local has_alnum = false 109 | local stack = {} 110 | local word = {} 111 | local add = function(c) 112 | table.insert(word, string.char(c)) 113 | if stack[#stack] == c then 114 | table.remove(stack, #stack) 115 | else 116 | if PAIRS[c] then 117 | table.insert(stack, c) 118 | end 119 | end 120 | end 121 | for i = 1, #text do 122 | local c = string.byte(text, i, i) 123 | if #word < min_length then 124 | table.insert(word, string.char(c)) 125 | elseif not INVALIDS[c] then 126 | add(c) 127 | has_alnum = has_alnum or char.is_alnum(c) 128 | elseif not has_alnum then 129 | add(c) 130 | elseif #stack ~= 0 then 131 | add(c) 132 | if has_alnum and #stack == 0 then 133 | break 134 | end 135 | else 136 | break 137 | end 138 | end 139 | if stop_char and word[#word] == string.char(stop_char) then 140 | table.remove(word, #word) 141 | end 142 | return table.concat(word, '') 143 | end 144 | 145 | ---Oneline 146 | ---@param text string 147 | ---@return string 148 | str.oneline = function(text) 149 | for i = 1, #text do 150 | if string.byte(text, i) == NR_BYTE then 151 | return string.sub(text, 1, i - 1) 152 | end 153 | end 154 | return text 155 | end 156 | 157 | ---Escape special chars 158 | ---@param text string 159 | ---@param chars string[] 160 | ---@return string 161 | str.escape = function(text, chars) 162 | table.insert(chars, '\\') 163 | local escaped = {} 164 | local i = 1 165 | while i <= #text do 166 | local c = string.sub(text, i, i) 167 | if vim.tbl_contains(chars, c) then 168 | table.insert(escaped, '\\') 169 | table.insert(escaped, c) 170 | else 171 | table.insert(escaped, c) 172 | end 173 | i = i + 1 174 | end 175 | return table.concat(escaped, '') 176 | end 177 | 178 | return str 179 | -------------------------------------------------------------------------------- /lua/cmp/utils/str_spec.lua: -------------------------------------------------------------------------------- 1 | local str = require('cmp.utils.str') 2 | 3 | describe('utils.str', function() 4 | it('get_word', function() 5 | assert.are.equal(str.get_word('print'), 'print') 6 | assert.are.equal(str.get_word('$variable'), '$variable') 7 | assert.are.equal(str.get_word('print()'), 'print') 8 | assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]') 9 | assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies') 10 | assert.are.equal(str.get_word('"devDependencies": ${1},', string.byte('"')), '"devDependencies') 11 | assert.are.equal(str.get_word('#[cfg(test)]'), '#[cfg(test)]') 12 | assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps') 13 | end) 14 | 15 | it('remove_suffix', function() 16 | assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') 17 | assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') 18 | assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()') 19 | assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}') 20 | end) 21 | 22 | it('escape', function() 23 | assert.are.equal(str.escape('plain', {}), 'plain') 24 | assert.are.equal(str.escape('plain\\', {}), 'plain\\\\') 25 | assert.are.equal(str.escape('plain\\"', {}), 'plain\\\\"') 26 | assert.are.equal(str.escape('pla"in', { '"' }), 'pla\\"in') 27 | assert.are.equal(str.escape('call("")', { '"' }), 'call(\\"\\")') 28 | end) 29 | end) 30 | -------------------------------------------------------------------------------- /lua/cmp/utils/window.lua: -------------------------------------------------------------------------------- 1 | local misc = require('cmp.utils.misc') 2 | local opt = require('cmp.utils.options') 3 | local buffer = require('cmp.utils.buffer') 4 | local api = require('cmp.utils.api') 5 | local config = require('cmp.config') 6 | 7 | ---@class cmp.WindowStyle 8 | ---@field public relative string 9 | ---@field public row integer 10 | ---@field public col integer 11 | ---@field public width integer|float 12 | ---@field public height integer|float 13 | ---@field public border string|string[]|nil 14 | ---@field public zindex integer|nil 15 | 16 | ---@class cmp.Window 17 | ---@field public name string 18 | ---@field public win integer|nil 19 | ---@field public thumb_win integer|nil 20 | ---@field public sbar_win integer|nil 21 | ---@field public style cmp.WindowStyle 22 | ---@field public opt table 23 | ---@field public buffer_opt table 24 | local window = {} 25 | 26 | ---new 27 | ---@return cmp.Window 28 | window.new = function() 29 | local self = setmetatable({}, { __index = window }) 30 | self.name = misc.id('cmp.utils.window.new') 31 | self.win = nil 32 | self.sbar_win = nil 33 | self.thumb_win = nil 34 | self.style = {} 35 | self.opt = {} 36 | self.buffer_opt = {} 37 | return self 38 | end 39 | 40 | ---Set window option. 41 | ---NOTE: If the window already visible, immediately applied to it. 42 | ---@param key string 43 | ---@param value any 44 | window.option = function(self, key, value) 45 | if vim.fn.exists('+' .. key) == 0 then 46 | return 47 | end 48 | 49 | if value == nil then 50 | return self.opt[key] 51 | end 52 | 53 | self.opt[key] = value 54 | if self:visible() then 55 | opt.win_set_option(self.win, key, value) 56 | end 57 | end 58 | 59 | ---Set buffer option. 60 | ---NOTE: If the buffer already visible, immediately applied to it. 61 | ---@param key string 62 | ---@param value any 63 | window.buffer_option = function(self, key, value) 64 | if vim.fn.exists('+' .. key) == 0 then 65 | return 66 | end 67 | 68 | if value == nil then 69 | return self.buffer_opt[key] 70 | end 71 | 72 | self.buffer_opt[key] = value 73 | local existing_buf = buffer.get(self.name) 74 | if existing_buf then 75 | opt.buf_set_option(existing_buf, key, value) 76 | end 77 | end 78 | 79 | ---Set style. 80 | ---@param style cmp.WindowStyle 81 | window.set_style = function(self, style) 82 | self.style = style 83 | local info = self:info() 84 | 85 | if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then 86 | self.style.height = vim.o.lines - info.row - info.border_info.vert - 1 87 | end 88 | 89 | self.style.zindex = self.style.zindex or 1 90 | 91 | --- GUI clients are allowed to return fractional bounds, but we need integer 92 | --- bounds to open the window 93 | self.style.width = math.ceil(self.style.width) 94 | self.style.height = math.ceil(self.style.height) 95 | end 96 | 97 | ---Return buffer id. 98 | ---@return integer 99 | window.get_buffer = function(self) 100 | local buf, created_new = buffer.ensure(self.name) 101 | if created_new then 102 | for k, v in pairs(self.buffer_opt) do 103 | opt.buf_set_option(buf, k, v) 104 | end 105 | end 106 | return buf 107 | end 108 | 109 | ---Open window 110 | ---@param style cmp.WindowStyle 111 | window.open = function(self, style) 112 | if style then 113 | self:set_style(style) 114 | end 115 | 116 | if self.style.width < 1 or self.style.height < 1 then 117 | return 118 | end 119 | 120 | if self.win and vim.api.nvim_win_is_valid(self.win) then 121 | vim.api.nvim_win_set_config(self.win, self.style) 122 | else 123 | local s = misc.copy(self.style) 124 | s.noautocmd = true 125 | self.win = vim.api.nvim_open_win(self:get_buffer(), false, s) 126 | for k, v in pairs(self.opt) do 127 | opt.win_set_option(self.win, k, v) 128 | end 129 | end 130 | self:update() 131 | end 132 | 133 | ---Update 134 | window.update = function(self) 135 | local info = self:info() 136 | if info.scrollable and self.style.height > 0 then 137 | -- Draw the background of the scrollbar 138 | 139 | if not info.border_info.visible then 140 | local style = { 141 | relative = 'editor', 142 | style = 'minimal', 143 | width = 1, 144 | height = self.style.height, 145 | row = info.row, 146 | col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset. 147 | zindex = (self.style.zindex and (self.style.zindex + 1) or 1), 148 | border = 'none', 149 | } 150 | if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then 151 | vim.api.nvim_win_set_config(self.sbar_win, style) 152 | else 153 | style.noautocmd = true 154 | self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style) 155 | opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar') 156 | end 157 | end 158 | 159 | -- Draw the scrollbar thumb 160 | local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height())) 161 | thumb_height = math.max(1, thumb_height) 162 | local topline = vim.fn.getwininfo(self.win)[1].topline 163 | local scroll_ratio = topline / (self:get_content_height() - info.inner_height + 1) 164 | -- row grid start from 0 on nvim-0.10 165 | local thumb_offset_raw = (info.inner_height - thumb_height) * scroll_ratio 166 | -- round half if topline > 1 167 | local thumb_offset = math.floor(thumb_offset_raw) 168 | if topline > 1 and thumb_offset_raw + 0.5 >= thumb_offset + 1 then 169 | thumb_offset = thumb_offset + 1 170 | end 171 | 172 | local style = { 173 | relative = 'editor', 174 | style = 'minimal', 175 | width = 1, 176 | height = thumb_height, 177 | row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0), 178 | col = info.col + info.width - 1, -- info.col was already added scrollbar offset. 179 | zindex = (self.style.zindex and (self.style.zindex + 2) or 2), 180 | border = 'none', 181 | } 182 | if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then 183 | vim.api.nvim_win_set_config(self.thumb_win, style) 184 | else 185 | style.noautocmd = true 186 | self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style) 187 | opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb') 188 | end 189 | else 190 | if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then 191 | vim.api.nvim_win_hide(self.sbar_win) 192 | self.sbar_win = nil 193 | end 194 | if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then 195 | vim.api.nvim_win_hide(self.thumb_win) 196 | self.thumb_win = nil 197 | end 198 | end 199 | 200 | -- In cmdline, vim does not redraw automatically. 201 | if api.is_cmdline_mode() then 202 | vim.api.nvim_win_call(self.win, function() 203 | misc.redraw() 204 | end) 205 | end 206 | end 207 | 208 | ---Close window 209 | window.close = function(self) 210 | if self.win and vim.api.nvim_win_is_valid(self.win) then 211 | if self.win and vim.api.nvim_win_is_valid(self.win) then 212 | vim.api.nvim_win_hide(self.win) 213 | self.win = nil 214 | end 215 | if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then 216 | vim.api.nvim_win_hide(self.sbar_win) 217 | self.sbar_win = nil 218 | end 219 | if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then 220 | vim.api.nvim_win_hide(self.thumb_win) 221 | self.thumb_win = nil 222 | end 223 | end 224 | end 225 | 226 | ---Return the window is visible or not. 227 | window.visible = function(self) 228 | return self.win and vim.api.nvim_win_is_valid(self.win) 229 | end 230 | 231 | ---Return win info. 232 | window.info = function(self) 233 | local border_info = self:get_border_info() 234 | local scrollbar = config.get().window.completion.scrollbar 235 | local info = { 236 | row = self.style.row, 237 | col = self.style.col, 238 | width = self.style.width + border_info.left + border_info.right, 239 | height = self.style.height + border_info.top + border_info.bottom, 240 | inner_width = self.style.width, 241 | inner_height = self.style.height, 242 | border_info = border_info, 243 | scrollable = false, 244 | scrollbar_offset = 0, 245 | } 246 | 247 | if self:get_content_height() > info.inner_height and scrollbar then 248 | info.scrollable = true 249 | if not border_info.visible then 250 | info.scrollbar_offset = 1 251 | info.width = info.width + 1 252 | end 253 | end 254 | 255 | return info 256 | end 257 | 258 | ---Return border information. 259 | ---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean } 260 | window.get_border_info = function(self) 261 | local border = self.style.border 262 | if not border or border == 'none' then 263 | return { 264 | top = 0, 265 | left = 0, 266 | right = 0, 267 | bottom = 0, 268 | vert = 0, 269 | horiz = 0, 270 | visible = false, 271 | } 272 | end 273 | if type(border) == 'string' then 274 | if border == 'shadow' then 275 | return { 276 | top = 0, 277 | left = 0, 278 | right = 1, 279 | bottom = 1, 280 | vert = 1, 281 | horiz = 1, 282 | visible = false, 283 | } 284 | end 285 | return { 286 | top = 1, 287 | left = 1, 288 | right = 1, 289 | bottom = 1, 290 | vert = 2, 291 | horiz = 2, 292 | visible = true, 293 | } 294 | end 295 | 296 | local new_border = {} 297 | while #new_border <= 8 do 298 | for _, b in ipairs(border) do 299 | table.insert(new_border, type(b) == 'string' and b or b[1]) 300 | end 301 | end 302 | local info = {} 303 | info.top = new_border[2] == '' and 0 or 1 304 | info.right = new_border[4] == '' and 0 or 1 305 | info.bottom = new_border[6] == '' and 0 or 1 306 | info.left = new_border[8] == '' and 0 or 1 307 | info.vert = info.top + info.bottom 308 | info.horiz = info.left + info.right 309 | info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8])) 310 | return info 311 | end 312 | 313 | ---Get scroll height. 314 | ---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view). 315 | ---@return integer 316 | window.get_content_height = function(self) 317 | if not self:option('wrap') then 318 | return vim.api.nvim_buf_line_count(self:get_buffer()) 319 | end 320 | local height = 0 321 | vim.api.nvim_buf_call(self:get_buffer(), function() 322 | for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do 323 | -- nvim_buf_get_lines sometimes returns a blob. see #2050 324 | if vim.fn.type(text) == vim.v.t_blob then 325 | text = vim.fn.string(text) 326 | end 327 | height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width)) 328 | end 329 | end) 330 | return height 331 | end 332 | 333 | return window 334 | -------------------------------------------------------------------------------- /lua/cmp/view.lua: -------------------------------------------------------------------------------- 1 | local config = require('cmp.config') 2 | local async = require('cmp.utils.async') 3 | local event = require('cmp.utils.event') 4 | local keymap = require('cmp.utils.keymap') 5 | local docs_view = require('cmp.view.docs_view') 6 | local custom_entries_view = require('cmp.view.custom_entries_view') 7 | local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view') 8 | local native_entries_view = require('cmp.view.native_entries_view') 9 | local ghost_text_view = require('cmp.view.ghost_text_view') 10 | 11 | ---@class cmp.View 12 | ---@field public event cmp.Event 13 | ---@field private is_docs_view_pinned boolean 14 | ---@field private resolve_dedup cmp.AsyncDedup 15 | ---@field private native_entries_view cmp.NativeEntriesView 16 | ---@field private custom_entries_view cmp.CustomEntriesView 17 | ---@field private wildmenu_entries_view cmp.CustomEntriesView 18 | ---@field private change_dedup cmp.AsyncDedup 19 | ---@field private docs_view cmp.DocsView 20 | ---@field private ghost_text_view cmp.GhostTextView 21 | local view = {} 22 | 23 | ---Create menu 24 | view.new = function() 25 | local self = setmetatable({}, { __index = view }) 26 | self.resolve_dedup = async.dedup() 27 | self.is_docs_view_pinned = false 28 | self.custom_entries_view = custom_entries_view.new() 29 | self.native_entries_view = native_entries_view.new() 30 | self.wildmenu_entries_view = wildmenu_entries_view.new() 31 | self.docs_view = docs_view.new() 32 | self.ghost_text_view = ghost_text_view.new() 33 | self.event = event.new() 34 | 35 | return self 36 | end 37 | 38 | ---Return the view components are available or not. 39 | ---@return boolean 40 | view.ready = function(self) 41 | return self:_get_entries_view():ready() 42 | end 43 | 44 | ---OnChange handler. 45 | view.on_change = function(self) 46 | self:_get_entries_view():on_change() 47 | end 48 | 49 | ---Open menu 50 | ---@param ctx cmp.Context 51 | ---@param sources cmp.Source[] 52 | ---@return boolean did_open 53 | view.open = function(self, ctx, sources) 54 | local source_group_map = {} 55 | for _, s in ipairs(sources) do 56 | local group_index = s:get_source_config().group_index or 0 57 | if not source_group_map[group_index] then 58 | source_group_map[group_index] = {} 59 | end 60 | table.insert(source_group_map[group_index], s) 61 | end 62 | 63 | local group_indexes = vim.tbl_keys(source_group_map) 64 | table.sort(group_indexes, function(a, b) 65 | return a ~= b and (a < b) or nil 66 | end) 67 | 68 | local entries = {} 69 | for _, group_index in ipairs(group_indexes) do 70 | local source_group = source_group_map[group_index] or {} 71 | 72 | -- check the source triggered by character 73 | local has_triggered_by_symbol_source = false 74 | for _, s in ipairs(source_group) do 75 | if #s:get_entries(ctx) > 0 then 76 | if s.is_triggered_by_symbol then 77 | has_triggered_by_symbol_source = true 78 | break 79 | end 80 | end 81 | end 82 | 83 | -- create filtered entries. 84 | local offset = ctx.cursor.col 85 | local group_entries = {} 86 | local max_item_counts = {} 87 | for i, s in ipairs(source_group) do 88 | if s.offset <= ctx.cursor.col then 89 | if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then 90 | -- prepare max_item_counts map for filtering after sort. 91 | local max_item_count = s:get_source_config().max_item_count 92 | if max_item_count ~= nil then 93 | max_item_counts[s.name] = max_item_count 94 | end 95 | 96 | -- source order priority bonus. 97 | local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) 98 | 99 | for _, e in ipairs(s:get_entries(ctx)) do 100 | e.score = e.score + priority 101 | table.insert(group_entries, e) 102 | offset = math.min(offset, e.offset) 103 | end 104 | end 105 | end 106 | end 107 | 108 | -- sort. 109 | local comparators = config.get().sorting.comparators 110 | table.sort(group_entries, function(e1, e2) 111 | for _, fn in ipairs(comparators) do 112 | local diff = fn(e1, e2) 113 | if diff ~= nil then 114 | return diff 115 | end 116 | end 117 | end) 118 | 119 | -- filter by max_item_count. 120 | for _, e in ipairs(group_entries) do 121 | if max_item_counts[e.source.name] ~= nil then 122 | if max_item_counts[e.source.name] > 0 then 123 | max_item_counts[e.source.name] = max_item_counts[e.source.name] - 1 124 | table.insert(entries, e) 125 | end 126 | else 127 | table.insert(entries, e) 128 | end 129 | end 130 | 131 | local max_view_entries = config.get().performance.max_view_entries or 200 132 | entries = vim.list_slice(entries, 1, max_view_entries) 133 | 134 | -- open 135 | if #entries > 0 then 136 | self:_get_entries_view():open(offset, entries) 137 | self.event:emit('menu_opened', { 138 | window = self:_get_entries_view(), 139 | }) 140 | break 141 | end 142 | end 143 | 144 | -- complete_done. 145 | if #entries == 0 then 146 | self:close() 147 | end 148 | return #entries > 0 149 | end 150 | 151 | ---Close menu 152 | view.close = function(self) 153 | if self:visible() then 154 | self.is_docs_view_pinned = false 155 | self.event:emit('complete_done', { 156 | entry = self:_get_entries_view():get_selected_entry(), 157 | }) 158 | end 159 | self:_get_entries_view():close() 160 | self.docs_view:close() 161 | self.ghost_text_view:hide() 162 | self.event:emit('menu_closed', { 163 | window = self:_get_entries_view(), 164 | }) 165 | end 166 | 167 | ---Abort menu 168 | view.abort = function(self) 169 | if self:visible() then 170 | self.is_docs_view_pinned = false 171 | end 172 | self:_get_entries_view():abort() 173 | self.docs_view:close() 174 | self.ghost_text_view:hide() 175 | self.event:emit('menu_closed', { 176 | window = self:_get_entries_view(), 177 | }) 178 | end 179 | 180 | ---Return the view is visible or not. 181 | ---@return boolean 182 | view.visible = function(self) 183 | return self:_get_entries_view():visible() 184 | end 185 | 186 | ---Opens the documentation window. 187 | view.open_docs = function(self) 188 | self.is_docs_view_pinned = true 189 | local e = self:get_selected_entry() 190 | if e then 191 | e:resolve(vim.schedule_wrap(self.resolve_dedup(function() 192 | if not self:visible() then 193 | return 194 | end 195 | self.docs_view:open(e, self:_get_entries_view():info()) 196 | end))) 197 | end 198 | end 199 | 200 | ---Closes the documentation window. 201 | view.close_docs = function(self) 202 | self.is_docs_view_pinned = false 203 | if self:get_selected_entry() then 204 | self.docs_view:close() 205 | end 206 | end 207 | 208 | ---Scroll documentation window if possible. 209 | ---@param delta integer 210 | view.scroll_docs = function(self, delta) 211 | self.docs_view:scroll(delta) 212 | end 213 | 214 | ---Get what number candidates are currently selected. 215 | ---If not selected, nil is returned. 216 | ---@return integer|nil 217 | view.get_selected_index = function(self) 218 | return self:_get_entries_view():get_selected_index() 219 | end 220 | 221 | ---Select prev menu item. 222 | ---@param option cmp.SelectOption 223 | view.select_next_item = function(self, option) 224 | self:_get_entries_view():select_next_item(option) 225 | end 226 | 227 | ---Select prev menu item. 228 | ---@param option cmp.SelectOption 229 | view.select_prev_item = function(self, option) 230 | self:_get_entries_view():select_prev_item(option) 231 | end 232 | 233 | ---Get offset. 234 | view.get_offset = function(self) 235 | return self:_get_entries_view():get_offset() 236 | end 237 | 238 | ---Get entries. 239 | ---@return cmp.Entry[] 240 | view.get_entries = function(self) 241 | return self:_get_entries_view():get_entries() 242 | end 243 | 244 | ---Get first entry 245 | ---@param self cmp.Entry|nil 246 | view.get_first_entry = function(self) 247 | return self:_get_entries_view():get_first_entry() 248 | end 249 | 250 | ---Get current selected entry 251 | ---@return cmp.Entry|nil 252 | view.get_selected_entry = function(self) 253 | return self:_get_entries_view():get_selected_entry() 254 | end 255 | 256 | ---Get current active entry 257 | ---@return cmp.Entry|nil 258 | view.get_active_entry = function(self) 259 | return self:_get_entries_view():get_active_entry() 260 | end 261 | 262 | ---Return current configured entries_view 263 | ---@return cmp.CustomEntriesView|cmp.NativeEntriesView 264 | view._get_entries_view = function(self) 265 | self.native_entries_view.event:clear() 266 | self.custom_entries_view.event:clear() 267 | self.wildmenu_entries_view.event:clear() 268 | 269 | local c = config.get() 270 | local v = self.custom_entries_view 271 | if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then 272 | v = self.wildmenu_entries_view 273 | elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then 274 | v = self.native_entries_view 275 | end 276 | v.event:on('change', function() 277 | self:on_entry_change() 278 | end) 279 | return v 280 | end 281 | 282 | ---On entry change 283 | view.on_entry_change = async.throttle(function(self) 284 | if not self:visible() then 285 | return 286 | end 287 | local e = self:get_selected_entry() 288 | if e then 289 | for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do 290 | keymap.listen('i', c, function(...) 291 | self.event:emit('keymap', ...) 292 | end) 293 | end 294 | e:resolve(vim.schedule_wrap(self.resolve_dedup(function() 295 | if not self:visible() then 296 | return 297 | end 298 | if self.is_docs_view_pinned or config.get().view.docs.auto_open then 299 | self.docs_view:open(e, self:_get_entries_view():info()) 300 | end 301 | end))) 302 | else 303 | self.docs_view:close() 304 | end 305 | 306 | e = e or self:get_first_entry() 307 | if e then 308 | self.ghost_text_view:show(e) 309 | else 310 | self.ghost_text_view:hide() 311 | end 312 | end, 20) 313 | 314 | return view 315 | -------------------------------------------------------------------------------- /lua/cmp/view/docs_view.lua: -------------------------------------------------------------------------------- 1 | local window = require('cmp.utils.window') 2 | local config = require('cmp.config') 3 | 4 | ---@class cmp.DocsView 5 | ---@field public window cmp.Window 6 | local docs_view = {} 7 | 8 | ---Create new floating window module 9 | docs_view.new = function() 10 | local self = setmetatable({}, { __index = docs_view }) 11 | self.entry = nil 12 | self.window = window.new() 13 | self.window:option('conceallevel', 2) 14 | self.window:option('concealcursor', 'n') 15 | self.window:option('foldenable', false) 16 | self.window:option('linebreak', true) 17 | self.window:option('scrolloff', 0) 18 | self.window:option('showbreak', 'NONE') 19 | self.window:option('wrap', true) 20 | self.window:buffer_option('filetype', 'cmp_docs') 21 | self.window:buffer_option('buftype', 'nofile') 22 | return self 23 | end 24 | 25 | ---Open documentation window 26 | ---@param e cmp.Entry 27 | ---@param view cmp.WindowStyle 28 | docs_view.open = function(self, e, view) 29 | local documentation = config.get().window.documentation 30 | if not documentation then 31 | return 32 | end 33 | 34 | if not e or not view then 35 | return self:close() 36 | end 37 | 38 | local border_info = window.get_border_info({ style = documentation }) 39 | local right_space = vim.o.columns - (view.col + view.width) - 1 40 | local left_space = view.col - 1 41 | local max_width = math.max(left_space, right_space) 42 | if documentation.max_width > 0 then 43 | max_width = math.min(documentation.max_width, max_width) 44 | end 45 | 46 | -- Update buffer content if needed. 47 | if not self.entry or e.id ~= self.entry.id then 48 | local documents = e:get_documentation() 49 | if #documents == 0 then 50 | return self:close() 51 | end 52 | 53 | self.entry = e 54 | vim.api.nvim_buf_call(self.window:get_buffer(), function() 55 | vim.cmd([[syntax clear]]) 56 | vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {}) 57 | end) 58 | local opts = { 59 | max_width = max_width - border_info.horiz, 60 | } 61 | if documentation.max_height > 0 then 62 | opts.max_height = documentation.max_height 63 | end 64 | vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, opts) 65 | end 66 | 67 | -- Set buffer as not modified, so it can be removed without errors 68 | vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false) 69 | 70 | -- Calculate window size. 71 | local opts = { 72 | max_width = max_width - border_info.horiz, 73 | } 74 | if documentation.max_height > 0 then 75 | opts.max_height = documentation.max_height - border_info.vert 76 | end 77 | local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), opts) 78 | if width <= 0 or height <= 0 then 79 | return self:close() 80 | end 81 | 82 | -- Calculate window position. 83 | local right_col = view.col + view.width 84 | local left_col = view.col - width - border_info.horiz 85 | local col, left 86 | if right_space >= width and left_space >= width then 87 | if right_space < left_space then 88 | col = left_col 89 | left = true 90 | else 91 | col = right_col 92 | end 93 | elseif right_space >= width then 94 | col = right_col 95 | elseif left_space >= width then 96 | col = left_col 97 | left = true 98 | else 99 | return self:close() 100 | end 101 | 102 | -- Render window. 103 | self.window:option('winblend', documentation.winblend) 104 | self.window:option('winhighlight', documentation.winhighlight) 105 | local style = { 106 | relative = 'editor', 107 | style = 'minimal', 108 | width = width, 109 | height = height, 110 | row = view.row, 111 | col = col, 112 | border = documentation.border, 113 | zindex = documentation.zindex or 50, 114 | } 115 | self.window:open(style) 116 | 117 | -- Correct left-col for scrollbar existence. 118 | if left then 119 | style.col = style.col - self.window:info().scrollbar_offset 120 | self.window:open(style) 121 | end 122 | end 123 | 124 | ---Close floating window 125 | docs_view.close = function(self) 126 | self.window:close() 127 | self.entry = nil 128 | end 129 | 130 | docs_view.scroll = function(self, delta) 131 | if self:visible() then 132 | local info = vim.fn.getwininfo(self.window.win)[1] or {} 133 | local top = info.topline or 1 134 | top = top + delta 135 | top = math.max(top, 1) 136 | top = math.min(top, self.window:get_content_height() - info.height + 1) 137 | 138 | vim.defer_fn(function() 139 | vim.api.nvim_buf_call(self.window:get_buffer(), function() 140 | vim.api.nvim_command('normal! ' .. top .. 'zt') 141 | self.window:update() 142 | end) 143 | end, 0) 144 | end 145 | end 146 | 147 | docs_view.visible = function(self) 148 | return self.window:visible() 149 | end 150 | 151 | return docs_view 152 | -------------------------------------------------------------------------------- /lua/cmp/view/ghost_text_view.lua: -------------------------------------------------------------------------------- 1 | local config = require('cmp.config') 2 | local misc = require('cmp.utils.misc') 3 | local snippet = require('cmp.utils.snippet') 4 | -- local str = require('cmp.utils.str') 5 | local api = require('cmp.utils.api') 6 | local types = require('cmp.types') 7 | 8 | ---@class cmp.GhostTextView 9 | ---@field win number|nil 10 | ---@field entry cmp.Entry|nil 11 | local ghost_text_view = {} 12 | 13 | ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT') 14 | 15 | local has_inline = (function() 16 | return (pcall(function() 17 | local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, { 18 | virt_text = { { ' ', 'Comment' } }, 19 | virt_text_pos = 'inline', 20 | hl_mode = 'combine', 21 | ephemeral = false, 22 | }) 23 | vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id) 24 | end)) 25 | end)() 26 | 27 | ghost_text_view.new = function() 28 | local self = setmetatable({}, { __index = ghost_text_view }) 29 | self.win = nil 30 | self.entry = nil 31 | self.extmark_id = nil 32 | vim.api.nvim_set_decoration_provider(ghost_text_view.ns, { 33 | on_win = function(_, win) 34 | if self.extmark_id then 35 | if vim.api.nvim_buf_is_loaded(self.extmark_buf) then 36 | vim.api.nvim_buf_del_extmark(self.extmark_buf, ghost_text_view.ns, self.extmark_id) 37 | end 38 | self.extmark_id = nil 39 | end 40 | 41 | if win ~= self.win then 42 | return false 43 | end 44 | 45 | local c = config.get().experimental.ghost_text 46 | if not c then 47 | return 48 | end 49 | 50 | if not self.entry then 51 | return 52 | end 53 | 54 | local row, col = unpack(vim.api.nvim_win_get_cursor(0)) 55 | 56 | local line = vim.api.nvim_get_current_line() 57 | if not has_inline then 58 | if string.sub(line, col + 1) ~= '' then 59 | return 60 | end 61 | end 62 | 63 | local text = self.text_gen(self, line, col) 64 | if #text > 0 then 65 | local virt_lines = {} 66 | for _, l in ipairs(vim.fn.split(text, '\n')) do 67 | table.insert(virt_lines, { { l, type(c) == 'table' and c.hl_group or 'Comment' } }) 68 | end 69 | local first_line = table.remove(virt_lines, 1) 70 | self.extmark_buf = vim.api.nvim_get_current_buf() 71 | self.extmark_id = vim.api.nvim_buf_set_extmark(self.extmark_buf, ghost_text_view.ns, row - 1, col, { 72 | right_gravity = true, 73 | virt_text = first_line, 74 | virt_text_pos = has_inline and 'inline' or 'overlay', 75 | virt_lines = virt_lines, 76 | hl_mode = 'combine', 77 | ephemeral = false, 78 | }) 79 | end 80 | end, 81 | }) 82 | return self 83 | end 84 | 85 | ---Generate the ghost text 86 | --- This function calculates the bytes of the entry to display calculating the number 87 | --- of character differences instead of just byte difference. 88 | ghost_text_view.text_gen = function(self, line, cursor_col) 89 | local word = self.entry:get_insert_text() 90 | if self.entry:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then 91 | word = tostring(snippet.parse(word)) 92 | end 93 | local word_clen = vim.fn.strchars(word, true) 94 | local cword = string.sub(line, self.entry.offset, cursor_col) 95 | local cword_clen = vim.fn.strchars(cword, true) 96 | -- Number of characters from entry text (word) to be displayed as ghost thext 97 | local nchars = word_clen - cword_clen 98 | -- Missing characters to complete the entry text 99 | local text 100 | if nchars > 0 then 101 | text = string.sub(word, misc.to_vimindex(word, word_clen - nchars)) 102 | else 103 | text = '' 104 | end 105 | return text 106 | end 107 | 108 | ---Show ghost text 109 | ---@param e cmp.Entry 110 | ghost_text_view.show = function(self, e) 111 | if not api.is_insert_mode() then 112 | return 113 | end 114 | local c = config.get().experimental.ghost_text 115 | if not c then 116 | return 117 | end 118 | local changed = e ~= self.entry 119 | self.win = vim.api.nvim_get_current_win() 120 | self.entry = e 121 | if changed then 122 | misc.redraw(true) -- force invoke decoration provider. 123 | end 124 | end 125 | 126 | ghost_text_view.hide = function(self) 127 | if self.win and self.entry then 128 | self.win = nil 129 | self.entry = nil 130 | misc.redraw(true) -- force invoke decoration provider. 131 | end 132 | end 133 | 134 | return ghost_text_view 135 | -------------------------------------------------------------------------------- /lua/cmp/view/native_entries_view.lua: -------------------------------------------------------------------------------- 1 | local event = require('cmp.utils.event') 2 | local autocmd = require('cmp.utils.autocmd') 3 | local keymap = require('cmp.utils.keymap') 4 | local feedkeys = require('cmp.utils.feedkeys') 5 | local types = require('cmp.types') 6 | local config = require('cmp.config') 7 | local api = require('cmp.utils.api') 8 | 9 | ---@class cmp.NativeEntriesView 10 | ---@field private offset integer 11 | ---@field private items vim.CompletedItem 12 | ---@field private entries cmp.Entry[] 13 | ---@field private preselect_index integer 14 | ---@field public event cmp.Event 15 | local native_entries_view = {} 16 | 17 | native_entries_view.new = function() 18 | local self = setmetatable({}, { __index = native_entries_view }) 19 | self.event = event.new() 20 | self.offset = -1 21 | self.items = {} 22 | self.entries = {} 23 | self.preselect_index = 0 24 | autocmd.subscribe('CompleteChanged', function() 25 | self.event:emit('change') 26 | end) 27 | return self 28 | end 29 | 30 | native_entries_view.ready = function(_) 31 | if vim.fn.pumvisible() == 0 then 32 | return true 33 | end 34 | return vim.fn.complete_info({ 'mode' }).mode == 'eval' 35 | end 36 | 37 | native_entries_view.on_change = function(self) 38 | if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] + 1 then 39 | local preselect_enabled = config.get().preselect == types.cmp.PreselectMode.Item 40 | 41 | local completeopt = vim.o.completeopt 42 | if self.preselect_index == 1 and preselect_enabled then 43 | vim.o.completeopt = 'menu,menuone,noinsert' 44 | else 45 | vim.o.completeopt = config.get().completion.completeopt 46 | end 47 | vim.fn.complete(self.offset, self.items) 48 | vim.o.completeopt = completeopt 49 | 50 | if self.preselect_index > 1 and preselect_enabled then 51 | self:preselect(self.preselect_index) 52 | end 53 | end 54 | end 55 | 56 | native_entries_view.open = function(self, offset, entries) 57 | local dedup = {} 58 | local items = {} 59 | local dedup_entries = {} 60 | local preselect_index = 0 61 | for _, e in ipairs(entries) do 62 | local item = e:get_vim_item(offset) 63 | if item.dup == 1 or not dedup[item.abbr] then 64 | dedup[item.abbr] = true 65 | table.insert(items, item) 66 | table.insert(dedup_entries, e) 67 | if preselect_index == 0 and e.completion_item.preselect then 68 | preselect_index = #dedup_entries 69 | end 70 | end 71 | end 72 | self.offset = offset 73 | self.items = items 74 | self.entries = dedup_entries 75 | self.preselect_index = preselect_index 76 | self:on_change() 77 | end 78 | 79 | native_entries_view.close = function(self) 80 | if api.is_insert_mode() and self:visible() then 81 | vim.api.nvim_select_popupmenu_item(-1, false, true, {}) 82 | end 83 | self.offset = -1 84 | self.entries = {} 85 | self.items = {} 86 | self.preselect_index = 0 87 | end 88 | 89 | native_entries_view.abort = function(_) 90 | if api.is_suitable_mode() then 91 | vim.api.nvim_select_popupmenu_item(-1, true, true, {}) 92 | end 93 | end 94 | 95 | native_entries_view.visible = function(_) 96 | return vim.fn.pumvisible() == 1 97 | end 98 | 99 | native_entries_view.info = function(self) 100 | if self:visible() then 101 | local info = vim.fn.pum_getpos() 102 | return { 103 | width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1), 104 | height = info.height, 105 | row = info.row, 106 | col = info.col == 0 and 0 or info.col - 1, 107 | } 108 | end 109 | end 110 | 111 | native_entries_view.preselect = function(self, index) 112 | if self:visible() then 113 | if index <= #self.entries then 114 | vim.api.nvim_select_popupmenu_item(index - 1, false, false, {}) 115 | end 116 | end 117 | end 118 | 119 | native_entries_view.get_selected_index = function(self) 120 | if self:visible() then 121 | local idx = vim.fn.complete_info({ 'selected' }).selected 122 | if idx > -1 then 123 | return math.max(0, idx) + 1 124 | end 125 | end 126 | end 127 | 128 | native_entries_view.select_next_item = function(self, option) 129 | local callback = function() 130 | self.event:emit('change') 131 | end 132 | if self:visible() then 133 | if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then 134 | feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) 135 | else 136 | feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) 137 | end 138 | end 139 | end 140 | 141 | native_entries_view.select_prev_item = function(self, option) 142 | local callback = function() 143 | self.event:emit('change') 144 | end 145 | if self:visible() then 146 | if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then 147 | feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) 148 | else 149 | feedkeys.call(keymap.t(string.rep('', option.count)), 'n', callback) 150 | end 151 | end 152 | end 153 | 154 | native_entries_view.get_offset = function(self) 155 | if self:visible() then 156 | return self.offset 157 | end 158 | return nil 159 | end 160 | 161 | native_entries_view.get_entries = function(self) 162 | if self:visible() then 163 | return self.entries 164 | end 165 | return {} 166 | end 167 | 168 | native_entries_view.get_first_entry = function(self) 169 | if self:visible() then 170 | return self.entries[1] 171 | end 172 | end 173 | 174 | native_entries_view.get_selected_entry = function(self) 175 | local idx = self:get_selected_index() 176 | if idx then 177 | return self.entries[idx] 178 | end 179 | end 180 | 181 | native_entries_view.get_active_entry = function(self) 182 | if self:visible() and (vim.v.completed_item or {}).word then 183 | return self:get_selected_entry() 184 | end 185 | end 186 | 187 | return native_entries_view 188 | -------------------------------------------------------------------------------- /lua/cmp/view/wildmenu_entries_view.lua: -------------------------------------------------------------------------------- 1 | local event = require('cmp.utils.event') 2 | local autocmd = require('cmp.utils.autocmd') 3 | local feedkeys = require('cmp.utils.feedkeys') 4 | local config = require('cmp.config') 5 | local window = require('cmp.utils.window') 6 | local types = require('cmp.types') 7 | local keymap = require('cmp.utils.keymap') 8 | local misc = require('cmp.utils.misc') 9 | local api = require('cmp.utils.api') 10 | 11 | ---@class cmp.CustomEntriesView 12 | ---@field private offset integer 13 | ---@field private entries_win cmp.Window 14 | ---@field private active boolean 15 | ---@field private entries cmp.Entry[] 16 | ---@field public event cmp.Event 17 | local wildmenu_entries_view = {} 18 | 19 | wildmenu_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view') 20 | 21 | wildmenu_entries_view.new = function() 22 | local self = setmetatable({}, { __index = wildmenu_entries_view }) 23 | self.event = event.new() 24 | self.offset = -1 25 | self.active = false 26 | self.entries = {} 27 | self.offsets = {} 28 | self.selected_index = 0 29 | self.entries_win = window.new() 30 | 31 | self.entries_win:option('conceallevel', 2) 32 | self.entries_win:option('concealcursor', 'n') 33 | self.entries_win:option('cursorlineopt', 'line') 34 | self.entries_win:option('foldenable', false) 35 | self.entries_win:option('wrap', false) 36 | self.entries_win:option('scrolloff', 0) 37 | self.entries_win:option('sidescrolloff', 0) 38 | self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None') 39 | self.entries_win:buffer_option('tabstop', 1) 40 | 41 | autocmd.subscribe( 42 | 'CompleteChanged', 43 | vim.schedule_wrap(function() 44 | if self:visible() and vim.fn.pumvisible() == 1 then 45 | self:close() 46 | end 47 | end) 48 | ) 49 | 50 | vim.api.nvim_set_decoration_provider(wildmenu_entries_view.ns, { 51 | on_win = function(_, win, buf, _, _) 52 | if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then 53 | return 54 | end 55 | 56 | for i, e in ipairs(self.entries) do 57 | if e then 58 | local view = e:get_view(self.offset, buf) 59 | vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], { 60 | end_line = 0, 61 | end_col = self.offsets[i] + view.abbr.bytes, 62 | hl_group = view.abbr.hl_group, 63 | hl_mode = 'combine', 64 | ephemeral = true, 65 | }) 66 | 67 | if i == self.selected_index then 68 | vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], { 69 | end_line = 0, 70 | end_col = self.offsets[i] + view.abbr.bytes, 71 | hl_group = 'PmenuSel', 72 | hl_mode = 'combine', 73 | ephemeral = true, 74 | }) 75 | end 76 | 77 | for _, m in ipairs(e:get_view_matches(view.abbr.text) or {}) do 78 | vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, { 79 | end_line = 0, 80 | end_col = self.offsets[i] + m.word_match_end, 81 | hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch', 82 | hl_mode = 'combine', 83 | ephemeral = true, 84 | }) 85 | end 86 | end 87 | end 88 | end, 89 | }) 90 | return self 91 | end 92 | 93 | wildmenu_entries_view.close = function(self) 94 | self.entries_win:close() 95 | end 96 | 97 | wildmenu_entries_view.ready = function() 98 | return vim.fn.pumvisible() == 0 99 | end 100 | 101 | wildmenu_entries_view.on_change = function(self) 102 | self.active = false 103 | end 104 | 105 | wildmenu_entries_view.open = function(self, offset, entries) 106 | self.offset = offset 107 | self.entries = {} 108 | 109 | -- Apply window options (that might be changed) on the custom completion menu. 110 | self.entries_win:option('winblend', vim.o.pumblend) 111 | 112 | local dedup = {} 113 | local preselect = 0 114 | local i = 1 115 | for _, e in ipairs(entries) do 116 | local view = e:get_view(offset, 0) 117 | if view.dup == 1 or not dedup[e.completion_item.label] then 118 | dedup[e.completion_item.label] = true 119 | table.insert(self.entries, e) 120 | if preselect == 0 and e.completion_item.preselect then 121 | preselect = i 122 | end 123 | i = i + 1 124 | end 125 | end 126 | 127 | self.entries_win:open({ 128 | relative = 'editor', 129 | style = 'minimal', 130 | row = vim.o.lines - 2, 131 | col = 0, 132 | width = vim.o.columns, 133 | height = 1, 134 | zindex = 1001, 135 | }) 136 | self:draw() 137 | 138 | if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then 139 | self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select }) 140 | elseif not string.match(config.get().completion.completeopt, 'noselect') then 141 | self:_select(1, { behavior = types.cmp.SelectBehavior.Select }) 142 | else 143 | self:_select(0, { behavior = types.cmp.SelectBehavior.Select }) 144 | end 145 | end 146 | 147 | wildmenu_entries_view.abort = function(self) 148 | feedkeys.call('', 'n', function() 149 | self:close() 150 | end) 151 | end 152 | 153 | wildmenu_entries_view.draw = function(self) 154 | self.offsets = {} 155 | 156 | local entries_buf = self.entries_win:get_buffer() 157 | local texts = {} 158 | local offset = 0 159 | for _, e in ipairs(self.entries) do 160 | local view = e:get_view(self.offset, entries_buf) 161 | table.insert(self.offsets, offset) 162 | table.insert(texts, view.abbr.text) 163 | offset = offset + view.abbr.bytes + #self:_get_separator() 164 | end 165 | 166 | vim.api.nvim_buf_set_lines(entries_buf, 0, 1, false, { table.concat(texts, self:_get_separator()) }) 167 | vim.api.nvim_buf_set_option(entries_buf, 'modified', false) 168 | 169 | vim.api.nvim_win_call(0, function() 170 | misc.redraw() 171 | end) 172 | end 173 | 174 | wildmenu_entries_view.visible = function(self) 175 | return self.entries_win:visible() 176 | end 177 | 178 | wildmenu_entries_view.info = function(self) 179 | return self.entries_win:info() 180 | end 181 | 182 | wildmenu_entries_view.get_selected_index = function(self) 183 | if self:visible() and self.active then 184 | return self.selected_index 185 | end 186 | end 187 | 188 | wildmenu_entries_view.select_next_item = function(self, option) 189 | if self:visible() then 190 | local cursor 191 | if self.selected_index == 0 or self.selected_index == #self.entries then 192 | cursor = option.count 193 | else 194 | cursor = self.selected_index + option.count 195 | end 196 | cursor = math.max(math.min(cursor, #self.entries), 0) 197 | self:_select(cursor, option) 198 | end 199 | end 200 | 201 | wildmenu_entries_view.select_prev_item = function(self, option) 202 | if self:visible() then 203 | if self.selected_index == 0 or self.selected_index <= 1 then 204 | self:_select(#self.entries, option) 205 | else 206 | self:_select(math.max(self.selected_index - option.count, 0), option) 207 | end 208 | end 209 | end 210 | 211 | wildmenu_entries_view.get_offset = function(self) 212 | if self:visible() then 213 | return self.offset 214 | end 215 | return nil 216 | end 217 | 218 | wildmenu_entries_view.get_entries = function(self) 219 | if self:visible() then 220 | return self.entries 221 | end 222 | return {} 223 | end 224 | 225 | wildmenu_entries_view.get_first_entry = function(self) 226 | if self:visible() then 227 | return self.entries[1] 228 | end 229 | end 230 | 231 | wildmenu_entries_view.get_selected_entry = function(self) 232 | local idx = self:get_selected_index() 233 | if idx then 234 | return self.entries[idx] 235 | end 236 | end 237 | 238 | wildmenu_entries_view.get_active_entry = function(self) 239 | if self:visible() and self.active then 240 | return self:get_selected_entry() 241 | end 242 | end 243 | 244 | wildmenu_entries_view._select = function(self, selected_index, option) 245 | local is_next = self.selected_index < selected_index 246 | self.selected_index = selected_index 247 | self.active = (selected_index ~= 0) 248 | 249 | if self.active then 250 | local e = self:get_active_entry() 251 | if option.behavior == types.cmp.SelectBehavior.Insert then 252 | local cursor = api.get_cursor() 253 | local word = e:get_vim_item(self.offset).word 254 | vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true) 255 | end 256 | vim.api.nvim_win_call(self.entries_win.win, function() 257 | local view = e:get_view(self.offset, self.entries_win:get_buffer()) 258 | vim.api.nvim_win_set_cursor(0, { 1, self.offsets[selected_index] + (is_next and view.abbr.bytes or 0) }) 259 | vim.cmd([[redraw!]]) -- Force refresh for vim.api.nvim_set_decoration_provider 260 | end) 261 | end 262 | 263 | self.event:emit('change') 264 | end 265 | 266 | wildmenu_entries_view._get_separator = function() 267 | local c = config.get() 268 | return (c and c.view and c.view.entries and c.view.entries.separator) or ' ' 269 | end 270 | 271 | return wildmenu_entries_view 272 | -------------------------------------------------------------------------------- /lua/cmp/vim_source.lua: -------------------------------------------------------------------------------- 1 | local misc = require('cmp.utils.misc') 2 | 3 | local vim_source = {} 4 | 5 | ---@param id integer 6 | ---@param args any[] 7 | vim_source.on_callback = function(id, args) 8 | if vim_source.to_callback.callbacks[id] then 9 | vim_source.to_callback.callbacks[id](unpack(args)) 10 | end 11 | end 12 | 13 | ---@param callback function 14 | ---@return integer 15 | vim_source.to_callback = setmetatable({ 16 | callbacks = {}, 17 | }, { 18 | __call = function(self, callback) 19 | local id = misc.id('cmp.vim_source.to_callback') 20 | self.callbacks[id] = function(...) 21 | callback(...) 22 | self.callbacks[id] = nil 23 | end 24 | return id 25 | end, 26 | }) 27 | 28 | ---Convert to serializable args. 29 | ---@param args any[] 30 | vim_source.to_args = function(args) 31 | for i, arg in ipairs(args) do 32 | if type(arg) == 'function' then 33 | args[i] = vim_source.to_callback(arg) 34 | end 35 | end 36 | return args 37 | end 38 | 39 | ---@param bridge_id integer 40 | ---@param methods string[] 41 | vim_source.new = function(bridge_id, methods) 42 | local self = {} 43 | for _, method in ipairs(methods) do 44 | self[method] = (function(m) 45 | return function(_, ...) 46 | return vim.fn['cmp#_method'](bridge_id, m, vim_source.to_args({ ... })) 47 | end 48 | end)(method) 49 | end 50 | return self 51 | end 52 | 53 | return vim_source 54 | -------------------------------------------------------------------------------- /nvim-cmp-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | local MODREV, SPECREV = 'scm', '-1' 2 | rockspec_format = '3.0' 3 | package = 'nvim-cmp' 4 | version = MODREV .. SPECREV 5 | 6 | description = { 7 | summary = 'A completion plugin for neovim', 8 | labels = { 'neovim' }, 9 | detailed = [[ 10 | A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced". 11 | ]], 12 | homepage = 'https://github.com/hrsh7th/nvim-cmp', 13 | license = 'MIT', 14 | } 15 | 16 | dependencies = { 17 | 'lua >= 5.1, < 5.4', 18 | } 19 | 20 | source = { 21 | url = 'git://github.com/hrsh7th/nvim-cmp', 22 | } 23 | 24 | build = { 25 | type = 'builtin', 26 | copy_directories = { 27 | 'autoload', 28 | 'plugin', 29 | 'doc' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plugin/cmp.lua: -------------------------------------------------------------------------------- 1 | if vim.g.loaded_cmp then 2 | return 3 | end 4 | vim.g.loaded_cmp = true 5 | 6 | if not vim.api.nvim_create_autocmd then 7 | return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.') 8 | end 9 | 10 | local api = require('cmp.utils.api') 11 | local types = require('cmp.types') 12 | local highlight = require('cmp.utils.highlight') 13 | local autocmd = require('cmp.utils.autocmd') 14 | 15 | vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true }) 16 | vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true }) 17 | vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true }) 18 | vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true }) 19 | vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true }) 20 | vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true }) 21 | for kind in pairs(types.lsp.CompletionItemKind) do 22 | if type(kind) == 'string' then 23 | local name = ('CmpItemKind%s'):format(kind) 24 | vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true }) 25 | end 26 | end 27 | 28 | autocmd.subscribe({ 'ColorScheme', 'UIEnter' }, function() 29 | highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false }) 30 | highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false }) 31 | highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false }) 32 | highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false }) 33 | highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false }) 34 | highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false }) 35 | for name in pairs(types.lsp.CompletionItemKind) do 36 | if type(name) == 'string' then 37 | vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false }) 38 | end 39 | end 40 | end) 41 | autocmd.emit('ColorScheme') 42 | 43 | if vim.on_key then 44 | local control_c_termcode = vim.api.nvim_replace_termcodes('', true, true, true) 45 | vim.on_key(function(keys) 46 | if keys == control_c_termcode then 47 | vim.schedule(function() 48 | if not api.is_suitable_mode() then 49 | autocmd.emit('InsertLeave') 50 | end 51 | end) 52 | end 53 | end, vim.api.nvim_create_namespace('cmp.plugin')) 54 | end 55 | 56 | 57 | vim.api.nvim_create_user_command('CmpStatus', function() 58 | require('cmp').status() 59 | end, { desc = 'Check status of cmp sources' }) 60 | 61 | vim.cmd([[doautocmd User CmpReady]]) 62 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | indent_width = 2 3 | column_width = 1200 4 | quote_style = "AutoPreferSingle" 5 | -------------------------------------------------------------------------------- /utils/vimrc.vim: -------------------------------------------------------------------------------- 1 | if has('vim_starting') 2 | set encoding=utf-8 3 | endif 4 | scriptencoding utf-8 5 | 6 | if &compatible 7 | set nocompatible 8 | endif 9 | 10 | let s:plug_dir = expand('/tmp/plugged/vim-plug') 11 | if !filereadable(s:plug_dir .. '/plug.vim') 12 | execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir) 13 | end 14 | 15 | execute 'set runtimepath+=' . s:plug_dir 16 | call plug#begin(s:plug_dir) 17 | Plug 'hrsh7th/nvim-cmp' 18 | Plug 'hrsh7th/cmp-buffer' 19 | Plug 'hrsh7th/cmp-nvim-lsp' 20 | Plug 'hrsh7th/vim-vsnip' 21 | Plug 'neovim/nvim-lspconfig' 22 | call plug#end() 23 | PlugInstall | quit 24 | 25 | " Setup global configuration. More on configuration below. 26 | lua << EOF 27 | local cmp = require "cmp" 28 | cmp.setup { 29 | snippet = { 30 | expand = function(args) 31 | vim.fn["vsnip#anonymous"](args.body) 32 | end, 33 | }, 34 | 35 | mapping = { 36 | [''] = cmp.mapping.confirm({ select = true }) 37 | }, 38 | 39 | sources = cmp.config.sources({ 40 | { name = "nvim_lsp" }, 41 | { name = "buffer" }, 42 | }), 43 | } 44 | EOF 45 | 46 | lua << EOF 47 | local capabilities = require('cmp_nvim_lsp').default_capabilities() 48 | 49 | require'lspconfig'.cssls.setup { 50 | capabilities = capabilities, 51 | } 52 | EOF 53 | 54 | --------------------------------------------------------------------------------