1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-02 22:20:06 +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: attributes:
label: FAQ label: FAQ
options: 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 required: true
- type: checkboxes - type: checkboxes
id: issue-prerequisite id: announcement-prerequisite
attributes: attributes:
label: Issues label: Announcement
options: 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 required: true
- type: input
attributes:
label: "Neovim Version"
description: "`nvim --version`:"
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Minimal reproducible full config" label: "Minimal reproducible full config"
@ -34,6 +27,10 @@ body:
2. Edit `~/cmp-repro.vim` for reproducing the issue 2. Edit `~/cmp-repro.vim` for reproducing the issue
3. Open `nvim -u ~/cmp-repro.vim` 3. Open `nvim -u ~/cmp-repro.vim`
4. Check reproduction step 4. Check reproduction step
value: |
```vim
```
validations: validations:
required: true 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 - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
default: true
override: true
- name: Setup neovim - name: Setup neovim
uses: rhysd/action-setup-vim@v1 uses: rhysd/action-setup-vim@v1
with: with:
@ -40,12 +33,9 @@ jobs:
- name: Setup tools - name: Setup tools
shell: bash shell: bash
run: | run: |
sudo apt install -y curl unzip --no-install-recommends
bash ./utils/install_stylua.sh
luarocks install luacheck luarocks install luacheck
luarocks install vusted luarocks install vusted
- name: Run tests - name: Run tests
shell: bash shell: bash
run: make integration 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 .PHONY: lint
lint: lint:
luacheck ./lua luacheck ./lua
@ -12,13 +8,10 @@ test:
.PHONY: pre-commit .PHONY: pre-commit
pre-commit: pre-commit:
./utils/stylua --config-path stylua.toml --glob 'lua/**/*.lua' -- lua
luacheck lua luacheck lua
vusted lua vusted lua
.PHONY: integration .PHONY: integration
integration: integration:
./utils/stylua --config-path stylua.toml --check --glob 'lua/**/*.lua' -- lua
luacheck lua luacheck lua
vusted lua vusted lua

View File

@ -3,34 +3,31 @@
A completion engine plugin for neovim written in Lua. A completion engine plugin for neovim written in Lua.
Completion sources are installed from external repositories and "sourced". 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! 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. 2. This is my hobby project. You can support me via GitHub sponsors.
3. Bug reports are welcome, but I might not fix if you don't provide a minimal reproduction configuration and steps. 3. Bug reports are welcome, but don't expect a fix unless you provide minimal configuration and steps to reproduce your issue.
4. The nvim-cmp documents is [here](./doc/cmp.txt). 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 Concept
==================== ====================
- Full support for LSP completion related capabilities - Full support for LSP completion related capabilities
- Powerful customizability via Lua functions - Powerful customizability via Lua functions
- Smart handling of key mapping - Smart handling of key mappings
- No flicker - No flicker
Setup Setup
==================== ====================
### Recommended Configuration ### 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 ```lua
call plug#begin(s:plug_dir) call plug#begin(s:plug_dir)
@ -59,10 +56,8 @@ Plug 'hrsh7th/vim-vsnip'
call plug#end() call plug#end()
set completeopt=menu,menuone,noselect
lua <<EOF lua <<EOF
-- Setup nvim-cmp. -- Set up nvim-cmp.
local cmp = require'cmp' local cmp = require'cmp'
cmp.setup({ cmp.setup({
@ -75,17 +70,17 @@ lua <<EOF
-- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users.
end, end,
}, },
mapping = { window = {
['<C-b>'] = cmp.mapping(cmp.mapping.scroll_docs(-4), { 'i', 'c' }), -- completion = cmp.config.window.bordered(),
['<C-f>'] = cmp.mapping(cmp.mapping.scroll_docs(4), { 'i', 'c' }), -- documentation = cmp.config.window.bordered(),
['<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.
}, },
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({ sources = cmp.config.sources({
{ name = 'nvim_lsp' }, { name = 'nvim_lsp' },
{ name = 'vsnip' }, -- For vsnip users. { name = 'vsnip' }, -- For vsnip users.
@ -100,14 +95,15 @@ lua <<EOF
-- Set configuration for specific filetype. -- Set configuration for specific filetype.
cmp.setup.filetype('gitcommit', { cmp.setup.filetype('gitcommit', {
sources = cmp.config.sources({ 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' }, { name = 'buffer' },
}) })
}) })
-- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore). -- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline('/', { cmp.setup.cmdline({ '/', '?' }, {
mapping = cmp.mapping.preset.cmdline(),
sources = { sources = {
{ name = 'buffer' } { name = 'buffer' }
} }
@ -115,6 +111,7 @@ lua <<EOF
-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
cmp.setup.cmdline(':', { cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({ sources = cmp.config.sources({
{ name = 'path' } { name = 'path' }
}, { }, {
@ -122,8 +119,8 @@ lua <<EOF
}) })
}) })
-- Setup lspconfig. -- Set up lspconfig.
local capabilities = require('cmp_nvim_lsp').update_capabilities(vim.lsp.protocol.make_client_capabilities()) local capabilities = require('cmp_nvim_lsp').default_capabilities()
-- Replace <YOUR_LSP_SERVER> with each lsp server you've enabled. -- Replace <YOUR_LSP_SERVER> with each lsp server you've enabled.
require('lspconfig')['<YOUR_LSP_SERVER>'].setup { require('lspconfig')['<YOUR_LSP_SERVER>'].setup {
capabilities = capabilities capabilities = capabilities
@ -131,85 +128,12 @@ lua <<EOF
EOF EOF
``` ```
### Where can I find more completion sources? ### 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? ### 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). See the [Wiki](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
]])
```

View File

@ -6,7 +6,16 @@ let s:sources = {}
" "
function! cmp#register_source(name, source) abort function! cmp#register_source(name, source) abort
let l:methods = [] 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 if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func
call add(l:methods, l:method) call add(l:methods, l:method)
endif endif
@ -39,6 +48,8 @@ function! cmp#_method(bridge_id, method, args) abort
return l:source[a:method]() return l:source[a:method]()
elseif a:method ==# 'get_debug_name' elseif a:method ==# 'get_debug_name'
return l:source[a:method]() 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' elseif a:method ==# 'get_keyword_pattern'
return l:source[a:method](a:args[0]) return l:source[a:method](a:args[0])
elseif a:method ==# 'get_trigger_characters' 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 ---@type cmp.ConfigSchema
config.global = require('cmp.config.default')() config.global = require('cmp.config.default')()
---@type table<number, cmp.ConfigSchema> ---@type table<integer, cmp.ConfigSchema>
config.buffers = {} config.buffers = {}
---@type table<string, cmp.ConfigSchema> ---@type table<string, cmp.ConfigSchema>
@ -29,14 +29,14 @@ config.onetime = {}
---Set configuration for global. ---Set configuration for global.
---@param c cmp.ConfigSchema ---@param c cmp.ConfigSchema
config.set_global = function(c) config.set_global = function(c)
config.global = misc.merge(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 or 1
config.global.revision = config.global.revision + 1 config.global.revision = config.global.revision + 1
end end
---Set configuration for buffer ---Set configuration for buffer
---@param c cmp.ConfigSchema ---@param c cmp.ConfigSchema
---@param bufnr number|nil ---@param bufnr integer
config.set_buffer = function(c, bufnr) config.set_buffer = function(c, bufnr)
local revision = (config.buffers[bufnr] or {}).revision or 1 local revision = (config.buffers[bufnr] or {}).revision or 1
config.buffers[bufnr] = c or {} config.buffers[bufnr] = c or {}
@ -56,11 +56,13 @@ end
---Set configuration for cmdline ---Set configuration for cmdline
---@param c cmp.ConfigSchema ---@param c cmp.ConfigSchema
---@param cmdtype string ---@param cmdtypes string|string[]
config.set_cmdline = function(c, cmdtype) config.set_cmdline = function(c, cmdtypes)
local revision = (config.cmdline[cmdtype] or {}).revision or 1 for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do
config.cmdline[cmdtype] = c or {} local revision = (config.cmdline[cmdtype] or {}).revision or 1
config.cmdline[cmdtype].revision = revision + 1 config.cmdline[cmdtype] = c or {}
config.cmdline[cmdtype].revision = revision + 1
end
end end
---Set configuration as oneshot completion. ---Set configuration as oneshot completion.
@ -74,7 +76,9 @@ end
---@return cmp.ConfigSchema ---@return cmp.ConfigSchema
config.get = function() config.get = function()
local global_config = config.global 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 local onetime_config = config.onetime
return config.cache:ensure({ return config.cache:ensure({
'get', 'get',
@ -82,7 +86,10 @@ config.get = function()
global_config.revision or 0, global_config.revision or 0,
onetime_config.revision or 0, onetime_config.revision or 0,
}, function() }, 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) end)
elseif api.is_cmdline_mode() then elseif api.is_cmdline_mode() then
local cmdtype = vim.fn.getcmdtype() local cmdtype = vim.fn.getcmdtype()
@ -94,7 +101,10 @@ config.get = function()
cmdtype, cmdtype,
cmdline_config.revision or 0, cmdline_config.revision or 0,
}, function() }, 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) end)
else else
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
@ -111,9 +121,9 @@ config.get = function()
buffer_config.revision or 0, buffer_config.revision or 0,
}, function() }, function()
local c = {} local c = {}
c = misc.merge(c, config.normalize(buffer_config)) c = misc.merge(config.normalize(c), config.normalize(buffer_config))
c = misc.merge(c, config.normalize(filetype_config)) c = misc.merge(config.normalize(c), config.normalize(filetype_config))
c = misc.merge(c, config.normalize(global_config)) c = misc.merge(config.normalize(c), config.normalize(global_config))
return c return c
end) end)
end end
@ -144,9 +154,6 @@ end
---Return the current menu is native or not. ---Return the current menu is native or not.
config.is_native_menu = function() config.is_native_menu = function()
local c = config.get() local c = config.get()
if c.experimental and c.experimental.native_menu then
return true
end
if c.view and c.view.entries then if c.view and c.view.entries then
return c.view.entries == 'native' or c.view.entries.name == 'native' return c.view.entries == 'native' or c.view.entries.name == 'native'
end end
@ -154,12 +161,14 @@ config.is_native_menu = function()
end end
---Normalize mapping key ---Normalize mapping key
---@param c cmp.ConfigSchema ---@param c any
---@return cmp.ConfigSchema ---@return cmp.ConfigSchema
config.normalize = function(c) config.normalize = function(c)
-- make sure c is not 'nil' -- make sure c is not 'nil'
---@type any
c = c == nil and {} or c c = c == nil and {} or c
-- Normalize mapping.
if c.mapping then if c.mapping then
local normalized = {} local normalized = {}
for k, v in pairs(c.mapping) do for k, v in pairs(c.mapping) do
@ -168,6 +177,7 @@ config.normalize = function(c)
c.mapping = normalized c.mapping = normalized
end end
-- Notice experimental.native_menu.
if c.experimental and c.experimental.native_menu then if c.experimental and c.experimental.native_menu then
vim.api.nvim_echo({ vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' }, { '[nvim-cmp] ', 'Normal' },
@ -182,6 +192,21 @@ config.normalize = function(c)
c.view.entries = c.view.entries or 'native' c.view.entries = c.view.entries or 'native'
end 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 if c.sources then
for _, s in ipairs(c.sources) do for _, s in ipairs(c.sources) do
if s.opts and not s.option then if s.opts and not s.option then

View File

@ -1,6 +1,5 @@
local types = require('cmp.types') local types = require('cmp.types')
local cache = require('cmp.utils.cache') local cache = require('cmp.utils.cache')
local misc = require('cmp.utils.misc')
local compare = {} local compare = {}
@ -71,7 +70,7 @@ end
-- sortText -- sortText
compare.sort_text = function(entry1, entry2) 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) local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText)
if diff < 0 then if diff < 0 then
return true return true
@ -108,7 +107,7 @@ compare.locality = setmetatable({
locality_map = {}, locality_map = {},
update = function(self) update = function(self)
local config = require('cmp').get_config() 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 return
end end
@ -132,7 +131,7 @@ compare.locality = setmetatable({
local s, e = regexp:match_str(buffer) local s, e = regexp:match_str(buffer)
if s and e then if s and e then
local w = string.sub(buffer, s + 1, e) 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) locality_map[w] = math.min(locality_map[w] or math.huge, d)
buffer = string.sub(buffer, e + 1) buffer = string.sub(buffer, e + 1)
else 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)) self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row))
end end
end end
end end,
}, { }, {
__call = function(self, entry1, entry2) __call = function(self, entry1, entry2)
local local1 = self.locality_map[entry1:get_word()] local local1 = self.locality_map[entry1:get_word()]
@ -159,7 +158,7 @@ compare.locality = setmetatable({
end end
return local1 < local2 return local1 < local2
end end
end end,
}) })
-- scopes -- scopes
@ -175,7 +174,6 @@ compare.scopes = setmetatable({
if ok then if ok then
local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf() 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 cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1
local ts_utils = require('nvim-treesitter.ts_utils')
-- Cursor scope. -- Cursor scope.
local cursor_scope = nil local cursor_scope = nil
@ -205,7 +203,8 @@ compare.scopes = setmetatable({
for _, definition in pairs(definitions) do for _, definition in pairs(definitions) do
if s <= definition.node:start() and definition.node:end_() <= e then if s <= definition.node:start() and definition.node:end_() <= e then
if scope:id() == locals.containing_scope(definition.node, buf):id() 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 if not self.scopes_map[text] then
self.scopes_map[text] = depth self.scopes_map[text] = depth
end end

View File

@ -1,65 +1,60 @@
local api = require('cmp.utils.api')
local context = {} local context = {}
---Check if cursor is in syntax group ---Check if cursor is in syntax group
---@param group string ---@param group string | []string
---@return boolean ---@return boolean
context.in_syntax_group = function(group) context.in_syntax_group = function(group)
local lnum, col = vim.fn.line('.'), math.min(vim.fn.col('.'), #vim.fn.getline('.')) local row, col = unpack(vim.api.nvim_win_get_cursor(0))
for _, syn_id in ipairs(vim.fn.synstack(lnum, col)) do 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 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 return true
end end
end end
return false return false
end end
---Check if cursor is in treesitter capture ---Check if cursor is in treesitter capture
---@param capture string ---@param capture string | []string
---@return boolean ---@return boolean
context.in_treesitter_capture = function(capture) 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 buf = vim.api.nvim_get_current_buf()
local row, col = unpack(vim.api.nvim_win_get_cursor(0)) local row, col = unpack(vim.api.nvim_win_get_cursor(0))
row = row - 1 row = row - 1
if vim.api.nvim_get_mode().mode == 'i' then if vim.api.nvim_get_mode().mode == 'i' then
col = col - 1 col = col - 1
end end
local self = highlighter.active[buf] local get_captures_at_pos = -- See neovim/neovim#20331
if not self then 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 return false
end elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then
return true
local node_types = {} elseif type(capture) == 'table' then
for _, v in ipairs(capture) do
self.tree:for_each_tree(function(tstree, tree) if vim.tbl_contains(captures_at_cursor, v) then
if not tstree then return true
return
end
local root = tstree:root()
local root_start_row, _, root_end_row, _ = root:range()
if root_start_row > row or root_end_row < row then
return
end
local query = self:get_query(tree:lang())
if not query:query() then
return
end
local iter = query:query():iter_captures(root, self.bufnr, row, row + 1)
for _, node, _ in iter do
if ts_utils.is_in_node_range(node, row, col) then
table.insert(node_types, node:type())
end end
end end
end, true) end
return vim.tbl_contains(node_types, capture) return false
end end
return context return context

View File

@ -1,13 +1,12 @@
local compare = require('cmp.config.compare') local compare = require('cmp.config.compare')
local mapping = require('cmp.config.mapping')
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types') local types = require('cmp.types')
local WIDE_HEIGHT = 40 local WIDE_HEIGHT = 40
---@return cmp.ConfigSchema ---@return cmp.ConfigSchema
return function() return function()
return { ---@type cmp.ConfigSchema
local config = {
enabled = function() enabled = function()
local disabled = false local disabled = false
disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt') disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt')
@ -16,85 +15,35 @@ return function()
return not disabled return not disabled
end, end,
preselect = types.cmp.PreselectMode.Item, performance = {
debounce = 60,
mapping = { throttle = 30,
['<Down>'] = mapping({ fetching_timeout = 500,
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }), async_budget = 1,
c = function(fallback) max_view_entries = 200,
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(),
}, },
preselect = types.cmp.PreselectMode.Item,
mapping = {},
snippet = { snippet = {
expand = function() expand = function(_)
error('snippet engine is not configured.') error('snippet engine is not configured.')
end, end,
}, },
completion = { completion = {
keyword_length = 1,
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
autocomplete = { autocomplete = {
types.cmp.TriggerEvent.TextChanged, types.cmp.TriggerEvent.TextChanged,
}, },
completeopt = 'menu,menuone,noselect', completeopt = 'menu,menuone,noselect',
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
keyword_length = 1,
}, },
formatting = { formatting = {
expandable_indicator = true,
fields = { 'abbr', 'kind', 'menu' }, fields = { 'abbr', 'kind', 'menu' },
format = function(_, vim_item) format = function(_, vim_item)
return vim_item return vim_item
@ -103,6 +52,8 @@ return function()
matching = { matching = {
disallow_fuzzy_matching = false, disallow_fuzzy_matching = false,
disallow_fullfuzzy_matching = false,
disallow_partial_fuzzy_matching = true,
disallow_partial_matching = false, disallow_partial_matching = false,
disallow_prefix_unmatching = false, disallow_prefix_unmatching = false,
}, },
@ -117,7 +68,7 @@ return function()
compare.recently_used, compare.recently_used,
compare.locality, compare.locality,
compare.kind, compare.kind,
compare.sort_text, -- compare.sort_text,
compare.length, compare.length,
compare.order, compare.order,
}, },
@ -125,13 +76,6 @@ return function()
sources = {}, sources = {},
documentation = {
border = { '', '', '', ' ', '', '', '', ' ' },
winhighlight = 'NormalFloat:NormalFloat,FloatBorder:NormalFloat',
maxwidth = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))),
maxheight = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)),
},
confirmation = { confirmation = {
default_behavior = types.cmp.ConfirmBehavior.Insert, default_behavior = types.cmp.ConfirmBehavior.Insert,
get_commit_characters = function(commit_characters) get_commit_characters = function(commit_characters)
@ -146,7 +90,28 @@ return function()
}, },
view = { 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 end

View File

@ -1,5 +1,22 @@
local mapping local types = require('cmp.types')
mapping = setmetatable({}, { 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) __call = function(_, invoke, modes)
if type(invoke) == 'function' then if type(invoke) == 'function' then
local map = {} local map = {}
@ -12,8 +29,111 @@ mapping = setmetatable({}, {
end, 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 ---Invoke completion
---@param option cmp.CompleteParams ---@param option? cmp.CompleteParams
mapping.complete = function(option) mapping.complete = function(option)
return function(fallback) return function(fallback)
if not require('cmp').complete(option) then 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 prev_context cmp.Context
---@field public option cmp.ContextOption ---@field public option cmp.ContextOption
---@field public filetype string ---@field public filetype string
---@field public time number ---@field public time integer
---@field public bufnr number ---@field public bufnr integer
---@field public cursor vim.Position|lsp.Position ---@field public cursor vim.Position|lsp.Position
---@field public cursor_line string ---@field public cursor_line string
---@field public cursor_after_line string ---@field public cursor_after_line string
---@field public cursor_before_line string ---@field public cursor_before_line string
---@field public aborted boolean
local context = {} local context = {}
---Create new empty context ---Create new empty context
@ -31,8 +32,8 @@ context.empty = function()
end end
---Create new context ---Create new context
---@param prev_context cmp.Context ---@param prev_context? cmp.Context
---@param option cmp.ContextOption ---@param option? cmp.ContextOption
---@return cmp.Context ---@return cmp.Context
context.new = function(prev_context, option) context.new = function(prev_context, option)
option = option or {} 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.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_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.cursor_after_line = string.sub(self.cursor_line, self.cursor.col)
self.aborted = false
return self return self
end end
context.abort = function(self)
self.aborted = true
end
---Return context creation reason. ---Return context creation reason.
---@return cmp.ContextReason ---@return cmp.ContextReason
context.get_reason = function(self) context.get_reason = function(self)
@ -65,7 +71,7 @@ context.get_reason = function(self)
end end
---Get keyword pattern offset ---Get keyword pattern offset
---@return number|nil ---@return integer
context.get_offset = function(self, keyword_pattern) context.get_offset = function(self, keyword_pattern)
return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function() 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 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 api = require('cmp.utils.api')
local event = require('cmp.utils.event') local event = require('cmp.utils.event')
local SOURCE_TIMEOUT = 500
local DEBOUNCE_TIME = 80
local THROTTLE_TIME = 40
---@class cmp.Core ---@class cmp.Core
---@field public suspending boolean ---@field public suspending boolean
---@field public view cmp.View ---@field public view cmp.View
@ -36,9 +32,11 @@ core.new = function()
self.view.event:on('keymap', function(...) self.view.event:on('keymap', function(...)
self:on_keymap(...) self:on_keymap(...)
end) end)
self.view.event:on('complete_done', function(evt) for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do
self.event:emit('complete_done', evt) self.view.event:on(event_name, function(evt)
end) self.event:emit(event_name, evt)
end)
end
return self return self
end end
@ -49,17 +47,19 @@ core.register_source = function(self, s)
end end
---Unregister source ---Unregister source
---@param source_id string ---@param source_id integer
core.unregister_source = function(self, source_id) core.unregister_source = function(self, source_id)
self.sources[source_id] = nil self.sources[source_id] = nil
end end
---Get new context ---Get new context
---@param option cmp.ContextOption ---@param option? cmp.ContextOption
---@return cmp.Context ---@return cmp.Context
core.get_context = function(self, option) core.get_context = function(self, option)
self.context:abort()
local prev = self.context:clone() local prev = self.context:clone()
prev.prev_context = nil prev.prev_context = nil
prev.cache = nil
local ctx = context.new(prev, option) local ctx = context.new(prev, option)
self:set_context(ctx) self:set_context(ctx)
return self.context return self.context
@ -74,13 +74,14 @@ end
---Suspend completion ---Suspend completion
core.suspend = function(self) core.suspend = function(self)
self.suspending = true self.suspending = true
return function() -- It's needed to avoid conflicting with autocmd debouncing.
return vim.schedule_wrap(function()
self.suspending = false self.suspending = false
end end)
end end
---Get sources that sorted by priority ---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[] ---@return cmp.Source[]
core.get_sources = function(self, filter) core.get_sources = function(self, filter)
local f = function(s) 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 if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then
self:complete(ctx) self:complete(ctx)
else 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() self:filter()
end end
else else
@ -221,7 +222,7 @@ end
---Complete common string for current completed entries. ---Complete common string for current completed entries.
core.complete_common_string = function(self) 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 return false
end end
@ -240,7 +241,7 @@ core.complete_common_string = function(self)
config.set_onetime({}) config.set_onetime({})
local cursor = api.get_cursor() local cursor = api.get_cursor()
local offset = self.view:get_offset() local offset = self.view:get_offset() or cursor[2]
local common_string local common_string
for _, e in ipairs(self.view:get_entries()) do for _, e in ipairs(self.view:get_entries()) do
local vim_item = e:get_vim_item(offset) 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) common_string = str.get_common_string(common_string, vim_item.word)
end end
end end
if common_string and #common_string > (1 + cursor[2] - offset) then local cursor_before_line = api.get_cursor_before_line()
feedkeys.call(keymap.backspace(string.sub(api.get_current_line(), offset, cursor[2])) .. common_string, 'n') 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 return true
end end
return false return false
@ -276,17 +279,9 @@ core.complete = function(self, ctx)
if s_.incomplete and new:changed(s_.context) then if s_.incomplete and new:changed(s_.context) then
s_:complete(new, callback) s_:complete(new, callback)
else else
for _, s__ in ipairs(self:get_sources({ source.SourceStatus.FETCHING })) do
if s_ == s__ then
break
end
if not s__.incomplete and SOURCE_TIMEOUT > s__:get_fetching_time() then
return
end
end
if not self.view:get_active_entry() then if not self.view:get_active_entry() then
self.filter.stop() self.filter.stop()
self.filter.timeout = self.view:visible() and DEBOUNCE_TIME or 0 self.filter.timeout = config.get().performance.debounce
self:filter() self:filter()
end end
end end
@ -296,14 +291,14 @@ core.complete = function(self, ctx)
end end
if not self.view:get_active_entry() then 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() self:filter()
end end
end end
---Update completion menu ---Update completion menu
core.filter = async.throttle(function(self) local async_filter = async.wrap(function(self)
self.filter.timeout = self.view:visible() and THROTTLE_TIME or 0 self.filter.timeout = config.get().performance.throttle
-- Check invalid condition. -- Check invalid condition.
local ignore = false local ignore = false
@ -315,11 +310,13 @@ core.filter = async.throttle(function(self)
-- Check fetching sources. -- Check fetching sources.
local sources = {} local sources = {}
for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do 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.
-- Reserve filter call for timeout. if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then
self.filter.timeout = SOURCE_TIMEOUT - s:get_fetching_time() self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time()
self:filter() self:filter()
break if #sources == 0 then
return
end
end end
table.insert(sources, s) table.insert(sources, s)
end end
@ -327,20 +324,17 @@ core.filter = async.throttle(function(self)
local ctx = self:get_context() local ctx = self:get_context()
-- Display completion results. -- 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. -- Check onetime config.
if #self:get_sources(function(s) if not did_open and fetching == 0 then
if s.status == source.SourceStatus.FETCHING then
return true
elseif #s:get_entries(ctx) > 0 then
return true
end
return false
end) == 0 then
config.set_onetime({}) config.set_onetime({})
end end
end, THROTTLE_TIME) end)
core.filter = async.throttle(async_filter, config.get().performance.throttle)
---Confirm completion. ---Confirm completion.
---@param e cmp.Entry ---@param e cmp.Entry
@ -348,7 +342,10 @@ end, THROTTLE_TIME)
---@param callback function ---@param callback function
core.confirm = function(self, e, option, callback) core.confirm = function(self, e, option, callback)
if not (e and not e.confirmed) then if not (e and not e.confirmed) then
return callback() if callback then
callback()
end
return
end end
e.confirmed = true e.confirmed = true
@ -361,33 +358,39 @@ core.confirm = function(self, e, option, callback)
feedkeys.call(keymap.indentkeys(), 'n') feedkeys.call(keymap.indentkeys(), 'n')
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
-- Emulate `<C-y>` behavior to save `.` register.
local ctx = context.new() local ctx = context.new()
local keys = {} 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, e:get_word())
table.insert(keys, keymap.undobreak()) table.insert(keys, keymap.undobreak())
feedkeys.call(table.concat(keys, ''), 'int') feedkeys.call(table.concat(keys, ''), 'in')
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
-- Restore the line at the time of request.
local ctx = context.new() local ctx = context.new()
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
local keys = {} local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor.character - 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())) table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
feedkeys.call(table.concat(keys, ''), 'in') feedkeys.call(table.concat(keys, ''), 'in')
else else
vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, { vim.cmd([[silent! undojoin]])
string.sub(e.context.cursor_before_line, e:get_offset()), -- 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 }) vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
end end
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
-- Apply additionalTextEdits.
local ctx = context.new() 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() e:resolve(function()
local new = context.new() local new = context.new()
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {} local text_edits = e:get_completion_item().additionalTextEdits or {}
if #text_edits == 0 then if #text_edits == 0 then
return return
end end
@ -407,18 +410,20 @@ core.confirm = function(self, e, option, callback)
if has_cursor_line_text_edit then if has_cursor_line_text_edit then
return return
end end
vim.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) end)
else 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
end) end)
feedkeys.call('', 'n', function() feedkeys.call('', 'n', function()
local ctx = context.new() local ctx = context.new()
local completion_item = misc.copy(e:get_completion_item()) 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 = {}
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 end
local behavior = option.behavior or config.get().confirmation.default_behavior local behavior = option.behavior or config.get().confirmation.default_behavior
if behavior == types.cmp.ConfirmBehavior.Replace then 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() completion_item.textEdit.range = e:get_insert_range()
end end
local diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.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 - e.context.cursor.character) local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col)
local new_text = completion_item.textEdit.newText 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 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 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 if is_snippet then
completion_item.textEdit.newText = '' completion_item.textEdit.newText = ''
end 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 texts = vim.split(completion_item.textEdit.newText, '\n')
local position = completion_item.textEdit.range.start vim.api.nvim_win_set_cursor(0, {
position.line = position.line + (#texts - 1) completion_item.textEdit.range.start.line + #texts,
if #texts == 1 then (#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]),
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 })
if is_snippet then if is_snippet then
config.get().snippet.expand({ config.get().snippet.expand({
body = new_text, body = new_text,
@ -459,8 +475,8 @@ core.confirm = function(self, e, option, callback)
end end
else else
local keys = {} local keys = {}
table.insert(keys, string.rep(keymap.t('<BS>'), diff_before)) table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1)))
table.insert(keys, string.rep(keymap.t('<Del>'), diff_after)) table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character)))
table.insert(keys, new_text) table.insert(keys, new_text)
feedkeys.call(table.concat(keys, ''), 'in') feedkeys.call(table.concat(keys, ''), 'in')
end end

View File

@ -8,9 +8,19 @@ local api = require('cmp.utils.api')
describe('cmp.core', function() describe('cmp.core', function()
describe('confirm', 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 c = core.new()
local s = source.new('spec', { local s = source.new('spec', {
get_position_encoding_kind = function()
return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16
end,
complete = function(_, _, callback) complete = function(_, _, callback)
callback({ completion_item }) callback({ completion_item })
end, end,
@ -23,7 +33,7 @@ describe('cmp.core', function()
end) end)
end) end)
feedkeys.call(filter, 'n', function() feedkeys.call(filter, 'n', function()
c:confirm(c.sources[s.id].entries[1], {}) c:confirm(c.sources[s.id].entries[1], {}, function() end)
end) end)
local state = {} local state = {}
feedkeys.call('', 'x', function() feedkeys.call('', 'x', function()
@ -80,6 +90,29 @@ describe('cmp.core', function()
assert.are.same(state.cursor, { 3, 3 }) assert.are.same(state.cursor, { 3, 3 })
end) 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() it('insertText & snippet', function()
local state = confirm('iA', 'IU', { local state = confirm('iA', 'IU', {
label = 'AIUEO', label = 'AIUEO',
@ -111,6 +144,46 @@ describe('cmp.core', function()
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' }) assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
assert.are.same(state.cursor, { 2, 2 }) assert.are.same(state.cursor, { 2, 2 })
end) 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) end)
describe('cmdline-mode', function() describe('cmdline-mode', function()

View File

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

View File

@ -1,6 +1,4 @@
local spec = require('cmp.utils.spec') local spec = require('cmp.utils.spec')
local source = require('cmp.source')
local async = require('cmp.utils.async')
local entry = require('cmp.entry') local entry = require('cmp.entry')
@ -290,41 +288,6 @@ describe('entry', function()
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string') assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string')
end) end)
it('[ansiblels] 1', function()
local item = {
detail = 'ansible.builtin',
filterText = 'blockinfile ansible.builtin.blockinfile',
kind = 7,
label = 'blockinfile',
sortText = '2_blockinfile',
textEdit = {
newText = '',
range = {
['end'] = {
character = 7,
line = 15,
},
start = {
character = 6,
line = 15,
},
},
},
}
local s = source.new('dummy', {
resolve = function(_, _, callback)
item.textEdit.newText = 'modified'
callback(item)
end,
})
local e = entry.new(spec.state('', 1, 1).manual(), s, item)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
async.sync(function(done)
e:resolve(done)
end, 100)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
end)
it('[#47] word should not contain \\n character', function() it('[#47] word should not contain \\n character', function()
local state = spec.state('', 1, 1) local state = spec.state('', 1, 1)
@ -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_vim_item(e:get_offset()).word, '__init__(self) -> None:')
assert.are.equal(e:get_filter_text(), '__init__') assert.are.equal(e:get_filter_text(), '__init__')
end) 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) end)

View File

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

View File

@ -66,14 +66,20 @@ end
-- --
-- `candlesingle` -> candle#accept#single -- `candlesingle` -> candle#accept#single
-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~ -- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~
--
-- * The `accept`'s `a` should not match to `candle`'s `a` -- * 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 ---Match entry
---@param input string ---@param input string
---@param word string ---@param word string
---@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean } ---@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 number ---@return integer
matcher.match = function(input, word, option) matcher.match = function(input, word, option)
option = option or {} option = option or {}
@ -100,12 +106,14 @@ matcher.match = function(input, word, option)
local input_end_index = 1 local input_end_index = 1
local word_index = 1 local word_index = 1
local word_bound_index = 1 local word_bound_index = 1
local no_symbol_match = false
while input_end_index <= #input and word_index <= #word do 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) 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 if m and input_end_index <= m.input_match_end then
m.index = word_bound_index m.index = word_bound_index
input_start_index = m.input_match_start + 1 input_start_index = m.input_match_start + 1
input_end_index = m.input_match_end + 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) word_index = char.get_next_semantic_index(word, m.word_match_end)
table.insert(matches, m) table.insert(matches, m)
else else
@ -120,6 +128,11 @@ matcher.match = function(input, word, option)
end end
if #matches == 0 then 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, {} return 0, {}
end end
@ -146,6 +159,10 @@ matcher.match = function(input, word, option)
end end
end end
if no_symbol_match and not prefix then
return 0, {}
end
-- Compute prefix match score -- Compute prefix match score
local score = prefix and matcher.PREFIX_FACTOR or 0 local score = prefix and matcher.PREFIX_FACTOR or 0
local offset = prefix and matches[1].index - 1 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 -- Check remaining input as fuzzy
if matches[#matches].input_match_end < #input then if matches[#matches].input_match_end < #input then
if not option.disallow_fuzzy_matching then if not option.disallow_fuzzy_matching then
if prefix and matcher.fuzzy(input, word, matches) then if not option.disallow_partial_fuzzy_matching or prefix then
return score, matches if matcher.fuzzy(input, word, matches, option) then
return score, matches
end
end end
end end
return 0, {} return 0, {}
@ -178,11 +197,10 @@ matcher.match = function(input, word, option)
end end
--- fuzzy --- fuzzy
matcher.fuzzy = function(input, word, matches) matcher.fuzzy = function(input, word, matches, option)
local last_match = matches[#matches] local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1
-- Lately specified middle of text. -- Lately specified middle of text.
local input_index = last_match.input_match_end + 1
for i = 1, #matches - 1 do for i = 1, #matches - 1 do
local curr_match = matches[i] local curr_match = matches[i]
local next_match = matches[i + 1] local next_match = matches[i + 1]
@ -200,10 +218,9 @@ matcher.fuzzy = function(input, word, matches)
end end
-- Remaining text fuzzy match. -- Remaining text fuzzy match.
local last_input_index = input_index
local matched = false local matched = false
local word_offset = 0 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_start = -1
local input_match_end = -1 local input_match_end = -1
local word_match_start = -1 local word_match_start = -1
@ -220,12 +237,26 @@ matcher.fuzzy = function(input, word, matches)
input_index = input_index + 1 input_index = input_index + 1
strict_count = strict_count + (c1 == c2 and 1 or 0) strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1 match_count = match_count + 1
elseif matched then else
input_index = last_input_index if option.disallow_fullfuzzy_matching then
input_match_end = input_index - 1 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 end
word_offset = word_offset + 1 word_offset = word_offset + 1
end end
if input_index > #input then if input_index > #input then
table.insert(matches, { table.insert(matches, {
input_match_start = input_match_start, 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 word_offset = 0
local strict_count = 0 local strict_count = 0
local match_count = 0 local match_count = 0
local no_symbol_match = false
while input_index <= #input and word_index + word_offset <= #word do while input_index <= #input and word_index + word_offset <= #word do
local c1 = string.byte(input, input_index) local c1 = string.byte(input, input_index)
local c2 = string.byte(word, word_index + word_offset) 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) strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1 match_count = match_count + 1
word_offset = word_offset + 1 word_offset = word_offset + 1
no_symbol_match = no_symbol_match or char.is_symbol(c1)
else else
-- Match end (partial region) -- Match end (partial region)
if input_match_start ~= -1 then 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_start = word_index,
word_match_end = word_index + word_offset - 1, word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count, strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false, fuzzy = false,
} }
else else
@ -298,6 +332,7 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
word_match_start = word_index, word_match_start = word_index,
word_match_end = word_index + word_offset - 1, word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count, strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false, fuzzy = false,
} }
end 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('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list'))
assert.is.truthy(matcher.match('2', '[[2021') >= 1) 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('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('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) end)
it('disallow_fuzzy_matching', function() it('disallow_fuzzy_matching', function()
@ -37,6 +64,11 @@ describe('matcher', function()
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1) assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1)
end) 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() 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 = true }) == 0)
assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1) 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') local char = require('cmp.utils.char')
---@class cmp.Source ---@class cmp.Source
---@field public id number ---@field public id integer
---@field public name string ---@field public name string
---@field public source any ---@field public source any
---@field public cache cmp.Cache ---@field public cache cmp.Cache
---@field public revision number ---@field public revision integer
---@field public incomplete boolean ---@field public incomplete boolean
---@field public is_triggered_by_symbol boolean ---@field public is_triggered_by_symbol boolean
---@field public entries cmp.Entry[] ---@field public entries cmp.Entry[]
---@field public offset number ---@field public offset integer
---@field public request_offset number ---@field public request_offset integer
---@field public context cmp.Context ---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext|nil ---@field public completion_context lsp.CompletionContext|nil
---@field public status cmp.SourceStatus ---@field public status cmp.SourceStatus
---@field public complete_dedup function ---@field public complete_dedup function
local source = {} local source = {}
---@alias cmp.SourceStatus "1" | "2" | "3" ---@alias cmp.SourceStatus 1 | 2 | 3
source.SourceStatus = {} source.SourceStatus = {}
source.SourceStatus.WAITING = 1 source.SourceStatus.WAITING = 1
source.SourceStatus.FETCHING = 2 source.SourceStatus.FETCHING = 2
@ -46,7 +46,6 @@ source.new = function(name, s)
end end
---Reset current completion state ---Reset current completion state
---@return boolean
source.reset = function(self) source.reset = function(self)
self.cache:clear() self.cache:clear()
self.revision = self.revision + 1 self.revision = self.revision + 1
@ -89,86 +88,92 @@ source.get_entries = function(self, ctx)
return {} return {}
end end
local target_entries = (function() local target_entries = self.entries
local key = { 'get_entries', self.revision }
for i = ctx.cursor.col, self.offset, -1 do local prev = self.cache:get({ 'get_entries', tostring(self.revision) })
key[3] = string.sub(ctx.cursor_before_line, 1, i) if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then
local prev_entries = self.cache:get(key) if ctx.cursor.col == prev.ctx.cursor.col then
if prev_entries then return prev.entries
return prev_entries
end
end end
return self.entries -- only use prev entries when cursor is moved forward.
end)() -- 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 = {} local inputs = {}
---@type cmp.Entry[]
local entries = {} local entries = {}
local matching_config = self:get_matching_config()
for _, e in ipairs(target_entries) do for _, e in ipairs(target_entries) do
local o = e:get_offset() local o = e:get_offset()
if not inputs[o] then if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o) inputs[o] = string.sub(ctx.cursor_before_line, o)
end end
local match = e:match(inputs[o], self:get_matching_config()) local match = e:match(inputs[o], matching_config)
e.score = match.score e.score = match.score
e.exact = false e.exact = false
if e.score >= 1 then if e.score >= 1 then
e.matches = match.matches e.matches = match.matches
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o] 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 if entry_filter(e, ctx) then
local limited_entries = {} entries[#entries + 1] = e
for _, e in ipairs(entries) do end
table.insert(limited_entries, e) end
if max_item_count and #limited_entries >= max_item_count then async.yield()
break if ctx.aborted then
async.abort()
end end
end end
return limited_entries
self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset })
return entries
end end
---Get default insert range ---Get default insert range (UTF8 byte index).
---@return lsp.Range|nil ---@return lsp.Range
source.get_default_insert_range = function(self) source.get_default_insert_range = function(self)
if not self.context then if not self.context then
return nil error('context is not initialized yet.')
end 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 { return {
start = { start = {
line = self.context.cursor.row - 1, line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, self.offset), character = self.offset - 1,
}, },
['end'] = { ['end'] = {
line = self.context.cursor.row - 1, 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)
end end
---Get default replace range ---Get default replace range (UTF8 byte index).
---@return lsp.Range|nil ---@return lsp.Range
source.get_default_replace_range = function(self) source.get_default_replace_range = function(self)
if not self.context then if not self.context then
return nil error('context is not initialized yet.')
end 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)) local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
return { return {
start = { start = {
line = self.context.cursor.row - 1, line = self.context.cursor.row - 1,
character = misc.to_utfindex(self.context.cursor_line, self.offset), character = self.offset,
}, },
['end'] = { ['end'] = {
line = self.context.cursor.row - 1, 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) end)
@ -217,13 +222,16 @@ source.get_keyword_pattern = function(self)
return c.keyword_pattern return c.keyword_pattern
end end
if self.source.get_keyword_pattern then if self.source.get_keyword_pattern then
return self.source:get_keyword_pattern(misc.copy(c)) local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c))
if keyword_pattern then
return keyword_pattern
end
end end
return config.get().completion.keyword_pattern return config.get().completion.keyword_pattern
end end
---Get keyword_length ---Get keyword_length
---@return number ---@return integer
source.get_keyword_length = function(self) source.get_keyword_length = function(self)
local c = self:get_source_config() local c = self:get_source_config()
if c.keyword_length then if c.keyword_length then
@ -232,10 +240,31 @@ source.get_keyword_length = function(self)
return config.get().completion.keyword_length or 1 return config.get().completion.keyword_length or 1
end 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 ---Invoke completion
---@param ctx cmp.Context ---@param ctx cmp.Context
---@param callback function ---@param callback function
---@return boolean Return true if not trigger completion. ---@return boolean? Return true if not trigger completion.
source.complete = function(self, ctx, callback) source.complete = function(self, ctx, callback)
local offset = ctx:get_offset(self:get_keyword_pattern()) local offset = ctx:get_offset(self:get_keyword_pattern())
@ -260,7 +289,7 @@ source.complete = function(self, ctx, callback)
triggerCharacter = before_char, triggerCharacter = before_char,
} }
elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then 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 if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then
completion_context = { completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions, triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
@ -300,6 +329,10 @@ source.complete = function(self, ctx, callback)
completion_context = completion_context, completion_context = completion_context,
}), }),
self.complete_dedup(vim.schedule_wrap(function(response) self.complete_dedup(vim.schedule_wrap(function(response)
if self.context ~= ctx then
return
end
---@type lsp.CompletionResponse
response = response or {} response = response or {}
self.incomplete = response.isIncomplete or false self.incomplete = response.isIncomplete or false
@ -312,14 +345,14 @@ source.complete = function(self, ctx, callback)
self.status = source.SourceStatus.COMPLETED self.status = source.SourceStatus.COMPLETED
self.entries = {} self.entries = {}
for i, item in ipairs(response.items or response) do for i, item in ipairs(response.items or response) do
if (misc.safe(item) or {}).label then if (item or {}).label then
local e = entry.new(ctx, self, item) local e = entry.new(ctx, self, item, response.itemDefaults)
self.entries[i] = e self.entries[i] = e
self.offset = math.min(self.offset, e:get_offset()) self.offset = math.min(self.offset, e:get_offset())
end end
end end
self.revision = self.revision + 1 self.revision = self.revision + 1
if #self:get_entries(ctx) == 0 then if #self.entries == 0 then
self.offset = old_offset self.offset = old_offset
self.entries = old_entries self.entries = old_entries
self.revision = self.revision + 1 self.revision = self.revision + 1

View File

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

View File

@ -1,151 +1,234 @@
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/ ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
---@class lsp ---@class lsp
local 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 lsp.Position = {
---@param buf number|string ---Convert lsp.Position to vim.Position
---@param position lsp.Position ---@param buf integer
---@return vim.Position ---@param position lsp.Position
lsp.Position.to_vim = function(buf, position) --
if not vim.api.nvim_buf_is_loaded(buf) then ---@return vim.Position
vim.fn.bufload(buf) to_vim = function(buf, position)
end if not vim.api.nvim_buf_is_loaded(buf) then
local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false) vim.fn.bufload(buf)
if #lines > 0 then 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 { return {
row = position.line + 1, row = position.line + 1,
col = misc.to_vimindex(lines[1], position.character), col = position.character + 1,
} }
end end,
return { ---Convert vim.Position to lsp.Position
row = position.line + 1, ---@param buf integer
col = position.character + 1, ---@param position vim.Position
} ---@return lsp.Position
end to_lsp = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
---Convert vim.Position to lsp.Position vim.fn.bufload(buf)
---@param buf number|string end
---@param position vim.Position local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false)
---@return lsp.Position if #lines > 0 then
lsp.Position.to_lsp = function(buf, position) return {
if not vim.api.nvim_buf_is_loaded(buf) then line = position.row - 1,
vim.fn.bufload(buf) character = misc.to_utfindex(lines[1], position.col),
end }
local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false) end
if #lines > 0 then
return { return {
line = position.row - 1, line = position.row - 1,
character = misc.to_utfindex(lines[1], position.col), character = position.col - 1,
} }
end end,
return {
line = position.row - 1,
character = position.col - 1,
}
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 local ok, byteindex = pcall(function()
---@param buf number|string return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16)
---@param range lsp.Range end)
---@return vim.Range if not ok then
lsp.Range.to_vim = function(buf, range) return position
return { end
start = lsp.Position.to_vim(buf, range.start), return { line = position.line, character = byteindex }
['end'] = lsp.Position.to_vim(buf, range['end']), end,
}
end
---Convert vim.Range to lsp.Range ---Convert position to utf16 from specified encoding.
---@param buf number|string ---@param text string
---@param range vim.Range ---@param position lsp.Position
---@return lsp.Range ---@param from_encoding? lsp.PositionEncodingKind
lsp.Range.to_lsp = function(buf, range) ---@return lsp.Position
return { to_utf16 = function(text, position, from_encoding)
start = lsp.Position.to_lsp(buf, range.start), from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
['end'] = lsp.Position.to_lsp(buf, range['end']), if from_encoding == lsp.PositionEncodingKind.UTF16 then
} return position
end end
---@alias lsp.CompletionTriggerKind "1" | "2" | "3" local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
lsp.CompletionTriggerKind = {} for index = utf8.character, 0, -1 do
lsp.CompletionTriggerKind.Invoked = 1 local ok, utf16index = pcall(function()
lsp.CompletionTriggerKind.TriggerCharacter = 2 return select(2, vim.str_utfindex(text, index))
lsp.CompletionTriggerKind.TriggerForIncompleteCompletions = 3 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 ---@class lsp.CompletionContext
---@field public triggerKind lsp.CompletionTriggerKind ---@field public triggerKind lsp.CompletionTriggerKind
---@field public triggerCharacter string|nil ---@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 ---@class lsp.CompletionList
---@field public isIncomplete boolean ---@field public isIncomplete boolean
---@field public itemDefaults? lsp.internal.CompletionItemDefaults
---@field public items lsp.CompletionItem[] ---@field public items lsp.CompletionItem[]
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil ---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]
---@class lsp.MarkupContent ---@class lsp.MarkupContent
---@field public kind lsp.MarkupKind ---@field public kind lsp.MarkupKind
---@field public value string ---@field public value string
---@class lsp.Position ---@class lsp.Position
---@field public line number ---@field public line integer
---@field public character number ---@field public character integer
---@class lsp.Range ---@class lsp.Range
---@field public start lsp.Position ---@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 range lsp.Range|nil
---@field public newText string ---@field public newText string
---@class lsp.InsertReplaceTextEdit ---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit
---@field public insert lsp.Range|nil
---@field public replace lsp.Range|nil ---@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 ---@field public newText string
---@class lsp.CompletionItemLabelDetails ---@class lsp.CompletionItemLabelDetails
---@field public detail string|nil ---@field public detail? string
---@field public description string|nil ---@field public description? string
---@class lsp.internal.CmpCompletionExtension
---@field public kind_text string
---@field public kind_hl_group string
---@class lsp.CompletionItem ---@class lsp.CompletionItem
---@field public label string ---@field public label string
---@field public labelDetails lsp.CompletionItemLabelDetails|nil ---@field public labelDetails? lsp.CompletionItemLabelDetails
---@field public kind lsp.CompletionItemKind|nil ---@field public kind? lsp.CompletionItemKind
---@field public tags lsp.CompletionItemTag[]|nil ---@field public tags? lsp.CompletionItemTag[]
---@field public detail string|nil ---@field public detail? string
---@field public documentation lsp.MarkupContent|string|nil ---@field public documentation? lsp.MarkupContent|string
---@field public deprecated boolean|nil ---@field public deprecated? boolean
---@field public preselect boolean|nil ---@field public preselect? boolean
---@field public sortText string|nil ---@field public sortText? string
---@field public filterText string|nil ---@field public filterText? string
---@field public insertText string|nil ---@field public insertText? string
---@field public insertTextFormat lsp.InsertTextFormat ---@field public insertTextFormat? lsp.InsertTextFormat
---@field public insertTextMode lsp.InsertTextMode ---@field public insertTextMode? lsp.InsertTextMode
---@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil ---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit
---@field public additionalTextEdits lsp.TextEdit[] ---@field public textEditText? string
---@field public commitCharacters string[]|nil ---@field public additionalTextEdits? lsp.TextEdit[]
---@field public command lsp.Command|nil ---@field public commitCharacters? string[]
---@field public data any|nil ---@field public command? lsp.Command
---@field public data? any
---@field public cmp? lsp.internal.CmpCompletionExtension
--- ---
---TODO: Should send the issue for upstream? ---TODO: Should send the issue for upstream?
---@field public word string|nil ---@field public word string|nil

View File

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

View File

@ -44,9 +44,10 @@ api.get_current_line = function()
return vim.api.nvim_get_current_line() return vim.api.nvim_get_current_line()
end end
---@return { [1]: integer, [2]: integer }
api.get_cursor = function() api.get_cursor = function()
if api.is_cmdline_mode() then 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 end
return vim.api.nvim_win_get_cursor(0) return vim.api.nvim_win_get_cursor(0)
end end
@ -54,7 +55,7 @@ end
api.get_screen_cursor = function() api.get_screen_cursor = function()
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
local cursor = api.get_cursor() 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 end
local cursor = api.get_cursor() local cursor = api.get_cursor()
local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1) local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1)

View File

@ -4,8 +4,8 @@ local feedkeys = require('cmp.utils.feedkeys')
local api = require('cmp.utils.api') local api = require('cmp.utils.api')
describe('api', function() describe('api', function()
before_each(spec.before)
describe('get_cursor', function() describe('get_cursor', function()
before_each(spec.before)
it('insert-mode', function() it('insert-mode', function()
local cursor local cursor
feedkeys.call(keymap.t('i\t1234567890'), 'nx', function() feedkeys.call(keymap.t('i\t1234567890'), 'nx', function()
@ -24,8 +24,26 @@ describe('api', function()
end) end)
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() describe('get_cursor_before_line', function()
before_each(spec.before)
it('insert-mode', function() it('insert-mode', function()
local cursor_before_line local cursor_before_line
feedkeys.call(keymap.t('i\t1234567890<Left><Left>'), 'nx', function() 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 = {} local async = {}
---@class cmp.AsyncThrottle ---@class cmp.AsyncThrottle
---@field public running boolean ---@field public running boolean
---@field public timeout number ---@field public timeout integer
---@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil) ---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil)
---@field public stop function ---@field public stop function
---@field public __call 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 fn function
---@param timeout number ---@param timeout integer
---@return cmp.AsyncThrottle ---@return cmp.AsyncThrottle
async.throttle = function(fn, timeout) async.throttle = function(fn, timeout)
local time = nil 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({ return setmetatable({
running = false, running = false,
timeout = timeout, timeout = timeout,
@ -21,9 +40,15 @@ async.throttle = function(fn, timeout)
return not self.running return not self.running
end) end)
end, end,
stop = function() stop = function(reset_time)
time = nil if reset_time ~= false then
time = nil
end
timer:stop() timer:stop()
if _async then
_async:cancel()
_async = nil
end
end, end,
}, { }, {
__call = function(self, ...) __call = function(self, ...)
@ -34,12 +59,23 @@ async.throttle = function(fn, timeout)
end end
self.running = true self.running = true
timer:stop() self.stop(false)
timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function() timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
vim.schedule(function() vim.schedule(function()
time = nil time = nil
fn(unpack(args)) local ret = fn(unpack(args))
self.running = false 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) end)
end, end,
@ -60,7 +96,7 @@ end
---Timeout callback function ---Timeout callback function
---@param fn function ---@param fn function
---@param timeout number ---@param timeout integer
---@return function ---@return function
async.timeout = function(fn, timeout) async.timeout = function(fn, timeout)
local timer local timer
@ -109,4 +145,146 @@ async.sync = function(runner, timeout)
end, 10, false) end, 10, false)
end 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 return async

View File

@ -2,20 +2,38 @@ local debug = require('cmp.utils.debug')
local autocmd = {} local autocmd = {}
autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true })
autocmd.events = {} autocmd.events = {}
---Subscribe autocmd ---Subscribe autocmd
---@param event string ---@param events string|string[]
---@param callback function ---@param callback function
---@return function ---@return function
autocmd.subscribe = function(event, callback) autocmd.subscribe = function(events, callback)
autocmd.events[event] = autocmd.events[event] or {} events = type(events) == 'string' and { events } or events
table.insert(autocmd.events[event], callback)
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() return function()
for i, callback_ in ipairs(autocmd.events[event]) do for _, event in ipairs(events) do
if callback_ == callback then for i, callback_ in ipairs(autocmd.events[event]) do
table.remove(autocmd.events[event], i) if callback_ == callback then
break table.remove(autocmd.events[event], i)
break
end
end end
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

@ -7,22 +7,18 @@ feedkeys.call = setmetatable({
callbacks = {}, callbacks = {},
}, { }, {
__call = function(self, keys, mode, callback) __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_insert = string.match(mode, 'i') ~= nil
local is_immediate = string.match(mode, 'x') ~= nil local is_immediate = string.match(mode, 'x') ~= nil
local queue = {} local queue = {}
if #keys > 0 then if #keys > 0 then
table.insert(queue, { keymap.t('<Cmd>set lazyredraw<CR>'), 'n' }) table.insert(queue, { keymap.t('<Cmd>setlocal lazyredraw<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>set textwidth=0<CR>'), 'n' }) table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=0<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>set eventignore=all<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, { 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>setlocal %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>setlocal 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 backspace=%s<CR>'):format(vim.go.backspace or 2), 'n' })
end end
if callback then if callback then
@ -54,57 +50,4 @@ misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id)
return '' return ''
end) 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 return feedkeys

View File

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

View File

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

View File

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

View File

@ -29,6 +29,24 @@ misc.concat = function(list1, list2)
return new_list return new_list
end 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. ---Return the valu is empty or not.
---@param v any ---@param v any
---@return boolean ---@return boolean
@ -56,42 +74,38 @@ misc.none = vim.NIL
---Merge two tables recursively ---Merge two tables recursively
---@generic T ---@generic T
---@param v1 T ---@param tbl1 T
---@param v2 T ---@param tbl2 T
---@return T ---@return T
misc.merge = function(v1, v2) misc.merge = function(tbl1, tbl2)
local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1)) local is_dict1 = type(tbl1) == 'table' and (not vim.tbl_islist(tbl1) or vim.tbl_isempty(tbl1))
local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2)) local is_dict2 = type(tbl2) == 'table' and (not vim.tbl_islist(tbl2) or vim.tbl_isempty(tbl2))
if merge1 and merge2 then if is_dict1 and is_dict2 then
local new_tbl = {} local new_tbl = {}
for k, v in pairs(v2) do for k, v in pairs(tbl2) do
new_tbl[k] = misc.merge(v1[k], v) if tbl1[k] ~= misc.none then
new_tbl[k] = misc.merge(tbl1[k], v)
end
end end
for k, v in pairs(v1) do for k, v in pairs(tbl1) do
if v2[k] == nil and v ~= misc.none then if tbl2[k] == nil then
new_tbl[k] = v if v ~= misc.none then
new_tbl[k] = misc.merge(v, {})
else
new_tbl[k] = nil
end
end end
end end
return new_tbl return new_tbl
end 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 end
---Generate id for group name ---Generate id for group name
@ -105,22 +119,12 @@ misc.id = setmetatable({
end, 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 ---Treat 1/0 as bool value
---@param v boolean|"1"|"0" ---@param v boolean|1|0
---@param def boolean ---@param def boolean
---@return boolean ---@return boolean
misc.bool = function(v, def) misc.bool = function(v, def)
if misc.safe(v) == nil then if v == nil then
return def return def
end end
return v == true or v == 1 return v == true or v == 1
@ -134,7 +138,7 @@ misc.set = function(t, keys, v)
local c = t local c = t
for i = 1, #keys - 1 do for i = 1, #keys - 1 do
local key = keys[i] local key = keys[i]
c[key] = misc.safe(c[key]) or {} c[key] = c[key] or {}
c = c[key] c = c[key]
end end
c[keys[#keys]] = v c[keys[#keys]] = v
@ -166,8 +170,8 @@ end
---Safe version of vim.str_utfindex ---Safe version of vim.str_utfindex
---@param text string ---@param text string
---@param vimindex number|nil ---@param vimindex integer|nil
---@return number ---@return integer
misc.to_utfindex = function(text, vimindex) misc.to_utfindex = function(text, vimindex)
vimindex = vimindex or #text + 1 vimindex = vimindex or #text + 1
return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text))) return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text)))
@ -175,8 +179,8 @@ end
---Safe version of vim.str_byteindex ---Safe version of vim.str_byteindex
---@param text string ---@param text string
---@param utfindex number ---@param utfindex integer
---@return number ---@return integer
misc.to_vimindex = function(text, utfindex) misc.to_vimindex = function(text, utfindex)
utfindex = utfindex or #text utfindex = utfindex or #text
for i = utfindex, 1, -1 do for i = utfindex, 1, -1 do
@ -206,12 +210,14 @@ end
misc.redraw = setmetatable({ misc.redraw = setmetatable({
doing = false, doing = false,
force = 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) __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.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then
if vim.o.incsearch 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
end end

View File

@ -16,6 +16,18 @@ describe('misc', function()
}) })
assert.are.equal(merged.a.b, 1) 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({ merged = misc.merge({
a = false, 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 char = require('cmp.utils.char')
local pattern = require('cmp.utils.pattern')
local str = {} local str = {}
@ -73,23 +72,6 @@ str.remove_suffix = function(text, suffix)
return string.sub(text, 1, -#suffix - 1) return string.sub(text, 1, -#suffix - 1)
end 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 ---trim
---@param text string ---@param text string
---@return string ---@return string
@ -117,8 +99,8 @@ end
---get_word ---get_word
---@param text string ---@param text string
---@param stop_char number ---@param stop_char integer
---@param min_length number ---@param min_length integer
---@return string ---@return string
str.get_word = function(text, stop_char, min_length) str.get_word = function(text, stop_char, min_length)
min_length = min_length or 0 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') assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps')
end) end)
it('strikethrough', function()
assert.are.equal(str.strikethrough('あいうえお'), 'あ̶い̶う̶え̶お̶')
end)
it('remove_suffix', function() it('remove_suffix', function()
assert.are.equal(str.remove_suffix('log()', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()') assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()')

View File

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

View File

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

View File

@ -8,13 +8,11 @@ local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api') 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 local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
---@class cmp.CustomEntriesView ---@class cmp.CustomEntriesView
---@field private entries_win cmp.Window ---@field private entries_win cmp.Window
---@field private offset number ---@field private offset integer
---@field private active boolean ---@field private active boolean
---@field private entries cmp.Entry[] ---@field private entries cmp.Entry[]
---@field private column_width any ---@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() custom_entries_view.new = function()
local self = setmetatable({}, { __index = custom_entries_view }) local self = setmetatable({}, { __index = custom_entries_view })
self.entries_win = window.new() self.entries_win = window.new()
self.entries_win:option('conceallevel', 2) self.entries_win:option('conceallevel', 2)
self.entries_win:option('concealcursor', 'n') self.entries_win:option('concealcursor', 'n')
self.entries_win:option('cursorlineopt', 'line') self.entries_win:option('cursorlineopt', 'line')
self.entries_win:option('foldenable', false) self.entries_win:option('foldenable', false)
self.entries_win:option('wrap', 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 -- This is done so that strdisplaywidth calculations for lines in the
-- custom_entries_view window exactly match with what is really displayed, -- 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 -- 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 -- always rendered one column wide, which removes the unpredictability coming
-- from variable width of the tab character. -- from variable width of the tab character.
self.entries_win:buffer_option('tabstop', 1) 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.event = event.new()
self.offset = -1 self.offset = -1
self.active = false self.active = false
@ -65,7 +64,7 @@ custom_entries_view.new = function()
local e = self.entries[i + 1] local e = self.entries[i + 1]
if e then if e then
local v = e:get_view(self.offset, buf) local v = e:get_view(self.offset, buf)
local o = SIDE_PADDING local o = config.get().window.completion.side_padding
local a = 0 local a = 0
for _, field in ipairs(fields) do for _, field in ipairs(fields) do
if field == types.cmp.ItemField.Abbr then if field == types.cmp.ItemField.Abbr then
@ -118,17 +117,15 @@ custom_entries_view.is_direction_top_down = function(self)
end end
custom_entries_view.open = function(self, offset, entries) custom_entries_view.open = function(self, offset, entries)
local completion = config.get().window.completion
self.offset = offset self.offset = offset
self.entries = {} self.entries = {}
self.column_width = { abbr = 0, kind = 0, menu = 0 } 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 entries_buf = self.entries_win:get_buffer()
local lines = {} local lines = {}
local dedup = {} local dedup = {}
local preselect = 0 local preselect_index = 0
for _, e in ipairs(entries) do for _, e in ipairs(entries) do
local view = e:get_view(offset, entries_buf) local view = e:get_view(offset, entries_buf)
if view.dup == 1 or not dedup[e.completion_item.label] then 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) self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
table.insert(self.entries, e) table.insert(self.entries, e)
table.insert(lines, ' ') table.insert(lines, ' ')
if preselect == 0 and e.completion_item.preselect then if preselect_index == 0 and e.completion_item.preselect then
preselect = #self.entries preselect_index = #self.entries
end end
end end
end end
@ -157,18 +154,26 @@ custom_entries_view.open = function(self, offset, entries)
height = math.min(height, #self.entries) height = math.min(height, #self.entries)
local pos = api.get_screen_cursor() local pos = api.get_screen_cursor()
local cursor = api.get_cursor() local cursor_before_line = api.get_cursor_before_line()
local delta = cursor[2] + 1 - self.offset local delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset))
local has_bottom_space = (vim.o.lines - pos[1]) >= DEFAULT_HEIGHT
local row, col = pos[1], pos[2] - delta - 1 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) 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 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) 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 end
if pos[1] > row then 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 for i = 1, math.floor(n / 2) do
self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i] self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i]
end end
if preselect ~= 0 then if preselect_index ~= 0 then
preselect = #self.entries - preselect + 1 preselect_index = #self.entries - preselect_index + 1
end end
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({ self.entries_win:open({
relative = 'editor', relative = 'editor',
style = 'minimal', style = 'minimal',
row = math.max(0, row), row = math.max(0, row),
col = math.max(0, col), col = math.max(0, col + completion.col_offset),
width = width, width = width,
height = height, 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 -- 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 }) vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 })
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select }) self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false })
elseif not string.match(config.get().completion.completeopt, 'noselect') then elseif not string.match(config.get().completion.completeopt, 'noselect') then
if self:is_direction_top_down() 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 else
self:_select(#self.entries - 1, { behavior = types.cmp.SelectBehavior.Select }) self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false })
end end
else else
if self:is_direction_top_down() then 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 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 end
end end
@ -245,12 +255,12 @@ custom_entries_view.draw = function(self)
if e then if e then
local view = e:get_view(self.offset, entries_buf) local view = e:get_view(self.offset, entries_buf)
local text = {} 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 for _, field in ipairs(fields) do
table.insert(text, view[field].text) table.insert(text, view[field].text)
table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width)) table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width))
end 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, '')) table.insert(texts, table.concat(text, ''))
end end
end end
@ -275,34 +285,74 @@ end
custom_entries_view.select_next_item = function(self, option) custom_entries_view.select_next_item = function(self, option)
if self:visible() then if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
if self:is_direction_top_down() then local is_top_down = self:is_direction_top_down()
cursor = cursor + 1 local last = #self.entries
else
cursor = cursor - 1
end
if not self.entries_win:option('cursorline') then if not self.entries_win:option('cursorline') then
cursor = (self:is_direction_top_down() and 1) or #self.entries cursor = (is_top_down and 1) or last
elseif #self.entries < cursor then else
cursor = (not self:is_direction_top_down() and #self.entries + 1) or 0 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 end
self:_select(cursor, option)
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end end
end end
custom_entries_view.select_prev_item = function(self, option) custom_entries_view.select_prev_item = function(self, option)
if self:visible() then if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
if self:is_direction_top_down() then local is_top_down = self:is_direction_top_down()
cursor = cursor - 1 local last = #self.entries
else
cursor = cursor + 1
end
if not self.entries_win:option('cursorline') then if not self.entries_win:option('cursorline') then
cursor = (self:is_direction_top_down() and #self.entries) or 1 cursor = (is_top_down and last) or 1
elseif #self.entries < cursor then else
cursor = (not self:is_direction_top_down() and 0) or #self.entries + 1 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 end
self:_select(cursor, option)
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end end
end end
@ -343,10 +393,9 @@ custom_entries_view._select = function(self, cursor, option)
if is_insert and not self.active then if is_insert and not self.active then
self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or '' self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or ''
end 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) self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries)
vim.api.nvim_win_set_cursor(self.entries_win.win, { vim.api.nvim_win_set_cursor(self.entries_win.win, {
math.max(math.min(cursor, #self.entries), 1), math.max(math.min(cursor, #self.entries), 1),
0, 0,
@ -368,7 +417,17 @@ custom_entries_view._insert = setmetatable({
word = word or '' word = word or ''
if api.is_cmdline_mode() then if api.is_cmdline_mode() then
local cursor = api.get_cursor() 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 else
if this.pending then if this.pending then
return return

View File

@ -15,7 +15,10 @@ docs_view.new = function()
self.window:option('foldenable', false) self.window:option('foldenable', false)
self.window:option('linebreak', true) self.window:option('linebreak', true)
self.window:option('scrolloff', 0) self.window:option('scrolloff', 0)
self.window:option('showbreak', 'NONE')
self.window:option('wrap', true) self.window:option('wrap', true)
self.window:buffer_option('filetype', 'cmp_docs')
self.window:buffer_option('buftype', 'nofile')
return self return self
end end
@ -23,7 +26,7 @@ end
---@param e cmp.Entry ---@param e cmp.Entry
---@param view cmp.WindowStyle ---@param view cmp.WindowStyle
docs_view.open = function(self, e, view) docs_view.open = function(self, e, view)
local documentation = config.get().documentation local documentation = config.get().window.documentation
if not documentation then if not documentation then
return return
end end
@ -32,11 +35,12 @@ docs_view.open = function(self, e, view)
return self:close() return self:close()
end end
local right_space = vim.o.columns - (view.col + view.width) - 2 local border_info = window.get_border_info({ style = documentation })
local left_space = view.col - 2 local right_space = vim.o.columns - (view.col + view.width) - 1
local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space) - 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 if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation() local documents = e:get_documentation()
if #documents == 0 then if #documents == 0 then
@ -46,24 +50,29 @@ docs_view.open = function(self, e, view)
self.entry = e self.entry = e
vim.api.nvim_buf_call(self.window:get_buffer(), function() vim.api.nvim_buf_call(self.window:get_buffer(), function()
vim.cmd([[syntax clear]]) vim.cmd([[syntax clear]])
vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {})
end) end)
vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, { vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, {
max_width = maxwidth, max_width = max_width - border_info.horiz,
max_height = documentation.maxheight, max_height = documentation.max_height,
}) })
end 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), { 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_width = max_width - border_info.horiz,
max_height = documentation.maxheight, max_height = documentation.max_height - border_info.vert,
}) })
if width <= 0 or height <= 0 then if width <= 0 or height <= 0 then
return self:close() return self:close()
end end
-- Calculate window position.
local right_col = view.col + view.width 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 local col, left
if right_space >= width and left_space >= width then if right_space >= width and left_space >= width then
if right_space < left_space then if right_space < left_space then
@ -81,8 +90,10 @@ docs_view.open = function(self, e, view)
return self:close() return self:close()
end end
-- Render window.
self.window:option('winblend', vim.o.pumblend)
self.window:option('winhighlight', documentation.winhighlight) self.window:option('winhighlight', documentation.winhighlight)
self.window:set_style({ local style = {
relative = 'editor', relative = 'editor',
style = 'minimal', style = 'minimal',
width = width, width = width,
@ -91,11 +102,14 @@ docs_view.open = function(self, e, view)
col = col, col = col,
border = documentation.border, border = documentation.border,
zindex = documentation.zindex or 50, zindex = documentation.zindex or 50,
}) }
if left and self.window:has_scrollbar() then self.window:open(style)
self.window.style.col = self.window.style.col - 1
-- Correct left-col for scrollbar existence.
if left then
style.col = style.col - self.window:info().scrollbar_offset
self.window:open(style)
end end
self.window:open()
end end
---Close floating window ---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') 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() ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view }) local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil self.win = nil
@ -17,7 +29,7 @@ ghost_text_view.new = function()
on_win = function(_, win) on_win = function(_, win)
return win == self.win return win == self.win
end, end,
on_line = function(_) on_line = function(_, _, _, on_row)
local c = config.get().experimental.ghost_text local c = config.get().experimental.ghost_text
if not c then if not c then
return return
@ -28,17 +40,23 @@ ghost_text_view.new = function()
end end
local row, col = unpack(vim.api.nvim_win_get_cursor(0)) local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line() if on_row ~= row - 1 then
if string.sub(line, col + 1) ~= '' then
return return
end 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) local text = self.text_gen(self, line, col)
if #text > 0 then if #text > 0 then
vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, row - 1, col, { vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, row - 1, col, {
right_gravity = false, right_gravity = false,
virt_text = { { text, c.hl_group or 'Comment' } }, virt_text = { { text, type(c) == 'table' and c.hl_group or 'Comment' } },
virt_text_pos = 'overlay', virt_text_pos = has_inline and 'inline' or 'overlay',
hl_mode = 'combine', hl_mode = 'combine',
ephemeral = true, ephemeral = true,
}) })
@ -78,6 +96,10 @@ ghost_text_view.show = function(self, e)
if not api.is_insert_mode() then if not api.is_insert_mode() then
return return
end end
local c = config.get().experimental.ghost_text
if not c then
return
end
local changed = e ~= self.entry local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win() self.win = vim.api.nvim_get_current_win()
self.entry = e self.entry = e

View File

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

View File

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

View File

@ -2,7 +2,7 @@ local misc = require('cmp.utils.misc')
local vim_source = {} local vim_source = {}
---@param id number ---@param id integer
---@param args any[] ---@param args any[]
vim_source.on_callback = function(id, args) vim_source.on_callback = function(id, args)
if vim_source.to_callback.callbacks[id] then if vim_source.to_callback.callbacks[id] then
@ -11,7 +11,7 @@ vim_source.on_callback = function(id, args)
end end
---@param callback function ---@param callback function
---@return number ---@return integer
vim_source.to_callback = setmetatable({ vim_source.to_callback = setmetatable({
callbacks = {}, callbacks = {},
}, { }, {
@ -36,7 +36,7 @@ vim_source.to_args = function(args)
return args return args
end end
---@param bridge_id number ---@param bridge_id integer
---@param methods string[] ---@param methods string[]
vim_source.new = function(bridge_id, methods) vim_source.new = function(bridge_id, methods)
local self = {} 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 end
vim.g.loaded_cmp = true vim.g.loaded_cmp = true
local api = require "cmp.utils.api" if not vim.api.nvim_create_autocmd then
local misc = require('cmp.utils.misc') 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 types = require('cmp.types')
local config = require('cmp.config')
local highlight = require('cmp.utils.highlight') local highlight = require('cmp.utils.highlight')
local autocmd = require('cmp.utils.autocmd')
-- TODO: https://github.com/neovim/neovim/pull/14661 vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true })
vim.cmd [[ vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true })
augroup ___cmp___ vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true })
autocmd! vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true })
autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter') vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true })
autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave') vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true })
autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged') for kind in pairs(types.lsp.CompletionItemKind) do
autocmd CursorMovedI * lua require'cmp.utils.autocmd'.emit('CursorMoved') if type(kind) == 'string' then
autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged') local name = ('CmpItemKind%s'):format(kind)
autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true })
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
end end
if vim.fn.expand('<afile>')~= '=' then end
vim.schedule(function()
if api.is_cmdline_mode() then
vim.cmd [[
augroup cmp-cmdline
autocmd!
autocmd CmdlineChanged * lua require'cmp.utils.autocmd'.emit('TextChanged')
autocmd CmdlineLeave * call v:lua.cmp.plugin.cmdline.leave()
augroup END
]]
require('cmp.utils.autocmd').emit('CmdlineEnter')
end
end)
end
end)
misc.set(_G, { 'cmp', 'plugin', 'cmdline', 'leave' }, function() autocmd.subscribe('ColorScheme', function()
if vim.fn.expand('<afile>') ~= '=' then highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false })
vim.cmd [[ highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false })
augroup cmp-cmdline highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false })
autocmd! highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false })
augroup END highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false })
]] highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false })
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',
})
for name in pairs(types.lsp.CompletionItemKind) do for name in pairs(types.lsp.CompletionItemKind) do
if type(name) == 'string' then 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
end end
highlight.inherit('CmpItemMenuDefault', 'Pmenu', {
guibg = 'NONE',
ctermbg = 'NONE',
})
end) end)
_G.cmp.plugin.colorscheme() autocmd.emit('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]]
if vim.on_key then if vim.on_key then
vim.on_key(function(keys) vim.on_key(function(keys)
if keys == vim.api.nvim_replace_termcodes('<C-c>', true, true, true) then if keys == vim.api.nvim_replace_termcodes('<C-c>', true, true, true) then
vim.schedule(function() vim.schedule(function()
if not api.is_suitable_mode() then if not api.is_suitable_mode() then
require('cmp.utils.autocmd').emit('InsertLeave') autocmd.emit('InsertLeave')
end end
end) end)
end end
end, vim.api.nvim_create_namespace('cmp.plugin')) end, vim.api.nvim_create_namespace('cmp.plugin'))
end 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 }) ['<CR>'] = cmp.mapping.confirm({ select = true })
}, },
sources = { sources = cmp.config.sources({
{ name = "nvim_lsp" }, { name = "nvim_lsp" },
{ name = "buffer" }, { name = "buffer" },
}, }),
} }
EOF EOF
lua << 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 { require'lspconfig'.cssls.setup {
capabilities = capabilities, capabilities = capabilities,