1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-23 10:40:03 +08:00

fix(bundle): update bundle nvim-cmp

close https://github.com/SpaceVim/SpaceVim/issues/4637
This commit is contained in:
wsdjeg 2022-04-13 10:40:59 +08:00
parent f1e57311ca
commit 207aa46f1d
45 changed files with 2366 additions and 2117 deletions

View File

@ -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)

View File

@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!-- I will close the issue if this template was ignored. -->
**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**

View File

@ -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"

View File

@ -26,6 +26,7 @@ jobs:
- name: Setup neovim
uses: rhysd/action-setup-vim@v1
with:
version: nightly
neovim: true
- name: Setup lua

View File

@ -1 +1,3 @@
doc/tags
utils/stylua

View File

@ -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 <<EOF
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
-- require'snippy'.expand_snippet(args.body) -- For `snippy` users.
end,
},
mapping = {
['<C-d>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }),
['<C-b>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }),
['<C-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }),
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
['<C-y>'] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `<C-y>` mapping.
@ -91,7 +84,7 @@ lua <<EOF
i = cmp.mapping.abort(),
c = cmp.mapping.close(),
}),
['<CR>'] = cmp.mapping.confirm({ select = true }),
['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
@ -104,6 +97,15 @@ lua <<EOF
})
})
-- Set configuration for specific filetype.
cmp.setup.filetype('gitcommit', {
sources = cmp.config.sources({
{ name = 'cmp_git' }, -- You can specify the `cmp_git` source if you were installed it.
}, {
{ name = 'buffer' },
})
})
-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', {
sources = {
@ -131,646 +133,83 @@ EOF
### Where can I find more completion sources?
You can search for various completion sources [here](https://github.com/topics/nvim-cmp).
A list of available sources can be found in the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) or by searching for projects that match the nvim-cmp [GitHub topic](https://github.com/topics/nvim-cmp).
### Where can I find advanced configuration examples?
Please see the corresponding [FAQ](#how-to-show-name-of-item-kind-and-source-like-compe) section or [Wiki pages](https://github.com/hrsh7th/nvim-cmp/wiki).
Configuration options
Advanced configuration example
====================
You can specify the following configuration options via `cmp.setup { ... }`.
### Use nvim-cmp as smart omnifunc handler.
The configuration options will be merged with the [default config](./lua/cmp/config/default.lua).
If you want to remove a default option, set it to `false`.
#### mapping (type: table<string, fun(fallback: function)>)
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 = {
['<C-n>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }),
['<C-p>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }),
['<Down>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
['<Up>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.close(),
['<CR>'] = 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 = {
...
['<Tab>'] = 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 = {
['<CR>'] = 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 = {
['<Tab>'] = 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 `<Tab>`.
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<cmp.SourceConfig>)
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 `<C-e>` 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 `<CR>` even if you don't do any mapping instructions for the plugin.
But I think the user want to override `<CR>` 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 = {
['<CR>'] = 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 <C-x><C-o> <Cmd>lua vimrc.cmp.lsp()<CR>
inoremap <C-x><C-s> <Cmd>lua vimrc.cmp.snippet()<CR>
]])
```
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
]])
```

View File

@ -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
"

View File

@ -1,9 +0,0 @@
let s:_plugin_name = expand('<sfile>: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

View File

@ -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('<sfile>'), '<SNR>\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('<SNR>%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

View File

@ -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('<sfile>'), '<SNR>\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('<SNR>%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

View File

@ -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('<sfile>'), '<SNR>\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('<SNR>%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

View File

@ -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('<sfile>'), '<SNR>\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('<SNR>%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 <buffer=%s> 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

View File

@ -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('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
execute join(['function! vital#_cmp#VS#Vim#Option#import() abort', printf("return map({'define': ''}, \"vital#_cmp#function('<SNR>%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

View File

@ -1,330 +0,0 @@
let s:plugin_name = expand('<sfile>:t:r')
let s:vital_base_dir = expand('<sfile>:h')
let s:project_root = expand('<sfile>: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 =~# '^<lambda>\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 <SID> 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('<sfile>: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 <sfile> 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("\<SNR>")), '"[\\x" . printf("%0x", char2nr("\<SNR>"[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<SNR>%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('<SNR>%s_%s', a:sid, a:funcname)
endfunction

View File

@ -1,4 +0,0 @@
cmp
2755f0c8fbd3442bcb7f567832e4d1455b57f9a2
VS.LSP.TextEdit

696
bundle/nvim-cmp/doc/cmp.txt Normal file
View File

@ -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 <<EOF
local cmp = require'cmp'
-- Global setup.
cmp.setup({
snippet = {
expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require'snippy'.expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
end,
},
mapping = {
['<C-d>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }),
['<C-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }),
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
['<C-e>'] = 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.
['<CR>'] = cmp.mapping.confirm({ select = true }),
},
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users.
-- { name = 'luasnip' }, -- For luasnip users.
-- { name = 'snippy' }, -- For snippy users.
-- { name = 'ultisnips' }, -- For ultisnips users.
}, {
{ name = 'buffer' },
})
})
-- `/` cmdline setup.
cmp.setup.cmdline('/', {
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 `<Cmd>lua require('cmp').complete()<CR>`.
*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 = {
['<C-s>'] = cmp.mapping.complete({
config = {
sources = {
{ name = 'vsnip' }
}
}
})
}
}
< >
inoremap <C-S> <Cmd>lua require('cmp').complete({ config = { sources = { { name = 'vsnip' } } } })<CR>
<
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 = {
['<C-l>'] = cmp.mapping(function(fallback)
if cmp.visible() then
return cmp.complete_common_string()
end
fallback()
end, { 'i', 'c' }),
}
}
<
*cmp.confirm* (option: cmp.ConfirmOption, callback: function)
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 `<CR>` or `(`.
The nvim-cmp will overwrite it but you can fallback to the original mapping via invoking the `fallback` function.
>
cmp.setup {
mapping = {
['<CR>'] = 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 = {
['<CR>'] = cmp.mapping(your_mapping_function, { 'i', 'c' })
}
}
<
And you can specify the different mapping function for each modes.
>
cmp.setup {
mapping = {
['<CR>'] = 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<string, fun(fallback: function)`
See |cmp-mapping| section.
*cmp-config.snippet.expand*
snippet.expand~
`fun(option: cmp.SnippetExpansionParams)`
The snippet expansion function. You must integrate your snippet engine plugin via this.
*cmp-config.completion.keyword_length*
completion.keyword_length~
`number`
The number of characters needed to trigger auto-completion.
*cmp-config.completion.keyword_pattern*
completion.keyword_pattern~
`string`
The default keyword pattern.
*cmp-config.completion.autocomplete*
completion.autocomplete~
`cmp.TriggerEvent[] | false`
The auto-completion trigger events. If you specify this value to false, the
nvim-cmp does not completion automatically but you can still use the manual
completion though.
*cmp-config.completion.completeopt*
completion.completeopt~
`string`
The vim's completeopt like setting. See 'completeopt'.
Besically, You don't need to modify this.
*cmp-config.formatting.fields*
formatting.fields~
`cmp.ItemField[]`
The array of completion menu field to specify the order of them.
*cmp-config.formatting.format*
formatting.format~
`fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem`
The function to customize the completion menu appearance. See |complete-items|.
This value also can be used to modify `dup` property.
NOTE: The `vim.CompletedItem` can have special properties `abbr_hl_group`,
`kind_hl_group` and `menu_hl_group`.
*cmp-config.matching.disallow_fuzzy_matching*
matching.disallow_fuzzy_matching~
`boolean`
Specify disallow or allow fuzzy matching.
*cmp-config.matching.disallow_partial_matching*
matching.disallow_partial_matching~
`boolean`
Specify disallow or allow partial matching.
*cmp-config.matching.disallow_prefix_unmatching*
matching.disallow_prefix_unmatching~
`boolean`
Specify disallow or allow prefix unmatching.
*cmp-config.sorting.priority_weight*
sorting.priority_weight~
`number`
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)
<
*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 <C-x><C-o> <Cmd>lua require('cmp').complete()<CR>
<
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 <expr> <Plug>(vimrc:copilot-dummy-map) copilot#Accept("\<Tab>")
<
You can manage copilot.vim's accept feature with nvim-cmp' key-mapping configuration.
>
cmp.setup {
mapping = {
['<C-g>'] = cmp.mapping(function(fallback)
vim.api.nvim_feedkeys(vim.fn['copilot#Accept'](vim.api.nvim_replace_termcodes('<Tab>', true, true, true)), 'n', true)
end)
},
experimental = {
ghost_text = false -- this feature conflict 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:

View File

@ -17,13 +17,19 @@ config.global = require('cmp.config.default')()
---@type table<number, cmp.ConfigSchema>
config.buffers = {}
---@type table<string, cmp.ConfigSchema>
config.filetypes = {}
---@type table<string, cmp.ConfigSchema>
config.cmdline = {}
---@type cmp.ConfigSchema
config.onetime = {}
---Set configuration for global.
---@param c cmp.ConfigSchema
config.set_global = function(c)
config.global = 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

View File

@ -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

View File

@ -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

View File

@ -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 = {
['<Down>'] = mapping({
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
@ -80,30 +38,38 @@ return function()
end,
}),
['<Tab>'] = 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('<C-z>'), 'in', true)
else
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'in', true)
end
end
end,
}),
['<S-Tab>'] = 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('<C-z><C-p><C-p>'), 'in', true)
else
vim.api.nvim_feedkeys(keymap.t('<C-p>'), 'in', true)
end
end
end,
}),
@ -113,6 +79,21 @@ return function()
['<C-e>'] = 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

View File

@ -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

View File

@ -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<string, cmp.Source>
---@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('<BS>'), diff_before))
table.insert(keys, string.rep(keymap.t('<Del>'), 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

View File

@ -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 <Tab> characters.
vim.api.nvim_buf_call(entries_buf, function()
view.abbr = {}
view.abbr.text = item.abbr or ''
view.abbr.bytes = #view.abbr.text
view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
view.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.

View File

@ -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)

View File

@ -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('<C-n>'), 'n')
feedkeys.call(keymap.t('<C-n>'), 'in')
else
feedkeys.call(keymap.t('<Down>'), 'n')
feedkeys.call(keymap.t('<Down>'), '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('<C-p>'), 'n')
feedkeys.call(keymap.t('<C-p>'), 'in')
else
feedkeys.call(keymap.t('<Up>'), 'n')
feedkeys.call(keymap.t('<Up>'), '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('<C-y>'), 'n')
feedkeys.call(keymap.t('<C-y>'), '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

View File

@ -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

View File

@ -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()

View File

@ -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 <CR> if the trigger characters contain the <Space>.
-- If the server returns an empty response in such a case, should invoke the keyword completion on the next keypress.
if offset == ctx.cursor.col then
self:reset()
end
self.status = prev_status

View File

@ -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<string, cmp.Mapping>
---@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

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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,
})

View File

@ -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

View File

@ -23,15 +23,6 @@ describe('feedkeys', function()
})
end)
it('autoindent', function()
vim.cmd([[setlocal indentkeys+==end]])
feedkeys.call(keymap.t('iif<CR><Tab>end') .. 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()

View File

@ -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])

View File

@ -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('<Cmd>setlocal cindent<CR>'))
table.insert(keys, keymap.t('<Cmd>setlocal indentkeys+=!^F<CR>'))
table.insert(keys, keymap.t('<C-f>'))
table.insert(keys, keymap.t('<Cmd>setlocal %scindent<CR>'):format(vim.bo.cindent and '' or 'no'))
table.insert(keys, keymap.t('<Cmd>setlocal indentkeys=%s<CR>'):format(vim.bo.indentkeys:gsub('|', '\\|')))
return table.concat(keys, '')
keymap.indentkeys = function(expr)
return string.format(keymap.t('<Cmd>set indentkeys=%s<CR>'), 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('<Plug>(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('<SNR>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 <Plug> 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 <Plug> 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 <Plug> mapping.
rhs = rhs
elseif map.script then
-- script mapping should always evacuate as <Plug> 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 = ('<Plug>(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 = ('<Plug>(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 = {},

View File

@ -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({
'<F1>',
'<C-a>',
'<C-]>',
'<C-[>',
'<C-^>',
'<C-@>',
'<C-\\>',
'<Tab>',
'<S-Tab>',
'<Plug>(example)',
'<C-r>="abc"<CR>',
'<Cmd>normal! ==<CR>',
}) 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'), '<CR>')
assert.are.equal(keymap.to_keymap('<CR>'), '<CR>')
assert.are.equal(keymap.to_keymap('|'), '<Bar>')
end)
describe('evacuate', function()
describe('fallback', function()
before_each(spec.before)
it('expr & register', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', [['<C-r>="("<CR>']], {
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 & <Plug> (tpope/vim-endwise)', function()
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(paren-close)', [[)<Left>]], {
expr = false,
noremap = true,
})
vim.api.nvim_buf_set_keymap(0, 'i', '(', [[(<Plug>(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', '<Tab>', [[v:true ? '<C-r>="foobar"<CR>' : '<Tab>aiueo']], {
expr = true,
noremap = false,
})
local fallback = keymap.evacuate(0, 'i', '<Tab>')
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', '<Tab>', [[v:false ? '<C-r>="foobar"<CR>' : '<Tab>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('<Plug>', function()
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(pairs)', '()<Left>', { noremap = true })
vim.api.nvim_buf_set_keymap(0, 'i', '(', '<Plug>(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('<C-r>=', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '<C-r>="()"<CR><Left>', {})
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('<Left>'), '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('<Left>')
end,
})
local fallback = keymap.evacuate(0, 'i', '<Tab>')
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 <Tab>', function()
-- local fallback = keymap.fallback(0, 'c', keymap.get_map('c', '<Tab>'))
-- 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', '(', '()<Left>', {
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', '(', '"()<Left>"', {
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('()<Left>')
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', '<c-n>', function(_, fallback)
fallback()
@ -70,6 +161,7 @@ describe('keymap', function()
vim.api.nvim_feedkeys(keymap.t('iaiueo<CR>a<C-n><C-n>'), 'tx', true)
assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
it('#414', function()
keymap.listen('i', '<M-j>', function()
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'int', true)
@ -77,5 +169,19 @@ describe('keymap', function()
vim.api.nvim_feedkeys(keymap.t('iaiueo<CR>a<M-j><M-j>'), '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', '<C-r>', 'recursive', {
noremap = true,
})
vim.api.nvim_buf_set_keymap(0, 'i', '<CR>', '<CR>recursive', {
noremap = false,
})
keymap.listen('i', '<CR>', function(_, fallback)
fallback()
end)
feedkeys.call(keymap.t('i<CR>'), 'tx')
assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
end)
end)

View File

@ -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('<C-r><Esc>', 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

View File

@ -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

View File

@ -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()

View File

@ -18,6 +18,7 @@ local api = require('cmp.utils.api')
---@field public swin2 number|nil
---@field public style cmp.WindowStyle
---@field public opt table<string, any>
---@field public buffer_opt table<string, any>
---@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

View File

@ -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

View File

@ -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 `<C-f>`
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('<afile>')~= '=' 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 <nomodeline> User cmp#ready]]
vim.cmd [[doautocmd <nomodeline> User CmpReady]]
if vim.on_key then
vim.on_key(function(keys)