diff --git a/bundle/README.md b/bundle/README.md index 59c1d6275..5c0d486fb 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -19,4 +19,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) +- [nvim-cmp](https://github.com/hrsh7th/nvim-cmp/tree/3192a0c57837c1ec5bf298e4f3ec984c7d2d60c0) diff --git a/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index aa60d97ac..000000000 --- a/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -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/ISSUE_TEMPLATE/bug_report.yml b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..2b24357df --- /dev/null +++ b/bundle/nvim-cmp/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,71 @@ +name: Bug Report +description: Report a problem in nvim-cmp +labels: [bug] +body: + - type: checkboxes + id: faq-prerequisite + attributes: + label: FAQ + options: + - label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/15f08a8faa22d52480cdcb9ef9ca698120f04363/doc/cmp.txt#L616) and it didn't resolve my problem. + required: true + + - type: checkboxes + id: issue-prerequisite + attributes: + label: Issues + options: + - label: I have checked [existing issues](https://github.com/hrsh7th/nvim-cmp/issues) and there are no open or closed issues with the same problem. + required: true + + - type: input + attributes: + label: "Neovim Version" + description: "`nvim --version`:" + validations: + required: true + + - type: textarea + attributes: + label: "Minimal reproducible full config" + description: | + You must provide a working config based on [this](https://github.com/hrsh7th/nvim-cmp/blob/main/utils/vimrc.vim). Not part of config. + 1. Copy the base minimal config to the `~/cmp-repro.vim` + 2. Edit `~/cmp-repro.vim` for reproducing the issue + 3. Open `nvim -u ~/cmp-repro.vim` + 4. Check reproduction step + validations: + required: true + + - type: textarea + attributes: + label: "Description" + description: "Describe in detail what happens" + validations: + required: true + + - type: textarea + attributes: + label: "Steps to reproduce" + description: "Full reproduction steps. Include a sample file if your issue relates to a specific filetype." + validations: + required: true + + - type: textarea + attributes: + label: "Expected behavior" + description: "A description of the behavior you expected." + validations: + required: true + + - type: textarea + attributes: + label: "Actual behavior" + description: "A description of the actual behavior." + validations: + required: true + + - type: textarea + attributes: + label: "Additional context" + description: "Any other relevant information" diff --git a/bundle/nvim-cmp/.github/workflows/integration.yaml b/bundle/nvim-cmp/.github/workflows/integration.yaml index 9dee7f2af..4bac4baca 100644 --- a/bundle/nvim-cmp/.github/workflows/integration.yaml +++ b/bundle/nvim-cmp/.github/workflows/integration.yaml @@ -26,6 +26,7 @@ jobs: - name: Setup neovim uses: rhysd/action-setup-vim@v1 with: + version: nightly neovim: true - name: Setup lua diff --git a/bundle/nvim-cmp/.gitignore b/bundle/nvim-cmp/.gitignore index 8ad54597a..883a84ae1 100644 --- a/bundle/nvim-cmp/.gitignore +++ b/bundle/nvim-cmp/.gitignore @@ -1 +1,3 @@ +doc/tags utils/stylua + diff --git a/bundle/nvim-cmp/README.md b/bundle/nvim-cmp/README.md index 30298442a..a096cc458 100644 --- a/bundle/nvim-cmp/README.md +++ b/bundle/nvim-cmp/README.md @@ -11,25 +11,18 @@ 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. +4. The nvim-cmp documents is [here](./doc/cmp.txt). + Concept ==================== +- Full support for LSP completion related capabilities +- Powerful customizability via Lua functions +- Smart handling of key mapping - 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 @@ -37,9 +30,9 @@ Setup ### Recommended Configuration -This example configuration uses `vim-plug` as the plugin manager. +This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as snippet plugin. -```viml +```lua call plug#begin(s:plug_dir) Plug 'neovim/nvim-lspconfig' Plug 'hrsh7th/cmp-nvim-lsp' @@ -78,12 +71,12 @@ lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), + [''] = 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. @@ -91,7 +84,7 @@ lua <'] = cmp.mapping.confirm({ select = true }), + [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. }, sources = cmp.config.sources({ { name = 'nvim_lsp' }, @@ -104,6 +97,15 @@ lua <) - -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: +nvim-cmp can be used as flexible omnifunc manager. ```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') +local cmp = require('cmp') cmp.setup { - formatting = { - format = lspkind.cmp_format(), + completion = { + autocomplete = false, -- disable auto-completion. }, } -``` -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 +_G.vimrc = _G.vimrc or {} +_G.vimrc.cmp = _G.vimrc.cmp or {} +_G.vimrc.cmp.lsp = function() + cmp.complete({ + config = { + sources = { + { name = 'nvim_lsp' } + } + } + }) 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' }, +_G.vimrc.cmp.snippet = function() + cmp.complete({ + config = { + sources = { + { name = 'vsnip' } + } + } }) 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()) +vim.cmd([[ + inoremap lua vimrc.cmp.lsp() + inoremap lua vimrc.cmp.snippet() +]]) ``` -You can also create a source by Vim script like this (This is useful to support callback style plugins). +### Full managed completion behavior. -- If you want to return `boolean`, you must return `v:true`/`v:false` instead of `0`/`1`. +```lua +local cmp = require('cmp') -```vim -let s:source = {} +cmp.setup { + completion = { + autocomplete = false, -- disable auto-completion. + } +} -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()) +_G.vimrc = _G.vimrc or {} +_G.vimrc.cmp = _G.vimrc.cmp or {} +_G.vimrc.cmp.on_text_changed = function() + local cursor = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_get_current_line() + local before = string.sub(line, 1, cursor[2] + 1) + if before:match('%s*$') then + cmp.complete() -- Trigger completion only if the cursor is placed at the end of line. + end +end +vim.cmd([[ + augroup vimrc + autocmd + autocmd TextChanged,TextChangedI,TextChangedP * call luaeval('vimrc.cmp.on_text_changed()') + augroup END +]]) ``` + + + diff --git a/bundle/nvim-cmp/autoload/cmp.vim b/bundle/nvim-cmp/autoload/cmp.vim index 8fcdeeb4a..43b8cc282 100644 --- a/bundle/nvim-cmp/autoload/cmp.vim +++ b/bundle/nvim-cmp/autoload/cmp.vim @@ -1,18 +1,6 @@ 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 " diff --git a/bundle/nvim-cmp/autoload/vital/_cmp.vim b/bundle/nvim-cmp/autoload/vital/_cmp.vim deleted file mode 100644 index 55104952e..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp.vim +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index f53c76a42..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Position.vim +++ /dev/null @@ -1,62 +0,0 @@ -" ___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 deleted file mode 100644 index 0c093149a..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/Text.vim +++ /dev/null @@ -1,23 +0,0 @@ -" ___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 deleted file mode 100644 index 09a8df81a..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp/VS/LSP/TextEdit.vim +++ /dev/null @@ -1,185 +0,0 @@ -" ___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 deleted file mode 100644 index df58dcd8d..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Buffer.vim +++ /dev/null @@ -1,126 +0,0 @@ -" ___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 deleted file mode 100644 index 043513382..000000000 --- a/bundle/nvim-cmp/autoload/vital/_cmp/VS/Vim/Option.vim +++ /dev/null @@ -1,21 +0,0 @@ -" ___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 deleted file mode 100644 index 6730f4e0a..000000000 --- a/bundle/nvim-cmp/autoload/vital/cmp.vim +++ /dev/null @@ -1,330 +0,0 @@ -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 deleted file mode 100644 index 1213d2a6e..000000000 --- a/bundle/nvim-cmp/autoload/vital/cmp.vital +++ /dev/null @@ -1,4 +0,0 @@ -cmp -2755f0c8fbd3442bcb7f567832e4d1455b57f9a2 - -VS.LSP.TextEdit diff --git a/bundle/nvim-cmp/doc/cmp.txt b/bundle/nvim-cmp/doc/cmp.txt new file mode 100644 index 000000000..bdb12d01d --- /dev/null +++ b/bundle/nvim-cmp/doc/cmp.txt @@ -0,0 +1,696 @@ +*nvim-cmp* *cmp* + +A completion plugin for neovim coded in Lua. + +============================================================================== +CONTENTS *cmp-contents* + +Abstract |cmp-abstract| +Concept |cmp-concept| +Usage |cmp-usage| +Function |cmp-function| +Mapping |cmp-mapping| +Command |cmp-command| +Highlight |cmp-highlight| +Autocmd |cmp-autocmd| +Config |cmp-config| +Develop |cmp-develop| +FAQ |cmp-faq| + + + +============================================================================== +Abstract *cmp-abstract* + +This is nvim-cmp's document. + +1. This docs uses the type definition notation like `{lsp,cmp,vim}.*` + - You can find it `../lua/cmp/types/init.lua`. +2. The advanced configuration is noted in wiki. + - https://github.com/hrsh7th/nvim-cmp/wiki + + + +============================================================================== +Concept *cmp-concept* + +- Full support for LSP completion related capabilities +- Powerful customizability via Lua functions +- Smart handling of key mapping +- No flicker + + + +============================================================================== +Usage *cmp-usage* + +The recommendation configurations are the below. + +NOTE: + 1. You must setup `snippet.expand` function. + 2. The `cmp.setup.cmdline` won't work if you are using `native` completion menu. + 3. You can disable the `default` options via specifying `cmp.config.disable` value. +> + 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 snippy users. + " Plug 'dcampos/nvim-snippy' + " Plug 'dcampos/cmp-snippy' + + " For ultisnips users. + " Plug 'SirVer/ultisnips' + " Plug 'quangnguyen30192/cmp-nvim-ultisnips' + + call plug#end() + + set completeopt=menu,menuone,noselect + + lua <'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), + [''] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }), + [''] = cmp.mapping({ + i = cmp.mapping.abort(), + c = cmp.mapping.close(), + }), + -- Accept currently selected item. If none selected, `select` first item. + -- Set `select` to `false` to only confirm explicitly selected items. + [''] = cmp.mapping.confirm({ select = true }), + }, + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'vsnip' }, -- For vsnip users. + -- { name = 'luasnip' }, -- For luasnip users. + -- { name = 'snippy' }, -- For snippy users. + -- { name = 'ultisnips' }, -- For ultisnips users. + }, { + { name = 'buffer' }, + }) + }) + + -- `/` cmdline setup. + cmp.setup.cmdline('/', { + sources = { + { name = 'buffer' } + } + }) + + -- `:` cmdline setup. + 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()) + require('lspconfig')[%YOUR_LSP_SERVER%].setup { + capabilities = capabilities + } + EOF +< + + +============================================================================== +Function *cmp-function* + +NOTE: You can call these functions in mapping via `lua require('cmp').complete()`. + +*cmp.setup* (config: cmp.ConfigSchema) + Setup global configuration. See configuration option. + +*cmp.setup.filetype* (filetype: string, config: cmp.ConfigSchema) + Setup filetype configuration to the specific filetype. + +*cmp.setup.buffer* (config: cmp.ConfigSchema) + Setup buffer configuration to the current buffer. + +*cmp.setup.cmdline* (cmdtype: string, config: cmp.ConfigSchema) + Setup cmdline configuration to the specific cmdtype. + See |getcmdtype()| + NOTE: nvim-cmp does not support the `=` cmdtype. + +*cmp.visible* () + Return the completion menu is visible or not. + +*cmp.get_entries* () + Return current all entries. + +*cmp.get_selected_entry* () + Return current selected entry. (contains preselected) + +*cmp.get_active_entry* () + Return current selected entry. (without preselected) + +*cmp.close* () + Just close the completion menu. + +*cmp.abort* () + Closes the completion menu and restore the current line to the state when it was started current completion. + +*cmp.select_next_item* (option: { behavior = cmp.SelectBehavior }) + Select next item. + +*cmp.select_prev_item* (option: { behavior = cmp.SelectBehavior })* + Select prev item. + +*cmp.scroll_docs* (delta: number) + Scroll docs if it visible. + +*cmp.complete* (option: { reason = cmp.ContextReason, config = cmp.ConfigSchema }) + Invoke completion. + + The following configurations defines the key mapping to invoke only snippet completion. +> + cmp.setup { + mapping = { + [''] = cmp.mapping.complete({ + config = { + sources = { + { name = 'vsnip' } + } + } + }) + } + } +< > + inoremap lua require('cmp').complete({ config = { sources = { { name = 'vsnip' } } } }) +< + NOTE: The `config` means a temporary setting, but the `config.mapping` remains permanent. + +*cmp.complete_common_string* () + Complete common string as like as shell completion behavior. +> + cmp.setup { + mapping = { + [''] = cmp.mapping(function(fallback) + if cmp.visible() then + return cmp.complete_common_string() + end + fallback() + end, { 'i', 'c' }), + } + } +< +*cmp.confirm* (option: cmp.ConfirmOption, callback: function) + Accept current selected completion item. + If you didn't select any items and specified the `{ select = true }` for + this, nvim-cmp will automatically select the first item. + +*cmp.event:on* ('%EVENT_NAME%, callback) + Subscribe nvim-cmp's events below. + + - `complete_done`: emit after current completion is done. + - `confirm_done`: emit after confirmation is done. + + + +============================================================================== +Mapping *cmp-mapping* + +The nvim-cmp's mapping mechanism is complex but flexible and user-friendly. + +You can specify the mapping as function that receives the `fallback` function as arguments. +The `fallback` function can be used to call an existing mapping. + +For example, typical pair-wise plugins automatically defines a mapping for `` or `(`. +The nvim-cmp will overwrite it but you can fallback to the original mapping via invoking the `fallback` function. +> + cmp.setup { + mapping = { + [''] = function(fallback) + if cmp.visible() then + cmp.confirm() + else + fallback() -- If you are using vim-endwise, this fallback function will be behaive as the vim-endwise. + end + end + } + } +< +And you can specify the mapping modes. +> + cmp.setup { + mapping = { + [''] = cmp.mapping(your_mapping_function, { 'i', 'c' }) + } + } +< +And you can specify the different mapping function for each modes. +> + cmp.setup { + mapping = { + [''] = cmp.mapping({ + i = your_mapping_function_a, + c = your_mapping_function_b, + }) + } + } +< +You can also use built-in mapping helpers. + + *cmp.mapping.close* () + Same as |cmp.close| + + *cmp.mapping.abort* () + Same as |cmp.abort| + + *cmp.mapping.select_next_item* (option: { behavior = cmp.SelectBehavior }) + Same as |cmp.select_next_item| + + *cmp.mapping.select_prev_item* (option: { behavior = cmp.SelectBehavior }) + Same as |cmp.select_prev_item| + + *cmp.mapping.scroll_docs* (delta: number) + Same as |cmp.scroll_docs| + + *cmp.mapping.complete* (option: cmp.CompleteParams) + Same as |cmp.complete| + + *cmp.mapping.complete_common_string* () + Same as |cmp.complete_common_string| + + *cmp.mapping.confirm* (option: cmp.ConfirmOption) + Same as |cmp.confirm| + +The built-in mapping helper is only available as a configuration option. +If you want to call the nvim-cmp features directly, please use |cmp-function| instead. + + + +============================================================================== +Command *cmp-command* + +*CmpStatus* + Prints source statuses for the current buffer and states. + Sometimes `unknown` source will be printed but it isn't problem. (e.g. `cmp-nvim-lsp`) + That the reason is the `cmp-nvim-lsp` will registered on the InsertEnter autocmd. + + + +============================================================================== +Highlight *cmp-highlight* + +*CmpItemAbbr* + The abbr field's highlight group. + +*CmpItemAbbrDeprecated* + The abbr field's highlight group that only used for deprecated item. + +*CmpItemAbbrMatch* + The matched character's highlight group. + +*CmpItemAbbrMatchFuzzy* + The fuzzy matched character's highlight group. + +*CmpItemKind* + The kind field's highlight group. + +*CmpItemKind%KIND_NAME%* + The kind field's highlight group for specific `lsp.CompletionItemKind`. + If you want to overwrite only the method kind's highlight group, you can do this. +> + highlight CmpItemKindMethod guibg=NONE guifg=Orange +< +*CmpItemMenu* + The menu field's highlight group. + + + +============================================================================== +Autocmd *cmp-autocmd* + +You can create custom autocommands for certain nvim-cmp events by defining +autocommands for the User event with the following patterns. + +*CmpReady* + Invoked when nvim-cmp gets sourced from `plugin/cmp.lua`. + + + +============================================================================== +Config *cmp-config* + +You can specify the following configuration option via `cmp.setup { ... }` call. + + *cmp-config.enabled* +enabled~ + `boolean | fun(): boolean` + You can control nvim-cmp should work or not via this option. + + *cmp-config.preselect* +preselect~ + `cmp.PreselectMode` + + 1. `cmp.PreselectMode.Item` + nvim-cmp will pre-select the item that the source specified. + 2. `cmp.PreselectMode.None` + nvim-cmp wouldn't pre-select any item. + + *cmp-config.mapping* +mapping~ + `table + final_score = orig_score + ((#sources - (source_index - 1)) * sorting.priority_weight) +< + *cmp-config.sorting.comparators* +sorting.comparators~ + `(fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil)[]` + The function to customize the sorting behavior. + You can use built-in comparators via `cmp.config.compare.*`. + + *cmp-config.sources* +sources~ + `cmp.SourceConfig[]` + Array of the source configuration to use. + The order will be used to the completion menu's sort order. + + *cmp-config.sources[n].name* +sources[n].name~ + `string` + The source name. + + *cmp-config.sources[n].option* +sources[n].option~ + `table` + The source specific custom option that defined by the source. + + *cmp-config.sources[n].keyword_length* +sources[n].keyword_length~ + `number` + The source specific keyword length to trigger auto completion. + + *cmp-config.sources[n].keyword_pattern* +sources[n].keyword_pattern~ + `number` + The source specific keyword pattern. + + *cmp-config.sources[n].trigger_characters* +sources[n].trigger_characters~ + `string[]` + The source specific keyword pattern. + + *cmp-config.sources[n].priority* +sources[n].priority~ + `number` + The source specific priority value. + + *cmp-config.sources[n].max_item_count* +sources[n].max_item_count~ + `number` + The source specific item count. + + *cmp-config.sources[n].group_index* +sources[n].group_index~ + `number` + The source group index. + + For example, You can specify the `buffer` source group index to bigger number + if you don't want to see the buffer source items when the nvim-lsp source is available. +> + cmp.setup { + sources = { + { name = 'nvim_lsp', group_index = 1 }, + { name = 'buffer', group_index = 2 }, + } + } +< + You can specify this via the built-in configuration helper like this. +> + cmp.setup { + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + }, { + { name = 'buffer' }, + }) + } +< + *cmp-config.view* +view~ + `{ entries: cmp.EntriesConfig|string }` + Specify the view class to customize appearance. + Currently, the possible configurations are: + + *cmp-config.experimental.ghost_text* +experimental.ghost_text~ + `boolean | { hl_group = string }` + The boolean value to enable or disable the ghost_text feature. + + + +============================================================================== +Develop *cmp-develop* + +Create custom source~ + +NOTE: + 1. The `complete` method is required. Others can be ommited. + 2. The `callback` argument must always be called. + 3. You can use only `require('cmp')` in custom source. + 4. If the LSP spec was changed, nvim-cmp will follow it without any announcement. + 5. You should read ./lua/cmp/types and https://microsoft.github.io/language-server-protocol/specifications/specification-current + 6. Please add the `nvim-cmp` topic for github repo. + +You can create custom source like the following example. + +> + local source = {} + + ---Return this source is available in current context or not. (Optional) + ---@return boolean + function source:is_available() + return true + end + + ---Return the debug name of this source. (Optional) + ---@return string + function source:get_debug_name() + return 'debug name' + end + + ---Return keyword pattern for triggering completion. (Optional) + ---If this is ommited, nvim-cmp will use default keyword pattern. See |cmp-config.completion.keyword_pattern| + ---@return string + function source:get_keyword_pattern() + return [[\k\+]] + end + + ---Return trigger characters for triggering completion. (Optional) + function source:get_trigger_characters() + return { '.' } + end + + ---Invoke completion. (Required) + ---@param params cmp.SourceCompletionApiParams + ---@param callback fun(response: lsp.CompletionResponse|nil) + function source:complete(params, callback) + callback({ + { label = 'January' }, + { label = 'February' }, + { label = 'March' }, + { label = 'April' }, + { label = 'May' }, + { label = 'June' }, + { label = 'July' }, + { label = 'August' }, + { label = 'September' }, + { label = 'October' }, + { label = 'November' }, + { label = 'December' }, + }) + end + + ---Resolve completion item. (Optional) + ---@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 after item was accepted. + ---@param completion_item lsp.CompletionItem + ---@param callback fun(completion_item: lsp.CompletionItem|nil) + function source:execute(completion_item, callback) + callback(completion_item) + end + + ---Register custom source to nvim-cmp. + require('cmp').register_source('month', source.new()) +< + + +============================================================================== +FAQ *cmp-faq* + +Why does cmp automatically select a particular item? ~ +How to disable the preselect feature? ~ + + The nvim-cmp respects LSP(Language Server Protocol) specification. + The LSP spec defines the `preselect` feature for completion. + + You can disable the `preselect` feature like the following. +> + cmp.setup { + preselect = cmp.PreselectMode.None + } +< + + +How to disable auto-completion?~ +How to use nvim-cmp as like omnifunc?~ + + You can disable auto-completion like this. +> + cmp.setup { + ... + completion = { + autocomplete = false + } + ... + } +< + And you can invoke completion manually. +> + inoremap lua require('cmp').complete() +< + +How to disable nvim-cmp on the specific buffer?~ +How to setup on the specific buffer?~ + + You can setup buffer specific configuration like this. +> + cmp.setup.filetype({ 'markdown', 'help' }, { + sources = { + { name = 'path' }, + { name = 'buffer' }, + } + }) +< + +How to integrate with copilot.vim?~ + + The copilot.vim and nvim-cmp both have a `key-mapping fallback` mechanism. + Therefore, You should manage those plugins by yourself. + + Fortunately, the copilot.vim has the feature that disables the fallback mechanism. +> + let g:copilot_no_tab_map = v:true + imap (vimrc:copilot-dummy-map) copilot#Accept("\") +< + You can manage copilot.vim's accept feature with nvim-cmp' key-mapping configuration. +> + cmp.setup { + mapping = { + [''] = cmp.mapping(function(fallback) + vim.api.nvim_feedkeys(vim.fn['copilot#Accept'](vim.api.nvim_replace_termcodes('', true, true, true)), 'n', true) + end) + }, + experimental = { + ghost_text = false -- this feature conflict to the copilot.vim's preview. + } + } +< + + +How to customize menu appearance?~ + + You can see the nvim-cmp wiki (https://github.com/hrsh7th/nvim-cmp/wiki). + + + +============================================================================== + vim:tw=78:ts=4:et:ft=help:norl: + diff --git a/bundle/nvim-cmp/lua/cmp/config.lua b/bundle/nvim-cmp/lua/cmp/config.lua index 42fdc4fb9..1772184b0 100644 --- a/bundle/nvim-cmp/lua/cmp/config.lua +++ b/bundle/nvim-cmp/lua/cmp/config.lua @@ -17,13 +17,19 @@ config.global = require('cmp.config.default')() ---@type table config.buffers = {} +---@type table +config.filetypes = {} + ---@type table config.cmdline = {} +---@type cmp.ConfigSchema +config.onetime = {} + ---Set configuration for global. ---@param c cmp.ConfigSchema config.set_global = function(c) - config.global = misc.merge(c, config.global) + config.global = misc.merge(config.normalize(c), config.normalize(config.global)) config.global.revision = config.global.revision or 1 config.global.revision = config.global.revision + 1 end @@ -33,31 +39,82 @@ end ---@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] = c or {} config.buffers[bufnr].revision = revision + 1 end +---Set configuration for filetype +---@param c cmp.ConfigSchema +---@param filetypes string[]|string +config.set_filetype = function(c, filetypes) + for _, filetype in ipairs(type(filetypes) == 'table' and filetypes or { filetypes }) do + local revision = (config.filetypes[filetype] or {}).revision or 1 + config.filetypes[filetype] = c or {} + config.filetypes[filetype].revision = revision + 1 + end +end + ---Set configuration for cmdline -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 +---@param c cmp.ConfigSchema +---@param cmdtype string +config.set_cmdline = function(c, cmdtype) + local revision = (config.cmdline[cmdtype] or {}).revision or 1 + config.cmdline[cmdtype] = c or {} + config.cmdline[cmdtype].revision = revision + 1 +end + +---Set configuration as oneshot completion. +---@param c cmp.ConfigSchema +config.set_onetime = function(c) + local revision = (config.onetime or {}).revision or 1 + config.onetime = c or {} + config.onetime.revision = revision + 1 end ---@return cmp.ConfigSchema config.get = function() - local global = config.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)) + local global_config = config.global + if config.onetime.sources then + local onetime_config = config.onetime + return config.cache:ensure({ + 'get', + 'onetime', + global_config.revision or 0, + onetime_config.revision or 0, + }, function() + return misc.merge(config.normalize(onetime_config), config.normalize(global_config)) + end) + elseif api.is_cmdline_mode() then + local cmdtype = vim.fn.getcmdtype() + local cmdline_config = config.cmdline[cmdtype] or { revision = 1, sources = {} } + return config.cache:ensure({ + 'get', + 'cmdline', + global_config.revision or 0, + cmdtype, + cmdline_config.revision or 0, + }, function() + return misc.merge(config.normalize(cmdline_config), config.normalize(global_config)) 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)) + local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') + local buffer_config = config.buffers[bufnr] or { revision = 1 } + local filetype_config = config.filetypes[filetype] or { revision = 1 } + return config.cache:ensure({ + 'get', + 'default', + global_config.revision or 0, + filetype, + filetype_config.revision or 0, + bufnr, + buffer_config.revision or 0, + }, function() + local c = {} + c = misc.merge(c, config.normalize(buffer_config)) + c = misc.merge(c, config.normalize(filetype_config)) + c = misc.merge(c, config.normalize(global_config)) + return c end) end end @@ -78,19 +135,31 @@ 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 +---Return the current menu is native or not. +config.is_native_menu = function() + local c = config.get() + if c.experimental and c.experimental.native_menu then + return true + end + if c.view and c.view.entries then + return c.view.entries == 'native' or c.view.entries.name == 'native' + end + return false +end + ---Normalize mapping key ---@param c cmp.ConfigSchema ---@return cmp.ConfigSchema config.normalize = function(c) + -- make sure c is not 'nil' + c = c == nil and {} or c + if c.mapping then local normalized = {} for k, v in pairs(c.mapping) do @@ -98,6 +167,39 @@ config.normalize = function(c) end c.mapping = normalized end + + if c.experimental and c.experimental.native_menu then + vim.api.nvim_echo({ + { '[nvim-cmp] ', 'Normal' }, + { 'experimental.native_menu', 'WarningMsg' }, + { ' is deprecated.\n', 'Normal' }, + { '[nvim-cmp] Please use ', 'Normal' }, + { 'view.entries = "native"', 'WarningMsg' }, + { ' instead.', 'Normal' }, + }, true, {}) + + c.view = c.view or {} + c.view.entries = c.view.entries or 'native' + end + + if c.sources then + for _, s in ipairs(c.sources) do + if s.opts and not s.option then + s.option = s.opts + s.opts = nil + vim.api.nvim_echo({ + { '[nvim-cmp] ', 'Normal' }, + { 'sources[number].opts', 'WarningMsg' }, + { ' is deprecated.\n', 'Normal' }, + { '[nvim-cmp] Please use ', 'Normal' }, + { 'sources[number].option', 'WarningMsg' }, + { ' instead.', 'Normal' }, + }, true, {}) + end + s.option = s.option or {} + end + end + return c end diff --git a/bundle/nvim-cmp/lua/cmp/config/compare.lua b/bundle/nvim-cmp/lua/cmp/config/compare.lua index d5657cbbb..91db9fc40 100644 --- a/bundle/nvim-cmp/lua/cmp/config/compare.lua +++ b/bundle/nvim-cmp/lua/cmp/config/compare.lua @@ -1,4 +1,5 @@ local types = require('cmp.types') +local cache = require('cmp.utils.cache') local misc = require('cmp.utils.misc') local compare = {} @@ -100,4 +101,135 @@ compare.order = function(entry1, entry2) end end +-- locality +compare.locality = setmetatable({ + lines_count = 10, + lines_cache = cache.new(), + locality_map = {}, + update = function(self) + local config = require('cmp').get_config() + if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then + return + end + + local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() + local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 + local max = vim.api.nvim_buf_line_count(buf) + + if self.lines_cache:get('buf') ~= buf then + self.lines_cache:clear() + self.lines_cache:set('buf', buf) + end + + self.locality_map = {} + for i = math.max(0, cursor_row - self.lines_count), math.min(max, cursor_row + self.lines_count) do + local is_above = i < cursor_row + local buffer = vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or '' + local locality_map = self.lines_cache:ensure({ 'line', buffer }, function() + local locality_map = {} + local regexp = vim.regex(config.completion.keyword_pattern) + while buffer ~= '' do + local s, e = regexp:match_str(buffer) + if s and e then + local w = string.sub(buffer, s + 1, e) + local d = math.abs(i - cursor_row) - (is_above and 0.1 or 0) + locality_map[w] = math.min(locality_map[w] or math.huge, d) + buffer = string.sub(buffer, e + 1) + else + break + end + end + return locality_map + end) + for w, d in pairs(locality_map) do + self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row)) + end + end + end +}, { + __call = function(self, entry1, entry2) + local local1 = self.locality_map[entry1:get_word()] + local local2 = self.locality_map[entry2:get_word()] + if local1 ~= local2 then + if local1 == nil then + return false + end + if local2 == nil then + return true + end + return local1 < local2 + end + end +}) + +-- scopes +compare.scopes = setmetatable({ + scopes_map = {}, + update = function(self) + local config = require('cmp').get_config() + if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then + return + end + + local ok, locals = pcall(require, 'nvim-treesitter.locals') + if ok then + local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() + local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1 + local ts_utils = require('nvim-treesitter.ts_utils') + + -- Cursor scope. + local cursor_scope = nil + for _, scope in ipairs(locals.get_scopes(buf)) do + if scope:start() <= cursor_row and cursor_row <= scope:end_() then + if not cursor_scope then + cursor_scope = scope + else + if cursor_scope:start() <= scope:start() and scope:end_() <= cursor_scope:end_() then + cursor_scope = scope + end + end + elseif cursor_scope and cursor_scope:end_() <= scope:start() then + break + end + end + + -- Definitions. + local definitions = locals.get_definitions_lookup_table(buf) + + -- Narrow definitions. + local depth = 0 + for scope in locals.iter_scope_tree(cursor_scope, buf) do + local s, e = scope:start(), scope:end_() + + -- Check scope's direct child. + for _, definition in pairs(definitions) do + if s <= definition.node:start() and definition.node:end_() <= e then + if scope:id() == locals.containing_scope(definition.node, buf):id() then + local text = ts_utils.get_node_text(definition.node)[1] + if not self.scopes_map[text] then + self.scopes_map[text] = depth + end + end + end + end + depth = depth + 1 + end + end + end, +}, { + __call = function(self, entry1, entry2) + local local1 = self.scopes_map[entry1:get_word()] + local local2 = self.scopes_map[entry2:get_word()] + if local1 ~= local2 then + if local1 == nil then + return false + end + if local2 == nil then + return true + end + return local1 < local2 + end + end, +}) + return compare diff --git a/bundle/nvim-cmp/lua/cmp/config/context.lua b/bundle/nvim-cmp/lua/cmp/config/context.lua new file mode 100644 index 000000000..584f38abe --- /dev/null +++ b/bundle/nvim-cmp/lua/cmp/config/context.lua @@ -0,0 +1,65 @@ +local context = {} + +---Check if cursor is in syntax group +---@param group string +---@return boolean +context.in_syntax_group = function(group) + local lnum, col = vim.fn.line('.'), math.min(vim.fn.col('.'), #vim.fn.getline('.')) + for _, syn_id in ipairs(vim.fn.synstack(lnum, col)) do + syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links + if vim.fn.synIDattr(syn_id, 'name') == group then + return true + end + end + return false +end + +---Check if cursor is in treesitter capture +---@param capture string +---@return boolean +context.in_treesitter_capture = function(capture) + local highlighter = require('vim.treesitter.highlighter') + local ts_utils = require('nvim-treesitter.ts_utils') + local buf = vim.api.nvim_get_current_buf() + + local row, col = unpack(vim.api.nvim_win_get_cursor(0)) + row = row - 1 + if vim.api.nvim_get_mode().mode == 'i' then + col = col - 1 + end + + local self = highlighter.active[buf] + if not self then + return false + end + + local node_types = {} + + self.tree:for_each_tree(function(tstree, tree) + if not tstree then + return + end + + local root = tstree:root() + local root_start_row, _, root_end_row, _ = root:range() + if root_start_row > row or root_end_row < row then + return + end + + local query = self:get_query(tree:lang()) + if not query:query() then + return + end + + local iter = query:query():iter_captures(root, self.bufnr, row, row + 1) + for _, node, _ in iter do + if ts_utils.is_in_node_range(node, row, col) then + table.insert(node_types, node:type()) + end + end + end, true) + + return vim.tbl_contains(node_types, capture) +end + +return context diff --git a/bundle/nvim-cmp/lua/cmp/config/default.lua b/bundle/nvim-cmp/lua/cmp/config/default.lua index c0d5f5a4e..d596444d6 100644 --- a/bundle/nvim-cmp/lua/cmp/config/default.lua +++ b/bundle/nvim-cmp/lua/cmp/config/default.lua @@ -1,5 +1,6 @@ local compare = require('cmp.config.compare') local mapping = require('cmp.config.mapping') +local keymap = require('cmp.utils.keymap') local types = require('cmp.types') local WIDE_HEIGHT = 40 @@ -8,58 +9,15 @@ local WIDE_HEIGHT = 40 return function() return { enabled = function() - return vim.api.nvim_buf_get_option(0, 'buftype') ~= 'prompt' + local disabled = false + disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt') + disabled = disabled or (vim.fn.reg_recording() ~= '') + disabled = disabled or (vim.fn.reg_executing() ~= '') + return not disabled 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 }), @@ -80,30 +38,38 @@ return function() end, }), [''] = mapping({ - c = function(fallback) + c = function() local cmp = require('cmp') - if #cmp.core:get_sources() > 0 and not cmp.get_config().experimental.native_menu then + if #cmp.core:get_sources() > 0 and not require('cmp.config').is_native_menu() then if cmp.visible() then cmp.select_next_item() else cmp.complete() end else - fallback() + if vim.fn.pumvisible() == 0 then + vim.api.nvim_feedkeys(keymap.t(''), 'in', true) + else + vim.api.nvim_feedkeys(keymap.t(''), 'in', true) + end end end, }), [''] = mapping({ - c = function(fallback) + c = function() local cmp = require('cmp') - if #cmp.core:get_sources() > 0 and not cmp.get_config().experimental.native_menu then + if #cmp.core:get_sources() > 0 and not require('cmp.config').is_native_menu() then if cmp.visible() then cmp.select_prev_item() else cmp.complete() end else - fallback() + if vim.fn.pumvisible() == 0 then + vim.api.nvim_feedkeys(keymap.t(''), 'in', true) + else + vim.api.nvim_feedkeys(keymap.t(''), 'in', true) + end end end, }), @@ -113,6 +79,21 @@ return function() [''] = mapping.abort(), }, + snippet = { + expand = function() + error('snippet engine is not configured.') + end, + }, + + completion = { + keyword_length = 1, + keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]], + autocomplete = { + types.cmp.TriggerEvent.TextChanged, + }, + completeopt = 'menu,menuone,noselect', + }, + formatting = { fields = { 'abbr', 'kind', 'menu' }, format = function(_, vim_item) @@ -120,11 +101,52 @@ return function() end, }, - experimental = { - native_menu = false, - ghost_text = false, + matching = { + disallow_fuzzy_matching = false, + disallow_partial_matching = false, + disallow_prefix_unmatching = false, + }, + + sorting = { + priority_weight = 2, + comparators = { + compare.offset, + compare.exact, + -- compare.scopes, + compare.score, + compare.recently_used, + compare.locality, + compare.kind, + compare.sort_text, + compare.length, + compare.order, + }, }, sources = {}, + + 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, + }, + + event = {}, + + experimental = { + ghost_text = false, + }, + + view = { + entries = { name = 'custom', selection_order = 'top_down' }, + }, } end diff --git a/bundle/nvim-cmp/lua/cmp/config/mapping.lua b/bundle/nvim-cmp/lua/cmp/config/mapping.lua index a4754f15f..0c2e81ae6 100644 --- a/bundle/nvim-cmp/lua/cmp/config/mapping.lua +++ b/bundle/nvim-cmp/lua/cmp/config/mapping.lua @@ -13,9 +13,19 @@ mapping = setmetatable({}, { }) ---Invoke completion -mapping.complete = function() +---@param option cmp.CompleteParams +mapping.complete = function(option) return function(fallback) - if not require('cmp').complete() then + if not require('cmp').complete(option) then + fallback() + end + end +end + +---Complete common string. +mapping.complete_common_string = function() + return function(fallback) + if not require('cmp').complete_common_string() then fallback() end end diff --git a/bundle/nvim-cmp/lua/cmp/core.lua b/bundle/nvim-cmp/lua/cmp/core.lua index 6ee30c2c7..169319dd9 100644 --- a/bundle/nvim-cmp/lua/cmp/core.lua +++ b/bundle/nvim-cmp/lua/cmp/core.lua @@ -1,4 +1,5 @@ local debug = require('cmp.utils.debug') +local str = require('cmp.utils.str') local char = require('cmp.utils.char') local pattern = require('cmp.utils.pattern') local feedkeys = require('cmp.utils.feedkeys') @@ -14,14 +15,13 @@ local api = require('cmp.utils.api') local event = require('cmp.utils.event') local SOURCE_TIMEOUT = 500 -local THROTTLE_TIME = 120 -local DEBOUNCE_TIME = 20 +local DEBOUNCE_TIME = 80 +local THROTTLE_TIME = 40 ---@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 = {} @@ -30,13 +30,15 @@ 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) + self.view.event:on('complete_done', function(evt) + self.event:emit('complete_done', evt) + end) return self end @@ -44,19 +46,11 @@ end ---@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 @@ -86,14 +80,23 @@ core.suspend = function(self) end ---Get sources that sorted by priority ----@param statuses cmp.SourceStatus[] +---@param filter cmp.SourceStatus[]|fun(s: cmp.Source): boolean ---@return cmp.Source[] -core.get_sources = function(self, statuses) +core.get_sources = function(self, filter) + local f = function(s) + if type(filter) == 'table' then + return vim.tbl_contains(filter, s.status) + elseif type(filter) == 'function' then + return filter(s) + end + return true + end + local sources = {} for _, c in pairs(config.get().sources) do - for _, s in 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 + for _, s in pairs(self.sources) do + if c.name == s.name then + if s:is_available() and f(s) then table.insert(sources, s) end end @@ -118,6 +121,7 @@ core.on_keymap = function(self, keys, fallback) local is_printable = char.is_printable(string.byte(chars, 1)) self:confirm(e, { behavior = is_printable and 'insert' or 'replace', + commit_character = chars, }, function() local ctx = self:get_context() local word = e:get_word() @@ -154,7 +158,6 @@ core.on_change = function(self, trigger_event) 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)) @@ -165,7 +168,7 @@ core.on_change = function(self, trigger_event) if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then self:complete(ctx) else - self.filter.timeout = THROTTLE_TIME + self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 self:filter() end else @@ -204,92 +207,140 @@ core.autoindent = function(self, trigger_event, callback) return callback() end - -- Scan indentkeys. + -- Reset current completion if indentkeys matched. for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then - 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 + self:reset() + self:set_context(context.empty()) + break end end - -- indentkeys does not matched. callback() end +---Complete common string for current completed entries. +core.complete_common_string = function(self) + if not self.view:visible() or self.view:get_active_entry() then + return false + end + + config.set_onetime({ + sources = config.get().sources, + matching = { + disallow_prefix_unmatching = true, + disallow_partial_matching = true, + disallow_fuzzy_matching = true, + }, + }) + + self:filter() + self.filter:sync(1000) + + config.set_onetime({}) + + local cursor = api.get_cursor() + local offset = self.view:get_offset() + local common_string + for _, e in ipairs(self.view:get_entries()) do + local vim_item = e:get_vim_item(offset) + if not common_string then + common_string = vim_item.word + else + common_string = str.get_common_string(common_string, vim_item.word) + end + end + if common_string and #common_string > (1 + cursor[2] - offset) then + feedkeys.call(keymap.backspace(string.sub(api.get_current_line(), offset, cursor[2])) .. common_string, 'n') + return true + end + return false +end + ---Invoke completion ---@param ctx cmp.Context core.complete = function(self, ctx) if not api.is_suitable_mode() then return end + self:set_context(ctx) - 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 + -- Invoke completion sources. + local sources = self:get_sources() + for _, s in ipairs(sources) do + local callback + callback = (function(s_) + return function() + local new = context.new(ctx) + if s_.incomplete and new:changed(s_.context) then + s_:complete(new, callback) + else + for _, s__ in ipairs(self:get_sources({ source.SourceStatus.FETCHING })) do + if s_ == s__ then + break + end + if not s__.incomplete and SOURCE_TIMEOUT > s__:get_fetching_time() then + return + end + end + if not self.view:get_active_entry() then self.filter.stop() - self.filter.timeout = DEBOUNCE_TIME + self.filter.timeout = self.view:visible() and DEBOUNCE_TIME or 0 self:filter() end end - return callback - end)(s) - ) + end + end)(s) + s:complete(ctx, callback) end - self.filter.timeout = THROTTLE_TIME - self:filter() + if not self.view:get_active_entry() then + self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 + self:filter() + end 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() +core.filter = async.throttle(function(self) + self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 - -- 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 + -- Check invalid condition. + local ignore = false + ignore = ignore or not api.is_suitable_mode() + if ignore then + return + end - self.view:open(ctx, sources) - end), - THROTTLE_TIME -) + -- Check fetching sources. + local sources = {} + for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do + if not s.incomplete and SOURCE_TIMEOUT > s:get_fetching_time() then + -- Reserve filter call for timeout. + self.filter.timeout = SOURCE_TIMEOUT - s:get_fetching_time() + self:filter() + break + end + table.insert(sources, s) + end + + local ctx = self:get_context() + + -- Display completion results. + self.view:open(ctx, sources) + + -- Check onetime config. + if #self:get_sources(function(s) + if s.status == source.SourceStatus.FETCHING then + return true + elseif #s:get_entries(ctx) > 0 then + return true + end + return false + end) == 0 then + config.set_onetime({}) + end +end, THROTTLE_TIME) ---Confirm completion. ---@param e cmp.Entry @@ -308,10 +359,11 @@ core.confirm = function(self, e, option, callback) -- Close menus. self.view:close() + feedkeys.call(keymap.indentkeys(), 'n') 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, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) table.insert(keys, e:get_word()) table.insert(keys, keymap.undobreak()) feedkeys.call(table.concat(keys, ''), 'int') @@ -320,9 +372,9 @@ core.confirm = function(self, e, option, callback) 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, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset()))) table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset())) - feedkeys.call(table.concat(keys, ''), 'int') + feedkeys.call(table.concat(keys, ''), 'in') 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()), @@ -331,8 +383,8 @@ core.confirm = function(self, e, option, callback) end end) feedkeys.call('', 'n', function() + local ctx = context.new() 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 {} @@ -341,8 +393,8 @@ core.confirm = function(self, e, option, callback) 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) + local minrow = math.min(ctx.cursor.row, new.cursor.row) + local maxrow = math.max(ctx.cursor.row, new.cursor.row) for _, te in ipairs(text_edits) do local srow = te.range.start.line + 1 local erow = te.range['end'].line + 1 @@ -355,10 +407,10 @@ core.confirm = function(self, e, option, callback) if has_cursor_line_text_edit then return end - vim.fn['cmp#apply_text_edits'](new.bufnr, text_edits) + vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16') end) else - vim.fn['cmp#apply_text_edits'](vim.api.nvim_get_current_buf(), e:get_completion_item().additionalTextEdits) + vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, 'utf-16') end end) feedkeys.call('', 'n', function() @@ -375,8 +427,8 @@ core.confirm = function(self, e, option, callback) 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 diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.character) + local diff_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character) local new_text = completion_item.textEdit.newText if api.is_insert_mode() then @@ -388,14 +440,14 @@ core.confirm = function(self, e, option, callback) if is_snippet then completion_item.textEdit.newText = '' end - vim.fn['cmp#apply_text_edits'](ctx.bufnr, { completion_item.textEdit }) + vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-16') 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]) + position.character = position.character + misc.to_utfindex(texts[1]) else - position.character = vim.str_utfindex(texts[#texts]) + position.character = misc.to_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 }) @@ -410,13 +462,17 @@ core.confirm = function(self, e, option, callback) 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') + feedkeys.call(table.concat(keys, ''), 'in') end end) + feedkeys.call(keymap.indentkeys(vim.bo.indentkeys), 'n') feedkeys.call('', 'n', function() e:execute(vim.schedule_wrap(function() release() - self.event:emit('confirm_done', e) + self.event:emit('confirm_done', { + entry = e, + commit_character = option.commit_character, + }) if callback then callback() end @@ -429,7 +485,7 @@ core.reset = function(self) for _, s in pairs(self.sources) do s:reset() end - self:get_context() -- To prevent new event + self.context = context.empty() end return core diff --git a/bundle/nvim-cmp/lua/cmp/entry.lua b/bundle/nvim-cmp/lua/cmp/entry.lua index 686c0de84..83c53a209 100644 --- a/bundle/nvim-cmp/lua/cmp/entry.lua +++ b/bundle/nvim-cmp/lua/cmp/entry.lua @@ -54,10 +54,10 @@ end ---Make offset value ---@return number entry.get_offset = function(self) - return self.cache:ensure('get_offset', function() + return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, 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 misc.safe(self:get_completion_item().textEdit) then + local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_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 @@ -98,28 +98,32 @@ entry.get_offset = function(self) end ---Create word for vim.CompletedItem +---NOTE: This method doesn't clear the cache after completionItem/resolve. ---@return string entry.get_word = function(self) - return self.cache:ensure('get_word', function() + 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 + if misc.safe(self:get_completion_item().word) then + return self:get_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)) + if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then + word = str.trim(self:get_completion_item().textEdit.newText) + if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = vim.lsp.util.parse_snippet(word) 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) + local overwrite = self:get_overwrite() + if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0) + end + elseif not misc.empty(self:get_completion_item().insertText) then + word = str.trim(self:get_completion_item().insertText) + if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then + word = str.get_word(vim.lsp.util.parse_snippet(word)) end else - word = str.trim(self.completion_item.label) + word = str.trim(self:get_completion_item().label) end return str.oneline(word) end) @@ -128,9 +132,9 @@ 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) + return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function() + if misc.safe(self:get_completion_item().textEdit) then + local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_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 @@ -144,27 +148,13 @@ end ---Create filter text ---@return string entry.get_filter_text = function(self) - return self.cache:ensure('get_filter_text', function() + return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function() local word - if misc.safe(self.completion_item.filterText) then - word = self.completion_item.filterText + if misc.safe(self:get_completion_item().filterText) then + word = self:get_completion_item().filterText else - word = str.trim(self.completion_item.label) + word = str.trim(self:get_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 @@ -172,20 +162,20 @@ end ---Get LSP's insert text ---@return string entry.get_insert_text = function(self) - return self.cache:ensure('get_insert_text', function() + return self.cache:ensure({ 'get_insert_text', self.resolved_completion_item and 1 or 0 }, 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 + if misc.safe(self:get_completion_item().textEdit) then + word = str.trim(self:get_completion_item().textEdit.newText) + if self:get_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 + elseif misc.safe(self:get_completion_item().insertText) then + word = str.trim(self:get_completion_item().insertText) + if self:get_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) + word = str.trim(self:get_completion_item().label) end return word end) @@ -194,31 +184,38 @@ 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) + return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated) end ---Return view information. +---@param suggest_offset number +---@param entries_buf number The buffer this entry will be rendered into. ---@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) +entry.get_view = function(self, suggest_offset, entries_buf) local item = self:get_vim_item(suggest_offset) - return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, 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 + -- The result of vim.fn.strdisplaywidth depends on which buffer it was + -- called in because it reads the values of the option 'tabstop' when + -- rendering characters. + vim.api.nvim_buf_call(entries_buf, function() + view.abbr = {} + view.abbr.text = item.abbr or '' + view.abbr.bytes = #view.abbr.text + view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text) + view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr') + view.kind = {} + view.kind.text = item.kind or '' + view.kind.bytes = #view.kind.text + view.kind.width = vim.fn.strdisplaywidth(view.kind.text) + view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or '')) + view.menu = {} + view.menu.text = item.menu or '' + view.menu.bytes = #view.menu.text + view.menu.width = vim.fn.strdisplaywidth(view.menu.text) + view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu' + view.dup = item.dup + end) return view end) end @@ -233,13 +230,16 @@ entry.get_vim_item = function(self, suggest_offset) local abbr = str.oneline(completion_item.label) -- ~ indicator + local is_snippet = false if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then - abbr = abbr .. '~' + is_snippet = true elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then - local insert_text = self:get_insert_text() - if word ~= insert_text then - abbr = abbr .. '~' - end + is_snippet = self:get_insert_text() ~= word + elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then + is_snippet = true + end + if is_snippet then + abbr = abbr .. '~' end -- append delta text @@ -260,10 +260,12 @@ entry.get_vim_item = function(self, suggest_offset) 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 + if self:get_offset() ~= self.context.cursor.col then + 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 end @@ -272,7 +274,7 @@ entry.get_vim_item = function(self, suggest_offset) abbr = abbr, kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], menu = menu, - dup = self.completion_item.dup or 1, + dup = self:get_completion_item().dup or 1, } if config.get().formatting.format then vim_item = config.get().formatting.format(self, vim_item) @@ -298,11 +300,11 @@ end ---@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 + if misc.safe(self:get_completion_item().textEdit) then + if misc.safe(self:get_completion_item().textEdit.insert) then + insert_range = self:get_completion_item().textEdit.insert else - insert_range = self.completion_item.textEdit.range + insert_range = self:get_completion_item().textEdit.range end else insert_range = { @@ -319,14 +321,10 @@ end ---Return replace range ---@return lsp.Range|nil entry.get_replace_range = function(self) - return self.cache:ensure('get_replace_range', function() + return self.cache:ensure({ 'get_replace_range', self.resolved_completion_item and 1 or 0 }, 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 + if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then + replace_range = self:get_completion_item().textEdit.replace else replace_range = { start = { @@ -342,14 +340,49 @@ end ---Match line. ---@param input string +---@param matching_config cmp.MatchingConfig ---@return { score: number, matches: table[] } -entry.match = function(self, input) - return self.match_cache:ensure(input, function() +entry.match = function(self, input, matching_config) + return self.match_cache:ensure({ + input, + self.resolved_completion_item and 1 or 0, + matching_config.disallow_fuzzy_matching and 1 or 0, + matching_config.disallow_partial_matching and 1 or 0, + matching_config.disallow_prefix_unmatching and 1 or 0, + }, function() + local option = { + disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching, + disallow_partial_matching = matching_config.disallow_partial_matching, + disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching, + synonyms = { + self:get_word(), + self:get_completion_item().label, + }, + } + local score, matches, _ - score, matches = matcher.match(input, self:get_filter_text(), { self:get_word(), self:get_completion_item().label }) + score, matches = matcher.match(input, self:get_filter_text(), option) + + -- Support the language server that doesn't respect VSCode's behaviors. + if score == 0 then + if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then + local diff = self.source_offset - self:get_offset() + if diff > 0 then + local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff) + local accept = false + accept = accept or string.match(prefix, '^[^%a]+$') + accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true) + if accept then + score, matches = matcher.match(input, prefix .. self:get_filter_text(), option) + end + end + end + end + 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 @@ -357,12 +390,12 @@ 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() + 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 + for k, v in pairs(self.resolved_completion_item) do + completion_item[k] = v or completion_item[k] + end return completion_item end return self.completion_item @@ -378,9 +411,14 @@ entry.get_documentation = function(self) -- detail if misc.safe(item.detail) and item.detail ~= '' then + local ft = self.context.filetype + local dot_index = string.find(ft, '%.') + if dot_index ~= nil then + ft = string.sub(ft, 0, dot_index - 1) + end table.insert(documents, { kind = types.lsp.MarkupKind.Markdown, - value = ('```%s\n%s\n```'):format(self.context.filetype, str.trim(item.detail)), + value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)), }) end @@ -399,7 +437,7 @@ 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 + return misc.safe(self:get_completion_item().kind) or types.lsp.CompletionItemKind.Text end ---Execute completion item's command. diff --git a/bundle/nvim-cmp/lua/cmp/entry_spec.lua b/bundle/nvim-cmp/lua/cmp/entry_spec.lua index 08223b4f7..d01125cac 100644 --- a/bundle/nvim-cmp/lua/cmp/entry_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/entry_spec.lua @@ -1,4 +1,6 @@ local spec = require('cmp.utils.spec') +local source = require('cmp.source') +local async = require('cmp.utils.async') local entry = require('cmp.entry') @@ -101,7 +103,7 @@ describe('entry', function() }, }) assert.are.equal(e:get_vim_item(4).word, '->foo') - assert.are.equal(e:get_filter_text(), '.foo') + assert.are.equal(e:get_filter_text(), 'foo') end) it('[typescript-language-server] 1', function() @@ -264,6 +266,65 @@ describe('entry', function() assert.are.equal(e:get_filter_text(), '$this') end) + it('[odin-language-server] 1', function() + local state = spec.state('\t\t', 1, 4) + + -- press g + state.input('s') + local e = entry.new(state.manual(), state.source(), { + additionalTextEdits = {}, + command = { + arguments = {}, + command = '', + title = '', + }, + deprecated = false, + detail = 'string', + documentation = '', + insertText = '', + insertTextFormat = 1, + kind = 14, + label = 'string', + tags = {}, + }) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string') + end) + + it('[ansiblels] 1', function() + local item = { + detail = 'ansible.builtin', + filterText = 'blockinfile ansible.builtin.blockinfile', + kind = 7, + label = 'blockinfile', + sortText = '2_blockinfile', + textEdit = { + newText = '', + range = { + ['end'] = { + character = 7, + line = 15, + }, + start = { + character = 6, + line = 15, + }, + }, + }, + } + local s = source.new('dummy', { + resolve = function(_, _, callback) + item.textEdit.newText = 'modified' + callback(item) + end, + }) + local e = entry.new(spec.state('', 1, 1).manual(), s, item) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile') + async.sync(function(done) + e:resolve(done) + end, 100) + assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile') + end) + it('[#47] word should not contain \\n character', function() local state = spec.state('', 1, 1) diff --git a/bundle/nvim-cmp/lua/cmp/init.lua b/bundle/nvim-cmp/lua/cmp/init.lua index b1ebf9d6f..dbcd8668b 100644 --- a/bundle/nvim-cmp/lua/cmp/init.lua +++ b/bundle/nvim-cmp/lua/cmp/init.lua @@ -17,17 +17,33 @@ end cmp.lsp = require('cmp.types.lsp') cmp.vim = require('cmp.types.vim') ----Export default config presets. +---Expose event +cmp.event = cmp.core.event + +---Export mapping for special case +cmp.mapping = require('cmp.config.mapping') + +---Export default config presets cmp.config = {} cmp.config.disable = misc.none cmp.config.compare = require('cmp.config.compare') cmp.config.sources = require('cmp.config.sources') +cmp.config.mapping = require('cmp.config.mapping') ----Expose event -cmp.event = cmp.core.event +---Sync asynchronous process. +cmp.sync = function(callback) + return function(...) + cmp.core.filter:sync(1000) + if callback then + return callback(...) + end + end +end ----Export mapping -cmp.mapping = require('cmp.config.mapping') +---Suspend completion. +cmp.suspend = function() + return cmp.core:suspend() +end ---Register completion sources ---@param name string @@ -52,28 +68,41 @@ cmp.get_config = function() end ---Invoke completion manually -cmp.complete = function() - cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.Manual })) +---@param option cmp.CompleteParams +cmp.complete = cmp.sync(function(option) + option = option or {} + config.set_onetime(option.config) + cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual })) return true -end +end) + +---Complete common string in current entries. +cmp.complete_common_string = cmp.sync(function() + return cmp.core:complete_common_string() +end) ---Return view is visible or not. -cmp.visible = function() +cmp.visible = cmp.sync(function() return cmp.core.view:visible() or vim.fn.pumvisible() == 1 -end +end) ---Get current selected entry or nil -cmp.get_selected_entry = function() +cmp.get_selected_entry = cmp.sync(function() return cmp.core.view:get_selected_entry() -end +end) ---Get current active entry or nil -cmp.get_active_entry = function() +cmp.get_active_entry = cmp.sync(function() return cmp.core.view:get_active_entry() -end +end) + +---Get current all entries +cmp.get_entries = cmp.sync(function() + return cmp.core.view:get_entries() +end) ---Close current completion -cmp.close = function() +cmp.close = cmp.sync(function() if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:close() @@ -83,10 +112,10 @@ cmp.close = function() else return false end -end +end) ---Abort current completion -cmp.abort = function() +cmp.abort = cmp.sync(function() if cmp.core.view:visible() then local release = cmp.core:suspend() cmp.core.view:abort() @@ -95,83 +124,65 @@ cmp.abort = function() else return false end -end - ----Suspend completion. -cmp.suspend = function() - return cmp.core:suspend() -end +end) ---Select next item if possible -cmp.select_next_item = function(option) +cmp.select_next_item = cmp.sync(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 + -- Special handling for native pum. Required to facilitate key mapping processing. if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'in') else - feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'in') end return true end return false -end +end) ---Select prev item if possible -cmp.select_prev_item = function(option) +cmp.select_prev_item = cmp.sync(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 + -- Special handling for native pum. Required to facilitate key mapping processing. if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then - feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'in') else - feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'in') end return true end return false -end +end) ---Scrolling documentation window if possible -cmp.scroll_docs = function(delta) +cmp.scroll_docs = cmp.sync(function(delta) if cmp.core.view:visible() then cmp.core.view:scroll_docs(delta) return true else return false end -end +end) ---Confirm completion -cmp.confirm = function(option, callback) +cmp.confirm = cmp.sync(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, { @@ -182,13 +193,14 @@ cmp.confirm = function(option, callback) end) return true else + -- Special handling for native puma. Required to facilitate key mapping processing. if vim.fn.complete_info({ 'selected' }).selected ~= -1 then - feedkeys.call(keymap.t(''), 'n') + feedkeys.call(keymap.t(''), 'in') return true end return false end -end +end) ---Show status cmp.status = function() @@ -255,6 +267,9 @@ cmp.setup = setmetatable({ global = function(c) config.set_global(c) end, + filetype = function(filetype, c) + config.set_filetype(c, filetype) + end, buffer = function(c) config.set_buffer(c, vim.api.nvim_get_current_buf()) end, @@ -302,11 +317,29 @@ end) autocmd.subscribe('CursorMoved', function() if config.enabled() then cmp.core:on_moved() + else + cmp.core:reset() + cmp.core.view:close() end end) -cmp.event:on('confirm_done', function(e) - cmp.config.compare.recently_used:add_entry(e) +autocmd.subscribe('InsertEnter', function() + cmp.config.compare.scopes:update() + cmp.config.compare.locality:update() +end) + +cmp.event:on('complete_done', function(evt) + if evt.entry then + cmp.config.compare.recently_used:add_entry(evt.entry) + end + cmp.config.compare.scopes:update() + cmp.config.compare.locality:update() +end) + +cmp.event:on('confirm_done', function(evt) + if evt.entry then + cmp.config.compare.recently_used:add_entry(evt.entry) + end end) return cmp diff --git a/bundle/nvim-cmp/lua/cmp/matcher.lua b/bundle/nvim-cmp/lua/cmp/matcher.lua index a68d6658f..7649f047b 100644 --- a/bundle/nvim-cmp/lua/cmp/matcher.lua +++ b/bundle/nvim-cmp/lua/cmp/matcher.lua @@ -72,9 +72,11 @@ end ---Match entry ---@param input string ---@param word string ----@param words string[] +---@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean } ---@return number -matcher.match = function(input, word, words) +matcher.match = function(input, word, option) + option = option or {} + -- Empty input if #input == 0 then return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {} @@ -85,7 +87,14 @@ matcher.match = function(input, word, words) return 0, {} end - --- Gather matched regions + -- Check prefix matching. + if option.disallow_prefix_unmatching then + if not char.match(string.byte(input, 1), string.byte(word, 1)) then + return 0, {} + end + end + + -- Gather matched regions local matches = {} local input_start_index = 1 local input_end_index = 1 @@ -105,6 +114,11 @@ matcher.match = function(input, word, words) word_bound_index = word_bound_index + 1 end + -- Check partial matching. + if option.disallow_partial_matching and #matches > 1 then + return 0, {} + end + if #matches == 0 then return 0, {} end @@ -116,11 +130,11 @@ matcher.match = function(input, word, words) 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 + for _, synonym in ipairs(option.synonyms or {}) do prefix = true local o = 1 for i = matches[1].input_match_start, matches[1].input_match_end do - if not char.match(string.byte(w, o), string.byte(input, i)) then + if not char.match(string.byte(synonym, o), string.byte(input, i)) then prefix = false break end @@ -152,8 +166,10 @@ matcher.match = function(input, word, words) -- 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 + if not option.disallow_fuzzy_matching then + if prefix and matcher.fuzzy(input, word, matches) then + return score, matches + end end return 0, {} end diff --git a/bundle/nvim-cmp/lua/cmp/matcher_spec.lua b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua index 1f8b0d1c1..768988e84 100644 --- a/bundle/nvim-cmp/lua/cmp/matcher_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/matcher_spec.lua @@ -16,20 +16,37 @@ describe('matcher', function() 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('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext')) 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('var_', 'var_dump') >= 1) + assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode')) 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('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) assert.is.truthy(matcher.match('2', '[[2021') >= 1) + + assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }) == matcher.match('true', 'true')) + assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }) > matcher.match('g', 'dein#get', { 'dein#get' })) + end) + + it('disallow_fuzzy_matching', function() + assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0) + assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) + end) + + it('disallow_partial_matching', function() + assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0) + assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) + assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1) + assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1) + end) + + it('disallow_prefix_unmatching', function() + assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0) + assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1) end) it('debug', function() diff --git a/bundle/nvim-cmp/lua/cmp/source.lua b/bundle/nvim-cmp/lua/cmp/source.lua index 077bb48bc..b73d43e05 100644 --- a/bundle/nvim-cmp/lua/cmp/source.lua +++ b/bundle/nvim-cmp/lua/cmp/source.lua @@ -15,12 +15,13 @@ local char = require('cmp.utils.char') ---@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 context cmp.Context +---@field public completion_context lsp.CompletionContext|nil ---@field public status cmp.SourceStatus ---@field public complete_dedup function local source = {} @@ -50,21 +51,28 @@ 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.request_offset = -1 + self.completion_context = nil self.status = source.SourceStatus.WAITING self.complete_dedup(function() end) end ----Return source option +---Return source config ---@return cmp.SourceConfig -source.get_config = function(self) +source.get_source_config = function(self) return config.get_source_config(self.name) or {} end +---Return matching config +---@return cmp.MatchingConfig +source.get_matching_config = function() + return config.get().matching +end + ---Get fetching time source.get_fetching_time = function(self) if self.status == source.SourceStatus.FETCHING then @@ -101,7 +109,7 @@ source.get_entries = function(self, ctx) inputs[o] = string.sub(ctx.cursor_before_line, o) end - local match = e:match(inputs[o]) + local match = e:match(inputs[o], self:get_matching_config()) e.score = match.score e.exact = false if e.score >= 1 then @@ -112,7 +120,7 @@ source.get_entries = function(self, ctx) 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 max_item_count = self:get_source_config().max_item_count or 200 local limited_entries = {} for _, e in ipairs(entries) do table.insert(limited_entries, e) @@ -183,17 +191,33 @@ source.is_available = function(self) return true end +---Get trigger_characters +---@return string[] +source.get_trigger_characters = function(self) + local c = self:get_source_config() + if c.trigger_characters then + return c.trigger_characters + end + + local trigger_characters = {} + if self.source.get_trigger_characters then + trigger_characters = self.source:get_trigger_characters(misc.copy(c)) or {} + end + if config.get().completion.get_trigger_characters then + return config.get().completion.get_trigger_characters(trigger_characters) + end + return trigger_characters +end + ---Get keyword_pattern ---@return string source.get_keyword_pattern = function(self) - local c = self:get_config() + local c = self:get_source_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, - }) + return self.source:get_keyword_pattern(misc.copy(c)) end return config.get().completion.keyword_pattern end @@ -201,48 +225,28 @@ end ---Get keyword_length ---@return number source.get_keyword_length = function(self) - local c = self:get_config() + local c = self:get_source_config() if c.keyword_length then return c.keyword_length end return config.get().completion.keyword_length or 1 end ----Get 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 + -- NOTE: This implementation is nvim-cmp specific. + -- We trigger new completion after core.confirm but we check only the symbol trigger_character in this case. local before_char = string.sub(ctx.cursor_before_line, -1) - 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 = string.match(ctx.cursor_before_line, '(.)%s*$') + if not before_char or not char.is_symbol(string.byte(before_char)) then before_char = '' end - if string.match(before_char_iw, '^%a+$') then - before_char_iw = '' - end end local completion_context @@ -250,39 +254,31 @@ source.complete = function(self, ctx, callback) 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 + elseif vim.tbl_contains(self:get_trigger_characters(), before_char) then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, + triggerCharacter = before_char, + } + elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then + if self:get_keyword_length() <= (ctx.cursor.col - offset) then + if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions, + } + elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then + completion_context = { + triggerKind = types.lsp.CompletionTriggerKind.Invoked, + } end else - self:reset() + self:reset() -- Should clear current completion if the TriggerKind isn't TriggerCharacter or Manual and keyword length does not enough. end + else + self:reset() -- Should clear current completion if ContextReason is TriggerOnly and the triggerCharacter isn't matched end + -- Does not perform completions. if not completion_context then - if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then - self:reset() - end - debug.log(self:get_debug_name(), 'skip completion') return end @@ -293,16 +289,16 @@ source.complete = function(self, ctx, callback) 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.request_offset = offset self.context = ctx + self.completion_context = completion_context self.source:complete( - { - context = ctx, + vim.tbl_extend('keep', misc.copy(self:get_source_config()), { offset = self.offset, - option = self:get_config().opts, + context = ctx, completion_context = completion_context, - }, + }), self.complete_dedup(vim.schedule_wrap(function(response) response = response or {} @@ -329,8 +325,9 @@ source.complete = function(self, ctx, callback) self.revision = self.revision + 1 end else - debug.log(self:get_debug_name(), 'continue', 'nil') - if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then + -- The completion will be invoked when pressing if the trigger characters contain the . + -- If the server returns an empty response in such a case, should invoke the keyword completion on the next keypress. + if offset == ctx.cursor.col then self:reset() end self.status = prev_status diff --git a/bundle/nvim-cmp/lua/cmp/types/cmp.lua b/bundle/nvim-cmp/lua/cmp/types/cmp.lua index c1fb3df7c..b4e940273 100644 --- a/bundle/nvim-cmp/lua/cmp/types/cmp.lua +++ b/bundle/nvim-cmp/lua/cmp/types/cmp.lua @@ -38,6 +38,7 @@ cmp.ItemField.Menu = 'menu' ---@class cmp.ConfirmOption ---@field public behavior cmp.ConfirmBehavior +---@field public commit_character? string ---@class cmp.SelectOption ---@field public behavior cmp.SelectBehavior @@ -46,18 +47,21 @@ cmp.ItemField.Menu = 'menu' ---@field public body string ---@field public insert_text_mode number +---@class cmp.CompleteParams +---@field public reason? cmp.ContextReason +---@field public config? cmp.ConfigSchema + ---@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.SourceApiParams: cmp.SourceConfig ----@class cmp.SourceCompletionApiParams : cmp.SourceBaseApiParams ----@field public context cmp.Context +---@class cmp.SourceCompletionApiParams : cmp.SourceConfig ---@field public offset number +---@field public context cmp.Context ---@field public completion_context lsp.CompletionContext ---@class cmp.Mapping @@ -73,11 +77,13 @@ cmp.ItemField.Menu = 'menu' ---@field public completion cmp.CompletionConfig ---@field public documentation cmp.DocumentationConfig|"false" ---@field public confirmation cmp.ConfirmationConfig +---@field public matching cmp.MatchingConfig ---@field public sorting cmp.SortingConfig ---@field public formatting cmp.FormattingConfig ---@field public snippet cmp.SnippetConfig ---@field public mapping table ---@field public sources cmp.SourceConfig[] +---@field public view cmp.ViewConfig ---@field public experimental cmp.ExperimentalConfig ---@class cmp.CompletionConfig @@ -98,6 +104,11 @@ cmp.ItemField.Menu = 'menu' ---@field public default_behavior cmp.ConfirmBehavior ---@field public get_commit_characters fun(commit_characters: string[]): string[] +---@class cmp.MatchingConfig +---@field public disallow_fuzzy_matching boolean +---@field public disallow_partial_matching boolean +---@field public disallow_prefix_unmatching boolean + ---@class cmp.SortingConfig ---@field public priority_weight number ---@field public comparators function[] @@ -118,11 +129,28 @@ cmp.ItemField.Menu = 'menu' ---@class cmp.SourceConfig ---@field public name string ----@field public opts table +---@field public option table|nil ---@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 +---@field public trigger_characters string[]|nil +---@field public keyword_pattern string|nil +---@field public keyword_length number|nil +---@field public max_item_count number|nil +---@field public group_index number|nil + +---@class cmp.ViewConfig +---@field public entries cmp.EntriesConfig + +---@alias cmp.EntriesConfig cmp.CustomEntriesConfig|cmp.NativeEntriesConfig|cmp.WildmenuEntriesConfig|string + +---@class cmp.CustomEntriesConfig +---@field name "'custom'" +---@field selection_order "'top_down'"|"'near_cursor'" + +---@class cmp.NativeEntriesConfig +---@field name "'native'" + +---@class cmp.WildmenuEntriesConfig +---@field name "'wildmenu'" +---@field separator string|nil return cmp diff --git a/bundle/nvim-cmp/lua/cmp/types/lsp.lua b/bundle/nvim-cmp/lua/cmp/types/lsp.lua index 45b432118..c8a04473d 100644 --- a/bundle/nvim-cmp/lua/cmp/types/lsp.lua +++ b/bundle/nvim-cmp/lua/cmp/types/lsp.lua @@ -26,7 +26,7 @@ lsp.Position.to_vim = function(buf, position) } end ----Convert lsp.Position to vim.Position +---Convert vim.Position to lsp.Position ---@param buf number|string ---@param position vim.Position ---@return lsp.Position @@ -49,7 +49,7 @@ end lsp.Range = {} ----Convert lsp.Position to vim.Position +---Convert lsp.Range to vim.Range ---@param buf number|string ---@param range lsp.Range ---@return vim.Range @@ -60,7 +60,7 @@ lsp.Range.to_vim = function(buf, range) } end ----Convert lsp.Position to vim.Position +---Convert vim.Range to lsp.Range ---@param buf number|string ---@param range vim.Range ---@return lsp.Range @@ -97,7 +97,6 @@ lsp.InsertTextMode = vim.tbl_add_reverse_lookup(lsp.InsertTextMode) 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" diff --git a/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua b/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua index 81d91216c..fc2b857ca 100644 --- a/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/types/lsp_spec.lua @@ -11,35 +11,36 @@ describe('types.lsp', function() }) local vim_position, lsp_position - vim_position = lsp.Position.to_vim('%', { line = 1, character = 3 }) + local bufnr = vim.api.nvim_get_current_buf() + vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 3 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 10) - lsp_position = lsp.Position.to_lsp('%', vim_position) + lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 3) - vim_position = lsp.Position.to_vim('%', { line = 1, character = 0 }) + vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 0 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 1) - lsp_position = lsp.Position.to_lsp('%', vim_position) + lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 0) - vim_position = lsp.Position.to_vim('%', { line = 1, character = 5 }) + vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 5 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 16) - lsp_position = lsp.Position.to_lsp('%', vim_position) + lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 5) -- overflow (lsp -> vim) - vim_position = lsp.Position.to_vim('%', { line = 1, character = 6 }) + vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 6 }) assert.are.equal(vim_position.row, 2) assert.are.equal(vim_position.col, 16) -- overflow(vim -> lsp) vim_position.col = vim_position.col + 1 - lsp_position = lsp.Position.to_lsp('%', vim_position) + lsp_position = lsp.Position.to_lsp(bufnr, vim_position) assert.are.equal(lsp_position.line, 1) assert.are.equal(lsp_position.character, 5) end) diff --git a/bundle/nvim-cmp/lua/cmp/types/vim.lua b/bundle/nvim-cmp/lua/cmp/types/vim.lua index a2a6cf4d1..0ae28c69f 100644 --- a/bundle/nvim-cmp/lua/cmp/types/vim.lua +++ b/bundle/nvim-cmp/lua/cmp/types/vim.lua @@ -7,6 +7,9 @@ ---@field public empty "1"|nil ---@field public dup "1"|nil ---@field public id any +---@field public abbr_hl_group string|nil +---@field public kind_hl_group string|nil +---@field public menu_hl_group string|nil ---@class vim.Position ---@field public row number diff --git a/bundle/nvim-cmp/lua/cmp/utils/api.lua b/bundle/nvim-cmp/lua/cmp/utils/api.lua index 9f9879d32..d0534092b 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/api.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/api.lua @@ -53,7 +53,8 @@ end api.get_screen_cursor = function() if api.is_cmdline_mode() then - return api.get_cursor() + local cursor = api.get_cursor() + return { cursor[1], cursor[2] + 1 } end local cursor = api.get_cursor() local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) diff --git a/bundle/nvim-cmp/lua/cmp/utils/async.lua b/bundle/nvim-cmp/lua/cmp/utils/async.lua index 39d8777e0..8c698e735 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/async.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/async.lua @@ -1,7 +1,9 @@ local async = {} ---@class cmp.AsyncThrottle +---@field public running boolean ---@field public timeout number +---@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil) ---@field public stop function ---@field public __call function @@ -12,7 +14,13 @@ async.throttle = function(fn, timeout) local time = nil local timer = vim.loop.new_timer() return setmetatable({ + running = false, timeout = timeout, + sync = function(self, timeout_) + vim.wait(timeout_ or 1000, function() + return not self.running + end) + end, stop = function() time = nil timer:stop() @@ -24,12 +32,15 @@ async.throttle = function(fn, timeout) 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)) + self.running = true + timer:stop() + timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() + vim.schedule(function() + time = nil + fn(unpack(args)) + self.running = false + end) end) end, }) diff --git a/bundle/nvim-cmp/lua/cmp/utils/buffer.lua b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua index f3d1c8e64..b48a1b1ff 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/buffer.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/buffer.lua @@ -1,17 +1,30 @@ 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, -}) +buffer.cache = {} + +---@return number buf +buffer.get = function(name) + local buf = buffer.cache[name] + if buf and vim.api.nvim_buf_is_valid(buf) then + return buf + else + return nil + end +end + +---@return number buf +---@return boolean created_new +buffer.ensure = function(name) + local created_new = false + local buf = buffer.get(name) + if not buf then + created_new = true + buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') + vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') + buffer.cache[name] = buf + end + return buf, created_new +end return buffer diff --git a/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua index d024bb04c..a4e71f364 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/feedkeys_spec.lua @@ -23,15 +23,6 @@ describe('feedkeys', function() }) 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() diff --git a/bundle/nvim-cmp/lua/cmp/utils/highlight.lua b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua index c8278b229..cbe25f5ce 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/highlight.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/highlight.lua @@ -10,7 +10,7 @@ highlight.keys = { } highlight.inherit = function(name, source, override) - local cmd = ('highlight! default %s'):format(name) + 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]) diff --git a/bundle/nvim-cmp/lua/cmp/utils/keymap.lua b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua index d0ca963b4..5727cd096 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/keymap.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/keymap.lua @@ -7,7 +7,9 @@ local keymap = {} ---@param keys string ---@return string keymap.t = function(keys) - return vim.api.nvim_replace_termcodes(keys, true, true, true) + return (string.gsub(keys, '(<[A-Za-z0-9\\%-%[%]%^@]->)', function(match) + return vim.api.nvim_eval(string.format([["\%s"]], match)) + end)) end ---Normalize key sequence. @@ -65,6 +67,9 @@ end ---@param count number ---@return string keymap.backspace = function(count) + if type(count) == 'string' then + count = vim.fn.strchars(count, true) + end if count <= 0 then return '' end @@ -73,16 +78,11 @@ keymap.backspace = function(count) return table.concat(keys, '') end ----Create autoindent keys +---Update indentkeys. +---@param expr string ---@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, '') +keymap.indentkeys = function(expr) + return string.format(keymap.t('set indentkeys=%s'), expr and vim.fn.escape(expr, '| \t\\') or '') end ---Return two key sequence are equal or not. @@ -97,25 +97,22 @@ end keymap.listen = function(mode, lhs, callback) lhs = keymap.normalize(keymap.to_keymap(lhs)) - local existing = keymap.get_mapping(mode, lhs) + local existing = keymap.get_map(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) + local fallback = keymap.fallback(bufnr, mode, existing) 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) + local ignore = false + ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=') + if ignore then + fallback() + else + callback(lhs, misc.once(fallback)) end - - callback( - lhs, - misc.once(function() - vim.api.nvim_feedkeys(keymap.t(fallback.keys), fallback.mode, true) - end) - ) end, { expr = false, noremap = true, @@ -123,19 +120,66 @@ keymap.listen = function(mode, lhs, callback) }) end ----Get mapping +---Fallback +keymap.fallback = function(bufnr, mode, map) + return function() + if map.expr then + local fallback_expr = string.format('(cmp.u.k.fallback_expr:%s)', map.lhs) + keymap.set_map(bufnr, mode, fallback_expr, function() + return keymap.solve(bufnr, mode, map).keys + end, { + expr = true, + noremap = map.noremap, + script = map.script, + nowait = map.nowait, + silent = map.silent and mode ~= 'c', + }) + vim.api.nvim_feedkeys(keymap.t(fallback_expr), 'im', true) + elseif not map.callback then + local solved = keymap.solve(bufnr, mode, map) + vim.api.nvim_feedkeys(solved.keys, solved.mode, true) + else + map.callback() + end + end +end + +---Solve +keymap.solve = function(bufnr, mode, map) + local lhs = keymap.t(map.lhs) + local rhs = map.expr and (map.callback and map.callback() or vim.api.nvim_eval(keymap.t(map.rhs))) or keymap.t(map.rhs) + + if map.noremap then + return { keys = rhs, mode = 'in' } + end + + if string.find(rhs, lhs, 1, true) == 1 then + local recursive = string.format('0_(cmp.u.k.recursive:%s)', lhs) + keymap.set_map(bufnr, mode, recursive, lhs, { + noremap = true, + script = map.script, + nowait = map.nowait, + silent = map.silent and mode ~= 'c', + }) + return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' } + end + return { keys = rhs, mode = 'im' } +end + +---Get map ---@param mode string ---@param lhs string ---@return table -keymap.get_mapping = function(mode, lhs) +keymap.get_map = function(mode, lhs) lhs = keymap.normalize(lhs) for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do if keymap.equals(map.lhs, lhs) then return { lhs = map.lhs, - rhs = map.rhs, + rhs = map.rhs or '', expr = map.expr == 1, + callback = map.callback, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, @@ -149,8 +193,9 @@ keymap.get_mapping = function(mode, lhs) if keymap.equals(map.lhs, lhs) then return { lhs = map.lhs, - rhs = map.rhs, + rhs = map.rhs or '', expr = map.expr == 1, + callback = map.callback, noremap = map.noremap == 1, script = map.script == 1, silent = map.silent == 1, @@ -164,80 +209,15 @@ keymap.get_mapping = function(mode, lhs) lhs = lhs, rhs = lhs, expr = false, + callback = nil, noremap = true, script = false, - silent = false, + silent = true, 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 = {}, diff --git a/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua index 315220cdb..959783f34 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/keymap_spec.lua @@ -1,68 +1,159 @@ local spec = require('cmp.utils.spec') +local api = require('cmp.utils.api') +local feedkeys = require('cmp.utils.feedkeys') local keymap = require('cmp.utils.keymap') describe('keymap', function() before_each(spec.before) + it('t', function() + for _, key in ipairs({ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '(example)', + '="abc"', + 'normal! ==', + }) do + assert.are.equal(keymap.t(key), vim.api.nvim_replace_termcodes(key, true, true, true)) + assert.are.equal(keymap.t(key .. key), vim.api.nvim_replace_termcodes(key .. key, true, true, true)) + assert.are.equal(keymap.t(key .. key .. key), vim.api.nvim_replace_termcodes(key .. key .. key, true, true, true)) + end + end) + it('to_keymap', function() assert.are.equal(keymap.to_keymap('\n'), '') assert.are.equal(keymap.to_keymap(''), '') assert.are.equal(keymap.to_keymap('|'), '') end) - describe('evacuate', function() + describe('fallback', 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)) + local run_fallback = function(keys, fallback) + local state = {} + feedkeys.call(keys, '', function() + fallback() end) - it('false', function() - vim.api.nvim_buf_set_keymap(0, 'i', '', [[v:false ? '="foobar"' : 'aiueo']], { + feedkeys.call('', '', function() + if api.is_cmdline_mode() then + state.buffer = { api.get_current_line() } + else + state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false) + end + state.cursor = api.get_cursor() + end) + feedkeys.call('', 'x') + return state + end + + describe('basic', function() + it('', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(pairs)', '()', { noremap = true }) + vim.api.nvim_buf_set_keymap(0, 'i', '(', '(pairs)', { noremap = false }) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + it('=', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '="()"', {}) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + it('callback', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { + callback = function() + vim.api.nvim_feedkeys('()' .. keymap.t(''), 'int', true) + end, + }) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + it('expr-callback', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { expr = true, noremap = false, + silent = true, + callback = function() + return '()' .. keymap.t('') + end, }) - local fallback = keymap.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)) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + -- it('cmdline default ', function() + -- local fallback = keymap.fallback(0, 'c', keymap.get_map('c', '')) + -- local state = run_fallback(':', fallback) + -- assert.are.same({ '' }, state.buffer) + -- assert.are.same({ 1, 0 }, state.cursor) + -- end) + end) + + describe('recursive', function() + it('non-expr', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '()', { + expr = false, + noremap = false, + silent = true, + }) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + it('expr', function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '"()"', { + expr = true, + noremap = false, + silent = true, + }) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) + + it('expr-callback', function() + pcall(function() + vim.api.nvim_buf_set_keymap(0, 'i', '(', '', { + expr = true, + noremap = false, + silent = true, + callback = function() + return keymap.t('()') + end, + }) + local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '(')) + local state = run_fallback('i', fallback) + assert.are.same({ '()' }, state.buffer) + assert.are.same({ 1, 1 }, state.cursor) + end) end) end) end) + describe('realworld', function() before_each(spec.before) + it('#226', function() keymap.listen('i', '', function(_, fallback) fallback() @@ -70,6 +161,7 @@ describe('keymap', function() 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) @@ -77,5 +169,19 @@ describe('keymap', function() vim.api.nvim_feedkeys(keymap.t('iaiueoa'), 'tx', true) assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) end) + + it('#744', function() + vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { + noremap = true, + }) + vim.api.nvim_buf_set_keymap(0, 'i', '', 'recursive', { + noremap = false, + }) + keymap.listen('i', '', function(_, fallback) + fallback() + end) + feedkeys.call(keymap.t('i'), 'tx') + assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) + end) end) end) diff --git a/bundle/nvim-cmp/lua/cmp/utils/misc.lua b/bundle/nvim-cmp/lua/cmp/utils/misc.lua index 8ce41d48c..8dd3529a8 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/misc.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/misc.lua @@ -29,6 +29,28 @@ misc.concat = function(list1, list2) return new_list end +---Return the valu is empty or not. +---@param v any +---@return boolean +misc.empty = function(v) + if not v then + return true + end + if v == vim.NIL then + return true + end + if type(v) == 'string' and v == '' then + return true + end + if type(v) == 'table' and vim.tbl_isempty(v) then + return true + end + if type(v) == 'number' and v == 0 then + return true + end + return false +end + ---The symbol to remove key in misc.merge. misc.none = vim.NIL @@ -77,7 +99,7 @@ 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] or 0 misc.id.group[group] = misc.id.group[group] + 1 return misc.id.group[group] end, @@ -144,9 +166,10 @@ end ---Safe version of vim.str_utfindex ---@param text string ----@param vimindex number +---@param vimindex number|nil ---@return number misc.to_utfindex = function(text, vimindex) + vimindex = vimindex or #text + 1 return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) end @@ -155,6 +178,7 @@ end ---@param utfindex number ---@return number misc.to_vimindex = function(text, utfindex) + utfindex = utfindex or #text for i = utfindex, 1, -1 do local s, v = pcall(function() return vim.str_byteindex(text, i) + 1 @@ -178,4 +202,34 @@ misc.deprecated = function(fn, msg) end end +--Redraw +misc.redraw = setmetatable({ + doing = false, + force = false, + termcode = vim.api.nvim_replace_termcodes('', true, true, true), +}, { + __call = function(self, force) + if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then + if vim.o.incsearch then + return vim.api.nvim_feedkeys(self.termcode, 'in', true) + end + end + + if self.doing then + return + end + self.doing = true + self.force = not not force + vim.schedule(function() + if self.force then + vim.cmd([[redraw!]]) + else + vim.cmd([[redraw]]) + end + self.doing = false + self.force = false + end) + end, +}) + return misc diff --git a/bundle/nvim-cmp/lua/cmp/utils/str.lua b/bundle/nvim-cmp/lua/cmp/utils/str.lua index 91d1bd75c..450c9916b 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/str.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/str.lua @@ -3,24 +3,29 @@ 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 INVALIDS = {} +INVALIDS[string.byte("'")] = true +INVALIDS[string.byte('"')] = true +INVALIDS[string.byte('=')] = true +INVALIDS[string.byte('$')] = true +INVALIDS[string.byte('(')] = true +INVALIDS[string.byte('[')] = true +INVALIDS[string.byte('<')] = true +INVALIDS[string.byte('{')] = true +INVALIDS[string.byte(' ')] = true +INVALIDS[string.byte('\t')] = true +INVALIDS[string.byte('\n')] = true +INVALIDS[string.byte('\r')] = true local NR_BYTE = string.byte('\n') -local PAIR_CHARS = {} -PAIR_CHARS[string.byte('[')] = string.byte(']') -PAIR_CHARS[string.byte('(')] = string.byte(')') -PAIR_CHARS[string.byte('<')] = string.byte('>') +local PAIRS = {} +PAIRS[string.byte('<')] = string.byte('>') +PAIRS[string.byte('[')] = string.byte(']') +PAIRS[string.byte('(')] = string.byte(')') +PAIRS[string.byte('{')] = string.byte('}') +PAIRS[string.byte('"')] = string.byte('"') +PAIRS[string.byte("'")] = string.byte("'") ---Return if specified text has prefix or not ---@param text string @@ -38,6 +43,17 @@ str.has_prefix = function(text, prefix) return true end +---get_common_string +str.get_common_string = function(text1, text2) + local min = math.min(#text1, #text2) + for i = 1, min do + if not char.match(string.byte(text1, i), string.byte(text2, i)) then + return string.sub(text1, 1, i - 1) + end + end + return string.sub(text1, 1, min) +end + ---Remove suffix ---@param text string ---@param suffix string @@ -101,23 +117,47 @@ end ---get_word ---@param text string +---@param stop_char number +---@param min_length number ---@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) +str.get_word = function(text, stop_char, min_length) + min_length = min_length or 0 + + local has_alnum = false + local stack = {} + local word = {} + local add = function(c) + table.insert(word, string.char(c)) + if stack[#stack] == c then + table.remove(stack, #stack) + else + if PAIRS[c] then + table.insert(stack, c) + end end - valids[c] = true - if PAIR_CHARS[c] then - valids[PAIR_CHARS[c]] = true - end - has_valid = has_valid or not invalid end - return text + for i = 1, #text do + local c = string.byte(text, i, i) + if #word < min_length then + table.insert(word, string.char(c)) + elseif not INVALIDS[c] then + add(c) + has_alnum = has_alnum or char.is_alnum(c) + elseif not has_alnum then + add(c) + elseif #stack ~= 0 then + add(c) + if has_alnum and #stack == 0 then + break + end + else + break + end + end + if stop_char and word[#word] == string.char(stop_char) then + table.remove(word, #word) + end + return table.concat(word, '') end ---Oneline diff --git a/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua index d4e492ea4..541414f21 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/str_spec.lua @@ -7,6 +7,9 @@ describe('utils.str', function() assert.are.equal(str.get_word('print()'), 'print') assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]') assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies') + assert.are.equal(str.get_word('"devDependencies": ${1},', string.byte('"')), '"devDependencies') + assert.are.equal(str.get_word('#[cfg(test)]'), '#[cfg(test)]') + assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps') end) it('strikethrough', function() diff --git a/bundle/nvim-cmp/lua/cmp/utils/window.lua b/bundle/nvim-cmp/lua/cmp/utils/window.lua index 98d8e1779..8e2bf02b5 100644 --- a/bundle/nvim-cmp/lua/cmp/utils/window.lua +++ b/bundle/nvim-cmp/lua/cmp/utils/window.lua @@ -18,6 +18,7 @@ local api = require('cmp.utils.api') ---@field public swin2 number|nil ---@field public style cmp.WindowStyle ---@field public opt table +---@field public buffer_opt table ---@field public cache cmp.Cache local window = {} @@ -32,6 +33,7 @@ window.new = function() self.style = {} self.cache = cache.new() self.opt = {} + self.buffer_opt = {} return self end @@ -54,6 +56,26 @@ window.option = function(self, key, value) end end +---Set buffer option. +---NOTE: If the buffer already visible, immediately applied to it. +---@param key string +---@param value any +window.buffer_option = function(self, key, value) + if vim.fn.exists('+' .. key) == 0 then + return + end + + if value == nil then + return self.buffer_opt[key] + end + + self.buffer_opt[key] = value + local existing_buf = buffer.get(self.name) + if existing_buf then + vim.api.nvim_buf_set_option(existing_buf, key, value) + end +end + ---Set style. ---@param style cmp.WindowStyle window.set_style = function(self, style) @@ -70,7 +92,13 @@ end ---Return buffer id. ---@return number window.get_buffer = function(self) - return buffer.ensure(self.name) + local buf, created_new = buffer.ensure(self.name) + if created_new then + for k, v in pairs(self.buffer_opt) do + vim.api.nvim_buf_set_option(buf, k, v) + end + end + return buf end ---Open window @@ -89,7 +117,7 @@ window.open = function(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) + self.win = vim.api.nvim_open_win(self:get_buffer(), false, s) for k, v in pairs(self.opt) do vim.api.nvim_win_set_option(self.win, k, v) end @@ -148,7 +176,7 @@ window.update = function(self) -- In cmdline, vim does not redraw automatically. if api.is_cmdline_mode() then vim.api.nvim_win_call(self.win, function() - vim.cmd([[redraw]]) + misc.redraw() end) end end @@ -251,9 +279,14 @@ window.get_content_height = function(self) 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 + local buf = self:get_buffer() + -- The result of vim.fn.strdisplaywidth depends on the buffer it was called + -- in (see comment in cmp.Entry.get_view). + vim.api.nvim_buf_call(buf, function() + for _, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do + height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width) + end + end) return height end) end diff --git a/bundle/nvim-cmp/lua/cmp/view.lua b/bundle/nvim-cmp/lua/cmp/view.lua index 135ffa6a7..981378b9d 100644 --- a/bundle/nvim-cmp/lua/cmp/view.lua +++ b/bundle/nvim-cmp/lua/cmp/view.lua @@ -4,6 +4,7 @@ local event = require('cmp.utils.event') local keymap = require('cmp.utils.keymap') local docs_view = require('cmp.view.docs_view') local custom_entries_view = require('cmp.view.custom_entries_view') +local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view') local native_entries_view = require('cmp.view.native_entries_view') local ghost_text_view = require('cmp.view.ghost_text_view') @@ -12,6 +13,7 @@ local ghost_text_view = require('cmp.view.ghost_text_view') ---@field private resolve_dedup cmp.AsyncDedup ---@field private native_entries_view cmp.NativeEntriesView ---@field private custom_entries_view cmp.CustomEntriesView +---@field private wildmenu_entries_view cmp.CustomEntriesView ---@field private change_dedup cmp.AsyncDedup ---@field private docs_view cmp.DocsView ---@field private ghost_text_view cmp.GhostTextView @@ -23,6 +25,7 @@ view.new = function() self.resolve_dedup = async.dedup() self.custom_entries_view = custom_entries_view.new() self.native_entries_view = native_entries_view.new() + self.wildmenu_entries_view = wildmenu_entries_view.new() self.docs_view = docs_view.new() self.ghost_text_view = ghost_text_view.new() self.event = event.new() @@ -47,7 +50,7 @@ end 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 + local group_index = s:get_source_config().group_index or 0 if not source_group_map[group_index] then source_group_map[group_index] = {} end @@ -77,10 +80,10 @@ view.open = function(self, ctx, sources) -- create filtered entries. local offset = ctx.cursor.col for i, s in ipairs(source_group) do - if s.offset <= offset then + if s.offset <= ctx.cursor.col 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) + local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight) for _, e in ipairs(s:get_entries(ctx)) do e.score = e.score + priority @@ -109,7 +112,7 @@ view.open = function(self, ctx, sources) end end - -- close. + -- complete_done. if #entries == 0 then self:close() end @@ -117,6 +120,11 @@ end ---Close menu view.close = function(self) + if self:visible() then + self.event:emit('complete_done', { + entry = self:_get_entries_view():get_selected_entry(), + }) + end self:_get_entries_view():close() self.docs_view:close() self.ghost_text_view:hide() @@ -153,6 +161,17 @@ view.select_prev_item = function(self, option) self:_get_entries_view():select_prev_item(option) end +---Get offset. +view.get_offset = function(self) + return self:_get_entries_view():get_offset() +end + +---Get entries. +---@return cmp.Entry[] +view.get_entries = function(self) + return self:_get_entries_view():get_entries() +end + ---Get first entry ---@param self cmp.Entry|nil view.get_first_entry = function(self) @@ -174,54 +193,51 @@ 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() + self.wildmenu_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 + local c = config.get() + local v = self.custom_entries_view + if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then + v = self.wildmenu_entries_view + elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then + v = self.native_entries_view end + v.event:on('change', function() + self:on_entry_change() + end) + return v end ---On entry change -view.on_entry_change = async.throttle( - vim.schedule_wrap(function(self) - if not self:visible() then - return +view.on_entry_change = async.throttle(function(self) + if not self:visible() then + return + end + local e = self:get_selected_entry() + if e then + for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do + keymap.listen('i', c, function(...) + self.event:emit('keymap', ...) + end) end - 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) + e:resolve(vim.schedule_wrap(self.resolve_dedup(function() + if not self:visible() then + return 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 + 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 -) + 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/plugin/cmp.lua b/bundle/nvim-cmp/plugin/cmp.lua index e0db0cc17..a977a78a7 100644 --- a/bundle/nvim-cmp/plugin/cmp.lua +++ b/bundle/nvim-cmp/plugin/cmp.lua @@ -5,6 +5,7 @@ vim.g.loaded_cmp = true local api = require "cmp.utils.api" local misc = require('cmp.utils.misc') +local types = require('cmp.types') local config = require('cmp.config') local highlight = require('cmp.utils.highlight') @@ -20,11 +21,12 @@ vim.cmd [[ 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() + autocmd CmdwinEnter * call v:lua.cmp.plugin.cmdline.leave() " for entering cmdwin with `` augroup END ]] misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function() - if config.get().experimental.native_menu then + if config.is_native_menu() then return end if vim.fn.expand('')~= '=' then @@ -78,6 +80,11 @@ misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function() guibg = 'NONE', ctermbg = 'NONE', }) + for name in pairs(types.lsp.CompletionItemKind) do + if type(name) == 'string' then + vim.cmd(([[highlight default link CmpItemKind%sDefault CmpItemKind]]):format(name)) + end + end highlight.inherit('CmpItemMenuDefault', 'Pmenu', { guibg = 'NONE', ctermbg = 'NONE', @@ -86,32 +93,40 @@ end) _G.cmp.plugin.colorscheme() if vim.fn.hlexists('CmpItemAbbr') ~= 1 then - vim.cmd [[highlight! default link CmpItemAbbr CmpItemAbbrDefault]] + vim.cmd [[highlight default link CmpItemAbbr CmpItemAbbrDefault]] end if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then - vim.cmd [[highlight! default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] + vim.cmd [[highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] end if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then - vim.cmd [[highlight! default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] + vim.cmd [[highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] end if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then - vim.cmd [[highlight! default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] + vim.cmd [[highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] end if vim.fn.hlexists('CmpItemKind') ~= 1 then - vim.cmd [[highlight! default link CmpItemKind CmpItemKindDefault]] + vim.cmd [[highlight default link CmpItemKind CmpItemKindDefault]] +end +for name in pairs(types.lsp.CompletionItemKind) do + if type(name) == 'string' then + local hi = ('CmpItemKind%s'):format(name) + if vim.fn.hlexists(hi) ~= 1 then + vim.cmd(([[highlight default link %s %sDefault]]):format(hi, hi)) + end + end end if vim.fn.hlexists('CmpItemMenu') ~= 1 then - vim.cmd [[highlight! default link CmpItemMenu CmpItemMenuDefault]] + vim.cmd [[highlight default link CmpItemMenu CmpItemMenuDefault]] end vim.cmd [[command! CmpStatus lua require('cmp').status()]] -vim.cmd [[doautocmd User cmp#ready]] +vim.cmd [[doautocmd User CmpReady]] if vim.on_key then vim.on_key(function(keys)