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

perf(cmp): update nvim-cmp

This commit is contained in:
wsdjeg 2023-06-08 21:15:37 +08:00
parent e9b5e713f9
commit 913901272d
56 changed files with 2636 additions and 1644 deletions

View File

@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [hrsh7th]

View File

@ -7,24 +7,17 @@ body:
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.
- label: I have checked the [FAQ](https://github.com/hrsh7th/nvim-cmp/blob/main/doc/cmp.txt) and it didn't resolve my problem.
required: true
- type: checkboxes
id: issue-prerequisite
id: announcement-prerequisite
attributes:
label: Issues
label: Announcement
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.
- label: I have checked [Breaking change announcement](https://github.com/hrsh7th/nvim-cmp/issues/231).
required: true
- type: input
attributes:
label: "Neovim Version"
description: "`nvim --version`:"
validations:
required: true
- type: textarea
attributes:
label: "Minimal reproducible full config"
@ -34,6 +27,10 @@ body:
2. Edit `~/cmp-repro.vim` for reproducing the issue
3. Open `nvim -u ~/cmp-repro.vim`
4. Check reproduction step
value: |
```vim
```
validations:
required: true

View File

@ -0,0 +1,25 @@
name: format
on:
push:
branches:
- main
paths:
- '**.lua'
jobs:
postprocessing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Format with Stylua
uses: JohnnyMorganz/stylua-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: v0.16.1
args: ./lua
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Format with stylua"

View File

@ -16,13 +16,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Setup rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
default: true
override: true
- name: Setup neovim
uses: rhysd/action-setup-vim@v1
with:
@ -40,12 +33,9 @@ jobs:
- name: Setup tools
shell: bash
run: |
sudo apt install -y curl unzip --no-install-recommends
bash ./utils/install_stylua.sh
luarocks install luacheck
luarocks install vusted
- name: Run tests
shell: bash
run: make integration

View File

@ -0,0 +1,19 @@
name: "release"
on:
push:
tags:
- 'v*'
jobs:
luarocks-upload:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v3
env:
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
with:
detailed_description: |
A completion engine plugin for neovim written in Lua.
Completion sources are installed from external repositories and "sourced".

View File

@ -1,7 +1,3 @@
.PHONY: fmt
fmt:
stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua
.PHONY: lint
lint:
luacheck ./lua
@ -12,13 +8,10 @@ test:
.PHONY: pre-commit
pre-commit:
./utils/stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua
luacheck lua
vusted lua
.PHONY: integration
integration:
./utils/stylua --config-path stylua.toml --check --glob 'lua/**/*.lua' -- lua
luacheck lua
vusted lua

View File

@ -3,34 +3,31 @@
A completion engine plugin for neovim written in Lua.
Completion sources are installed from external repositories and "sourced".
<video src="https://user-images.githubusercontent.com/629908/139000570-3ac39587-a88b-43c6-b35e-207489719359.mp4" width="100%"></video>
https://github.com/hrsh7th/nvim-cmp/assets/22756295/afa70011-9121-4e42-aedd-0153b630eeab
Readme!
====================
1. nvim-cmp's breaking changes are documented [here](https://github.com/hrsh7th/nvim-cmp/issues/231).
1. There is a GitHub issue that documents [breaking changes](https://github.com/hrsh7th/nvim-cmp/issues/231) for nvim-cmp. Subscribe to the issue to be notified of upcoming breaking changes.
2. This is my hobby project. You can support me via GitHub sponsors.
3. Bug reports are welcome, but I might not fix if you don't provide a minimal reproduction configuration and steps.
4. The nvim-cmp documents is [here](./doc/cmp.txt).
3. Bug reports are welcome, but don't expect a fix unless you provide minimal configuration and steps to reproduce your issue.
4. The `cmp.mapping.preset.*` is pre-defined configuration that aims to mimic neovim's native like behavior. It can be changed without announcement. Please manage key-mapping by yourself.
Concept
====================
- Full support for LSP completion related capabilities
- Powerful customizability via Lua functions
- Smart handling of key mapping
- Smart handling of key mappings
- No flicker
Setup
====================
### Recommended Configuration
This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as snippet plugin.
This example configuration uses `vim-plug` as the plugin manager and `vim-vsnip` as a snippet plugin.
```lua
call plug#begin(s:plug_dir)
@ -59,10 +56,8 @@ Plug 'hrsh7th/vim-vsnip'
call plug#end()
set completeopt=menu,menuone,noselect
lua <<EOF
-- Setup nvim-cmp.
-- Set up nvim-cmp.
local cmp = require'cmp'
cmp.setup({
@ -75,17 +70,17 @@ lua <<EOF
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
end,
},
mapping = {
['<C-b>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }),
['<C-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }),
['<C-Space>'] = cmp.mapping(cmp.mapping.complete(), { 'i', 'c' }),
['<C-y>'] = cmp.config.disable, -- Specify `cmp.config.disable` if you want to remove the default `<C-y>` mapping.
['<C-e>'] = cmp.mapping({
i = cmp.mapping.abort(),
c = cmp.mapping.close(),
}),
['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
window = {
-- completion = cmp.config.window.bordered(),
-- documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users.
@ -100,14 +95,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 = 'git' }, -- You can specify the `git` source if [you were installed it](https://github.com/petertriho/cmp-git).
}, {
{ name = 'buffer' },
})
})
-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', {
-- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline({ '/', '?' }, {
mapping = cmp.mapping.preset.cmdline(),
sources = {
{ name = 'buffer' }
}
@ -115,6 +111,7 @@ lua <<EOF
-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = 'path' }
}, {
@ -122,8 +119,8 @@ lua <<EOF
})
})
-- Setup lspconfig.
local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities())
-- Set up lspconfig.
local capabilities = require('cmp_nvim_lsp').default_capabilities()
-- Replace <YOUR_LSP_SERVER> with each lsp server you've enabled.
require('lspconfig')['<YOUR_LSP_SERVER>'].setup {
capabilities = capabilities
@ -131,85 +128,12 @@ lua <<EOF
EOF
```
### Where can I find more completion sources?
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).
Have a look at the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki/List-of-sources) and the `nvim-cmp` [GitHub topic](https://github.com/topics/nvim-cmp).
### 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).
Advanced configuration example
====================
### Use nvim-cmp as smart omnifunc handler.
nvim-cmp can be used as flexible omnifunc manager.
```lua
local cmp = require('cmp')
cmp.setup {
completion = {
autocomplete = false, -- disable auto-completion.
},
}
_G.vimrc = _G.vimrc or {}
_G.vimrc.cmp = _G.vimrc.cmp or {}
_G.vimrc.cmp.lsp = function()
cmp.complete({
config = {
sources = {
{ name = 'nvim_lsp' }
}
}
})
end
_G.vimrc.cmp.snippet = function()
cmp.complete({
config = {
sources = {
{ name = 'vsnip' }
}
}
})
end
vim.cmd([[
inoremap <C-x><C-o> <Cmd>lua vimrc.cmp.lsp()<CR>
inoremap <C-x><C-s> <Cmd>lua vimrc.cmp.snippet()<CR>
]])
```
### Full managed completion behavior.
```lua
local cmp = require('cmp')
cmp.setup {
completion = {
autocomplete = false, -- disable auto-completion.
}
}
_G.vimrc = _G.vimrc or {}
_G.vimrc.cmp = _G.vimrc.cmp or {}
_G.vimrc.cmp.on_text_changed = function()
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local before = string.sub(line, 1, cursor[2] + 1)
if before:match('%s*$') then
cmp.complete() -- Trigger completion only if the cursor is placed at the end of line.
end
end
vim.cmd([[
augroup vimrc
autocmd
autocmd TextChanged,TextChangedI,TextChangedP * call luaeval('vimrc.cmp.on_text_changed()')
augroup END
]])
```
See the [Wiki](https://github.com/hrsh7th/nvim-cmp/wiki).

View File

@ -6,7 +6,16 @@ let s:sources = {}
"
function! cmp#register_source(name, source) abort
let l:methods = []
for l:method in ['is_available', 'get_debug_name', 'get_trigger_characters', 'get_keyword_pattern', 'complete', 'execute', 'resolve']
for l:method in [
\ 'is_available',
\ 'get_debug_name',
\ 'get_position_encoding_kind',
\ 'get_trigger_characters',
\ 'get_keyword_pattern',
\ 'complete',
\ 'execute',
\ 'resolve'
\ ]
if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func
call add(l:methods, l:method)
endif
@ -39,6 +48,8 @@ function! cmp#_method(bridge_id, method, args) abort
return l:source[a:method]()
elseif a:method ==# 'get_debug_name'
return l:source[a:method]()
elseif a:method ==# 'get_position_encoding_kind'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_keyword_pattern'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_trigger_characters'

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ config.cache = cache.new()
---@type cmp.ConfigSchema
config.global = require('cmp.config.default')()
---@type table<number, cmp.ConfigSchema>
---@type table<integer, cmp.ConfigSchema>
config.buffers = {}
---@type table<string, cmp.ConfigSchema>
@ -29,14 +29,14 @@ config.onetime = {}
---Set configuration for global.
---@param c cmp.ConfigSchema
config.set_global = function(c)
config.global = misc.merge(config.normalize(c), config.normalize(config.global))
config.global = config.normalize(misc.merge(c, config.global))
config.global.revision = config.global.revision or 1
config.global.revision = config.global.revision + 1
end
---Set configuration for buffer
---@param c cmp.ConfigSchema
---@param bufnr number|nil
---@param bufnr integer
config.set_buffer = function(c, bufnr)
local revision = (config.buffers[bufnr] or {}).revision or 1
config.buffers[bufnr] = c or {}
@ -56,11 +56,13 @@ end
---Set configuration for cmdline
---@param c cmp.ConfigSchema
---@param cmdtype string
config.set_cmdline = function(c, cmdtype)
local revision = (config.cmdline[cmdtype] or {}).revision or 1
config.cmdline[cmdtype] = c or {}
config.cmdline[cmdtype].revision = revision + 1
---@param cmdtypes string|string[]
config.set_cmdline = function(c, cmdtypes)
for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do
local revision = (config.cmdline[cmdtype] or {}).revision or 1
config.cmdline[cmdtype] = c or {}
config.cmdline[cmdtype].revision = revision + 1
end
end
---Set configuration as oneshot completion.
@ -74,7 +76,9 @@ end
---@return cmp.ConfigSchema
config.get = function()
local global_config = config.global
if config.onetime.sources then
-- The config object already has `revision` key.
if #vim.tbl_keys(config.onetime) > 1 then
local onetime_config = config.onetime
return config.cache:ensure({
'get',
@ -82,7 +86,10 @@ config.get = function()
global_config.revision or 0,
onetime_config.revision or 0,
}, function()
return misc.merge(config.normalize(onetime_config), config.normalize(global_config))
local c = {}
c = misc.merge(c, config.normalize(onetime_config))
c = misc.merge(c, config.normalize(global_config))
return c
end)
elseif api.is_cmdline_mode() then
local cmdtype = vim.fn.getcmdtype()
@ -94,7 +101,10 @@ config.get = function()
cmdtype,
cmdline_config.revision or 0,
}, function()
return misc.merge(config.normalize(cmdline_config), config.normalize(global_config))
local c = {}
c = misc.merge(c, config.normalize(cmdline_config))
c = misc.merge(c, config.normalize(global_config))
return c
end)
else
local bufnr = vim.api.nvim_get_current_buf()
@ -111,9 +121,9 @@ config.get = function()
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))
c = misc.merge(config.normalize(c), config.normalize(buffer_config))
c = misc.merge(config.normalize(c), config.normalize(filetype_config))
c = misc.merge(config.normalize(c), config.normalize(global_config))
return c
end)
end
@ -144,9 +154,6 @@ 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
@ -154,12 +161,14 @@ config.is_native_menu = function()
end
---Normalize mapping key
---@param c cmp.ConfigSchema
---@param c any
---@return cmp.ConfigSchema
config.normalize = function(c)
-- make sure c is not 'nil'
---@type any
c = c == nil and {} or c
-- Normalize mapping.
if c.mapping then
local normalized = {}
for k, v in pairs(c.mapping) do
@ -168,6 +177,7 @@ config.normalize = function(c)
c.mapping = normalized
end
-- Notice experimental.native_menu.
if c.experimental and c.experimental.native_menu then
vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' },
@ -182,6 +192,21 @@ config.normalize = function(c)
c.view.entries = c.view.entries or 'native'
end
-- Notice documentation.
if c.documentation ~= nil then
vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' },
{ 'documentation', 'WarningMsg' },
{ ' is deprecated.\n', 'Normal' },
{ '[nvim-cmp] Please use ', 'Normal' },
{ 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' },
{ ' instead.', 'Normal' },
}, true, {})
c.window = c.window or {}
c.window.documentation = c.documentation
end
-- Notice sources.[n].opts
if c.sources then
for _, s in ipairs(c.sources) do
if s.opts and not s.option then

View File

@ -1,6 +1,5 @@
local types = require('cmp.types')
local cache = require('cmp.utils.cache')
local misc = require('cmp.utils.misc')
local compare = {}
@ -71,7 +70,7 @@ end
-- sortText
compare.sort_text = function(entry1, entry2)
if misc.safe(entry1.completion_item.sortText) and misc.safe(entry2.completion_item.sortText) then
if entry1.completion_item.sortText and entry2.completion_item.sortText then
local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText)
if diff < 0 then
return true
@ -108,7 +107,7 @@ compare.locality = setmetatable({
locality_map = {},
update = function(self)
local config = require('cmp').get_config()
if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then
if not vim.tbl_contains(config.sorting.comparators, compare.locality) then
return
end
@ -132,7 +131,7 @@ compare.locality = setmetatable({
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)
local d = math.abs(i - cursor_row) - (is_above and 1 or 0)
locality_map[w] = math.min(locality_map[w] or math.huge, d)
buffer = string.sub(buffer, e + 1)
else
@ -145,7 +144,7 @@ compare.locality = setmetatable({
self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row))
end
end
end
end,
}, {
__call = function(self, entry1, entry2)
local local1 = self.locality_map[entry1:get_word()]
@ -159,7 +158,7 @@ compare.locality = setmetatable({
end
return local1 < local2
end
end
end,
})
-- scopes
@ -175,7 +174,6 @@ compare.scopes = setmetatable({
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
@ -205,7 +203,8 @@ compare.scopes = setmetatable({
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]
local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text
local text = get_node_text(definition.node, buf) or ''
if not self.scopes_map[text] then
self.scopes_map[text] = depth
end

View File

@ -1,65 +1,60 @@
local api = require('cmp.utils.api')
local context = {}
---Check if cursor is in syntax group
---@param group string
---@param group string | []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
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
if not api.is_insert_mode() then
col = col + 1
end
for _, syn_id in ipairs(vim.fn.synstack(row, col)) do
syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links
if vim.fn.synIDattr(syn_id, 'name') == group then
local g = vim.fn.synIDattr(syn_id, 'name')
if type(group) == 'string' and g == group then
return true
elseif type(group) == 'table' and vim.tbl_contains(group, g) then
return true
end
end
return false
end
---Check if cursor is in treesitter capture
---@param capture string
---@param capture string | []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
local get_captures_at_pos = -- See neovim/neovim#20331
require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8
local captures_at_cursor = vim.tbl_map(function(x)
return x.capture
end, get_captures_at_pos(buf, row, col))
if vim.tbl_isempty(captures_at_cursor) 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())
elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then
return true
elseif type(capture) == 'table' then
for _, v in ipairs(capture) do
if vim.tbl_contains(captures_at_cursor, v) then
return true
end
end
end, true)
end
return vim.tbl_contains(node_types, capture)
return false
end
return context

View File

@ -1,13 +1,12 @@
local compare = require('cmp.config.compare')
local mapping = require('cmp.config.mapping')
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types')
local WIDE_HEIGHT = 40
---@return cmp.ConfigSchema
return function()
return {
---@type cmp.ConfigSchema
local config = {
enabled = function()
local disabled = false
disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt')
@ -16,85 +15,35 @@ return function()
return not disabled
end,
preselect = types.cmp.PreselectMode.Item,
mapping = {
['<Down>'] = mapping({
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
c = function(fallback)
local cmp = require('cmp')
cmp.close()
vim.schedule(cmp.suspend())
fallback()
end,
}),
['<Up>'] = mapping({
i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
c = function(fallback)
local cmp = require('cmp')
cmp.close()
vim.schedule(cmp.suspend())
fallback()
end,
}),
['<Tab>'] = mapping({
c = function()
local cmp = require('cmp')
if #cmp.core:get_sources() > 0 and not require('cmp.config').is_native_menu() then
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
else
if vim.fn.pumvisible() == 0 then
vim.api.nvim_feedkeys(keymap.t('<C-z>'), 'in', true)
else
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'in', true)
end
end
end,
}),
['<S-Tab>'] = mapping({
c = function()
local cmp = require('cmp')
if #cmp.core:get_sources() > 0 and not require('cmp.config').is_native_menu() then
if cmp.visible() then
cmp.select_prev_item()
else
cmp.complete()
end
else
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,
}),
['<C-n>'] = mapping(mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }),
['<C-p>'] = mapping(mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }), { 'i', 'c' }),
['<C-y>'] = mapping.confirm({ select = false }),
['<C-e>'] = mapping.abort(),
performance = {
debounce = 60,
throttle = 30,
fetching_timeout = 500,
async_budget = 1,
max_view_entries = 200,
},
preselect = types.cmp.PreselectMode.Item,
mapping = {},
snippet = {
expand = function()
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',
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
keyword_length = 1,
},
formatting = {
expandable_indicator = true,
fields = { 'abbr', 'kind', 'menu' },
format = function(_, vim_item)
return vim_item
@ -103,6 +52,8 @@ return function()
matching = {
disallow_fuzzy_matching = false,
disallow_fullfuzzy_matching = false,
disallow_partial_fuzzy_matching = true,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
},
@ -117,7 +68,7 @@ return function()
compare.recently_used,
compare.locality,
compare.kind,
compare.sort_text,
-- compare.sort_text,
compare.length,
compare.order,
},
@ -125,13 +76,6 @@ return function()
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)
@ -146,7 +90,28 @@ return function()
},
view = {
entries = { name = 'custom', selection_order = 'top_down' },
entries = {
name = 'custom',
selection_order = 'top_down',
},
},
window = {
completion = {
border = { '', '', '', '', '', '', '', '' },
winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None',
scrolloff = 0,
col_offset = 0,
side_padding = 1,
scrollbar = true,
},
documentation = {
max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)),
max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))),
border = { '', '', '', ' ', '', '', '', ' ' },
winhighlight = 'FloatBorder:NormalFloat',
},
},
}
return config
end

View File

@ -1,5 +1,22 @@
local mapping
mapping = setmetatable({}, {
local types = require('cmp.types')
local misc = require('cmp.utils.misc')
local keymap = require('cmp.utils.keymap')
local function merge_keymaps(base, override)
local normalized_base = {}
for k, v in pairs(base) do
normalized_base[keymap.normalize(k)] = v
end
local normalized_override = {}
for k, v in pairs(override) do
normalized_override[keymap.normalize(k)] = v
end
return misc.merge(normalized_base, normalized_override)
end
local mapping = setmetatable({}, {
__call = function(_, invoke, modes)
if type(invoke) == 'function' then
local map = {}
@ -12,8 +29,111 @@ mapping = setmetatable({}, {
end,
})
---Mapping preset configuration.
mapping.preset = {}
---Mapping preset insert-mode configuration.
mapping.preset.insert = function(override)
return merge_keymaps(override or {}, {
['<Down>'] = {
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<Up>'] = {
i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<C-n>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-p>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-y>'] = {
i = mapping.confirm({ select = false }),
},
['<C-e>'] = {
i = mapping.abort(),
},
})
end
---Mapping preset cmdline-mode configuration.
mapping.preset.cmdline = function(override)
return merge_keymaps(override or {}, {
['<C-z>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<S-Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
cmp.complete()
end
end,
},
['<C-n>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end,
},
['<C-p>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
fallback()
end
end,
},
['<C-e>'] = {
c = mapping.abort(),
},
['<C-y>'] = {
c = mapping.confirm({ select = false }),
},
})
end
---Invoke completion
---@param option cmp.CompleteParams
---@param option? cmp.CompleteParams
mapping.complete = function(option)
return function(fallback)
if not require('cmp').complete(option) then

View File

@ -0,0 +1,16 @@
local window = {}
window.bordered = function(opts)
opts = opts or {}
return {
border = opts.border or 'rounded',
winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:Normal,CursorLine:Visual,Search:None',
zindex = opts.zindex or 1001,
scrolloff = opts.scrolloff or 0,
col_offset = opts.col_offset or 0,
side_padding = opts.side_padding or 1,
scrollbar = opts.scrollbar == nil and true or opts.scrollbar,
}
end
return window

View File

@ -10,12 +10,13 @@ local api = require('cmp.utils.api')
---@field public prev_context cmp.Context
---@field public option cmp.ContextOption
---@field public filetype string
---@field public time number
---@field public bufnr number
---@field public time integer
---@field public bufnr integer
---@field public cursor vim.Position|lsp.Position
---@field public cursor_line string
---@field public cursor_after_line string
---@field public cursor_before_line string
---@field public aborted boolean
local context = {}
---Create new empty context
@ -31,8 +32,8 @@ context.empty = function()
end
---Create new context
---@param prev_context cmp.Context
---@param option cmp.ContextOption
---@param prev_context? cmp.Context
---@param option? cmp.ContextOption
---@return cmp.Context
context.new = function(prev_context, option)
option = option or {}
@ -55,9 +56,14 @@ context.new = function(prev_context, option)
self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col)
self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1)
self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col)
self.aborted = false
return self
end
context.abort = function(self)
self.aborted = true
end
---Return context creation reason.
---@return cmp.ContextReason
context.get_reason = function(self)
@ -65,7 +71,7 @@ context.get_reason = function(self)
end
---Get keyword pattern offset
---@return number|nil
---@return integer
context.get_offset = function(self, keyword_pattern)
return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function()
return pattern.offset(keyword_pattern .. '\\m$', self.cursor_before_line) or self.cursor.col

View File

@ -14,10 +14,6 @@ local types = require('cmp.types')
local api = require('cmp.utils.api')
local event = require('cmp.utils.event')
local SOURCE_TIMEOUT = 500
local DEBOUNCE_TIME = 80
local THROTTLE_TIME = 40
---@class cmp.Core
---@field public suspending boolean
---@field public view cmp.View
@ -36,9 +32,11 @@ core.new = function()
self.view.event:on('keymap', function(...)
self:on_keymap(...)
end)
self.view.event:on('complete_done', function(evt)
self.event:emit('complete_done', evt)
end)
for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do
self.view.event:on(event_name, function(evt)
self.event:emit(event_name, evt)
end)
end
return self
end
@ -49,17 +47,19 @@ core.register_source = function(self, s)
end
---Unregister source
---@param source_id string
---@param source_id integer
core.unregister_source = function(self, source_id)
self.sources[source_id] = nil
end
---Get new context
---@param option cmp.ContextOption
---@param option? cmp.ContextOption
---@return cmp.Context
core.get_context = function(self, option)
self.context:abort()
local prev = self.context:clone()
prev.prev_context = nil
prev.cache = nil
local ctx = context.new(prev, option)
self:set_context(ctx)
return self.context
@ -74,13 +74,14 @@ end
---Suspend completion
core.suspend = function(self)
self.suspending = true
return function()
-- It's needed to avoid conflicting with autocmd debouncing.
return vim.schedule_wrap(function()
self.suspending = false
end
end)
end
---Get sources that sorted by priority
---@param filter cmp.SourceStatus[]|fun(s: cmp.Source): boolean
---@param filter? cmp.SourceStatus[]|fun(s: cmp.Source): boolean
---@return cmp.Source[]
core.get_sources = function(self, filter)
local f = function(s)
@ -168,7 +169,7 @@ core.on_change = function(self, trigger_event)
if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then
self:complete(ctx)
else
self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 0
self:filter()
end
else
@ -221,7 +222,7 @@ 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
if not self.view:visible() or self.view:get_selected_entry() then
return false
end
@ -240,7 +241,7 @@ core.complete_common_string = function(self)
config.set_onetime({})
local cursor = api.get_cursor()
local offset = self.view:get_offset()
local offset = self.view:get_offset() or cursor[2]
local common_string
for _, e in ipairs(self.view:get_entries()) do
local vim_item = e:get_vim_item(offset)
@ -250,8 +251,10 @@ core.complete_common_string = function(self)
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')
local cursor_before_line = api.get_cursor_before_line()
local pretext = cursor_before_line:sub(offset)
if common_string and #common_string > #pretext then
feedkeys.call(keymap.backspace(pretext) .. common_string, 'n')
return true
end
return false
@ -276,17 +279,9 @@ core.complete = function(self, ctx)
if s_.incomplete and new:changed(s_.context) then
s_:complete(new, callback)
else
for _, s__ in ipairs(self:get_sources({ source.SourceStatus.FETCHING })) do
if s_ == s__ then
break
end
if not s__.incomplete and SOURCE_TIMEOUT > s__:get_fetching_time() then
return
end
end
if not self.view:get_active_entry() then
self.filter.stop()
self.filter.timeout = self.view:visible() and DEBOUNCE_TIME or 0
self.filter.timeout = config.get().performance.debounce
self:filter()
end
end
@ -296,14 +291,14 @@ core.complete = function(self, ctx)
end
if not self.view:get_active_entry() then
self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 1
self:filter()
end
end
---Update completion menu
core.filter = async.throttle(function(self)
self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0
local async_filter = async.wrap(function(self)
self.filter.timeout = config.get().performance.throttle
-- Check invalid condition.
local ignore = false
@ -315,11 +310,13 @@ core.filter = async.throttle(function(self)
-- Check fetching sources.
local sources = {}
for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do
if not s.incomplete and SOURCE_TIMEOUT > s:get_fetching_time() then
-- Reserve filter call for timeout.
self.filter.timeout = SOURCE_TIMEOUT - s:get_fetching_time()
-- Reserve filter call for timeout.
if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then
self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time()
self:filter()
break
if #sources == 0 then
return
end
end
table.insert(sources, s)
end
@ -327,20 +324,17 @@ core.filter = async.throttle(function(self)
local ctx = self:get_context()
-- Display completion results.
self.view:open(ctx, sources)
local did_open = self.view:open(ctx, sources)
local fetching = #self:get_sources(function(s)
return s.status == source.SourceStatus.FETCHING
end)
-- 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
if not did_open and fetching == 0 then
config.set_onetime({})
end
end, THROTTLE_TIME)
end)
core.filter = async.throttle(async_filter, config.get().performance.throttle)
---Confirm completion.
---@param e cmp.Entry
@ -348,7 +342,10 @@ end, THROTTLE_TIME)
---@param callback function
core.confirm = function(self, e, option, callback)
if not (e and not e.confirmed) then
return callback()
if callback then
callback()
end
return
end
e.confirmed = true
@ -361,33 +358,39 @@ core.confirm = function(self, e, option, callback)
feedkeys.call(keymap.indentkeys(), 'n')
feedkeys.call('', 'n', function()
-- Emulate `<C-y>` behavior to save `.` register.
local ctx = context.new()
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset())))
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, e:get_word())
table.insert(keys, keymap.undobreak())
feedkeys.call(table.concat(keys, ''), 'int')
feedkeys.call(table.concat(keys, ''), 'in')
end)
feedkeys.call('', 'n', function()
-- Restore the line at the time of request.
local ctx = context.new()
if api.is_cmdline_mode() then
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset())))
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
feedkeys.call(table.concat(keys, ''), 'in')
else
vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
string.sub(e.context.cursor_before_line, e:get_offset()),
vim.cmd([[silent! undojoin]])
-- This logic must be used nvim_buf_set_text.
-- If not used, the snippet engine's placeholder wil be broken.
vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
e.context.cursor_before_line:sub(e:get_offset()),
})
vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
end
end)
feedkeys.call('', 'n', function()
-- Apply additionalTextEdits.
local ctx = context.new()
if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
if #(e:get_completion_item().additionalTextEdits or {}) == 0 then
e:resolve(function()
local new = context.new()
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
local text_edits = e:get_completion_item().additionalTextEdits or {}
if #text_edits == 0 then
return
end
@ -407,18 +410,20 @@ core.confirm = function(self, e, option, callback)
if has_cursor_line_text_edit then
return
end
vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16')
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind())
end)
else
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, 'utf-16')
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind())
end
end)
feedkeys.call('', 'n', function()
local ctx = context.new()
local completion_item = misc.copy(e:get_completion_item())
if not misc.safe(completion_item.textEdit) then
if not completion_item.textEdit then
completion_item.textEdit = {}
completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label
completion_item.textEdit.newText = completion_item.insertText or completion_item.word or completion_item.label
end
local behavior = option.behavior or config.get().confirmation.default_behavior
if behavior == types.cmp.ConfirmBehavior.Replace then
@ -427,30 +432,41 @@ core.confirm = function(self, e, option, callback)
completion_item.textEdit.range = e:get_insert_range()
end
local diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.character)
local diff_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character)
local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1))
local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col)
local new_text = completion_item.textEdit.newText
completion_item.textEdit.range.start.line = ctx.cursor.line
completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before
completion_item.textEdit.range['end'].line = ctx.cursor.line
completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after
if api.is_insert_mode() then
if false then
--To use complex expansion debug.
vim.print({ -- luacheck: ignore
item = e:get_completion_item(),
diff_before = diff_before,
diff_after = diff_after,
new_text = new_text,
text_edit_new_text = completion_item.textEdit.newText,
range_start = completion_item.textEdit.range.start.character,
range_end = completion_item.textEdit.range['end'].character,
original_range_start = e:get_completion_item().textEdit.range.start.character,
original_range_end = e:get_completion_item().textEdit.range['end'].character,
cursor_line = ctx.cursor_line,
cursor_col0 = ctx.cursor.col - 1,
})
end
local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
completion_item.textEdit.range.start.line = ctx.cursor.line
completion_item.textEdit.range.start.character = ctx.cursor.character - diff_before
completion_item.textEdit.range['end'].line = ctx.cursor.line
completion_item.textEdit.range['end'].character = ctx.cursor.character + diff_after
if is_snippet then
completion_item.textEdit.newText = ''
end
vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-16')
vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8')
local texts = vim.split(completion_item.textEdit.newText, '\n')
local position = completion_item.textEdit.range.start
position.line = position.line + (#texts - 1)
if #texts == 1 then
position.character = position.character + misc.to_utfindex(texts[1])
else
position.character = misc.to_utfindex(texts[#texts])
end
local pos = types.lsp.Position.to_vim(0, position)
vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 })
vim.api.nvim_win_set_cursor(0, {
completion_item.textEdit.range.start.line + #texts,
(#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]),
})
if is_snippet then
config.get().snippet.expand({
body = new_text,
@ -459,8 +475,8 @@ core.confirm = function(self, e, option, callback)
end
else
local keys = {}
table.insert(keys, string.rep(keymap.t('<BS>'), diff_before))
table.insert(keys, string.rep(keymap.t('<Del>'), diff_after))
table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1)))
table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character)))
table.insert(keys, new_text)
feedkeys.call(table.concat(keys, ''), 'in')
end

View File

@ -8,9 +8,19 @@ local api = require('cmp.utils.api')
describe('cmp.core', function()
describe('confirm', function()
local confirm = function(request, filter, completion_item)
---@param request string
---@param filter string
---@param completion_item lsp.CompletionItem
---@param option? { position_encoding_kind: lsp.PositionEncodingKind }
---@return table
local confirm = function(request, filter, completion_item, option)
option = option or {}
local c = core.new()
local s = source.new('spec', {
get_position_encoding_kind = function()
return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16
end,
complete = function(_, _, callback)
callback({ completion_item })
end,
@ -23,7 +33,7 @@ describe('cmp.core', function()
end)
end)
feedkeys.call(filter, 'n', function()
c:confirm(c.sources[s.id].entries[1], {})
c:confirm(c.sources[s.id].entries[1], {}, function() end)
end)
local state = {}
feedkeys.call('', 'x', function()
@ -80,6 +90,29 @@ describe('cmp.core', function()
assert.are.same(state.cursor, { 3, 3 })
end)
it('#1552', function()
local state = confirm(keymap.t('ios.'), '', {
filterText = 'IsPermission',
insertTextFormat = 2,
label = 'IsPermission',
textEdit = {
newText = 'IsPermission($0)',
range = {
['end'] = {
character = 3,
line = 0,
},
start = {
character = 3,
line = 0,
},
},
},
})
assert.are.same(state.buffer, { 'os.IsPermission()' })
assert.are.same(state.cursor, { 1, 16 })
end)
it('insertText & snippet', function()
local state = confirm('iA', 'IU', {
label = 'AIUEO',
@ -111,6 +144,46 @@ describe('cmp.core', function()
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
assert.are.same(state.cursor, { 2, 2 })
end)
local char = '🗿'
for _, case in ipairs({
{
encoding = types.lsp.PositionEncodingKind.UTF8,
char_size = #char,
},
{
encoding = types.lsp.PositionEncodingKind.UTF16,
char_size = select(2, vim.str_utfindex(char)),
},
{
encoding = types.lsp.PositionEncodingKind.UTF32,
char_size = select(1, vim.str_utfindex(char)),
},
}) do
it('textEdit & multibyte: ' .. case.encoding, function()
local state = confirm(keymap.t('i%s:%s%s:%s<Left><Left><Left>'):format(char, char, char, char), char, {
label = char .. char .. char,
textEdit = {
range = {
start = {
line = 0,
character = case.char_size + #':',
},
['end'] = {
line = 0,
character = case.char_size + #':' + case.char_size + case.char_size,
},
},
newText = char .. char .. char .. char .. char,
},
}, {
position_encoding_kind = case.encoding,
})
vim.print({ state = state, case = case })
assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) })
assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) })
end)
end
end)
describe('cmdline-mode', function()

View File

@ -7,15 +7,15 @@ local types = require('cmp.types')
local matcher = require('cmp.matcher')
---@class cmp.Entry
---@field public id number
---@field public id integer
---@field public cache cmp.Cache
---@field public match_cache cmp.Cache
---@field public score number
---@field public score integer
---@field public exact boolean
---@field public matches table
---@field public context cmp.Context
---@field public source cmp.Source
---@field public source_offset number
---@field public source_offset integer
---@field public source_insert_range lsp.Range
---@field public source_replace_range lsp.Range
---@field public completion_item lsp.CompletionItem
@ -29,8 +29,9 @@ local entry = {}
---@param ctx cmp.Context
---@param source cmp.Source
---@param completion_item lsp.CompletionItem
---@param item_defaults? lsp.internal.CompletionItemDefaults
---@return cmp.Entry
entry.new = function(ctx, source, completion_item)
entry.new = function(ctx, source, completion_item, item_defaults)
local self = setmetatable({}, { __index = entry })
self.id = misc.id('entry.new')
self.cache = cache.new()
@ -43,7 +44,7 @@ entry.new = function(ctx, source, completion_item)
self.source_offset = source.request_offset
self.source_insert_range = source:get_default_insert_range()
self.source_replace_range = source:get_default_replace_range()
self.completion_item = completion_item
self.completion_item = self:fill_defaults(completion_item, item_defaults)
self.resolved_completion_item = nil
self.resolved_callbacks = {}
self.resolving = false
@ -52,20 +53,23 @@ entry.new = function(ctx, source, completion_item)
end
---Make offset value
---@return number
---@return integer
entry.get_offset = function(self)
return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_offset', function()
local offset = self.source_offset
if misc.safe(self:get_completion_item().textEdit) then
local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
if self:get_completion_item().textEdit then
local range = self:get_insert_range()
if range then
local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
for idx = c, self.source_offset do
if not char.is_white(string.byte(self.context.cursor_line, idx)) then
offset = idx
break
offset = self.context.cache:ensure('entry:' .. 'get_offset:' .. tostring(range.start.character), function()
local start = math.min(range.start.character + 1, offset)
for idx = start, self.source_offset do
local byte = string.byte(self.context.cursor_line, idx)
if byte == nil or not char.is_white(byte) then
return idx
end
end
end
return offset
end)
end
else
-- NOTE
@ -101,14 +105,14 @@ end
---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@return string
entry.get_word = function(self)
return self.cache:ensure({ 'get_word' }, function()
return self.cache:ensure('get_word', function()
--NOTE: This is nvim-cmp specific implementation.
if misc.safe(self:get_completion_item().word) then
if self:get_completion_item().word then
return self:get_completion_item().word
end
local word
if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = vim.lsp.util.parse_snippet(word)
@ -126,20 +130,24 @@ entry.get_word = function(self)
word = str.trim(self:get_completion_item().label)
end
return str.oneline(word)
end)
end) --[[@as string]]
end
---Get overwrite information
---@return number, number
---@return integer[]
entry.get_overwrite = function(self)
return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function()
if misc.safe(self:get_completion_item().textEdit) then
local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
local s = misc.to_vimindex(self.context.cursor_line, r.start.character)
local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
local before = self.context.cursor.col - s
local after = e - self.context.cursor.col
return { before, after }
return self.cache:ensure('get_overwrite', function()
if self:get_completion_item().textEdit then
local range = self:get_insert_range()
if range then
return self.context.cache:ensure('entry:' .. 'get_overwrite:' .. tostring(range.start.character) .. ':' .. tostring(range['end'].character), function()
local vim_start = range.start.character + 1
local vim_end = range['end'].character + 1
local before = self.context.cursor.col - vim_start
local after = vim_end - self.context.cursor.col
return { before, after }
end)
end
end
return { 0, 0 }
end)
@ -148,9 +156,9 @@ end
---Create filter text
---@return string
entry.get_filter_text = function(self)
return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_filter_text', function()
local word
if misc.safe(self:get_completion_item().filterText) then
if self:get_completion_item().filterText then
word = self:get_completion_item().filterText
else
word = str.trim(self:get_completion_item().label)
@ -162,14 +170,14 @@ end
---Get LSP's insert text
---@return string
entry.get_insert_text = function(self)
return self.cache:ensure({ 'get_insert_text', self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_insert_text', function()
local word
if misc.safe(self:get_completion_item().textEdit) then
if self:get_completion_item().textEdit then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
elseif misc.safe(self:get_completion_item().insertText) then
elseif self:get_completion_item().insertText then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
@ -188,12 +196,12 @@ entry.is_deprecated = function(self)
end
---Return view information.
---@param suggest_offset number
---@param entries_buf number The buffer this entry will be rendered into.
---@return { abbr: { text: string, bytes: number, width: number, hl_group: string }, kind: { text: string, bytes: number, width: number, hl_group: string }, menu: { text: string, bytes: number, width: number, hl_group: string } }
---@param suggest_offset integer
---@param entries_buf integer The buffer this entry will be rendered into.
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
entry.get_view = function(self, suggest_offset, entries_buf)
local item = self:get_vim_item(suggest_offset)
return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, function()
return self.cache:ensure('get_view:' .. tostring(entries_buf), function()
local view = {}
-- The result of vim.fn.strdisplaywidth depends on which buffer it was
-- called in because it reads the values of the option 'tabstop' when
@ -221,24 +229,25 @@ entry.get_view = function(self, suggest_offset, entries_buf)
end
---Make vim.CompletedItem
---@param suggest_offset number
---@param suggest_offset integer
---@return vim.CompletedItem
entry.get_vim_item = function(self, suggest_offset)
return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), function()
local completion_item = self:get_completion_item()
local word = self:get_word()
local abbr = str.oneline(completion_item.label)
-- ~ indicator
local is_snippet = false
if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
is_snippet = true
local is_expandable = false
local expandable_indicator = config.get().formatting.expandable_indicator
if #(completion_item.additionalTextEdits or {}) > 0 then
is_expandable = true
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
is_snippet = self:get_insert_text() ~= word
is_expandable = self:get_insert_text() ~= word
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
is_snippet = true
is_expandable = true
end
if is_snippet then
if expandable_indicator and is_expandable then
abbr = abbr .. '~'
end
@ -249,19 +258,19 @@ entry.get_vim_item = function(self, suggest_offset)
-- labelDetails.
local menu = nil
if misc.safe(completion_item.labelDetails) then
if completion_item.labelDetails then
menu = ''
if misc.safe(completion_item.labelDetails.detail) then
if completion_item.labelDetails.detail then
menu = menu .. completion_item.labelDetails.detail
end
if misc.safe(completion_item.labelDetails.description) then
if completion_item.labelDetails.description then
menu = menu .. completion_item.labelDetails.description
end
end
-- remove duplicated string.
if self:get_offset() ~= self.context.cursor.col then
for i = 1, #word - 1 do
for i = 1, #word do
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
word = string.sub(word, 1, i - 1)
break
@ -269,10 +278,13 @@ entry.get_vim_item = function(self, suggest_offset)
end
end
local cmp_opts = self:get_completion_item().cmp or {}
local vim_item = {
word = word,
abbr = abbr,
kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind_hl_group = cmp_opts.kind_hl_group,
menu = menu,
dup = self:get_completion_item().dup or 1,
}
@ -293,24 +305,25 @@ end
---Get commit characters
---@return string[]
entry.get_commit_characters = function(self)
return misc.safe(self:get_completion_item().commitCharacters) or {}
return self:get_completion_item().commitCharacters or {}
end
---Return insert range
---@return lsp.Range|nil
entry.get_insert_range = function(self)
local insert_range
if misc.safe(self:get_completion_item().textEdit) then
if misc.safe(self:get_completion_item().textEdit.insert) then
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.insert then
insert_range = self:get_completion_item().textEdit.insert
else
insert_range = self:get_completion_item().textEdit.range
insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
insert_range = self:convert_range_encoding(insert_range)
else
insert_range = {
start = {
line = self.context.cursor.row - 1,
character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character),
character = self:get_offset() - 1,
},
['end'] = self.source_insert_range['end'],
}
@ -321,15 +334,22 @@ end
---Return replace range
---@return lsp.Range|nil
entry.get_replace_range = function(self)
return self.cache:ensure({ 'get_replace_range', self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_replace_range', function()
local replace_range
if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then
replace_range = self:get_completion_item().textEdit.replace
else
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.replace then
replace_range = self:get_completion_item().textEdit.replace
else
replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
replace_range = self:convert_range_encoding(replace_range)
end
if not replace_range or ((self.context.cursor.col - 1) == replace_range['end'].character) then
replace_range = {
start = {
line = self.source_replace_range.start.line,
character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character),
character = self:get_offset() - 1,
},
['end'] = self.source_replace_range['end'],
}
@ -341,17 +361,12 @@ end
---Match line.
---@param input string
---@param matching_config cmp.MatchingConfig
---@return { score: number, matches: table[] }
---@return { score: integer, matches: table[] }
entry.match = function(self, input, matching_config)
return self.match_cache:ensure({
input,
self.resolved_completion_item and 1 or 0,
matching_config.disallow_fuzzy_matching and 1 or 0,
matching_config.disallow_partial_matching and 1 or 0,
matching_config.disallow_prefix_unmatching and 1 or 0,
}, function()
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_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_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching,
disallow_partial_matching = matching_config.disallow_partial_matching,
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
synonyms = {
@ -360,27 +375,42 @@ entry.match = function(self, input, matching_config)
},
}
local score, matches, _
score, matches = matcher.match(input, self:get_filter_text(), option)
local score, matches, filter_text, _
local checked = {} ---@type table<string, boolean>
filter_text = self:get_filter_text()
checked[filter_text] = true
score, matches = matcher.match(input, filter_text, option)
-- Support the language server that doesn't respect VSCode's behaviors.
local prefix = ''
if score == 0 then
if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
if 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
prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
local accept = nil
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)
filter_text = prefix .. self:get_filter_text()
if not checked[filter_text] then
checked[filter_text] = true
score, matches = matcher.match(input, filter_text, option)
end
end
end
end
end
if self:get_filter_text() ~= self:get_completion_item().label then
_, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() })
-- Fix highlight if filterText is not the same to vim_item.abbr.
if score > 0 then
local vim_item = self:get_vim_item(self.source_offset)
filter_text = vim_item.abbr or vim_item.word
if not checked[filter_text] then
local diff = self.source_offset - self:get_offset()
_, matches = matcher.match(input:sub(1 + diff), filter_text, option)
end
end
return { score = score, matches = matches }
@ -390,7 +420,7 @@ end
---Get resolved completion item if possible.
---@return lsp.CompletionItem
entry.get_completion_item = function(self)
return self.cache:ensure({ 'get_completion_item', self.resolved_completion_item and 1 or 0 }, function()
return self.cache:ensure('get_completion_item', function()
if self.resolved_completion_item then
local completion_item = misc.copy(self.completion_item)
for k, v in pairs(self.resolved_completion_item) do
@ -410,7 +440,7 @@ entry.get_documentation = function(self)
local documents = {}
-- detail
if misc.safe(item.detail) and item.detail ~= '' then
if item.detail and item.detail ~= '' then
local ft = self.context.filetype
local dot_index = string.find(ft, '%.')
if dot_index ~= nil then
@ -422,13 +452,23 @@ entry.get_documentation = function(self)
})
end
if type(item.documentation) == 'string' and item.documentation ~= '' then
table.insert(documents, {
kind = types.lsp.MarkupKind.PlainText,
value = str.trim(item.documentation),
})
elseif type(item.documentation) == 'table' and item.documentation.value ~= '' then
table.insert(documents, item.documentation)
local documentation = item.documentation
if type(documentation) == 'string' and documentation ~= '' then
local value = str.trim(documentation)
if value ~= '' then
table.insert(documents, {
kind = types.lsp.MarkupKind.PlainText,
value = value,
})
end
elseif type(documentation) == 'table' and not misc.empty(documentation.value) then
local value = str.trim(documentation.value)
if value ~= '' then
table.insert(documents, {
kind = documentation.kind,
value = value,
})
end
end
return vim.lsp.util.convert_input_to_markdown_lines(documents)
@ -437,7 +477,7 @@ end
---Get completion item kind
---@return lsp.CompletionItemKind
entry.get_kind = function(self)
return misc.safe(self:get_completion_item().kind) or types.lsp.CompletionItemKind.Text
return self:get_completion_item().kind or types.lsp.CompletionItemKind.Text
end
---Execute completion item's command.
@ -457,7 +497,12 @@ entry.resolve = function(self, callback)
if not self.resolving then
self.resolving = true
self.source:resolve(self.completion_item, function(completion_item)
self.resolved_completion_item = misc.safe(completion_item) or self.completion_item
self.resolving = false
if not completion_item then
return
end
self.resolved_completion_item = completion_item or self.completion_item
self.cache:clear()
for _, c in ipairs(self.resolved_callbacks) do
c()
end
@ -465,4 +510,57 @@ entry.resolve = function(self, callback)
end
end
---@param completion_item lsp.CompletionItem
---@param defaults? lsp.internal.CompletionItemDefaults
---@return lsp.CompletionItem
entry.fill_defaults = function(_, completion_item, defaults)
defaults = defaults or {}
if defaults.data then
completion_item.data = completion_item.data or defaults.data
end
if defaults.commitCharacters then
completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters
end
if defaults.insertTextFormat then
completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat
end
if defaults.insertTextMode then
completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode
end
if defaults.editRange then
if not completion_item.textEdit then
if defaults.editRange.insert then
completion_item.textEdit = {
insert = defaults.editRange.insert,
replace = defaults.editRange.replace,
newText = completion_item.textEditText or completion_item.label,
}
else
completion_item.textEdit = {
range = defaults.editRange, --[[@as lsp.Range]]
newText = completion_item.textEditText or completion_item.label,
}
end
end
end
return completion_item
end
---Convert the oneline range encoding.
entry.convert_range_encoding = function(self, range)
local from_encoding = self.source:get_position_encoding_kind()
return self.context.cache:ensure('entry.convert_range_encoding:' .. range.start.character .. ':' .. range['end'].character .. ':' .. from_encoding, function()
return {
start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding),
['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding),
}
end)
end
return entry

View File

@ -1,6 +1,4 @@
local spec = require('cmp.utils.spec')
local source = require('cmp.source')
local async = require('cmp.utils.async')
local entry = require('cmp.entry')
@ -290,41 +288,6 @@ describe('entry', function()
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string')
end)
it('[ansiblels] 1', function()
local item = {
detail = 'ansible.builtin',
filterText = 'blockinfile ansible.builtin.blockinfile',
kind = 7,
label = 'blockinfile',
sortText = '2_blockinfile',
textEdit = {
newText = '',
range = {
['end'] = {
character = 7,
line = 15,
},
start = {
character = 6,
line = 15,
},
},
},
}
local s = source.new('dummy', {
resolve = function(_, _, callback)
item.textEdit.newText = 'modified'
callback(item)
end,
})
local e = entry.new(spec.state('', 1, 1).manual(), s, item)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
async.sync(function(done)
e:resolve(done)
end, 100)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
end)
it('[#47] word should not contain \\n character', function()
local state = spec.state('', 1, 1)
@ -339,4 +302,65 @@ describe('entry', function()
assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:')
assert.are.equal(e:get_filter_text(), '__init__')
end)
-- I can't understand this test case...
-- it('[#1533] keyword pattern that include whitespace', function()
-- local state = spec.state(' ', 1, 2)
-- local state_source = state.source()
-- state_source.get_keyword_pattern = function(_)
-- return '.'
-- end
-- state.input(' ')
-- local e = entry.new(state.manual(), state_source, {
-- filterText = "constructor() {\n ... st = 'test';\n ",
-- kind = 1,
-- label = "constructor() {\n ... st = 'test';\n }",
-- textEdit = {
-- newText = "constructor() {\n this.test = 'test';\n }",
-- range = {
-- ['end'] = {
-- character = 2,
-- line = 2,
-- },
-- start = {
-- character = 0,
-- line = 2,
-- },
-- },
-- },
-- })
-- assert.are.equal(e:get_offset(), 2)
-- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {')
-- end)
it('[#1533] clang regression test', function()
local state = spec.state('jsonReader', 3, 11)
local state_source = state.source()
state.input('.')
local e = entry.new(state.manual(), state_source, {
filterText = 'getPath()',
kind = 1,
label = 'getPath()',
textEdit = {
newText = 'getPath()',
range = {
['end'] = {
character = 11,
col = 12,
line = 2,
row = 3,
},
start = {
character = 11,
line = 2,
},
},
},
})
assert.are.equal(e:get_offset(), 12)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'getPath()')
end)
end)

View File

@ -5,6 +5,7 @@ local feedkeys = require('cmp.utils.feedkeys')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local async = require('cmp.utils.async')
local cmp = {}
@ -29,6 +30,7 @@ cmp.config.disable = misc.none
cmp.config.compare = require('cmp.config.compare')
cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')
cmp.config.window = require('cmp.config.window')
---Sync asynchronous process.
cmp.sync = function(callback)
@ -48,7 +50,7 @@ end
---Register completion sources
---@param name string
---@param s cmp.Source
---@return number
---@return integer
cmp.register_source = function(name, s)
local src = source.new(name, s)
cmp.core:register_source(src)
@ -56,7 +58,7 @@ cmp.register_source = function(name, s)
end
---Unregister completion source
---@param id number
---@param id integer
cmp.unregister_source = function(id)
cmp.core:unregister_source(id)
end
@ -106,7 +108,6 @@ cmp.close = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:close()
cmp.core:reset()
vim.schedule(release)
return true
else
@ -129,6 +130,8 @@ end)
---Select next item if possible
cmp.select_next_item = cmp.sync(function(option)
option = option or {}
option.behavior = option.behavior or cmp.SelectBehavior.Insert
option.count = option.count or 1
if cmp.core.view:visible() then
local release = cmp.core:suspend()
@ -136,11 +139,10 @@ cmp.select_next_item = cmp.sync(function(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
-- Special handling for native pum. Required to facilitate key mapping processing.
if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-n>'), 'in')
if option.behavior == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'in')
else
feedkeys.call(keymap.t('<Down>'), 'in')
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'in')
end
return true
end
@ -150,6 +152,8 @@ end)
---Select prev item if possible
cmp.select_prev_item = cmp.sync(function(option)
option = option or {}
option.behavior = option.behavior or cmp.SelectBehavior.Insert
option.count = option.count or 1
if cmp.core.view:visible() then
local release = cmp.core:suspend()
@ -157,11 +161,10 @@ cmp.select_prev_item = cmp.sync(function(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
-- Special handling for native pum. Required to facilitate key mapping processing.
if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-p>'), 'in')
if option.behavior == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'in')
else
feedkeys.call(keymap.t('<Up>'), 'in')
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'in')
end
return true
end
@ -170,7 +173,7 @@ end)
---Scrolling documentation window if possible
cmp.scroll_docs = cmp.sync(function(delta)
if cmp.core.view:visible() then
if cmp.core.view.docs_view:visible() then
cmp.core.view:scroll_docs(delta)
return true
else
@ -181,25 +184,35 @@ end)
---Confirm completion
cmp.confirm = cmp.sync(function(option, callback)
option = option or {}
option.select = option.select or false
option.behavior = option.behavior or cmp.get_config().confirmation.default_behavior or cmp.ConfirmBehavior.Insert
callback = callback or function() end
local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil)
if e then
cmp.core:confirm(e, {
behavior = option.behavior,
}, function()
callback()
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end)
return true
else
-- Special handling for native puma. Required to facilitate key mapping processing.
if vim.fn.complete_info({ 'selected' }).selected ~= -1 then
feedkeys.call(keymap.t('<C-y>'), 'in')
if cmp.core.view:visible() then
local e = cmp.core.view:get_selected_entry()
if not e and option.select then
e = cmp.core.view:get_first_entry()
end
if e then
cmp.core:confirm(e, {
behavior = option.behavior,
}, function()
callback()
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end)
return true
end
elseif vim.fn.pumvisible() == 1 then
local index = vim.fn.complete_info({ 'selected' }).selected
if index == -1 and option.select then
index = 0
end
if index ~= -1 then
vim.api.nvim_select_popupmenu_item(index, true, true, {})
return true
end
return false
end
return false
end)
---Show status
@ -282,39 +295,28 @@ cmp.setup = setmetatable({
end,
})
autocmd.subscribe('InsertEnter', function()
feedkeys.call('', 'i', function()
if config.enabled() then
cmp.core:prepare()
cmp.core:on_change('InsertEnter')
end
end)
end)
autocmd.subscribe('InsertLeave', function()
cmp.core:reset()
cmp.core.view:close()
end)
autocmd.subscribe('CmdlineEnter', function()
-- In InsertEnter autocmd, vim will detects mode=normal unexpectedly.
local on_insert_enter = function()
if config.enabled() then
cmp.config.compare.scopes:update()
cmp.config.compare.locality:update()
cmp.core:prepare()
cmp.core:on_change('InsertEnter')
end
end)
end
autocmd.subscribe({ 'CmdlineEnter' }, async.debounce_next_tick(on_insert_enter))
autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter))
autocmd.subscribe('CmdlineLeave', function()
cmp.core:reset()
cmp.core.view:close()
end)
autocmd.subscribe('TextChanged', function()
-- async.throttle is needed for performance. The mapping `:<C-u>...<CR>` will fire `CmdlineChanged` for each character.
local on_text_changed = function()
if config.enabled() then
cmp.core:on_change('TextChanged')
end
end)
end
autocmd.subscribe({ 'TextChangedI', 'TextChangedP' }, on_text_changed)
autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed))
autocmd.subscribe('CursorMoved', function()
autocmd.subscribe('CursorMovedI', function()
if config.enabled() then
cmp.core:on_moved()
else
@ -323,9 +325,10 @@ autocmd.subscribe('CursorMoved', function()
end
end)
autocmd.subscribe('InsertEnter', function()
cmp.config.compare.scopes:update()
cmp.config.compare.locality:update()
-- If make this asynchronous, the completion menu will not close when the command output is displayed.
autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function()
cmp.core:reset()
cmp.core.view:close()
end)
cmp.event:on('complete_done', function(evt)

View File

@ -66,14 +66,20 @@ end
--
-- `candlesingle` -> candle#accept#single
-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~
--
-- * The `accept`'s `a` should not match to `candle`'s `a`
--
-- 7. Avoid false positive matching
--
-- `,` -> print,
-- ~
-- * Typically, the middle match with symbol characters only is false positive. should be ignored.
--
--
---Match entry
---@param input string
---@param word string
---@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean }
---@return number
---@param option { synonyms: string[], disallow_fullfuzzy_matching: boolean, disallow_fuzzy_matching: boolean, disallow_partial_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean }
---@return integer
matcher.match = function(input, word, option)
option = option or {}
@ -100,12 +106,14 @@ matcher.match = function(input, word, option)
local input_end_index = 1
local word_index = 1
local word_bound_index = 1
local no_symbol_match = false
while input_end_index <= #input and word_index <= #word do
local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index)
if m and input_end_index <= m.input_match_end then
m.index = word_bound_index
input_start_index = m.input_match_start + 1
input_end_index = m.input_match_end + 1
no_symbol_match = no_symbol_match or m.no_symbol_match
word_index = char.get_next_semantic_index(word, m.word_match_end)
table.insert(matches, m)
else
@ -120,6 +128,11 @@ matcher.match = function(input, word, option)
end
if #matches == 0 then
if not option.disallow_fuzzy_matching and not option.disallow_prefix_unmatching and not option.disallow_partial_fuzzy_matching then
if matcher.fuzzy(input, word, matches, option) then
return 1, matches
end
end
return 0, {}
end
@ -146,6 +159,10 @@ matcher.match = function(input, word, option)
end
end
if no_symbol_match and not prefix then
return 0, {}
end
-- Compute prefix match score
local score = prefix and matcher.PREFIX_FACTOR or 0
local offset = prefix and matches[1].index - 1 or 0
@ -167,8 +184,10 @@ matcher.match = function(input, word, option)
-- Check remaining input as fuzzy
if matches[#matches].input_match_end < #input then
if not option.disallow_fuzzy_matching then
if prefix and matcher.fuzzy(input, word, matches) then
return score, matches
if not option.disallow_partial_fuzzy_matching or prefix then
if matcher.fuzzy(input, word, matches, option) then
return score, matches
end
end
end
return 0, {}
@ -178,11 +197,10 @@ matcher.match = function(input, word, option)
end
--- fuzzy
matcher.fuzzy = function(input, word, matches)
local last_match = matches[#matches]
matcher.fuzzy = function(input, word, matches, option)
local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1
-- Lately specified middle of text.
local input_index = last_match.input_match_end + 1
for i = 1, #matches - 1 do
local curr_match = matches[i]
local next_match = matches[i + 1]
@ -200,10 +218,9 @@ matcher.fuzzy = function(input, word, matches)
end
-- Remaining text fuzzy match.
local last_input_index = input_index
local matched = false
local word_offset = 0
local word_index = last_match.word_match_end + 1
local word_index = matches[#matches] and (matches[#matches].word_match_end + 1) or 1
local input_match_start = -1
local input_match_end = -1
local word_match_start = -1
@ -220,12 +237,26 @@ matcher.fuzzy = function(input, word, matches)
input_index = input_index + 1
strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1
elseif matched then
input_index = last_input_index
input_match_end = input_index - 1
else
if option.disallow_fullfuzzy_matching then
break
else
if matched then
table.insert(matches, {
input_match_start = input_match_start,
input_match_end = input_index - 1,
word_match_start = word_match_start,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
fuzzy = true,
})
end
end
matched = false
end
word_offset = word_offset + 1
end
if input_index > #input then
table.insert(matches, {
input_match_start = input_match_start,
@ -260,6 +291,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
local word_offset = 0
local strict_count = 0
local match_count = 0
local no_symbol_match = false
while input_index <= #input and word_index + word_offset <= #word do
local c1 = string.byte(input, input_index)
local c2 = string.byte(word, word_index + word_offset)
@ -272,6 +304,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1
word_offset = word_offset + 1
no_symbol_match = no_symbol_match or char.is_symbol(c1)
else
-- Match end (partial region)
if input_match_start ~= -1 then
@ -281,6 +314,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
word_match_start = word_index,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false,
}
else
@ -298,6 +332,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
word_match_start = word_index,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false,
}
end

View File

@ -28,8 +28,35 @@ describe('matcher', function()
assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list'))
assert.is.truthy(matcher.match('2', '[[2021') >= 1)
assert.is.truthy(matcher.match(',', 'pri,') == 0)
assert.is.truthy(matcher.match('/', '/**') >= 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' }))
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }) == 0)
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }) >= 1)
assert.is.truthy(matcher.match('emg', 'error_msg') >= 1)
assert.is.truthy(matcher.match('sasr', 'saved_splitright') >= 1)
local score, matches
score, matches = matcher.match('tail', 'HCDetails', {
disallow_fuzzy_matching = false,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
disallow_partial_fuzzy_matching = false,
})
assert.is.truthy(score >= 1)
assert.equals(matches[1].word_match_start, 5)
score = matcher.match('tail', 'HCDetails', {
disallow_fuzzy_matching = false,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
disallow_partial_fuzzy_matching = true,
})
assert.is.truthy(score == 0)
end)
it('disallow_fuzzy_matching', function()
@ -37,6 +64,11 @@ describe('matcher', function()
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1)
end)
it('disallow_fullfuzzy_matching', function()
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0)
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_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)

View File

@ -10,23 +10,23 @@ local pattern = require('cmp.utils.pattern')
local char = require('cmp.utils.char')
---@class cmp.Source
---@field public id number
---@field public id integer
---@field public name string
---@field public source any
---@field public cache cmp.Cache
---@field public revision number
---@field public revision integer
---@field public incomplete boolean
---@field public is_triggered_by_symbol boolean
---@field public entries cmp.Entry[]
---@field public offset number
---@field public request_offset number
---@field public offset integer
---@field public request_offset integer
---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext|nil
---@field public status cmp.SourceStatus
---@field public complete_dedup function
local source = {}
---@alias cmp.SourceStatus "1" | "2" | "3"
---@alias cmp.SourceStatus 1 | 2 | 3
source.SourceStatus = {}
source.SourceStatus.WAITING = 1
source.SourceStatus.FETCHING = 2
@ -46,7 +46,6 @@ source.new = function(name, s)
end
---Reset current completion state
---@return boolean
source.reset = function(self)
self.cache:clear()
self.revision = self.revision + 1
@ -89,86 +88,92 @@ source.get_entries = function(self, ctx)
return {}
end
local target_entries = (function()
local key = { 'get_entries', self.revision }
for i = ctx.cursor.col, self.offset, -1 do
key[3] = string.sub(ctx.cursor_before_line, 1, i)
local prev_entries = self.cache:get(key)
if prev_entries then
return prev_entries
end
local target_entries = self.entries
local prev = self.cache:get({ 'get_entries', tostring(self.revision) })
if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then
if ctx.cursor.col == prev.ctx.cursor.col then
return prev.entries
end
return self.entries
end)()
-- only use prev entries when cursor is moved forward.
-- and the pattern offset is the same.
if prev.ctx.cursor.col <= ctx.cursor.col then
target_entries = prev.entries
end
end
local entry_filter = self:get_entry_filter()
local inputs = {}
---@type cmp.Entry[]
local entries = {}
local matching_config = self:get_matching_config()
for _, e in ipairs(target_entries) do
local o = e:get_offset()
if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o)
end
local match = e:match(inputs[o], self:get_matching_config())
local match = e:match(inputs[o], matching_config)
e.score = match.score
e.exact = false
if e.score >= 1 then
e.matches = match.matches
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o]
table.insert(entries, e)
end
end
self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries)
local max_item_count = self:get_source_config().max_item_count or 200
local limited_entries = {}
for _, e in ipairs(entries) do
table.insert(limited_entries, e)
if max_item_count and #limited_entries >= max_item_count then
break
if entry_filter(e, ctx) then
entries[#entries + 1] = e
end
end
async.yield()
if ctx.aborted then
async.abort()
end
end
return limited_entries
self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset })
return entries
end
---Get default insert range
---@return lsp.Range|nil
---Get default insert range (UTF8 byte index).
---@return lsp.Range
source.get_default_insert_range = function(self)
if not self.context then
return nil
error('context is not initialized yet.')
end
return self.cache:ensure({ 'get_default_insert_range', self.revision }, function()
return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function()
return {
start = {
line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, self.offset),
character = self.offset - 1,
},
['end'] = {
line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, self.context.cursor.col),
character = self.context.cursor.col - 1,
},
}
end)
end
---Get default replace range
---@return lsp.Range|nil
---Get default replace range (UTF8 byte index).
---@return lsp.Range
source.get_default_replace_range = function(self)
if not self.context then
return nil
error('context is not initialized yet.')
end
return self.cache:ensure({ 'get_default_replace_range', self.revision }, function()
return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function()
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
return {
start = {
line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, self.offset),
character = self.offset,
},
['end'] = {
line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, e and self.offset + e - 1 or self.context.cursor.col),
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end)
@ -217,13 +222,16 @@ source.get_keyword_pattern = function(self)
return c.keyword_pattern
end
if self.source.get_keyword_pattern then
return self.source:get_keyword_pattern(misc.copy(c))
local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c))
if keyword_pattern then
return keyword_pattern
end
end
return config.get().completion.keyword_pattern
end
---Get keyword_length
---@return number
---@return integer
source.get_keyword_length = function(self)
local c = self:get_source_config()
if c.keyword_length then
@ -232,10 +240,31 @@ source.get_keyword_length = function(self)
return config.get().completion.keyword_length or 1
end
---Get filter
--@return fun(entry: cmp.Entry, context: cmp.Context): boolean
source.get_entry_filter = function(self)
local c = self:get_source_config()
if c.entry_filter then
return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]]
end
return function(_, _)
return true
end
end
---Get lsp.PositionEncodingKind
---@return lsp.PositionEncodingKind
source.get_position_encoding_kind = function(self)
if self.source.get_position_encoding_kind then
return self.source:get_position_encoding_kind()
end
return types.lsp.PositionEncodingKind.UTF16
end
---Invoke completion
---@param ctx cmp.Context
---@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)
local offset = ctx:get_offset(self:get_keyword_pattern())
@ -260,7 +289,7 @@ source.complete = function(self, ctx, callback)
triggerCharacter = before_char,
}
elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then
if self:get_keyword_length() <= (ctx.cursor.col - offset) then
if offset < ctx.cursor.col and self:get_keyword_length() <= (ctx.cursor.col - offset) then
if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
@ -300,6 +329,10 @@ source.complete = function(self, ctx, callback)
completion_context = completion_context,
}),
self.complete_dedup(vim.schedule_wrap(function(response)
if self.context ~= ctx then
return
end
---@type lsp.CompletionResponse
response = response or {}
self.incomplete = response.isIncomplete or false
@ -312,14 +345,14 @@ source.complete = function(self, ctx, callback)
self.status = source.SourceStatus.COMPLETED
self.entries = {}
for i, item in ipairs(response.items or response) do
if (misc.safe(item) or {}).label then
local e = entry.new(ctx, self, item)
if (item or {}).label then
local e = entry.new(ctx, self, item, response.itemDefaults)
self.entries[i] = e
self.offset = math.min(self.offset, e:get_offset())
end
end
self.revision = self.revision + 1
if #self:get_entries(ctx) == 0 then
if #self.entries == 0 then
self.offset = old_offset
self.entries = old_entries
self.revision = self.revision + 1

View File

@ -1,37 +1,43 @@
local cmp = {}
---@alias cmp.ConfirmBehavior "'insert'" | "'replace'"
cmp.ConfirmBehavior = {}
cmp.ConfirmBehavior.Insert = 'insert'
cmp.ConfirmBehavior.Replace = 'replace'
---@alias cmp.ConfirmBehavior 'insert' | 'replace'
cmp.ConfirmBehavior = {
Insert = 'insert',
Replace = 'replace',
}
---@alias cmp.SelectBehavior "'insert'" | "'select'"
cmp.SelectBehavior = {}
cmp.SelectBehavior.Insert = 'insert'
cmp.SelectBehavior.Select = 'select'
---@alias cmp.SelectBehavior 'insert' | 'select'
cmp.SelectBehavior = {
Insert = 'insert',
Select = 'select',
}
---@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'"
cmp.ContextReason = {}
cmp.ContextReason.Auto = 'auto'
cmp.ContextReason.Manual = 'manual'
cmp.ContextReason.TriggerOnly = 'triggerOnly'
cmp.ContextReason.None = 'none'
---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none'
cmp.ContextReason = {
Auto = 'auto',
Manual = 'manual',
TriggerOnly = 'triggerOnly',
None = 'none',
}
---@alias cmp.TriggerEvent "'InsertEnter'" | "'TextChanged'"
cmp.TriggerEvent = {}
cmp.TriggerEvent.InsertEnter = 'InsertEnter'
cmp.TriggerEvent.TextChanged = 'TextChanged'
---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged'
cmp.TriggerEvent = {
InsertEnter = 'InsertEnter',
TextChanged = 'TextChanged',
}
---@alias cmp.PreselectMode "'item'" | "'None'"
cmp.PreselectMode = {}
cmp.PreselectMode.Item = 'item'
cmp.PreselectMode.None = 'none'
---@alias cmp.PreselectMode 'item' | 'None'
cmp.PreselectMode = {
Item = 'item',
None = 'none',
}
---@alias cmp.ItemField "'abbr'" | "'kind'" | "'menu'"
cmp.ItemField = {}
cmp.ItemField.Abbr = 'abbr'
cmp.ItemField.Kind = 'kind'
cmp.ItemField.Menu = 'menu'
---@alias cmp.ItemField 'abbr' | 'kind' | 'menu'
cmp.ItemField = {
Abbr = 'abbr',
Kind = 'kind',
Menu = 'menu',
}
---@class cmp.ContextOption
---@field public reason cmp.ContextReason|nil
@ -45,22 +51,24 @@ cmp.ItemField.Menu = 'menu'
---@class cmp.SnippetExpansionParams
---@field public body string
---@field public insert_text_mode number
---@field public insert_text_mode integer
---@class cmp.CompleteParams
---@field public reason? cmp.ContextReason
---@field public config? cmp.ConfigSchema
---@class cmp.Setup
---@field public __call fun(c: cmp.ConfigSchema)
---@class cmp.SetupProperty
---@field public buffer 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|string[], c: cmp.ConfigSchema)
---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema)
---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema)
---@class cmp.SourceApiParams: cmp.SourceConfig
---@class cmp.SourceCompletionApiParams : cmp.SourceConfig
---@field public offset number
---@field public offset integer
---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext
@ -71,11 +79,12 @@ cmp.ItemField.Menu = 'menu'
---@field public s nil|function(fallback: function): void
---@class cmp.ConfigSchema
---@field private revision number
---@field public enabled fun():boolean|boolean
---@field private revision integer
---@field public enabled boolean | fun(): boolean
---@field public performance cmp.PerformanceConfig
---@field public preselect cmp.PreselectMode
---@field public completion cmp.CompletionConfig
---@field public documentation cmp.DocumentationConfig|"false"
---@field public window cmp.WindowConfig|nil
---@field public confirmation cmp.ConfirmationConfig
---@field public matching cmp.MatchingConfig
---@field public sorting cmp.SortingConfig
@ -86,19 +95,32 @@ cmp.ItemField.Menu = 'menu'
---@field public view cmp.ViewConfig
---@field public experimental cmp.ExperimentalConfig
---@class cmp.PerformanceConfig
---@field public debounce integer
---@field public throttle integer
---@field public fetching_timeout integer
---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop.
---@field public max_view_entries integer
---@class cmp.WindowConfig
---@field completion cmp.WindowConfig
---@field documentation cmp.WindowConfig|nil
---@class cmp.CompletionConfig
---@field public autocomplete cmp.TriggerEvent[]
---@field public completeopt string
---@field public keyword_pattern string
---@field public keyword_length number
---@field public get_trigger_characters fun(trigger_characters: string[]): string[]
---@field public keyword_length integer
---@field public keyword_pattern string
---@class cmp.DocumentationConfig
---@field public border string[]
---@class cmp.WindowConfig
---@field public border string|string[]
---@field public winhighlight string
---@field public maxwidth number|nil
---@field public maxheight number|nil
---@field public zindex number|nil
---@field public zindex integer|nil
---@field public max_width integer|nil
---@field public max_height integer|nil
---@field public scrolloff integer|nil
---@field public scrollbar boolean|true
---@class cmp.ConfirmationConfig
---@field public default_behavior cmp.ConfirmBehavior
@ -106,23 +128,25 @@ cmp.ItemField.Menu = 'menu'
---@class cmp.MatchingConfig
---@field public disallow_fuzzy_matching boolean
---@field public disallow_fullfuzzy_matching boolean
---@field public disallow_partial_fuzzy_matching boolean
---@field public disallow_partial_matching boolean
---@field public disallow_prefix_unmatching boolean
---@class cmp.SortingConfig
---@field public priority_weight number
---@field public priority_weight integer
---@field public comparators function[]
---@class cmp.FormattingConfig
---@field public fields cmp.ItemField[]
---@field public expandable_indicator boolean
---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem
---@class cmp.SnippetConfig
---@field public expand fun(args: cmp.SnippetExpansionParams)
---@class cmp.ExperimentalConfig
---@field public native_menu boolean
---@field public ghost_text cmp.GhostTextConfig|"false"
---@field public ghost_text cmp.GhostTextConfig|false
---@class cmp.GhostTextConfig
---@field hl_group string
@ -130,12 +154,12 @@ cmp.ItemField.Menu = 'menu'
---@class cmp.SourceConfig
---@field public name string
---@field public option table|nil
---@field public priority number|nil
---@field public priority integer|nil
---@field public trigger_characters string[]|nil
---@field public keyword_pattern string|nil
---@field public keyword_length number|nil
---@field public max_item_count number|nil
---@field public group_index number|nil
---@field public keyword_length integer|nil
---@field public group_index integer|nil
---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean
---@class cmp.ViewConfig
---@field public entries cmp.EntriesConfig
@ -143,14 +167,14 @@ cmp.ItemField.Menu = 'menu'
---@alias cmp.EntriesConfig cmp.CustomEntriesConfig|cmp.NativeEntriesConfig|cmp.WildmenuEntriesConfig|string
---@class cmp.CustomEntriesConfig
---@field name "'custom'"
---@field selection_order "'top_down'"|"'near_cursor'"
---@field name 'custom'
---@field selection_order 'top_down'|'near_cursor'
---@class cmp.NativeEntriesConfig
---@field name "'native'"
---@field name 'native'
---@class cmp.WildmenuEntriesConfig
---@field name "'wildmenu'"
---@field name 'wildmenu'
---@field separator string|nil
return cmp

View File

@ -1,151 +1,234 @@
local misc = require('cmp.utils.misc')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
---@class lsp
local lsp = {}
lsp.Position = {}
---@enum lsp.PositionEncodingKind
lsp.PositionEncodingKind = {
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32',
}
---Convert lsp.Position to vim.Position
---@param buf number|string
---@param position lsp.Position
---@return vim.Position
lsp.Position.to_vim = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false)
if #lines > 0 then
lsp.Position = {
---Convert lsp.Position to vim.Position
---@param buf integer
---@param position lsp.Position
--
---@return vim.Position
to_vim = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false)
if #lines > 0 then
return {
row = position.line + 1,
col = misc.to_vimindex(lines[1], position.character),
}
end
return {
row = position.line + 1,
col = misc.to_vimindex(lines[1], position.character),
col = position.character + 1,
}
end
return {
row = position.line + 1,
col = position.character + 1,
}
end
---Convert vim.Position to lsp.Position
---@param buf number|string
---@param position vim.Position
---@return lsp.Position
lsp.Position.to_lsp = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false)
if #lines > 0 then
end,
---Convert vim.Position to lsp.Position
---@param buf integer
---@param position vim.Position
---@return lsp.Position
to_lsp = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false)
if #lines > 0 then
return {
line = position.row - 1,
character = misc.to_utfindex(lines[1], position.col),
}
end
return {
line = position.row - 1,
character = misc.to_utfindex(lines[1], position.col),
character = position.col - 1,
}
end
return {
line = position.row - 1,
character = position.col - 1,
}
end
end,
lsp.Range = {}
---Convert position to utf8 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf8 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF8 then
return position
end
---Convert lsp.Range to vim.Range
---@param buf number|string
---@param range lsp.Range
---@return vim.Range
lsp.Range.to_vim = function(buf, range)
return {
start = lsp.Position.to_vim(buf, range.start),
['end'] = lsp.Position.to_vim(buf, range['end']),
}
end
local ok, byteindex = pcall(function()
return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16)
end)
if not ok then
return position
end
return { line = position.line, character = byteindex }
end,
---Convert vim.Range to lsp.Range
---@param buf number|string
---@param range vim.Range
---@return lsp.Range
lsp.Range.to_lsp = function(buf, range)
return {
start = lsp.Position.to_lsp(buf, range.start),
['end'] = lsp.Position.to_lsp(buf, range['end']),
}
end
---Convert position to utf16 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf16 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF16 then
return position
end
---@alias lsp.CompletionTriggerKind "1" | "2" | "3"
lsp.CompletionTriggerKind = {}
lsp.CompletionTriggerKind.Invoked = 1
lsp.CompletionTriggerKind.TriggerCharacter = 2
lsp.CompletionTriggerKind.TriggerForIncompleteCompletions = 3
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
for index = utf8.character, 0, -1 do
local ok, utf16index = pcall(function()
return select(2, vim.str_utfindex(text, index))
end)
if ok then
return { line = utf8.line, character = utf16index }
end
end
return position
end,
---Convert position to utf32 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf32 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF32 then
return position
end
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
for index = utf8.character, 0, -1 do
local ok, utf32index = pcall(function()
return select(1, vim.str_utfindex(text, index))
end)
if ok then
return { line = utf8.line, character = utf32index }
end
end
return position
end,
}
lsp.Range = {
---Convert lsp.Range to vim.Range
---@param buf integer
---@param range lsp.Range
---@return vim.Range
to_vim = function(buf, range)
return {
start = lsp.Position.to_vim(buf, range.start),
['end'] = lsp.Position.to_vim(buf, range['end']),
}
end,
---Convert vim.Range to lsp.Range
---@param buf integer
---@param range vim.Range
---@return lsp.Range
to_lsp = function(buf, range)
return {
start = lsp.Position.to_lsp(buf, range.start),
['end'] = lsp.Position.to_lsp(buf, range['end']),
}
end,
}
---@alias lsp.CompletionTriggerKind 1 | 2 | 3
lsp.CompletionTriggerKind = {
Invoked = 1,
TriggerCharacter = 2,
TriggerForIncompleteCompletions = 3,
}
---@alias lsp.InsertTextFormat 1 | 2
lsp.InsertTextFormat = {}
lsp.InsertTextFormat.PlainText = 1
lsp.InsertTextFormat.Snippet = 2
---@alias lsp.InsertTextMode 1 | 2
lsp.InsertTextMode = {
AsIs = 1,
AdjustIndentation = 2,
}
---@alias lsp.MarkupKind 'plaintext' | 'markdown'
lsp.MarkupKind = {
PlainText = 'plaintext',
Markdown = 'markdown',
}
---@alias lsp.CompletionItemTag 1
lsp.CompletionItemTag = {
Deprecated = 1,
}
---@alias lsp.CompletionItemKind 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25
lsp.CompletionItemKind = {
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18,
Folder = 19,
EnumMember = 20,
Constant = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25,
}
lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind)
---@class lsp.internal.CompletionItemDefaults
---@field public commitCharacters? string[]
---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range }
---@field public insertTextFormat? lsp.InsertTextFormat
---@field public insertTextMode? lsp.InsertTextMode
---@field public data? any
---@class lsp.CompletionContext
---@field public triggerKind lsp.CompletionTriggerKind
---@field public triggerCharacter string|nil
---@alias lsp.InsertTextFormat "1" | "2"
lsp.InsertTextFormat = {}
lsp.InsertTextFormat.PlainText = 1
lsp.InsertTextFormat.Snippet = 2
lsp.InsertTextFormat = vim.tbl_add_reverse_lookup(lsp.InsertTextFormat)
---@alias lsp.InsertTextMode "1" | "2"
lsp.InsertTextMode = {}
lsp.InsertTextMode.AsIs = 0
lsp.InsertTextMode.AdjustIndentation = 1
lsp.InsertTextMode = vim.tbl_add_reverse_lookup(lsp.InsertTextMode)
---@alias lsp.MarkupKind "'plaintext'" | "'markdown'"
lsp.MarkupKind = {}
lsp.MarkupKind.PlainText = 'plaintext'
lsp.MarkupKind.Markdown = 'markdown'
lsp.MarkupKind = vim.tbl_add_reverse_lookup(lsp.MarkupKind)
---@alias lsp.CompletionItemTag "1"
lsp.CompletionItemTag = {}
lsp.CompletionItemTag.Deprecated = 1
lsp.CompletionItemTag = vim.tbl_add_reverse_lookup(lsp.CompletionItemTag)
---@alias lsp.CompletionItemKind "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20" | "21" | "22" | "23" | "24" | "25"
lsp.CompletionItemKind = {}
lsp.CompletionItemKind.Text = 1
lsp.CompletionItemKind.Method = 2
lsp.CompletionItemKind.Function = 3
lsp.CompletionItemKind.Constructor = 4
lsp.CompletionItemKind.Field = 5
lsp.CompletionItemKind.Variable = 6
lsp.CompletionItemKind.Class = 7
lsp.CompletionItemKind.Interface = 8
lsp.CompletionItemKind.Module = 9
lsp.CompletionItemKind.Property = 10
lsp.CompletionItemKind.Unit = 11
lsp.CompletionItemKind.Value = 12
lsp.CompletionItemKind.Enum = 13
lsp.CompletionItemKind.Keyword = 14
lsp.CompletionItemKind.Snippet = 15
lsp.CompletionItemKind.Color = 16
lsp.CompletionItemKind.File = 17
lsp.CompletionItemKind.Reference = 18
lsp.CompletionItemKind.Folder = 19
lsp.CompletionItemKind.EnumMember = 20
lsp.CompletionItemKind.Constant = 21
lsp.CompletionItemKind.Struct = 22
lsp.CompletionItemKind.Event = 23
lsp.CompletionItemKind.Operator = 24
lsp.CompletionItemKind.TypeParameter = 25
lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind)
---@class lsp.CompletionList
---@field public isIncomplete boolean
---@field public itemDefaults? lsp.internal.CompletionItemDefaults
---@field public items lsp.CompletionItem[]
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]
---@class lsp.MarkupContent
---@field public kind lsp.MarkupKind
---@field public value string
---@class lsp.Position
---@field public line number
---@field public character number
---@field public line integer
---@field public character integer
---@class lsp.Range
---@field public start lsp.Position
@ -160,34 +243,45 @@ lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind)
---@field public range lsp.Range|nil
---@field public newText string
---@class lsp.InsertReplaceTextEdit
---@field public insert lsp.Range|nil
---@field public replace lsp.Range|nil
---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit
---@class lsp.internal.InsertTextEdit
---@field public insert lsp.Range
---@field public newText string
---@class lsp.internal.ReplaceTextEdit
---@field public replace lsp.Range
---@field public newText string
---@class lsp.CompletionItemLabelDetails
---@field public detail string|nil
---@field public description string|nil
---@field public detail? string
---@field public description? string
---@class lsp.internal.CmpCompletionExtension
---@field public kind_text string
---@field public kind_hl_group string
---@class lsp.CompletionItem
---@field public label string
---@field public labelDetails lsp.CompletionItemLabelDetails|nil
---@field public kind lsp.CompletionItemKind|nil
---@field public tags lsp.CompletionItemTag[]|nil
---@field public detail string|nil
---@field public documentation lsp.MarkupContent|string|nil
---@field public deprecated boolean|nil
---@field public preselect boolean|nil
---@field public sortText string|nil
---@field public filterText string|nil
---@field public insertText string|nil
---@field public insertTextFormat lsp.InsertTextFormat
---@field public insertTextMode lsp.InsertTextMode
---@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil
---@field public additionalTextEdits lsp.TextEdit[]
---@field public commitCharacters string[]|nil
---@field public command lsp.Command|nil
---@field public data any|nil
---@field public labelDetails? lsp.CompletionItemLabelDetails
---@field public kind? lsp.CompletionItemKind
---@field public tags? lsp.CompletionItemTag[]
---@field public detail? string
---@field public documentation? lsp.MarkupContent|string
---@field public deprecated? boolean
---@field public preselect? boolean
---@field public sortText? string
---@field public filterText? string
---@field public insertText? string
---@field public insertTextFormat? lsp.InsertTextFormat
---@field public insertTextMode? lsp.InsertTextMode
---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit
---@field public textEditText? string
---@field public additionalTextEdits? lsp.TextEdit[]
---@field public commitCharacters? string[]
---@field public command? lsp.Command
---@field public data? any
---@field public cmp? lsp.internal.CmpCompletionExtension
---
---TODO: Should send the issue for upstream?
---@field public word string|nil

View File

@ -3,17 +3,17 @@
---@field public abbr string|nil
---@field public kind string|nil
---@field public menu string|nil
---@field public equal "1"|nil
---@field public empty "1"|nil
---@field public dup "1"|nil
---@field public equal 1|nil
---@field public empty 1|nil
---@field public dup 1|nil
---@field public id any
---@field public abbr_hl_group string|nil
---@field public kind_hl_group string|nil
---@field public menu_hl_group string|nil
---@class vim.Position
---@field public row number
---@field public col number
---@class vim.Position 1-based index
---@field public row integer
---@field public col integer
---@class vim.Range
---@field public start vim.Position

View File

@ -44,9 +44,10 @@ api.get_current_line = function()
return vim.api.nvim_get_current_line()
end
---@return { [1]: integer, [2]: integer }
api.get_cursor = function()
if api.is_cmdline_mode() then
return { vim.o.lines - (vim.api.nvim_get_option('cmdheight') or 1) + 1, vim.fn.getcmdpos() - 1 }
return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option('cmdheight') - 1)), vim.fn.getcmdpos() - 1 }
end
return vim.api.nvim_win_get_cursor(0)
end
@ -54,7 +55,7 @@ end
api.get_screen_cursor = function()
if api.is_cmdline_mode() then
local cursor = api.get_cursor()
return { cursor[1], cursor[2] + 1 }
return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) }
end
local cursor = api.get_cursor()
local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1)

View File

@ -4,8 +4,8 @@ local feedkeys = require('cmp.utils.feedkeys')
local api = require('cmp.utils.api')
describe('api', function()
before_each(spec.before)
describe('get_cursor', function()
before_each(spec.before)
it('insert-mode', function()
local cursor
feedkeys.call(keymap.t('i\t1234567890'), 'nx', function()
@ -24,8 +24,26 @@ describe('api', function()
end)
end)
describe('get_screen_cursor', function()
it('insert-mode', function()
local screen_cursor
feedkeys.call(keymap.t('iあいうえお'), 'nx', function()
screen_cursor = api.get_screen_cursor()
end)
assert.are.equal(10, screen_cursor[2])
end)
it('cmdline-mode', function()
local screen_cursor
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
screen_cursor = api.get_screen_cursor()
end, { expr = true, noremap = true })
feedkeys.call(keymap.t(':あいうえお'), 'n')
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
assert.are.equal(10, screen_cursor[2])
end)
end)
describe('get_cursor_before_line', function()
before_each(spec.before)
it('insert-mode', function()
local cursor_before_line
feedkeys.call(keymap.t('i\t1234567890<Left><Left>'), 'nx', function()

View File

@ -1,18 +1,37 @@
local feedkeys = require('cmp.utils.feedkeys')
local config = require('cmp.config')
local async = {}
---@class cmp.AsyncThrottle
---@field public running boolean
---@field public timeout number
---@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil)
---@field public timeout integer
---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil)
---@field public stop function
---@field public __call function
---@type uv_timer_t[]
local timers = {}
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
for _, timer in pairs(timers) do
if timer and not timer:is_closing() then
timer:stop()
timer:close()
end
end
end,
})
---@param fn function
---@param timeout number
---@param timeout integer
---@return cmp.AsyncThrottle
async.throttle = function(fn, timeout)
local time = nil
local timer = vim.loop.new_timer()
local timer = assert(vim.loop.new_timer())
local _async = nil ---@type Async?
timers[#timers + 1] = timer
return setmetatable({
running = false,
timeout = timeout,
@ -21,9 +40,15 @@ async.throttle = function(fn, timeout)
return not self.running
end)
end,
stop = function()
time = nil
stop = function(reset_time)
if reset_time ~= false then
time = nil
end
timer:stop()
if _async then
_async:cancel()
_async = nil
end
end,
}, {
__call = function(self, ...)
@ -34,12 +59,23 @@ async.throttle = function(fn, timeout)
end
self.running = true
timer:stop()
self.stop(false)
timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
vim.schedule(function()
time = nil
fn(unpack(args))
self.running = false
local ret = fn(unpack(args))
if async.is_async(ret) then
---@cast ret Async
_async = ret
_async:await(function(_, error)
self.running = false
if error and error ~= 'abort' then
vim.notify(error, vim.log.levels.ERROR)
end
end)
else
self.running = false
end
end)
end)
end,
@ -60,7 +96,7 @@ end
---Timeout callback function
---@param fn function
---@param timeout number
---@param timeout integer
---@return function
async.timeout = function(fn, timeout)
local timer
@ -109,4 +145,146 @@ async.sync = function(runner, timeout)
end, 10, false)
end
---Wait and callback for next safe state.
async.debounce_next_tick = function(callback)
local running = false
return function()
if running then
return
end
running = true
vim.schedule(function()
running = false
callback()
end)
end
end
---Wait and callback for consuming next keymap.
async.debounce_next_tick_by_keymap = function(callback)
return function()
feedkeys.call('', '', callback)
end
end
local Scheduler = {}
Scheduler._queue = {}
Scheduler._executor = assert(vim.loop.new_check())
function Scheduler.step()
local budget = config.get().performance.async_budget * 1e6
local start = vim.loop.hrtime()
while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do
local a = table.remove(Scheduler._queue, 1)
a:_step()
if a.running then
table.insert(Scheduler._queue, a)
end
end
if #Scheduler._queue == 0 then
return Scheduler._executor:stop()
end
end
---@param a Async
function Scheduler.add(a)
table.insert(Scheduler._queue, a)
if not Scheduler._executor:is_active() then
Scheduler._executor:start(vim.schedule_wrap(Scheduler.step))
end
end
--- @alias AsyncCallback fun(result?:any, error?:string)
--- @class Async
--- @field running boolean
--- @field result? any
--- @field error? string
--- @field callbacks AsyncCallback[]
--- @field thread thread
local Async = {}
Async.__index = Async
function Async.new(fn)
local self = setmetatable({}, Async)
self.callbacks = {}
self.running = true
self.thread = coroutine.create(fn)
Scheduler.add(self)
return self
end
---@param result? any
---@param error? string
function Async:_done(result, error)
self.running = false
self.result = result
self.error = error
for _, callback in ipairs(self.callbacks) do
callback(result, error)
end
end
function Async:_step()
local ok, res = coroutine.resume(self.thread)
if not ok then
return self:_done(nil, res)
elseif res == 'abort' then
return self:_done(nil, 'abort')
elseif coroutine.status(self.thread) == 'dead' then
return self:_done(res)
end
end
function Async:cancel()
self.running = false
end
---@param cb AsyncCallback
function Async:await(cb)
if not cb then
error('callback is required')
end
if self.running then
table.insert(self.callbacks, cb)
else
cb(self.result, self.error)
end
end
function Async:sync()
while self.running do
vim.wait(10)
end
return self.error and error(self.error) or self.result
end
--- @return boolean
function async.is_async(obj)
return obj and type(obj) == 'table' and getmetatable(obj) == Async
end
--- @return fun(...): Async
function async.wrap(fn)
return function(...)
local args = { ... }
return Async.new(function()
return fn(unpack(args))
end)
end
end
-- This will yield when called from a coroutine
function async.yield(...)
if not coroutine.isyieldable() then
error('Trying to yield from a non-yieldable context')
return ...
end
return coroutine.yield(...)
end
function async.abort()
return async.yield('abort')
end
return async

View File

@ -2,20 +2,38 @@ local debug = require('cmp.utils.debug')
local autocmd = {}
autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true })
autocmd.events = {}
---Subscribe autocmd
---@param event string
---@param events string|string[]
---@param callback function
---@return function
autocmd.subscribe = function(event, callback)
autocmd.events[event] = autocmd.events[event] or {}
table.insert(autocmd.events[event], callback)
autocmd.subscribe = function(events, callback)
events = type(events) == 'string' and { events } or events
for _, event in ipairs(events) do
if not autocmd.events[event] then
autocmd.events[event] = {}
vim.api.nvim_create_autocmd(event, {
desc = ('nvim-cmp: autocmd: %s'):format(event),
group = autocmd.group,
callback = function()
autocmd.emit(event)
end,
})
end
table.insert(autocmd.events[event], callback)
end
return function()
for i, callback_ in ipairs(autocmd.events[event]) do
if callback_ == callback then
table.remove(autocmd.events[event], i)
break
for _, event in ipairs(events) do
for i, callback_ in ipairs(autocmd.events[event]) do
if callback_ == callback then
table.remove(autocmd.events[event], i)
break
end
end
end
end

View File

@ -3,7 +3,7 @@ local binary = {}
---Insert item to list to ordered index
---@param list any[]
---@param item any
---@param func fun(a: any, b: any): "1"|"-1"|"0"
---@param func fun(a: any, b: any): 1|-1|0
binary.insort = function(list, item, func)
table.insert(list, binary.search(list, item, func), item)
end
@ -11,8 +11,8 @@ end
---Search suitable index from list
---@param list any[]
---@param item any
---@param func fun(a: any, b: any): "1"|"-1"|"0"
---@return number
---@param func fun(a: any, b: any): 1|-1|0
---@return integer
binary.search = function(list, item, func)
local s = 1
local e = #list

View File

@ -2,7 +2,7 @@ local buffer = {}
buffer.cache = {}
---@return number buf
---@return integer buf
buffer.get = function(name)
local buf = buffer.cache[name]
if buf and vim.api.nvim_buf_is_valid(buf) then
@ -12,7 +12,7 @@ buffer.get = function(name)
end
end
---@return number buf
---@return integer buf
---@return boolean created_new
buffer.ensure = function(name)
local created_new = false
@ -20,8 +20,6 @@ buffer.ensure = function(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

View File

@ -9,7 +9,7 @@ cache.new = function()
end
---Get cache value
---@param key string
---@param key string|string[]
---@return any|nil
cache.get = function(self, key)
key = self:key(key)
@ -20,7 +20,7 @@ cache.get = function(self, key)
end
---Set cache value explicitly
---@param key string
---@param key string|string[]
---@vararg any
cache.set = function(self, key, value)
key = self:key(key)
@ -28,8 +28,10 @@ cache.set = function(self, key, value)
end
---Ensure value by callback
---@param key string
---@param callback fun(): any
---@generic T
---@param key string|string[]
---@param callback fun(): T
---@return T
cache.ensure = function(self, key, callback)
local value = self:get(key)
if value == nil then
@ -46,7 +48,7 @@ cache.clear = function(self)
end
---Create key
---@param key string|table
---@param key string|string[]
---@return string
cache.key = function(_, key)
if type(key) == 'table' then

View File

@ -1,69 +1,71 @@
local _
local alpha = {}
string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
_ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
alpha[string.byte(char)] = true
end)
local ALPHA = {}
string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
_ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
ALPHA[string.byte(char)] = true
end)
local digit = {}
string.gsub('1234567890', '.', function(char)
_ = string.gsub('1234567890', '.', function(char)
digit[string.byte(char)] = true
end)
local white = {}
string.gsub(' \t\n', '.', function(char)
_ = string.gsub(' \t\n', '.', function(char)
white[string.byte(char)] = true
end)
local char = {}
---@param byte number
---@param byte integer
---@return boolean
char.is_upper = function(byte)
return ALPHA[byte]
end
---@param byte number
---@param byte integer
---@return boolean
char.is_alpha = function(byte)
return alpha[byte] or ALPHA[byte]
end
---@param byte number
---@param byte integer
---@return boolean
char.is_digit = function(byte)
return digit[byte]
end
---@param byte number
---@param byte integer
---@return boolean
char.is_white = function(byte)
return white[byte]
end
---@param byte number
---@param byte integer
---@return boolean
char.is_symbol = function(byte)
return not (char.is_alnum(byte) or char.is_white(byte))
end
---@param byte number
---@param byte integer
---@return boolean
char.is_printable = function(byte)
return string.match(string.char(byte), '^%c$') == nil
end
---@param byte number
---@param byte integer
---@return boolean
char.is_alnum = function(byte)
return char.is_alpha(byte) or char.is_digit(byte)
end
---@param text string
---@param index number
---@param index integer
---@return boolean
char.is_semantic_index = function(text, index)
if index <= 1 then
@ -89,8 +91,8 @@ char.is_semantic_index = function(text, index)
end
---@param text string
---@param current_index number
---@return boolean
---@param current_index integer
---@return integer
char.get_next_semantic_index = function(text, current_index)
for i = current_index + 1, #text do
if char.is_semantic_index(text, i) then
@ -101,8 +103,8 @@ char.get_next_semantic_index = function(text, current_index)
end
---Ignore case match
---@param byte1 number
---@param byte2 number
---@param byte1 integer
---@param byte2 integer
---@return boolean
char.match = function(byte1, byte2)
if not char.is_alpha(byte1) or not char.is_alpha(byte2) then

View File

@ -7,22 +7,18 @@ feedkeys.call = setmetatable({
callbacks = {},
}, {
__call = function(self, keys, mode, callback)
if vim.fn.reg_recording() ~= '' then
return feedkeys.call_macro(keys, mode, callback)
end
local is_insert = string.match(mode, 'i') ~= nil
local is_immediate = string.match(mode, 'x') ~= nil
local queue = {}
if #keys > 0 then
table.insert(queue, { keymap.t('<Cmd>set lazyredraw<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>set textwidth=0<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>set eventignore=all<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal lazyredraw<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=0<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=2<CR>'), 'n' })
table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true })
table.insert(queue, { keymap.t('<Cmd>set %slazyredraw<CR>'):format(vim.o.lazyredraw and '' or 'no'), 'n' })
table.insert(queue, { keymap.t('<Cmd>set textwidth=%s<CR>'):format(vim.bo.textwidth or 0), 'n' })
table.insert(queue, { keymap.t('<Cmd>set eventignore=%s<CR>'):format(vim.o.eventignore or ''), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal %slazyredraw<CR>'):format(vim.o.lazyredraw and '' or 'no'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=%s<CR>'):format(vim.bo.textwidth or 0), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=%s<CR>'):format(vim.go.backspace or 2), 'n' })
end
if callback then
@ -54,57 +50,4 @@ misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id)
return ''
end)
feedkeys.call_macro = setmetatable({
queue = {},
current = nil,
timer = vim.loop.new_timer(),
running = false,
}, {
__call = function(self, keys, mode, callback)
local is_insert = string.match(mode, 'i') ~= nil
table.insert(self.queue, is_insert and 1 or #self.queue + 1, {
keys = keys,
mode = mode,
callback = callback,
})
if not self.running then
self.running = true
local consume
consume = vim.schedule_wrap(function()
if vim.fn.getchar(1) == 0 then
if self.current then
vim.cmd(('set backspace=%s'):format(self.current.backspace or ''))
vim.cmd(('set eventignore=%s'):format(self.current.eventignore or ''))
if self.current.callback then
self.current.callback()
end
self.current = nil
end
local current = table.remove(self.queue, 1)
if current then
self.current = {
keys = current.keys,
callback = current.callback,
backspace = vim.o.backspace,
eventignore = vim.o.eventignore,
}
vim.api.nvim_feedkeys(keymap.t('<Cmd>set backspace=start<CR>'), 'n', true)
vim.api.nvim_feedkeys(keymap.t('<Cmd>set eventignore=all<CR>'), 'n', true)
vim.api.nvim_feedkeys(current.keys, string.gsub(current.mode, '[i]', ''), true) -- 'i' flag is manually resolved.
end
end
if #self.queue ~= 0 or self.current then
vim.defer_fn(consume, 1)
else
self.running = false
end
end)
vim.defer_fn(consume, 1)
end
end,
})
return feedkeys

View File

@ -23,6 +23,15 @@ describe('feedkeys', function()
})
end)
it('bacckspace', function()
vim.cmd([[setlocal backspace=0]])
feedkeys.call(keymap.t('iaiueo'), 'nx')
feedkeys.call(keymap.t('a<BS><BS>'), 'nx')
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), {
'aiu',
})
end)
it('testability', function()
feedkeys.call('i', 'n', function()
feedkeys.call('', 'n', function()

View File

@ -1,46 +1,31 @@
local highlight = {}
highlight.keys = {
'gui',
'guifg',
'guibg',
'cterm',
'ctermfg',
'ctermbg',
'fg',
'bg',
'bold',
'italic',
'reverse',
'standout',
'underline',
'undercurl',
'strikethrough',
}
highlight.inherit = function(name, source, override)
local cmd = ('highlight default %s'):format(name)
highlight.inherit = function(name, source, settings)
for _, key in ipairs(highlight.keys) do
if override[key] then
cmd = cmd .. (' %s=%s'):format(key, override[key])
else
local v = highlight.get(source, key)
v = v == '' and 'NONE' or v
cmd = cmd .. (' %s=%s'):format(key, v)
end
end
vim.cmd(cmd)
end
highlight.get = function(source, key)
if key == 'gui' or key == 'cterm' then
local ui = {}
for _, k in ipairs({ 'bold', 'italic', 'reverse', 'inverse', 'standout', 'underline', 'undercurl', 'strikethrough' }) do
if vim.fn.synIDattr(vim.fn.hlID(source), k, key) == 1 then
table.insert(ui, k)
if not settings[key] then
local v = vim.fn.synIDattr(vim.fn.hlID(source), key)
if key == 'fg' or key == 'bg' then
local n = tonumber(v, 10)
v = type(n) == 'number' and n or v
else
v = v == 1
end
settings[key] = v == '' and 'NONE' or v
end
return table.concat(ui, ',')
elseif key == 'guifg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'fg#', 'gui')
elseif key == 'guibg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'bg#', 'gui')
elseif key == 'ctermfg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'fg', 'term')
elseif key == 'ctermbg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'bg', 'term')
end
vim.api.nvim_set_hl(0, name, settings)
end
return highlight

View File

@ -1,4 +1,5 @@
local misc = require('cmp.utils.misc')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local keymap = {}
@ -16,12 +17,16 @@ end
---@param keys string
---@return string
keymap.normalize = function(keys)
vim.api.nvim_set_keymap('t', '<Plug>(cmp.utils.keymap.normalize)', keys, {})
for _, map in ipairs(vim.api.nvim_get_keymap('t')) do
if keymap.equals(map.lhs, '<Plug>(cmp.utils.keymap.normalize)') then
return map.rhs
local normalize_buf = buffer.ensure('cmp.util.keymap.normalize')
vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '<Plug>(cmp.utils.keymap.normalize)', {})
for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do
if keymap.t(map.rhs) == keymap.t('<Plug>(cmp.utils.keymap.normalize)') then
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
return map.lhs
end
end
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
vim.api.nvim_buf_delete(normalize_buf, {})
return keys
end
@ -64,7 +69,7 @@ keymap.undojoin = function()
end
---Create backspace keys.
---@param count number
---@param count string|integer
---@return string
keymap.backspace = function(count)
if type(count) == 'string' then
@ -78,8 +83,23 @@ keymap.backspace = function(count)
return table.concat(keys, '')
end
---Create delete keys.
---@param count string|integer
---@return string
keymap.delete = function(count)
if type(count) == 'string' then
count = vim.fn.strchars(count, true)
end
if count <= 0 then
return ''
end
local keys = {}
table.insert(keys, keymap.t(string.rep('<Del>', count)))
return table.concat(keys, '')
end
---Update indentkeys.
---@param expr string
---@param expr? string
---@return string
keymap.indentkeys = function(expr)
return string.format(keymap.t('<Cmd>set indentkeys=%s<CR>'), expr and vim.fn.escape(expr, '| \t\\') or '')
@ -90,7 +110,7 @@ end
---@param b string
---@return boolean
keymap.equals = function(a, b)
return keymap.t(a) == keymap.t(b)
return keymap.normalize(a) == keymap.normalize(b)
end
---Register keypress handler.
@ -98,8 +118,7 @@ keymap.listen = function(mode, lhs, callback)
lhs = keymap.normalize(keymap.to_keymap(lhs))
local existing = keymap.get_map(mode, lhs)
local id = string.match(existing.rhs, 'v:lua%.cmp%.utils%.keymap%.set_map%((%d+)%)')
if id and keymap.set_map.callbacks[tonumber(id, 10)] then
if existing.desc == 'cmp.utils.keymap.set_map' then
return
end
@ -124,8 +143,8 @@ end
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()
local fallback_lhs = string.format('<Plug>(cmp.u.k.fallback_expr:%s)', map.lhs)
keymap.set_map(bufnr, mode, fallback_lhs, function()
return keymap.solve(bufnr, mode, map).keys
end, {
expr = true,
@ -133,13 +152,14 @@ keymap.fallback = function(bufnr, mode, map)
script = map.script,
nowait = map.nowait,
silent = map.silent and mode ~= 'c',
replace_keycodes = map.replace_keycodes,
})
vim.api.nvim_feedkeys(keymap.t(fallback_expr), 'im', true)
elseif not map.callback then
vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true)
elseif map.callback then
map.callback()
else
local solved = keymap.solve(bufnr, mode, map)
vim.api.nvim_feedkeys(solved.keys, solved.mode, true)
else
map.callback()
end
end
end
@ -147,7 +167,14 @@ 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)
local rhs = keymap.t(map.rhs)
if map.expr then
if map.callback then
rhs = map.callback()
else
rhs = vim.api.nvim_eval(keymap.t(map.rhs))
end
end
if map.noremap then
return { keys = rhs, mode = 'in' }
@ -157,9 +184,10 @@ keymap.solve = function(bufnr, mode, map)
local recursive = string.format('<SNR>0_(cmp.u.k.recursive:%s)', lhs)
keymap.set_map(bufnr, mode, recursive, lhs, {
noremap = true,
script = map.script,
script = true,
nowait = map.nowait,
silent = map.silent and mode ~= 'c',
replace_keycodes = map.replace_keycodes,
})
return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' }
end
@ -180,11 +208,13 @@ keymap.get_map = function(mode, lhs)
rhs = map.rhs or '',
expr = map.expr == 1,
callback = map.callback,
desc = map.desc,
noremap = map.noremap == 1,
script = map.script == 1,
silent = map.silent == 1,
nowait = map.nowait == 1,
buffer = true,
replace_keycodes = map.replace_keycodes == 1,
}
end
end
@ -196,11 +226,13 @@ keymap.get_map = function(mode, lhs)
rhs = map.rhs or '',
expr = map.expr == 1,
callback = map.callback,
desc = map.desc,
noremap = map.noremap == 1,
script = map.script == 1,
silent = map.silent == 1,
nowait = map.nowait == 1,
buffer = false,
replace_keycodes = map.replace_keycodes == 1,
}
end
end
@ -215,33 +247,27 @@ keymap.get_map = function(mode, lhs)
silent = true,
nowait = false,
buffer = false,
replace_keycodes = true,
}
end
---Set keymapping
keymap.set_map = setmetatable({
callbacks = {},
}, {
__call = function(self, bufnr, mode, lhs, rhs, opts)
if type(rhs) == 'function' then
local id = misc.id('cmp.utils.keymap.set_map')
self.callbacks[id] = rhs
if opts.expr then
rhs = ('v:lua.cmp.utils.keymap.set_map(%s)'):format(id)
else
rhs = ('<Cmd>call v:lua.cmp.utils.keymap.set_map(%s)<CR>'):format(id)
end
end
keymap.set_map = function(bufnr, mode, lhs, rhs, opts)
if type(rhs) == 'function' then
opts.callback = rhs
rhs = ''
end
opts.desc = 'cmp.utils.keymap.set_map'
if bufnr == -1 then
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
else
vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
end
end,
})
misc.set(_G, { 'cmp', 'utils', 'keymap', 'set_map' }, function(id)
return keymap.set_map.callbacks[id]() or ''
end)
if vim.fn.has('nvim-0.8') == 0 then
opts.replace_keycodes = nil
end
if bufnr == -1 then
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
else
vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
end
end
return keymap

View File

@ -29,6 +29,24 @@ misc.concat = function(list1, list2)
return new_list
end
---Repeat values
---@generic T
---@param str_or_tbl T
---@param count integer
---@return T
misc.rep = function(str_or_tbl, count)
if type(str_or_tbl) == 'string' then
return string.rep(str_or_tbl, count)
end
local rep = {}
for _ = 1, count do
for _, v in ipairs(str_or_tbl) do
table.insert(rep, v)
end
end
return rep
end
---Return the valu is empty or not.
---@param v any
---@return boolean
@ -56,42 +74,38 @@ misc.none = vim.NIL
---Merge two tables recursively
---@generic T
---@param v1 T
---@param v2 T
---@param tbl1 T
---@param tbl2 T
---@return T
misc.merge = function(v1, v2)
local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1))
local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2))
if merge1 and merge2 then
misc.merge = function(tbl1, tbl2)
local is_dict1 = type(tbl1) == 'table' and (not vim.tbl_islist(tbl1) or vim.tbl_isempty(tbl1))
local is_dict2 = type(tbl2) == 'table' and (not vim.tbl_islist(tbl2) or vim.tbl_isempty(tbl2))
if is_dict1 and is_dict2 then
local new_tbl = {}
for k, v in pairs(v2) do
new_tbl[k] = misc.merge(v1[k], v)
for k, v in pairs(tbl2) do
if tbl1[k] ~= misc.none then
new_tbl[k] = misc.merge(tbl1[k], v)
end
end
for k, v in pairs(v1) do
if v2[k] == nil and v ~= misc.none then
new_tbl[k] = v
for k, v in pairs(tbl1) do
if tbl2[k] == nil then
if v ~= misc.none then
new_tbl[k] = misc.merge(v, {})
else
new_tbl[k] = nil
end
end
end
return new_tbl
end
if v1 == misc.none then
return nil
end
if v1 == nil then
if v2 == misc.none then
return nil
else
return v2
end
end
if v1 == true then
if merge2 then
return v2
end
return {}
end
return v1
if tbl1 == misc.none then
return nil
elseif tbl1 == nil then
return misc.merge(tbl2, {})
else
return tbl1
end
end
---Generate id for group name
@ -105,22 +119,12 @@ misc.id = setmetatable({
end,
})
---Check the value is nil or not.
---@param v boolean
---@return boolean
misc.safe = function(v)
if v == nil or v == vim.NIL then
return nil
end
return v
end
---Treat 1/0 as bool value
---@param v boolean|"1"|"0"
---@param v boolean|1|0
---@param def boolean
---@return boolean
misc.bool = function(v, def)
if misc.safe(v) == nil then
if v == nil then
return def
end
return v == true or v == 1
@ -134,7 +138,7 @@ misc.set = function(t, keys, v)
local c = t
for i = 1, #keys - 1 do
local key = keys[i]
c[key] = misc.safe(c[key]) or {}
c[key] = c[key] or {}
c = c[key]
end
c[keys[#keys]] = v
@ -166,8 +170,8 @@ end
---Safe version of vim.str_utfindex
---@param text string
---@param vimindex number|nil
---@return number
---@param vimindex integer|nil
---@return integer
misc.to_utfindex = function(text, vimindex)
vimindex = vimindex or #text + 1
return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text)))
@ -175,8 +179,8 @@ end
---Safe version of vim.str_byteindex
---@param text string
---@param utfindex number
---@return number
---@param utfindex integer
---@return integer
misc.to_vimindex = function(text, utfindex)
utfindex = utfindex or #text
for i = utfindex, 1, -1 do
@ -206,12 +210,14 @@ end
misc.redraw = setmetatable({
doing = false,
force = false,
termcode = vim.api.nvim_replace_termcodes('<C-r><Esc>', true, true, true),
-- We use `<Up><Down>` to redraw the screen. (Previously, We use <C-r><ESC>. it will remove the unmatches search history.)
incsearch_redraw_keys = ' <BS>',
}, {
__call = function(self, force)
local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true)
if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then
if vim.o.incsearch then
return vim.api.nvim_feedkeys(self.termcode, 'in', true)
return vim.api.nvim_feedkeys(termcode, 'ni', true)
end
end

View File

@ -16,6 +16,18 @@ describe('misc', function()
})
assert.are.equal(merged.a.b, 1)
merged = misc.merge({
a = {
i = 1,
},
}, {
a = {
c = 2,
},
})
assert.are.equal(merged.a.i, 1)
assert.are.equal(merged.a.c, 2)
merged = misc.merge({
a = false,
}, {

View File

@ -0,0 +1,25 @@
local M = {}
-- Set window option without triggering the OptionSet event
---@param window number
---@param name string
---@param value any
M.win_set_option = function(window, name, value)
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore:append('OptionSet')
vim.api.nvim_win_set_option(window, name, value)
vim.opt.eventignore = eventignore
end
-- Set buffer option without triggering the OptionSet event
---@param buffer number
---@param name string
---@param value any
M.buf_set_option = function(buffer, name, value)
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore:append('OptionSet')
vim.api.nvim_buf_set_option(buffer, name, value)
vim.opt.eventignore = eventignore
end
return M

View File

@ -1,5 +1,4 @@
local char = require('cmp.utils.char')
local pattern = require('cmp.utils.pattern')
local str = {}
@ -73,23 +72,6 @@ str.remove_suffix = function(text, suffix)
return string.sub(text, 1, -#suffix - 1)
end
---strikethrough
---@param text string
---@return string
str.strikethrough = function(text)
local r = pattern.regex('.')
local buffer = ''
while text ~= '' do
local s, e = r:match_str(text)
if not s then
break
end
buffer = buffer .. string.sub(text, s, e) .. '̶'
text = string.sub(text, e + 1)
end
return buffer
end
---trim
---@param text string
---@return string
@ -117,8 +99,8 @@ end
---get_word
---@param text string
---@param stop_char number
---@param min_length number
---@param stop_char integer
---@param min_length integer
---@return string
str.get_word = function(text, stop_char, min_length)
min_length = min_length or 0

View File

@ -12,10 +12,6 @@ describe('utils.str', function()
assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps')
end)
it('strikethrough', function()
assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶')
end)
it('remove_suffix', function()
assert.are.equal(str.remove_suffix('log()', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()')

View File

@ -1,25 +1,26 @@
local cache = require('cmp.utils.cache')
local misc = require('cmp.utils.misc')
local opt = require('cmp.utils.options')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local config = require('cmp.config')
---@class cmp.WindowStyle
---@field public relative string
---@field public row number
---@field public col number
---@field public width number
---@field public height number
---@field public zindex number|nil
---@field public row integer
---@field public col integer
---@field public width integer|float
---@field public height integer|float
---@field public border string|string[]|nil
---@field public zindex integer|nil
---@class cmp.Window
---@field public name string
---@field public win number|nil
---@field public swin1 number|nil
---@field public swin2 number|nil
---@field public win integer|nil
---@field public thumb_win integer|nil
---@field public sbar_win integer|nil
---@field public style cmp.WindowStyle
---@field public opt table<string, any>
---@field public buffer_opt table<string, any>
---@field public cache cmp.Cache
local window = {}
---new
@ -28,10 +29,9 @@ window.new = function()
local self = setmetatable({}, { __index = window })
self.name = misc.id('cmp.utils.window.new')
self.win = nil
self.swin1 = nil
self.swin2 = nil
self.sbar_win = nil
self.thumb_win = nil
self.style = {}
self.cache = cache.new()
self.opt = {}
self.buffer_opt = {}
return self
@ -52,7 +52,7 @@ window.option = function(self, key, value)
self.opt[key] = value
if self:visible() then
vim.api.nvim_win_set_option(self.win, key, value)
opt.win_set_option(self.win, key, value)
end
end
@ -72,30 +72,35 @@ window.buffer_option = function(self, key, value)
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)
opt.buf_set_option(existing_buf, key, value)
end
end
---Set style.
---@param style cmp.WindowStyle
window.set_style = function(self, style)
if vim.o.columns and vim.o.columns <= style.col + style.width then
style.width = vim.o.columns - style.col - 1
end
if vim.o.lines and vim.o.lines <= style.row + style.height then
style.height = vim.o.lines - style.row - 1
end
self.style = style
local info = self:info()
if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then
self.style.height = vim.o.lines - info.row - info.border_info.vert - 1
end
self.style.zindex = self.style.zindex or 1
--- GUI clients are allowed to return fractional bounds, but we need integer
--- bounds to open the window
self.style.width = math.ceil(self.style.width)
self.style.height = math.ceil(self.style.height)
end
---Return buffer id.
---@return number
---@return integer
window.get_buffer = function(self)
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)
opt.buf_set_option(buf, k, v)
end
end
return buf
@ -119,7 +124,7 @@ window.open = function(self, style)
s.noautocmd = true
self.win = vim.api.nvim_open_win(self:get_buffer(), false, s)
for k, v in pairs(self.opt) do
vim.api.nvim_win_set_option(self.win, k, v)
opt.win_set_option(self.win, k, v)
end
end
self:update()
@ -127,49 +132,57 @@ end
---Update
window.update = function(self)
if self:has_scrollbar() then
local total = self:get_content_height()
local info = self:info()
local bar_height = math.ceil(info.height * (info.height / total))
local bar_offset = math.min(info.height - bar_height, math.floor(info.height * (vim.fn.getwininfo(self.win)[1].topline / total)))
local style1 = {}
style1.relative = 'editor'
style1.style = 'minimal'
style1.width = 1
style1.height = info.height
style1.row = info.row
style1.col = info.col + info.width - (info.has_scrollbar and 1 or 0)
style1.zindex = (self.style.zindex and (self.style.zindex + 1) or 1)
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_set_config(self.swin1, style1)
else
style1.noautocmd = true
self.swin1 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf1'), false, style1)
vim.api.nvim_win_set_option(self.swin1, 'winhighlight', 'EndOfBuffer:PmenuSbar,Normal:PmenuSbar,NormalNC:PmenuSbar,NormalFloat:PmenuSbar')
local info = self:info()
if info.scrollable then
-- Draw the background of the scrollbar
if not info.border_info.visible then
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = self.style.height,
row = info.row,
col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 1) or 1),
}
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_set_config(self.sbar_win, style)
else
style.noautocmd = true
self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style)
opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar')
end
end
local style2 = {}
style2.relative = 'editor'
style2.style = 'minimal'
style2.width = 1
style2.height = bar_height
style2.row = info.row + bar_offset
style2.col = info.col + info.width - (info.has_scrollbar and 1 or 0)
style2.zindex = (self.style.zindex and (self.style.zindex + 2) or 2)
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_set_config(self.swin2, style2)
-- Draw the scrollbar thumb
local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5)
local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height()))
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = math.max(1, thumb_height),
row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0),
col = info.col + info.width - 1, -- info.col was already added scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 2) or 2),
}
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_set_config(self.thumb_win, style)
else
style2.noautocmd = true
self.swin2 = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbuf2'), false, style2)
vim.api.nvim_win_set_option(self.swin2, 'winhighlight', 'EndOfBuffer:PmenuThumb,Normal:PmenuThumb,NormalNC:PmenuThumb,NormalFloat:PmenuThumb')
style.noautocmd = true
self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style)
opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb')
end
else
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_hide(self.swin1)
self.swin1 = nil
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_hide(self.swin2)
self.swin2 = nil
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
end
end
@ -188,13 +201,13 @@ window.close = function(self)
vim.api.nvim_win_hide(self.win)
self.win = nil
end
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_hide(self.swin1)
self.swin1 = nil
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_hide(self.swin2)
self.swin2 = nil
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
end
end
end
@ -204,91 +217,102 @@ window.visible = function(self)
return self.win and vim.api.nvim_win_is_valid(self.win)
end
---Return the scrollbar will shown or not.
window.has_scrollbar = function(self)
return (self.style.height or 0) < self:get_content_height()
end
---Return win info.
window.info = function(self)
local border_width = self:get_border_width()
local has_scrollbar = self:has_scrollbar()
return {
local border_info = self:get_border_info()
local scrollbar = config.get().window.completion.scrollbar
local info = {
row = self.style.row,
col = self.style.col,
width = self.style.width + border_width + (has_scrollbar and 1 or 0),
height = self.style.height,
border_width = border_width,
has_scrollbar = has_scrollbar,
width = self.style.width + border_info.left + border_info.right,
height = self.style.height + border_info.top + border_info.bottom,
inner_width = self.style.width,
inner_height = self.style.height,
border_info = border_info,
scrollable = false,
scrollbar_offset = 0,
}
if self:get_content_height() > info.inner_height and scrollbar then
info.scrollable = true
if not border_info.visible then
info.scrollbar_offset = 1
info.width = info.width + 1
end
end
return info
end
---Get border width
---@return number
window.get_border_width = function(self)
---Return border information.
---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean }
window.get_border_info = function(self)
local border = self.style.border
if type(border) == 'table' then
local new_border = {}
while #new_border < 8 do
for _, b in ipairs(border) do
table.insert(new_border, b)
end
if not border or border == 'none' then
return {
top = 0,
left = 0,
right = 0,
bottom = 0,
vert = 0,
horiz = 0,
visible = false,
}
end
if type(border) == 'string' then
if border == 'shadow' then
return {
top = 0,
left = 0,
right = 1,
bottom = 1,
vert = 1,
horiz = 1,
visible = false,
}
end
border = new_border
return {
top = 1,
left = 1,
right = 1,
bottom = 1,
vert = 2,
horiz = 2,
visible = true,
}
end
local w = 0
if border then
if type(border) == 'string' then
if border == 'single' then
w = 2
elseif border == 'solid' then
w = 2
elseif border == 'double' then
w = 2
elseif border == 'rounded' then
w = 2
elseif border == 'shadow' then
w = 1
end
elseif type(border) == 'table' then
local b4 = type(border[4]) == 'table' and border[4][1] or border[4]
if #b4 > 0 then
w = w + 1
end
local b8 = type(border[8]) == 'table' and border[8][1] or border[8]
if #b8 > 0 then
w = w + 1
end
local new_border = {}
while #new_border <= 8 do
for _, b in ipairs(border) do
table.insert(new_border, type(b) == 'string' and b or b[1])
end
end
return w
local info = {}
info.top = new_border[2] == '' and 0 or 1
info.right = new_border[4] == '' and 0 or 1
info.bottom = new_border[6] == '' and 0 or 1
info.left = new_border[8] == '' and 0 or 1
info.vert = info.top + info.bottom
info.horiz = info.left + info.right
info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8]))
return info
end
---Get scroll height.
---@return number
---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view).
---@return integer
window.get_content_height = function(self)
if not self:option('wrap') then
return vim.api.nvim_buf_line_count(self:get_buffer())
end
return self.cache:ensure({
'get_content_height',
self.style.width,
self:get_buffer(),
vim.api.nvim_buf_get_changedtick(self:get_buffer()),
}, function()
local height = 0
local buf = self:get_buffer()
-- The result of vim.fn.strdisplaywidth depends on the buffer it was called
-- in (see comment in cmp.Entry.get_view).
vim.api.nvim_buf_call(buf, function()
for _, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width)
end
end)
return height
local height = 0
vim.api.nvim_buf_call(self:get_buffer(), function()
for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do
height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width))
end
end)
return height
end
return window

View File

@ -47,6 +47,7 @@ end
---Open menu
---@param ctx cmp.Context
---@param sources cmp.Source[]
---@return boolean did_open
view.open = function(self, ctx, sources)
local source_group_map = {}
for _, s in ipairs(sources) do
@ -104,10 +105,15 @@ view.open = function(self, ctx, sources)
end
end
end)
local max_item_count = config.get().performance.max_view_entries or 200
entries = vim.list_slice(entries, 1, max_item_count)
-- open
if #entries > 0 then
self:_get_entries_view():open(offset, entries)
self.event:emit('menu_opened', {
window = self:_get_entries_view(),
})
break
end
end
@ -116,6 +122,7 @@ view.open = function(self, ctx, sources)
if #entries == 0 then
self:close()
end
return #entries > 0
end
---Close menu
@ -128,6 +135,9 @@ view.close = function(self)
self:_get_entries_view():close()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self:_get_entries_view(),
})
end
---Abort menu
@ -135,6 +145,9 @@ view.abort = function(self)
self:_get_entries_view():abort()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self:_get_entries_view(),
})
end
---Return the view is visible or not.
@ -144,7 +157,7 @@ view.visible = function(self)
end
---Scroll documentation window if possible.
---@param delta number
---@param delta integer
view.scroll_docs = function(self, delta)
self.docs_view:scroll(delta)
end

View File

@ -8,13 +8,11 @@ local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api')
local SIDE_PADDING = 1
local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
---@class cmp.CustomEntriesView
---@field private entries_win cmp.Window
---@field private offset number
---@field private offset integer
---@field private active boolean
---@field private entries cmp.Entry[]
---@field private column_width any
@ -25,20 +23,21 @@ custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_
custom_entries_view.new = function()
local self = setmetatable({}, { __index = custom_entries_view })
self.entries_win = window.new()
self.entries_win:option('conceallevel', 2)
self.entries_win:option('concealcursor', 'n')
self.entries_win:option('cursorlineopt', 'line')
self.entries_win:option('foldenable', false)
self.entries_win:option('wrap', false)
self.entries_win:option('scrolloff', 0)
self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None')
-- This is done so that strdisplaywidth calculations for lines in the
-- custom_entries_view window exactly match with what is really displayed,
-- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be
-- always rendered one column wide, which removes the unpredictability coming
-- from variable width of the tab character.
self.entries_win:buffer_option('tabstop', 1)
self.entries_win:buffer_option('filetype', 'cmp_menu')
self.entries_win:buffer_option('buftype', 'nofile')
self.event = event.new()
self.offset = -1
self.active = false
@ -65,7 +64,7 @@ custom_entries_view.new = function()
local e = self.entries[i + 1]
if e then
local v = e:get_view(self.offset, buf)
local o = SIDE_PADDING
local o = config.get().window.completion.side_padding
local a = 0
for _, field in ipairs(fields) do
if field == types.cmp.ItemField.Abbr then
@ -118,17 +117,15 @@ custom_entries_view.is_direction_top_down = function(self)
end
custom_entries_view.open = function(self, offset, entries)
local completion = config.get().window.completion
self.offset = offset
self.entries = {}
self.column_width = { abbr = 0, kind = 0, menu = 0 }
-- Apply window options (that might be changed) on the custom completion menu.
self.entries_win:option('winblend', vim.o.pumblend)
local entries_buf = self.entries_win:get_buffer()
local lines = {}
local dedup = {}
local preselect = 0
local preselect_index = 0
for _, e in ipairs(entries) do
local view = e:get_view(offset, entries_buf)
if view.dup == 1 or not dedup[e.completion_item.label] then
@ -138,8 +135,8 @@ custom_entries_view.open = function(self, offset, entries)
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
table.insert(self.entries, e)
table.insert(lines, ' ')
if preselect == 0 and e.completion_item.preselect then
preselect = #self.entries
if preselect_index == 0 and e.completion_item.preselect then
preselect_index = #self.entries
end
end
end
@ -157,18 +154,26 @@ custom_entries_view.open = function(self, offset, entries)
height = math.min(height, #self.entries)
local pos = api.get_screen_cursor()
local cursor = api.get_cursor()
local delta = cursor[2] + 1 - self.offset
local has_bottom_space = (vim.o.lines - pos[1]) >= DEFAULT_HEIGHT
local cursor_before_line = api.get_cursor_before_line()
local delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset))
local row, col = pos[1], pos[2] - delta - 1
if not has_bottom_space and math.floor(vim.o.lines * 0.5) <= row and vim.o.lines - row <= height then
local border_info = window.get_border_info({ style = completion })
local border_offset_row = border_info.top + border_info.bottom
local border_offset_col = border_info.left + border_info.right
if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then
height = math.min(height, row - 1)
row = row - height - 1
row = row - height - border_offset_row - 1
if row < 0 then
height = height + row
end
end
if math.floor(vim.o.columns * 0.5) <= col and vim.o.columns - col <= width then
if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then
width = math.min(width, vim.o.columns - 1)
col = vim.o.columns - width - 1
col = vim.o.columns - width - border_offset_col - 1
if col < 0 then
width = width + col
end
end
if pos[1] > row then
@ -182,35 +187,40 @@ custom_entries_view.open = function(self, offset, entries)
for i = 1, math.floor(n / 2) do
self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i]
end
if preselect ~= 0 then
preselect = #self.entries - preselect + 1
if preselect_index ~= 0 then
preselect_index = #self.entries - preselect_index + 1
end
end
-- Apply window options (that might be changed) on the custom completion menu.
self.entries_win:option('winblend', vim.o.pumblend)
self.entries_win:option('winhighlight', completion.winhighlight)
self.entries_win:option('scrolloff', completion.scrolloff)
self.entries_win:open({
relative = 'editor',
style = 'minimal',
row = math.max(0, row),
col = math.max(0, col),
col = math.max(0, col + completion.col_offset),
width = width,
height = height,
zindex = 1001,
border = completion.border,
zindex = completion.zindex or 1001,
})
-- always set cursor when starting. It will be adjusted on the call to _select
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 })
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select })
if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false })
elseif not string.match(config.get().completion.completeopt, 'noselect') then
if self:is_direction_top_down() then
self:_select(1, { behavior = types.cmp.SelectBehavior.Select })
self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false })
else
self:_select(#self.entries - 1, { behavior = types.cmp.SelectBehavior.Select })
self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false })
end
else
if self:is_direction_top_down() then
self:_select(0, { behavior = types.cmp.SelectBehavior.Select })
self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false })
else
self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select })
self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false })
end
end
end
@ -245,12 +255,12 @@ custom_entries_view.draw = function(self)
if e then
local view = e:get_view(self.offset, entries_buf)
local text = {}
table.insert(text, string.rep(' ', SIDE_PADDING))
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
for _, field in ipairs(fields) do
table.insert(text, view[field].text)
table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width))
end
table.insert(text, string.rep(' ', SIDE_PADDING))
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
table.insert(texts, table.concat(text, ''))
end
end
@ -275,34 +285,74 @@ end
custom_entries_view.select_next_item = function(self, option)
if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
if self:is_direction_top_down() then
cursor = cursor + 1
else
cursor = cursor - 1
end
local is_top_down = self:is_direction_top_down()
local last = #self.entries
if not self.entries_win:option('cursorline') then
cursor = (self:is_direction_top_down() and 1) or #self.entries
elseif #self.entries < cursor then
cursor = (not self:is_direction_top_down() and #self.entries + 1) or 0
cursor = (is_top_down and 1) or last
else
if is_top_down then
if cursor == last then
cursor = 0
else
cursor = cursor + option.count
if last < cursor then
cursor = last
end
end
else
if cursor == 0 then
cursor = last
else
cursor = cursor - option.count
if cursor < 0 then
cursor = 0
end
end
end
end
self:_select(cursor, option)
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end
end
custom_entries_view.select_prev_item = function(self, option)
if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
if self:is_direction_top_down() then
cursor = cursor - 1
else
cursor = cursor + 1
end
local is_top_down = self:is_direction_top_down()
local last = #self.entries
if not self.entries_win:option('cursorline') then
cursor = (self:is_direction_top_down() and #self.entries) or 1
elseif #self.entries < cursor then
cursor = (not self:is_direction_top_down() and 0) or #self.entries + 1
cursor = (is_top_down and last) or 1
else
if is_top_down then
if cursor == 1 then
cursor = 0
else
cursor = cursor - option.count
if cursor < 0 then
cursor = 1
end
end
else
if cursor == last then
cursor = 0
else
cursor = cursor + option.count
if last < cursor then
cursor = last
end
end
end
end
self:_select(cursor, option)
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end
end
@ -343,10 +393,9 @@ custom_entries_view._select = function(self, cursor, option)
if is_insert and not self.active then
self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or ''
end
self.active = (0 < cursor and cursor <= #self.entries and option.active == true)
self.active = cursor > 0 and cursor <= #self.entries and is_insert
self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries)
vim.api.nvim_win_set_cursor(self.entries_win.win, {
math.max(math.min(cursor, #self.entries), 1),
0,
@ -368,7 +417,17 @@ custom_entries_view._insert = setmetatable({
word = word or ''
if api.is_cmdline_mode() then
local cursor = api.get_cursor()
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
-- setcmdline() added in v0.8.0
if vim.fn.has('nvim-0.8') == 1 then
local current_line = api.get_current_line()
local before_line = current_line:sub(1, self.offset - 1)
local after_line = current_line:sub(cursor[2] + 1)
local pos = #before_line + #word + 1
vim.fn.setcmdline(before_line .. word .. after_line, pos)
vim.api.nvim_feedkeys(keymap.t('<Cmd>redraw<CR>'), 'ni', false)
else
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
end
else
if this.pending then
return

View File

@ -15,7 +15,10 @@ docs_view.new = function()
self.window:option('foldenable', false)
self.window:option('linebreak', true)
self.window:option('scrolloff', 0)
self.window:option('showbreak', 'NONE')
self.window:option('wrap', true)
self.window:buffer_option('filetype', 'cmp_docs')
self.window:buffer_option('buftype', 'nofile')
return self
end
@ -23,7 +26,7 @@ end
---@param e cmp.Entry
---@param view cmp.WindowStyle
docs_view.open = function(self, e, view)
local documentation = config.get().documentation
local documentation = config.get().window.documentation
if not documentation then
return
end
@ -32,11 +35,12 @@ docs_view.open = function(self, e, view)
return self:close()
end
local right_space = vim.o.columns - (view.col + view.width) - 2
local left_space = view.col - 2
local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space) - 1)
local border_info = window.get_border_info({ style = documentation })
local right_space = vim.o.columns - (view.col + view.width) - 1
local left_space = view.col - 1
local max_width = math.min(documentation.max_width, math.max(left_space, right_space))
-- update buffer content if needed.
-- Update buffer content if needed.
if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation()
if #documents == 0 then
@ -46,24 +50,29 @@ docs_view.open = function(self, e, view)
self.entry = e
vim.api.nvim_buf_call(self.window:get_buffer(), function()
vim.cmd([[syntax clear]])
vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {})
end)
vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, {
max_width = maxwidth,
max_height = documentation.maxheight,
max_width = max_width - border_info.horiz,
max_height = documentation.max_height,
})
end
-- Set buffer as not modified, so it can be removed without errors
vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false)
-- Calculate window size.
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), {
max_width = maxwidth,
max_height = documentation.maxheight,
max_width = max_width - border_info.horiz,
max_height = documentation.max_height - border_info.vert,
})
if width <= 0 or height <= 0 then
return self:close()
end
-- Calculate window position.
local right_col = view.col + view.width
local left_col = view.col - width - 2
local left_col = view.col - width - border_info.horiz
local col, left
if right_space >= width and left_space >= width then
if right_space < left_space then
@ -81,8 +90,10 @@ docs_view.open = function(self, e, view)
return self:close()
end
-- Render window.
self.window:option('winblend', vim.o.pumblend)
self.window:option('winhighlight', documentation.winhighlight)
self.window:set_style({
local style = {
relative = 'editor',
style = 'minimal',
width = width,
@ -91,11 +102,14 @@ docs_view.open = function(self, e, view)
col = col,
border = documentation.border,
zindex = documentation.zindex or 50,
})
if left and self.window:has_scrollbar() then
self.window.style.col = self.window.style.col - 1
}
self.window:open(style)
-- Correct left-col for scrollbar existence.
if left then
style.col = style.col - self.window:info().scrollbar_offset
self.window:open(style)
end
self.window:open()
end
---Close floating window

View File

@ -9,6 +9,18 @@ local ghost_text_view = {}
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
local has_inline = (function()
return (pcall(function()
local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, {
virt_text = { { ' ', 'Comment' } },
virt_text_pos = 'inline',
hl_mode = 'combine',
ephemeral = true,
})
vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id)
end))
end)()
ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil
@ -17,7 +29,7 @@ ghost_text_view.new = function()
on_win = function(_, win)
return win == self.win
end,
on_line = function(_)
on_line = function(_, _, _, on_row)
local c = config.get().experimental.ghost_text
if not c then
return
@ -28,17 +40,23 @@ ghost_text_view.new = function()
end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line()
if string.sub(line, col + 1) ~= '' then
if on_row ~= row - 1 then
return
end
local line = vim.api.nvim_get_current_line()
if not has_inline then
if string.sub(line, col + 1) ~= '' then
return
end
end
local text = self.text_gen(self, line, col)
if #text > 0 then
vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, row - 1, col, {
right_gravity = false,
virt_text = { { text, c.hl_group or 'Comment' } },
virt_text_pos = 'overlay',
virt_text = { { text, type(c) == 'table' and c.hl_group or 'Comment' } },
virt_text_pos = has_inline and 'inline' or 'overlay',
hl_mode = 'combine',
ephemeral = true,
})
@ -78,6 +96,10 @@ ghost_text_view.show = function(self, e)
if not api.is_insert_mode() then
return
end
local c = config.get().experimental.ghost_text
if not c then
return
end
local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win()
self.entry = e

View File

@ -7,10 +7,10 @@ local config = require('cmp.config')
local api = require('cmp.utils.api')
---@class cmp.NativeEntriesView
---@field private offset number
---@field private offset integer
---@field private items vim.CompletedItem
---@field private entries cmp.Entry[]
---@field private preselect_index number
---@field private preselect_index integer
---@field public event cmp.Event
local native_entries_view = {}
@ -77,8 +77,7 @@ native_entries_view.open = function(self, offset, entries)
end
native_entries_view.close = function(self)
if api.is_suitable_mode() and self:visible() then
vim.fn.complete(1, {})
if api.is_insert_mode() and self:visible() then
vim.api.nvim_select_popupmenu_item(-1, false, true, {})
end
self.offset = -1
@ -101,10 +100,10 @@ native_entries_view.info = function(self)
if self:visible() then
local info = vim.fn.pum_getpos()
return {
width = info.width + (info.scrollbar and 1 or 0),
width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1),
height = info.height,
row = info.row,
col = info.col,
col = info.col == 0 and 0 or info.col - 1,
}
end
end
@ -123,9 +122,9 @@ native_entries_view.select_next_item = function(self, option)
end
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-n>'), 'n', callback)
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'n', callback)
else
feedkeys.call(keymap.t('<Down>'), 'n', callback)
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'n', callback)
end
end
end
@ -136,9 +135,9 @@ native_entries_view.select_prev_item = function(self, option)
end
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t('<C-p>'), 'n', callback)
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'n', callback)
else
feedkeys.call(keymap.t('<Up>'), 'n', callback)
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'n', callback)
end
end
end

View File

@ -9,7 +9,7 @@ local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api')
---@class cmp.CustomEntriesView
---@field private offset number
---@field private offset integer
---@field private entries_win cmp.Window
---@field private active boolean
---@field private entries cmp.Entry[]
@ -181,11 +181,14 @@ end
wildmenu_entries_view.select_next_item = function(self, option)
if self:visible() then
local cursor
if self.selected_index == 0 or self.selected_index == #self.entries then
self:_select(1, option)
cursor = option.count
else
self:_select(self.selected_index + 1, option)
cursor = self.selected_index + option.count
end
cursor = math.max(math.min(cursor, #self.entries), 0)
self:_select(cursor, option)
end
end
@ -194,7 +197,7 @@ wildmenu_entries_view.select_prev_item = function(self, option)
if self.selected_index == 0 or self.selected_index <= 1 then
self:_select(#self.entries, option)
else
self:_select(self.selected_index - 1, option)
self:_select(math.max(self.selected_index - option.count, 0), option)
end
end
end

View File

@ -2,7 +2,7 @@ local misc = require('cmp.utils.misc')
local vim_source = {}
---@param id number
---@param id integer
---@param args any[]
vim_source.on_callback = function(id, args)
if vim_source.to_callback.callbacks[id] then
@ -11,7 +11,7 @@ vim_source.on_callback = function(id, args)
end
---@param callback function
---@return number
---@return integer
vim_source.to_callback = setmetatable({
callbacks = {},
}, {
@ -36,7 +36,7 @@ vim_source.to_args = function(args)
return args
end
---@param bridge_id number
---@param bridge_id integer
---@param methods string[]
vim_source.new = function(bridge_id, methods)
local self = {}

31
bundle/nvim-cmp/nvim-cmp-scm-1.rockspec vendored Normal file
View File

@ -0,0 +1,31 @@
local MODREV, SPECREV = 'scm', '-1'
rockspec_format = '3.0'
package = 'nvim-cmp'
version = MODREV .. SPECREV
description = {
summary = 'A completion plugin for neovim',
labels = { 'neovim' },
detailed = [[
A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced".
]],
homepage = 'https://github.com/hrsh7th/nvim-cmp',
license = 'MIT',
}
dependencies = {
'lua >= 5.1, < 5.4',
}
source = {
url = 'git://github.com/hrsh7th/nvim-cmp',
}
build = {
type = 'builtin',
copy_directories = {
'autoload',
'plugin',
'doc'
}
}

View File

@ -3,140 +3,57 @@ if vim.g.loaded_cmp then
end
vim.g.loaded_cmp = true
local api = require "cmp.utils.api"
local misc = require('cmp.utils.misc')
if not vim.api.nvim_create_autocmd then
return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.')
end
local api = require('cmp.utils.api')
local types = require('cmp.types')
local config = require('cmp.config')
local highlight = require('cmp.utils.highlight')
local autocmd = require('cmp.utils.autocmd')
-- TODO: https://github.com/neovim/neovim/pull/14661
vim.cmd [[
augroup ___cmp___
autocmd!
autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter')
autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave')
autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged')
autocmd CursorMovedI * lua require'cmp.utils.autocmd'.emit('CursorMoved')
autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged')
autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone')
autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme()
autocmd CmdlineEnter * call v:lua.cmp.plugin.cmdline.enter()
autocmd CmdwinEnter * call v:lua.cmp.plugin.cmdline.leave() " for entering cmdwin with `<C-f>`
augroup END
]]
misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'enter' }, function()
if config.is_native_menu() then
return
vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true })
for kind in pairs(types.lsp.CompletionItemKind) do
if type(kind) == 'string' then
local name = ('CmpItemKind%s'):format(kind)
vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true })
end
if vim.fn.expand('<afile>')~= '=' then
vim.schedule(function()
if api.is_cmdline_mode() then
vim.cmd [[
augroup cmp-cmdline
autocmd!
autocmd CmdlineChanged * lua require'cmp.utils.autocmd'.emit('TextChanged')
autocmd CmdlineLeave * call v:lua.cmp.plugin.cmdline.leave()
augroup END
]]
require('cmp.utils.autocmd').emit('CmdlineEnter')
end
end)
end
end)
end
misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'leave' }, function()
if vim.fn.expand('<afile>') ~= '=' then
vim.cmd [[
augroup cmp-cmdline
autocmd!
augroup END
]]
require('cmp.utils.autocmd').emit('CmdlineLeave')
end
end)
misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function()
highlight.inherit('CmpItemAbbrDefault', 'Pmenu', {
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', {
gui = 'NONE',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', {
gui = 'NONE',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', {
gui = 'NONE',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemKindDefault', 'Special', {
guibg = 'NONE',
ctermbg = 'NONE',
})
autocmd.subscribe('ColorScheme', function()
highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false })
highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false })
for name in pairs(types.lsp.CompletionItemKind) do
if type(name) == 'string' then
vim.cmd(([[highlight default link CmpItemKind%sDefault CmpItemKind]]):format(name))
vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false })
end
end
highlight.inherit('CmpItemMenuDefault', 'Pmenu', {
guibg = 'NONE',
ctermbg = 'NONE',
})
end)
_G.cmp.plugin.colorscheme()
if vim.fn.hlexists('CmpItemAbbr') ~= 1 then
vim.cmd [[highlight default link CmpItemAbbr CmpItemAbbrDefault]]
end
if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then
vim.cmd [[highlight default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]]
end
if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then
vim.cmd [[highlight default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]]
end
if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then
vim.cmd [[highlight default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]]
end
if vim.fn.hlexists('CmpItemKind') ~= 1 then
vim.cmd [[highlight default link CmpItemKind CmpItemKindDefault]]
end
for name in pairs(types.lsp.CompletionItemKind) do
if type(name) == 'string' then
local hi = ('CmpItemKind%s'):format(name)
if vim.fn.hlexists(hi) ~= 1 then
vim.cmd(([[highlight default link %s %sDefault]]):format(hi, hi))
end
end
end
if vim.fn.hlexists('CmpItemMenu') ~= 1 then
vim.cmd [[highlight default link CmpItemMenu CmpItemMenuDefault]]
end
vim.cmd [[command! CmpStatus lua require('cmp').status()]]
vim.cmd [[doautocmd <nomodeline> User CmpReady]]
autocmd.emit('ColorScheme')
if vim.on_key then
vim.on_key(function(keys)
if keys == vim.api.nvim_replace_termcodes('<C-c>', true, true, true) then
vim.schedule(function()
if not api.is_suitable_mode() then
require('cmp.utils.autocmd').emit('InsertLeave')
autocmd.emit('InsertLeave')
end
end)
end
end, vim.api.nvim_create_namespace('cmp.plugin'))
end
vim.api.nvim_create_user_command('CmpStatus', function()
require('cmp').status()
end, { desc = 'Check status of cmp sources' })
vim.cmd([[doautocmd <nomodeline> User CmpReady]])

View File

@ -1,63 +0,0 @@
#!/usr/bin/env bash
set -eu pipefall
declare -r INSTALL_DIR="$PWD/utils"
declare -r RELEASE="0.10.0"
declare -r OS="linux"
# declare -r OS="$(uname -s)"
declare -r FILENAME="stylua-$RELEASE-$OS"
declare -a __deps=("curl" "unzip")
function check_deps() {
for dep in "${__deps[@]}"; do
if ! command -v "$dep" >/dev/null; then
echo "Missing depdendecy!"
echo "The \"$dep\" command was not found!. Please install and try again."
fi
done
}
function download_stylua() {
local DOWNLOAD_DIR
local URL="https://github.com/JohnnyMorganz/StyLua/releases/download/v$RELEASE/$FILENAME.zip"
DOWNLOAD_DIR="$(mktemp -d)"
echo "Initiating download for Stylua v$RELEASE"
if ! curl --progress-bar --fail -L "$URL" -o "$DOWNLOAD_DIR/$FILENAME.zip"; then
echo "Download failed. Check that the release/filename are correct."
exit 1
fi
echo "Installation in progress.."
unzip -q "$DOWNLOAD_DIR/$FILENAME.zip" -d "$DOWNLOAD_DIR"
if [ -f "$DOWNLOAD_DIR/stylua" ]; then
mv "$DOWNLOAD_DIR/stylua" "$INSTALL_DIR/stylua"
else
mv "$DOWNLOAD_DIR/$FILENAME/stylua" "$INSTALL_DIR/."
fi
chmod u+x "$INSTALL_DIR/stylua"
}
function verify_install() {
echo "Verifying installation.."
local DOWNLOADED_VER
DOWNLOADED_VER="$("$INSTALL_DIR/stylua" -V | awk '{ print $2 }')"
if [ "$DOWNLOADED_VER" != "$RELEASE" ]; then
echo "Mismatched version!"
echo "Expected: v$RELEASE but got v$DOWNLOADED_VER"
exit 1
fi
echo "Verification complete!"
}
function main() {
check_deps
download_stylua
verify_install
}
main "$@"

View File

@ -36,15 +36,15 @@ cmp.setup {
['<CR>'] = cmp.mapping.confirm({ select = true })
},
sources = {
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "buffer" },
},
}),
}
EOF
lua << EOF
local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities())
local capabilities = require('cmp_nvim_lsp').default_capabilities()
require'lspconfig'.cssls.setup {
capabilities = capabilities,