From 6dbd97087f96f6ae64a1852258a720bcb299edee Mon Sep 17 00:00:00 2001 From: Wang Shidong Date: Sat, 1 Jan 2022 22:13:13 +0800 Subject: [PATCH] feat(autocomplete): add nvim-cmp support --- autoload/SpaceVim.vim | 7 +- autoload/SpaceVim/api/logger.vim | 2 +- autoload/SpaceVim/autocmds.vim | 1 + autoload/SpaceVim/default.vim | 2 + autoload/SpaceVim/layers/autocomplete.vim | 23 +- autoload/SpaceVim/layers/lsp.vim | 16 +- autoload/SpaceVim/mapping/g.vim | 1 + autoload/SpaceVim/mapping/leader.vim | 1 + autoload/SpaceVim/mapping/space.vim | 1 + autoload/SpaceVim/mapping/tab.vim | 10 +- autoload/SpaceVim/mapping/z.vim | 1 + autoload/SpaceVim/plugins.vim | 10 +- bundle/README.md | 1 + bundle/cmp-buffer/LICENSE | 21 + bundle/cmp-buffer/README.md | 62 ++ bundle/cmp-buffer/after/plugin/cmp_buffer.lua | 2 + bundle/cmp-buffer/lua/cmp_buffer/buffer.lua | 235 ++++++ bundle/cmp-buffer/lua/cmp_buffer/init.lua | 91 ++ bundle/cmp-cmdline/README.md | 13 + .../cmp-cmdline/after/plugin/cmp_cmdline.lua | 1 + bundle/cmp-cmdline/lua/cmp_cmdline/init.lua | 135 +++ bundle/cmp-nvim-lsp/LICENSE | 21 + bundle/cmp-nvim-lsp/README.md | 24 + .../after/plugin/cmp_nvim_lsp.lua | 1 + bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/init.lua | 84 ++ .../cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua | 117 +++ bundle/cmp-path/LICENSE | 21 + bundle/cmp-path/README.md | 15 + bundle/cmp-path/after/plugin/cmp_path.lua | 1 + bundle/cmp-path/lua/cmp_path/init.lua | 207 +++++ bundle/lspkind-nvim/LICENSE | 21 + bundle/lspkind-nvim/README.md | 74 ++ bundle/lspkind-nvim/lua/lspkind/init.lua | 143 ++++ bundle/nvim-cmp/.githooks/pre-commit | 9 + bundle/nvim-cmp/.github/FUNDING.yml | 3 + .../.github/ISSUE_TEMPLATE/bug_report.md | 31 + .../.github/workflows/integration.yaml | 50 ++ bundle/nvim-cmp/.gitignore | 1 + bundle/nvim-cmp/.luacheckrc | 2 + bundle/nvim-cmp/LICENSE | 21 + bundle/nvim-cmp/Makefile | 24 + bundle/nvim-cmp/README.md | 776 ++++++++++++++++++ bundle/nvim-cmp/autoload/cmp.vim | 76 ++ bundle/nvim-cmp/autoload/vital/_cmp.vim | 9 + .../autoload/vital/_cmp/VS/LSP/Position.vim | 62 ++ .../autoload/vital/_cmp/VS/LSP/Text.vim | 23 + .../autoload/vital/_cmp/VS/LSP/TextEdit.vim | 185 +++++ .../autoload/vital/_cmp/VS/Vim/Buffer.vim | 126 +++ .../autoload/vital/_cmp/VS/Vim/Option.vim | 21 + bundle/nvim-cmp/autoload/vital/cmp.vim | 330 ++++++++ bundle/nvim-cmp/autoload/vital/cmp.vital | 4 + bundle/nvim-cmp/init.sh | 7 + bundle/nvim-cmp/lua/cmp/config.lua | 104 +++ bundle/nvim-cmp/lua/cmp/config/compare.lua | 103 +++ bundle/nvim-cmp/lua/cmp/config/default.lua | 130 +++ bundle/nvim-cmp/lua/cmp/config/mapping.lua | 82 ++ bundle/nvim-cmp/lua/cmp/config/sources.lua | 10 + bundle/nvim-cmp/lua/cmp/context.lua | 105 +++ bundle/nvim-cmp/lua/cmp/context_spec.lua | 31 + bundle/nvim-cmp/lua/cmp/core.lua | 435 ++++++++++ bundle/nvim-cmp/lua/cmp/core_spec.lua | 158 ++++ bundle/nvim-cmp/lua/cmp/entry.lua | 430 ++++++++++ bundle/nvim-cmp/lua/cmp/entry_spec.lua | 281 +++++++ bundle/nvim-cmp/lua/cmp/init.lua | 312 +++++++ bundle/nvim-cmp/lua/cmp/matcher.lua | 292 +++++++ bundle/nvim-cmp/lua/cmp/matcher_spec.lua | 44 + bundle/nvim-cmp/lua/cmp/source.lua | 368 +++++++++ bundle/nvim-cmp/lua/cmp/source_spec.lua | 109 +++ bundle/nvim-cmp/lua/cmp/types/cmp.lua | 128 +++ bundle/nvim-cmp/lua/cmp/types/init.lua | 7 + bundle/nvim-cmp/lua/cmp/types/lsp.lua | 197 +++++ bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua | 46 ++ bundle/nvim-cmp/lua/cmp/types/vim.lua | 17 + bundle/nvim-cmp/lua/cmp/utils/api.lua | 68 ++ bundle/nvim-cmp/lua/cmp/utils/api_spec.lua | 46 ++ bundle/nvim-cmp/lua/cmp/utils/async.lua | 101 +++ bundle/nvim-cmp/lua/cmp/utils/async_spec.lua | 69 ++ bundle/nvim-cmp/lua/cmp/utils/autocmd.lua | 35 + bundle/nvim-cmp/lua/cmp/utils/binary.lua | 33 + bundle/nvim-cmp/lua/cmp/utils/binary_spec.lua | 28 + bundle/nvim-cmp/lua/cmp/utils/buffer.lua | 17 + bundle/nvim-cmp/lua/cmp/utils/cache.lua | 58 ++ bundle/nvim-cmp/lua/cmp/utils/char.lua | 115 +++ bundle/nvim-cmp/lua/cmp/utils/debug.lua | 20 + bundle/nvim-cmp/lua/cmp/utils/event.lua | 51 ++ bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua | 110 +++ .../nvim-cmp/lua/cmp/utils/feedkeys_spec.lua | 56 ++ bundle/nvim-cmp/lua/cmp/utils/highlight.lua | 46 ++ bundle/nvim-cmp/lua/cmp/utils/keymap.lua | 267 ++++++ bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua | 81 ++ bundle/nvim-cmp/lua/cmp/utils/misc.lua | 181 ++++ bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua | 51 ++ bundle/nvim-cmp/lua/cmp/utils/pattern.lua | 28 + bundle/nvim-cmp/lua/cmp/utils/spec.lua | 92 +++ bundle/nvim-cmp/lua/cmp/utils/str.lua | 156 ++++ bundle/nvim-cmp/lua/cmp/utils/str_spec.lua | 30 + bundle/nvim-cmp/lua/cmp/utils/window.lua | 261 ++++++ bundle/nvim-cmp/lua/cmp/view.lua | 227 +++++ bundle/nvim-cmp/lua/cmp/vim_source.lua | 53 ++ bundle/nvim-cmp/plugin/cmp.lua | 127 +++ bundle/nvim-cmp/stylua.toml | 4 + bundle/nvim-cmp/utils/install_stylua.sh | 63 ++ bundle/nvim-cmp/utils/vimrc.vim | 53 ++ config/plugins/nvim-cmp.vim | 38 + 104 files changed, 9061 insertions(+), 18 deletions(-) create mode 100644 bundle/cmp-buffer/LICENSE create mode 100644 bundle/cmp-buffer/README.md create mode 100644 bundle/cmp-buffer/after/plugin/cmp_buffer.lua create mode 100644 bundle/cmp-buffer/lua/cmp_buffer/buffer.lua create mode 100644 bundle/cmp-buffer/lua/cmp_buffer/init.lua create mode 100644 bundle/cmp-cmdline/README.md create mode 100644 bundle/cmp-cmdline/after/plugin/cmp_cmdline.lua create mode 100644 bundle/cmp-cmdline/lua/cmp_cmdline/init.lua create mode 100644 bundle/cmp-nvim-lsp/LICENSE create mode 100644 bundle/cmp-nvim-lsp/README.md create mode 100644 bundle/cmp-nvim-lsp/after/plugin/cmp_nvim_lsp.lua create mode 100644 bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/init.lua create mode 100644 bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua create mode 100644 bundle/cmp-path/LICENSE create mode 100644 bundle/cmp-path/README.md create mode 100644 bundle/cmp-path/after/plugin/cmp_path.lua create mode 100644 bundle/cmp-path/lua/cmp_path/init.lua create mode 100644 bundle/lspkind-nvim/LICENSE create mode 100644 bundle/lspkind-nvim/README.md create mode 100644 bundle/lspkind-nvim/lua/lspkind/init.lua create mode 100644 bundle/nvim-cmp/.githooks/pre-commit create mode 100644 bundle/nvim-cmp/.github/FUNDING.yml create mode 100644 bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 bundle/nvim-cmp/.github/workflows/integration.yaml create mode 100644 bundle/nvim-cmp/.gitignore create mode 100644 bundle/nvim-cmp/.luacheckrc create mode 100644 bundle/nvim-cmp/LICENSE create mode 100644 bundle/nvim-cmp/Makefile create mode 100644 bundle/nvim-cmp/README.md create mode 100644 bundle/nvim-cmp/autoload/cmp.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Position.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Text.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/TextEdit.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Buffer.vim create mode 100644 bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Option.vim create mode 100644 bundle/nvim-cmp/autoload/vital/cmp.vim create mode 100644 bundle/nvim-cmp/autoload/vital/cmp.vital create mode 100644 bundle/nvim-cmp/init.sh create mode 100644 bundle/nvim-cmp/lua/cmp/config.lua create mode 100644 bundle/nvim-cmp/lua/cmp/config/compare.lua create mode 100644 bundle/nvim-cmp/lua/cmp/config/default.lua create mode 100644 bundle/nvim-cmp/lua/cmp/config/mapping.lua create mode 100644 bundle/nvim-cmp/lua/cmp/config/sources.lua create mode 100644 bundle/nvim-cmp/lua/cmp/context.lua create mode 100644 bundle/nvim-cmp/lua/cmp/context_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/core.lua create mode 100644 bundle/nvim-cmp/lua/cmp/core_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/entry.lua create mode 100644 bundle/nvim-cmp/lua/cmp/entry_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/init.lua create mode 100644 bundle/nvim-cmp/lua/cmp/matcher.lua create mode 100644 bundle/nvim-cmp/lua/cmp/matcher_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/source.lua create mode 100644 bundle/nvim-cmp/lua/cmp/source_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/types/cmp.lua create mode 100644 bundle/nvim-cmp/lua/cmp/types/init.lua create mode 100644 bundle/nvim-cmp/lua/cmp/types/lsp.lua create mode 100644 bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/types/vim.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/api.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/api_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/async.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/async_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/autocmd.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/binary.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/binary_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/buffer.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/cache.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/char.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/debug.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/event.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/highlight.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/keymap.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/misc.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/pattern.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/str.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/str_spec.lua create mode 100644 bundle/nvim-cmp/lua/cmp/utils/window.lua create mode 100644 bundle/nvim-cmp/lua/cmp/view.lua create mode 100644 bundle/nvim-cmp/lua/cmp/vim_source.lua create mode 100644 bundle/nvim-cmp/plugin/cmp.lua create mode 100644 bundle/nvim-cmp/stylua.toml create mode 100644 bundle/nvim-cmp/utils/install_stylua.sh create mode 100644 bundle/nvim-cmp/utils/vimrc.vim create mode 100644 config/plugins/nvim-cmp.vim diff --git a/autoload/SpaceVim.vim b/autoload/SpaceVim.vim index 14f2eb8fa..08d228ff4 100644 --- a/autoload/SpaceVim.vim +++ b/autoload/SpaceVim.vim @@ -315,7 +315,9 @@ let g:spacevim_realtime_leader_guide = 1 " let g:spacevim_enable_key_frequency = 1 " < let g:spacevim_enable_key_frequency = 0 -if (has('python3') +if has('nvim-0.5.0') + let g:spacevim_autocomplete_method = 'nvim-cmp' +elseif (has('python3') \ && (SpaceVim#util#haspy3lib('neovim') \ || SpaceVim#util#haspy3lib('pynvim'))) && \ (has('nvim') || (has('patch-8.0.0027'))) @@ -1384,7 +1386,6 @@ function! SpaceVim#end() abort elseif g:spacevim_vim_help_language ==# 'ja' let &helplang = 'jp' endif - "" " generate tags for SpaceVim let help = fnamemodify(g:_spacevim_root_dir, ':p:h') . '/doc' try @@ -1392,8 +1393,6 @@ function! SpaceVim#end() abort catch call SpaceVim#logger#warn('Failed to generate helptags for SpaceVim') endtry - - "" " set language if !empty(g:spacevim_language) silent exec 'lan ' . g:spacevim_language diff --git a/autoload/SpaceVim/api/logger.vim b/autoload/SpaceVim/api/logger.vim index b2051d701..c0fbf5072 100644 --- a/autoload/SpaceVim/api/logger.vim +++ b/autoload/SpaceVim/api/logger.vim @@ -52,7 +52,7 @@ endfunction function! s:self._build_msg(msg, l) abort let msg = a:msg let time = strftime('%H:%M:%S') - let log = printf('[ %s ] [%s] [%00.3f] [ %s ] %s', + let log = printf('[ %s ] [%s] [%00.3f] [ %5s ] %s', \ self.name, \ time, \ reltimefloat(reltime(self.clock)), diff --git a/autoload/SpaceVim/autocmds.vim b/autoload/SpaceVim/autocmds.vim index d78d3e1d8..e9c38fc8b 100644 --- a/autoload/SpaceVim/autocmds.vim +++ b/autoload/SpaceVim/autocmds.vim @@ -12,6 +12,7 @@ let s:VIM = SpaceVim#api#import('vim') "autocmds function! SpaceVim#autocmds#init() abort + call SpaceVim#logger#debug('init SpaceVim_core autocmd group') augroup SpaceVim_core au! autocmd BufWinEnter quickfix nnoremap diff --git a/autoload/SpaceVim/default.vim b/autoload/SpaceVim/default.vim index e50ceac23..289d05736 100644 --- a/autoload/SpaceVim/default.vim +++ b/autoload/SpaceVim/default.vim @@ -147,6 +147,7 @@ endfunction "}}} function! SpaceVim#default#layers() abort + call SpaceVim#logger#debug('init default layer list.') call SpaceVim#layers#load('autocomplete') call SpaceVim#layers#load('checkers') call SpaceVim#layers#load('format') @@ -159,6 +160,7 @@ function! SpaceVim#default#layers() abort endfunction function! SpaceVim#default#keyBindings() abort + call SpaceVim#logger#debug('init default key bindings.') " yank and paste if has('unnamedplus') xnoremap y "+y diff --git a/autoload/SpaceVim/layers/autocomplete.vim b/autoload/SpaceVim/layers/autocomplete.vim index a444be2a2..db19ae188 100644 --- a/autoload/SpaceVim/layers/autocomplete.vim +++ b/autoload/SpaceVim/layers/autocomplete.vim @@ -86,6 +86,22 @@ function! SpaceVim#layers#autocomplete#plugins() abort \ 'on_event' : 'InsertEnter', \ 'loadconf' : 1, \ }]) + elseif g:spacevim_autocomplete_method ==# 'nvim-cmp' + " use bundle nvim-cmp + call add(plugins, [g:_spacevim_root_dir . 'bundle/nvim-cmp', { + \ 'merged' : 0, + \ 'loadconf' : 1, + \ }]) + call add(plugins, [g:_spacevim_root_dir . 'bundle/cmp-buffer', { + \ 'merged' : 0, + \ }]) + call add(plugins, [g:_spacevim_root_dir . 'bundle/cmp-path', { + \ 'merged' : 0, + \ }]) + call add(plugins, [g:_spacevim_root_dir . 'bundle/lspkind-nvim', { + \ 'merged' : 0, + \ 'loadconf' : 1, + \ }]) elseif g:spacevim_autocomplete_method ==# 'asyncomplete' call add(plugins, ['prabirshrestha/asyncomplete.vim', { \ 'loadconf' : 1, @@ -120,8 +136,11 @@ function! SpaceVim#layers#autocomplete#plugins() abort \ 'on_event' : 'CompleteDone', \ 'loadconf_before' : 1, \ }]) - call add(plugins, [g:_spacevim_root_dir . 'bundle/CompleteParameter.vim', - \ { 'merged' : 0}]) + if g:spacevim_autocomplete_method !=# 'nvim-cmp' + " this plugin use same namespace as nvim-cmp + call add(plugins, [g:_spacevim_root_dir . 'bundle/CompleteParameter.vim', + \ { 'merged' : 0}]) + endif endif return plugins endfunction diff --git a/autoload/SpaceVim/layers/lsp.vim b/autoload/SpaceVim/layers/lsp.vim index a74674c0a..cf438c7f5 100644 --- a/autoload/SpaceVim/layers/lsp.vim +++ b/autoload/SpaceVim/layers/lsp.vim @@ -51,12 +51,12 @@ endfunction function! SpaceVim#layers#lsp#setup() abort -lua << EOF -local nvim_lsp = require('lspconfig') + lua << EOF + local nvim_lsp = require('lspconfig') --- Use an on_attach function to only map the following keys --- after the language server attaches to the current buffer -local on_attach = function(client, bufnr) + -- Use an on_attach function to only map the following keys + -- after the language server attaches to the current buffer + local on_attach = function(client, bufnr) local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end @@ -95,8 +95,8 @@ for _, lsp in ipairs(servers) do on_attach = on_attach, flags = { debounce_text_changes = 150, + } } - } end EOF endfunction @@ -108,6 +108,10 @@ function! SpaceVim#layers#lsp#plugins() abort call add(plugins, [g:_spacevim_root_dir . 'bundle/nvim-lspconfig', {'merged' : 0, 'loadconf' : 1}]) if g:spacevim_autocomplete_method ==# 'deoplete' call add(plugins, [g:_spacevim_root_dir . 'bundle/deoplete-lsp', {'merged' : 0}]) + elseif g:spacevim_autocomplete_method ==# 'nvim-cmp' + call add(plugins, [g:_spacevim_root_dir . 'bundle/cmp-nvim-lsp', { + \ 'merged' : 0, + \ }]) endif elseif SpaceVim#layers#isLoaded('autocomplete') && get(g:, 'spacevim_autocomplete_method') ==# 'coc' " nop diff --git a/autoload/SpaceVim/mapping/g.vim b/autoload/SpaceVim/mapping/g.vim index 130dd3afd..9e7201fc9 100644 --- a/autoload/SpaceVim/mapping/g.vim +++ b/autoload/SpaceVim/mapping/g.vim @@ -7,6 +7,7 @@ "============================================================================= function! SpaceVim#mapping#g#init() abort + call SpaceVim#logger#debug('init g key bindings') nnoremap [G] :LeaderGuide "g" nmap g [G] let g:_spacevim_mappings_g = {} diff --git a/autoload/SpaceVim/mapping/leader.vim b/autoload/SpaceVim/mapping/leader.vim index 36ef92c74..1c3979615 100644 --- a/autoload/SpaceVim/mapping/leader.vim +++ b/autoload/SpaceVim/mapping/leader.vim @@ -199,6 +199,7 @@ function! SpaceVim#mapping#leader#getName(key) abort endfunction function! SpaceVim#mapping#leader#defindKEYs() abort + call SpaceVim#logger#debug('defind SPC h k prefixs') let g:_spacevim_mappings_prefixs = {} if !g:spacevim_vimcompatible && !empty(g:spacevim_windows_leader) let g:_spacevim_mappings_prefixs[g:spacevim_windows_leader] = {'name' : '+Window prefix'} diff --git a/autoload/SpaceVim/mapping/space.vim b/autoload/SpaceVim/mapping/space.vim index 2f2522056..e47b1ab29 100644 --- a/autoload/SpaceVim/mapping/space.vim +++ b/autoload/SpaceVim/mapping/space.vim @@ -11,6 +11,7 @@ let s:BUF = SpaceVim#api#import('vim#buffer') let s:file = expand(':~') let s:funcbeginline = expand('') + 1 function! SpaceVim#mapping#space#init() abort + call SpaceVim#logger#debug('init SPC key bindings') let g:_spacevim_mappings_space = {} let g:_spacevim_mappings_prefixs['[SPC]'] = {'name' : '+SPC prefix'} let g:_spacevim_mappings_space.t = {'name' : '+Toggles'} diff --git a/autoload/SpaceVim/mapping/tab.vim b/autoload/SpaceVim/mapping/tab.vim index d994dc778..12fdd01ea 100644 --- a/autoload/SpaceVim/mapping/tab.vim +++ b/autoload/SpaceVim/mapping/tab.vim @@ -20,8 +20,16 @@ if g:spacevim_snippet_engine ==# 'neosnippet' elseif neosnippet#expandable_or_jumpable() && getline('.')[col('.')-2] !=#'(' return "\(neosnippet_expand_or_jump)" elseif pumvisible() + \ || + \ ( + \ g:spacevim_autocomplete_method ==# 'nvim-cmp' + \ && luaeval("require('cmp').visible()") + \ ) return "\" - elseif has('patch-7.4.774') && complete_parameter#jumpable(1) && getline('.')[col('.')-2] !=# ')' + elseif has('patch-7.4.774') + \ && g:spacevim_autocomplete_method !=# 'nvim-cmp' + \ && complete_parameter#jumpable(1) + \ && getline('.')[col('.')-2] !=# ')' return "\(complete_parameter#goto_next_parameter)" else return "\" diff --git a/autoload/SpaceVim/mapping/z.vim b/autoload/SpaceVim/mapping/z.vim index 7c5b68d83..711d6b29f 100644 --- a/autoload/SpaceVim/mapping/z.vim +++ b/autoload/SpaceVim/mapping/z.vim @@ -7,6 +7,7 @@ "============================================================================= function! SpaceVim#mapping#z#init() abort "{{{ + call SpaceVim#logger#debug('init z key bindings') nnoremap [Z] :LeaderGuide "z" nmap z [Z] let g:_spacevim_mappings_z = {} diff --git a/autoload/SpaceVim/plugins.vim b/autoload/SpaceVim/plugins.vim index 5c425702d..37dfbd12f 100644 --- a/autoload/SpaceVim/plugins.vim +++ b/autoload/SpaceVim/plugins.vim @@ -18,9 +18,10 @@ function! SpaceVim#plugins#load() abort endfunction function! s:load_plugins() abort - for group in SpaceVim#layers#get() - let g:_spacevim_plugin_layer = group - for plugin in s:getLayerPlugins(group) + for layer in SpaceVim#layers#get() + call SpaceVim#logger#debug('init ' . layer . ' layer plugins list.') + let g:_spacevim_plugin_layer = layer + for plugin in s:getLayerPlugins(layer) if len(plugin) == 2 call SpaceVim#plugins#add(plugin[0], extend(plugin[1], {'overwrite' : 1})) if SpaceVim#plugins#tap(split(plugin[0], '/')[-1]) && get(plugin[1], 'loadconf', 0 ) @@ -33,7 +34,7 @@ function! s:load_plugins() abort call SpaceVim#plugins#add(plugin[0], {'overwrite' : 1}) endif endfor - call s:loadLayerConfig(group) + call s:loadLayerConfig(layer) endfor unlet g:_spacevim_plugin_layer for plugin in g:spacevim_custom_plugins @@ -55,6 +56,7 @@ function! s:getLayerPlugins(layer) abort endfunction function! s:loadLayerConfig(layer) abort + call SpaceVim#logger#debug('load ' . a:layer . ' layer config.') try call SpaceVim#layers#{a:layer}#config() catch /^Vim\%((\a\+)\)\=:E117/ diff --git a/bundle/README.md b/bundle/README.md index ca69cbef9..deaf35b1b 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -16,3 +16,4 @@ In `bundle/` directory, there are two kinds of plugins: forked plugins without c - [indent-blankline.nvim](https://github.com/lukas-reineke/indent-blankline.nvim/tree/17a83ea765831cb0cc64f768b8c3f43479b90bbe) - [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/tree/507f8a570ac2b8b8dabdd0f62da3b3194bf822f8) - [deoplete-lsp](https://github.com/deoplete-plugins/deoplete-lsp/tree/6299a22bedfb4f814d95cb0010291501472f8fd0) +- [nvim-cmp](https://github.com/hrsh7th/nvim-cmp/tree/1cfe2f7dfdd877b54c0f4b0f9a15f525e7a3ea01) diff --git a/bundle/cmp-buffer/LICENSE b/bundle/cmp-buffer/LICENSE new file mode 100644 index 000000000..ae725ef17 --- /dev/null +++ b/bundle/cmp-buffer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 hrsh7th + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundle/cmp-buffer/README.md b/bundle/cmp-buffer/README.md new file mode 100644 index 000000000..fd9667408 --- /dev/null +++ b/bundle/cmp-buffer/README.md @@ -0,0 +1,62 @@ +# cmp-buffer + +nvim-cmp source for buffer words. + +# Setup + +```lua +require'cmp'.setup { + sources = { + { name = 'buffer' } + } +} +``` + +# Configuration + +The below source configuration are available. + + +### keyword_length (type: number) + +_Default:_ `3` + +Specify word length to gather. + + +### keyword_pattern (type: string) + +_Default:_ `[[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%([\-.]\w*\)*\)]]` + +A vim's regular expression for creating a word list from buffer content. + +You can set this to `\k\+` if you want to use the `iskeyword` option for recognizing words. + + +### get_bufnrs (type: fun(): number[]) + +_Default:_ `function() return { vim.api.nvim_get_current_buf() } end` + +A function that specifies the buffer numbers to complete. + +You can use the following pre-defined recipes. + +##### All buffers + +```lua +get_bufnrs = function() + return vim.api.nvim_list_bufs() +end +``` + +##### Visible buffers + +```lua +get_bufnrs = function() + local bufs = {} + for _, win in ipairs(vim.api.nvim_list_wins()) do + bufs[vim.api.nvim_win_get_buf(win)] = true + end + return vim.tbl_keys(bufs) +end +``` diff --git a/bundle/cmp-buffer/after/plugin/cmp_buffer.lua b/bundle/cmp-buffer/after/plugin/cmp_buffer.lua new file mode 100644 index 000000000..f68f3a91b --- /dev/null +++ b/bundle/cmp-buffer/after/plugin/cmp_buffer.lua @@ -0,0 +1,2 @@ +require'cmp'.register_source('buffer', require'cmp_buffer'.new()) + diff --git a/bundle/cmp-buffer/lua/cmp_buffer/buffer.lua b/bundle/cmp-buffer/lua/cmp_buffer/buffer.lua new file mode 100644 index 000000000..fb48925cd --- /dev/null +++ b/bundle/cmp-buffer/lua/cmp_buffer/buffer.lua @@ -0,0 +1,235 @@ +---@class cmp_buffer.Buffer +---@field public bufnr number +---@field public regex any +---@field public length number +---@field public pattern string +---@field public indexing_chunk_size number +---@field public indexing_interval number +---@field public timer any|nil +---@field public lines_count number +---@field public lines_words table +---@field public closed boolean +---@field public on_close_cb fun()|nil +local buffer = {} + +---Create new buffer object +---@param bufnr number +---@param length number +---@param pattern string +---@return cmp_buffer.Buffer +function buffer.new(bufnr, length, pattern) + local self = setmetatable({}, { __index = buffer }) + self.bufnr = bufnr + self.regex = vim.regex(pattern) + self.length = length + self.pattern = pattern + self.indexing_chunk_size = 1000 + self.indexing_interval = 200 + self.timer = nil + self.lines_count = 0 + self.lines_words = {} + self.closed = false + self.on_close_cb = nil + return self +end + +---Close buffer +function buffer.close(self) + self.closed = true + self:stop_indexing_timer() + self.lines_count = 0 + self.lines_words = {} + if self.on_close_cb then + self.on_close_cb() + end +end + +function buffer.stop_indexing_timer(self) + if self.timer and not self.timer:is_closing() then + self.timer:stop() + self.timer:close() + end + self.timer = nil +end + +---Indexing buffer +function buffer.index(self) + self.lines_count = vim.api.nvim_buf_line_count(self.bufnr) + for i = 1, self.lines_count do + self.lines_words[i] = {} + end + + self:index_range_async(0, self.lines_count) +end + +function buffer.index_range(self, range_start, range_end) + vim.api.nvim_buf_call(self.bufnr, function() + local lines = vim.api.nvim_buf_get_lines(self.bufnr, range_start, range_end, true) + for i, line in ipairs(lines) do + self:index_line(range_start + i, line) + end + end) +end + +function buffer.index_range_async(self, range_start, range_end) + local chunk_start = range_start + + local lines = vim.api.nvim_buf_get_lines(self.bufnr, range_start, range_end, true) + + self.timer = vim.loop.new_timer() + self.timer:start( + 0, + self.indexing_interval, + vim.schedule_wrap(function() + if self.closed then + return + end + + local chunk_end = math.min(chunk_start + self.indexing_chunk_size, range_end) + vim.api.nvim_buf_call(self.bufnr, function() + for linenr = chunk_start + 1, chunk_end do + self:index_line(linenr, lines[linenr]) + end + end) + chunk_start = chunk_end + + if chunk_end >= range_end then + self:stop_indexing_timer() + end + end) + ) +end + +--- watch +function buffer.watch(self) + -- NOTE: As far as I know, indexing in watching can't be done asynchronously + -- because even built-in commands generate multiple consequent `on_lines` + -- events, and I'm not even mentioning plugins here. To get accurate results + -- we would have to either re-index the entire file on throttled events (slow + -- and looses the benefit of on_lines watching), or put the events in a + -- queue, which would complicate the plugin a lot. Plus, most changes which + -- trigger this event will be from regular editing, and so 99% of the time + -- they will affect only 1-2 lines. + vim.api.nvim_buf_attach(self.bufnr, false, { + -- NOTE: line indexes are 0-based and the last line is not inclusive. + on_lines = function(_, _, _, first_line, old_last_line, new_last_line, _, _, _) + if self.closed then + return true + end + + local delta = new_last_line - old_last_line + local old_lines_count = self.lines_count + local new_lines_count = old_lines_count + delta + if new_lines_count == 0 then -- clear + -- This branch protects against bugs after full-file deletion. If you + -- do, for example, gdGG, the new_last_line of the event will be zero. + -- Which is not true, a buffer always contains at least one empty line, + -- only unloaded buffers contain zero lines. + new_lines_count = 1 + for i = old_lines_count, 2, -1 do + self.lines_words[i] = nil + end + self.lines_words[1] = {} + elseif delta > 0 then -- append + -- Explicitly reserve more slots in the array part of the lines table, + -- all of them will be filled in the next loop, but in reverse order + -- (which is why I am concerned about preallocation). Why is there no + -- built-in function to do this in Lua??? + for i = old_lines_count + 1, new_lines_count do + self.lines_words[i] = vim.NIL + end + -- Move forwards the unchanged elements in the tail part. + for i = old_lines_count, old_last_line + 1, -1 do + self.lines_words[i + delta] = self.lines_words[i] + end + -- Fill in new tables for the added lines. + for i = old_last_line + 1, new_last_line do + self.lines_words[i] = {} + end + elseif delta < 0 then -- remove + -- Move backwards the unchanged elements in the tail part. + for i = old_last_line + 1, old_lines_count do + self.lines_words[i + delta] = self.lines_words[i] + end + -- Remove (already copied) tables from the end, in reverse order, so + -- that we don't make holes in the lines table. + for i = old_lines_count, new_lines_count + 1, -1 do + self.lines_words[i] = nil + end + end + self.lines_count = new_lines_count + + -- replace lines + self:index_range(first_line, new_last_line) + end, + + on_reload = function(_, _) + if self.closed then + return true + end + + -- The logic for adjusting lines list on buffer reloads is much simpler + -- because tables of all lines can be assumed to be fresh. + local new_lines_count = vim.api.nvim_buf_line_count(self.bufnr) + if new_lines_count > self.lines_count then -- append + for i = self.lines_count + 1, new_lines_count do + self.lines_words[i] = {} + end + elseif new_lines_count < self.lines_count then -- remove + for i = self.lines_count, new_lines_count + 1, -1 do + self.lines_words[i] = nil + end + end + self.lines_count = new_lines_count + + self:index_range(0, self.lines_count) + end, + + on_detach = function(_, _) + if self.closed then + return true + end + self:close() + end, + }) +end + +---@param linenr number +---@param line string +function buffer.index_line(self, linenr, line) + local words = self.lines_words[linenr] + for k, _ in ipairs(words) do + words[k] = nil + end + local word_i = 1 + + local remaining = line + while #remaining > 0 do + -- NOTE: Both start and end indexes here are 0-based (unlike Lua strings), + -- and the end index is not inclusive. + local match_start, match_end = self.regex:match_str(remaining) + if match_start and match_end then + local word = remaining:sub(match_start + 1, match_end) + if #word >= self.length then + words[word_i] = word + word_i = word_i + 1 + end + remaining = remaining:sub(match_end + 1) + else + break + end + end +end + +--- get_words +function buffer.get_words(self) + local words = {} + for _, line in ipairs(self.lines_words) do + for _, w in ipairs(line) do + table.insert(words, w) + end + end + return words +end + +return buffer diff --git a/bundle/cmp-buffer/lua/cmp_buffer/init.lua b/bundle/cmp-buffer/lua/cmp_buffer/init.lua new file mode 100644 index 000000000..c8627f444 --- /dev/null +++ b/bundle/cmp-buffer/lua/cmp_buffer/init.lua @@ -0,0 +1,91 @@ +local buffer = require('cmp_buffer.buffer') + +local defaults = { + keyword_length = 3, + keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%([\-]\w*\)*\)]], + get_bufnrs = function() + return { vim.api.nvim_get_current_buf() } + end, +} + +local source = {} + +source.new = function() + local self = setmetatable({}, { __index = source }) + self.buffers = {} + return self +end + +source.get_keyword_pattern = function(_, params) + params.option = vim.tbl_deep_extend('keep', params.option, defaults) + vim.validate({ + keyword_length = { params.option.keyword_length, 'number', '`opts.keyword_length` must be `number`' }, + keyword_pattern = { params.option.keyword_pattern, 'string', '`opts.keyword_pattern` must be `string`' }, + get_bufnrs = { params.option.get_bufnrs, 'function', '`opts.get_bufnrs` must be `function`' }, + }) + return params.option.keyword_pattern +end + +source.complete = function(self, params, callback) + params.option = vim.tbl_deep_extend('keep', params.option, defaults) + vim.validate({ + keyword_pattern = { params.option.keyword_pattern, 'string', '`opts.keyword_pattern` must be `string`' }, + get_bufnrs = { params.option.get_bufnrs, 'function', '`opts.get_bufnrs` must be `function`' }, + }) + + local processing = false + local bufs = self:_get_buffers(params) + for _, buf in ipairs(bufs) do + if buf.timer then + processing = true + break + end + end + + vim.defer_fn(function() + local input = string.sub(params.context.cursor_before_line, params.offset) + local items = {} + local words = {} + for _, buf in ipairs(bufs) do + for _, word in ipairs(buf:get_words()) do + if not words[word] and input ~= word then + words[word] = true + table.insert(items, { + label = word, + dup = 0, + }) + end + end + end + + callback({ + items = items, + isIncomplete = processing, + }) + end, processing and 100 or 0) +end + +--- _get_bufs +source._get_buffers = function(self, params) + local buffers = {} + for _, bufnr in ipairs(params.option.get_bufnrs()) do + if not self.buffers[bufnr] then + local new_buf = buffer.new( + bufnr, + params.option.keyword_length, + params.option.keyword_pattern + ) + new_buf.on_close_cb = function() + self.buffers[bufnr] = nil + end + new_buf:index() + new_buf:watch() + self.buffers[bufnr] = new_buf + end + table.insert(buffers, self.buffers[bufnr]) + end + + return buffers +end + +return source diff --git a/bundle/cmp-cmdline/README.md b/bundle/cmp-cmdline/README.md new file mode 100644 index 000000000..edd6c87e1 --- /dev/null +++ b/bundle/cmp-cmdline/README.md @@ -0,0 +1,13 @@ +# cmp-cmdline + +nvim-cmp source for vim's cmdline. + +# Setup + +```lua +require'cmp'.setup.cmdline(':', { + sources = { + { name = 'cmdline' } + } +}) +``` diff --git a/bundle/cmp-cmdline/after/plugin/cmp_cmdline.lua b/bundle/cmp-cmdline/after/plugin/cmp_cmdline.lua new file mode 100644 index 000000000..936e662f8 --- /dev/null +++ b/bundle/cmp-cmdline/after/plugin/cmp_cmdline.lua @@ -0,0 +1 @@ +require('cmp').register_source('cmdline', require('cmp_cmdline').new()) diff --git a/bundle/cmp-cmdline/lua/cmp_cmdline/init.lua b/bundle/cmp-cmdline/lua/cmp_cmdline/init.lua new file mode 100644 index 000000000..981b6de71 --- /dev/null +++ b/bundle/cmp-cmdline/lua/cmp_cmdline/init.lua @@ -0,0 +1,135 @@ +local cmp = require('cmp') + +local definitions = { + { + ctype = 'customlist', + regex = [=[[^[:blank:]]*$]=], + kind = cmp.lsp.CompletionItemKind.Variable, + fallback = true, + isIncomplete = false, + exec = function(arglead, cmdline, curpos) + local name = cmdline:match([=[^[ <'>]*(%a*)]=]) + if not name then + return {} + end + for name_, option in pairs(vim.api.nvim_get_commands({ builtin = false })) do + if name_ == name then + if vim.tbl_contains({ 'customlist', 'custom' }, option.complete) then + local ok, items = pcall(function() + local func = string.gsub(option.complete_arg, 's:', ('%d_'):format(option.script_id)) + return vim.fn.eval(('%s("%s", "%s", "%s")'):format( + func, + vim.fn.escape(arglead, '"'), + vim.fn.escape(cmdline, '"'), + vim.fn.escape(curpos, '"') + )) + end) + if not ok then + return {} + end + if type(items) == 'string' then + return vim.split(items, '\n') + elseif type(items) == 'table' then + return items + end + return {} + end + end + end + return {} + end + }, + { + ctype = 'cmdline', + regex = [=[^[^!].*]=], + kind = cmp.lsp.CompletionItemKind.Variable, + isIncomplete = true, + exec = function(_, cmdline, _) + return vim.fn.getcompletion(cmdline, 'cmdline') + end + }, +} + +local source = {} + +source.new = function() + return setmetatable({ + before_line = '', + offset = -1, + ctype = '', + items = {}, + }, { __index = source }) +end + +source.get_keyword_pattern = function() + return [=[[[:keyword:]-]*]=] +end + +source.get_trigger_characters = function() + return { ' ', '.' } +end + +source.is_available = function() + return vim.api.nvim_get_mode().mode == 'c' +end + +source.complete = function(self, params, callback) + local offset = 0 + local ctype = '' + local items = {} + local kind = '' + local isIncomplete = false + for _, def in ipairs(definitions) do + local s, e = vim.regex(def.regex):match_str(params.context.cursor_before_line) + if s and e then + offset = s + ctype = def.type + items = def.exec( + string.sub(params.context.cursor_before_line, s + 1), + params.context.cursor_before_line, + params.context.cursor.col + ) + kind = def.kind + isIncomplete = def.isIncomplete + if not (#items == 0 and def.fallback) then + break + end + end + end + + local labels = {} + items = vim.tbl_map(function(item) + if type(item) == 'string' then + labels[item] = true + return { label = item, kind = kind } + end + labels[item.word] = true + return { label = item.word, kind = kind } + end, items) + + local match_prefix = false + if #params.context.cursor_before_line > #self.before_line then + match_prefix = string.find(params.context.cursor_before_line, self.before_line, 1, true) == 1 + elseif #params.context.cursor_before_line < #self.before_line then + match_prefix = string.find(self.before_line, params.context.cursor_before_line, 1, true) == 1 + end + if match_prefix and self.offset == offset and self.ctype == ctype then + for _, item in ipairs(self.items) do + if not labels[item.label] then + table.insert(items, item) + end + end + end + self.before_line = params.context.cursor_before_line + self.offset = offset + self.ctype = ctype + self.items = items + + callback({ + isIncomplete = isIncomplete, + items = items, + }) +end + +return source + diff --git a/bundle/cmp-nvim-lsp/LICENSE b/bundle/cmp-nvim-lsp/LICENSE new file mode 100644 index 000000000..ae725ef17 --- /dev/null +++ b/bundle/cmp-nvim-lsp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 hrsh7th + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundle/cmp-nvim-lsp/README.md b/bundle/cmp-nvim-lsp/README.md new file mode 100644 index 000000000..1b065273d --- /dev/null +++ b/bundle/cmp-nvim-lsp/README.md @@ -0,0 +1,24 @@ +# cmp-nvim-lsp + +nvim-cmp source for neovim builtin LSP client + +# Setup + +```lua + +require'cmp'.setup { + sources = { + { name = 'nvim_lsp' } + } +} + +-- The nvim-cmp almost supports LSP's capabilities so You should advertise it to LSP servers.. +local capabilities = vim.lsp.protocol.make_client_capabilities() +capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities) + +-- The following example advertise capabilities to `clangd`. +require'lspconfig'.clangd.setup { + capabilities = capabilities, +} +``` + diff --git a/bundle/cmp-nvim-lsp/after/plugin/cmp_nvim_lsp.lua b/bundle/cmp-nvim-lsp/after/plugin/cmp_nvim_lsp.lua new file mode 100644 index 000000000..6d566fa04 --- /dev/null +++ b/bundle/cmp-nvim-lsp/after/plugin/cmp_nvim_lsp.lua @@ -0,0 +1 @@ +require('cmp_nvim_lsp').setup() diff --git a/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/init.lua b/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/init.lua new file mode 100644 index 000000000..9feb93afb --- /dev/null +++ b/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/init.lua @@ -0,0 +1,84 @@ +local source = require('cmp_nvim_lsp.source') + +local M = {} + +---Registered client and source mapping. +M.client_source_map = {} + +---Setup cmp-nvim-lsp source. +M.setup = function() + vim.cmd([[ + augroup cmp_nvim_lsp + autocmd! + autocmd InsertEnter * lua require'cmp_nvim_lsp'._on_insert_enter() + augroup END + ]]) +end + +local if_nil = function(val, default) + if val == nil then return default end + return val +end + +M.update_capabilities = function(capabilities, override) + override = override or {} + + local completionItem = capabilities.textDocument.completion.completionItem + + completionItem.snippetSupport = if_nil(override.snippetSupport, true) + completionItem.preselectSupport = if_nil(override.preselectSupport, true) + completionItem.insertReplaceSupport = if_nil(override.insertReplaceSupport, true) + completionItem.labelDetailsSupport = if_nil(override.labelDetailsSupport, true) + completionItem.deprecatedSupport = if_nil(override.deprecatedSupport, true) + completionItem.commitCharactersSupport = if_nil(override.commitCharactersSupport, true) + completionItem.tagSupport = if_nil(override.tagSupport, { valueSet = { 1 } }) + completionItem.resolveSupport = if_nil(override.resolveSupport, { + properties = { + 'documentation', + 'detail', + 'additionalTextEdits', + } + }) + + return capabilities +end + +---Refresh sources on InsertEnter. +M._on_insert_enter = function() + local cmp = require('cmp') + + local allowed_clients = {} + + -- register all active clients. + for _, client in ipairs(vim.lsp.get_active_clients()) do + allowed_clients[client.id] = client + if not M.client_source_map[client.id] then + local s = source.new(client) + if s:is_available() then + M.client_source_map[client.id] = cmp.register_source('nvim_lsp', s) + end + end + end + + -- register all buffer clients (early register before activation) + for _, client in ipairs(vim.lsp.buf_get_clients(0)) do + allowed_clients[client.id] = client + if not M.client_source_map[client.id] then + local s = source.new(client) + if s:is_available() then + M.client_source_map[client.id] = cmp.register_source('nvim_lsp', s) + end + end + end + + -- unregister stopped/detached clients. + for client_id, source_id in pairs(M.client_source_map) do + if not allowed_clients[client_id] or allowed_clients[client_id]:is_stopped() then + cmp.unregister_source(source_id) + M.client_source_map[client_id] = nil + end + end +end + +return M + diff --git a/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua b/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua new file mode 100644 index 000000000..47ddbcb34 --- /dev/null +++ b/bundle/cmp-nvim-lsp/lua/cmp_nvim_lsp/source.lua @@ -0,0 +1,117 @@ +local source = {} + +source.new = function(client) + local self = setmetatable({}, { __index = source }) + self.client = client + self.request_ids = {} + return self +end + +source.get_debug_name = function(self) + return table.concat({ 'nvim_lsp', self.client.name }, ':') +end + +source.is_available = function(self) + -- client is stopped. + if self.client.is_stopped() then + return false + end + + -- client is not attached to current buffer. + if not vim.lsp.buf_get_clients(vim.api.nvim_get_current_buf())[self.client.id] then + return false + end + + -- client has no completion capability. + if not self:_get(self.client.server_capabilities, { 'completionProvider' }) then + return false + end + return true; +end + +source.get_trigger_characters = function(self) + return self:_get(self.client.server_capabilities, { 'completionProvider', 'triggerCharacters' }) or {} +end + +source.complete = function(self, request, callback) + local params = vim.lsp.util.make_position_params() + params.context = {} + params.context.triggerKind = request.completion_context.triggerKind + params.context.triggerCharacter = request.completion_context.triggerCharacter + + self:_request('textDocument/completion', params, function(_, response) + callback(response) + end) +end + +source.resolve = function(self, completion_item, callback) + -- client is stopped. + if self.client.is_stopped() then + return callback() + end + + -- client has no completion capability. + if not self:_get(self.client.server_capabilities, { 'completionProvider', 'resolveProvider' }) then + return callback() + end + + self:_request('completionItem/resolve', completion_item, function(_, response) + callback(response or completion_item) + end) +end + +source.execute = function(self, completion_item, callback) + -- client is stopped. + if self.client.is_stopped() then + return callback() + end + + -- completion_item has no command. + if not completion_item.command then + return callback() + end + + self:_request('workspace/executeCommand', completion_item.command, function(_, _) + callback() + end) +end + +source._get = function(_, root, paths) + local c = root + for _, path in ipairs(paths) do + c = c[path] + if not c then + return nil + end + end + return c +end + +source._request = function(self, method, params, callback) + if self.request_ids[method] ~= nil then + self.client.cancel_request(self.request_ids[method]) + self.request_ids[method] = nil + end + local _, request_id + _, request_id = self.client.request(method, params, function(arg1, arg2, arg3) + if self.request_ids[method] ~= request_id then + return + end + self.request_ids[method] = nil + + -- Text changed, retry + if arg1 and arg1.code == -32801 then + self:_request(method, params, callback) + return + end + + if method == arg2 then + callback(arg1, arg3) -- old signature + else + callback(arg1, arg2) -- new signature + end + end) + self.request_ids[method] = request_id +end + +return source diff --git a/bundle/cmp-path/LICENSE b/bundle/cmp-path/LICENSE new file mode 100644 index 000000000..ae725ef17 --- /dev/null +++ b/bundle/cmp-path/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 hrsh7th + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundle/cmp-path/README.md b/bundle/cmp-path/README.md new file mode 100644 index 000000000..72a3e6feb --- /dev/null +++ b/bundle/cmp-path/README.md @@ -0,0 +1,15 @@ +# cmp-path + +nvim-cmp source for filesystem paths. + +# Setup + +```lua +require'cmp'.setup { + sources = { + { name = 'path' } + } +} +``` + + diff --git a/bundle/cmp-path/after/plugin/cmp_path.lua b/bundle/cmp-path/after/plugin/cmp_path.lua new file mode 100644 index 000000000..1d5de278c --- /dev/null +++ b/bundle/cmp-path/after/plugin/cmp_path.lua @@ -0,0 +1 @@ +require('cmp').register_source('path', require('cmp_path').new()) diff --git a/bundle/cmp-path/lua/cmp_path/init.lua b/bundle/cmp-path/lua/cmp_path/init.lua new file mode 100644 index 000000000..114aa06e9 --- /dev/null +++ b/bundle/cmp-path/lua/cmp_path/init.lua @@ -0,0 +1,207 @@ +local cmp = require'cmp' + +local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)' +local PATH_REGEX = vim.regex(([[\%(/PAT\+\)*/\zePAT*$]]):gsub('PAT', NAME_REGEX)) + +local source = {} + +local defaults = { + max_lines = 20, +} + +source.new = function() + return setmetatable({}, { __index = source }) +end + +source.get_trigger_characters = function() + return { '/', '.' } +end + +source.get_keyword_pattern = function() + return NAME_REGEX .. '*' +end + +source.complete = function(self, params, callback) + local dirname = self:_dirname(params) + if not dirname then + return callback() + end + + local stat = self:_stat(dirname) + if not stat then + return callback() + end + + self:_candidates(params, dirname, params.offset, function(err, candidates) + if err then + return callback() + end + callback(candidates) + end) +end + +source._dirname = function(self, params) + local s = PATH_REGEX:match_str(params.context.cursor_before_line) + if not s then + return nil + end + + local dirname = string.gsub(string.sub(params.context.cursor_before_line, s + 2), '%a*$', '') -- exclude '/' + local prefix = string.sub(params.context.cursor_before_line, 1, s + 1) -- include '/' + + local buf_dirname = vim.fn.expand(('#%d:p:h'):format(params.context.bufnr)) + if vim.api.nvim_get_mode().mode == 'c' then + buf_dirname = vim.fn.getcwd() + end + if prefix:match('%.%./$') then + return vim.fn.resolve(buf_dirname .. '/../' .. dirname) + end + if prefix:match('%./$') then + return vim.fn.resolve(buf_dirname .. '/' .. dirname) + end + if prefix:match('~/$') then + return vim.fn.resolve(vim.fn.expand('~') .. '/' .. dirname) + end + local env_var_name = prefix:match('%$([%a_]+)/$') + if env_var_name then + local env_var_value = vim.fn.getenv(env_var_name) + if env_var_value ~= vim.NIL then + return vim.fn.resolve(env_var_value .. '/' .. dirname) + end + end + if prefix:match('/$') then + local accept = true + -- Ignore URL components + accept = accept and not prefix:match('%a/$') + -- Ignore URL scheme + accept = accept and not prefix:match('%a+:/$') and not prefix:match('%a+://$') + -- Ignore HTML closing tags + accept = accept and not prefix:match('= count then + break + end + end + lines[#lines + 1] = '```' + return lines +end + +local function try_get_lines(file, count) + status, ret = pcall(lines_from, file, count) + if status then + return ret + else + return nil + end +end + +source._candidates = function(_, params, dirname, offset, callback) + local fs, err = vim.loop.fs_scandir(dirname) + if err then + return callback(err, nil) + end + + local items = {} + + + local include_hidden = string.sub(params.context.cursor_before_line, offset, offset) == '.' + while true do + local name, type, e = vim.loop.fs_scandir_next(fs) + if e then + return callback(type, nil) + end + if not name then + break + end + + local accept = false + accept = accept or include_hidden + accept = accept or name:sub(1, 1) ~= '.' + + -- Create items + if accept then + if type == 'directory' then + table.insert(items, { + word = name, + label = name, + insertText = name .. '/', + kind = cmp.lsp.CompletionItemKind.Folder, + }) + elseif type == 'link' then + local stat = vim.loop.fs_stat(dirname .. '/' .. name) + if stat then + if stat.type == 'directory' then + table.insert(items, { + word = name, + label = name, + insertText = name .. '/', + kind = cmp.lsp.CompletionItemKind.Folder, + }) + else + table.insert(items, { + label = name, + filterText = name, + insertText = name, + kind = cmp.lsp.CompletionItemKind.File, + data = {path = dirname .. '/' .. name}, + }) + end + end + elseif type == 'file' then + table.insert(items, { + label = name, + filterText = name, + insertText = name, + kind = cmp.lsp.CompletionItemKind.File, + data = {path = dirname .. '/' .. name}, + }) + end + end + end + callback(nil, items) +end + +source._is_slash_comment = function(_) + local commentstring = vim.bo.commentstring or '' + local no_filetype = vim.bo.filetype == '' + local is_slash_comment = false + is_slash_comment = is_slash_comment or commentstring:match('/%*') + is_slash_comment = is_slash_comment or commentstring:match('//') + return is_slash_comment and not no_filetype +end + +function source:resolve(completion_item, callback) + if completion_item.kind == cmp.lsp.CompletionItemKind.File then + completion_item.documentation = try_get_lines(completion_item.data.path, defaults.max_lines) + end + callback(completion_item) +end + +return source diff --git a/bundle/lspkind-nvim/LICENSE b/bundle/lspkind-nvim/LICENSE new file mode 100644 index 000000000..a07085d0a --- /dev/null +++ b/bundle/lspkind-nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andrey Kuznetsov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundle/lspkind-nvim/README.md b/bundle/lspkind-nvim/README.md new file mode 100644 index 000000000..f5d0e8fba --- /dev/null +++ b/bundle/lspkind-nvim/README.md @@ -0,0 +1,74 @@ +# lspkind-nvim + +This tiny plugin adds vscode-like pictograms to neovim built-in lsp: + +![Screenshot](https://github.com/onsails/lspkind-nvim/raw/images/images/screenshot.png "Screenshot") +[nvim-compe](https://github.com/hrsh7th/nvim-compe), [vim-vsnip](https://github.com/hrsh7th/vim-vsnip), [vim-vsnip-integ](https://github.com/hrsh7th/vim-vsnip-integ), [jellybeans-nvim](https://github.com/metalelf0/jellybeans-nvim) + +## Configuration + +### Option 1: vanilla Neovim LSP + +Wherever you configure lsp put the following lua command: + +```lua +require('lspkind').init({ + -- enables text annotations + -- + -- default: true + with_text = true, + + -- default symbol map + -- can be either 'default' (requires nerd-fonts font) or + -- 'codicons' for codicon preset (requires vscode-codicons font) + -- + -- default: 'default' + preset = 'codicons', + + -- override preset symbols + -- + -- default: {} + symbol_map = { + Text = "", + Method = "", + Function = "", + Constructor = "", + Field = "ﰠ", + Variable = "", + Class = "ﴯ", + Interface = "", + Module = "", + Property = "ﰠ", + Unit = "塞", + Value = "", + Enum = "", + Keyword = "", + Snippet = "", + Color = "", + File = "", + Reference = "", + Folder = "", + EnumMember = "", + Constant = "", + Struct = "פּ", + Event = "", + Operator = "", + TypeParameter = "" + }, +}) +``` + +### Option 2: [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) + +```lua +local lspkind = require('lspkind') +cmp.setup { + formatting = { + format = lspkind.cmp_format({with_text = false, maxwidth = 50}) + } +} +``` + +## Related LSP plugins + +[diaglist.nvim](https://github.com/onsails/diaglist.nvim) – live render workspace diagnostics in quickfix with current buf errors on top, buffer diagnostics in loclist diff --git a/bundle/lspkind-nvim/lua/lspkind/init.lua b/bundle/lspkind-nvim/lua/lspkind/init.lua new file mode 100644 index 000000000..84ca62ffa --- /dev/null +++ b/bundle/lspkind-nvim/lua/lspkind/init.lua @@ -0,0 +1,143 @@ +local lspkind = {} +local fmt = string.format + +local kind_presets = { + default = { +-- if you change or add symbol here +-- replace corresponding line in readme + Text = "", + Method = "", + Function = "", + Constructor = "", + Field = "ﰠ", + Variable = "", + Class = "ﴯ", + Interface = "", + Module = "", + Property = "ﰠ", + Unit = "塞", + Value = "", + Enum = "", + Keyword = "", + Snippet = "", + Color = "", + File = "", + Reference = "", + Folder = "", + EnumMember = "", + Constant = "", + Struct = "פּ", + Event = "", + Operator = "", + TypeParameter = "" + }, + codicons = { + Text = "", + Method = "", + Function = "", + Constructor = "", + Field = "", + Variable = "", + Class = "", + Interface = "", + Module = "", + Property = "", + Unit = "", + Value = "", + Enum = "", + Keyword = "", + Snippet = "", + Color = "", + File = "", + Reference = "", + Folder = "", + EnumMember = "", + Constant = "", + Struct = "", + Event = "", + Operator = "", + TypeParameter = "", + }, +} + +local kind_order = { + 'Text', 'Method', 'Function', 'Constructor', 'Field', 'Variable', 'Class', 'Interface', 'Module', + 'Property', 'Unit', 'Value', 'Enum', 'Keyword', 'Snippet', 'Color', 'File', 'Reference', 'Folder', + 'EnumMember', 'Constant', 'Struct', 'Event', 'Operator', 'TypeParameter' +} +local kind_len = 25 + +-- default true +local function opt_with_text(opts) + return opts == nil or opts['with_text'] == nil or opts['with_text'] +end + +-- default 'default' +local function opt_preset(opts) + local preset + if opts == nil or opts['preset'] == nil then + preset = 'default' + else + preset = opts['preset'] + end + return preset +end + +function lspkind.init(opts) + local preset = opt_preset(opts) + + local symbol_map = kind_presets[preset] + lspkind.symbol_map = (opts and opts['symbol_map'] and + vim.tbl_extend('force', symbol_map, opts['symbol_map'])) or symbol_map + + local symbols = {} + local len = kind_len + for i = 1, len do + local name = kind_order[i] + symbols[i] = lspkind.symbolic(name, opts) + end + + for k,v in pairs(symbols) do + require('vim.lsp.protocol').CompletionItemKind[k] = v + end +end + +lspkind.presets = kind_presets +lspkind.symbol_map = kind_presets.default + +function lspkind.symbolic(kind, opts) + local with_text = opt_with_text(opts) + + local symbol = lspkind.symbol_map[kind] + if with_text == true then + symbol = symbol and (symbol .. ' ') or '' + return fmt('%s%s', symbol, kind) + else + return symbol + end +end + +function lspkind.cmp_format(opts) + if opts == nil then + opts = {} + end + if opts.preset or opts.symbol_map then + lspkind.init(opts) + end + + return function(entry, vim_item) + vim_item.kind = lspkind.symbolic(vim_item.kind, opts) + + if opts.menu ~= nil then + vim_item.menu = opts.menu[entry.source.name] + end + + if opts.maxwidth ~= nil then + vim_item.abbr = string.sub(vim_item.abbr, 1, opts.maxwidth) + end + + return vim_item + end +end + +return lspkind diff --git a/bundle/nvim-cmp/.githooks/pre-commit b/bundle/nvim-cmp/.githooks/pre-commit new file mode 100644 index 000000000..1d0c8d5d8 --- /dev/null +++ b/bundle/nvim-cmp/.githooks/pre-commit @@ -0,0 +1,9 @@ +#!/bin/sh + +DIR="$(dirname $(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )))" + +cd $DIR +make pre-commit +for FILE in `git diff --staged --name-only`; do + git add $FILE +done diff --git a/bundle/nvim-cmp/.github/FUNDING.yml b/bundle/nvim-cmp/.github/FUNDING.yml new file mode 100644 index 000000000..ccdeccc61 --- /dev/null +++ b/bundle/nvim-cmp/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [hrsh7th] diff --git a/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..aa60d97ac --- /dev/null +++ b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +**Describe the bug** + + + +**Minimal config based on [this](https://github.com/hrsh7th/nvim-cmp/blob/main/utils/vimrc.vim)** + +```vim +``` + +**To Reproduce** + +1. ... +2. ... +3. ... + +**Expected behavior** + + + +**Additional context** diff --git a/bundle/nvim-cmp/.github/workflows/integration.yaml b/bundle/nvim-cmp/.github/workflows/integration.yaml new file mode 100644 index 000000000..9dee7f2af --- /dev/null +++ b/bundle/nvim-cmp/.github/workflows/integration.yaml @@ -0,0 +1,50 @@ +name: integration + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + integration: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + default: true + override: true + + - name: Setup neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + + - name: Setup lua + uses: leafo/gh-actions-lua@v8 + with: + luaVersion: "luajit-2.1.0-beta3" + + - name: Setup luarocks + uses: leafo/gh-actions-luarocks@v4 + + - name: Setup tools + shell: bash + run: | + sudo apt install -y curl unzip --no-install-recommends + bash ./utils/install_stylua.sh + luarocks install luacheck + luarocks install vusted + + - name: Run tests + shell: bash + run: make integration + diff --git a/bundle/nvim-cmp/.gitignore b/bundle/nvim-cmp/.gitignore new file mode 100644 index 000000000..8ad54597a --- /dev/null +++ b/bundle/nvim-cmp/.gitignore @@ -0,0 +1 @@ +utils/stylua diff --git a/bundle/nvim-cmp/.luacheckrc b/bundle/nvim-cmp/.luacheckrc new file mode 100644 index 000000000..004e8c101 --- /dev/null +++ b/bundle/nvim-cmp/.luacheckrc @@ -0,0 +1,2 @@ +globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'assert', 'async' } +max_line_length = false diff --git a/bundle/nvim-cmp/LICENSE b/bundle/nvim-cmp/LICENSE new file mode 100644 index 000000000..ae725ef17 --- /dev/null +++ b/bundle/nvim-cmp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 hrsh7th + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bundle/nvim-cmp/Makefile b/bundle/nvim-cmp/Makefile new file mode 100644 index 000000000..731b3f283 --- /dev/null +++ b/bundle/nvim-cmp/Makefile @@ -0,0 +1,24 @@ +.PHONY: fmt +fmt: + stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua + +.PHONY: lint +lint: + luacheck ./lua + +.PHONY: test +test: + vusted --output=gtest ./lua + +.PHONY: pre-commit +pre-commit: + ./utils/stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua + luacheck lua + vusted lua + +.PHONY: integration +integration: + ./utils/stylua --config-path stylua.toml --check --glob 'lua/**/*.lua' -- lua + luacheck lua + vusted lua + diff --git a/bundle/nvim-cmp/README.md b/bundle/nvim-cmp/README.md new file mode 100644 index 000000000..30298442a --- /dev/null +++ b/bundle/nvim-cmp/README.md @@ -0,0 +1,776 @@ +# nvim-cmp + +A completion engine plugin for neovim written in Lua. +Completion sources are installed from external repositories and "sourced". + + + +Readme! +==================== + +1. nvim-cmp's breaking changes are documented [here](https://github.com/hrsh7th/nvim-cmp/issues/231). +2. This is my hobby project. You can support me via GitHub sponsors. +3. Bug reports are welcome, but I might not fix if you don't provide a minimal reproduction configuration and steps. + + +Concept +==================== + +- No flicker +- Works properly +- Fully customizable via Lua functions +- Fully supports LSP's completion capabilities + - Snippets + - CommitCharacters + - TriggerCharacters + - TextEdit and InsertReplaceTextEdit + - AdditionalTextEdits + - Markdown documentation + - Execute commands (Some LSP server needs it to auto-importing. e.g. `sumneko_lua` or `purescript-language-server`) + - Preselect + - CompletionItemTags +- Support pairs-wise plugin automatically + + +Setup +==================== + +### Recommended Configuration + +This example configuration uses `vim-plug` as the plugin manager. + +```viml +call plug#begin(s:plug_dir) +Plug 'neovim/nvim-lspconfig' +Plug 'hrsh7th/cmp-nvim-lsp' +Plug 'hrsh7th/cmp-buffer' +Plug 'hrsh7th/cmp-path' +Plug 'hrsh7th/cmp-cmdline' +Plug 'hrsh7th/nvim-cmp' + +" For vsnip users. +Plug 'hrsh7th/cmp-vsnip' +Plug 'hrsh7th/vim-vsnip' + +" For luasnip users. +" Plug 'L3MON4D3/LuaSnip' +" Plug 'saadparwaiz1/cmp_luasnip' + +" For ultisnips users. +" Plug 'SirVer/ultisnips' +" Plug 'quangnguyen30192/cmp-nvim-ultisnips' + +" For snippy users. +" Plug 'dcampos/nvim-snippy' +" Plug 'dcampos/cmp-snippy' + +call plug#end() + +set completeopt=menu,menuone,noselect + +lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }), + [''] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `` mapping. + [''] = cmp.mapping({ + i = cmp.mapping.abort(), + c = cmp.mapping.close(), + }), + [''] = cmp.mapping.confirm({ select = true }), + }, + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'vsnip' }, -- For vsnip users. + -- { name = 'luasnip' }, -- For luasnip users. + -- { name = 'ultisnips' }, -- For ultisnips users. + -- { name = 'snippy' }, -- For snippy users. + }, { + { name = 'buffer' }, + }) + }) + + -- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore). + cmp.setup.cmdline('/', { + sources = { + { name = 'buffer' } + } + }) + + -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). + cmp.setup.cmdline(':', { + sources = cmp.config.sources({ + { name = 'path' } + }, { + { name = 'cmdline' } + }) + }) + + -- Setup lspconfig. + local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()) + -- Replace with each lsp server you've enabled. + require('lspconfig')[''].setup { + capabilities = capabilities + } +EOF +``` + +### Where can I find more completion sources? + +You can search for various completion sources [here](https://github.com/topics/nvim-cmp). + + +Configuration options +==================== + +You can specify the following configuration options via `cmp.setup { ... }`. + +The configuration options will be merged with the [default config](./lua/cmp/config/default.lua). + +If you want to remove a default option, set it to `false`. + + +#### mapping (type: table) + +Defines the action of each key mapping. The following lists all the built-in actions: + +- `cmp.mapping.select_prev_item({ cmp.SelectBehavior.{Insert,Select} })` +- `cmp.mapping.select_next_item({ cmp.SelectBehavior.{Insert,Select} })` +- `cmp.mapping.scroll_docs(number)` +- `cmp.mapping.complete()` +- `cmp.mapping.close()` +- `cmp.mapping.abort()` +- `cmp.mapping.confirm({ select = bool, behavior = cmp.ConfirmBehavior.{Insert,Replace} })`: If `select` is true and you haven't select any item, automatically selects the first item. + +You can configure `nvim-cmp` to use these `cmp.mapping` like this: + +```lua +mapping = { + [''] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }), + [''] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }), + [''] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }), + [''] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }), + [''] = cmp.mapping.scroll_docs(-4), + [''] = cmp.mapping.scroll_docs(4), + [''] = cmp.mapping.complete(), + [''] = cmp.mapping.close(), + [''] = cmp.mapping.confirm({ + behavior = cmp.ConfirmBehavior.Replace, + select = true, + }) +} +``` + +In addition, the mapping mode can be specified with the help of `cmp.mapping(...)`. The default is the insert mode (i) if not specified. + +```lua +mapping = { + ... + [''] = cmp.mapping(cmp.mapping.select_next_item(), { 'i', 's' }) + ... +} +``` + +The mapping mode can also be specified using a table. This is particularly useful to set different actions for each mode. + +```lua +mapping = { + [''] = cmp.mapping({ + i = cmp.mapping.confirm({ select = true }), + c = cmp.mapping.confirm({ select = false }), + }) +} +``` + +You can also provide a custom function as the action. + +```lua +mapping = { + [''] = function(fallback) + if ...some_condition... then + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('...', true, true, true), 'n', true) + else + fallback() -- The fallback function is treated as original mapped key. In this case, it might be ``. + end + end, +} +``` + +#### enabled (type: fun(): boolean|boolean) + +A boolean value, or a function returning a boolean, that specifies whether to enable nvim-cmp's features or not. + +Default: + +```lua +function() + return vim.api.nvim_buf_get_option(0, 'buftype') ~= 'prompt' +end +``` + +#### sources (type: table) + +Lists all the global completion sources that will be enabled in all buffers. +The order of the list defines the priority of each source. See the +*sorting.priority_weight* option below. + +It is possible to set up different sources for different filetypes using +`FileType` autocommand and `cmp.setup.buffer` to override the global +configuration. + +```viml +" Setup buffer configuration (nvim-lua source only enables in Lua filetype). +autocmd FileType lua lua require'cmp'.setup.buffer { +\ sources = { +\ { name = 'nvim_lua' }, +\ { name = 'buffer' }, +\ }, +\ } +``` + +Note that the source name isn't necessarily the source repository name. Source +names are defined in the source repository README files. For example, look at +the [hrsh7th/cmp-buffer](https://github.com/hrsh7th/cmp-buffer) source README +which defines the source name as `buffer`. + +#### sources[number].name (type: string) + +The source name. + +#### sources[number].opts (type: table) + +The source customization options. It is defined by each source. + +#### sources[number].priority (type: number|nil) + +The priority of the source. If you don't specify it, the source priority will +be determined by the default algorithm (see `sorting.priority_weight`). + +#### sources[number].keyword_pattern (type: string) + +The source specific keyword_pattern for override. + +#### sources[number].keyword_length (type: number) + +The source specific keyword_length for override. + +#### sources[number].max_item_count (type: number) + +The source specific maximum item count. + +#### sources[number].group_index (type: number) + +The source group index. + +You can call built-in utility like `cmp.config.sources({ { name = 'a' } }, { { name = 'b' } })`. + +#### preselect (type: cmp.PreselectMode) + +Specify preselect mode. The following modes are available. + +- `cmp.PreselectMode.Item` + - If the item has `preselect = true`, `nvim-cmp` will preselect it. +- `cmp.PreselectMode.None` + - Disable preselect feature. + +Default: `cmp.PreselectMode.Item` + +#### completion.autocomplete (type: cmp.TriggerEvent[]) + +Which events should trigger `autocompletion`. + +If you set this to `false`, `nvim-cmp` will not perform completion +automatically. You can still use manual completion though (like omni-completion +via the `cmp.mapping.complete` function). + +Default: `{ types.cmp.TriggerEvent.TextChanged }` + +#### completion.keyword_pattern (type: string) + +The default keyword pattern. This value will be used if a source does not set +a source specific pattern. + +Default: `[[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]]` + +#### completion.keyword_length (type: number) + +The minimum length of a word to complete on; e.g., do not try to complete when the +length of the word to the left of the cursor is less than `keyword_length`. + +Default: `1` + +#### completion.get_trigger_characters (type: fun(trigger_characters: string[]): string[]) + +The function to resolve trigger_characters. + +Default: `function(trigger_characters) return trigger_characters end` + +#### completion.completeopt (type: string) + +vim's `completeopt` setting. Warning: Be careful when changing this value. + +Default: `menu,menuone,noselect` + +#### confirmation.default_behavior (type: cmp.ConfirmBehavior) + +A default `cmp.ConfirmBehavior` value when to use confirmed by commitCharacters + +Default: `cmp.ConfirmBehavior.Insert` + +#### confirmation.get_commit_characters (type: fun(commit_characters: string[]): string[]) + +The function to resolve commit_characters. + +#### sorting.priority_weight (type: number) + +The score multiplier of source when calculating the items' priorities. +Specifically, each item's original priority (given by its corresponding source) +will be increased by `#sources - (source_index - 1)` multiplied by +`priority_weight`. That is, the final priority is calculated by the following formula: + +`final_score = orig_score + ((#sources - (source_index - 1)) * sorting.priority_weight)` + +Default: `2` + +#### sorting.comparators (type: function[]) + +When sorting completion items, the sort logic tries each function in +`sorting.comparators` consecutively when comparing two items. The first function +to return something other than `nil` takes precedence. + +Each function must return `boolean|nil`. + +You can use the preset functions from `cmp.config.compare.*`. + +Default: +```lua +{ + cmp.config.compare.offset, + cmp.config.compare.exact, + cmp.config.compare.score, + cmp.config.compare.recently_used, + cmp.config.compare.kind, + cmp.config.compare.sort_text, + cmp.config.compare.length, + cmp.config.compare.order, +} +``` + +#### documentation (type: false | cmp.DocumentationConfig) + +If set to `false`, the documentation of each item will not be shown. +Else, a table representing documentation configuration should be provided. +The following are the possible options: + +#### documentation.border (type: string[]) + +Border characters used for documentation window. + +#### documentation.winhighlight (type: string) + +A neovim's `winhighlight` option for documentation window. + +#### documentation.maxwidth (type: number) + +The documentation window's max width. + +#### documentation.maxheight (type: number) + +The documentation window's max height. + +#### documentation.zindex (type: number) + +The documentation window's zindex. + +#### formatting.fields (type: cmp.ItemField[]) + +The order of item's fields for completion menu. + +#### formatting.format (type: fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem) + +A function to customize completion menu. +The return value is defined by vim. See `:help complete-items`. + +You can display the fancy icons to completion-menu with [lspkind-nvim](https://github.com/onsails/lspkind-nvim). + +Please see [FAQ](#how-to-show-name-of-item-kind-and-source-like-compe) if you would like to show symbol-text (e.g. function) and source (e.g. LSP) like compe. + +```lua +local lspkind = require('lspkind') +cmp.setup { + formatting = { + format = lspkind.cmp_format(), + }, +} +``` + +See the [wiki](https://github.com/hrsh7th/nvim-cmp/wiki/Menu-Appearance#basic-customisations) for more info on customizing menu appearance. + +#### experimental.native_menu (type: boolean) + +Use vim's native completion menu instead of custom floating menu. + +Default: `false` + +#### experimental.ghost_text (type: cmp.GhostTextConfig | false) + +Specify whether to display ghost text. + +Default: `false` + +Commands +==================== + +#### `CmpStatus` + +Show the source statuses + +Autocmds +==================== + +#### `cmp#ready` + +Invoke after nvim-cmp setup. + +Highlights +==================== + +#### `CmpItemAbbr` + +The abbr field. + +#### `CmpItemAbbrDeprecated` + +The deprecated item's abbr field. + +#### `CmpItemAbbrMatch` + +The matched characters highlight. + +#### `CmpItemAbbrMatchFuzzy` + +The fuzzy matched characters highlight. + +#### `CmpItemKind` + +The kind field. + +#### `CmpItemMenu` + +The menu field. + +Programatic API +==================== + +You can use the following APIs. + +#### `cmp.event:on(name: string, callback: string)` + +Subscribes to the following events. + +- `confirm_done` + +#### `cmp.get_config()` + +Returns the current configuration. + +#### `cmp.visible()` + +Returns the completion menu is visible or not. + +NOTE: This method returns true if the native popup menu is visible, for the convenience of defining mappings. + +#### `cmp.get_selected_entry()` + +Returns the selected entry. + +#### `cmp.get_active_entry()` + +Returns the active entry. + +NOTE: The `preselected` entry does not returned from this method. + +#### `cmp.confirm({ select = boolean, behavior = cmp.ConfirmBehavior.{Insert,Replace} }, callback)` + +Confirms the current selected item, if possible. If `select` is true and no item has been selected, selects the first item. + +#### `cmp.complete()` + +Invokes manual completion. + +#### `cmp.close()` + +Closes the current completion menu. + +#### `cmp.abort()` + +Closes the current completion menu and restore the current line (similar to native `` behavior). + +#### `cmp.select_next_item({ cmp.SelectBehavior.{Insert,Select} })` + +Selects the next completion item if possible. + +#### `cmp.select_prev_item({ cmp.SelectBehavior.{Insert,Select} })` + +Selects the previous completion item if possible. + +#### `cmp.scroll_docs(delta)` + +Scrolls the documentation window by `delta` lines, if possible. + + +FAQ +==================== + +#### I can't get the specific source working. + +Check the output of command `:CmpStatus`. It is likely that you specify the source name incorrectly. + +NOTE: `nvim_lsp` will be sourced on `InsertEnter` event. It will show as `unknown source`, but this isn't a problem. + + +#### What is the `pair-wise plugin automatically supported`? + +Some pair-wise plugin set up the mapping automatically. +For example, `vim-endwise` will map `` even if you don't do any mapping instructions for the plugin. + +But I think the user want to override `` mapping only when the mapping item is selected. + +The `nvim-cmp` does it automatically. + +The following configuration will be working as + +1. If the completion-item is selected, will be working as `cmp.mapping.confirm`. +2. If the completion-item isn't selected, will be working as vim-endwise feature. + +```lua +mapping = { + [''] = cmp.mapping.confirm() +} +``` + + +#### What is the equivalence of nvim-compe's `preselect = 'always'`? + +You can use the following configuration. + +```lua +cmp.setup { + completion = { + completeopt = 'menu,menuone,noinsert', + } +} +``` + +#### I don't use a snippet plugin. + +At the moment, nvim-cmp requires a snippet engine to function correctly. +You need to specify one in `snippet`. + +```lua +snippet = { + -- REQUIRED - you must specify a snippet engine + expand = function(args) + vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users. + -- require('luasnip').lsp_expand(args.body) -- For `luasnip` users. + -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. + -- require'snippy'.expand_snippet(args.body) -- For `snippy` users. + end, +} +``` + + +#### I dislike auto-completion + +You can use `nvim-cmp` without auto-completion like this. + +```lua +cmp.setup { + completion = { + autocomplete = false + } +} +``` + + +#### How to disable nvim-cmp on the specific buffer? + +You can specify `enabled = false` like this. + +```vim +autocmd FileType TelescopePrompt lua require('cmp').setup.buffer { enabled = false } +``` + + +#### nvim-cmp is slow. + +I've optimized `nvim-cmp` as much as possible, but there are currently some known / unfixable issues. + +**`cmp-buffer` source and too large buffer** + +The `cmp-buffer` source makes an index of the current buffer so if the current buffer is too large, it will slowdown the main UI thread. + +**`vim.lsp.set_log_level`** + +This setting will cause the filesystem operation for each LSP payload. +This will greatly slow down nvim-cmp (and other LSP related features). + + +#### How to show name of item kind and source (like compe)? + +```lua +formatting = { + format = require("lspkind").cmp_format({with_text = true, menu = ({ + buffer = "[Buffer]", + nvim_lsp = "[LSP]", + luasnip = "[LuaSnip]", + nvim_lua = "[Lua]", + latex_symbols = "[Latex]", + })}), +}, +``` + + +#### How to set up mappings? + +You can find all the mapping examples in [Example mappings](https://github.com/hrsh7th/nvim-cmp/wiki/Example-mappings). + + +Create a Custom Source +==================== + +Warning: If the LSP spec is changed, nvim-cmp will keep up to it without an announcement. + +If you publish `nvim-cmp` source to GitHub, please add `nvim-cmp` topic for the repo. + +You should read [cmp types](/lua/cmp/types) and [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/specification-current/) to create sources. + +- The `complete` function is required. Others can be omitted. +- The `callback` argument must always be called. +- The custom source should only use `require('cmp')`. +- The custom source can specify `word` property to CompletionItem. (It isn't an LSP specification but supported as a special case.) + +Here is an example of a custom source. + +```lua +local source = {} + +---Source constructor. +source.new = function() + local self = setmetatable({}, { __index = source }) + self.your_awesome_variable = 1 + return self +end + +---Return the source is available or not. +---@return boolean +function source:is_available() + return true +end + +---Return the source name for some information. +function source:get_debug_name() + return 'example' +end + +---Return keyword pattern which will be used... +--- 1. Trigger keyword completion +--- 2. Detect menu start offset +--- 3. Reset completion state +---@param params cmp.SourceBaseApiParams +---@return string +function source:get_keyword_pattern(params) + return '???' +end + +---Return trigger characters. +---@param params cmp.SourceBaseApiParams +---@return string[] +function source:get_trigger_characters(params) + return { ??? } +end + +---Invoke completion (required). +--- If you want to abort completion, just call the callback without arguments. +---@param params cmp.SourceCompletionApiParams +---@param callback fun(response: lsp.CompletionResponse|nil) +function source:complete(params, callback) + callback({ + { label = 'January' }, + { label = 'February' }, + { label = 'March' }, + { label = 'April' }, + { label = 'May' }, + { label = 'June' }, + { label = 'July' }, + { label = 'August' }, + { label = 'September' }, + { label = 'October' }, + { label = 'November' }, + { label = 'December' }, + }) +end + +---Resolve completion item that will be called when the item selected or before the item confirmation. +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +function source:resolve(completion_item, callback) + callback(completion_item) +end + +---Execute command that will be called when after the item confirmation. +---@param completion_item lsp.CompletionItem +---@param callback fun(completion_item: lsp.CompletionItem|nil) +function source:execute(completion_item, callback) + callback(completion_item) +end + +require('cmp').register_source(source.new()) +``` + +You can also create a source by Vim script like this (This is useful to support callback style plugins). + +- If you want to return `boolean`, you must return `v:true`/`v:false` instead of `0`/`1`. + +```vim +let s:source = {} + +function! s:source.new() abort + return extend(deepcopy(s:source)) +endfunction + +" The other APIs are also available. + +function! s:source.complete(params, callback) abort + call a:callback({ + \ { 'label': 'January' }, + \ { 'label': 'February' }, + \ { 'label': 'March' }, + \ { 'label': 'April' }, + \ { 'label': 'May' }, + \ { 'label': 'June' }, + \ { 'label': 'July' }, + \ { 'label': 'August' }, + \ { 'label': 'September' }, + \ { 'label': 'October' }, + \ { 'label': 'November' }, + \ { 'label': 'December' }, + \ }) +endfunction + +call cmp#register_source('month', s:source.new()) +``` diff --git a/bundle/nvim-cmp/autoload/cmp.vim b/bundle/nvim-cmp/autoload/cmp.vim new file mode 100644 index 000000000..8fcdeeb4a --- /dev/null +++ b/bundle/nvim-cmp/autoload/cmp.vim @@ -0,0 +1,76 @@ +let s:bridge_id = 0 +let s:sources = {} + +" +" cmp#apply_text_edits +" +" TODO: Remove this if nvim's apply_text_edits will be improved. +" +function! cmp#apply_text_edits(bufnr, text_edits) abort + if !exists('s:TextEdit') + let s:TextEdit = vital#cmp#import('VS.LSP.TextEdit') + endif + call s:TextEdit.apply(a:bufnr, a:text_edits) +endfunction + +" +" cmp#register_source +" +function! cmp#register_source(name, source) abort + let l:methods = [] + for l:method in ['is_available', 'get_debug_name', 'get_trigger_characters', 'get_keyword_pattern', 'complete', 'execute', 'resolve'] + if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func + call add(l:methods, l:method) + endif + endfor + + let s:bridge_id += 1 + let a:source.bridge_id = s:bridge_id + let a:source.id = luaeval('require("cmp").register_source(_A[1], require("cmp.vim_source").new(_A[2], _A[3]))', [a:name, s:bridge_id, l:methods]) + let s:sources[s:bridge_id] = a:source + return a:source.id +endfunction + +" +" cmp#unregister_source +" +function! cmp#unregister_source(id) abort + if has_key(s:sources, a:id) + unlet s:sources[a:id] + endif + call luaeval('require("cmp").unregister_source(_A)', a:id) +endfunction + +" +" cmp#_method +" +function! cmp#_method(bridge_id, method, args) abort + try + let l:source = s:sources[a:bridge_id] + if a:method ==# 'is_available' + return l:source[a:method]() + elseif a:method ==# 'get_debug_name' + return l:source[a:method]() + elseif a:method ==# 'get_keyword_pattern' + return l:source[a:method](a:args[0]) + elseif a:method ==# 'get_trigger_characters' + return l:source[a:method](a:args[0]) + elseif a:method ==# 'complete' + return l:source[a:method](a:args[0], s:callback(a:args[1])) + elseif a:method ==# 'resolve' + return l:source[a:method](a:args[0], s:callback(a:args[1])) + elseif a:method ==# 'execute' + return l:source[a:method](a:args[0], s:callback(a:args[1])) + endif + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + endtry + return v:null +endfunction + +" +" s:callback +" +function! s:callback(id) abort + return { ... -> luaeval('require("cmp.vim_source").on_callback(_A[1], _A[2])', [a:id, a:000]) } +endfunction diff --git a/bundle/nvim-cmp/autoload/vital/_cmp.vim b/bundle/nvim-cmp/autoload/vital/_cmp.vim new file mode 100644 index 000000000..55104952e --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp.vim @@ -0,0 +1,9 @@ +let s:_plugin_name = expand(':t:r') + +function! vital#{s:_plugin_name}#new() abort + return vital#{s:_plugin_name[1:]}#new() +endfunction + +function! vital#{s:_plugin_name}#function(funcname) abort + silent! return function(a:funcname) +endfunction diff --git a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Position.vim b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Position.vim new file mode 100644 index 000000000..f53c76a42 --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Position.vim @@ -0,0 +1,62 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_cmp#VS#LSP#Position#import() abort', printf("return map({'cursor': '', 'vim_to_lsp': '', 'lsp_to_vim': ''}, \"vital#_cmp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" cursor +" +function! s:cursor() abort + return s:vim_to_lsp('%', getpos('.')[1 : 3]) +endfunction + +" +" vim_to_lsp +" +function! s:vim_to_lsp(expr, pos) abort + let l:line = s:_get_buffer_line(a:expr, a:pos[0]) + if l:line is v:null + return { + \ 'line': a:pos[0] - 1, + \ 'character': a:pos[1] - 1 + \ } + endif + + return { + \ 'line': a:pos[0] - 1, + \ 'character': strchars(strpart(l:line, 0, a:pos[1] - 1)) + \ } +endfunction + +" +" lsp_to_vim +" +function! s:lsp_to_vim(expr, position) abort + let l:line = s:_get_buffer_line(a:expr, a:position.line + 1) + if l:line is v:null + return [a:position.line + 1, a:position.character + 1] + endif + return [a:position.line + 1, byteidx(l:line, a:position.character) + 1] +endfunction + +" +" _get_buffer_line +" +function! s:_get_buffer_line(expr, lnum) abort + try + let l:expr = bufnr(a:expr) + catch /.*/ + let l:expr = a:expr + endtry + if bufloaded(l:expr) + return get(getbufline(l:expr, a:lnum), 0, v:null) + elseif filereadable(a:expr) + return get(readfile(a:expr, '', a:lnum), 0, v:null) + endif + return v:null +endfunction + diff --git a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Text.vim b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Text.vim new file mode 100644 index 000000000..0c093149a --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Text.vim @@ -0,0 +1,23 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_cmp#VS#LSP#Text#import() abort', printf("return map({'normalize_eol': '', 'split_by_eol': ''}, \"vital#_cmp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" normalize_eol +" +function! s:normalize_eol(text) abort + return substitute(a:text, "\r\n\\|\r", "\n", 'g') +endfunction + +" +" split_by_eol +" +function! s:split_by_eol(text) abort + return split(a:text, "\r\n\\|\r\\|\n", v:true) +endfunction + diff --git a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/TextEdit.vim b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/TextEdit.vim new file mode 100644 index 000000000..09a8df81a --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/TextEdit.vim @@ -0,0 +1,185 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_cmp#VS#LSP#TextEdit#import() abort', printf("return map({'_vital_depends': '', 'apply': '', '_vital_loaded': ''}, \"vital#_cmp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" _vital_loaded +" +function! s:_vital_loaded(V) abort + let s:Text = a:V.import('VS.LSP.Text') + let s:Position = a:V.import('VS.LSP.Position') + let s:Buffer = a:V.import('VS.Vim.Buffer') + let s:Option = a:V.import('VS.Vim.Option') +endfunction + +" +" _vital_depends +" +function! s:_vital_depends() abort + return ['VS.LSP.Text', 'VS.LSP.Position', 'VS.Vim.Buffer', 'VS.Vim.Option'] +endfunction + +" +" apply +" +function! s:apply(path, text_edits) abort + let l:current_bufname = bufname('%') + let l:current_position = s:Position.cursor() + + let l:target_bufnr = s:_switch(a:path) + call s:_substitute(l:target_bufnr, a:text_edits, l:current_position) + let l:current_bufnr = s:_switch(l:current_bufname) + + if l:current_bufnr == l:target_bufnr + call cursor(s:Position.lsp_to_vim('%', l:current_position)) + endif +endfunction + +" +" _substitute +" +function! s:_substitute(bufnr, text_edits, current_position) abort + try + " Save state. + let l:Restore = s:Option.define({ + \ 'foldenable': '0', + \ }) + let l:view = winsaveview() + + " Apply substitute. + let [l:fixeol, l:text_edits] = s:_normalize(a:bufnr, a:text_edits) + for l:text_edit in l:text_edits + let l:start = s:Position.lsp_to_vim(a:bufnr, l:text_edit.range.start) + let l:end = s:Position.lsp_to_vim(a:bufnr, l:text_edit.range.end) + let l:text = s:Text.normalize_eol(l:text_edit.newText) + execute printf('noautocmd keeppatterns keepjumps silent %ssubstitute/\%%%sl\%%%sc\_.\{-}\%%%sl\%%%sc/\=l:text/%se', + \ l:start[0], + \ l:start[0], + \ l:start[1], + \ l:end[0], + \ l:end[1], + \ &gdefault ? 'g' : '' + \ ) + call s:_fix_cursor_position(a:current_position, l:text_edit, s:Text.split_by_eol(l:text)) + endfor + + " Remove last empty line if fixeol enabled. + if l:fixeol && getline('$') ==# '' + noautocmd keeppatterns keepjumps silent $delete _ + endif + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + finally + " Restore state. + call l:Restore() + call winrestview(l:view) + endtry +endfunction + +" +" _fix_cursor_position +" +function! s:_fix_cursor_position(position, text_edit, lines) abort + let l:lines_len = len(a:lines) + let l:range_len = (a:text_edit.range.end.line - a:text_edit.range.start.line) + 1 + + if a:text_edit.range.end.line < a:position.line + let a:position.line += l:lines_len - l:range_len + elseif a:text_edit.range.end.line == a:position.line && a:text_edit.range.end.character <= a:position.character + let a:position.line += l:lines_len - l:range_len + let a:position.character = strchars(a:lines[-1]) + (a:position.character - a:text_edit.range.end.character) + if l:lines_len == 1 + let a:position.character += a:text_edit.range.start.character + endif + endif +endfunction + +" +" _normalize +" +function! s:_normalize(bufnr, text_edits) abort + let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits] + let l:text_edits = s:_range(l:text_edits) + let l:text_edits = sort(l:text_edits, function('s:_compare')) + let l:text_edits = reverse(l:text_edits) + return s:_fix_text_edits(a:bufnr, l:text_edits) +endfunction + +" +" _range +" +function! s:_range(text_edits) abort + let l:text_edits = [] + for l:text_edit in a:text_edits + if type(l:text_edit) != type({}) + continue + endif + if l:text_edit.range.start.line > l:text_edit.range.end.line || ( + \ l:text_edit.range.start.line == l:text_edit.range.end.line && + \ l:text_edit.range.start.character > l:text_edit.range.end.character + \ ) + let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start } + endif + let l:text_edits += [l:text_edit] + endfor + return l:text_edits +endfunction + +" +" _compare +" +function! s:_compare(text_edit1, text_edit2) abort + let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line + if l:diff == 0 + return a:text_edit1.range.start.character - a:text_edit2.range.start.character + endif + return l:diff +endfunction + +" +" _fix_text_edits +" +function! s:_fix_text_edits(bufnr, text_edits) abort + let l:max = s:Buffer.get_line_count(a:bufnr) + + let l:fixeol = v:false + let l:text_edits = [] + for l:text_edit in a:text_edits + if l:max <= l:text_edit.range.start.line + let l:text_edit.range.start.line = l:max - 1 + let l:text_edit.range.start.character = strchars(get(getbufline(a:bufnr, '$'), 0, '')) + let l:text_edit.newText = "\n" . l:text_edit.newText + let l:fixeol = &fixendofline && !&binary + endif + if l:max <= l:text_edit.range.end.line + let l:text_edit.range.end.line = l:max - 1 + let l:text_edit.range.end.character = strchars(get(getbufline(a:bufnr, '$'), 0, '')) + let l:fixeol = &fixendofline && !&binary + endif + call add(l:text_edits, l:text_edit) + endfor + + return [l:fixeol, l:text_edits] +endfunction + +" +" _switch +" +function! s:_switch(path) abort + let l:curr = bufnr('%') + let l:next = bufnr(a:path) + if l:next >= 0 + if l:curr != l:next + execute printf('noautocmd keepalt keepjumps %sbuffer!', bufnr(a:path)) + endif + else + execute printf('noautocmd keepalt keepjumps edit! %s', fnameescape(a:path)) + endif + return bufnr('%') +endfunction + diff --git a/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Buffer.vim b/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Buffer.vim new file mode 100644 index 000000000..df58dcd8d --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Buffer.vim @@ -0,0 +1,126 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_cmp#VS#Vim#Buffer#import() abort', printf("return map({'get_line_count': '', 'do': '', 'create': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_cmp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +let s:Do = { -> {} } + +let g:___VS_Vim_Buffer_id = get(g:, '___VS_Vim_Buffer_id', 0) + +" +" get_line_count +" +if exists('*nvim_buf_line_count') + function! s:get_line_count(bufnr) abort + return nvim_buf_line_count(a:bufnr) + endfunction +elseif has('patch-8.2.0019') + function! s:get_line_count(bufnr) abort + return getbufinfo(a:bufnr)[0].linecount + endfunction +else + function! s:get_line_count(bufnr) abort + if bufnr('%') == bufnr(a:bufnr) + return line('$') + endif + return len(getbufline(a:bufnr, '^', '$')) + endfunction +endif + +" +" create +" +function! s:create(...) abort + let g:___VS_Vim_Buffer_id += 1 + let l:bufname = printf('VS.Vim.Buffer: %s: %s', + \ g:___VS_Vim_Buffer_id, + \ get(a:000, 0, 'VS.Vim.Buffer.Default') + \ ) + return s:load(l:bufname) +endfunction + +" +" ensure +" +function! s:ensure(expr) abort + if !bufexists(a:expr) + if type(a:expr) == type(0) + throw printf('VS.Vim.Buffer: `%s` is not valid expr.', a:expr) + endif + badd `=a:expr` + endif + return bufnr(a:expr) +endfunction + +" +" load +" +if exists('*bufload') + function! s:load(expr) abort + let l:bufnr = s:ensure(a:expr) + if !bufloaded(l:bufnr) + call bufload(l:bufnr) + endif + return l:bufnr + endfunction +else + function! s:load(expr) abort + let l:curr_bufnr = bufnr('%') + try + let l:bufnr = s:ensure(a:expr) + execute printf('keepalt keepjumps silent %sbuffer', l:bufnr) + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + finally + execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr) + endtry + return l:bufnr + endfunction +endif + +" +" do +" +function! s:do(bufnr, func) abort + let l:curr_bufnr = bufnr('%') + if l:curr_bufnr == a:bufnr + call a:func() + return + endif + + try + execute printf('noautocmd keepalt keepjumps silent %sbuffer', a:bufnr) + call a:func() + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + finally + execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr) + endtry +endfunction + +" +" pseudo +" +function! s:pseudo(filepath) abort + if !filereadable(a:filepath) + throw printf('VS.Vim.Buffer: `%s` is not valid filepath.', a:filepath) + endif + + " create pseudo buffer + let l:bufname = printf('VSVimBufferPseudo://%s', a:filepath) + if bufexists(l:bufname) + return s:ensure(l:bufname) + endif + + let l:bufnr = s:ensure(l:bufname) + let l:group = printf('VS_Vim_Buffer_pseudo:%s', l:bufnr) + execute printf('augroup %s', l:group) + execute printf('autocmd BufReadCmd call setline(1, readfile(bufname("%")[20 : -1])) | try | filetype detect | catch /.*/ | endtry | augroup %s | autocmd! | augroup END', l:bufnr, l:group) + augroup END + return l:bufnr +endfunction + diff --git a/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Option.vim b/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Option.vim new file mode 100644 index 000000000..043513382 --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Option.vim @@ -0,0 +1,21 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_cmp#VS#Vim#Option#import() abort', printf("return map({'define': ''}, \"vital#_cmp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" define +" +function! s:define(map) abort + let l:old = {} + for [l:key, l:value] in items(a:map) + let l:old[l:key] = eval(printf('&%s', l:key)) + execute printf('let &%s = "%s"', l:key, l:value) + endfor + return { -> s:define(l:old) } +endfunction + diff --git a/bundle/nvim-cmp/autoload/vital/cmp.vim b/bundle/nvim-cmp/autoload/vital/cmp.vim new file mode 100644 index 000000000..6730f4e0a --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/cmp.vim @@ -0,0 +1,330 @@ +let s:plugin_name = expand(':t:r') +let s:vital_base_dir = expand(':h') +let s:project_root = expand(':h:h:h') +let s:is_vital_vim = s:plugin_name is# 'vital' + +let s:loaded = {} +let s:cache_sid = {} + +function! vital#{s:plugin_name}#new() abort + return s:new(s:plugin_name) +endfunction + +function! vital#{s:plugin_name}#import(...) abort + if !exists('s:V') + let s:V = s:new(s:plugin_name) + endif + return call(s:V.import, a:000, s:V) +endfunction + +let s:Vital = {} + +function! s:new(plugin_name) abort + let base = deepcopy(s:Vital) + let base._plugin_name = a:plugin_name + return base +endfunction + +function! s:vital_files() abort + if !exists('s:vital_files') + let s:vital_files = map( + \ s:is_vital_vim ? s:_global_vital_files() : s:_self_vital_files(), + \ 'fnamemodify(v:val, ":p:gs?[\\\\/]?/?")') + endif + return copy(s:vital_files) +endfunction +let s:Vital.vital_files = function('s:vital_files') + +function! s:import(name, ...) abort dict + let target = {} + let functions = [] + for a in a:000 + if type(a) == type({}) + let target = a + elseif type(a) == type([]) + let functions = a + endif + unlet a + endfor + let module = self._import(a:name) + if empty(functions) + call extend(target, module, 'keep') + else + for f in functions + if has_key(module, f) && !has_key(target, f) + let target[f] = module[f] + endif + endfor + endif + return target +endfunction +let s:Vital.import = function('s:import') + +function! s:load(...) abort dict + for arg in a:000 + let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg] + let target = split(join(as, ''), '\W\+') + let dict = self + let dict_type = type({}) + while !empty(target) + let ns = remove(target, 0) + if !has_key(dict, ns) + let dict[ns] = {} + endif + if type(dict[ns]) == dict_type + let dict = dict[ns] + else + unlet dict + break + endif + endwhile + if exists('dict') + call extend(dict, self._import(name)) + endif + unlet arg + endfor + return self +endfunction +let s:Vital.load = function('s:load') + +function! s:unload() abort dict + let s:loaded = {} + let s:cache_sid = {} + unlet! s:vital_files +endfunction +let s:Vital.unload = function('s:unload') + +function! s:exists(name) abort dict + if a:name !~# '\v^\u\w*%(\.\u\w*)*$' + throw 'vital: Invalid module name: ' . a:name + endif + return s:_module_path(a:name) isnot# '' +endfunction +let s:Vital.exists = function('s:exists') + +function! s:search(pattern) abort dict + let paths = s:_extract_files(a:pattern, self.vital_files()) + let modules = sort(map(paths, 's:_file2module(v:val)')) + return uniq(modules) +endfunction +let s:Vital.search = function('s:search') + +function! s:plugin_name() abort dict + return self._plugin_name +endfunction +let s:Vital.plugin_name = function('s:plugin_name') + +function! s:_self_vital_files() abort + let builtin = printf('%s/__%s__/', s:vital_base_dir, s:plugin_name) + let installed = printf('%s/_%s/', s:vital_base_dir, s:plugin_name) + let base = builtin . ',' . installed + return split(globpath(base, '**/*.vim', 1), "\n") +endfunction + +function! s:_global_vital_files() abort + let pattern = 'autoload/vital/__*__/**/*.vim' + return split(globpath(&runtimepath, pattern, 1), "\n") +endfunction + +function! s:_extract_files(pattern, files) abort + let tr = {'.': '/', '*': '[^/]*', '**': '.*'} + let target = substitute(a:pattern, '\.\|\*\*\?', '\=tr[submatch(0)]', 'g') + let regexp = printf('autoload/vital/[^/]\+/%s.vim$', target) + return filter(a:files, 'v:val =~# regexp') +endfunction + +function! s:_file2module(file) abort + let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?') + let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$') + return join(split(tail, '[\\/]\+'), '.') +endfunction + +" @param {string} name e.g. Data.List +function! s:_import(name) abort dict + if has_key(s:loaded, a:name) + return copy(s:loaded[a:name]) + endif + let module = self._get_module(a:name) + if has_key(module, '_vital_created') + call module._vital_created(module) + endif + let export_module = filter(copy(module), 'v:key =~# "^\\a"') + " Cache module before calling module._vital_loaded() to avoid cyclic + " dependences but remove the cache if module._vital_loaded() fails. + " let s:loaded[a:name] = export_module + let s:loaded[a:name] = export_module + if has_key(module, '_vital_loaded') + try + call module._vital_loaded(vital#{s:plugin_name}#new()) + catch + unlet s:loaded[a:name] + throw 'vital: fail to call ._vital_loaded(): ' . v:exception . " from:\n" . s:_format_throwpoint(v:throwpoint) + endtry + endif + return copy(s:loaded[a:name]) +endfunction +let s:Vital._import = function('s:_import') + +function! s:_format_throwpoint(throwpoint) abort + let funcs = [] + let stack = matchstr(a:throwpoint, '^function \zs.*, .\{-} \d\+$') + for line in split(stack, '\.\.') + let m = matchlist(line, '^\(.\+\)\%(\[\(\d\+\)\]\|, .\{-} \(\d\+\)\)$') + if !empty(m) + let [name, lnum, lnum2] = m[1:3] + if empty(lnum) + let lnum = lnum2 + endif + let info = s:_get_func_info(name) + if !empty(info) + let attrs = empty(info.attrs) ? '' : join([''] + info.attrs) + let flnum = info.lnum == 0 ? '' : printf(' Line:%d', info.lnum + lnum) + call add(funcs, printf('function %s(...)%s Line:%d (%s%s)', + \ info.funcname, attrs, lnum, info.filename, flnum)) + continue + endif + endif + " fallback when function information cannot be detected + call add(funcs, line) + endfor + return join(funcs, "\n") +endfunction + +function! s:_get_func_info(name) abort + let name = a:name + if a:name =~# '^\d\+$' " is anonymous-function + let name = printf('{%s}', a:name) + elseif a:name =~# '^\d\+$' " is lambda-function + let name = printf("{'%s'}", a:name) + endif + if !exists('*' . name) + return {} + endif + let body = execute(printf('verbose function %s', name)) + let lines = split(body, "\n") + let signature = matchstr(lines[0], '^\s*\zs.*') + let [_, file, lnum; __] = matchlist(lines[1], + \ '^\t\%(Last set from\|.\{-}:\)\s*\zs\(.\{-}\)\%( \S\+ \(\d\+\)\)\?$') + return { + \ 'filename': substitute(file, '[/\\]\+', '/', 'g'), + \ 'lnum': 0 + lnum, + \ 'funcname': a:name, + \ 'arguments': split(matchstr(signature, '(\zs.*\ze)'), '\s*,\s*'), + \ 'attrs': filter(['dict', 'abort', 'range', 'closure'], 'signature =~# (").*" . v:val)'), + \ } +endfunction + +" s:_get_module() returns module object wihch has all script local functions. +function! s:_get_module(name) abort dict + let funcname = s:_import_func_name(self.plugin_name(), a:name) + try + return call(funcname, []) + catch /^Vim\%((\a\+)\)\?:E117:/ + return s:_get_builtin_module(a:name) + endtry +endfunction + +function! s:_get_builtin_module(name) abort + return s:sid2sfuncs(s:_module_sid(a:name)) +endfunction + +if s:is_vital_vim + " For vital.vim, we can use s:_get_builtin_module directly + let s:Vital._get_module = function('s:_get_builtin_module') +else + let s:Vital._get_module = function('s:_get_module') +endif + +function! s:_import_func_name(plugin_name, module_name) abort + return printf('vital#_%s#%s#import', a:plugin_name, s:_dot_to_sharp(a:module_name)) +endfunction + +function! s:_module_sid(name) abort + let path = s:_module_path(a:name) + if !filereadable(path) + throw 'vital: module not found: ' . a:name + endif + let vital_dir = s:is_vital_vim ? '__\w\+__' : printf('_\{1,2}%s\%%(__\)\?', s:plugin_name) + let base = join([vital_dir, ''], '[/\\]\+') + let p = base . substitute('' . a:name, '\.', '[/\\\\]\\+', 'g') + let sid = s:_sid(path, p) + if !sid + call s:_source(path) + let sid = s:_sid(path, p) + if !sid + throw printf('vital: cannot get from path: %s', path) + endif + endif + return sid +endfunction + +function! s:_module_path(name) abort + return get(s:_extract_files(a:name, s:vital_files()), 0, '') +endfunction + +function! s:_module_sid_base_dir() abort + return s:is_vital_vim ? &rtp : s:project_root +endfunction + +function! s:_dot_to_sharp(name) abort + return substitute(a:name, '\.', '#', 'g') +endfunction + +function! s:_source(path) abort + execute 'source' fnameescape(a:path) +endfunction + +" @vimlint(EVL102, 1, l:_) +" @vimlint(EVL102, 1, l:__) +function! s:_sid(path, filter_pattern) abort + let unified_path = s:_unify_path(a:path) + if has_key(s:cache_sid, unified_path) + return s:cache_sid[unified_path] + endif + for line in filter(split(execute(':scriptnames'), "\n"), 'v:val =~# a:filter_pattern') + let [_, sid, path; __] = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$') + if s:_unify_path(path) is# unified_path + let s:cache_sid[unified_path] = sid + return s:cache_sid[unified_path] + endif + endfor + return 0 +endfunction + +if filereadable(expand(':r') . '.VIM') " is case-insensitive or not + let s:_unify_path_cache = {} + " resolve() is slow, so we cache results. + " Note: On windows, vim can't expand path names from 8.3 formats. + " So if getting full path via and $HOME was set as 8.3 format, + " vital load duplicated scripts. Below's :~ avoid this issue. + function! s:_unify_path(path) abort + if has_key(s:_unify_path_cache, a:path) + return s:_unify_path_cache[a:path] + endif + let value = tolower(fnamemodify(resolve(fnamemodify( + \ a:path, ':p')), ':~:gs?[\\/]?/?')) + let s:_unify_path_cache[a:path] = value + return value + endfunction +else + function! s:_unify_path(path) abort + return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?')) + endfunction +endif + +" copied and modified from Vim.ScriptLocal +let s:SNR = join(map(range(len("\")), '"[\\x" . printf("%0x", char2nr("\"[v:val])) . "]"'), '') +function! s:sid2sfuncs(sid) abort + let fs = split(execute(printf(':function /^%s%s_', s:SNR, a:sid)), "\n") + let r = {} + let pattern = printf('\m^function\s%d_\zs\w\{-}\ze(', a:sid) + for fname in map(fs, 'matchstr(v:val, pattern)') + let r[fname] = function(s:_sfuncname(a:sid, fname)) + endfor + return r +endfunction + +"" Return funcname of script local functions with SID +function! s:_sfuncname(sid, funcname) abort + return printf('%s_%s', a:sid, a:funcname) +endfunction diff --git a/bundle/nvim-cmp/autoload/vital/cmp.vital b/bundle/nvim-cmp/autoload/vital/cmp.vital new file mode 100644 index 000000000..1213d2a6e --- /dev/null +++ b/bundle/nvim-cmp/autoload/vital/cmp.vital @@ -0,0 +1,4 @@ +cmp +2755f0c8fbd3442bcb7f567832e4d1455b57f9a2 + +VS.LSP.TextEdit diff --git a/bundle/nvim-cmp/init.sh b/bundle/nvim-cmp/init.sh new file mode 100644 index 000000000..ea6b54368 --- /dev/null +++ b/bundle/nvim-cmp/init.sh @@ -0,0 +1,7 @@ +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +rm $DIR/.git/hooks/* +cp $DIR/.githooks/* $DIR/.git/hooks/ +chmod 755 $DIR/.git/hooks/* + + diff --git a/bundle/nvim-cmp/lua/cmp/config.lua b/bundle/nvim-cmp/lua/cmp/config.lua new file mode 100644 index 000000000..42fdc4fb9 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config.lua @@ -0,0 +1,104 @@ +local mapping = require('cmp.config.mapping') +local cache = require('cmp.utils.cache') +local keymap = require('cmp.utils.keymap') +local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') + +---@class cmp.Config +---@field public g cmp.ConfigSchema +local config = {} + +---@type cmp.Cache +config.cache = cache.new() + +---@type cmp.ConfigSchema +config.global = require('cmp.config.default')() + +---@type table +config.buffers = {} + +---@type table +config.cmdline = {} + +---Set configuration for global. +---@param c cmp.ConfigSchema +config.set_global = function(c) + config.global = misc.merge(c, config.global) + config.global.revision = config.global.revision or 1 + config.global.revision = config.global.revision + 1 +end + +---Set configuration for buffer +---@param c cmp.ConfigSchema +---@param bufnr number|nil +config.set_buffer = function(c, bufnr) + local revision = (config.buffers[bufnr] or {}).revision or 1 + config.buffers[bufnr] = c + config.buffers[bufnr].revision = revision + 1 +end + +---Set configuration for cmdline +config.set_cmdline = function(c, type) + local revision = (config.cmdline[type] or {}).revision or 1 + config.cmdline[type] = c + config.cmdline[type].revision = revision + 1 +end + +---@return cmp.ConfigSchema +config.get = function() + local global = config.global + if api.is_cmdline_mode() then + local type = vim.fn.getcmdtype() + local cmdline = config.cmdline[type] or { revision = 1, sources = {} } + return config.cache:ensure({ 'get_cmdline', type, global.revision or 0, cmdline.revision or 0 }, function() + return misc.merge(config.normalize(cmdline), config.normalize(global)) + end) + else + local bufnr = vim.api.nvim_get_current_buf() + local buffer = config.buffers[bufnr] or { revision = 1 } + return config.cache:ensure({ 'get_buffer', bufnr, global.revision or 0, buffer.revision or 0 }, function() + return misc.merge(config.normalize(buffer), config.normalize(global)) + end) + end +end + +---Return cmp is enabled or not. +config.enabled = function() + local enabled = config.get().enabled + if type(enabled) == 'function' then + enabled = enabled() + end + return enabled and api.is_suitable_mode() +end + +---Return source config +---@param name string +---@return cmp.SourceConfig +config.get_source_config = function(name) + local c = config.get() + for _, s in ipairs(c.sources) do + if s.name == name then + if type(s.opts) ~= 'table' then + s.opts = {} + end + return s + end + end + return nil +end + +---Normalize mapping key +---@param c cmp.ConfigSchema +---@return cmp.ConfigSchema +config.normalize = function(c) + if c.mapping then + local normalized = {} + for k, v in pairs(c.mapping) do + normalized[keymap.normalize(k)] = mapping(v, { 'i' }) + end + c.mapping = normalized + end + return c +end + +return config diff --git a/bundle/nvim-cmp/lua/cmp/config/compare.lua b/bundle/nvim-cmp/lua/cmp/config/compare.lua new file mode 100644 index 000000000..d5657cbbb --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/compare.lua @@ -0,0 +1,103 @@ +local types = require('cmp.types') +local misc = require('cmp.utils.misc') + +local compare = {} + +-- offset +compare.offset = function(entry1, entry2) + local diff = entry1:get_offset() - entry2:get_offset() + if diff < 0 then + return true + elseif diff > 0 then + return false + end +end + +-- exact +compare.exact = function(entry1, entry2) + if entry1.exact ~= entry2.exact then + return entry1.exact + end +end + +-- score +compare.score = function(entry1, entry2) + local diff = entry2.score - entry1.score + if diff < 0 then + return true + elseif diff > 0 then + return false + end +end + +-- recently_used +compare.recently_used = setmetatable({ + records = {}, + add_entry = function(self, e) + self.records[e.completion_item.label] = vim.loop.now() + end, +}, { + __call = function(self, entry1, entry2) + local t1 = self.records[entry1.completion_item.label] or -1 + local t2 = self.records[entry2.completion_item.label] or -1 + if t1 ~= t2 then + return t1 > t2 + end + end, +}) + +-- kind +compare.kind = function(entry1, entry2) + local kind1 = entry1:get_kind() + kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1 + local kind2 = entry2:get_kind() + kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2 + if kind1 ~= kind2 then + if kind1 == types.lsp.CompletionItemKind.Snippet then + return true + end + if kind2 == types.lsp.CompletionItemKind.Snippet then + return false + end + local diff = kind1 - kind2 + if diff < 0 then + return true + elseif diff > 0 then + return false + end + end +end + +-- sortText +compare.sort_text = function(entry1, entry2) + if misc.safe(entry1.completion_item.sortText) and misc.safe(entry2.completion_item.sortText) then + local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText) + if diff < 0 then + return true + elseif diff > 0 then + return false + end + end +end + +-- length +compare.length = function(entry1, entry2) + local diff = #entry1.completion_item.label - #entry2.completion_item.label + if diff < 0 then + return true + elseif diff > 0 then + return false + end +end + +-- order +compare.order = function(entry1, entry2) + local diff = entry1.id - entry2.id + if diff < 0 then + return true + elseif diff > 0 then + return false + end +end + +return compare diff --git a/bundle/nvim-cmp/lua/cmp/config/default.lua b/bundle/nvim-cmp/lua/cmp/config/default.lua new file mode 100644 index 000000000..c0d5f5a4e --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/default.lua @@ -0,0 +1,130 @@ +local compare = require('cmp.config.compare') +local mapping = require('cmp.config.mapping') +local types = require('cmp.types') + +local WIDE_HEIGHT = 40 + +---@return cmp.ConfigSchema +return function() + return { + enabled = function() + return vim.api.nvim_buf_get_option(0, 'buftype') ~= 'prompt' + end, + completion = { + autocomplete = { + types.cmp.TriggerEvent.TextChanged, + }, + completeopt = 'menu,menuone,noselect', + keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], + keyword_length = 1, + get_trigger_characters = function(trigger_characters) + return trigger_characters + end, + }, + + snippet = { + expand = function() + error('snippet engine is not configured.') + end, + }, + + preselect = types.cmp.PreselectMode.Item, + + documentation = { + border = { '', '', '', ' ', '', '', '', ' ' }, + winhighlight = 'NormalFloat:NormalFloat,FloatBorder:NormalFloat', + maxwidth = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))), + maxheight = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)), + }, + + confirmation = { + default_behavior = types.cmp.ConfirmBehavior.Insert, + get_commit_characters = function(commit_characters) + return commit_characters + end, + }, + + sorting = { + priority_weight = 2, + comparators = { + compare.offset, + compare.exact, + compare.score, + compare.recently_used, + compare.kind, + compare.sort_text, + compare.length, + compare.order, + }, + }, + + event = {}, + + mapping = { + [''] = mapping({ + i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), + c = function(fallback) + local cmp = require('cmp') + cmp.close() + vim.schedule(cmp.suspend()) + fallback() + end, + }), + [''] = mapping({ + i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }), + c = function(fallback) + local cmp = require('cmp') + cmp.close() + vim.schedule(cmp.suspend()) + fallback() + end, + }), + [''] = mapping({ + c = function(fallback) + local cmp = require('cmp') + if #cmp.core:get_sources() > 0 and not cmp.get_config().experimental.native_menu then + if cmp.visible() then + cmp.select_next_item() + else + cmp.complete() + end + else + fallback() + end + end, + }), + [''] = mapping({ + c = function(fallback) + local cmp = require('cmp') + if #cmp.core:get_sources() > 0 and not cmp.get_config().experimental.native_menu then + if cmp.visible() then + cmp.select_prev_item() + else + cmp.complete() + end + else + fallback() + end + end, + }), + [''] = mapping(mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }), + [''] = mapping(mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }), + [''] = mapping.confirm({ select = false }), + [''] = mapping.abort(), + }, + + formatting = { + fields = { 'abbr', 'kind', 'menu' }, + format = function(_, vim_item) + return vim_item + end, + }, + + experimental = { + native_menu = false, + ghost_text = false, + }, + + sources = {}, + } +end diff --git a/bundle/nvim-cmp/lua/cmp/config/mapping.lua b/bundle/nvim-cmp/lua/cmp/config/mapping.lua new file mode 100644 index 000000000..a4754f15f --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/mapping.lua @@ -0,0 +1,82 @@ +local mapping +mapping = setmetatable({}, { + __call = function(_, invoke, modes) + if type(invoke) == 'function' then + local map = {} + for _, mode in ipairs(modes or { 'i' }) do + map[mode] = invoke + end + return map + end + return invoke + end, +}) + +---Invoke completion +mapping.complete = function() + return function(fallback) + if not require('cmp').complete() then + fallback() + end + end +end + +---Close current completion menu if it displayed. +mapping.close = function() + return function(fallback) + if not require('cmp').close() then + fallback() + end + end +end + +---Abort current completion menu if it displayed. +mapping.abort = function() + return function(fallback) + if not require('cmp').abort() then + fallback() + end + end +end + +---Scroll documentation window. +mapping.scroll_docs = function(delta) + return function(fallback) + if not require('cmp').scroll_docs(delta) then + fallback() + end + end +end + +---Select next completion item. +mapping.select_next_item = function(option) + return function(fallback) + if not require('cmp').select_next_item(option) then + local release = require('cmp').core:suspend() + fallback() + vim.schedule(release) + end + end +end + +---Select prev completion item. +mapping.select_prev_item = function(option) + return function(fallback) + if not require('cmp').select_prev_item(option) then + local release = require('cmp').core:suspend() + fallback() + vim.schedule(release) + end + end +end + +---Confirm selection +mapping.confirm = function(option) + return function(fallback) + if not require('cmp').confirm(option) then + fallback() + end + end +end + +return mapping diff --git a/bundle/nvim-cmp/lua/cmp/config/sources.lua b/bundle/nvim-cmp/lua/cmp/config/sources.lua new file mode 100644 index 000000000..cfb09c07c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/sources.lua @@ -0,0 +1,10 @@ +return function(...) + local sources = {} + for i, group in ipairs({ ... }) do + for _, source in ipairs(group) do + source.group_index = i + table.insert(sources, source) + end + end + return sources +end diff --git a/bundle/nvim-cmp/lua/cmp/context.lua b/bundle/nvim-cmp/lua/cmp/context.lua new file mode 100644 index 000000000..6188259eb --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/context.lua @@ -0,0 +1,105 @@ +local misc = require('cmp.utils.misc') +local pattern = require('cmp.utils.pattern') +local types = require('cmp.types') +local cache = require('cmp.utils.cache') +local api = require('cmp.utils.api') + +---@class cmp.Context +---@field public id string +---@field public cache cmp.Cache +---@field public prev_context cmp.Context +---@field public option cmp.ContextOption +---@field public filetype string +---@field public time number +---@field public bufnr number +---@field public cursor vim.Position|lsp.Position +---@field public cursor_line string +---@field public cursor_after_line string +---@field public cursor_before_line string +local context = {} + +---Create new empty context +---@return cmp.Context +context.empty = function() + local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`. + ctx.bufnr = -1 + ctx.input = '' + ctx.cursor = {} + ctx.cursor.row = -1 + ctx.cursor.col = -1 + return ctx +end + +---Create new context +---@param prev_context cmp.Context +---@param option cmp.ContextOption +---@return cmp.Context +context.new = function(prev_context, option) + option = option or {} + + local self = setmetatable({}, { __index = context }) + self.id = misc.id('cmp.context.new') + self.cache = cache.new() + self.prev_context = prev_context or context.empty() + self.option = option or { reason = types.cmp.ContextReason.None } + self.filetype = vim.api.nvim_buf_get_option(0, 'filetype') + self.time = vim.loop.now() + self.bufnr = vim.api.nvim_get_current_buf() + + local cursor = api.get_cursor() + self.cursor_line = api.get_current_line() + self.cursor = {} + self.cursor.row = cursor[1] + self.cursor.col = cursor[2] + 1 + self.cursor.line = self.cursor.row - 1 + self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col) + self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1) + self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col) + return self +end + +---Return context creation reason. +---@return cmp.ContextReason +context.get_reason = function(self) + return self.option.reason +end + +---Get keyword pattern offset +---@return number|nil +context.get_offset = function(self, keyword_pattern) + return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function() + return pattern.offset(keyword_pattern .. '\\m$', self.cursor_before_line) or self.cursor.col + end) +end + +---Return if this context is changed from previous context or not. +---@return boolean +context.changed = function(self, ctx) + local curr = self + + if curr.bufnr ~= ctx.bufnr then + return true + end + if curr.cursor.row ~= ctx.cursor.row then + return true + end + if curr.cursor.col ~= ctx.cursor.col then + return true + end + if curr:get_reason() == types.cmp.ContextReason.Manual then + return true + end + + return false +end + +---Shallow clone +context.clone = function(self) + local cloned = {} + for k, v in pairs(self) do + cloned[k] = v + end + return cloned +end + +return context diff --git a/bundle/nvim-cmp/lua/cmp/context_spec.lua b/bundle/nvim-cmp/lua/cmp/context_spec.lua new file mode 100644 index 000000000..976e194a4 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/context_spec.lua @@ -0,0 +1,31 @@ +local spec = require('cmp.utils.spec') + +local context = require('cmp.context') + +describe('context', function() + before_each(spec.before) + + describe('new', function() + it('middle of text', function() + vim.fn.setline('1', 'function! s:name() abort') + vim.bo.filetype = 'vim' + vim.fn.execute('normal! fm') + local ctx = context.new() + assert.are.equal(ctx.filetype, 'vim') + assert.are.equal(ctx.cursor.row, 1) + assert.are.equal(ctx.cursor.col, 15) + assert.are.equal(ctx.cursor_line, 'function! s:name() abort') + end) + + it('tab indent', function() + vim.fn.setline('1', '\t\tab') + vim.bo.filetype = 'vim' + vim.fn.execute('normal! fb') + local ctx = context.new() + assert.are.equal(ctx.filetype, 'vim') + assert.are.equal(ctx.cursor.row, 1) + assert.are.equal(ctx.cursor.col, 4) + assert.are.equal(ctx.cursor_line, '\t\tab') + end) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/core.lua b/bundle/nvim-cmp/lua/cmp/core.lua new file mode 100644 index 000000000..6ee30c2c7 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/core.lua @@ -0,0 +1,435 @@ +local debug = require('cmp.utils.debug') +local char = require('cmp.utils.char') +local pattern = require('cmp.utils.pattern') +local feedkeys = require('cmp.utils.feedkeys') +local async = require('cmp.utils.async') +local keymap = require('cmp.utils.keymap') +local context = require('cmp.context') +local source = require('cmp.source') +local view = require('cmp.view') +local misc = require('cmp.utils.misc') +local config = require('cmp.config') +local types = require('cmp.types') +local api = require('cmp.utils.api') +local event = require('cmp.utils.event') + +local SOURCE_TIMEOUT = 500 +local THROTTLE_TIME = 120 +local DEBOUNCE_TIME = 20 + +---@class cmp.Core +---@field public suspending boolean +---@field public view cmp.View +---@field public sources cmp.Source[] +---@field public sources_by_name table +---@field public context cmp.Context +---@field public event cmp.Event +local core = {} + +core.new = function() + local self = setmetatable({}, { __index = core }) + self.suspending = false + self.sources = {} + self.sources_by_name = {} + self.context = context.new() + self.event = event.new() + self.view = view.new() + self.view.event:on('keymap', function(...) + self:on_keymap(...) + end) + return self +end + +---Register source +---@param s cmp.Source +core.register_source = function(self, s) + self.sources[s.id] = s + if not self.sources_by_name[s.name] then + self.sources_by_name[s.name] = {} + end + table.insert(self.sources_by_name[s.name], s) +end + +---Unregister source +---@param source_id string +core.unregister_source = function(self, source_id) + local name = self.sources[source_id].name + self.sources_by_name[name] = vim.tbl_filter(function(s) + return s.id ~= source_id + end, self.sources_by_name[name]) + self.sources[source_id] = nil +end + +---Get new context +---@param option cmp.ContextOption +---@return cmp.Context +core.get_context = function(self, option) + local prev = self.context:clone() + prev.prev_context = nil + local ctx = context.new(prev, option) + self:set_context(ctx) + return self.context +end + +---Set new context +---@param ctx cmp.Context +core.set_context = function(self, ctx) + self.context = ctx +end + +---Suspend completion +core.suspend = function(self) + self.suspending = true + return function() + self.suspending = false + end +end + +---Get sources that sorted by priority +---@param statuses cmp.SourceStatus[] +---@return cmp.Source[] +core.get_sources = function(self, statuses) + local sources = {} + for _, c in pairs(config.get().sources) do + for _, s in ipairs(self.sources_by_name[c.name] or {}) do + if not statuses or vim.tbl_contains(statuses, s.status) then + if s:is_available() then + table.insert(sources, s) + end + end + end + end + return sources +end + +---Keypress handler +core.on_keymap = function(self, keys, fallback) + local mode = api.get_mode() + for key, mapping in pairs(config.get().mapping) do + if keymap.equals(key, keys) and mapping[mode] then + return mapping[mode](fallback) + end + end + + --Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly. + local chars = keymap.t(keys) + local e = self.view:get_active_entry() + if e and vim.tbl_contains(config.get().confirmation.get_commit_characters(e:get_commit_characters()), chars) then + local is_printable = char.is_printable(string.byte(chars, 1)) + self:confirm(e, { + behavior = is_printable and 'insert' or 'replace', + }, function() + local ctx = self:get_context() + local word = e:get_word() + if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then + fallback() + else + self:reset() + end + end) + return + end + + fallback() +end + +---Prepare completion +core.prepare = function(self) + for keys, mapping in pairs(config.get().mapping) do + for mode in pairs(mapping) do + keymap.listen(mode, keys, function(...) + self:on_keymap(...) + end) + end + end +end + +---Check auto-completion +core.on_change = function(self, trigger_event) + local ignore = false + ignore = ignore or self.suspending + ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word) + ignore = ignore or not self.view:ready() + if ignore then + self:get_context({ reason = types.cmp.ContextReason.Auto }) + return + end + + self:autoindent(trigger_event, function() + local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto }) + debug.log(('ctx: `%s`'):format(ctx.cursor_before_line)) + if ctx:changed(ctx.prev_context) then + self.view:on_change() + debug.log('changed') + + if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then + self:complete(ctx) + else + self.filter.timeout = THROTTLE_TIME + self:filter() + end + else + debug.log('unchanged') + end + end) +end + +---Cursor moved. +core.on_moved = function(self) + local ignore = false + ignore = ignore or self.suspending + ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word) + ignore = ignore or not self.view:visible() + if ignore then + return + end + self:filter() +end + +---Check autoindent +---@param trigger_event cmp.TriggerEvent +---@param callback function +core.autoindent = function(self, trigger_event, callback) + if trigger_event ~= types.cmp.TriggerEvent.TextChanged then + return callback() + end + if not api.is_insert_mode() then + return callback() + end + + -- Check prefix + local cursor_before_line = api.get_cursor_before_line() + local prefix = pattern.matchstr('[^[:blank:]]\\+$', cursor_before_line) or '' + if #prefix == 0 then + return callback() + end + + -- Scan indentkeys. + for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do + if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then + local release = self:suspend() + vim.schedule(function() -- Check autoindent already applied. + if cursor_before_line == api.get_cursor_before_line() then + feedkeys.call(keymap.autoindent(), 'n', function() + release() + callback() + end) + else + callback() + end + end) + return + end + end + + -- indentkeys does not matched. + callback() +end + +---Invoke completion +---@param ctx cmp.Context +core.complete = function(self, ctx) + if not api.is_suitable_mode() then + return + end + self:set_context(ctx) + + for _, s in ipairs(self:get_sources({ source.SourceStatus.WAITING, source.SourceStatus.COMPLETED })) do + s:complete( + ctx, + (function(src) + local callback + callback = function() + local new = context.new(ctx) + if new:changed(new.prev_context) and ctx == self.context then + src:complete(new, callback) + else + self.filter.stop() + self.filter.timeout = DEBOUNCE_TIME + self:filter() + end + end + return callback + end)(s) + ) + end + + self.filter.timeout = THROTTLE_TIME + self:filter() +end + +---Update completion menu +core.filter = async.throttle( + vim.schedule_wrap(function(self) + if not api.is_suitable_mode() then + return + end + if self.view:get_active_entry() ~= nil then + return + end + local ctx = self:get_context() + + -- To wait for processing source for that's timeout. + local sources = {} + for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do + local time = SOURCE_TIMEOUT - s:get_fetching_time() + if not s.incomplete and time > 0 then + if #sources == 0 then + self.filter.stop() + self.filter.timeout = time + 1 + self:filter() + return + end + break + end + table.insert(sources, s) + end + self.filter.timeout = THROTTLE_TIME + + self.view:open(ctx, sources) + end), + THROTTLE_TIME +) + +---Confirm completion. +---@param e cmp.Entry +---@param option cmp.ConfirmOption +---@param callback function +core.confirm = function(self, e, option, callback) + if not (e and not e.confirmed) then + return callback() + end + e.confirmed = true + + debug.log('entry.confirm', e:get_completion_item()) + + local release = self:suspend() + + -- Close menus. + self.view:close() + + feedkeys.call('', 'n', function() + local ctx = context.new() + local keys = {} + table.insert(keys, keymap.backspace(ctx.cursor.character - vim.str_utfindex(ctx.cursor_line, e:get_offset() - 1))) + table.insert(keys, e:get_word()) + table.insert(keys, keymap.undobreak()) + feedkeys.call(table.concat(keys, ''), 'int') + end) + feedkeys.call('', 'n', function() + local ctx = context.new() + if api.is_cmdline_mode() then + local keys = {} + table.insert(keys, keymap.backspace(ctx.cursor.character - vim.str_utfindex(ctx.cursor_line, e:get_offset() - 1))) + table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset())) + feedkeys.call(table.concat(keys, ''), 'int') + else + vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { + string.sub(e.context.cursor_before_line, e:get_offset()), + }) + vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 }) + end + end) + feedkeys.call('', 'n', function() + if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then + local pre = context.new() + e:resolve(function() + local new = context.new() + local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {} + if #text_edits == 0 then + return + end + + local has_cursor_line_text_edit = (function() + local minrow = math.min(pre.cursor.row, new.cursor.row) + local maxrow = math.max(pre.cursor.row, new.cursor.row) + for _, te in ipairs(text_edits) do + local srow = te.range.start.line + 1 + local erow = te.range['end'].line + 1 + if srow <= minrow and maxrow <= erow then + return true + end + end + return false + end)() + if has_cursor_line_text_edit then + return + end + vim.fn['cmp#apply_text_edits'](new.bufnr, text_edits) + end) + else + vim.fn['cmp#apply_text_edits'](vim.api.nvim_get_current_buf(), e:get_completion_item().additionalTextEdits) + end + end) + feedkeys.call('', 'n', function() + local ctx = context.new() + local completion_item = misc.copy(e:get_completion_item()) + if not misc.safe(completion_item.textEdit) then + completion_item.textEdit = {} + completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label + end + local behavior = option.behavior or config.get().confirmation.default_behavior + if behavior == types.cmp.ConfirmBehavior.Replace then + completion_item.textEdit.range = e:get_replace_range() + else + completion_item.textEdit.range = e:get_insert_range() + end + + local diff_before = e.context.cursor.character - completion_item.textEdit.range.start.character + local diff_after = completion_item.textEdit.range['end'].character - e.context.cursor.character + local new_text = completion_item.textEdit.newText + + if api.is_insert_mode() then + local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet + completion_item.textEdit.range.start.line = ctx.cursor.line + completion_item.textEdit.range.start.character = ctx.cursor.character - diff_before + completion_item.textEdit.range['end'].line = ctx.cursor.line + completion_item.textEdit.range['end'].character = ctx.cursor.character + diff_after + if is_snippet then + completion_item.textEdit.newText = '' + end + vim.fn['cmp#apply_text_edits'](ctx.bufnr, { completion_item.textEdit }) + local texts = vim.split(completion_item.textEdit.newText, '\n') + local position = completion_item.textEdit.range.start + position.line = position.line + (#texts - 1) + if #texts == 1 then + position.character = position.character + vim.str_utfindex(texts[1]) + else + position.character = vim.str_utfindex(texts[#texts]) + end + local pos = types.lsp.Position.to_vim(0, position) + vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 }) + if is_snippet then + config.get().snippet.expand({ + body = new_text, + insert_text_mode = completion_item.insertTextMode, + }) + end + else + local keys = {} + table.insert(keys, string.rep(keymap.t(''), diff_before)) + table.insert(keys, string.rep(keymap.t(''), diff_after)) + table.insert(keys, new_text) + feedkeys.call(table.concat(keys, ''), 'int') + end + end) + feedkeys.call('', 'n', function() + e:execute(vim.schedule_wrap(function() + release() + self.event:emit('confirm_done', e) + if callback then + callback() + end + end)) + end) +end + +---Reset current completion state +core.reset = function(self) + for _, s in pairs(self.sources) do + s:reset() + end + self:get_context() -- To prevent new event +end + +return core diff --git a/bundle/nvim-cmp/lua/cmp/core_spec.lua b/bundle/nvim-cmp/lua/cmp/core_spec.lua new file mode 100644 index 000000000..c37090e2b --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/core_spec.lua @@ -0,0 +1,158 @@ +local spec = require('cmp.utils.spec') +local feedkeys = require('cmp.utils.feedkeys') +local types = require('cmp.types') +local core = require('cmp.core') +local source = require('cmp.source') +local keymap = require('cmp.utils.keymap') +local api = require('cmp.utils.api') + +describe('cmp.core', function() + describe('confirm', function() + local confirm = function(request, filter, completion_item) + local c = core.new() + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ completion_item }) + end, + }) + c:register_source(s) + feedkeys.call(request, 'n', function() + c:complete(c:get_context({ reason = types.cmp.ContextReason.Manual })) + vim.wait(5000, function() + return #c.sources[s.id].entries > 0 + end) + end) + feedkeys.call(filter, 'n', function() + c:confirm(c.sources[s.id].entries[1], {}) + end) + local state = {} + feedkeys.call('', 'x', function() + feedkeys.call('', 'n', function() + if api.is_cmdline_mode() then + state.buffer = { api.get_current_line() } + else + state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) + end + state.cursor = api.get_cursor() + end) + end) + return state + end + + describe('insert-mode', function() + before_each(spec.before) + + it('label', function() + local state = confirm('iA', 'IU', { + label = 'AIUEO', + }) + assert.are.same(state.buffer, { 'AIUEO' }) + assert.are.same(state.cursor, { 1, 5 }) + end) + + it('insertText', function() + local state = confirm('iA', 'IU', { + label = 'AIUEO', + insertText = '_AIUEO_', + }) + assert.are.same(state.buffer, { '_AIUEO_' }) + assert.are.same(state.cursor, { 1, 7 }) + end) + + it('textEdit', function() + local state = confirm(keymap.t('i***AEO***'), 'IU', { + label = 'AIUEO', + textEdit = { + range = { + start = { + line = 0, + character = 3, + }, + ['end'] = { + line = 0, + character = 6, + }, + }, + newText = 'foo\nbar\nbaz', + }, + }) + assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) + assert.are.same(state.cursor, { 3, 3 }) + end) + + it('insertText & snippet', function() + local state = confirm('iA', 'IU', { + label = 'AIUEO', + insertText = 'AIUEO($0)', + insertTextFormat = types.lsp.InsertTextFormat.Snippet, + }) + assert.are.same(state.buffer, { 'AIUEO()' }) + assert.are.same(state.cursor, { 1, 6 }) + end) + + it('textEdit & snippet', function() + local state = confirm(keymap.t('i***AEO***'), 'IU', { + label = 'AIUEO', + insertTextFormat = types.lsp.InsertTextFormat.Snippet, + textEdit = { + range = { + start = { + line = 0, + character = 3, + }, + ['end'] = { + line = 0, + character = 6, + }, + }, + newText = 'foo\nba$0r\nbaz', + }, + }) + assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) + assert.are.same(state.cursor, { 2, 2 }) + end) + end) + + describe('cmdline-mode', function() + before_each(spec.before) + + it('label', function() + local state = confirm(':A', 'IU', { + label = 'AIUEO', + }) + assert.are.same(state.buffer, { 'AIUEO' }) + assert.are.same(state.cursor[2], 5) + end) + + it('insertText', function() + local state = confirm(':A', 'IU', { + label = 'AIUEO', + insertText = '_AIUEO_', + }) + assert.are.same(state.buffer, { '_AIUEO_' }) + assert.are.same(state.cursor[2], 7) + end) + + it('textEdit', function() + local state = confirm(keymap.t(':***AEO***'), 'IU', { + label = 'AIUEO', + textEdit = { + range = { + start = { + line = 0, + character = 3, + }, + ['end'] = { + line = 0, + character = 6, + }, + }, + newText = 'foobarbaz', + }, + }) + assert.are.same(state.buffer, { '***foobarbaz***' }) + assert.are.same(state.cursor[2], 12) + end) + end) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/entry.lua b/bundle/nvim-cmp/lua/cmp/entry.lua new file mode 100644 index 000000000..686c0de84 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/entry.lua @@ -0,0 +1,430 @@ +local cache = require('cmp.utils.cache') +local char = require('cmp.utils.char') +local misc = require('cmp.utils.misc') +local str = require('cmp.utils.str') +local config = require('cmp.config') +local types = require('cmp.types') +local matcher = require('cmp.matcher') + +---@class cmp.Entry +---@field public id number +---@field public cache cmp.Cache +---@field public match_cache cmp.Cache +---@field public score number +---@field public exact boolean +---@field public matches table +---@field public context cmp.Context +---@field public source cmp.Source +---@field public source_offset number +---@field public source_insert_range lsp.Range +---@field public source_replace_range lsp.Range +---@field public completion_item lsp.CompletionItem +---@field public resolved_completion_item lsp.CompletionItem|nil +---@field public resolved_callbacks fun()[] +---@field public resolving boolean +---@field public confirmed boolean +local entry = {} + +---Create new entry +---@param ctx cmp.Context +---@param source cmp.Source +---@param completion_item lsp.CompletionItem +---@return cmp.Entry +entry.new = function(ctx, source, completion_item) + local self = setmetatable({}, { __index = entry }) + self.id = misc.id('entry.new') + self.cache = cache.new() + self.match_cache = cache.new() + self.score = 0 + self.exact = false + self.matches = {} + self.context = ctx + self.source = source + self.source_offset = source.request_offset + self.source_insert_range = source:get_default_insert_range() + self.source_replace_range = source:get_default_replace_range() + self.completion_item = completion_item + self.resolved_completion_item = nil + self.resolved_callbacks = {} + self.resolving = false + self.confirmed = false + return self +end + +---Make offset value +---@return number +entry.get_offset = function(self) + return self.cache:ensure('get_offset', function() + local offset = self.source_offset + if misc.safe(self.completion_item.textEdit) then + local range = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range) + if range then + local c = misc.to_vimindex(self.context.cursor_line, range.start.character) + for idx = c, self.source_offset do + if not char.is_white(string.byte(self.context.cursor_line, idx)) then + offset = idx + break + end + end + end + else + -- NOTE + -- The VSCode does not implement this but it's useful if the server does not care about word patterns. + -- We should care about this performance. + local word = self:get_word() + for idx = self.source_offset - 1, self.source_offset - #word, -1 do + if char.is_semantic_index(self.context.cursor_line, idx) then + local c = string.byte(self.context.cursor_line, idx) + if char.is_white(c) then + break + end + local match = true + for i = 1, self.source_offset - idx do + local c1 = string.byte(word, i) + local c2 = string.byte(self.context.cursor_line, idx + i - 1) + if not c1 or not c2 or c1 ~= c2 then + match = false + break + end + end + if match then + offset = math.min(offset, idx) + end + end + end + end + return offset + end) +end + +---Create word for vim.CompletedItem +---@return string +entry.get_word = function(self) + return self.cache:ensure('get_word', function() + --NOTE: This is nvim-cmp specific implementation. + if misc.safe(self.completion_item.word) then + return self.completion_item.word + end + + local word + if misc.safe(self.completion_item.textEdit) then + word = str.trim(self.completion_item.textEdit.newText) + local overwrite = self:get_overwrite() + if 0 < overwrite[2] or self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.get_word(word, string.byte(self.context.cursor_after_line, 1)) + end + elseif misc.safe(self.completion_item.insertText) then + word = str.trim(self.completion_item.insertText) + if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.get_word(word) + end + else + word = str.trim(self.completion_item.label) + end + return str.oneline(word) + end) +end + +---Get overwrite information +---@return number, number +entry.get_overwrite = function(self) + return self.cache:ensure('get_overwrite', function() + if misc.safe(self.completion_item.textEdit) then + local r = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range) + local s = misc.to_vimindex(self.context.cursor_line, r.start.character) + local e = misc.to_vimindex(self.context.cursor_line, r['end'].character) + local before = self.context.cursor.col - s + local after = e - self.context.cursor.col + return { before, after } + end + return { 0, 0 } + end) +end + +---Create filter text +---@return string +entry.get_filter_text = function(self) + return self.cache:ensure('get_filter_text', function() + local word + if misc.safe(self.completion_item.filterText) then + word = self.completion_item.filterText + else + word = str.trim(self.completion_item.label) + end + + -- @see https://github.com/clangd/clangd/issues/815 + if misc.safe(self.completion_item.textEdit) then + local diff = self.source_offset - self:get_offset() + if diff > 0 then + if char.is_symbol(string.byte(self.context.cursor_line, self:get_offset())) then + local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff) + if string.find(word, prefix, 1, true) ~= 1 then + word = prefix .. word + end + end + end + end + + return word + end) +end + +---Get LSP's insert text +---@return string +entry.get_insert_text = function(self) + return self.cache:ensure('get_insert_text', function() + local word + if misc.safe(self.completion_item.textEdit) then + word = str.trim(self.completion_item.textEdit.newText) + if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') + end + elseif misc.safe(self.completion_item.insertText) then + word = str.trim(self.completion_item.insertText) + if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') + end + else + word = str.trim(self.completion_item.label) + end + return word + end) +end + +---Return the item is deprecated or not. +---@return boolean +entry.is_deprecated = function(self) + return self.completion_item.deprecated or vim.tbl_contains(self.completion_item.tags or {}, types.lsp.CompletionItemTag.Deprecated) +end + +---Return view information. +---@return { abbr: { text: string, bytes: number, width: number, hl_group: string }, kind: { text: string, bytes: number, width: number, hl_group: string }, menu: { text: string, bytes: number, width: number, hl_group: string } } +entry.get_view = function(self, suggest_offset) + local item = self:get_vim_item(suggest_offset) + return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0 }, function() + local view = {} + view.abbr = {} + view.abbr.text = item.abbr or '' + view.abbr.bytes = #view.abbr.text + view.abbr.width = vim.str_utfindex(view.abbr.text) + view.abbr.hl_group = self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr' + view.kind = {} + view.kind.text = item.kind or '' + view.kind.bytes = #view.kind.text + view.kind.width = vim.str_utfindex(view.kind.text) + view.kind.hl_group = 'CmpItemKind' + view.menu = {} + view.menu.text = item.menu or '' + view.menu.bytes = #view.menu.text + view.menu.width = vim.str_utfindex(view.menu.text) + view.menu.hl_group = 'CmpItemMenu' + view.dup = item.dup + return view + end) +end + +---Make vim.CompletedItem +---@param suggest_offset number +---@return vim.CompletedItem +entry.get_vim_item = function(self, suggest_offset) + return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function() + local completion_item = self:get_completion_item() + local word = self:get_word() + local abbr = str.oneline(completion_item.label) + + -- ~ indicator + if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then + abbr = abbr .. '~' + elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then + local insert_text = self:get_insert_text() + if word ~= insert_text then + abbr = abbr .. '~' + end + end + + -- append delta text + if suggest_offset < self:get_offset() then + word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word + end + + -- labelDetails. + local menu = nil + if misc.safe(completion_item.labelDetails) then + menu = '' + if misc.safe(completion_item.labelDetails.detail) then + menu = menu .. completion_item.labelDetails.detail + end + if misc.safe(completion_item.labelDetails.description) then + menu = menu .. completion_item.labelDetails.description + end + end + + -- remove duplicated string. + for i = 1, #word - 1 do + if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then + word = string.sub(word, 1, i - 1) + break + end + end + + local vim_item = { + word = word, + abbr = abbr, + kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], + menu = menu, + dup = self.completion_item.dup or 1, + } + if config.get().formatting.format then + vim_item = config.get().formatting.format(self, vim_item) + end + vim_item.word = str.oneline(vim_item.word or '') + vim_item.abbr = str.oneline(vim_item.abbr or '') + vim_item.kind = str.oneline(vim_item.kind or '') + vim_item.menu = str.oneline(vim_item.menu or '') + vim_item.equal = 1 + vim_item.empty = 1 + + return vim_item + end) +end + +---Get commit characters +---@return string[] +entry.get_commit_characters = function(self) + return misc.safe(self:get_completion_item().commitCharacters) or {} +end + +---Return insert range +---@return lsp.Range|nil +entry.get_insert_range = function(self) + local insert_range + if misc.safe(self.completion_item.textEdit) then + if misc.safe(self.completion_item.textEdit.insert) then + insert_range = self.completion_item.textEdit.insert + else + insert_range = self.completion_item.textEdit.range + end + else + insert_range = { + start = { + line = self.context.cursor.row - 1, + character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character), + }, + ['end'] = self.source_insert_range['end'], + } + end + return insert_range +end + +---Return replace range +---@return lsp.Range|nil +entry.get_replace_range = function(self) + return self.cache:ensure('get_replace_range', function() + local replace_range + if misc.safe(self.completion_item.textEdit) then + if misc.safe(self.completion_item.textEdit.replace) then + replace_range = self.completion_item.textEdit.replace + else + replace_range = self.completion_item.textEdit.range + end + else + replace_range = { + start = { + line = self.source_replace_range.start.line, + character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character), + }, + ['end'] = self.source_replace_range['end'], + } + end + return replace_range + end) +end + +---Match line. +---@param input string +---@return { score: number, matches: table[] } +entry.match = function(self, input) + return self.match_cache:ensure(input, function() + local score, matches, _ + score, matches = matcher.match(input, self:get_filter_text(), { self:get_word(), self:get_completion_item().label }) + if self:get_filter_text() ~= self:get_completion_item().label then + _, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() }) + end + return { score = score, matches = matches } + end) +end + +---Get resolved completion item if possible. +---@return lsp.CompletionItem +entry.get_completion_item = function(self) + return self.cache:ensure({ 'get_completion_item', (self.resolved_completion_item and 1 or 0) }, function() + if self.resolved_completion_item then + local completion_item = misc.copy(self.completion_item) + completion_item.detail = self.resolved_completion_item.detail or completion_item.detail + completion_item.documentation = self.resolved_completion_item.documentation or completion_item.documentation + completion_item.additionalTextEdits = self.resolved_completion_item.additionalTextEdits or completion_item.additionalTextEdits + return completion_item + end + return self.completion_item + end) +end + +---Create documentation +---@return string +entry.get_documentation = function(self) + local item = self:get_completion_item() + + local documents = {} + + -- detail + if misc.safe(item.detail) and item.detail ~= '' then + table.insert(documents, { + kind = types.lsp.MarkupKind.Markdown, + value = ('```%s\n%s\n```'):format(self.context.filetype, str.trim(item.detail)), + }) + end + + if type(item.documentation) == 'string' and item.documentation ~= '' then + table.insert(documents, { + kind = types.lsp.MarkupKind.PlainText, + value = str.trim(item.documentation), + }) + elseif type(item.documentation) == 'table' and item.documentation.value ~= '' then + table.insert(documents, item.documentation) + end + + return vim.lsp.util.convert_input_to_markdown_lines(documents) +end + +---Get completion item kind +---@return lsp.CompletionItemKind +entry.get_kind = function(self) + return misc.safe(self.completion_item.kind) or types.lsp.CompletionItemKind.Text +end + +---Execute completion item's command. +---@param callback fun() +entry.execute = function(self, callback) + self.source:execute(self:get_completion_item(), callback) +end + +---Resolve completion item. +---@param callback fun() +entry.resolve = function(self, callback) + if self.resolved_completion_item then + return callback() + end + table.insert(self.resolved_callbacks, callback) + + if not self.resolving then + self.resolving = true + self.source:resolve(self.completion_item, function(completion_item) + self.resolved_completion_item = misc.safe(completion_item) or self.completion_item + for _, c in ipairs(self.resolved_callbacks) do + c() + end + end) + end +end + +return entry diff --git a/bundle/nvim-cmp/lua/cmp/entry_spec.lua b/bundle/nvim-cmp/lua/cmp/entry_spec.lua new file mode 100644 index 000000000..08223b4f7 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/entry_spec.lua @@ -0,0 +1,281 @@ +local spec = require('cmp.utils.spec') + +local entry = require('cmp.entry') + +describe('entry', function() + before_each(spec.before) + + it('one char', function() + local state = spec.state('@.', 1, 3) + state.input('@') + local e = entry.new(state.manual(), state.source(), { + label = '@', + }) + assert.are.equal(e:get_offset(), 3) + assert.are.equal(e:get_vim_item(e:get_offset()).word, '@') + end) + + it('word length (no fix)', function() + local state = spec.state('a.b', 1, 4) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = 'b', + }) + assert.are.equal(e:get_offset(), 5) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b') + end) + + it('word length (fix)', function() + local state = spec.state('a.b', 1, 4) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = 'b.', + }) + assert.are.equal(e:get_offset(), 3) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b.') + end) + + it('semantic index (no fix)', function() + local state = spec.state('a.bc', 1, 5) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = 'c.', + }) + assert.are.equal(e:get_offset(), 6) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'c.') + end) + + it('semantic index (fix)', function() + local state = spec.state('a.bc', 1, 5) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = 'bc.', + }) + assert.are.equal(e:get_offset(), 3) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'bc.') + end) + + it('[vscode-html-language-server] 1', function() + local state = spec.state(' ', 1, 7) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = '/div', + textEdit = { + range = { + start = { + line = 0, + character = 0, + }, + ['end'] = { + line = 0, + character = 6, + }, + }, + newText = ' foo') + assert.are.equal(e:get_filter_text(), '.foo') + end) + + it('[typescript-language-server] 1', function() + local state = spec.state('Promise.resolve()', 1, 18) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + label = 'catch', + }) + -- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate. + assert.are.equal(e:get_vim_item(18).word, '.catch') + assert.are.equal(e:get_filter_text(), 'catch') + end) + + it('[typescript-language-server] 2', function() + local state = spec.state('Promise.resolve()', 1, 18) + state.input('.') + local e = entry.new(state.manual(), state.source(), { + filterText = '.Symbol', + label = 'Symbol', + textEdit = { + newText = '[Symbol]', + range = { + ['end'] = { + character = 18, + line = 0, + }, + start = { + character = 17, + line = 0, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(18).word, '[Symbol]') + assert.are.equal(e:get_filter_text(), '.Symbol') + end) + + it('[lua-language-server] 1', function() + local state = spec.state("local m = require'cmp.confi", 1, 28) + local e + + -- press g + state.input('g') + e = entry.new(state.manual(), state.source(), { + insertTextFormat = 2, + label = 'cmp.config', + textEdit = { + newText = 'cmp.config', + range = { + ['end'] = { + character = 27, + line = 1, + }, + start = { + character = 18, + line = 1, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(19).word, 'cmp.config') + assert.are.equal(e:get_filter_text(), 'cmp.config') + + -- press ' + state.input("'") + e = entry.new(state.manual(), state.source(), { + insertTextFormat = 2, + label = 'cmp.config', + textEdit = { + newText = 'cmp.config', + range = { + ['end'] = { + character = 27, + line = 1, + }, + start = { + character = 18, + line = 1, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(19).word, 'cmp.config') + assert.are.equal(e:get_filter_text(), 'cmp.config') + end) + + it('[lua-language-server] 2', function() + local state = spec.state("local m = require'cmp.confi", 1, 28) + local e + + -- press g + state.input('g') + e = entry.new(state.manual(), state.source(), { + insertTextFormat = 2, + label = 'lua.cmp.config', + textEdit = { + newText = 'lua.cmp.config', + range = { + ['end'] = { + character = 27, + line = 1, + }, + start = { + character = 18, + line = 1, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') + assert.are.equal(e:get_filter_text(), 'lua.cmp.config') + + -- press ' + state.input("'") + e = entry.new(state.manual(), state.source(), { + insertTextFormat = 2, + label = 'lua.cmp.config', + textEdit = { + newText = 'lua.cmp.config', + range = { + ['end'] = { + character = 27, + line = 1, + }, + start = { + character = 18, + line = 1, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config') + assert.are.equal(e:get_filter_text(), 'lua.cmp.config') + end) + + it('[intelephense] 1', function() + local state = spec.state('\t\t', 1, 4) + + -- press g + state.input('$') + local e = entry.new(state.manual(), state.source(), { + kind = 6, + label = '$this', + sortText = '$this', + textEdit = { + newText = '$this', + range = { + ['end'] = { + character = 3, + line = 1, + }, + start = { + character = 2, + line = 1, + }, + }, + }, + }) + assert.are.equal(e:get_vim_item(e:get_offset()).word, '$this') + assert.are.equal(e:get_filter_text(), '$this') + end) + + it('[#47] word should not contain \\n character', function() + local state = spec.state('', 1, 1) + + -- press g + state.input('_') + local e = entry.new(state.manual(), state.source(), { + kind = 6, + label = '__init__', + insertTextFormat = 1, + insertText = '__init__(self) -> None:\n pass', + }) + assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:') + assert.are.equal(e:get_filter_text(), '__init__') + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/init.lua b/bundle/nvim-cmp/lua/cmp/init.lua new file mode 100644 index 000000000..b1ebf9d6f --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/init.lua @@ -0,0 +1,312 @@ +local core = require('cmp.core') +local source = require('cmp.source') +local config = require('cmp.config') +local feedkeys = require('cmp.utils.feedkeys') +local autocmd = require('cmp.utils.autocmd') +local keymap = require('cmp.utils.keymap') +local misc = require('cmp.utils.misc') + +local cmp = {} + +cmp.core = core.new() + +---Expose types +for k, v in pairs(require('cmp.types.cmp')) do + cmp[k] = v +end +cmp.lsp = require('cmp.types.lsp') +cmp.vim = require('cmp.types.vim') + +---Export default config presets. +cmp.config = {} +cmp.config.disable = misc.none +cmp.config.compare = require('cmp.config.compare') +cmp.config.sources = require('cmp.config.sources') + +---Expose event +cmp.event = cmp.core.event + +---Export mapping +cmp.mapping = require('cmp.config.mapping') + +---Register completion sources +---@param name string +---@param s cmp.Source +---@return number +cmp.register_source = function(name, s) + local src = source.new(name, s) + cmp.core:register_source(src) + return src.id +end + +---Unregister completion source +---@param id number +cmp.unregister_source = function(id) + cmp.core:unregister_source(id) +end + +---Get current configuration. +---@return cmp.ConfigSchema +cmp.get_config = function() + return require('cmp.config').get() +end + +---Invoke completion manually +cmp.complete = function() + cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.Manual })) + return true +end + +---Return view is visible or not. +cmp.visible = function() + return cmp.core.view:visible() or vim.fn.pumvisible() == 1 +end + +---Get current selected entry or nil +cmp.get_selected_entry = function() + return cmp.core.view:get_selected_entry() +end + +---Get current active entry or nil +cmp.get_active_entry = function() + return cmp.core.view:get_active_entry() +end + +---Close current completion +cmp.close = function() + if cmp.core.view:visible() then + local release = cmp.core:suspend() + cmp.core.view:close() + cmp.core:reset() + vim.schedule(release) + return true + else + return false + end +end + +---Abort current completion +cmp.abort = function() + if cmp.core.view:visible() then + local release = cmp.core:suspend() + cmp.core.view:abort() + vim.schedule(release) + return true + else + return false + end +end + +---Suspend completion. +cmp.suspend = function() + return cmp.core:suspend() +end + +---Select next item if possible +cmp.select_next_item = function(option) + option = option or {} + + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + + if cmp.core.view:visible() then + local release = cmp.core:suspend() + cmp.core.view:select_next_item(option) + vim.schedule(release) + return true + elseif vim.fn.pumvisible() == 1 then + if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then + feedkeys.call(keymap.t(''), 'n') + else + feedkeys.call(keymap.t(''), 'n') + end + return true + end + return false +end + +---Select prev item if possible +cmp.select_prev_item = function(option) + option = option or {} + + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + + if cmp.core.view:visible() then + local release = cmp.core:suspend() + cmp.core.view:select_prev_item(option) + vim.schedule(release) + return true + elseif vim.fn.pumvisible() == 1 then + if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then + feedkeys.call(keymap.t(''), 'n') + else + feedkeys.call(keymap.t(''), 'n') + end + return true + end + return false +end + +---Scrolling documentation window if possible +cmp.scroll_docs = function(delta) + if cmp.core.view:visible() then + cmp.core.view:scroll_docs(delta) + return true + else + return false + end +end + +---Confirm completion +cmp.confirm = function(option, callback) + option = option or {} + callback = callback or function() end + + -- Hack: Ignore when executing macro. + if vim.fn.reg_executing() ~= '' then + return true + end + + local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil) + if e then + cmp.core:confirm(e, { + behavior = option.behavior, + }, function() + callback() + cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly })) + end) + return true + else + if vim.fn.complete_info({ 'selected' }).selected ~= -1 then + feedkeys.call(keymap.t(''), 'n') + return true + end + return false + end +end + +---Show status +cmp.status = function() + local kinds = {} + kinds.available = {} + kinds.unavailable = {} + kinds.installed = {} + kinds.invalid = {} + local names = {} + for _, s in pairs(cmp.core.sources) do + names[s.name] = true + + if config.get_source_config(s.name) then + if s:is_available() then + table.insert(kinds.available, s:get_debug_name()) + else + table.insert(kinds.unavailable, s:get_debug_name()) + end + else + table.insert(kinds.installed, s:get_debug_name()) + end + end + for _, s in ipairs(config.get().sources) do + if not names[s.name] then + table.insert(kinds.invalid, s.name) + end + end + + if #kinds.available > 0 then + vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) + vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {}) + for _, name in ipairs(kinds.available) do + vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) + end + end + + if #kinds.unavailable > 0 then + vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) + vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {}) + for _, name in ipairs(kinds.unavailable) do + vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) + end + end + + if #kinds.installed > 0 then + vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) + vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {}) + for _, name in ipairs(kinds.installed) do + vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) + end + end + + if #kinds.invalid > 0 then + vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {}) + vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {}) + for _, name in ipairs(kinds.invalid) do + vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {}) + end + end +end + +---@type cmp.Setup +cmp.setup = setmetatable({ + global = function(c) + config.set_global(c) + end, + buffer = function(c) + config.set_buffer(c, vim.api.nvim_get_current_buf()) + end, + cmdline = function(type, c) + config.set_cmdline(c, type) + end, +}, { + __call = function(self, c) + self.global(c) + end, +}) + +autocmd.subscribe('InsertEnter', function() + feedkeys.call('', 'i', function() + if config.enabled() then + cmp.core:prepare() + cmp.core:on_change('InsertEnter') + end + end) +end) + +autocmd.subscribe('InsertLeave', function() + cmp.core:reset() + cmp.core.view:close() +end) + +autocmd.subscribe('CmdlineEnter', function() + if config.enabled() then + cmp.core:prepare() + cmp.core:on_change('InsertEnter') + end +end) + +autocmd.subscribe('CmdlineLeave', function() + cmp.core:reset() + cmp.core.view:close() +end) + +autocmd.subscribe('TextChanged', function() + if config.enabled() then + cmp.core:on_change('TextChanged') + end +end) + +autocmd.subscribe('CursorMoved', function() + if config.enabled() then + cmp.core:on_moved() + end +end) + +cmp.event:on('confirm_done', function(e) + cmp.config.compare.recently_used:add_entry(e) +end) + +return cmp diff --git a/bundle/nvim-cmp/lua/cmp/matcher.lua b/bundle/nvim-cmp/lua/cmp/matcher.lua new file mode 100644 index 000000000..a68d6658f --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/matcher.lua @@ -0,0 +1,292 @@ +local char = require('cmp.utils.char') + +local matcher = {} + +matcher.WORD_BOUNDALY_ORDER_FACTOR = 10 + +matcher.PREFIX_FACTOR = 8 +matcher.NOT_FUZZY_FACTOR = 6 + +---@type function +matcher.debug = function(...) + return ... +end + +--- score +-- +-- ### The score +-- +-- The `score` is `matched char count` generally. +-- +-- But cmp will fix the score with some of the below points so the actual score is not `matched char count`. +-- +-- 1. Word boundary order +-- +-- cmp prefers the match that near by word-beggining. +-- +-- 2. Strict case +-- +-- cmp prefers strict match than ignorecase match. +-- +-- +-- ### Matching specs. +-- +-- 1. Prefix matching per word boundary +-- +-- `bora` -> `border-radius` # imaginary score: 4 +-- ^^~~ ^^ ~~ +-- +-- 2. Try sequential match first +-- +-- `woroff` -> `word_offset` # imaginary score: 6 +-- ^^^~~~ ^^^ ~~~ +-- +-- * The `woroff`'s second `o` should not match `word_offset`'s first `o` +-- +-- 3. Prefer early word boundary +-- +-- `call` -> `call` # imaginary score: 4.1 +-- ^^^^ ^^^^ +-- `call` -> `condition_all` # imaginary score: 4 +-- ^~~~ ^ ~~~ +-- +-- 4. Prefer strict match +-- +-- `Buffer` -> `Buffer` # imaginary score: 6.1 +-- ^^^^^^ ^^^^^^ +-- `buffer` -> `Buffer` # imaginary score: 6 +-- ^^^^^^ ^^^^^^ +-- +-- 5. Use remaining characters for substring match +-- +-- `fmodify` -> `fnamemodify` # imaginary score: 1 +-- ^~~~~~~ ^ ~~~~~~ +-- +-- 6. Avoid unexpected match detection +-- +-- `candlesingle` -> candle#accept#single +-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~ +-- +-- * The `accept`'s `a` should not match to `candle`'s `a` +-- +---Match entry +---@param input string +---@param word string +---@param words string[] +---@return number +matcher.match = function(input, word, words) + -- Empty input + if #input == 0 then + return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {} + end + + -- Ignore if input is long than word + if #input > #word then + return 0, {} + end + + --- Gather matched regions + local matches = {} + local input_start_index = 1 + local input_end_index = 1 + local word_index = 1 + local word_bound_index = 1 + while input_end_index <= #input and word_index <= #word do + local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index) + if m and input_end_index <= m.input_match_end then + m.index = word_bound_index + input_start_index = m.input_match_start + 1 + input_end_index = m.input_match_end + 1 + word_index = char.get_next_semantic_index(word, m.word_match_end) + table.insert(matches, m) + else + word_index = char.get_next_semantic_index(word, word_index) + end + word_bound_index = word_bound_index + 1 + end + + if #matches == 0 then + return 0, {} + end + + matcher.debug(word, matches) + + -- Add prefix bonus + local prefix = false + if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then + prefix = true + else + for _, w in ipairs(words or {}) do + prefix = true + local o = 1 + for i = matches[1].input_match_start, matches[1].input_match_end do + if not char.match(string.byte(w, o), string.byte(input, i)) then + prefix = false + break + end + o = o + 1 + end + if prefix then + break + end + end + end + + -- Compute prefix match score + local score = prefix and matcher.PREFIX_FACTOR or 0 + local offset = prefix and matches[1].index - 1 or 0 + local idx = 1 + for _, m in ipairs(matches) do + local s = 0 + for i = math.max(idx, m.input_match_start), m.input_match_end do + s = s + 1 + idx = i + end + idx = idx + 1 + if s > 0 then + s = s * (1 + m.strict_ratio) + s = s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - offset)) / matcher.WORD_BOUNDALY_ORDER_FACTOR) + score = score + s + end + end + + -- Check remaining input as fuzzy + if matches[#matches].input_match_end < #input then + if prefix and matcher.fuzzy(input, word, matches) then + return score, matches + end + return 0, {} + end + + return score + matcher.NOT_FUZZY_FACTOR, matches +end + +--- fuzzy +matcher.fuzzy = function(input, word, matches) + local last_match = matches[#matches] + + -- Lately specified middle of text. + local input_index = last_match.input_match_end + 1 + for i = 1, #matches - 1 do + local curr_match = matches[i] + local next_match = matches[i + 1] + local word_offset = 0 + local word_index = char.get_next_semantic_index(word, curr_match.word_match_end) + while word_offset + word_index < next_match.word_match_start and input_index <= #input do + if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then + input_index = input_index + 1 + word_offset = word_offset + 1 + else + word_index = char.get_next_semantic_index(word, word_index + word_offset) + word_offset = 0 + end + end + end + + -- Remaining text fuzzy match. + local last_input_index = input_index + local matched = false + local word_offset = 0 + local word_index = last_match.word_match_end + 1 + local input_match_start = -1 + local input_match_end = -1 + local word_match_start = -1 + local strict_count = 0 + local match_count = 0 + while word_offset + word_index <= #word and input_index <= #input do + local c1, c2 = string.byte(word, word_index + word_offset), string.byte(input, input_index) + if char.match(c1, c2) then + if not matched then + input_match_start = input_index + word_match_start = word_index + word_offset + end + matched = true + input_index = input_index + 1 + strict_count = strict_count + (c1 == c2 and 1 or 0) + match_count = match_count + 1 + elseif matched then + input_index = last_input_index + input_match_end = input_index - 1 + end + word_offset = word_offset + 1 + end + if input_index > #input then + table.insert(matches, { + input_match_start = input_match_start, + input_match_end = input_match_end, + word_match_start = word_match_start, + word_match_end = word_index + word_offset - 1, + strict_ratio = strict_count / match_count, + fuzzy = true, + }) + return true + end + return false +end + +--- find_match_region +matcher.find_match_region = function(input, input_start_index, input_end_index, word, word_index) + -- determine input position ( woroff -> word_offset ) + while input_start_index < input_end_index do + if char.match(string.byte(input, input_end_index), string.byte(word, word_index)) then + break + end + input_end_index = input_end_index - 1 + end + + -- Can't determine input position + if input_end_index < input_start_index then + return nil + end + + local input_match_start = -1 + local input_index = input_end_index + local word_offset = 0 + local strict_count = 0 + local match_count = 0 + while input_index <= #input and word_index + word_offset <= #word do + local c1 = string.byte(input, input_index) + local c2 = string.byte(word, word_index + word_offset) + if char.match(c1, c2) then + -- Match start. + if input_match_start == -1 then + input_match_start = input_index + end + + strict_count = strict_count + (c1 == c2 and 1 or 0) + match_count = match_count + 1 + word_offset = word_offset + 1 + else + -- Match end (partial region) + if input_match_start ~= -1 then + return { + input_match_start = input_match_start, + input_match_end = input_index - 1, + word_match_start = word_index, + word_match_end = word_index + word_offset - 1, + strict_ratio = strict_count / match_count, + fuzzy = false, + } + else + return nil + end + end + input_index = input_index + 1 + end + + -- Match end (whole region) + if input_match_start ~= -1 then + return { + input_match_start = input_match_start, + input_match_end = input_index - 1, + word_match_start = word_index, + word_match_end = word_index + word_offset - 1, + strict_ratio = strict_count / match_count, + fuzzy = false, + } + end + + return nil +end + +return matcher diff --git a/bundle/nvim-cmp/lua/cmp/matcher_spec.lua b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua new file mode 100644 index 000000000..1f8b0d1c1 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua @@ -0,0 +1,44 @@ +local spec = require('cmp.utils.spec') + +local matcher = require('cmp.matcher') + +describe('matcher', function() + before_each(spec.before) + + it('match', function() + assert.is.truthy(matcher.match('', 'a') >= 1) + assert.is.truthy(matcher.match('a', 'a') >= 1) + assert.is.truthy(matcher.match('ab', 'a') == 0) + assert.is.truthy(matcher.match('ab', 'ab') > matcher.match('ab', 'a_b')) + assert.is.truthy(matcher.match('ab', 'a_b_c') > matcher.match('ac', 'a_b_c')) + + assert.is.truthy(matcher.match('bora', 'border-radius') >= 1) + assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1) + assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all')) + assert.is.truthy(matcher.match('Buffer', 'Buffer') > matcher.match('Buffer', 'buffer')) + assert.is.truthy(matcher.match('fmodify', 'fnamemodify') >= 1) + assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single') >= 1) + assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode')) + assert.is.truthy(matcher.match('var_', 'var_dump') >= 1) + assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) + assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext')) + assert.is.truthy(matcher.match('call', 'calc') == 0) + + assert.is.truthy(matcher.match('vi', 'void#') >= 1) + assert.is.truthy(matcher.match('vo', 'void#') >= 1) + assert.is.truthy(matcher.match('usela', 'useLayoutEffect') > matcher.match('usela', 'useDataLayer')) + assert.is.truthy(matcher.match('true', 'v:true', { 'true' }) == matcher.match('true', 'true')) + assert.is.truthy(matcher.match('g', 'get', { 'get' }) > matcher.match('g', 'dein#get', { 'dein#get' })) + assert.is.truthy(matcher.match('2', '[[2021') >= 1) + end) + + it('debug', function() + matcher.debug = function(...) + print(vim.inspect({ ... })) + end + -- print(vim.inspect({ + -- a = matcher.match('true', 'v:true', { 'true' }), + -- b = matcher.match('true', 'true'), + -- })) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/source.lua b/bundle/nvim-cmp/lua/cmp/source.lua new file mode 100644 index 000000000..077bb48bc --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/source.lua @@ -0,0 +1,368 @@ +local context = require('cmp.context') +local config = require('cmp.config') +local entry = require('cmp.entry') +local debug = require('cmp.utils.debug') +local misc = require('cmp.utils.misc') +local cache = require('cmp.utils.cache') +local types = require('cmp.types') +local async = require('cmp.utils.async') +local pattern = require('cmp.utils.pattern') +local char = require('cmp.utils.char') + +---@class cmp.Source +---@field public id number +---@field public name string +---@field public source any +---@field public cache cmp.Cache +---@field public revision number +---@field public context cmp.Context +---@field public incomplete boolean +---@field public is_triggered_by_symbol boolean +---@field public entries cmp.Entry[] +---@field public offset number +---@field public request_offset number +---@field public status cmp.SourceStatus +---@field public complete_dedup function +local source = {} + +---@alias cmp.SourceStatus "1" | "2" | "3" +source.SourceStatus = {} +source.SourceStatus.WAITING = 1 +source.SourceStatus.FETCHING = 2 +source.SourceStatus.COMPLETED = 3 + +---@return cmp.Source +source.new = function(name, s) + local self = setmetatable({}, { __index = source }) + self.id = misc.id('cmp.source.new') + self.name = name + self.source = s + self.cache = cache.new() + self.complete_dedup = async.dedup() + self.revision = 0 + self:reset() + return self +end + +---Reset current completion state +---@return boolean +source.reset = function(self) + self.cache:clear() + self.revision = self.revision + 1 + self.context = context.empty() + self.request_offset = -1 + self.is_triggered_by_symbol = false + self.incomplete = false + self.entries = {} + self.offset = -1 + self.status = source.SourceStatus.WAITING + self.complete_dedup(function() end) +end + +---Return source option +---@return cmp.SourceConfig +source.get_config = function(self) + return config.get_source_config(self.name) or {} +end + +---Get fetching time +source.get_fetching_time = function(self) + if self.status == source.SourceStatus.FETCHING then + return vim.loop.now() - self.context.time + end + return 100 * 1000 -- return pseudo time if source isn't fetching. +end + +---Return filtered entries +---@param ctx cmp.Context +---@return cmp.Entry[] +source.get_entries = function(self, ctx) + if self.offset == -1 then + return {} + end + + local target_entries = (function() + local key = { 'get_entries', self.revision } + for i = ctx.cursor.col, self.offset, -1 do + key[3] = string.sub(ctx.cursor_before_line, 1, i) + local prev_entries = self.cache:get(key) + if prev_entries then + return prev_entries + end + end + return self.entries + end)() + + local inputs = {} + local entries = {} + for _, e in ipairs(target_entries) do + local o = e:get_offset() + if not inputs[o] then + inputs[o] = string.sub(ctx.cursor_before_line, o) + end + + local match = e:match(inputs[o]) + e.score = match.score + e.exact = false + if e.score >= 1 then + e.matches = match.matches + e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o] + table.insert(entries, e) + end + end + self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries) + + local max_item_count = self:get_config().max_item_count or 200 + local limited_entries = {} + for _, e in ipairs(entries) do + table.insert(limited_entries, e) + if max_item_count and #limited_entries >= max_item_count then + break + end + end + return limited_entries +end + +---Get default insert range +---@return lsp.Range|nil +source.get_default_insert_range = function(self) + if not self.context then + return nil + end + + return self.cache:ensure({ 'get_default_insert_range', self.revision }, function() + return { + start = { + line = self.context.cursor.row - 1, + character = misc.to_utfindex(self.context.cursor_line, self.offset), + }, + ['end'] = { + line = self.context.cursor.row - 1, + character = misc.to_utfindex(self.context.cursor_line, self.context.cursor.col), + }, + } + end) +end + +---Get default replace range +---@return lsp.Range|nil +source.get_default_replace_range = function(self) + if not self.context then + return nil + end + + return self.cache:ensure({ 'get_default_replace_range', self.revision }, function() + local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset)) + return { + start = { + line = self.context.cursor.row - 1, + character = misc.to_utfindex(self.context.cursor_line, self.offset), + }, + ['end'] = { + line = self.context.cursor.row - 1, + character = misc.to_utfindex(self.context.cursor_line, e and self.offset + e - 1 or self.context.cursor.col), + }, + } + end) +end + +---Return source name. +source.get_debug_name = function(self) + local name = self.name + if self.source.get_debug_name then + name = self.source:get_debug_name() + end + return name +end + +---Return the source is available or not. +source.is_available = function(self) + if self.source.is_available then + return self.source:is_available() + end + return true +end + +---Get keyword_pattern +---@return string +source.get_keyword_pattern = function(self) + local c = self:get_config() + if c.keyword_pattern then + return c.keyword_pattern + end + if self.source.get_keyword_pattern then + return self.source:get_keyword_pattern({ + option = self:get_config().opts, + }) + end + return config.get().completion.keyword_pattern +end + +---Get keyword_length +---@return number +source.get_keyword_length = function(self) + local c = self:get_config() + if c.keyword_length then + return c.keyword_length + end + return config.get().completion.keyword_length or 1 +end + +---Get trigger_characters +---@return string[] +source.get_trigger_characters = function(self) + local trigger_characters = {} + if self.source.get_trigger_characters then + trigger_characters = self.source:get_trigger_characters({ + option = self:get_config().opts, + }) or {} + end + if config.get().completion.get_trigger_characters then + return config.get().completion.get_trigger_characters(trigger_characters) + end + return trigger_characters +end + +---Invoke completion +---@param ctx cmp.Context +---@param callback function +---@return boolean Return true if not trigger completion. +source.complete = function(self, ctx, callback) + local offset = ctx:get_offset(self:get_keyword_pattern()) + if ctx.cursor.col <= offset then + self:reset() + end + + local before_char = string.sub(ctx.cursor_before_line, -1) + local before_char_iw = string.match(ctx.cursor_before_line, '(.)%s*$') or before_char + + if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then + if string.match(before_char, '^%a+$') then + before_char = '' + end + if string.match(before_char_iw, '^%a+$') then + before_char_iw = '' + end + end + + local completion_context + if ctx:get_reason() == types.cmp.ContextReason.Manual then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.Invoked, + } + else + if vim.tbl_contains(self:get_trigger_characters(), before_char) then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, + triggerCharacter = before_char, + } + elseif vim.tbl_contains(self:get_trigger_characters(), before_char_iw) then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, + triggerCharacter = before_char_iw, + } + elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then + if self:get_keyword_length() <= (ctx.cursor.col - offset) then + if self.incomplete and self.context.cursor.col ~= ctx.cursor.col then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions, + } + elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.Invoked, + } + end + end + else + self:reset() + end + end + + if not completion_context then + if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then + self:reset() + end + debug.log(self:get_debug_name(), 'skip completion') + return + end + + if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then + self.is_triggered_by_symbol = char.is_symbol(string.byte(completion_context.triggerCharacter)) + end + + debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context)) + local prev_status = self.status + self.status = source.SourceStatus.FETCHING + self.request_offset = offset + self.offset = offset + self.context = ctx + self.source:complete( + { + context = ctx, + offset = self.offset, + option = self:get_config().opts, + completion_context = completion_context, + }, + self.complete_dedup(vim.schedule_wrap(function(response) + response = response or {} + + self.incomplete = response.isIncomplete or false + + if #(response.items or response) > 0 then + debug.log(self:get_debug_name(), 'retrieve', #(response.items or response)) + local old_offset = self.offset + local old_entries = self.entries + + self.status = source.SourceStatus.COMPLETED + self.entries = {} + for i, item in ipairs(response.items or response) do + if (misc.safe(item) or {}).label then + local e = entry.new(ctx, self, item) + self.entries[i] = e + self.offset = math.min(self.offset, e:get_offset()) + end + end + self.revision = self.revision + 1 + if #self:get_entries(ctx) == 0 then + self.offset = old_offset + self.entries = old_entries + self.revision = self.revision + 1 + end + else + debug.log(self:get_debug_name(), 'continue', 'nil') + if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then + self:reset() + end + self.status = prev_status + end + callback() + end)) + ) + return true +end + +---Resolve CompletionItem +---@param item lsp.CompletionItem +---@param callback fun(item: lsp.CompletionItem) +source.resolve = function(self, item, callback) + if not self.source.resolve then + return callback(item) + end + self.source:resolve(item, function(resolved_item) + callback(resolved_item or item) + end) +end + +---Execute command +---@param item lsp.CompletionItem +---@param callback fun() +source.execute = function(self, item, callback) + if not self.source.execute then + return callback() + end + self.source:execute(item, function() + callback() + end) +end + +return source diff --git a/bundle/nvim-cmp/lua/cmp/source_spec.lua b/bundle/nvim-cmp/lua/cmp/source_spec.lua new file mode 100644 index 000000000..339149f69 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/source_spec.lua @@ -0,0 +1,109 @@ +local config = require('cmp.config') +local spec = require('cmp.utils.spec') + +local source = require('cmp.source') + +describe('source', function() + before_each(spec.before) + + describe('keyword length', function() + it('not enough', function() + config.set_buffer({ + completion = { + keyword_length = 3, + }, + }, vim.api.nvim_get_current_buf()) + + local state = spec.state('', 1, 1) + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ { label = 'spec' } }) + end, + }) + assert.is.truthy(not s:complete(state.input('a'), function() end)) + end) + + it('enough', function() + config.set_buffer({ + completion = { + keyword_length = 3, + }, + }, vim.api.nvim_get_current_buf()) + + local state = spec.state('', 1, 1) + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ { label = 'spec' } }) + end, + }) + assert.is.truthy(s:complete(state.input('aiu'), function() end)) + end) + + it('enough -> not enough', function() + config.set_buffer({ + completion = { + keyword_length = 3, + }, + }, vim.api.nvim_get_current_buf()) + + local state = spec.state('', 1, 1) + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ { label = 'spec' } }) + end, + }) + assert.is.truthy(s:complete(state.input('aiu'), function() end)) + assert.is.truthy(not s:complete(state.backspace(), function() end)) + end) + + it('continue', function() + config.set_buffer({ + completion = { + keyword_length = 3, + }, + }, vim.api.nvim_get_current_buf()) + + local state = spec.state('', 1, 1) + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ { label = 'spec' } }) + end, + }) + assert.is.truthy(s:complete(state.input('aiu'), function() end)) + assert.is.truthy(not s:complete(state.input('eo'), function() end)) + end) + end) + + describe('isIncomplete', function() + it('isIncomplete=true', function() + local state = spec.state('', 1, 1) + local s = source.new('spec', { + complete = function(_, _, callback) + callback({ + items = { { label = 'spec' } }, + isIncomplete = true, + }) + end, + }) + vim.wait(100, function() + return s.status == source.SourceStatus.COMPLETED + end, 100, false) + assert.is.truthy(s:complete(state.input('s'), function() end)) + vim.wait(100, function() + return s.status == source.SourceStatus.COMPLETED + end, 100, false) + assert.is.truthy(s:complete(state.input('p'), function() end)) + vim.wait(100, function() + return s.status == source.SourceStatus.COMPLETED + end, 100, false) + assert.is.truthy(s:complete(state.input('e'), function() end)) + vim.wait(100, function() + return s.status == source.SourceStatus.COMPLETED + end, 100, false) + assert.is.truthy(s:complete(state.input('c'), function() end)) + vim.wait(100, function() + return s.status == source.SourceStatus.COMPLETED + end, 100, false) + end) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/types/cmp.lua b/bundle/nvim-cmp/lua/cmp/types/cmp.lua new file mode 100644 index 000000000..c1fb3df7c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/types/cmp.lua @@ -0,0 +1,128 @@ +local cmp = {} + +---@alias cmp.ConfirmBehavior "'insert'" | "'replace'" +cmp.ConfirmBehavior = {} +cmp.ConfirmBehavior.Insert = 'insert' +cmp.ConfirmBehavior.Replace = 'replace' + +---@alias cmp.SelectBehavior "'insert'" | "'select'" +cmp.SelectBehavior = {} +cmp.SelectBehavior.Insert = 'insert' +cmp.SelectBehavior.Select = 'select' + +---@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'" +cmp.ContextReason = {} +cmp.ContextReason.Auto = 'auto' +cmp.ContextReason.Manual = 'manual' +cmp.ContextReason.TriggerOnly = 'triggerOnly' +cmp.ContextReason.None = 'none' + +---@alias cmp.TriggerEvent "'InsertEnter'" | "'TextChanged'" +cmp.TriggerEvent = {} +cmp.TriggerEvent.InsertEnter = 'InsertEnter' +cmp.TriggerEvent.TextChanged = 'TextChanged' + +---@alias cmp.PreselectMode "'item'" | "'None'" +cmp.PreselectMode = {} +cmp.PreselectMode.Item = 'item' +cmp.PreselectMode.None = 'none' + +---@alias cmp.ItemField "'abbr'" | "'kind'" | "'menu'" +cmp.ItemField = {} +cmp.ItemField.Abbr = 'abbr' +cmp.ItemField.Kind = 'kind' +cmp.ItemField.Menu = 'menu' + +---@class cmp.ContextOption +---@field public reason cmp.ContextReason|nil + +---@class cmp.ConfirmOption +---@field public behavior cmp.ConfirmBehavior + +---@class cmp.SelectOption +---@field public behavior cmp.SelectBehavior + +---@class cmp.SnippetExpansionParams +---@field public body string +---@field public insert_text_mode number + +---@class cmp.Setup +---@field public __call fun(c: cmp.ConfigSchema) +---@field public buffer fun(c: cmp.ConfigSchema) +---@field public global fun(c: cmp.ConfigSchema) +---@field public cmdline fun(type: string, c: cmp.ConfigSchema) + +---@class cmp.SourceBaseApiParams +---@field public option table + +---@class cmp.SourceCompletionApiParams : cmp.SourceBaseApiParams +---@field public context cmp.Context +---@field public offset number +---@field public completion_context lsp.CompletionContext + +---@class cmp.Mapping +---@field public i nil|function(fallback: function): void +---@field public c nil|function(fallback: function): void +---@field public x nil|function(fallback: function): void +---@field public s nil|function(fallback: function): void + +---@class cmp.ConfigSchema +---@field private revision number +---@field public enabled fun():boolean|boolean +---@field public preselect cmp.PreselectMode +---@field public completion cmp.CompletionConfig +---@field public documentation cmp.DocumentationConfig|"false" +---@field public confirmation cmp.ConfirmationConfig +---@field public sorting cmp.SortingConfig +---@field public formatting cmp.FormattingConfig +---@field public snippet cmp.SnippetConfig +---@field public mapping table +---@field public sources cmp.SourceConfig[] +---@field public experimental cmp.ExperimentalConfig + +---@class cmp.CompletionConfig +---@field public autocomplete cmp.TriggerEvent[] +---@field public completeopt string +---@field public keyword_pattern string +---@field public keyword_length number +---@field public get_trigger_characters fun(trigger_characters: string[]): string[] + +---@class cmp.DocumentationConfig +---@field public border string[] +---@field public winhighlight string +---@field public maxwidth number|nil +---@field public maxheight number|nil +---@field public zindex number|nil + +---@class cmp.ConfirmationConfig +---@field public default_behavior cmp.ConfirmBehavior +---@field public get_commit_characters fun(commit_characters: string[]): string[] + +---@class cmp.SortingConfig +---@field public priority_weight number +---@field public comparators function[] + +---@class cmp.FormattingConfig +---@field public fields cmp.ItemField[] +---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem + +---@class cmp.SnippetConfig +---@field public expand fun(args: cmp.SnippetExpansionParams) + +---@class cmp.ExperimentalConfig +---@field public native_menu boolean +---@field public ghost_text cmp.GhostTextConfig|"false" + +---@class cmp.GhostTextConfig +---@field hl_group string + +---@class cmp.SourceConfig +---@field public name string +---@field public opts table +---@field public priority number|nil +---@field public keyword_pattern string +---@field public keyword_length number +---@field public max_item_count number +---@field public group_index number + +return cmp diff --git a/bundle/nvim-cmp/lua/cmp/types/init.lua b/bundle/nvim-cmp/lua/cmp/types/init.lua new file mode 100644 index 000000000..c4f601ef9 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/types/init.lua @@ -0,0 +1,7 @@ +local types = {} + +types.cmp = require('cmp.types.cmp') +types.lsp = require('cmp.types.lsp') +types.vim = require('cmp.types.vim') + +return types diff --git a/bundle/nvim-cmp/lua/cmp/types/lsp.lua b/bundle/nvim-cmp/lua/cmp/types/lsp.lua new file mode 100644 index 000000000..45b432118 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/types/lsp.lua @@ -0,0 +1,197 @@ +local misc = require('cmp.utils.misc') +---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/ +---@class lsp +local lsp = {} + +lsp.Position = {} + +---Convert lsp.Position to vim.Position +---@param buf number|string +---@param position lsp.Position +---@return vim.Position +lsp.Position.to_vim = function(buf, position) + if not vim.api.nvim_buf_is_loaded(buf) then + vim.fn.bufload(buf) + end + local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) + if #lines > 0 then + return { + row = position.line + 1, + col = misc.to_vimindex(lines[1], position.character), + } + end + return { + row = position.line + 1, + col = position.character + 1, + } +end + +---Convert lsp.Position to vim.Position +---@param buf number|string +---@param position vim.Position +---@return lsp.Position +lsp.Position.to_lsp = function(buf, position) + if not vim.api.nvim_buf_is_loaded(buf) then + vim.fn.bufload(buf) + end + local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) + if #lines > 0 then + return { + line = position.row - 1, + character = misc.to_utfindex(lines[1], position.col), + } + end + return { + line = position.row - 1, + character = position.col - 1, + } +end + +lsp.Range = {} + +---Convert lsp.Position to vim.Position +---@param buf number|string +---@param range lsp.Range +---@return vim.Range +lsp.Range.to_vim = function(buf, range) + return { + start = lsp.Position.to_vim(buf, range.start), + ['end'] = lsp.Position.to_vim(buf, range['end']), + } +end + +---Convert lsp.Position to vim.Position +---@param buf number|string +---@param range vim.Range +---@return lsp.Range +lsp.Range.to_lsp = function(buf, range) + return { + start = lsp.Position.to_lsp(buf, range.start), + ['end'] = lsp.Position.to_lsp(buf, range['end']), + } +end + +---@alias lsp.CompletionTriggerKind "1" | "2" | "3" +lsp.CompletionTriggerKind = {} +lsp.CompletionTriggerKind.Invoked = 1 +lsp.CompletionTriggerKind.TriggerCharacter = 2 +lsp.CompletionTriggerKind.TriggerForIncompleteCompletions = 3 + +---@class lsp.CompletionContext +---@field public triggerKind lsp.CompletionTriggerKind +---@field public triggerCharacter string|nil + +---@alias lsp.InsertTextFormat "1" | "2" +lsp.InsertTextFormat = {} +lsp.InsertTextFormat.PlainText = 1 +lsp.InsertTextFormat.Snippet = 2 +lsp.InsertTextFormat = vim.tbl_add_reverse_lookup(lsp.InsertTextFormat) + +---@alias lsp.InsertTextMode "1" | "2" +lsp.InsertTextMode = {} +lsp.InsertTextMode.AsIs = 0 +lsp.InsertTextMode.AdjustIndentation = 1 +lsp.InsertTextMode = vim.tbl_add_reverse_lookup(lsp.InsertTextMode) + +---@alias lsp.MarkupKind "'plaintext'" | "'markdown'" +lsp.MarkupKind = {} +lsp.MarkupKind.PlainText = 'plaintext' +lsp.MarkupKind.Markdown = 'markdown' +lsp.MarkupKind.Markdown = 'markdown' +lsp.MarkupKind = vim.tbl_add_reverse_lookup(lsp.MarkupKind) + +---@alias lsp.CompletionItemTag "1" +lsp.CompletionItemTag = {} +lsp.CompletionItemTag.Deprecated = 1 +lsp.CompletionItemTag = vim.tbl_add_reverse_lookup(lsp.CompletionItemTag) + +---@alias lsp.CompletionItemKind "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20" | "21" | "22" | "23" | "24" | "25" +lsp.CompletionItemKind = {} +lsp.CompletionItemKind.Text = 1 +lsp.CompletionItemKind.Method = 2 +lsp.CompletionItemKind.Function = 3 +lsp.CompletionItemKind.Constructor = 4 +lsp.CompletionItemKind.Field = 5 +lsp.CompletionItemKind.Variable = 6 +lsp.CompletionItemKind.Class = 7 +lsp.CompletionItemKind.Interface = 8 +lsp.CompletionItemKind.Module = 9 +lsp.CompletionItemKind.Property = 10 +lsp.CompletionItemKind.Unit = 11 +lsp.CompletionItemKind.Value = 12 +lsp.CompletionItemKind.Enum = 13 +lsp.CompletionItemKind.Keyword = 14 +lsp.CompletionItemKind.Snippet = 15 +lsp.CompletionItemKind.Color = 16 +lsp.CompletionItemKind.File = 17 +lsp.CompletionItemKind.Reference = 18 +lsp.CompletionItemKind.Folder = 19 +lsp.CompletionItemKind.EnumMember = 20 +lsp.CompletionItemKind.Constant = 21 +lsp.CompletionItemKind.Struct = 22 +lsp.CompletionItemKind.Event = 23 +lsp.CompletionItemKind.Operator = 24 +lsp.CompletionItemKind.TypeParameter = 25 +lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind) + +---@class lsp.CompletionList +---@field public isIncomplete boolean +---@field public items lsp.CompletionItem[] + +---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil + +---@class lsp.MarkupContent +---@field public kind lsp.MarkupKind +---@field public value string + +---@class lsp.Position +---@field public line number +---@field public character number + +---@class lsp.Range +---@field public start lsp.Position +---@field public end lsp.Position + +---@class lsp.Command +---@field public title string +---@field public command string +---@field public arguments any[]|nil + +---@class lsp.TextEdit +---@field public range lsp.Range|nil +---@field public newText string + +---@class lsp.InsertReplaceTextEdit +---@field public insert lsp.Range|nil +---@field public replace lsp.Range|nil +---@field public newText string + +---@class lsp.CompletionItemLabelDetails +---@field public detail string|nil +---@field public description string|nil + +---@class lsp.CompletionItem +---@field public label string +---@field public labelDetails lsp.CompletionItemLabelDetails|nil +---@field public kind lsp.CompletionItemKind|nil +---@field public tags lsp.CompletionItemTag[]|nil +---@field public detail string|nil +---@field public documentation lsp.MarkupContent|string|nil +---@field public deprecated boolean|nil +---@field public preselect boolean|nil +---@field public sortText string|nil +---@field public filterText string|nil +---@field public insertText string|nil +---@field public insertTextFormat lsp.InsertTextFormat +---@field public insertTextMode lsp.InsertTextMode +---@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil +---@field public additionalTextEdits lsp.TextEdit[] +---@field public commitCharacters string[]|nil +---@field public command lsp.Command|nil +---@field public data any|nil +--- +---TODO: Should send the issue for upstream? +---@field public word string|nil +---@field public dup boolean|nil + +return lsp diff --git a/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua b/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua new file mode 100644 index 000000000..81d91216c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua @@ -0,0 +1,46 @@ +local spec = require('cmp.utils.spec') +local lsp = require('cmp.types.lsp') + +describe('types.lsp', function() + before_each(spec.before) + describe('Position', function() + vim.fn.setline('1', { + 'あいうえお', + 'かきくけこ', + 'さしすせそ', + }) + local vim_position, lsp_position + + vim_position = lsp.Position.to_vim('%', { line = 1, character = 3 }) + assert.are.equal(vim_position.row, 2) + assert.are.equal(vim_position.col, 10) + lsp_position = lsp.Position.to_lsp('%', vim_position) + assert.are.equal(lsp_position.line, 1) + assert.are.equal(lsp_position.character, 3) + + vim_position = lsp.Position.to_vim('%', { line = 1, character = 0 }) + assert.are.equal(vim_position.row, 2) + assert.are.equal(vim_position.col, 1) + lsp_position = lsp.Position.to_lsp('%', vim_position) + assert.are.equal(lsp_position.line, 1) + assert.are.equal(lsp_position.character, 0) + + vim_position = lsp.Position.to_vim('%', { line = 1, character = 5 }) + assert.are.equal(vim_position.row, 2) + assert.are.equal(vim_position.col, 16) + lsp_position = lsp.Position.to_lsp('%', vim_position) + assert.are.equal(lsp_position.line, 1) + assert.are.equal(lsp_position.character, 5) + + -- overflow (lsp -> vim) + vim_position = lsp.Position.to_vim('%', { line = 1, character = 6 }) + assert.are.equal(vim_position.row, 2) + assert.are.equal(vim_position.col, 16) + + -- overflow(vim -> lsp) + vim_position.col = vim_position.col + 1 + lsp_position = lsp.Position.to_lsp('%', vim_position) + assert.are.equal(lsp_position.line, 1) + assert.are.equal(lsp_position.character, 5) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/types/vim.lua b/bundle/nvim-cmp/lua/cmp/types/vim.lua new file mode 100644 index 000000000..a2a6cf4d1 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/types/vim.lua @@ -0,0 +1,17 @@ +---@class vim.CompletedItem +---@field public word string +---@field public abbr string|nil +---@field public kind string|nil +---@field public menu string|nil +---@field public equal "1"|nil +---@field public empty "1"|nil +---@field public dup "1"|nil +---@field public id any + +---@class vim.Position +---@field public row number +---@field public col number + +---@class vim.Range +---@field public start vim.Position +---@field public end vim.Position diff --git a/bundle/nvim-cmp/lua/cmp/utils/api.lua b/bundle/nvim-cmp/lua/cmp/utils/api.lua new file mode 100644 index 000000000..9f9879d32 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/api.lua @@ -0,0 +1,68 @@ +local api = {} + +local CTRL_V = vim.api.nvim_replace_termcodes('', true, true, true) +local CTRL_S = vim.api.nvim_replace_termcodes('', true, true, true) + +api.get_mode = function() + local mode = vim.api.nvim_get_mode().mode:sub(1, 1) + if mode == 'i' then + return 'i' -- insert + elseif mode == 'v' or mode == 'V' or mode == CTRL_V then + return 'x' -- visual + elseif mode == 's' or mode == 'S' or mode == CTRL_S then + return 's' -- select + elseif mode == 'c' and vim.fn.getcmdtype() ~= '=' then + return 'c' -- cmdline + end +end + +api.is_insert_mode = function() + return api.get_mode() == 'i' +end + +api.is_cmdline_mode = function() + return api.get_mode() == 'c' +end + +api.is_select_mode = function() + return api.get_mode() == 's' +end + +api.is_visual_mode = function() + return api.get_mode() == 'x' +end + +api.is_suitable_mode = function() + local mode = api.get_mode() + return mode == 'i' or mode == 'c' +end + +api.get_current_line = function() + if api.is_cmdline_mode() then + return vim.fn.getcmdline() + end + return vim.api.nvim_get_current_line() +end + +api.get_cursor = function() + if api.is_cmdline_mode() then + return { vim.o.lines - (vim.api.nvim_get_option('cmdheight') or 1) + 1, vim.fn.getcmdpos() - 1 } + end + return vim.api.nvim_win_get_cursor(0) +end + +api.get_screen_cursor = function() + if api.is_cmdline_mode() then + return api.get_cursor() + end + local cursor = api.get_cursor() + local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) + return { pos.row, pos.col - 1 } +end + +api.get_cursor_before_line = function() + local cursor = api.get_cursor() + return string.sub(api.get_current_line(), 1, cursor[2]) +end + +return api diff --git a/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua new file mode 100644 index 000000000..5363b485b --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/api_spec.lua @@ -0,0 +1,46 @@ +local spec = require('cmp.utils.spec') +local keymap = require('cmp.utils.keymap') +local feedkeys = require('cmp.utils.feedkeys') +local api = require('cmp.utils.api') + +describe('api', function() + describe('get_cursor', function() + before_each(spec.before) + it('insert-mode', function() + local cursor + feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() + cursor = api.get_cursor() + end) + assert.are.equal(cursor[2], 11) + end) + it('cmdline-mode', function() + local cursor + keymap.set_map(0, 'c', '(cmp-spec-spy)', function() + cursor = api.get_cursor() + end, { expr = true, noremap = true }) + feedkeys.call(keymap.t(':\t1234567890'), 'n') + feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') + assert.are.equal(cursor[2], 11) + end) + end) + + describe('get_cursor_before_line', function() + before_each(spec.before) + it('insert-mode', function() + local cursor_before_line + feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() + cursor_before_line = api.get_cursor_before_line() + end) + assert.are.same(cursor_before_line, '\t12345678') + end) + it('cmdline-mode', function() + local cursor_before_line + keymap.set_map(0, 'c', '(cmp-spec-spy)', function() + cursor_before_line = api.get_cursor_before_line() + end, { expr = true, noremap = true }) + feedkeys.call(keymap.t(':\t1234567890'), 'n') + feedkeys.call(keymap.t('(cmp-spec-spy)'), 'x') + assert.are.same(cursor_before_line, '\t12345678') + end) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/async.lua b/bundle/nvim-cmp/lua/cmp/utils/async.lua new file mode 100644 index 000000000..39d8777e0 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/async.lua @@ -0,0 +1,101 @@ +local async = {} + +---@class cmp.AsyncThrottle +---@field public timeout number +---@field public stop function +---@field public __call function + +---@param fn function +---@param timeout number +---@return cmp.AsyncThrottle +async.throttle = function(fn, timeout) + local time = nil + local timer = vim.loop.new_timer() + return setmetatable({ + timeout = timeout, + stop = function() + time = nil + timer:stop() + end, + }, { + __call = function(self, ...) + local args = { ... } + + if time == nil then + time = vim.loop.now() + end + timer:stop() + + local delta = math.max(1, self.timeout - (vim.loop.now() - time)) + timer:start(delta, 0, function() + time = nil + fn(unpack(args)) + end) + end, + }) +end + +---Control async tasks. +async.step = function(...) + local tasks = { ... } + local next + next = function(...) + if #tasks > 0 then + table.remove(tasks, 1)(next, ...) + end + end + table.remove(tasks, 1)(next) +end + +---Timeout callback function +---@param fn function +---@param timeout number +---@return function +async.timeout = function(fn, timeout) + local timer + local done = false + local callback = function(...) + if not done then + done = true + timer:stop() + timer:close() + fn(...) + end + end + timer = vim.loop.new_timer() + timer:start(timeout, 0, function() + callback() + end) + return callback +end + +---@alias cmp.AsyncDedup fun(callback: function): function + +---Create deduplicated callback +---@return function +async.dedup = function() + local id = 0 + return function(callback) + id = id + 1 + + local current = id + return function(...) + if current == id then + callback(...) + end + end + end +end + +---Convert async process as sync +async.sync = function(runner, timeout) + local done = false + runner(function() + done = true + end) + vim.wait(timeout, function() + return done + end, 10, false) +end + +return async diff --git a/bundle/nvim-cmp/lua/cmp/utils/async_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/async_spec.lua new file mode 100644 index 000000000..62f5379e2 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/async_spec.lua @@ -0,0 +1,69 @@ +local async = require('cmp.utils.async') + +describe('utils.async', function() + it('throttle', function() + local count = 0 + local now + local f = async.throttle(function() + count = count + 1 + end, 100) + + -- 1. delay for 100ms + now = vim.loop.now() + f.timeout = 100 + f() + vim.wait(1000, function() + return count == 1 + end) + assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) + + -- 2. delay for 500ms + now = vim.loop.now() + f.timeout = 500 + f() + vim.wait(1000, function() + return count == 2 + end) + assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10) + + -- 4. delay for 500ms and wait 100ms (remain 400ms) + f.timeout = 500 + f() + vim.wait(100) -- remain 400ms + + -- 5. call immediately (100ms already elapsed from No.4) + now = vim.loop.now() + f.timeout = 100 + f() + vim.wait(1000, function() + return count == 3 + end) + assert.is.truthy(math.abs(vim.loop.now() - now) < 10) + end) + it('step', function() + local done = false + local step = {} + async.step(function(next) + vim.defer_fn(function() + table.insert(step, 1) + next() + end, 10) + end, function(next) + vim.defer_fn(function() + table.insert(step, 2) + next() + end, 10) + end, function(next) + vim.defer_fn(function() + table.insert(step, 3) + next() + end, 10) + end, function() + done = true + end) + vim.wait(1000, function() + return done + end) + assert.are.same(step, { 1, 2, 3 }) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua b/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua new file mode 100644 index 000000000..4af766a07 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/autocmd.lua @@ -0,0 +1,35 @@ +local debug = require('cmp.utils.debug') + +local autocmd = {} + +autocmd.events = {} + +---Subscribe autocmd +---@param event string +---@param callback function +---@return function +autocmd.subscribe = function(event, callback) + autocmd.events[event] = autocmd.events[event] or {} + table.insert(autocmd.events[event], callback) + return function() + for i, callback_ in ipairs(autocmd.events[event]) do + if callback_ == callback then + table.remove(autocmd.events[event], i) + break + end + end + end +end + +---Emit autocmd +---@param event string +autocmd.emit = function(event) + debug.log(' ') + debug.log(string.format('>>> %s', event)) + autocmd.events[event] = autocmd.events[event] or {} + for _, callback in ipairs(autocmd.events[event]) do + callback() + end +end + +return autocmd diff --git a/bundle/nvim-cmp/lua/cmp/utils/binary.lua b/bundle/nvim-cmp/lua/cmp/utils/binary.lua new file mode 100644 index 000000000..ab1b0d225 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/binary.lua @@ -0,0 +1,33 @@ +local binary = {} + +---Insert item to list to ordered index +---@param list any[] +---@param item any +---@param func fun(a: any, b: any): "1"|"-1"|"0" +binary.insort = function(list, item, func) + table.insert(list, binary.search(list, item, func), item) +end + +---Search suitable index from list +---@param list any[] +---@param item any +---@param func fun(a: any, b: any): "1"|"-1"|"0" +---@return number +binary.search = function(list, item, func) + local s = 1 + local e = #list + while s <= e do + local idx = math.floor((e + s) / 2) + local diff = func(item, list[idx]) + if diff > 0 then + s = idx + 1 + elseif diff < 0 then + e = idx - 1 + else + return idx + 1 + end + end + return s +end + +return binary diff --git a/bundle/nvim-cmp/lua/cmp/utils/binary_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/binary_spec.lua new file mode 100644 index 000000000..92fe129e7 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/binary_spec.lua @@ -0,0 +1,28 @@ +local binary = require('cmp.utils.binary') + +describe('utils.binary', function() + it('insort', function() + local func = function(a, b) + return a.score - b.score + end + local list = {} + binary.insort(list, { id = 'a', score = 1 }, func) + binary.insort(list, { id = 'b', score = 5 }, func) + binary.insort(list, { id = 'c', score = 2.5 }, func) + binary.insort(list, { id = 'd', score = 2 }, func) + binary.insort(list, { id = 'e', score = 8 }, func) + binary.insort(list, { id = 'g', score = 8 }, func) + binary.insort(list, { id = 'h', score = 7 }, func) + binary.insort(list, { id = 'i', score = 6 }, func) + binary.insort(list, { id = 'j', score = 4 }, func) + assert.are.equal(list[1].id, 'a') + assert.are.equal(list[2].id, 'd') + assert.are.equal(list[3].id, 'c') + assert.are.equal(list[4].id, 'j') + assert.are.equal(list[5].id, 'b') + assert.are.equal(list[6].id, 'i') + assert.are.equal(list[7].id, 'h') + assert.are.equal(list[8].id, 'e') + assert.are.equal(list[9].id, 'g') + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/buffer.lua b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua new file mode 100644 index 000000000..f3d1c8e64 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua @@ -0,0 +1,17 @@ +local buffer = {} + +buffer.ensure = setmetatable({ + cache = {}, +}, { + __call = function(self, name) + if not (self.cache[name] and vim.api.nvim_buf_is_valid(self.cache[name])) then + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') + vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') + self.cache[name] = buf + end + return self.cache[name] + end, +}) + +return buffer diff --git a/bundle/nvim-cmp/lua/cmp/utils/cache.lua b/bundle/nvim-cmp/lua/cmp/utils/cache.lua new file mode 100644 index 000000000..8607b2a3f --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/cache.lua @@ -0,0 +1,58 @@ +---@class cmp.Cache +---@field public entries any +local cache = {} + +cache.new = function() + local self = setmetatable({}, { __index = cache }) + self.entries = {} + return self +end + +---Get cache value +---@param key string +---@return any|nil +cache.get = function(self, key) + key = self:key(key) + if self.entries[key] ~= nil then + return self.entries[key] + end + return nil +end + +---Set cache value explicitly +---@param key string +---@vararg any +cache.set = function(self, key, value) + key = self:key(key) + self.entries[key] = value +end + +---Ensure value by callback +---@param key string +---@param callback fun(): any +cache.ensure = function(self, key, callback) + local value = self:get(key) + if value == nil then + local v = callback() + self:set(key, v) + return v + end + return value +end + +---Clear all cache entries +cache.clear = function(self) + self.entries = {} +end + +---Create key +---@param key string|table +---@return string +cache.key = function(_, key) + if type(key) == 'table' then + return table.concat(key, ':') + end + return key +end + +return cache diff --git a/bundle/nvim-cmp/lua/cmp/utils/char.lua b/bundle/nvim-cmp/lua/cmp/utils/char.lua new file mode 100644 index 000000000..12a1809e5 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/char.lua @@ -0,0 +1,115 @@ +local alpha = {} +string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char) + alpha[string.byte(char)] = true +end) + +local ALPHA = {} +string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char) + ALPHA[string.byte(char)] = true +end) + +local digit = {} +string.gsub('1234567890', '.', function(char) + digit[string.byte(char)] = true +end) + +local white = {} +string.gsub(' \t\n', '.', function(char) + white[string.byte(char)] = true +end) + +local char = {} + +---@param byte number +---@return boolean +char.is_upper = function(byte) + return ALPHA[byte] +end + +---@param byte number +---@return boolean +char.is_alpha = function(byte) + return alpha[byte] or ALPHA[byte] +end + +---@param byte number +---@return boolean +char.is_digit = function(byte) + return digit[byte] +end + +---@param byte number +---@return boolean +char.is_white = function(byte) + return white[byte] +end + +---@param byte number +---@return boolean +char.is_symbol = function(byte) + return not (char.is_alnum(byte) or char.is_white(byte)) +end + +---@param byte number +---@return boolean +char.is_printable = function(byte) + return string.match(string.char(byte), '^%c$') == nil +end + +---@param byte number +---@return boolean +char.is_alnum = function(byte) + return char.is_alpha(byte) or char.is_digit(byte) +end + +---@param text string +---@param index number +---@return boolean +char.is_semantic_index = function(text, index) + if index <= 1 then + return true + end + + local prev = string.byte(text, index - 1) + local curr = string.byte(text, index) + + if not char.is_upper(prev) and char.is_upper(curr) then + return true + end + if char.is_symbol(curr) or char.is_white(curr) then + return true + end + if not char.is_alpha(prev) and char.is_alpha(curr) then + return true + end + if not char.is_digit(prev) and char.is_digit(curr) then + return true + end + return false +end + +---@param text string +---@param current_index number +---@return boolean +char.get_next_semantic_index = function(text, current_index) + for i = current_index + 1, #text do + if char.is_semantic_index(text, i) then + return i + end + end + return #text + 1 +end + +---Ignore case match +---@param byte1 number +---@param byte2 number +---@return boolean +char.match = function(byte1, byte2) + if not char.is_alpha(byte1) or not char.is_alpha(byte2) then + return byte1 == byte2 + end + local diff = byte1 - byte2 + return diff == 0 or diff == 32 or diff == -32 +end + +return char diff --git a/bundle/nvim-cmp/lua/cmp/utils/debug.lua b/bundle/nvim-cmp/lua/cmp/utils/debug.lua new file mode 100644 index 000000000..c8b0dba16 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/debug.lua @@ -0,0 +1,20 @@ +local debug = {} + +debug.flag = false + +---Print log +---@vararg any +debug.log = function(...) + if debug.flag then + local data = {} + for _, v in ipairs({ ... }) do + if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then + v = vim.inspect(v) + end + table.insert(data, v) + end + print(table.concat(data, '\t')) + end +end + +return debug diff --git a/bundle/nvim-cmp/lua/cmp/utils/event.lua b/bundle/nvim-cmp/lua/cmp/utils/event.lua new file mode 100644 index 000000000..662d5731e --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/event.lua @@ -0,0 +1,51 @@ +---@class cmp.Event +---@field private events table +local event = {} + +---Create vents +event.new = function() + local self = setmetatable({}, { __index = event }) + self.events = {} + return self +end + +---Add event listener +---@param name string +---@param callback function +---@return function +event.on = function(self, name, callback) + if not self.events[name] then + self.events[name] = {} + end + table.insert(self.events[name], callback) + return function() + self:off(name, callback) + end +end + +---Remove event listener +---@param name string +---@param callback function +event.off = function(self, name, callback) + for i, callback_ in ipairs(self.events[name] or {}) do + if callback_ == callback then + table.remove(self.events[name], i) + break + end + end +end + +---Remove all events +event.clear = function(self) + self.events = {} +end + +---Emit event +---@param name string +event.emit = function(self, name, ...) + for _, callback in ipairs(self.events[name] or {}) do + callback(...) + end +end + +return event diff --git a/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua b/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua new file mode 100644 index 000000000..e7810a7e5 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/feedkeys.lua @@ -0,0 +1,110 @@ +local keymap = require('cmp.utils.keymap') +local misc = require('cmp.utils.misc') + +local feedkeys = {} + +feedkeys.call = setmetatable({ + callbacks = {}, +}, { + __call = function(self, keys, mode, callback) + if vim.fn.reg_recording() ~= '' then + return feedkeys.call_macro(keys, mode, callback) + end + + local is_insert = string.match(mode, 'i') ~= nil + local is_immediate = string.match(mode, 'x') ~= nil + + local queue = {} + if #keys > 0 then + table.insert(queue, { keymap.t('set lazyredraw'), 'n' }) + table.insert(queue, { keymap.t('set textwidth=0'), 'n' }) + table.insert(queue, { keymap.t('set eventignore=all'), 'n' }) + table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true }) + table.insert(queue, { keymap.t('set %slazyredraw'):format(vim.o.lazyredraw and '' or 'no'), 'n' }) + table.insert(queue, { keymap.t('set textwidth=%s'):format(vim.bo.textwidth or 0), 'n' }) + table.insert(queue, { keymap.t('set eventignore=%s'):format(vim.o.eventignore or ''), 'n' }) + end + + if callback then + local id = misc.id('cmp.utils.feedkeys.call') + self.callbacks[id] = callback + table.insert(queue, { keymap.t('call v:lua.cmp.utils.feedkeys.call.run(%s)'):format(id), 'n', true }) + end + + if is_insert then + for i = #queue, 1, -1 do + vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3]) + end + else + for i = 1, #queue do + vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3]) + end + end + + if is_immediate then + vim.api.nvim_feedkeys('', 'x', true) + end + end, +}) +misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id) + if feedkeys.call.callbacks[id] then + feedkeys.call.callbacks[id]() + feedkeys.call.callbacks[id] = nil + end + return '' +end) + +feedkeys.call_macro = setmetatable({ + queue = {}, + current = nil, + timer = vim.loop.new_timer(), + running = false, +}, { + __call = function(self, keys, mode, callback) + local is_insert = string.match(mode, 'i') ~= nil + table.insert(self.queue, is_insert and 1 or #self.queue + 1, { + keys = keys, + mode = mode, + callback = callback, + }) + + if not self.running then + self.running = true + local consume + consume = vim.schedule_wrap(function() + if vim.fn.getchar(1) == 0 then + if self.current then + vim.cmd(('set backspace=%s'):format(self.current.backspace or '')) + vim.cmd(('set eventignore=%s'):format(self.current.eventignore or '')) + if self.current.callback then + self.current.callback() + end + self.current = nil + end + + local current = table.remove(self.queue, 1) + if current then + self.current = { + keys = current.keys, + callback = current.callback, + backspace = vim.o.backspace, + eventignore = vim.o.eventignore, + } + vim.api.nvim_feedkeys(keymap.t('set backspace=start'), 'n', true) + vim.api.nvim_feedkeys(keymap.t('set eventignore=all'), 'n', true) + vim.api.nvim_feedkeys(current.keys, string.gsub(current.mode, '[i]', ''), true) -- 'i' flag is manually resolved. + end + end + + if #self.queue ~= 0 or self.current then + vim.defer_fn(consume, 1) + else + self.running = false + end + end) + vim.defer_fn(consume, 1) + end + end, +}) + +return feedkeys diff --git a/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua new file mode 100644 index 000000000..d024bb04c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua @@ -0,0 +1,56 @@ +local spec = require('cmp.utils.spec') +local keymap = require('cmp.utils.keymap') + +local feedkeys = require('cmp.utils.feedkeys') + +describe('feedkeys', function() + before_each(spec.before) + + it('dot-repeat', function() + local reg + feedkeys.call(keymap.t('iaiueo'), 'nx', function() + reg = vim.fn.getreg('.') + end) + assert.are.equal(reg, keymap.t('aiueo')) + end) + + it('textwidth', function() + vim.cmd([[setlocal textwidth=6]]) + feedkeys.call(keymap.t('iaiueo '), 'nx') + feedkeys.call(keymap.t('aaiueoaiueo'), 'nx') + assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { + 'aiueo aiueoaiueo', + }) + end) + + it('autoindent', function() + vim.cmd([[setlocal indentkeys+==end]]) + feedkeys.call(keymap.t('iifend') .. keymap.autoindent(), 'nx') + assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { + 'if', + 'end', + }) + end) + + it('testability', function() + feedkeys.call('i', 'n', function() + feedkeys.call('', 'n', function() + feedkeys.call('aiueo', 'in') + end) + feedkeys.call('', 'n', function() + feedkeys.call(keymap.t(''), 'in') + end) + feedkeys.call('', 'n', function() + feedkeys.call(keymap.t('abcde'), 'in') + end) + feedkeys.call('', 'n', function() + feedkeys.call(keymap.t(''), 'in') + end) + feedkeys.call('', 'n', function() + feedkeys.call(keymap.t('12345'), 'in') + end) + end) + feedkeys.call('', 'x') + assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { '12345' }) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/highlight.lua b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua new file mode 100644 index 000000000..c8278b229 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua @@ -0,0 +1,46 @@ +local highlight = {} + +highlight.keys = { + 'gui', + 'guifg', + 'guibg', + 'cterm', + 'ctermfg', + 'ctermbg', +} + +highlight.inherit = function(name, source, override) + local cmd = ('highlight! default %s'):format(name) + for _, key in ipairs(highlight.keys) do + if override[key] then + cmd = cmd .. (' %s=%s'):format(key, override[key]) + else + local v = highlight.get(source, key) + v = v == '' and 'NONE' or v + cmd = cmd .. (' %s=%s'):format(key, v) + end + end + vim.cmd(cmd) +end + +highlight.get = function(source, key) + if key == 'gui' or key == 'cterm' then + local ui = {} + for _, k in ipairs({ 'bold', 'italic', 'reverse', 'inverse', 'standout', 'underline', 'undercurl', 'strikethrough' }) do + if vim.fn.synIDattr(vim.fn.hlID(source), k, key) == 1 then + table.insert(ui, k) + end + end + return table.concat(ui, ',') + elseif key == 'guifg' then + return vim.fn.synIDattr(vim.fn.hlID(source), 'fg#', 'gui') + elseif key == 'guibg' then + return vim.fn.synIDattr(vim.fn.hlID(source), 'bg#', 'gui') + elseif key == 'ctermfg' then + return vim.fn.synIDattr(vim.fn.hlID(source), 'fg', 'term') + elseif key == 'ctermbg' then + return vim.fn.synIDattr(vim.fn.hlID(source), 'bg', 'term') + end +end + +return highlight diff --git a/bundle/nvim-cmp/lua/cmp/utils/keymap.lua b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua new file mode 100644 index 000000000..d0ca963b4 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua @@ -0,0 +1,267 @@ +local misc = require('cmp.utils.misc') +local api = require('cmp.utils.api') + +local keymap = {} + +---Shortcut for nvim_replace_termcodes +---@param keys string +---@return string +keymap.t = function(keys) + return vim.api.nvim_replace_termcodes(keys, true, true, true) +end + +---Normalize key sequence. +---@param keys string +---@return string +keymap.normalize = function(keys) + vim.api.nvim_set_keymap('t', '(cmp.utils.keymap.normalize)', keys, {}) + for _, map in ipairs(vim.api.nvim_get_keymap('t')) do + if keymap.equals(map.lhs, '(cmp.utils.keymap.normalize)') then + return map.rhs + end + end + return keys +end + +---Return vim notation keymapping (simple conversion). +---@param s string +---@return string +keymap.to_keymap = setmetatable({ + [''] = { '\n', '\r', '\r\n' }, + [''] = { '\t' }, + [''] = { '\\' }, + [''] = { '|' }, + [''] = { ' ' }, +}, { + __call = function(self, s) + return string.gsub(s, '.', function(c) + for key, chars in pairs(self) do + if vim.tbl_contains(chars, c) then + return key + end + end + return c + end) + end, +}) + +---Mode safe break undo +keymap.undobreak = function() + if not api.is_insert_mode() then + return '' + end + return keymap.t('u') +end + +---Mode safe join undo +keymap.undojoin = function() + if not api.is_insert_mode() then + return '' + end + return keymap.t('U') +end + +---Create backspace keys. +---@param count number +---@return string +keymap.backspace = function(count) + if count <= 0 then + return '' + end + local keys = {} + table.insert(keys, keymap.t(string.rep('', count))) + return table.concat(keys, '') +end + +---Create autoindent keys +---@return string +keymap.autoindent = function() + local keys = {} + table.insert(keys, keymap.t('setlocal cindent')) + table.insert(keys, keymap.t('setlocal indentkeys+=!^F')) + table.insert(keys, keymap.t('')) + table.insert(keys, keymap.t('setlocal %scindent'):format(vim.bo.cindent and '' or 'no')) + table.insert(keys, keymap.t('setlocal indentkeys=%s'):format(vim.bo.indentkeys:gsub('|', '\\|'))) + return table.concat(keys, '') +end + +---Return two key sequence are equal or not. +---@param a string +---@param b string +---@return boolean +keymap.equals = function(a, b) + return keymap.t(a) == keymap.t(b) +end + +---Register keypress handler. +keymap.listen = function(mode, lhs, callback) + lhs = keymap.normalize(keymap.to_keymap(lhs)) + + local existing = keymap.get_mapping(mode, lhs) + local id = string.match(existing.rhs, 'v:lua%.cmp%.utils%.keymap%.set_map%((%d+)%)') + if id and keymap.set_map.callbacks[tonumber(id, 10)] then + return + end + + local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1 + local fallback = keymap.evacuate(bufnr, mode, lhs) + keymap.set_map(bufnr, mode, lhs, function() + if mode == 'c' and vim.fn.getcmdtype() == '=' then + return vim.api.nvim_feedkeys(keymap.t(fallback.keys), fallback.mode, true) + end + + callback( + lhs, + misc.once(function() + vim.api.nvim_feedkeys(keymap.t(fallback.keys), fallback.mode, true) + end) + ) + end, { + expr = false, + noremap = true, + silent = true, + }) +end + +---Get mapping +---@param mode string +---@param lhs string +---@return table +keymap.get_mapping = function(mode, lhs) + lhs = keymap.normalize(lhs) + + for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do + if keymap.equals(map.lhs, lhs) then + return { + lhs = map.lhs, + rhs = map.rhs, + expr = map.expr == 1, + noremap = map.noremap == 1, + script = map.script == 1, + silent = map.silent == 1, + nowait = map.nowait == 1, + buffer = true, + } + end + end + + for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do + if keymap.equals(map.lhs, lhs) then + return { + lhs = map.lhs, + rhs = map.rhs, + expr = map.expr == 1, + noremap = map.noremap == 1, + script = map.script == 1, + silent = map.silent == 1, + nowait = map.nowait == 1, + buffer = false, + } + end + end + + return { + lhs = lhs, + rhs = lhs, + expr = false, + noremap = true, + script = false, + silent = false, + nowait = false, + buffer = false, + } +end + +---Evacuate existing key mapping +---@param bufnr number +---@param mode string +---@param lhs string +---@return { keys: string, mode: string } +keymap.evacuate = function(bufnr, mode, lhs) + local map = keymap.get_mapping(mode, lhs) + if not map then + return { keys = lhs, mode = 'itn' } + end + + -- Keep existing mapping as mapping. We escape fisrt recursive key sequence. See `:help recursive_mapping`) + local rhs = map.rhs + if not map.noremap and map.expr then + -- remap & expr mapping should evacuate as mapping with solving recursive mapping. + rhs = function() + return keymap.t(keymap.recursive(bufnr, mode, lhs, vim.api.nvim_eval(map.rhs))) + end + elseif map.noremap and map.expr then + -- noremap & expr mapping should always evacuate as mapping. + rhs = rhs + elseif map.script then + -- script mapping should always evacuate as mapping. + rhs = rhs + elseif not map.noremap then + -- remap & non-expr mapping should be checked if recursive or not. + rhs = keymap.recursive(bufnr, mode, lhs, rhs) + if keymap.equals(rhs, map.rhs) or map.noremap then + return { keys = rhs, mode = 'it' .. (map.noremap and 'n' or '') } + end + else + -- noremap & non-expr mapping doesn't need to evacuate. + return { keys = rhs, mode = 'it' .. (map.noremap and 'n' or '') } + end + + local fallback = ('(cmp.utils.keymap.evacuate:%s)'):format(map.lhs) + keymap.set_map(bufnr, mode, fallback, rhs, { + expr = map.expr, + noremap = map.noremap, + script = map.script, + silent = mode ~= 'c', -- I can't understand but it solves the #427 (wilder.nvim's mapping does not work if silent=true in cmdline mode...) + }) + return { keys = fallback, mode = 'it' } +end + +---Solve recursive mapping +---@param bufnr number +---@param mode string +---@param lhs string +---@param rhs string +---@return string +keymap.recursive = function(bufnr, mode, lhs, rhs) + rhs = keymap.normalize(rhs) + + local recursive_lhs = ('(cmp.utils.keymap.recursive:%s)'):format(lhs) + local recursive_rhs = string.gsub(rhs, '^' .. vim.pesc(keymap.normalize(lhs)), recursive_lhs) + if not keymap.equals(recursive_rhs, rhs) then + keymap.set_map(bufnr, mode, recursive_lhs, lhs, { + expr = false, + noremap = true, + silent = true, + }) + end + return recursive_rhs +end + +---Set keymapping +keymap.set_map = setmetatable({ + callbacks = {}, +}, { + __call = function(self, bufnr, mode, lhs, rhs, opts) + if type(rhs) == 'function' then + local id = misc.id('cmp.utils.keymap.set_map') + self.callbacks[id] = rhs + if opts.expr then + rhs = ('v:lua.cmp.utils.keymap.set_map(%s)'):format(id) + else + rhs = ('call v:lua.cmp.utils.keymap.set_map(%s)'):format(id) + end + end + + if bufnr == -1 then + vim.api.nvim_set_keymap(mode, lhs, rhs, opts) + else + vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) + end + end, +}) +misc.set(_G, { 'cmp', 'utils', 'keymap', 'set_map' }, function(id) + return keymap.set_map.callbacks[id]() or '' +end) + +return keymap diff --git a/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua new file mode 100644 index 000000000..315220cdb --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua @@ -0,0 +1,81 @@ +local spec = require('cmp.utils.spec') + +local keymap = require('cmp.utils.keymap') + +describe('keymap', function() + before_each(spec.before) + + it('to_keymap', function() + assert.are.equal(keymap.to_keymap('\n'), '') + assert.are.equal(keymap.to_keymap(''), '') + assert.are.equal(keymap.to_keymap('|'), '') + end) + + describe('evacuate', function() + before_each(spec.before) + + it('expr & register', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', [['="("']], { + expr = true, + noremap = false, + }) + local fallback = keymap.evacuate(0, 'i', '(') + vim.api.nvim_feedkeys('i' .. keymap.t(fallback.keys), fallback.mode .. 'x', true) + assert.are.same({ '(' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + + it('recursive & (tpope/vim-endwise)', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(paren-close)', [[)]], { + expr = false, + noremap = true, + }) + vim.api.nvim_buf_set_keymap(0, 'i', '(', [[((paren-close)]], { + expr = false, + noremap = false, + }) + local fallback = keymap.evacuate(0, 'i', '(') + vim.api.nvim_feedkeys('i' .. keymap.t(fallback.keys), fallback.mode .. 'x', true) + assert.are.same({ '()' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + + describe('expr & recursive', function() + before_each(spec.before) + + it('true', function() + vim.api.nvim_buf_set_keymap(0, 'i', '', [[v:true ? '="foobar"' : 'aiueo']], { + expr = true, + noremap = false, + }) + local fallback = keymap.evacuate(0, 'i', '') + vim.api.nvim_feedkeys('i' .. keymap.t(fallback.keys), fallback.mode .. 'x', true) + assert.are.same({ 'foobar' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + it('false', function() + vim.api.nvim_buf_set_keymap(0, 'i', '', [[v:false ? '="foobar"' : 'aiueo']], { + expr = true, + noremap = false, + }) + local fallback = keymap.evacuate(0, 'i', '') + vim.api.nvim_feedkeys('i' .. keymap.t(fallback.keys), fallback.mode .. 'x', true) + assert.are.same({ '\taiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + end) + end) + describe('realworld', function() + before_each(spec.before) + it('#226', function() + keymap.listen('i', '', function(_, fallback) + fallback() + end) + vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) + assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + it('#414', function() + keymap.listen('i', '', function() + vim.api.nvim_feedkeys(keymap.t(''), 'int', true) + end) + vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) + assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/misc.lua b/bundle/nvim-cmp/lua/cmp/utils/misc.lua new file mode 100644 index 000000000..8ce41d48c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/misc.lua @@ -0,0 +1,181 @@ +local misc = {} + +---Create once callback +---@param callback function +---@return function +misc.once = function(callback) + local done = false + return function(...) + if done then + return + end + done = true + callback(...) + end +end + +---Return concatenated list +---@param list1 any[] +---@param list2 any[] +---@return any[] +misc.concat = function(list1, list2) + local new_list = {} + for _, v in ipairs(list1) do + table.insert(new_list, v) + end + for _, v in ipairs(list2) do + table.insert(new_list, v) + end + return new_list +end + +---The symbol to remove key in misc.merge. +misc.none = vim.NIL + +---Merge two tables recursively +---@generic T +---@param v1 T +---@param v2 T +---@return T +misc.merge = function(v1, v2) + local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1)) + local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2)) + if merge1 and merge2 then + local new_tbl = {} + for k, v in pairs(v2) do + new_tbl[k] = misc.merge(v1[k], v) + end + for k, v in pairs(v1) do + if v2[k] == nil and v ~= misc.none then + new_tbl[k] = v + end + end + return new_tbl + end + if v1 == misc.none then + return nil + end + if v1 == nil then + if v2 == misc.none then + return nil + else + return v2 + end + end + if v1 == true then + if merge2 then + return v2 + end + return {} + end + + return v1 +end + +---Generate id for group name +misc.id = setmetatable({ + group = {}, +}, { + __call = function(_, group) + misc.id.group[group] = misc.id.group[group] or vim.loop.now() + misc.id.group[group] = misc.id.group[group] + 1 + return misc.id.group[group] + end, +}) + +---Check the value is nil or not. +---@param v boolean +---@return boolean +misc.safe = function(v) + if v == nil or v == vim.NIL then + return nil + end + return v +end + +---Treat 1/0 as bool value +---@param v boolean|"1"|"0" +---@param def boolean +---@return boolean +misc.bool = function(v, def) + if misc.safe(v) == nil then + return def + end + return v == true or v == 1 +end + +---Set value to deep object +---@param t table +---@param keys string[] +---@param v any +misc.set = function(t, keys, v) + local c = t + for i = 1, #keys - 1 do + local key = keys[i] + c[key] = misc.safe(c[key]) or {} + c = c[key] + end + c[keys[#keys]] = v +end + +---Copy table +---@generic T +---@param tbl T +---@return T +misc.copy = function(tbl) + if type(tbl) ~= 'table' then + return tbl + end + + if vim.tbl_islist(tbl) then + local copy = {} + for i, value in ipairs(tbl) do + copy[i] = misc.copy(value) + end + return copy + end + + local copy = {} + for key, value in pairs(tbl) do + copy[key] = misc.copy(value) + end + return copy +end + +---Safe version of vim.str_utfindex +---@param text string +---@param vimindex number +---@return number +misc.to_utfindex = function(text, vimindex) + return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) +end + +---Safe version of vim.str_byteindex +---@param text string +---@param utfindex number +---@return number +misc.to_vimindex = function(text, utfindex) + for i = utfindex, 1, -1 do + local s, v = pcall(function() + return vim.str_byteindex(text, i) + 1 + end) + if s then + return v + end + end + return utfindex + 1 +end + +---Mark the function as deprecated +misc.deprecated = function(fn, msg) + local printed = false + return function(...) + if not printed then + print(msg) + printed = true + end + return fn(...) + end +end + +return misc diff --git a/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua new file mode 100644 index 000000000..4e705efa1 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/misc_spec.lua @@ -0,0 +1,51 @@ +local spec = require('cmp.utils.spec') + +local misc = require('cmp.utils.misc') + +describe('misc', function() + before_each(spec.before) + + it('merge', function() + local merged + merged = misc.merge({ + a = {}, + }, { + a = { + b = 1, + }, + }) + assert.are.equal(merged.a.b, 1) + + merged = misc.merge({ + a = false, + }, { + a = { + b = 1, + }, + }) + assert.are.equal(merged.a, false) + + merged = misc.merge({ + a = misc.none, + }, { + a = { + b = 1, + }, + }) + assert.are.equal(merged.a, nil) + + merged = misc.merge({ + a = misc.none, + }, { + a = nil, + }) + assert.are.equal(merged.a, nil) + + merged = misc.merge({ + a = nil, + }, { + a = misc.none, + }) + assert.are.equal(merged.a, nil) + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/pattern.lua b/bundle/nvim-cmp/lua/cmp/utils/pattern.lua new file mode 100644 index 000000000..1481e84db --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/pattern.lua @@ -0,0 +1,28 @@ +local pattern = {} + +pattern._regexes = {} + +pattern.regex = function(p) + if not pattern._regexes[p] then + pattern._regexes[p] = vim.regex(p) + end + return pattern._regexes[p] +end + +pattern.offset = function(p, text) + local s, e = pattern.regex(p):match_str(text) + if s then + return s + 1, e + 1 + end + return nil, nil +end + +pattern.matchstr = function(p, text) + local s, e = pattern.offset(p, text) + if s then + return string.sub(text, s, e) + end + return nil +end + +return pattern diff --git a/bundle/nvim-cmp/lua/cmp/utils/spec.lua b/bundle/nvim-cmp/lua/cmp/utils/spec.lua new file mode 100644 index 000000000..a4b2c8314 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/spec.lua @@ -0,0 +1,92 @@ +local context = require('cmp.context') +local source = require('cmp.source') +local types = require('cmp.types') +local config = require('cmp.config') + +local spec = {} + +spec.before = function() + vim.cmd([[ + bdelete! + enew! + imapclear + imapclear + cmapclear + cmapclear + smapclear + smapclear + xmapclear + xmapclear + tmapclear + tmapclear + setlocal noswapfile + setlocal virtualedit=all + setlocal completeopt=menu,menuone,noselect + ]]) + config.set_global({ + sources = { + { name = 'spec' }, + }, + snippet = { + expand = function(args) + local ctx = context.new() + vim.api.nvim_buf_set_text(ctx.bufnr, ctx.cursor.row - 1, ctx.cursor.col - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, vim.split(string.gsub(args.body, '%$0', ''), '\n')) + for i, t in ipairs(vim.split(args.body, '\n')) do + local s = string.find(t, '$0', 1, true) + if s then + if i == 1 then + vim.api.nvim_win_set_cursor(0, { ctx.cursor.row, ctx.cursor.col + s - 2 }) + else + vim.api.nvim_win_set_cursor(0, { ctx.cursor.row + i - 1, s - 1 }) + end + break + end + end + end, + }, + }) + config.set_cmdline({ + sources = { + { name = 'spec' }, + }, + }, ':') +end + +spec.state = function(text, row, col) + vim.fn.setline(1, text) + vim.fn.cursor(row, col) + local ctx = context.empty() + local s = source.new('spec', { + complete = function() end, + }) + return { + context = function() + return ctx + end, + source = function() + return s + end, + backspace = function() + vim.fn.feedkeys('x', 'nx') + vim.fn.feedkeys('h', 'nx') + ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) + s:complete(ctx, function() end) + return ctx + end, + input = function(char) + vim.fn.feedkeys(('i%s'):format(char), 'nx') + vim.fn.feedkeys(string.rep('l', #char), 'nx') + ctx.prev_context = nil + ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto }) + s:complete(ctx, function() end) + return ctx + end, + manual = function() + ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual }) + s:complete(ctx, function() end) + return ctx + end, + } +end + +return spec diff --git a/bundle/nvim-cmp/lua/cmp/utils/str.lua b/bundle/nvim-cmp/lua/cmp/utils/str.lua new file mode 100644 index 000000000..91d1bd75c --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/str.lua @@ -0,0 +1,156 @@ +local char = require('cmp.utils.char') +local pattern = require('cmp.utils.pattern') + +local str = {} + +local INVALID_CHARS = {} +INVALID_CHARS[string.byte("'")] = true +INVALID_CHARS[string.byte('"')] = true +INVALID_CHARS[string.byte('=')] = true +INVALID_CHARS[string.byte('$')] = true +INVALID_CHARS[string.byte('(')] = true +INVALID_CHARS[string.byte('[')] = true +INVALID_CHARS[string.byte(' ')] = true +INVALID_CHARS[string.byte('\t')] = true +INVALID_CHARS[string.byte('\n')] = true +INVALID_CHARS[string.byte('\r')] = true + +local NR_BYTE = string.byte('\n') + +local PAIR_CHARS = {} +PAIR_CHARS[string.byte('[')] = string.byte(']') +PAIR_CHARS[string.byte('(')] = string.byte(')') +PAIR_CHARS[string.byte('<')] = string.byte('>') + +---Return if specified text has prefix or not +---@param text string +---@param prefix string +---@return boolean +str.has_prefix = function(text, prefix) + if #text < #prefix then + return false + end + for i = 1, #prefix do + if not char.match(string.byte(text, i), string.byte(prefix, i)) then + return false + end + end + return true +end + +---Remove suffix +---@param text string +---@param suffix string +---@return string +str.remove_suffix = function(text, suffix) + if #text < #suffix then + return text + end + + local i = 0 + while i < #suffix do + if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then + return text + end + i = i + 1 + end + return string.sub(text, 1, -#suffix - 1) +end + +---strikethrough +---@param text string +---@return string +str.strikethrough = function(text) + local r = pattern.regex('.') + local buffer = '' + while text ~= '' do + local s, e = r:match_str(text) + if not s then + break + end + buffer = buffer .. string.sub(text, s, e) .. '̶' + text = string.sub(text, e + 1) + end + return buffer +end + +---trim +---@param text string +---@return string +str.trim = function(text) + local s = 1 + for i = 1, #text do + if not char.is_white(string.byte(text, i)) then + s = i + break + end + end + + local e = #text + for i = #text, 1, -1 do + if not char.is_white(string.byte(text, i)) then + e = i + break + end + end + if s == 1 and e == #text then + return text + end + return string.sub(text, s, e) +end + +---get_word +---@param text string +---@return string +str.get_word = function(text, stop_char) + local valids = {} + local has_valid = false + for idx = 1, #text do + local c = string.byte(text, idx) + local invalid = INVALID_CHARS[c] and not (valids[c] and stop_char ~= c) + if has_valid and invalid then + return string.sub(text, 1, idx - 1) + end + valids[c] = true + if PAIR_CHARS[c] then + valids[PAIR_CHARS[c]] = true + end + has_valid = has_valid or not invalid + end + return text +end + +---Oneline +---@param text string +---@return string +str.oneline = function(text) + for i = 1, #text do + if string.byte(text, i) == NR_BYTE then + return string.sub(text, 1, i - 1) + end + end + return text +end + +---Escape special chars +---@param text string +---@param chars string[] +---@return string +str.escape = function(text, chars) + table.insert(chars, '\\') + local escaped = {} + local i = 1 + while i <= #text do + local c = string.sub(text, i, i) + if vim.tbl_contains(chars, c) then + table.insert(escaped, '\\') + table.insert(escaped, c) + else + table.insert(escaped, c) + end + i = i + 1 + end + return table.concat(escaped, '') +end + +return str diff --git a/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua new file mode 100644 index 000000000..d4e492ea4 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua @@ -0,0 +1,30 @@ +local str = require('cmp.utils.str') + +describe('utils.str', function() + it('get_word', function() + assert.are.equal(str.get_word('print'), 'print') + assert.are.equal(str.get_word('$variable'), '$variable') + assert.are.equal(str.get_word('print()'), 'print') + assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]') + assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies') + end) + + it('strikethrough', function() + assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶') + end) + + it('remove_suffix', function() + assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') + assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') + assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()') + assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}') + end) + + it('escape', function() + assert.are.equal(str.escape('plain', {}), 'plain') + assert.are.equal(str.escape('plain\\', {}), 'plain\\\\') + assert.are.equal(str.escape('plain\\"', {}), 'plain\\\\"') + assert.are.equal(str.escape('pla"in', { '"' }), 'pla\\"in') + assert.are.equal(str.escape('call("")', { '"' }), 'call(\\"\\")') + end) +end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/window.lua b/bundle/nvim-cmp/lua/cmp/utils/window.lua new file mode 100644 index 000000000..98d8e1779 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/utils/window.lua @@ -0,0 +1,261 @@ +local cache = require('cmp.utils.cache') +local misc = require('cmp.utils.misc') +local buffer = require('cmp.utils.buffer') +local api = require('cmp.utils.api') + +---@class cmp.WindowStyle +---@field public relative string +---@field public row number +---@field public col number +---@field public width number +---@field public height number +---@field public zindex number|nil + +---@class cmp.Window +---@field public name string +---@field public win number|nil +---@field public swin1 number|nil +---@field public swin2 number|nil +---@field public style cmp.WindowStyle +---@field public opt table +---@field public cache cmp.Cache +local window = {} + +---new +---@return cmp.Window +window.new = function() + local self = setmetatable({}, { __index = window }) + self.name = misc.id('cmp.utils.window.new') + self.win = nil + self.swin1 = nil + self.swin2 = nil + self.style = {} + self.cache = cache.new() + self.opt = {} + return self +end + +---Set window option. +---NOTE: If the window already visible, immediately applied to it. +---@param key string +---@param value any +window.option = function(self, key, value) + if vim.fn.exists('+' .. key) == 0 then + return + end + + if value == nil then + return self.opt[key] + end + + self.opt[key] = value + if self:visible() then + vim.api.nvim_win_set_option(self.win, key, value) + end +end + +---Set style. +---@param style cmp.WindowStyle +window.set_style = function(self, style) + if vim.o.columns and vim.o.columns <= style.col + style.width then + style.width = vim.o.columns - style.col - 1 + end + if vim.o.lines and vim.o.lines <= style.row + style.height then + style.height = vim.o.lines - style.row - 1 + end + self.style = style + self.style.zindex = self.style.zindex or 1 +end + +---Return buffer id. +---@return number +window.get_buffer = function(self) + return buffer.ensure(self.name) +end + +---Open window +---@param style cmp.WindowStyle +window.open = function(self, style) + if style then + self:set_style(style) + end + + if self.style.width < 1 or self.style.height < 1 then + return + end + + if self.win and vim.api.nvim_win_is_valid(self.win) then + vim.api.nvim_win_set_config(self.win, self.style) + else + local s = misc.copy(self.style) + s.noautocmd = true + self.win = vim.api.nvim_open_win(buffer.ensure(self.name), false, s) + for k, v in pairs(self.opt) do + vim.api.nvim_win_set_option(self.win, k, v) + end + end + self:update() +end + +---Update +window.update = function(self) + if self:has_scrollbar() then + local total = self:get_content_height() + local info = self:info() + local bar_height = math.ceil(info.height * (info.height / total)) + local bar_offset = math.min(info.height - bar_height, math.floor(info.height * (vim.fn.getwininfo(self.win)[1].topline / total))) + local style1 = {} + style1.relative = 'editor' + style1.style = 'minimal' + style1.width = 1 + style1.height = info.height + style1.row = info.row + style1.col = info.col + info.width - (info.has_scrollbar and 1 or 0) + style1.zindex = (self.style.zindex and (self.style.zindex + 1) or 1) + if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then + vim.api.nvim_win_set_config(self.swin1, style1) + else + style1.noautocmd = true + self.swin1 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf1'), false, style1) + vim.api.nvim_win_set_option(self.swin1, 'winhighlight', 'EndOfBuffer:PmenuSbar,Normal:PmenuSbar,NormalNC:PmenuSbar,NormalFloat:PmenuSbar') + end + local style2 = {} + style2.relative = 'editor' + style2.style = 'minimal' + style2.width = 1 + style2.height = bar_height + style2.row = info.row + bar_offset + style2.col = info.col + info.width - (info.has_scrollbar and 1 or 0) + style2.zindex = (self.style.zindex and (self.style.zindex + 2) or 2) + if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then + vim.api.nvim_win_set_config(self.swin2, style2) + else + style2.noautocmd = true + self.swin2 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf2'), false, style2) + vim.api.nvim_win_set_option(self.swin2, 'winhighlight', 'EndOfBuffer:PmenuThumb,Normal:PmenuThumb,NormalNC:PmenuThumb,NormalFloat:PmenuThumb') + end + else + if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then + vim.api.nvim_win_hide(self.swin1) + self.swin1 = nil + end + if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then + vim.api.nvim_win_hide(self.swin2) + self.swin2 = nil + end + end + + -- In cmdline, vim does not redraw automatically. + if api.is_cmdline_mode() then + vim.api.nvim_win_call(self.win, function() + vim.cmd([[redraw]]) + end) + end +end + +---Close window +window.close = function(self) + if self.win and vim.api.nvim_win_is_valid(self.win) then + if self.win and vim.api.nvim_win_is_valid(self.win) then + vim.api.nvim_win_hide(self.win) + self.win = nil + end + if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then + vim.api.nvim_win_hide(self.swin1) + self.swin1 = nil + end + if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then + vim.api.nvim_win_hide(self.swin2) + self.swin2 = nil + end + end +end + +---Return the window is visible or not. +window.visible = function(self) + return self.win and vim.api.nvim_win_is_valid(self.win) +end + +---Return the scrollbar will shown or not. +window.has_scrollbar = function(self) + return (self.style.height or 0) < self:get_content_height() +end + +---Return win info. +window.info = function(self) + local border_width = self:get_border_width() + local has_scrollbar = self:has_scrollbar() + return { + row = self.style.row, + col = self.style.col, + width = self.style.width + border_width + (has_scrollbar and 1 or 0), + height = self.style.height, + border_width = border_width, + has_scrollbar = has_scrollbar, + } +end + +---Get border width +---@return number +window.get_border_width = function(self) + local border = self.style.border + if type(border) == 'table' then + local new_border = {} + while #new_border < 8 do + for _, b in ipairs(border) do + table.insert(new_border, b) + end + end + border = new_border + end + + local w = 0 + if border then + if type(border) == 'string' then + if border == 'single' then + w = 2 + elseif border == 'solid' then + w = 2 + elseif border == 'double' then + w = 2 + elseif border == 'rounded' then + w = 2 + elseif border == 'shadow' then + w = 1 + end + elseif type(border) == 'table' then + local b4 = type(border[4]) == 'table' and border[4][1] or border[4] + if #b4 > 0 then + w = w + 1 + end + local b8 = type(border[8]) == 'table' and border[8][1] or border[8] + if #b8 > 0 then + w = w + 1 + end + end + end + return w +end + +---Get scroll height. +---@return number +window.get_content_height = function(self) + if not self:option('wrap') then + return vim.api.nvim_buf_line_count(self:get_buffer()) + end + + return self.cache:ensure({ + 'get_content_height', + self.style.width, + self:get_buffer(), + vim.api.nvim_buf_get_changedtick(self:get_buffer()), + }, function() + local height = 0 + for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do + height = height + math.ceil(math.max(1, vim.str_utfindex(text)) / self.style.width) + end + return height + end) +end + +return window diff --git a/bundle/nvim-cmp/lua/cmp/view.lua b/bundle/nvim-cmp/lua/cmp/view.lua new file mode 100644 index 000000000..135ffa6a7 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/view.lua @@ -0,0 +1,227 @@ +local config = require('cmp.config') +local async = require('cmp.utils.async') +local event = require('cmp.utils.event') +local keymap = require('cmp.utils.keymap') +local docs_view = require('cmp.view.docs_view') +local custom_entries_view = require('cmp.view.custom_entries_view') +local native_entries_view = require('cmp.view.native_entries_view') +local ghost_text_view = require('cmp.view.ghost_text_view') + +---@class cmp.View +---@field public event cmp.Event +---@field private resolve_dedup cmp.AsyncDedup +---@field private native_entries_view cmp.NativeEntriesView +---@field private custom_entries_view cmp.CustomEntriesView +---@field private change_dedup cmp.AsyncDedup +---@field private docs_view cmp.DocsView +---@field private ghost_text_view cmp.GhostTextView +local view = {} + +---Create menu +view.new = function() + local self = setmetatable({}, { __index = view }) + self.resolve_dedup = async.dedup() + self.custom_entries_view = custom_entries_view.new() + self.native_entries_view = native_entries_view.new() + self.docs_view = docs_view.new() + self.ghost_text_view = ghost_text_view.new() + self.event = event.new() + + return self +end + +---Return the view components are available or not. +---@return boolean +view.ready = function(self) + return self:_get_entries_view():ready() +end + +---OnChange handler. +view.on_change = function(self) + self:_get_entries_view():on_change() +end + +---Open menu +---@param ctx cmp.Context +---@param sources cmp.Source[] +view.open = function(self, ctx, sources) + local source_group_map = {} + for _, s in ipairs(sources) do + local group_index = s:get_config().group_index or 0 + if not source_group_map[group_index] then + source_group_map[group_index] = {} + end + table.insert(source_group_map[group_index], s) + end + + local group_indexes = vim.tbl_keys(source_group_map) + table.sort(group_indexes, function(a, b) + return a ~= b and (a < b) or nil + end) + + local entries = {} + for _, group_index in ipairs(group_indexes) do + local source_group = source_group_map[group_index] or {} + + -- check the source triggered by character + local has_triggered_by_symbol_source = false + for _, s in ipairs(source_group) do + if #s:get_entries(ctx) > 0 then + if s.is_triggered_by_symbol then + has_triggered_by_symbol_source = true + break + end + end + end + + -- create filtered entries. + local offset = ctx.cursor.col + for i, s in ipairs(source_group) do + if s.offset <= offset then + if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then + -- source order priority bonus. + local priority = s:get_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) + + for _, e in ipairs(s:get_entries(ctx)) do + e.score = e.score + priority + table.insert(entries, e) + offset = math.min(offset, e:get_offset()) + end + end + end + end + + -- sort. + local comparetors = config.get().sorting.comparators + table.sort(entries, function(e1, e2) + for _, fn in ipairs(comparetors) do + local diff = fn(e1, e2) + if diff ~= nil then + return diff + end + end + end) + + -- open + if #entries > 0 then + self:_get_entries_view():open(offset, entries) + break + end + end + + -- close. + if #entries == 0 then + self:close() + end +end + +---Close menu +view.close = function(self) + self:_get_entries_view():close() + self.docs_view:close() + self.ghost_text_view:hide() +end + +---Abort menu +view.abort = function(self) + self:_get_entries_view():abort() + self.docs_view:close() + self.ghost_text_view:hide() +end + +---Return the view is visible or not. +---@return boolean +view.visible = function(self) + return self:_get_entries_view():visible() +end + +---Scroll documentation window if possible. +---@param delta number +view.scroll_docs = function(self, delta) + self.docs_view:scroll(delta) +end + +---Select prev menu item. +---@param option cmp.SelectOption +view.select_next_item = function(self, option) + self:_get_entries_view():select_next_item(option) +end + +---Select prev menu item. +---@param option cmp.SelectOption +view.select_prev_item = function(self, option) + self:_get_entries_view():select_prev_item(option) +end + +---Get first entry +---@param self cmp.Entry|nil +view.get_first_entry = function(self) + return self:_get_entries_view():get_first_entry() +end + +---Get current selected entry +---@return cmp.Entry|nil +view.get_selected_entry = function(self) + return self:_get_entries_view():get_selected_entry() +end + +---Get current active entry +---@return cmp.Entry|nil +view.get_active_entry = function(self) + return self:_get_entries_view():get_active_entry() +end + +---Return current configured entries_view +---@return cmp.CustomEntriesView|cmp.NativeEntriesView +view._get_entries_view = function(self) + local c = config.get() + self.native_entries_view.event:clear() + self.custom_entries_view.event:clear() + + if c.experimental.native_menu then + self.native_entries_view.event:on('change', function() + self:on_entry_change() + end) + return self.native_entries_view + else + self.custom_entries_view.event:on('change', function() + self:on_entry_change() + end) + return self.custom_entries_view + end +end + +---On entry change +view.on_entry_change = async.throttle( + vim.schedule_wrap(function(self) + if not self:visible() then + return + end + local e = self:get_selected_entry() + if e then + for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do + keymap.listen('i', c, function(...) + self.event:emit('keymap', ...) + end) + end + e:resolve(vim.schedule_wrap(self.resolve_dedup(function() + if not self:visible() then + return + end + self.docs_view:open(e, self:_get_entries_view():info()) + end))) + else + self.docs_view:close() + end + + e = e or self:get_first_entry() + if e then + self.ghost_text_view:show(e) + else + self.ghost_text_view:hide() + end + end), + 20 +) + +return view diff --git a/bundle/nvim-cmp/lua/cmp/vim_source.lua b/bundle/nvim-cmp/lua/cmp/vim_source.lua new file mode 100644 index 000000000..2ee8fbf37 --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/vim_source.lua @@ -0,0 +1,53 @@ +local misc = require('cmp.utils.misc') + +local vim_source = {} + +---@param id number +---@param args any[] +vim_source.on_callback = function(id, args) + if vim_source.to_callback.callbacks[id] then + vim_source.to_callback.callbacks[id](unpack(args)) + end +end + +---@param callback function +---@return number +vim_source.to_callback = setmetatable({ + callbacks = {}, +}, { + __call = function(self, callback) + local id = misc.id('cmp.vim_source.to_callback') + self.callbacks[id] = function(...) + callback(...) + self.callbacks[id] = nil + end + return id + end, +}) + +---Convert to serializable args. +---@param args any[] +vim_source.to_args = function(args) + for i, arg in ipairs(args) do + if type(arg) == 'function' then + args[i] = vim_source.to_callback(arg) + end + end + return args +end + +---@param bridge_id number +---@param methods string[] +vim_source.new = function(bridge_id, methods) + local self = {} + for _, method in ipairs(methods) do + self[method] = (function(m) + return function(_, ...) + return vim.fn['cmp#_method'](bridge_id, m, vim_source.to_args({ ... })) + end + end)(method) + end + return self +end + +return vim_source diff --git a/bundle/nvim-cmp/plugin/cmp.lua b/bundle/nvim-cmp/plugin/cmp.lua new file mode 100644 index 000000000..e0db0cc17 --- /dev/null +++ b/bundle/nvim-cmp/plugin/cmp.lua @@ -0,0 +1,127 @@ +if vim.g.loaded_cmp then + return +end +vim.g.loaded_cmp = true + +local api = require "cmp.utils.api" +local misc = require('cmp.utils.misc') +local config = require('cmp.config') +local highlight = require('cmp.utils.highlight') + +-- TODO: https://github.com/neovim/neovim/pull/14661 +vim.cmd [[ + augroup ___cmp___ + autocmd! + autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter') + autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave') + autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged') + autocmd CursorMovedI * lua require'cmp.utils.autocmd'.emit('CursorMoved') + autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged') + autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') + autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme() + autocmd CmdlineEnter * call v:lua.cmp.plugin.cmdline.enter() + augroup END +]] + +misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function() + if config.get().experimental.native_menu then + return + end + if vim.fn.expand('')~= '=' then + vim.schedule(function() + if api.is_cmdline_mode() then + vim.cmd [[ + augroup cmp-cmdline + autocmd! + autocmd CmdlineChanged * lua require'cmp.utils.autocmd'.emit('TextChanged') + autocmd CmdlineLeave * call v:lua.cmp.plugin.cmdline.leave() + augroup END + ]] + require('cmp.utils.autocmd').emit('CmdlineEnter') + end + end) + end +end) + +misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'leave' }, function() + if vim.fn.expand('') ~= '=' then + vim.cmd [[ + augroup cmp-cmdline + autocmd! + augroup END + ]] + require('cmp.utils.autocmd').emit('CmdlineLeave') + end +end) + +misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function() + highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { + guibg = 'NONE', + ctermbg = 'NONE', + }) + highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { + gui = 'NONE', + guibg = 'NONE', + ctermbg = 'NONE', + }) + highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { + gui = 'NONE', + guibg = 'NONE', + ctermbg = 'NONE', + }) + highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { + gui = 'NONE', + guibg = 'NONE', + ctermbg = 'NONE', + }) + highlight.inherit('CmpItemKindDefault', 'Special', { + guibg = 'NONE', + ctermbg = 'NONE', + }) + highlight.inherit('CmpItemMenuDefault', 'Pmenu', { + guibg = 'NONE', + ctermbg = 'NONE', + }) +end) +_G.cmp.plugin.colorscheme() + +if vim.fn.hlexists('CmpItemAbbr') ~= 1 then + vim.cmd [[highlight! default link CmpItemAbbr CmpItemAbbrDefault]] +end + +if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then + vim.cmd [[highlight! default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] +end + +if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then + vim.cmd [[highlight! default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] +end + +if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then + vim.cmd [[highlight! default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] +end + +if vim.fn.hlexists('CmpItemKind') ~= 1 then + vim.cmd [[highlight! default link CmpItemKind CmpItemKindDefault]] +end + +if vim.fn.hlexists('CmpItemMenu') ~= 1 then + vim.cmd [[highlight! default link CmpItemMenu CmpItemMenuDefault]] +end + +vim.cmd [[command! CmpStatus lua require('cmp').status()]] + +vim.cmd [[doautocmd User cmp#ready]] + +if vim.on_key then + vim.on_key(function(keys) + if keys == vim.api.nvim_replace_termcodes('', true, true, true) then + vim.schedule(function() + if not api.is_suitable_mode() then + require('cmp.utils.autocmd').emit('InsertLeave') + end + end) + end + end, vim.api.nvim_create_namespace('cmp.plugin')) +end + diff --git a/bundle/nvim-cmp/stylua.toml b/bundle/nvim-cmp/stylua.toml new file mode 100644 index 000000000..e682e2562 --- /dev/null +++ b/bundle/nvim-cmp/stylua.toml @@ -0,0 +1,4 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 1200 +quote_style = "AutoPreferSingle" diff --git a/bundle/nvim-cmp/utils/install_stylua.sh b/bundle/nvim-cmp/utils/install_stylua.sh new file mode 100644 index 000000000..963416ea0 --- /dev/null +++ b/bundle/nvim-cmp/utils/install_stylua.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -eu pipefall + +declare -r INSTALL_DIR="$PWD/utils" +declare -r RELEASE="0.10.0" +declare -r OS="linux" +# declare -r OS="$(uname -s)" +declare -r FILENAME="stylua-$RELEASE-$OS" + +declare -a __deps=("curl" "unzip") + +function check_deps() { + for dep in "${__deps[@]}"; do + if ! command -v "$dep" >/dev/null; then + echo "Missing depdendecy!" + echo "The \"$dep\" command was not found!. Please install and try again." + fi + done +} + +function download_stylua() { + local DOWNLOAD_DIR + local URL="https://github.com/JohnnyMorganz/StyLua/releases/download/v$RELEASE/$FILENAME.zip" + + DOWNLOAD_DIR="$(mktemp -d)" + echo "Initiating download for Stylua v$RELEASE" + if ! curl --progress-bar --fail -L "$URL" -o "$DOWNLOAD_DIR/$FILENAME.zip"; then + echo "Download failed. Check that the release/filename are correct." + exit 1 + fi + + echo "Installation in progress.." + unzip -q "$DOWNLOAD_DIR/$FILENAME.zip" -d "$DOWNLOAD_DIR" + + if [ -f "$DOWNLOAD_DIR/stylua" ]; then + mv "$DOWNLOAD_DIR/stylua" "$INSTALL_DIR/stylua" + else + mv "$DOWNLOAD_DIR/$FILENAME/stylua" "$INSTALL_DIR/." + fi + + chmod u+x "$INSTALL_DIR/stylua" +} + +function verify_install() { + echo "Verifying installation.." + local DOWNLOADED_VER + DOWNLOADED_VER="$("$INSTALL_DIR/stylua" -V | awk '{ print $2 }')" + if [ "$DOWNLOADED_VER" != "$RELEASE" ]; then + echo "Mismatched version!" + echo "Expected: v$RELEASE but got v$DOWNLOADED_VER" + exit 1 + fi + echo "Verification complete!" +} + +function main() { + check_deps + download_stylua + verify_install +} + +main "$@" diff --git a/bundle/nvim-cmp/utils/vimrc.vim b/bundle/nvim-cmp/utils/vimrc.vim new file mode 100644 index 000000000..a83e71d31 --- /dev/null +++ b/bundle/nvim-cmp/utils/vimrc.vim @@ -0,0 +1,53 @@ +if has('vim_starting') + set encoding=utf-8 +endif +scriptencoding utf-8 + +if &compatible + set nocompatible +endif + +let s:plug_dir = expand('/tmp/plugged/vim-plug') +if !filereadable(s:plug_dir .. '/plug.vim') + execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir) +end + +execute 'set runtimepath+=' . s:plug_dir +call plug#begin(s:plug_dir) +Plug 'hrsh7th/nvim-cmp' +Plug 'hrsh7th/cmp-buffer' +Plug 'hrsh7th/cmp-nvim-lsp' +Plug 'hrsh7th/vim-vsnip' +Plug 'neovim/nvim-lspconfig' +call plug#end() +PlugInstall | quit + +" Setup global configuration. More on configuration below. +lua << EOF +local cmp = require "cmp" +cmp.setup { + snippet = { + expand = function(args) + vim.fn["vsnip#anonymous"](args.body) + end, + }, + + mapping = { + [''] = cmp.mapping.confirm({ select = true }) + }, + + sources = { + { name = "nvim_lsp" }, + { name = "buffer" }, + }, +} +EOF + +lua << EOF +local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()) + +require'lspconfig'.cssls.setup { + capabilities = capabilities, +} +EOF + diff --git a/config/plugins/nvim-cmp.vim b/config/plugins/nvim-cmp.vim new file mode 100644 index 000000000..63676e0b7 --- /dev/null +++ b/config/plugins/nvim-cmp.vim @@ -0,0 +1,38 @@ +lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }), + [''] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `` mapping. + [''] = cmp.mapping({ + i = cmp.mapping.abort(), + c = cmp.mapping.close(), + }), + [''] = cmp.mapping.confirm({ select = true }), + }, + formatting = { + format = require("lspkind").cmp_format({with_text = true, menu = ({ + buffer = "[Buffer]", + })}), + }, + sources = cmp.config.sources({ + { name = 'path' }, + }, { + { name = 'buffer' }, + }, { + { name = 'nvim_lsp' }, + }) + }) +-- The nvim-cmp almost supports LSP's capabilities so You should advertise it to LSP servers.. +local capabilities = vim.lsp.protocol.make_client_capabilities() +capabilities = require('cmp_nvim_lsp').update_capabilities(capabilities) + +-- The following example advertise capabilities to `clangd`. +require'lspconfig'.clangd.setup { + capabilities = capabilities, +} +EOF