1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 00:50:05 +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) - [indent-blankline.nvim](https://github.com/lukas-reineke/indent-blankline.nvim/tree/17a83ea765831cb0cc64f768b8c3f43479b90bbe)
- [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/tree/507f8a570ac2b8b8dabdd0f62da3b3194bf822f8) - [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/tree/507f8a570ac2b8b8dabdd0f62da3b3194bf822f8)
- [deoplete-lsp](https://github.com/deoplete-plugins/deoplete-lsp/tree/6299a22bedfb4f814d95cb0010291501472f8fd0) - [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 - name: Setup neovim
uses: rhysd/action-setup-vim@v1 uses: rhysd/action-setup-vim@v1
with: with:
version: nightly
neovim: true neovim: true
- name: Setup lua - name: Setup lua

View File

@ -1 +1,3 @@
doc/tags
utils/stylua 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). 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. 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. 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 Concept
==================== ====================
- Full support for LSP completion related capabilities
- Powerful customizability via Lua functions
- Smart handling of key mapping
- No flicker - 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 Setup
@ -37,9 +30,9 @@ Setup
### Recommended Configuration ### 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) call plug#begin(s:plug_dir)
Plug 'neovim/nvim-lspconfig' Plug 'neovim/nvim-lspconfig'
Plug 'hrsh7th/cmp-nvim-lsp' Plug 'hrsh7th/cmp-nvim-lsp'
@ -78,12 +71,12 @@ lua <<EOF
expand = function(args) expand = function(args)
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users. vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users.
-- require('luasnip').lsp_expand(args.body) -- For `luasnip` users. -- require('luasnip').lsp_expand(args.body) -- For `luasnip` users.
-- require('snippy').expand_snippet(args.body) -- For `snippy` users.
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
-- require'snippy'.expand_snippet(args.body) -- For `snippy` users.
end, end,
}, },
mapping = { 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-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }),
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { '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. ['<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(), i = cmp.mapping.abort(),
c = cmp.mapping.close(), 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({ sources = cmp.config.sources({
{ name = 'nvim_lsp' }, { 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). -- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', { cmp.setup.cmdline('/', {
sources = { sources = {
@ -131,646 +133,83 @@ EOF
### Where can I find more completion sources? ### 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). nvim-cmp can be used as flexible omnifunc manager.
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:
```lua ```lua
mapping = { local cmp = require('cmp')
['<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')
cmp.setup { cmp.setup {
formatting = { completion = {
format = lspkind.cmp_format(), 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. _G.vimrc = _G.vimrc or {}
_G.vimrc.cmp = _G.vimrc.cmp or {}
#### experimental.native_menu (type: boolean) _G.vimrc.cmp.lsp = function()
cmp.complete({
Use vim's native completion menu instead of custom floating menu. config = {
sources = {
Default: `false` { name = 'nvim_lsp' }
}
#### 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
end end
_G.vimrc.cmp.snippet = function()
---Return the source is available or not. cmp.complete({
---@return boolean config = {
function source:is_available() sources = {
return true { name = 'vsnip' }
end }
}
---Return the source name for some information.
function source:get_debug_name()
return 'example'
end
---Return keyword pattern which will be used...
--- 1. Trigger keyword completion
--- 2. Detect menu start offset
--- 3. Reset completion state
---@param params cmp.SourceBaseApiParams
---@return string
function source:get_keyword_pattern(params)
return '???'
end
---Return trigger characters.
---@param params cmp.SourceBaseApiParams
---@return string[]
function source:get_trigger_characters(params)
return { ??? }
end
---Invoke completion (required).
--- If you want to abort completion, just call the callback without arguments.
---@param params cmp.SourceCompletionApiParams
---@param callback fun(response: lsp.CompletionResponse|nil)
function source:complete(params, callback)
callback({
{ label = 'January' },
{ label = 'February' },
{ label = 'March' },
{ label = 'April' },
{ label = 'May' },
{ label = 'June' },
{ label = 'July' },
{ label = 'August' },
{ label = 'September' },
{ label = 'October' },
{ label = 'November' },
{ label = 'December' },
}) })
end end
---Resolve completion item that will be called when the item selected or before the item confirmation. vim.cmd([[
---@param completion_item lsp.CompletionItem inoremap <C-x><C-o> <Cmd>lua vimrc.cmp.lsp()<CR>
---@param callback fun(completion_item: lsp.CompletionItem|nil) inoremap <C-x><C-s> <Cmd>lua vimrc.cmp.snippet()<CR>
function source:resolve(completion_item, callback) ]])
callback(completion_item)
end
---Execute command that will be called when after the item confirmation.
---@param completion_item lsp.CompletionItem
---@param callback fun(completion_item: lsp.CompletionItem|nil)
function source:execute(completion_item, callback)
callback(completion_item)
end
require('cmp').register_source(source.new())
``` ```
You can also create a source by Vim script like this (This is useful to support callback style plugins). ### 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 cmp.setup {
let s:source = {} completion = {
autocomplete = false, -- disable auto-completion.
}
}
function! s:source.new() abort _G.vimrc = _G.vimrc or {}
return extend(deepcopy(s:source)) _G.vimrc.cmp = _G.vimrc.cmp or {}
endfunction _G.vimrc.cmp.on_text_changed = function()
local cursor = vim.api.nvim_win_get_cursor(0)
" The other APIs are also available. local line = vim.api.nvim_get_current_line()
local before = string.sub(line, 1, cursor[2] + 1)
function! s:source.complete(params, callback) abort if before:match('%s*$') then
call a:callback({ cmp.complete() -- Trigger completion only if the cursor is placed at the end of line.
\ { 'label': 'January' }, end
\ { 'label': 'February' }, end
\ { 'label': 'March' }, vim.cmd([[
\ { 'label': 'April' }, augroup vimrc
\ { 'label': 'May' }, autocmd
\ { 'label': 'June' }, autocmd TextChanged,TextChangedI,TextChangedP * call luaeval('vimrc.cmp.on_text_changed()')
\ { 'label': 'July' }, augroup END
\ { 'label': 'August' }, ]])
\ { 'label': 'September' },
\ { 'label': 'October' },
\ { 'label': 'November' },
\ { 'label': 'December' },
\ })
endfunction
call cmp#register_source('month', s:source.new())
``` ```

View File

@ -1,18 +1,6 @@
let s:bridge_id = 0 let s:bridge_id = 0
let s:sources = {} 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 " 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> ---@type table<number, cmp.ConfigSchema>
config.buffers = {} config.buffers = {}
---@type table<string, cmp.ConfigSchema>
config.filetypes = {}
---@type table<string, cmp.ConfigSchema> ---@type table<string, cmp.ConfigSchema>
config.cmdline = {} config.cmdline = {}
---@type cmp.ConfigSchema
config.onetime = {}
---Set configuration for global. ---Set configuration for global.
---@param c cmp.ConfigSchema ---@param c cmp.ConfigSchema
config.set_global = function(c) 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 or 1
config.global.revision = config.global.revision + 1 config.global.revision = config.global.revision + 1
end end
@ -33,31 +39,82 @@ end
---@param bufnr number|nil ---@param bufnr number|nil
config.set_buffer = function(c, bufnr) config.set_buffer = function(c, bufnr)
local revision = (config.buffers[bufnr] or {}).revision or 1 local revision = (config.buffers[bufnr] or {}).revision or 1
config.buffers[bufnr] = c config.buffers[bufnr] = c or {}
config.buffers[bufnr].revision = revision + 1 config.buffers[bufnr].revision = revision + 1
end 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 ---Set configuration for cmdline
config.set_cmdline = function(c, type) ---@param c cmp.ConfigSchema
local revision = (config.cmdline[type] or {}).revision or 1 ---@param cmdtype string
config.cmdline[type] = c config.set_cmdline = function(c, cmdtype)
config.cmdline[type].revision = revision + 1 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 end
---@return cmp.ConfigSchema ---@return cmp.ConfigSchema
config.get = function() config.get = function()
local global = config.global local global_config = config.global
if api.is_cmdline_mode() then if config.onetime.sources then
local type = vim.fn.getcmdtype() local onetime_config = config.onetime
local cmdline = config.cmdline[type] or { revision = 1, sources = {} } return config.cache:ensure({
return config.cache:ensure({ 'get_cmdline', type, global.revision or 0, cmdline.revision or 0 }, function() 'get',
return misc.merge(config.normalize(cmdline), config.normalize(global)) '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) end)
else else
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local buffer = config.buffers[bufnr] or { revision = 1 } local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype')
return config.cache:ensure({ 'get_buffer', bufnr, global.revision or 0, buffer.revision or 0 }, function() local buffer_config = config.buffers[bufnr] or { revision = 1 }
return misc.merge(config.normalize(buffer), config.normalize(global)) 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 end
end end
@ -78,19 +135,31 @@ config.get_source_config = function(name)
local c = config.get() local c = config.get()
for _, s in ipairs(c.sources) do for _, s in ipairs(c.sources) do
if s.name == name then if s.name == name then
if type(s.opts) ~= 'table' then
s.opts = {}
end
return s return s
end end
end end
return nil return nil
end 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 ---Normalize mapping key
---@param c cmp.ConfigSchema ---@param c cmp.ConfigSchema
---@return cmp.ConfigSchema ---@return cmp.ConfigSchema
config.normalize = function(c) config.normalize = function(c)
-- make sure c is not 'nil'
c = c == nil and {} or c
if c.mapping then if c.mapping then
local normalized = {} local normalized = {}
for k, v in pairs(c.mapping) do for k, v in pairs(c.mapping) do
@ -98,6 +167,39 @@ config.normalize = function(c)
end end
c.mapping = normalized c.mapping = normalized
end 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 return c
end end

View File

@ -1,4 +1,5 @@
local types = require('cmp.types') local types = require('cmp.types')
local cache = require('cmp.utils.cache')
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
local compare = {} local compare = {}
@ -100,4 +101,135 @@ compare.order = function(entry1, entry2)
end end
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 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 compare = require('cmp.config.compare')
local mapping = require('cmp.config.mapping') local mapping = require('cmp.config.mapping')
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types') local types = require('cmp.types')
local WIDE_HEIGHT = 40 local WIDE_HEIGHT = 40
@ -8,58 +9,15 @@ local WIDE_HEIGHT = 40
return function() return function()
return { return {
enabled = function() 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, 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, preselect = types.cmp.PreselectMode.Item,
documentation = {
border = { '', '', '', ' ', '', '', '', ' ' },
winhighlight = 'NormalFloat:NormalFloat,FloatBorder:NormalFloat',
maxwidth = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))),
maxheight = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)),
},
confirmation = {
default_behavior = types.cmp.ConfirmBehavior.Insert,
get_commit_characters = function(commit_characters)
return commit_characters
end,
},
sorting = {
priority_weight = 2,
comparators = {
compare.offset,
compare.exact,
compare.score,
compare.recently_used,
compare.kind,
compare.sort_text,
compare.length,
compare.order,
},
},
event = {},
mapping = { mapping = {
['<Down>'] = mapping({ ['<Down>'] = mapping({
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
@ -80,30 +38,38 @@ return function()
end, end,
}), }),
['<Tab>'] = mapping({ ['<Tab>'] = mapping({
c = function(fallback) c = function()
local cmp = require('cmp') 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 if cmp.visible() then
cmp.select_next_item() cmp.select_next_item()
else else
cmp.complete() cmp.complete()
end end
else 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
end, end,
}), }),
['<S-Tab>'] = mapping({ ['<S-Tab>'] = mapping({
c = function(fallback) c = function()
local cmp = require('cmp') 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 if cmp.visible() then
cmp.select_prev_item() cmp.select_prev_item()
else else
cmp.complete() cmp.complete()
end end
else 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
end, end,
}), }),
@ -113,6 +79,21 @@ return function()
['<C-e>'] = mapping.abort(), ['<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 = { formatting = {
fields = { 'abbr', 'kind', 'menu' }, fields = { 'abbr', 'kind', 'menu' },
format = function(_, vim_item) format = function(_, vim_item)
@ -120,11 +101,52 @@ return function()
end, end,
}, },
experimental = { matching = {
native_menu = false, disallow_fuzzy_matching = false,
ghost_text = 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 = {}, 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 end

View File

@ -13,9 +13,19 @@ mapping = setmetatable({}, {
}) })
---Invoke completion ---Invoke completion
mapping.complete = function() ---@param option cmp.CompleteParams
mapping.complete = function(option)
return function(fallback) 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() fallback()
end end
end end

View File

@ -1,4 +1,5 @@
local debug = require('cmp.utils.debug') local debug = require('cmp.utils.debug')
local str = require('cmp.utils.str')
local char = require('cmp.utils.char') local char = require('cmp.utils.char')
local pattern = require('cmp.utils.pattern') local pattern = require('cmp.utils.pattern')
local feedkeys = require('cmp.utils.feedkeys') local feedkeys = require('cmp.utils.feedkeys')
@ -14,14 +15,13 @@ local api = require('cmp.utils.api')
local event = require('cmp.utils.event') local event = require('cmp.utils.event')
local SOURCE_TIMEOUT = 500 local SOURCE_TIMEOUT = 500
local THROTTLE_TIME = 120 local DEBOUNCE_TIME = 80
local DEBOUNCE_TIME = 20 local THROTTLE_TIME = 40
---@class cmp.Core ---@class cmp.Core
---@field public suspending boolean ---@field public suspending boolean
---@field public view cmp.View ---@field public view cmp.View
---@field public sources cmp.Source[] ---@field public sources cmp.Source[]
---@field public sources_by_name table<string, cmp.Source>
---@field public context cmp.Context ---@field public context cmp.Context
---@field public event cmp.Event ---@field public event cmp.Event
local core = {} local core = {}
@ -30,13 +30,15 @@ core.new = function()
local self = setmetatable({}, { __index = core }) local self = setmetatable({}, { __index = core })
self.suspending = false self.suspending = false
self.sources = {} self.sources = {}
self.sources_by_name = {}
self.context = context.new() self.context = context.new()
self.event = event.new() self.event = event.new()
self.view = view.new() self.view = view.new()
self.view.event:on('keymap', function(...) self.view.event:on('keymap', function(...)
self:on_keymap(...) self:on_keymap(...)
end) end)
self.view.event:on('complete_done', function(evt)
self.event:emit('complete_done', evt)
end)
return self return self
end end
@ -44,19 +46,11 @@ end
---@param s cmp.Source ---@param s cmp.Source
core.register_source = function(self, s) core.register_source = function(self, s)
self.sources[s.id] = 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 end
---Unregister source ---Unregister source
---@param source_id string ---@param source_id string
core.unregister_source = function(self, source_id) 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 self.sources[source_id] = nil
end end
@ -86,14 +80,23 @@ core.suspend = function(self)
end end
---Get sources that sorted by priority ---Get sources that sorted by priority
---@param statuses cmp.SourceStatus[] ---@param filter cmp.SourceStatus[]|fun(s: cmp.Source): boolean
---@return cmp.Source[] ---@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 = {} local sources = {}
for _, c in pairs(config.get().sources) do for _, c in pairs(config.get().sources) do
for _, s in ipairs(self.sources_by_name[c.name] or {}) do for _, s in pairs(self.sources) do
if not statuses or vim.tbl_contains(statuses, s.status) then if c.name == s.name then
if s:is_available() then if s:is_available() and f(s) then
table.insert(sources, s) table.insert(sources, s)
end end
end end
@ -118,6 +121,7 @@ core.on_keymap = function(self, keys, fallback)
local is_printable = char.is_printable(string.byte(chars, 1)) local is_printable = char.is_printable(string.byte(chars, 1))
self:confirm(e, { self:confirm(e, {
behavior = is_printable and 'insert' or 'replace', behavior = is_printable and 'insert' or 'replace',
commit_character = chars,
}, function() }, function()
local ctx = self:get_context() local ctx = self:get_context()
local word = e:get_word() local word = e:get_word()
@ -154,7 +158,6 @@ core.on_change = function(self, trigger_event)
self:get_context({ reason = types.cmp.ContextReason.Auto }) self:get_context({ reason = types.cmp.ContextReason.Auto })
return return
end end
self:autoindent(trigger_event, function() self:autoindent(trigger_event, function()
local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto }) local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto })
debug.log(('ctx: `%s`'):format(ctx.cursor_before_line)) 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 if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then
self:complete(ctx) self:complete(ctx)
else else
self.filter.timeout = THROTTLE_TIME self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
self:filter() self:filter()
end end
else else
@ -204,92 +207,140 @@ core.autoindent = function(self, trigger_event, callback)
return callback() return callback()
end end
-- Scan indentkeys. -- Reset current completion if indentkeys matched.
for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do
if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then
local release = self:suspend() self:reset()
vim.schedule(function() -- Check autoindent already applied. self:set_context(context.empty())
if cursor_before_line == api.get_cursor_before_line() then break
feedkeys.call(keymap.autoindent(), 'n', function()
release()
callback()
end)
else
callback()
end
end)
return
end end
end end
-- indentkeys does not matched.
callback() callback()
end 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 ---Invoke completion
---@param ctx cmp.Context ---@param ctx cmp.Context
core.complete = function(self, ctx) core.complete = function(self, ctx)
if not api.is_suitable_mode() then if not api.is_suitable_mode() then
return return
end end
self:set_context(ctx) self:set_context(ctx)
for _, s in ipairs(self:get_sources({ source.SourceStatus.WAITING, source.SourceStatus.COMPLETED })) do -- Invoke completion sources.
s:complete( local sources = self:get_sources()
ctx, for _, s in ipairs(sources) do
(function(src) local callback
local callback callback = (function(s_)
callback = function() return function()
local new = context.new(ctx) local new = context.new(ctx)
if new:changed(new.prev_context) and ctx == self.context then if s_.incomplete and new:changed(s_.context) then
src:complete(new, callback) s_:complete(new, callback)
else 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.stop()
self.filter.timeout = DEBOUNCE_TIME self.filter.timeout = self.view:visible() and DEBOUNCE_TIME or 0
self:filter() self:filter()
end end
end end
return callback end
end)(s) end)(s)
) s:complete(ctx, callback)
end end
self.filter.timeout = THROTTLE_TIME if not self.view:get_active_entry() then
self:filter() self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
self:filter()
end
end end
---Update completion menu ---Update completion menu
core.filter = async.throttle( core.filter = async.throttle(function(self)
vim.schedule_wrap(function(self) self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
if not api.is_suitable_mode() then
return
end
if self.view:get_active_entry() ~= nil then
return
end
local ctx = self:get_context()
-- To wait for processing source for that's timeout. -- Check invalid condition.
local sources = {} local ignore = false
for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do ignore = ignore or not api.is_suitable_mode()
local time = SOURCE_TIMEOUT - s:get_fetching_time() if ignore then
if not s.incomplete and time > 0 then return
if #sources == 0 then end
self.filter.stop()
self.filter.timeout = time + 1
self:filter()
return
end
break
end
table.insert(sources, s)
end
self.filter.timeout = THROTTLE_TIME
self.view:open(ctx, sources) -- Check fetching sources.
end), local sources = {}
THROTTLE_TIME 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. ---Confirm completion.
---@param e cmp.Entry ---@param e cmp.Entry
@ -308,10 +359,11 @@ core.confirm = function(self, e, option, callback)
-- Close menus. -- Close menus.
self.view:close() self.view:close()
feedkeys.call(keymap.indentkeys(), 'n')
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
local ctx = context.new() local ctx = context.new()
local keys = {} 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, e:get_word())
table.insert(keys, keymap.undobreak()) table.insert(keys, keymap.undobreak())
feedkeys.call(table.concat(keys, ''), 'int') feedkeys.call(table.concat(keys, ''), 'int')
@ -320,9 +372,9 @@ core.confirm = function(self, e, option, callback)
local ctx = context.new() local ctx = context.new()
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
local keys = {} 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())) 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 else
vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { 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()), string.sub(e.context.cursor_before_line, e:get_offset()),
@ -331,8 +383,8 @@ core.confirm = function(self, e, option, callback)
end end
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
local ctx = context.new()
if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
local pre = context.new()
e:resolve(function() e:resolve(function()
local new = context.new() local new = context.new()
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {} local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
@ -341,8 +393,8 @@ core.confirm = function(self, e, option, callback)
end end
local has_cursor_line_text_edit = (function() local has_cursor_line_text_edit = (function()
local minrow = math.min(pre.cursor.row, new.cursor.row) local minrow = math.min(ctx.cursor.row, new.cursor.row)
local maxrow = math.max(pre.cursor.row, new.cursor.row) local maxrow = math.max(ctx.cursor.row, new.cursor.row)
for _, te in ipairs(text_edits) do for _, te in ipairs(text_edits) do
local srow = te.range.start.line + 1 local srow = te.range.start.line + 1
local erow = te.range['end'].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 if has_cursor_line_text_edit then
return return
end end
vim.fn['cmp#apply_text_edits'](new.bufnr, text_edits) vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16')
end) end)
else 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
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
@ -375,8 +427,8 @@ core.confirm = function(self, e, option, callback)
completion_item.textEdit.range = e:get_insert_range() completion_item.textEdit.range = e:get_insert_range()
end end
local diff_before = e.context.cursor.character - completion_item.textEdit.range.start.character local diff_before = math.max(0, 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_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character)
local new_text = completion_item.textEdit.newText local new_text = completion_item.textEdit.newText
if api.is_insert_mode() then if api.is_insert_mode() then
@ -388,14 +440,14 @@ core.confirm = function(self, e, option, callback)
if is_snippet then if is_snippet then
completion_item.textEdit.newText = '' completion_item.textEdit.newText = ''
end 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 texts = vim.split(completion_item.textEdit.newText, '\n')
local position = completion_item.textEdit.range.start local position = completion_item.textEdit.range.start
position.line = position.line + (#texts - 1) position.line = position.line + (#texts - 1)
if #texts == 1 then if #texts == 1 then
position.character = position.character + vim.str_utfindex(texts[1]) position.character = position.character + misc.to_utfindex(texts[1])
else else
position.character = vim.str_utfindex(texts[#texts]) position.character = misc.to_utfindex(texts[#texts])
end end
local pos = types.lsp.Position.to_vim(0, position) local pos = types.lsp.Position.to_vim(0, position)
vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 }) 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('<BS>'), diff_before))
table.insert(keys, string.rep(keymap.t('<Del>'), diff_after)) table.insert(keys, string.rep(keymap.t('<Del>'), diff_after))
table.insert(keys, new_text) table.insert(keys, new_text)
feedkeys.call(table.concat(keys, ''), 'int') feedkeys.call(table.concat(keys, ''), 'in')
end end
end) end)
feedkeys.call(keymap.indentkeys(vim.bo.indentkeys), 'n')
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
e:execute(vim.schedule_wrap(function() e:execute(vim.schedule_wrap(function()
release() release()
self.event:emit('confirm_done', e) self.event:emit('confirm_done', {
entry = e,
commit_character = option.commit_character,
})
if callback then if callback then
callback() callback()
end end
@ -429,7 +485,7 @@ core.reset = function(self)
for _, s in pairs(self.sources) do for _, s in pairs(self.sources) do
s:reset() s:reset()
end end
self:get_context() -- To prevent new event self.context = context.empty()
end end
return core return core

View File

@ -54,10 +54,10 @@ end
---Make offset value ---Make offset value
---@return number ---@return number
entry.get_offset = function(self) 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 local offset = self.source_offset
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) then
local range = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range) local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
if range then if range then
local c = misc.to_vimindex(self.context.cursor_line, range.start.character) local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
for idx = c, self.source_offset do for idx = c, self.source_offset do
@ -98,28 +98,32 @@ entry.get_offset = function(self)
end end
---Create word for vim.CompletedItem ---Create word for vim.CompletedItem
---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@return string ---@return string
entry.get_word = function(self) 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. --NOTE: This is nvim-cmp specific implementation.
if misc.safe(self.completion_item.word) then if misc.safe(self:get_completion_item().word) then
return self.completion_item.word return self:get_completion_item().word
end end
local word local word
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
word = str.trim(self.completion_item.textEdit.newText) word = str.trim(self:get_completion_item().textEdit.newText)
local overwrite = self:get_overwrite() if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
if 0 < overwrite[2] or self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = vim.lsp.util.parse_snippet(word)
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1))
end end
elseif misc.safe(self.completion_item.insertText) then local overwrite = self:get_overwrite()
word = str.trim(self.completion_item.insertText) if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
if self.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)
word = str.get_word(word) 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 end
else else
word = str.trim(self.completion_item.label) word = str.trim(self:get_completion_item().label)
end end
return str.oneline(word) return str.oneline(word)
end) end)
@ -128,9 +132,9 @@ end
---Get overwrite information ---Get overwrite information
---@return number, number ---@return number, number
entry.get_overwrite = function(self) entry.get_overwrite = function(self)
return self.cache:ensure('get_overwrite', function() return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function()
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) then
local r = misc.safe(self.completion_item.textEdit.insert) or misc.safe(self.completion_item.textEdit.range) 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 s = misc.to_vimindex(self.context.cursor_line, r.start.character)
local e = misc.to_vimindex(self.context.cursor_line, r['end'].character) local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
local before = self.context.cursor.col - s local before = self.context.cursor.col - s
@ -144,27 +148,13 @@ end
---Create filter text ---Create filter text
---@return string ---@return string
entry.get_filter_text = function(self) 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 local word
if misc.safe(self.completion_item.filterText) then if misc.safe(self:get_completion_item().filterText) then
word = self.completion_item.filterText word = self:get_completion_item().filterText
else else
word = str.trim(self.completion_item.label) word = str.trim(self:get_completion_item().label)
end 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 return word
end) end)
end end
@ -172,20 +162,20 @@ end
---Get LSP's insert text ---Get LSP's insert text
---@return string ---@return string
entry.get_insert_text = function(self) 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 local word
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) then
word = str.trim(self.completion_item.textEdit.newText) word = str.trim(self:get_completion_item().textEdit.newText)
if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end end
elseif misc.safe(self.completion_item.insertText) then elseif misc.safe(self:get_completion_item().insertText) then
word = str.trim(self.completion_item.insertText) word = str.trim(self:get_completion_item().insertText)
if self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}') word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end end
else else
word = str.trim(self.completion_item.label) word = str.trim(self:get_completion_item().label)
end end
return word return word
end) end)
@ -194,31 +184,38 @@ end
---Return the item is deprecated or not. ---Return the item is deprecated or not.
---@return boolean ---@return boolean
entry.is_deprecated = function(self) 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 end
---Return view information. ---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 } } ---@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) 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 = {} local view = {}
view.abbr = {} -- The result of vim.fn.strdisplaywidth depends on which buffer it was
view.abbr.text = item.abbr or '' -- called in because it reads the values of the option 'tabstop' when
view.abbr.bytes = #view.abbr.text -- rendering <Tab> characters.
view.abbr.width = vim.str_utfindex(view.abbr.text) vim.api.nvim_buf_call(entries_buf, function()
view.abbr.hl_group = self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr' view.abbr = {}
view.kind = {} view.abbr.text = item.abbr or ''
view.kind.text = item.kind or '' view.abbr.bytes = #view.abbr.text
view.kind.bytes = #view.kind.text view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
view.kind.width = vim.str_utfindex(view.kind.text) view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
view.kind.hl_group = 'CmpItemKind' view.kind = {}
view.menu = {} view.kind.text = item.kind or ''
view.menu.text = item.menu or '' view.kind.bytes = #view.kind.text
view.menu.bytes = #view.menu.text view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
view.menu.width = vim.str_utfindex(view.menu.text) view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
view.menu.hl_group = 'CmpItemMenu' view.menu = {}
view.dup = item.dup 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 return view
end) end)
end end
@ -233,13 +230,16 @@ entry.get_vim_item = function(self, suggest_offset)
local abbr = str.oneline(completion_item.label) local abbr = str.oneline(completion_item.label)
-- ~ indicator -- ~ indicator
local is_snippet = false
if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
abbr = abbr .. '~' is_snippet = true
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
local insert_text = self:get_insert_text() is_snippet = self:get_insert_text() ~= word
if word ~= insert_text then elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
abbr = abbr .. '~' is_snippet = true
end end
if is_snippet then
abbr = abbr .. '~'
end end
-- append delta text -- append delta text
@ -260,10 +260,12 @@ entry.get_vim_item = function(self, suggest_offset)
end end
-- remove duplicated string. -- remove duplicated string.
for i = 1, #word - 1 do if self:get_offset() ~= self.context.cursor.col then
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then for i = 1, #word - 1 do
word = string.sub(word, 1, i - 1) if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
break word = string.sub(word, 1, i - 1)
break
end
end end
end end
@ -272,7 +274,7 @@ entry.get_vim_item = function(self, suggest_offset)
abbr = abbr, abbr = abbr,
kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1], kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
menu = menu, menu = menu,
dup = self.completion_item.dup or 1, dup = self:get_completion_item().dup or 1,
} }
if config.get().formatting.format then if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item) vim_item = config.get().formatting.format(self, vim_item)
@ -298,11 +300,11 @@ end
---@return lsp.Range|nil ---@return lsp.Range|nil
entry.get_insert_range = function(self) entry.get_insert_range = function(self)
local insert_range local insert_range
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) then
if misc.safe(self.completion_item.textEdit.insert) then if misc.safe(self:get_completion_item().textEdit.insert) then
insert_range = self.completion_item.textEdit.insert insert_range = self:get_completion_item().textEdit.insert
else else
insert_range = self.completion_item.textEdit.range insert_range = self:get_completion_item().textEdit.range
end end
else else
insert_range = { insert_range = {
@ -319,14 +321,10 @@ end
---Return replace range ---Return replace range
---@return lsp.Range|nil ---@return lsp.Range|nil
entry.get_replace_range = function(self) 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 local replace_range
if misc.safe(self.completion_item.textEdit) then if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then
if misc.safe(self.completion_item.textEdit.replace) then replace_range = self:get_completion_item().textEdit.replace
replace_range = self.completion_item.textEdit.replace
else
replace_range = self.completion_item.textEdit.range
end
else else
replace_range = { replace_range = {
start = { start = {
@ -342,14 +340,49 @@ end
---Match line. ---Match line.
---@param input string ---@param input string
---@param matching_config cmp.MatchingConfig
---@return { score: number, matches: table[] } ---@return { score: number, matches: table[] }
entry.match = function(self, input) entry.match = function(self, input, matching_config)
return self.match_cache:ensure(input, function() 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, _ 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 if self:get_filter_text() ~= self:get_completion_item().label then
_, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() }) _, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() })
end end
return { score = score, matches = matches } return { score = score, matches = matches }
end) end)
end end
@ -357,12 +390,12 @@ end
---Get resolved completion item if possible. ---Get resolved completion item if possible.
---@return lsp.CompletionItem ---@return lsp.CompletionItem
entry.get_completion_item = function(self) 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 if self.resolved_completion_item then
local completion_item = misc.copy(self.completion_item) local completion_item = misc.copy(self.completion_item)
completion_item.detail = self.resolved_completion_item.detail or completion_item.detail for k, v in pairs(self.resolved_completion_item) do
completion_item.documentation = self.resolved_completion_item.documentation or completion_item.documentation completion_item[k] = v or completion_item[k]
completion_item.additionalTextEdits = self.resolved_completion_item.additionalTextEdits or completion_item.additionalTextEdits end
return completion_item return completion_item
end end
return self.completion_item return self.completion_item
@ -378,9 +411,14 @@ entry.get_documentation = function(self)
-- detail -- detail
if misc.safe(item.detail) and item.detail ~= '' then 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, { table.insert(documents, {
kind = types.lsp.MarkupKind.Markdown, 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 end
@ -399,7 +437,7 @@ end
---Get completion item kind ---Get completion item kind
---@return lsp.CompletionItemKind ---@return lsp.CompletionItemKind
entry.get_kind = function(self) 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 end
---Execute completion item's command. ---Execute completion item's command.

View File

@ -1,4 +1,6 @@
local spec = require('cmp.utils.spec') local spec = require('cmp.utils.spec')
local source = require('cmp.source')
local async = require('cmp.utils.async')
local entry = require('cmp.entry') 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_vim_item(4).word, '->foo')
assert.are.equal(e:get_filter_text(), '.foo') assert.are.equal(e:get_filter_text(), 'foo')
end) end)
it('[typescript-language-server] 1', function() it('[typescript-language-server] 1', function()
@ -264,6 +266,65 @@ describe('entry', function()
assert.are.equal(e:get_filter_text(), '$this') assert.are.equal(e:get_filter_text(), '$this')
end) 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() it('[#47] word should not contain \\n character', function()
local state = spec.state('', 1, 1) local state = spec.state('', 1, 1)

View File

@ -17,17 +17,33 @@ end
cmp.lsp = require('cmp.types.lsp') cmp.lsp = require('cmp.types.lsp')
cmp.vim = require('cmp.types.vim') 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 = {}
cmp.config.disable = misc.none cmp.config.disable = misc.none
cmp.config.compare = require('cmp.config.compare') cmp.config.compare = require('cmp.config.compare')
cmp.config.sources = require('cmp.config.sources') cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')
---Expose event ---Sync asynchronous process.
cmp.event = cmp.core.event cmp.sync = function(callback)
return function(...)
cmp.core.filter:sync(1000)
if callback then
return callback(...)
end
end
end
---Export mapping ---Suspend completion.
cmp.mapping = require('cmp.config.mapping') cmp.suspend = function()
return cmp.core:suspend()
end
---Register completion sources ---Register completion sources
---@param name string ---@param name string
@ -52,28 +68,41 @@ cmp.get_config = function()
end end
---Invoke completion manually ---Invoke completion manually
cmp.complete = function() ---@param option cmp.CompleteParams
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.Manual })) 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 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. ---Return view is visible or not.
cmp.visible = function() cmp.visible = cmp.sync(function()
return cmp.core.view:visible() or vim.fn.pumvisible() == 1 return cmp.core.view:visible() or vim.fn.pumvisible() == 1
end end)
---Get current selected entry or nil ---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() return cmp.core.view:get_selected_entry()
end end)
---Get current active entry or nil ---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() 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 ---Close current completion
cmp.close = function() cmp.close = cmp.sync(function()
if cmp.core.view:visible() then if cmp.core.view:visible() then
local release = cmp.core:suspend() local release = cmp.core:suspend()
cmp.core.view:close() cmp.core.view:close()
@ -83,10 +112,10 @@ cmp.close = function()
else else
return false return false
end end
end end)
---Abort current completion ---Abort current completion
cmp.abort = function() cmp.abort = cmp.sync(function()
if cmp.core.view:visible() then if cmp.core.view:visible() then
local release = cmp.core:suspend() local release = cmp.core:suspend()
cmp.core.view:abort() cmp.core.view:abort()
@ -95,83 +124,65 @@ cmp.abort = function()
else else
return false return false
end end
end end)
---Suspend completion.
cmp.suspend = function()
return cmp.core:suspend()
end
---Select next item if possible ---Select next item if possible
cmp.select_next_item = function(option) cmp.select_next_item = cmp.sync(function(option)
option = option or {} option = option or {}
-- Hack: Ignore when executing macro.
if vim.fn.reg_executing() ~= '' then
return true
end
if cmp.core.view:visible() then if cmp.core.view:visible() then
local release = cmp.core:suspend() local release = cmp.core:suspend()
cmp.core.view:select_next_item(option) cmp.core.view:select_next_item(option)
vim.schedule(release) vim.schedule(release)
return true return true
elseif vim.fn.pumvisible() == 1 then 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 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 else
feedkeys.call(keymap.t('<Down>'), 'n') feedkeys.call(keymap.t('<Down>'), 'in')
end end
return true return true
end end
return false return false
end end)
---Select prev item if possible ---Select prev item if possible
cmp.select_prev_item = function(option) cmp.select_prev_item = cmp.sync(function(option)
option = option or {} option = option or {}
-- Hack: Ignore when executing macro.
if vim.fn.reg_executing() ~= '' then
return true
end
if cmp.core.view:visible() then if cmp.core.view:visible() then
local release = cmp.core:suspend() local release = cmp.core:suspend()
cmp.core.view:select_prev_item(option) cmp.core.view:select_prev_item(option)
vim.schedule(release) vim.schedule(release)
return true return true
elseif vim.fn.pumvisible() == 1 then 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 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 else
feedkeys.call(keymap.t('<Up>'), 'n') feedkeys.call(keymap.t('<Up>'), 'in')
end end
return true return true
end end
return false return false
end end)
---Scrolling documentation window if possible ---Scrolling documentation window if possible
cmp.scroll_docs = function(delta) cmp.scroll_docs = cmp.sync(function(delta)
if cmp.core.view:visible() then if cmp.core.view:visible() then
cmp.core.view:scroll_docs(delta) cmp.core.view:scroll_docs(delta)
return true return true
else else
return false return false
end end
end end)
---Confirm completion ---Confirm completion
cmp.confirm = function(option, callback) cmp.confirm = cmp.sync(function(option, callback)
option = option or {} option = option or {}
callback = callback or function() end 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) local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil)
if e then if e then
cmp.core:confirm(e, { cmp.core:confirm(e, {
@ -182,13 +193,14 @@ cmp.confirm = function(option, callback)
end) end)
return true return true
else else
-- Special handling for native puma. Required to facilitate key mapping processing.
if vim.fn.complete_info({ 'selected' }).selected ~= -1 then 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 return true
end end
return false return false
end end
end end)
---Show status ---Show status
cmp.status = function() cmp.status = function()
@ -255,6 +267,9 @@ cmp.setup = setmetatable({
global = function(c) global = function(c)
config.set_global(c) config.set_global(c)
end, end,
filetype = function(filetype, c)
config.set_filetype(c, filetype)
end,
buffer = function(c) buffer = function(c)
config.set_buffer(c, vim.api.nvim_get_current_buf()) config.set_buffer(c, vim.api.nvim_get_current_buf())
end, end,
@ -302,11 +317,29 @@ end)
autocmd.subscribe('CursorMoved', function() autocmd.subscribe('CursorMoved', function()
if config.enabled() then if config.enabled() then
cmp.core:on_moved() cmp.core:on_moved()
else
cmp.core:reset()
cmp.core.view:close()
end end
end) end)
cmp.event:on('confirm_done', function(e) autocmd.subscribe('InsertEnter', function()
cmp.config.compare.recently_used:add_entry(e) 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) end)
return cmp return cmp

View File

@ -72,9 +72,11 @@ end
---Match entry ---Match entry
---@param input string ---@param input string
---@param word 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 ---@return number
matcher.match = function(input, word, words) matcher.match = function(input, word, option)
option = option or {}
-- Empty input -- Empty input
if #input == 0 then if #input == 0 then
return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {} return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {}
@ -85,7 +87,14 @@ matcher.match = function(input, word, words)
return 0, {} return 0, {}
end 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 matches = {}
local input_start_index = 1 local input_start_index = 1
local input_end_index = 1 local input_end_index = 1
@ -105,6 +114,11 @@ matcher.match = function(input, word, words)
word_bound_index = word_bound_index + 1 word_bound_index = word_bound_index + 1
end end
-- Check partial matching.
if option.disallow_partial_matching and #matches > 1 then
return 0, {}
end
if #matches == 0 then if #matches == 0 then
return 0, {} return 0, {}
end 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 if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then
prefix = true prefix = true
else else
for _, w in ipairs(words or {}) do for _, synonym in ipairs(option.synonyms or {}) do
prefix = true prefix = true
local o = 1 local o = 1
for i = matches[1].input_match_start, matches[1].input_match_end do 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 prefix = false
break break
end end
@ -152,8 +166,10 @@ matcher.match = function(input, word, words)
-- Check remaining input as fuzzy -- Check remaining input as fuzzy
if matches[#matches].input_match_end < #input then if matches[#matches].input_match_end < #input then
if prefix and matcher.fuzzy(input, word, matches) then if not option.disallow_fuzzy_matching then
return score, matches if prefix and matcher.fuzzy(input, word, matches) then
return score, matches
end
end end
return 0, {} return 0, {}
end end

View File

@ -16,20 +16,37 @@ describe('matcher', function()
assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1) assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1)
assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all')) assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all'))
assert.is.truthy(matcher.match('Buffer', 'Buffer') > matcher.match('Buffer', 'buffer')) 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('fmodify', 'fnamemodify') >= 1)
assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single') >= 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('vi', 'void#') >= 1)
assert.is.truthy(matcher.match('vo', '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('usela', 'useLayoutEffect') > matcher.match('usela', 'useDataLayer'))
assert.is.truthy(matcher.match('true', 'v:true', { 'true' }) == matcher.match('true', 'true')) assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list'))
assert.is.truthy(matcher.match('g', 'get', { 'get' }) > matcher.match('g', 'dein#get', { 'dein#get' }))
assert.is.truthy(matcher.match('2', '[[2021') >= 1) 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) end)
it('debug', function() it('debug', function()

View File

@ -15,12 +15,13 @@ local char = require('cmp.utils.char')
---@field public source any ---@field public source any
---@field public cache cmp.Cache ---@field public cache cmp.Cache
---@field public revision number ---@field public revision number
---@field public context cmp.Context
---@field public incomplete boolean ---@field public incomplete boolean
---@field public is_triggered_by_symbol boolean ---@field public is_triggered_by_symbol boolean
---@field public entries cmp.Entry[] ---@field public entries cmp.Entry[]
---@field public offset number ---@field public offset number
---@field public request_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 status cmp.SourceStatus
---@field public complete_dedup function ---@field public complete_dedup function
local source = {} local source = {}
@ -50,21 +51,28 @@ source.reset = function(self)
self.cache:clear() self.cache:clear()
self.revision = self.revision + 1 self.revision = self.revision + 1
self.context = context.empty() self.context = context.empty()
self.request_offset = -1
self.is_triggered_by_symbol = false self.is_triggered_by_symbol = false
self.incomplete = false self.incomplete = false
self.entries = {} self.entries = {}
self.offset = -1 self.offset = -1
self.request_offset = -1
self.completion_context = nil
self.status = source.SourceStatus.WAITING self.status = source.SourceStatus.WAITING
self.complete_dedup(function() end) self.complete_dedup(function() end)
end end
---Return source option ---Return source config
---@return cmp.SourceConfig ---@return cmp.SourceConfig
source.get_config = function(self) source.get_source_config = function(self)
return config.get_source_config(self.name) or {} return config.get_source_config(self.name) or {}
end end
---Return matching config
---@return cmp.MatchingConfig
source.get_matching_config = function()
return config.get().matching
end
---Get fetching time ---Get fetching time
source.get_fetching_time = function(self) source.get_fetching_time = function(self)
if self.status == source.SourceStatus.FETCHING then 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) inputs[o] = string.sub(ctx.cursor_before_line, o)
end end
local match = e:match(inputs[o]) local match = e:match(inputs[o], self:get_matching_config())
e.score = match.score e.score = match.score
e.exact = false e.exact = false
if e.score >= 1 then if e.score >= 1 then
@ -112,7 +120,7 @@ source.get_entries = function(self, ctx)
end end
self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries) 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 = {} local limited_entries = {}
for _, e in ipairs(entries) do for _, e in ipairs(entries) do
table.insert(limited_entries, e) table.insert(limited_entries, e)
@ -183,17 +191,33 @@ source.is_available = function(self)
return true return true
end 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 ---Get keyword_pattern
---@return string ---@return string
source.get_keyword_pattern = function(self) source.get_keyword_pattern = function(self)
local c = self:get_config() local c = self:get_source_config()
if c.keyword_pattern then if c.keyword_pattern then
return c.keyword_pattern return c.keyword_pattern
end end
if self.source.get_keyword_pattern then if self.source.get_keyword_pattern then
return self.source:get_keyword_pattern({ return self.source:get_keyword_pattern(misc.copy(c))
option = self:get_config().opts,
})
end end
return config.get().completion.keyword_pattern return config.get().completion.keyword_pattern
end end
@ -201,48 +225,28 @@ end
---Get keyword_length ---Get keyword_length
---@return number ---@return number
source.get_keyword_length = function(self) source.get_keyword_length = function(self)
local c = self:get_config() local c = self:get_source_config()
if c.keyword_length then if c.keyword_length then
return c.keyword_length return c.keyword_length
end end
return config.get().completion.keyword_length or 1 return config.get().completion.keyword_length or 1
end 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 ---Invoke completion
---@param ctx cmp.Context ---@param ctx cmp.Context
---@param callback function ---@param callback function
---@return boolean Return true if not trigger completion. ---@return boolean Return true if not trigger completion.
source.complete = function(self, ctx, callback) source.complete = function(self, ctx, callback)
local offset = ctx:get_offset(self:get_keyword_pattern()) 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 = 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 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 = '' before_char = ''
end end
if string.match(before_char_iw, '^%a+$') then
before_char_iw = ''
end
end end
local completion_context local completion_context
@ -250,39 +254,31 @@ source.complete = function(self, ctx, callback)
completion_context = { completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.Invoked, triggerKind = types.lsp.CompletionTriggerKind.Invoked,
} }
else elseif vim.tbl_contains(self:get_trigger_characters(), before_char) then
if vim.tbl_contains(self:get_trigger_characters(), before_char) then completion_context = {
completion_context = { triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter,
triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, triggerCharacter = before_char,
triggerCharacter = before_char, }
} elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then
elseif vim.tbl_contains(self:get_trigger_characters(), before_char_iw) then if self:get_keyword_length() <= (ctx.cursor.col - offset) then
completion_context = { if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then
triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter, completion_context = {
triggerCharacter = before_char_iw, triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
} }
elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then
if self:get_keyword_length() <= (ctx.cursor.col - offset) then completion_context = {
if self.incomplete and self.context.cursor.col ~= ctx.cursor.col then triggerKind = types.lsp.CompletionTriggerKind.Invoked,
completion_context = { }
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
}
elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
}
end
end end
else else
self:reset() self:reset() -- Should clear current completion if the TriggerKind isn't TriggerCharacter or Manual and keyword length does not enough.
end end
else
self:reset() -- Should clear current completion if ContextReason is TriggerOnly and the triggerCharacter isn't matched
end end
-- Does not perform completions.
if not completion_context then 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 return
end end
@ -293,16 +289,16 @@ source.complete = function(self, ctx, callback)
debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context)) debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context))
local prev_status = self.status local prev_status = self.status
self.status = source.SourceStatus.FETCHING self.status = source.SourceStatus.FETCHING
self.request_offset = offset
self.offset = offset self.offset = offset
self.request_offset = offset
self.context = ctx self.context = ctx
self.completion_context = completion_context
self.source:complete( self.source:complete(
{ vim.tbl_extend('keep', misc.copy(self:get_source_config()), {
context = ctx,
offset = self.offset, offset = self.offset,
option = self:get_config().opts, context = ctx,
completion_context = completion_context, completion_context = completion_context,
}, }),
self.complete_dedup(vim.schedule_wrap(function(response) self.complete_dedup(vim.schedule_wrap(function(response)
response = response or {} response = response or {}
@ -329,8 +325,9 @@ source.complete = function(self, ctx, callback)
self.revision = self.revision + 1 self.revision = self.revision + 1
end end
else else
debug.log(self:get_debug_name(), 'continue', 'nil') -- The completion will be invoked when pressing <CR> if the trigger characters contain the <Space>.
if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then -- 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() self:reset()
end end
self.status = prev_status self.status = prev_status

View File

@ -38,6 +38,7 @@ cmp.ItemField.Menu = 'menu'
---@class cmp.ConfirmOption ---@class cmp.ConfirmOption
---@field public behavior cmp.ConfirmBehavior ---@field public behavior cmp.ConfirmBehavior
---@field public commit_character? string
---@class cmp.SelectOption ---@class cmp.SelectOption
---@field public behavior cmp.SelectBehavior ---@field public behavior cmp.SelectBehavior
@ -46,18 +47,21 @@ cmp.ItemField.Menu = 'menu'
---@field public body string ---@field public body string
---@field public insert_text_mode number ---@field public insert_text_mode number
---@class cmp.CompleteParams
---@field public reason? cmp.ContextReason
---@field public config? cmp.ConfigSchema
---@class cmp.Setup ---@class cmp.Setup
---@field public __call fun(c: cmp.ConfigSchema) ---@field public __call fun(c: cmp.ConfigSchema)
---@field public buffer fun(c: cmp.ConfigSchema) ---@field public buffer fun(c: cmp.ConfigSchema)
---@field public global fun(c: cmp.ConfigSchema) ---@field public global fun(c: cmp.ConfigSchema)
---@field public cmdline fun(type: string, c: cmp.ConfigSchema) ---@field public cmdline fun(type: string, c: cmp.ConfigSchema)
---@class cmp.SourceBaseApiParams ---@class cmp.SourceApiParams: cmp.SourceConfig
---@field public option table
---@class cmp.SourceCompletionApiParams : cmp.SourceBaseApiParams ---@class cmp.SourceCompletionApiParams : cmp.SourceConfig
---@field public context cmp.Context
---@field public offset number ---@field public offset number
---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext ---@field public completion_context lsp.CompletionContext
---@class cmp.Mapping ---@class cmp.Mapping
@ -73,11 +77,13 @@ cmp.ItemField.Menu = 'menu'
---@field public completion cmp.CompletionConfig ---@field public completion cmp.CompletionConfig
---@field public documentation cmp.DocumentationConfig|"false" ---@field public documentation cmp.DocumentationConfig|"false"
---@field public confirmation cmp.ConfirmationConfig ---@field public confirmation cmp.ConfirmationConfig
---@field public matching cmp.MatchingConfig
---@field public sorting cmp.SortingConfig ---@field public sorting cmp.SortingConfig
---@field public formatting cmp.FormattingConfig ---@field public formatting cmp.FormattingConfig
---@field public snippet cmp.SnippetConfig ---@field public snippet cmp.SnippetConfig
---@field public mapping table<string, cmp.Mapping> ---@field public mapping table<string, cmp.Mapping>
---@field public sources cmp.SourceConfig[] ---@field public sources cmp.SourceConfig[]
---@field public view cmp.ViewConfig
---@field public experimental cmp.ExperimentalConfig ---@field public experimental cmp.ExperimentalConfig
---@class cmp.CompletionConfig ---@class cmp.CompletionConfig
@ -98,6 +104,11 @@ cmp.ItemField.Menu = 'menu'
---@field public default_behavior cmp.ConfirmBehavior ---@field public default_behavior cmp.ConfirmBehavior
---@field public get_commit_characters fun(commit_characters: string[]): string[] ---@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 ---@class cmp.SortingConfig
---@field public priority_weight number ---@field public priority_weight number
---@field public comparators function[] ---@field public comparators function[]
@ -118,11 +129,28 @@ cmp.ItemField.Menu = 'menu'
---@class cmp.SourceConfig ---@class cmp.SourceConfig
---@field public name string ---@field public name string
---@field public opts table ---@field public option table|nil
---@field public priority number|nil ---@field public priority number|nil
---@field public keyword_pattern string ---@field public trigger_characters string[]|nil
---@field public keyword_length number ---@field public keyword_pattern string|nil
---@field public max_item_count number ---@field public keyword_length number|nil
---@field public group_index number ---@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 return cmp

View File

@ -26,7 +26,7 @@ lsp.Position.to_vim = function(buf, position)
} }
end end
---Convert lsp.Position to vim.Position ---Convert vim.Position to lsp.Position
---@param buf number|string ---@param buf number|string
---@param position vim.Position ---@param position vim.Position
---@return lsp.Position ---@return lsp.Position
@ -49,7 +49,7 @@ end
lsp.Range = {} lsp.Range = {}
---Convert lsp.Position to vim.Position ---Convert lsp.Range to vim.Range
---@param buf number|string ---@param buf number|string
---@param range lsp.Range ---@param range lsp.Range
---@return vim.Range ---@return vim.Range
@ -60,7 +60,7 @@ lsp.Range.to_vim = function(buf, range)
} }
end end
---Convert lsp.Position to vim.Position ---Convert vim.Range to lsp.Range
---@param buf number|string ---@param buf number|string
---@param range vim.Range ---@param range vim.Range
---@return lsp.Range ---@return lsp.Range
@ -97,7 +97,6 @@ lsp.InsertTextMode = vim.tbl_add_reverse_lookup(lsp.InsertTextMode)
lsp.MarkupKind = {} lsp.MarkupKind = {}
lsp.MarkupKind.PlainText = 'plaintext' lsp.MarkupKind.PlainText = 'plaintext'
lsp.MarkupKind.Markdown = 'markdown' lsp.MarkupKind.Markdown = 'markdown'
lsp.MarkupKind.Markdown = 'markdown'
lsp.MarkupKind = vim.tbl_add_reverse_lookup(lsp.MarkupKind) lsp.MarkupKind = vim.tbl_add_reverse_lookup(lsp.MarkupKind)
---@alias lsp.CompletionItemTag "1" ---@alias lsp.CompletionItemTag "1"

View File

@ -11,35 +11,36 @@ describe('types.lsp', function()
}) })
local vim_position, lsp_position 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.row, 2)
assert.are.equal(vim_position.col, 10) 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.line, 1)
assert.are.equal(lsp_position.character, 3) 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.row, 2)
assert.are.equal(vim_position.col, 1) 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.line, 1)
assert.are.equal(lsp_position.character, 0) 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.row, 2)
assert.are.equal(vim_position.col, 16) 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.line, 1)
assert.are.equal(lsp_position.character, 5) assert.are.equal(lsp_position.character, 5)
-- overflow (lsp -> vim) -- 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.row, 2)
assert.are.equal(vim_position.col, 16) assert.are.equal(vim_position.col, 16)
-- overflow(vim -> lsp) -- overflow(vim -> lsp)
vim_position.col = vim_position.col + 1 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.line, 1)
assert.are.equal(lsp_position.character, 5) assert.are.equal(lsp_position.character, 5)
end) end)

View File

@ -7,6 +7,9 @@
---@field public empty "1"|nil ---@field public empty "1"|nil
---@field public dup "1"|nil ---@field public dup "1"|nil
---@field public id any ---@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 ---@class vim.Position
---@field public row number ---@field public row number

View File

@ -53,7 +53,8 @@ end
api.get_screen_cursor = function() api.get_screen_cursor = function()
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
return api.get_cursor() local cursor = api.get_cursor()
return { cursor[1], cursor[2] + 1 }
end end
local cursor = api.get_cursor() local cursor = api.get_cursor()
local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1)

View File

@ -1,7 +1,9 @@
local async = {} local async = {}
---@class cmp.AsyncThrottle ---@class cmp.AsyncThrottle
---@field public running boolean
---@field public timeout number ---@field public timeout number
---@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil)
---@field public stop function ---@field public stop function
---@field public __call function ---@field public __call function
@ -12,7 +14,13 @@ async.throttle = function(fn, timeout)
local time = nil local time = nil
local timer = vim.loop.new_timer() local timer = vim.loop.new_timer()
return setmetatable({ return setmetatable({
running = false,
timeout = timeout, timeout = timeout,
sync = function(self, timeout_)
vim.wait(timeout_ or 1000, function()
return not self.running
end)
end,
stop = function() stop = function()
time = nil time = nil
timer:stop() timer:stop()
@ -24,12 +32,15 @@ async.throttle = function(fn, timeout)
if time == nil then if time == nil then
time = vim.loop.now() time = vim.loop.now()
end end
timer:stop()
local delta = math.max(1, self.timeout - (vim.loop.now() - time)) self.running = true
timer:start(delta, 0, function() timer:stop()
time = nil timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
fn(unpack(args)) vim.schedule(function()
time = nil
fn(unpack(args))
self.running = false
end)
end) end)
end, end,
}) })

View File

@ -1,17 +1,30 @@
local buffer = {} local buffer = {}
buffer.ensure = setmetatable({ buffer.cache = {}
cache = {},
}, { ---@return number buf
__call = function(self, name) buffer.get = function(name)
if not (self.cache[name] and vim.api.nvim_buf_is_valid(self.cache[name])) then local buf = buffer.cache[name]
local buf = vim.api.nvim_create_buf(false, true) if buf and vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') return buf
vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') else
self.cache[name] = buf return nil
end end
return self.cache[name] 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 return buffer

View File

@ -23,15 +23,6 @@ describe('feedkeys', function()
}) })
end) 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() it('testability', function()
feedkeys.call('i', 'n', function() feedkeys.call('i', 'n', function()
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()

View File

@ -10,7 +10,7 @@ highlight.keys = {
} }
highlight.inherit = function(name, source, override) 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 for _, key in ipairs(highlight.keys) do
if override[key] then if override[key] then
cmd = cmd .. (' %s=%s'):format(key, override[key]) cmd = cmd .. (' %s=%s'):format(key, override[key])

View File

@ -7,7 +7,9 @@ local keymap = {}
---@param keys string ---@param keys string
---@return string ---@return string
keymap.t = function(keys) 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 end
---Normalize key sequence. ---Normalize key sequence.
@ -65,6 +67,9 @@ end
---@param count number ---@param count number
---@return string ---@return string
keymap.backspace = function(count) keymap.backspace = function(count)
if type(count) == 'string' then
count = vim.fn.strchars(count, true)
end
if count <= 0 then if count <= 0 then
return '' return ''
end end
@ -73,16 +78,11 @@ keymap.backspace = function(count)
return table.concat(keys, '') return table.concat(keys, '')
end end
---Create autoindent keys ---Update indentkeys.
---@param expr string
---@return string ---@return string
keymap.autoindent = function() keymap.indentkeys = function(expr)
local keys = {} return string.format(keymap.t('<Cmd>set indentkeys=%s<CR>'), expr and vim.fn.escape(expr, '| \t\\') or '')
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, '')
end end
---Return two key sequence are equal or not. ---Return two key sequence are equal or not.
@ -97,25 +97,22 @@ end
keymap.listen = function(mode, lhs, callback) keymap.listen = function(mode, lhs, callback)
lhs = keymap.normalize(keymap.to_keymap(lhs)) 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+)%)') 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 if id and keymap.set_map.callbacks[tonumber(id, 10)] then
return return
end end
local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1 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() keymap.set_map(bufnr, mode, lhs, function()
if mode == 'c' and vim.fn.getcmdtype() == '=' then local ignore = false
return vim.api.nvim_feedkeys(keymap.t(fallback.keys), fallback.mode, true) ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=')
if ignore then
fallback()
else
callback(lhs, misc.once(fallback))
end end
callback(
lhs,
misc.once(function()
vim.api.nvim_feedkeys(keymap.t(fallback.keys), fallback.mode, true)
end)
)
end, { end, {
expr = false, expr = false,
noremap = true, noremap = true,
@ -123,19 +120,66 @@ keymap.listen = function(mode, lhs, callback)
}) })
end 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 mode string
---@param lhs string ---@param lhs string
---@return table ---@return table
keymap.get_mapping = function(mode, lhs) keymap.get_map = function(mode, lhs)
lhs = keymap.normalize(lhs) lhs = keymap.normalize(lhs)
for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do
if keymap.equals(map.lhs, lhs) then if keymap.equals(map.lhs, lhs) then
return { return {
lhs = map.lhs, lhs = map.lhs,
rhs = map.rhs, rhs = map.rhs or '',
expr = map.expr == 1, expr = map.expr == 1,
callback = map.callback,
noremap = map.noremap == 1, noremap = map.noremap == 1,
script = map.script == 1, script = map.script == 1,
silent = map.silent == 1, silent = map.silent == 1,
@ -149,8 +193,9 @@ keymap.get_mapping = function(mode, lhs)
if keymap.equals(map.lhs, lhs) then if keymap.equals(map.lhs, lhs) then
return { return {
lhs = map.lhs, lhs = map.lhs,
rhs = map.rhs, rhs = map.rhs or '',
expr = map.expr == 1, expr = map.expr == 1,
callback = map.callback,
noremap = map.noremap == 1, noremap = map.noremap == 1,
script = map.script == 1, script = map.script == 1,
silent = map.silent == 1, silent = map.silent == 1,
@ -164,80 +209,15 @@ keymap.get_mapping = function(mode, lhs)
lhs = lhs, lhs = lhs,
rhs = lhs, rhs = lhs,
expr = false, expr = false,
callback = nil,
noremap = true, noremap = true,
script = false, script = false,
silent = false, silent = true,
nowait = false, nowait = false,
buffer = false, buffer = false,
} }
end 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 ---Set keymapping
keymap.set_map = setmetatable({ keymap.set_map = setmetatable({
callbacks = {}, callbacks = {},

View File

@ -1,68 +1,159 @@
local spec = require('cmp.utils.spec') local spec = require('cmp.utils.spec')
local api = require('cmp.utils.api')
local feedkeys = require('cmp.utils.feedkeys')
local keymap = require('cmp.utils.keymap') local keymap = require('cmp.utils.keymap')
describe('keymap', function() describe('keymap', function()
before_each(spec.before) 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() it('to_keymap', function()
assert.are.equal(keymap.to_keymap('\n'), '<CR>') assert.are.equal(keymap.to_keymap('\n'), '<CR>')
assert.are.equal(keymap.to_keymap('<CR>'), '<CR>') assert.are.equal(keymap.to_keymap('<CR>'), '<CR>')
assert.are.equal(keymap.to_keymap('|'), '<Bar>') assert.are.equal(keymap.to_keymap('|'), '<Bar>')
end) end)
describe('evacuate', function() describe('fallback', function()
before_each(spec.before) before_each(spec.before)
it('expr & register', function() local run_fallback = function(keys, fallback)
vim.api.nvim_buf_set_keymap(0, 'i', '(', [['<C-r>="("<CR>']], { local state = {}
expr = true, feedkeys.call(keys, '', function()
noremap = false, fallback()
})
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))
end) end)
it('false', function() feedkeys.call('', '', function()
vim.api.nvim_buf_set_keymap(0, 'i', '<Tab>', [[v:false ? '<C-r>="foobar"<CR>' : '<Tab>aiueo']], { 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, expr = true,
noremap = false, noremap = false,
silent = true,
callback = function()
return '()' .. keymap.t('<Left>')
end,
}) })
local fallback = keymap.evacuate(0, 'i', '<Tab>') local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
vim.api.nvim_feedkeys('i' .. keymap.t(fallback.keys), fallback.mode .. 'x', true) local state = run_fallback('i', fallback)
assert.are.same({ '\taiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) 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) end)
end) end)
describe('realworld', function() describe('realworld', function()
before_each(spec.before) before_each(spec.before)
it('#226', function() it('#226', function()
keymap.listen('i', '<c-n>', function(_, fallback) keymap.listen('i', '<c-n>', function(_, fallback)
fallback() fallback()
@ -70,6 +161,7 @@ describe('keymap', function()
vim.api.nvim_feedkeys(keymap.t('iaiueo<CR>a<C-n><C-n>'), 'tx', true) 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)) assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end) end)
it('#414', function() it('#414', function()
keymap.listen('i', '<M-j>', function() keymap.listen('i', '<M-j>', function()
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'int', true) 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) 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)) assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end) 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)
end) end)

View File

@ -29,6 +29,28 @@ misc.concat = function(list1, list2)
return new_list return new_list
end 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. ---The symbol to remove key in misc.merge.
misc.none = vim.NIL misc.none = vim.NIL
@ -77,7 +99,7 @@ misc.id = setmetatable({
group = {}, group = {},
}, { }, {
__call = function(_, 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 misc.id.group[group] = misc.id.group[group] + 1
return misc.id.group[group] return misc.id.group[group]
end, end,
@ -144,9 +166,10 @@ end
---Safe version of vim.str_utfindex ---Safe version of vim.str_utfindex
---@param text string ---@param text string
---@param vimindex number ---@param vimindex number|nil
---@return number ---@return number
misc.to_utfindex = function(text, vimindex) misc.to_utfindex = function(text, vimindex)
vimindex = vimindex or #text + 1
return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text)))
end end
@ -155,6 +178,7 @@ end
---@param utfindex number ---@param utfindex number
---@return number ---@return number
misc.to_vimindex = function(text, utfindex) misc.to_vimindex = function(text, utfindex)
utfindex = utfindex or #text
for i = utfindex, 1, -1 do for i = utfindex, 1, -1 do
local s, v = pcall(function() local s, v = pcall(function()
return vim.str_byteindex(text, i) + 1 return vim.str_byteindex(text, i) + 1
@ -178,4 +202,34 @@ misc.deprecated = function(fn, msg)
end end
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 return misc

View File

@ -3,24 +3,29 @@ local pattern = require('cmp.utils.pattern')
local str = {} local str = {}
local INVALID_CHARS = {} local INVALIDS = {}
INVALID_CHARS[string.byte("'")] = true INVALIDS[string.byte("'")] = true
INVALID_CHARS[string.byte('"')] = true INVALIDS[string.byte('"')] = true
INVALID_CHARS[string.byte('=')] = true INVALIDS[string.byte('=')] = true
INVALID_CHARS[string.byte('$')] = true INVALIDS[string.byte('$')] = true
INVALID_CHARS[string.byte('(')] = true INVALIDS[string.byte('(')] = true
INVALID_CHARS[string.byte('[')] = true INVALIDS[string.byte('[')] = true
INVALID_CHARS[string.byte(' ')] = true INVALIDS[string.byte('<')] = true
INVALID_CHARS[string.byte('\t')] = true INVALIDS[string.byte('{')] = true
INVALID_CHARS[string.byte('\n')] = true INVALIDS[string.byte(' ')] = true
INVALID_CHARS[string.byte('\r')] = true INVALIDS[string.byte('\t')] = true
INVALIDS[string.byte('\n')] = true
INVALIDS[string.byte('\r')] = true
local NR_BYTE = string.byte('\n') local NR_BYTE = string.byte('\n')
local PAIR_CHARS = {} local PAIRS = {}
PAIR_CHARS[string.byte('[')] = string.byte(']') PAIRS[string.byte('<')] = string.byte('>')
PAIR_CHARS[string.byte('(')] = string.byte(')') PAIRS[string.byte('[')] = string.byte(']')
PAIR_CHARS[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 ---Return if specified text has prefix or not
---@param text string ---@param text string
@ -38,6 +43,17 @@ str.has_prefix = function(text, prefix)
return true return true
end 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 ---Remove suffix
---@param text string ---@param text string
---@param suffix string ---@param suffix string
@ -101,23 +117,47 @@ end
---get_word ---get_word
---@param text string ---@param text string
---@param stop_char number
---@param min_length number
---@return string ---@return string
str.get_word = function(text, stop_char) str.get_word = function(text, stop_char, min_length)
local valids = {} min_length = min_length or 0
local has_valid = false
for idx = 1, #text do local has_alnum = false
local c = string.byte(text, idx) local stack = {}
local invalid = INVALID_CHARS[c] and not (valids[c] and stop_char ~= c) local word = {}
if has_valid and invalid then local add = function(c)
return string.sub(text, 1, idx - 1) 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 end
valids[c] = true
if PAIR_CHARS[c] then
valids[PAIR_CHARS[c]] = true
end
has_valid = has_valid or not invalid
end 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 end
---Oneline ---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('print()'), 'print')
assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]') 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":', 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) end)
it('strikethrough', function() it('strikethrough', function()

View File

@ -18,6 +18,7 @@ local api = require('cmp.utils.api')
---@field public swin2 number|nil ---@field public swin2 number|nil
---@field public style cmp.WindowStyle ---@field public style cmp.WindowStyle
---@field public opt table<string, any> ---@field public opt table<string, any>
---@field public buffer_opt table<string, any>
---@field public cache cmp.Cache ---@field public cache cmp.Cache
local window = {} local window = {}
@ -32,6 +33,7 @@ window.new = function()
self.style = {} self.style = {}
self.cache = cache.new() self.cache = cache.new()
self.opt = {} self.opt = {}
self.buffer_opt = {}
return self return self
end end
@ -54,6 +56,26 @@ window.option = function(self, key, value)
end end
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. ---Set style.
---@param style cmp.WindowStyle ---@param style cmp.WindowStyle
window.set_style = function(self, style) window.set_style = function(self, style)
@ -70,7 +92,13 @@ end
---Return buffer id. ---Return buffer id.
---@return number ---@return number
window.get_buffer = function(self) 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 end
---Open window ---Open window
@ -89,7 +117,7 @@ window.open = function(self, style)
else else
local s = misc.copy(self.style) local s = misc.copy(self.style)
s.noautocmd = true 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 for k, v in pairs(self.opt) do
vim.api.nvim_win_set_option(self.win, k, v) vim.api.nvim_win_set_option(self.win, k, v)
end end
@ -148,7 +176,7 @@ window.update = function(self)
-- In cmdline, vim does not redraw automatically. -- In cmdline, vim does not redraw automatically.
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
vim.api.nvim_win_call(self.win, function() vim.api.nvim_win_call(self.win, function()
vim.cmd([[redraw]]) misc.redraw()
end) end)
end end
end end
@ -251,9 +279,14 @@ window.get_content_height = function(self)
vim.api.nvim_buf_get_changedtick(self:get_buffer()), vim.api.nvim_buf_get_changedtick(self:get_buffer()),
}, function() }, function()
local height = 0 local height = 0
for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do local buf = self:get_buffer()
height = height + math.ceil(math.max(1, vim.str_utfindex(text)) / self.style.width) -- The result of vim.fn.strdisplaywidth depends on the buffer it was called
end -- 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 return height
end) end)
end end

View File

@ -4,6 +4,7 @@ local event = require('cmp.utils.event')
local keymap = require('cmp.utils.keymap') local keymap = require('cmp.utils.keymap')
local docs_view = require('cmp.view.docs_view') local docs_view = require('cmp.view.docs_view')
local custom_entries_view = require('cmp.view.custom_entries_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 native_entries_view = require('cmp.view.native_entries_view')
local ghost_text_view = require('cmp.view.ghost_text_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 resolve_dedup cmp.AsyncDedup
---@field private native_entries_view cmp.NativeEntriesView ---@field private native_entries_view cmp.NativeEntriesView
---@field private custom_entries_view cmp.CustomEntriesView ---@field private custom_entries_view cmp.CustomEntriesView
---@field private wildmenu_entries_view cmp.CustomEntriesView
---@field private change_dedup cmp.AsyncDedup ---@field private change_dedup cmp.AsyncDedup
---@field private docs_view cmp.DocsView ---@field private docs_view cmp.DocsView
---@field private ghost_text_view cmp.GhostTextView ---@field private ghost_text_view cmp.GhostTextView
@ -23,6 +25,7 @@ view.new = function()
self.resolve_dedup = async.dedup() self.resolve_dedup = async.dedup()
self.custom_entries_view = custom_entries_view.new() self.custom_entries_view = custom_entries_view.new()
self.native_entries_view = native_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.docs_view = docs_view.new()
self.ghost_text_view = ghost_text_view.new() self.ghost_text_view = ghost_text_view.new()
self.event = event.new() self.event = event.new()
@ -47,7 +50,7 @@ end
view.open = function(self, ctx, sources) view.open = function(self, ctx, sources)
local source_group_map = {} local source_group_map = {}
for _, s in ipairs(sources) do 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 if not source_group_map[group_index] then
source_group_map[group_index] = {} source_group_map[group_index] = {}
end end
@ -77,10 +80,10 @@ view.open = function(self, ctx, sources)
-- create filtered entries. -- create filtered entries.
local offset = ctx.cursor.col local offset = ctx.cursor.col
for i, s in ipairs(source_group) do 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 if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then
-- source order priority bonus. -- 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 for _, e in ipairs(s:get_entries(ctx)) do
e.score = e.score + priority e.score = e.score + priority
@ -109,7 +112,7 @@ view.open = function(self, ctx, sources)
end end
end end
-- close. -- complete_done.
if #entries == 0 then if #entries == 0 then
self:close() self:close()
end end
@ -117,6 +120,11 @@ end
---Close menu ---Close menu
view.close = function(self) 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:_get_entries_view():close()
self.docs_view:close() self.docs_view:close()
self.ghost_text_view:hide() self.ghost_text_view:hide()
@ -153,6 +161,17 @@ view.select_prev_item = function(self, option)
self:_get_entries_view():select_prev_item(option) self:_get_entries_view():select_prev_item(option)
end 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 ---Get first entry
---@param self cmp.Entry|nil ---@param self cmp.Entry|nil
view.get_first_entry = function(self) view.get_first_entry = function(self)
@ -174,54 +193,51 @@ end
---Return current configured entries_view ---Return current configured entries_view
---@return cmp.CustomEntriesView|cmp.NativeEntriesView ---@return cmp.CustomEntriesView|cmp.NativeEntriesView
view._get_entries_view = function(self) view._get_entries_view = function(self)
local c = config.get()
self.native_entries_view.event:clear() self.native_entries_view.event:clear()
self.custom_entries_view.event:clear() self.custom_entries_view.event:clear()
self.wildmenu_entries_view.event:clear()
if c.experimental.native_menu then local c = config.get()
self.native_entries_view.event:on('change', function() local v = self.custom_entries_view
self:on_entry_change() if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then
end) v = self.wildmenu_entries_view
return self.native_entries_view elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then
else v = self.native_entries_view
self.custom_entries_view.event:on('change', function()
self:on_entry_change()
end)
return self.custom_entries_view
end end
v.event:on('change', function()
self:on_entry_change()
end)
return v
end end
---On entry change ---On entry change
view.on_entry_change = async.throttle( view.on_entry_change = async.throttle(function(self)
vim.schedule_wrap(function(self) if not self:visible() then
if not self:visible() then return
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 end
local e = self:get_selected_entry() e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
if e then if not self:visible() then
for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do return
keymap.listen('i', c, function(...)
self.event:emit('keymap', ...)
end)
end end
e:resolve(vim.schedule_wrap(self.resolve_dedup(function() self.docs_view:open(e, self:_get_entries_view():info())
if not self:visible() then end)))
return else
end self.docs_view:close()
self.docs_view:open(e, self:_get_entries_view():info()) end
end)))
else
self.docs_view:close()
end
e = e or self:get_first_entry() e = e or self:get_first_entry()
if e then if e then
self.ghost_text_view:show(e) self.ghost_text_view:show(e)
else else
self.ghost_text_view:hide() self.ghost_text_view:hide()
end end
end), end, 20)
20
)
return view return view

View File

@ -5,6 +5,7 @@ vim.g.loaded_cmp = true
local api = require "cmp.utils.api" local api = require "cmp.utils.api"
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
local types = require('cmp.types')
local config = require('cmp.config') local config = require('cmp.config')
local highlight = require('cmp.utils.highlight') local highlight = require('cmp.utils.highlight')
@ -20,11 +21,12 @@ vim.cmd [[
autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone')
autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme() autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme()
autocmd CmdlineEnter * call v:lua.cmp.plugin.cmdline.enter() 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 augroup END
]] ]]
misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function() misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function()
if config.get().experimental.native_menu then if config.is_native_menu() then
return return
end end
if vim.fn.expand('<afile>')~= '=' then if vim.fn.expand('<afile>')~= '=' then
@ -78,6 +80,11 @@ misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function()
guibg = 'NONE', guibg = 'NONE',
ctermbg = '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', { highlight.inherit('CmpItemMenuDefault', 'Pmenu', {
guibg = 'NONE', guibg = 'NONE',
ctermbg = 'NONE', ctermbg = 'NONE',
@ -86,32 +93,40 @@ end)
_G.cmp.plugin.colorscheme() _G.cmp.plugin.colorscheme()
if vim.fn.hlexists('CmpItemAbbr') ~= 1 then if vim.fn.hlexists('CmpItemAbbr') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbr CmpItemAbbrDefault]] vim.cmd [[highlight default link CmpItemAbbr CmpItemAbbrDefault]]
end end
if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]] vim.cmd [[highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]]
end end
if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]] vim.cmd [[highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]]
end end
if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]] vim.cmd [[highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]]
end end
if vim.fn.hlexists('CmpItemKind') ~= 1 then 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 end
if vim.fn.hlexists('CmpItemMenu') ~= 1 then if vim.fn.hlexists('CmpItemMenu') ~= 1 then
vim.cmd [[highlight! default link CmpItemMenu CmpItemMenuDefault]] vim.cmd [[highlight default link CmpItemMenu CmpItemMenuDefault]]
end end
vim.cmd [[command! CmpStatus lua require('cmp').status()]] 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 if vim.on_key then
vim.on_key(function(keys) vim.on_key(function(keys)